diff --git a/doc/connections/AI-CONNECTIONS.md b/doc/connections/AI-CONNECTIONS.md
index d31ce5fea97..c0ca25fa2c6 100644
--- a/doc/connections/AI-CONNECTIONS.md
+++ b/doc/connections/AI-CONNECTIONS.md
@@ -186,6 +186,17 @@ and contain neither credentials nor raw provider errors. Expired credentials
report `authentication_required`; the probe does not exchange refresh tokens
or change connection health.
+The Costs dashboard uses the company quota endpoint. It reads each authorized
+managed subscription separately, retaining the last successful observation on
+transient errors. For managed Codex accounts, a 401 can trigger one persisted
+OAuth refresh; it defers while potentially competing OpenAI work is active.
+Refresh takes the company secret-mutation lock before database row locks and
+requires those row locks immediately, before exchanging a single-use token.
+Reconnect and runtime credential write-back follow the same lock order.
+Credential resolution checks its version after any database wait and reloads a
+rotated value before giving it to a new run. Unknown utilization has no progress
+bar, but any provider-reported reset time remains visible.
+
Verification:
```sh
diff --git a/packages/adapters/codex-local/src/server/codex-auth-cache.test.ts b/packages/adapters/codex-local/src/server/codex-auth-cache.test.ts
index 0ca1bee28ca..2cd6ba7c173 100644
--- a/packages/adapters/codex-local/src/server/codex-auth-cache.test.ts
+++ b/packages/adapters/codex-local/src/server/codex-auth-cache.test.ts
@@ -463,13 +463,15 @@ describe("codex auth cache store", () => {
releaseFirstCaller = resolve;
});
+ const entered = deferredSignal();
const firstCall = withAccountHomeSecretMutationLock(env, "company-shared", async () => {
events.push("first-enter");
+ entered.resolve();
await firstCallerGate;
events.push("first-exit");
return "first";
});
- await new Promise((resolve) => setTimeout(resolve, 20));
+ await entered.promise;
const secondCall = withAccountHomeSecretMutationLock(env, "company-shared", async () => {
events.push("second-enter");
events.push("second-exit");
@@ -497,12 +499,14 @@ describe("codex auth cache store", () => {
releaseCompanyA = resolve;
});
+ const entered = deferredSignal();
const companyACall = withAccountHomeSecretMutationLock(env, "company-a", async () => {
events.push("a-enter");
+ entered.resolve();
await companyAGate;
events.push("a-exit");
});
- await new Promise((resolve) => setTimeout(resolve, 20));
+ await entered.promise;
const companyBCall = withAccountHomeSecretMutationLock(env, "company-b", async () => {
events.push("b-enter");
events.push("b-exit");
@@ -536,6 +540,28 @@ describe("codex auth cache store", () => {
expect(events).toEqual(["promotion-enter", "mutation-enter", "mutation-exit", "promotion-exit"]);
});
+ it("allows awaited nested writes but expires inherited ownership after release", async () => {
+ const env = envFor(await makeInstanceRoot());
+ const detachedGate = deferredSignal();
+ let detached!: Promise;
+ let enteredDetached = false;
+ await withAccountHomeSecretMutationLock(env, "nested", async () => {
+ await expect(withAccountHomeSecretMutationLock(env, "nested", async () => "nested write")).resolves.toBe("nested write");
+ detached = detachedGate.promise.then(() => withAccountHomeSecretMutationLock(env, "nested", async () => { enteredDetached = true; }));
+ });
+ const holding = deferredSignal();
+ const release = deferredSignal();
+ const other = withAccountHomeSecretMutationLock(env, "nested", async () => { holding.resolve(); await release.promise; });
+ await holding.promise;
+ detachedGate.resolve();
+ try {
+ await new Promise(resolve => setTimeout(resolve, 30));
+ expect(enteredDetached).toBe(false);
+ } finally { release.resolve(); }
+ await Promise.all([other, detached]);
+ expect(enteredDetached).toBe(true);
+ });
+
});
describe("Phase 8: account-home directory validity before a secret write commits", () => {
@@ -581,3 +607,9 @@ describe("codex auth cache store", () => {
});
});
});
+
+function deferredSignal() {
+ let resolve!: () => void;
+ const promise = new Promise(done => { resolve = done; });
+ return { promise, resolve };
+}
diff --git a/packages/adapters/codex-local/src/server/codex-auth-cache.ts b/packages/adapters/codex-local/src/server/codex-auth-cache.ts
index c51921fff6b..d6dd757313a 100644
--- a/packages/adapters/codex-local/src/server/codex-auth-cache.ts
+++ b/packages/adapters/codex-local/src/server/codex-auth-cache.ts
@@ -1,4 +1,5 @@
-import { lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises";
+import { AsyncLocalStorage } from "node:async_hooks";
+import { lstat, mkdir, open, readFile, realpath, rename, rm } from "node:fs/promises";
import path from "node:path";
import { randomUUID } from "node:crypto";
import { resolvePaperclipInstanceRootForAdapter } from "@paperclipai/adapter-utils/server-utils";
@@ -334,6 +335,11 @@ export async function withCodexAccountHomePromotionLock(
* create) can still call into a secrets-service write that takes this lock
* without deadlocking on itself.
*/
+// An outer credential transaction acquires this lock before database row locks.
+// Nested secret-service writes must share that ownership, not acquire it again.
+// Expire ownership on return so detached work cannot inherit a released lock.
+const secretMutationOwnership = new AsyncLocalStorage
)}
{account.windows.map((window, index) => window.usedPercent == null ? (
-
-
{window.label}
-
{window.valueLabel ?? "Usage not reported"}
+
+
+ {window.label}
+ {window.valueLabel ?? "Usage not reported"}
+
+ {window.resetsAt &&
Resets {new Date(window.resetsAt).toLocaleString()}
}
) : (