Skip to content

Commit 8c88321

Browse files
authored
Merge pull request #750 from luvs01/agent/724-refresh-pool-plan
fix(codex): persist refreshed pool account plans
2 parents 18545f8 + 0a05150 commit 8c88321

11 files changed

Lines changed: 1592 additions & 249 deletions

src/codex/account-store.ts

Lines changed: 84 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import { createHash } from "node:crypto";
22
import { closeSync, existsSync, readFileSync, mkdirSync, openSync, unlinkSync, writeFileSync } from "node:fs";
33
import { join } from "node:path";
4-
import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret } from "../config";
4+
import {
5+
ConfigMutationLockError,
6+
getConfigDir,
7+
atomicWriteFile,
8+
backupInvalidConfig,
9+
hardenConfigDir,
10+
hardenExistingSecret,
11+
withConfigMutationLockSync,
12+
} from "../config";
513
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
614
import type { CodexAccountCredentialRecord, CodexAccountCredentials } from "../types";
715

@@ -121,44 +129,50 @@ export function getCodexAccountCredential(id: string): CodexAccountCredentials |
121129
}
122130

123131
export function saveCodexAccountCredential(id: string, cred: CodexAccountCredentials): void {
124-
const store = loadCodexAccountRecordStore();
125-
const current = store[id];
126-
const refreshGrantFingerprint = current?.credential?.refreshToken === cred.refreshToken
127-
? current.refreshGrantFingerprint ?? refreshGrantFingerprintForToken(cred.refreshToken)
128-
: refreshGrantFingerprintForToken(cred.refreshToken);
129-
store[id] = {
130-
credential: cred,
131-
generation: (current?.generation ?? 0) + 1,
132-
refreshGrantFingerprint,
133-
replacedAt: current ? Date.now() : undefined,
134-
...preservedValidationMetadata(current),
135-
};
136-
persist(store);
132+
withCredentialMutationLockSync(() => {
133+
const store = loadCodexAccountRecordStore();
134+
const current = store[id];
135+
const refreshGrantFingerprint = current?.credential?.refreshToken === cred.refreshToken
136+
? current.refreshGrantFingerprint ?? refreshGrantFingerprintForToken(cred.refreshToken)
137+
: refreshGrantFingerprintForToken(cred.refreshToken);
138+
store[id] = {
139+
credential: cred,
140+
generation: (current?.generation ?? 0) + 1,
141+
refreshGrantFingerprint,
142+
replacedAt: current ? Date.now() : undefined,
143+
...preservedValidationMetadata(current),
144+
};
145+
persist(store);
146+
});
137147
}
138148

139149
export function markCodexAccountValidated(id: string, atMs: number = Date.now()): void {
140-
const store = loadCodexAccountRecordStore();
141-
const current = store[id];
142-
if (!current || current.deletedAt != null || !current.credential) return;
143-
store[id] = {
144-
...current,
145-
lastCodexValidatedAt: atMs,
146-
lastCodexValidationStatus: "ok",
147-
lastCodexValidationError: undefined,
148-
};
149-
persist(store);
150+
withCredentialMutationLockSync(() => {
151+
const store = loadCodexAccountRecordStore();
152+
const current = store[id];
153+
if (!current || current.deletedAt != null || !current.credential) return;
154+
store[id] = {
155+
...current,
156+
lastCodexValidatedAt: atMs,
157+
lastCodexValidationStatus: "ok",
158+
lastCodexValidationError: undefined,
159+
};
160+
persist(store);
161+
});
150162
}
151163

152164
export function markCodexAccountValidationFailed(id: string, reason: string): void {
153-
const store = loadCodexAccountRecordStore();
154-
const current = store[id];
155-
if (!current || current.deletedAt != null || !current.credential) return;
156-
store[id] = {
157-
...current,
158-
lastCodexValidationStatus: "failed",
159-
lastCodexValidationError: reason,
160-
};
161-
persist(store);
165+
withCredentialMutationLockSync(() => {
166+
const store = loadCodexAccountRecordStore();
167+
const current = store[id];
168+
if (!current || current.deletedAt != null || !current.credential) return;
169+
store[id] = {
170+
...current,
171+
lastCodexValidationStatus: "failed",
172+
lastCodexValidationError: reason,
173+
};
174+
persist(store);
175+
});
162176
}
163177

164178
export function removeCodexAccountCredential(id: string): void {
@@ -183,32 +197,36 @@ export function saveCodexAccountCredentialIfGeneration(
183197
generation: number,
184198
cred: CodexAccountCredentials,
185199
): boolean {
186-
const store = loadCodexAccountRecordStore();
187-
const current = store[id];
188-
if (!current || current.generation !== generation || current.deletedAt != null || !current.credential) {
189-
return false;
190-
}
191-
const refreshGrantFingerprint = current.credential.refreshToken === cred.refreshToken
192-
? current.refreshGrantFingerprint ?? refreshGrantFingerprintForToken(cred.refreshToken)
193-
: refreshGrantFingerprintForToken(cred.refreshToken);
194-
store[id] = {
195-
credential: cred,
196-
generation: generation + 1,
197-
refreshGrantFingerprint,
198-
replacedAt: current.replacedAt,
199-
...preservedValidationMetadata(current),
200-
};
201-
persist(store);
202-
return true;
200+
return withCredentialMutationLockSync(() => {
201+
const store = loadCodexAccountRecordStore();
202+
const current = store[id];
203+
if (!current || current.generation !== generation || current.deletedAt != null || !current.credential) {
204+
return false;
205+
}
206+
const refreshGrantFingerprint = current.credential.refreshToken === cred.refreshToken
207+
? current.refreshGrantFingerprint ?? refreshGrantFingerprintForToken(cred.refreshToken)
208+
: refreshGrantFingerprintForToken(cred.refreshToken);
209+
store[id] = {
210+
credential: cred,
211+
generation: generation + 1,
212+
refreshGrantFingerprint,
213+
replacedAt: current.replacedAt,
214+
...preservedValidationMetadata(current),
215+
};
216+
persist(store);
217+
return true;
218+
});
203219
}
204220

205221
export function tombstoneCodexAccount(id: string): number {
206-
const store = loadCodexAccountRecordStore();
207-
const current = store[id];
208-
const generation = (current?.generation ?? 0) + 1;
209-
store[id] = { generation, deletedAt: Date.now() };
210-
persist(store);
211-
return generation;
222+
return withCredentialMutationLockSync(() => {
223+
const store = loadCodexAccountRecordStore();
224+
const current = store[id];
225+
const generation = (current?.generation ?? 0) + 1;
226+
store[id] = { generation, deletedAt: Date.now() };
227+
persist(store);
228+
return generation;
229+
});
212230
}
213231

214232
const CHATGPT_TOKEN_URL = "https://auth.openai.com/oauth/token";
@@ -237,6 +255,16 @@ export class CodexCredentialRefreshLockTimeoutError extends Error {
237255
}
238256
}
239257

258+
/** Credential writers share the config mutation coordinator; contention is transient, not reauth. */
259+
function withCredentialMutationLockSync<T>(fn: () => T): T {
260+
try {
261+
return withConfigMutationLockSync(fn);
262+
} catch (error) {
263+
if (error instanceof ConfigMutationLockError) throw new CodexCredentialRefreshLockTimeoutError();
264+
throw error;
265+
}
266+
}
267+
240268
type CodexTokenResult = { accessToken: string; chatgptAccountId: string; generation: number };
241269
type CodexRefreshResult = CodexTokenResult & { credential?: CodexAccountCredentials };
242270
const refreshLocks = new Map<string, Promise<CodexRefreshResult>>();

0 commit comments

Comments
 (0)