From 1616059a03171b897f74a831cca4e03d1d160d76 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Tue, 28 Apr 2026 17:57:27 +0200 Subject: [PATCH] Refresh relogin snapshots without slowing account switches Direct account switching should remain a fast local file swap, while official codex login flows still need to persist refreshed auth bytes back to the matching saved snapshot. The switch command now skips the external-sync preflight and the service updates the active registry pointer without a full reconcile. Relogin sync now prefers the active or identity-matching saved snapshot, including aliases, so refreshed tokens replace the right account instead of creating avoidable duplicates or returning early. Constraint: Official codex login is the primary relogin path and the shell hook forces external sync after codex exits. Rejected: Always infer a canonical email snapshot for relogin | breaks alias-based account names and creates duplicate snapshots. Confidence: high Scope-risk: moderate Directive: Do not broaden same-email forced overwrite beyond the active snapshot without preserving the duplicate-safety tests. Tested: npm test --silent Tested: openspec validate agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46 --strict --- .../proposal.md | 17 ++ .../specs/snapshot-sync/spec.md | 33 +++ .../tasks.md | 27 +++ src/commands/login.ts | 2 + src/commands/save.ts | 2 + src/commands/use.ts | 2 + src/lib/accounts/account-service.ts | 215 ++++++++++-------- src/tests/save-account-safety.test.ts | 172 +++++++++++++- 8 files changed, 369 insertions(+), 101 deletions(-) create mode 100644 openspec/changes/agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46/proposal.md create mode 100644 openspec/changes/agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46/specs/snapshot-sync/spec.md create mode 100644 openspec/changes/agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46/tasks.md diff --git a/openspec/changes/agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46/proposal.md b/openspec/changes/agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46/proposal.md new file mode 100644 index 00000000..ffddd7b6 --- /dev/null +++ b/openspec/changes/agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46/proposal.md @@ -0,0 +1,17 @@ +# Speed up account switching and relogin snapshot sync + +## Why + +`codex-auth use ` should switch accounts immediately, without spending the command budget on external-login reconciliation that is handled by the login hook. When a user relogs in through the official `codex login` flow, the saved snapshot for that Codex account should be refreshed with the new auth bytes instead of returning early or creating an avoidable duplicate. + +## What changes + +- Make direct account switching use the fast path. +- Preserve and refresh matching saved snapshot names, including aliases, during official relogin sync. +- Update command copy for reused saved account names. +- Add regression coverage for same-identity token refresh, alias preservation, and session fingerprint updates. + +## Verification + +- `npm test --silent` +- `openspec validate agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46 --strict` diff --git a/openspec/changes/agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46/specs/snapshot-sync/spec.md b/openspec/changes/agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46/specs/snapshot-sync/spec.md new file mode 100644 index 00000000..acfb8f98 --- /dev/null +++ b/openspec/changes/agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46/specs/snapshot-sync/spec.md @@ -0,0 +1,33 @@ +# snapshot-sync Spec Delta + +## ADDED Requirements + +### Requirement: Fast direct account switch + +`codex-auth use ` SHALL switch `~/.codex/auth.json` to the selected saved snapshot without running the external-login sync preflight. + +#### Scenario: Switching a saved account + +- **WHEN** the user runs `codex-auth use team-primary` +- **THEN** the command copies `accounts/team-primary.json` into `auth.json` +- **AND** updates the active account pointers for the current terminal session +- **AND** does not run the external-login sync preflight before the copy + +### Requirement: Relogin refreshes matching saved snapshot + +External Codex login sync SHALL refresh the saved snapshot that matches the newly written `auth.json` by identity or email, preferring the active session snapshot and preserving alias names. + +#### Scenario: Same account relogin refreshes tokens + +- **GIVEN** `team-primary` is the active saved snapshot +- **AND** `auth.json` is rewritten by `codex login` for the same Codex identity with new tokens +- **WHEN** external sync runs +- **THEN** `accounts/team-primary.json` is overwritten with the new auth bytes +- **AND** the active account remains `team-primary` + +#### Scenario: New account relogin is added + +- **GIVEN** no saved snapshot matches the newly written `auth.json` +- **WHEN** external sync runs +- **THEN** a new snapshot name is inferred from the Codex auth email +- **AND** that snapshot becomes the active account diff --git a/openspec/changes/agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46/tasks.md b/openspec/changes/agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46/tasks.md new file mode 100644 index 00000000..74b22cdc --- /dev/null +++ b/openspec/changes/agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46/tasks.md @@ -0,0 +1,27 @@ +# Tasks + +## 1. Spec + +- [x] Define account-switch fast path and relogin snapshot refresh behavior. + +## 2. Tests + +- [x] Add regression tests for same-account relogin token refresh. +- [x] Add regression tests for alias-preserving relogin sync. +- [x] Add regression tests for session fingerprint refresh after `useAccount`. + +## 3. Implementation + +- [x] Make `codex-auth use` skip pre-run external sync. +- [x] Update `useAccount` to avoid full registry reconciliation for the switch hot path. +- [x] Make external relogin sync refresh matching saved snapshots, including aliases. + +## 4. Verification + +- [x] Run `npm test --silent`. +- [x] Run `openspec validate agent-codex-speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46 --strict`. + +## 5. Cleanup + +- [ ] Commit, push, create/update PR, wait for `MERGED`, and prune sandbox with `gx branch finish --branch agent/codex/speed-up-codex-account-switch-relogin-sn-2026-04-28-17-46 --base main --via-pr --wait-for-merge --cleanup`. +- [ ] Record PR URL and final `MERGED` evidence. diff --git a/src/commands/login.ts b/src/commands/login.ts index 27030d17..a65fcaac 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -59,6 +59,8 @@ export default class LoginCommand extends BaseCommand { ? "" : resolvedName.source === "active" ? " (reused active account name)" + : resolvedName.source === "existing" + ? " (reused saved account name)" : " (inferred from auth email)"; this.log(`Saved current Codex auth tokens as "${savedName}"${suffix}.`); }); diff --git a/src/commands/save.ts b/src/commands/save.ts index 4261a20e..507471a5 100644 --- a/src/commands/save.ts +++ b/src/commands/save.ts @@ -37,6 +37,8 @@ export default class SaveCommand extends BaseCommand { ? "" : resolvedName.source === "active" ? " (reused active account name)" + : resolvedName.source === "existing" + ? " (reused saved account name)" : " (inferred from auth email)"; this.log(`Saved current Codex auth tokens as "${savedName}"${suffix}.`); }); diff --git a/src/commands/use.ts b/src/commands/use.ts index b0a29e57..e0568a4e 100644 --- a/src/commands/use.ts +++ b/src/commands/use.ts @@ -4,6 +4,8 @@ import { BaseCommand } from "../lib/base-command"; import { NoAccountsSavedError, PromptCancelledError } from "../lib/accounts"; export default class UseCommand extends BaseCommand { + protected readonly syncExternalAuthBeforeRun = false; + static description = "Switch ~/.codex/auth.json to the selected account"; static args = { diff --git a/src/lib/accounts/account-service.ts b/src/lib/accounts/account-service.ts index 98c8a090..49867c77 100644 --- a/src/lib/accounts/account-service.ts +++ b/src/lib/accounts/account-service.ts @@ -84,15 +84,17 @@ export interface SaveAccountOptions { force?: boolean; } +type ResolvedAccountNameSource = "active" | "existing" | "inferred"; + export interface ResolvedDefaultAccountName { name: string; - source: "active" | "inferred"; + source: ResolvedAccountNameSource; forceOverwrite?: boolean; } export interface ResolvedLoginAccountName { name: string; - source: "active" | "inferred"; + source: ResolvedAccountNameSource; forceOverwrite?: boolean; } @@ -158,33 +160,23 @@ export class AccountService { } } - const resolvedName = await this.resolveLoginAccountNameFromCurrentAuth(); const activeName = await this.getCurrentAccountName(); - if (activeName) { - const activeSnapshotPath = this.accountFilePath(activeName); - if (await this.pathExists(activeSnapshotPath)) { - const activeSnapshot = await parseAuthSnapshotFile(activeSnapshotPath); - if (this.snapshotsShareIdentity(activeSnapshot, incomingSnapshot)) { - if (activeName === resolvedName.name) { - return rememberAuthState({ - synchronized: false, - autoSwitchDisabled: false, - }); - } - - const authMatchesActiveSnapshot = await this.filesMatch(authPath, activeSnapshotPath); - if (authMatchesActiveSnapshot) { - return rememberAuthState({ - synchronized: false, - autoSwitchDisabled: false, - }); - } - } - } + const resolvedName = await this.resolveLoginAccountNameForSnapshot(incomingSnapshot, activeName); + const resolvedSnapshotPath = this.accountFilePath(resolvedName.name); + if ( + activeName === resolvedName.name && + (await this.pathExists(resolvedSnapshotPath)) && + (await this.filesMatch(authPath, resolvedSnapshotPath)) + ) { + return rememberAuthState({ + synchronized: false, + autoSwitchDisabled: false, + }); } const status = await this.getStatus(); - const autoSwitchDisabled = status.autoSwitchEnabled; + const sameActiveAccountRefresh = activeName === resolvedName.name && resolvedName.source === "active"; + const autoSwitchDisabled = status.autoSwitchEnabled && !sameActiveAccountRefresh; if (autoSwitchDisabled) { await this.setAutoSwitchEnabled(false); } @@ -416,32 +408,12 @@ export class AccountService { const authPath = resolveAuthPath(); await this.ensureAuthFileExists(authPath); const incomingSnapshot = await parseAuthSnapshotFile(authPath); - const activeName = await this.getCurrentAccountName(); - if (activeName) { - const activeSnapshotPath = this.accountFilePath(activeName); - if (await this.pathExists(activeSnapshotPath)) { - const activeSnapshot = await parseAuthSnapshotFile(activeSnapshotPath); - - if (this.snapshotsShareIdentity(activeSnapshot, incomingSnapshot)) { - return { - name: activeName, - source: "active", - }; - } - - if (this.canRefreshActiveCanonicalEmailSnapshot(activeName, activeSnapshot, incomingSnapshot)) { - return { - name: activeName, - source: "active", - forceOverwrite: true, - }; - } - } - } + const existing = await this.resolveExistingAccountNameForIncomingSnapshot(incomingSnapshot, activeName); + if (existing) return existing; return { - name: await this.inferAccountNameFromCurrentAuth(), + name: await this.inferAccountNameFromSnapshot(incomingSnapshot), source: "inferred", }; } @@ -451,41 +423,17 @@ export class AccountService { await this.ensureAuthFileExists(authPath); const incomingSnapshot = await parseAuthSnapshotFile(authPath); const activeName = await this.getCurrentAccountName(); - - if (activeName) { - const activeSnapshotPath = this.accountFilePath(activeName); - if (await this.pathExists(activeSnapshotPath)) { - const activeSnapshot = await parseAuthSnapshotFile(activeSnapshotPath); - - if (this.canRefreshActiveCanonicalEmailSnapshot(activeName, activeSnapshot, incomingSnapshot)) { - return this.snapshotsShareIdentity(activeSnapshot, incomingSnapshot) - ? { - name: activeName, - source: "active", - } - : { - name: activeName, - source: "active", - forceOverwrite: true, - }; - } - } - } - - return { - name: await this.inferAccountNameFromCurrentAuth(), - source: "inferred", - }; + return this.resolveLoginAccountNameForSnapshot(incomingSnapshot, activeName); } public async useAccount(rawName: string): Promise { const name = this.normalizeAccountName(rawName); await this.activateSnapshot(name); - const registry = await this.loadReconciledRegistry(); - await this.hydrateSnapshotMetadata(registry, name); + const registry = await loadRegistry(); + await this.hydrateSnapshotMetadataIfMissing(registry, name); registry.activeAccountName = name; - await this.persistRegistry(registry); + await saveRegistry(registry); return name; } @@ -1035,6 +983,89 @@ export class AccountService { registry.accounts[accountName] = entry; } + private async hydrateSnapshotMetadataIfMissing(registry: RegistryData, accountName: string): Promise { + const entry = registry.accounts[accountName]; + if (entry?.email && entry.accountId && entry.userId && entry.planType) { + return; + } + + await this.hydrateSnapshotMetadata(registry, accountName); + } + + private async resolveLoginAccountNameForSnapshot( + incomingSnapshot: ParsedAuthSnapshot, + activeName: string | null, + ): Promise { + const existing = await this.resolveExistingAccountNameForIncomingSnapshot(incomingSnapshot, activeName); + if (existing) return existing; + + return { + name: await this.inferAccountNameFromSnapshot(incomingSnapshot), + source: "inferred", + }; + } + + private async resolveExistingAccountNameForIncomingSnapshot( + incomingSnapshot: ParsedAuthSnapshot, + activeName: string | null, + ): Promise { + let emailMatch: ResolvedDefaultAccountName | null = null; + const accountNames = await this.listAccountNames(); + const candidates = this.orderReloginSnapshotCandidates(accountNames, incomingSnapshot, activeName); + + for (const name of candidates) { + const snapshotPath = this.accountFilePath(name); + if (!(await this.pathExists(snapshotPath))) continue; + + const existingSnapshot = await parseAuthSnapshotFile(snapshotPath); + if (this.snapshotsShareIdentity(existingSnapshot, incomingSnapshot)) { + return { + name, + source: activeName === name ? "active" : "existing", + }; + } + + if (!emailMatch && activeName === name && this.snapshotsShareEmail(existingSnapshot, incomingSnapshot)) { + emailMatch = { + name, + source: "active", + forceOverwrite: true, + }; + } + } + + return emailMatch; + } + + private orderReloginSnapshotCandidates( + accountNames: string[], + incomingSnapshot: ParsedAuthSnapshot, + activeName: string | null, + ): string[] { + const ordered: string[] = []; + const add = (name: string | null | undefined): void => { + if (!name || !accountNames.includes(name) || ordered.includes(name)) return; + ordered.push(name); + }; + + add(activeName); + + const incomingEmail = incomingSnapshot.email?.trim().toLowerCase(); + if (incomingEmail) { + try { + add(this.normalizeAccountName(incomingEmail)); + } catch { + // Invalid email-shaped snapshot names fall through to identity scan. + } + } + + for (const name of accountNames) { + add(name); + } + + return ordered; + } + private async resolveUniqueInferredName( baseName: string, incomingSnapshot: ParsedAuthSnapshot, @@ -1067,6 +1098,16 @@ export class AccountService { throw new AccountNameInferenceError(); } + private async inferAccountNameFromSnapshot(incomingSnapshot: ParsedAuthSnapshot): Promise { + const email = incomingSnapshot.email?.trim().toLowerCase(); + if (!email || !email.includes("@")) { + throw new AccountNameInferenceError(); + } + + const baseCandidate = this.normalizeAccountName(email); + return this.resolveUniqueInferredName(baseCandidate, incomingSnapshot); + } + private async loadReconciledRegistry(): Promise { const accountNames = await this.listAccountNames(); const loaded = await loadRegistry(); @@ -1092,6 +1133,7 @@ export class AccountService { await fsp.copyFile(source, authPath); await this.writeCurrentName(name); + await this.rememberSessionAuthFingerprint(authPath); } private async clearActivePointers(): Promise { @@ -1351,23 +1393,10 @@ export class AccountService { return false; } - private canRefreshActiveCanonicalEmailSnapshot( - activeName: string, - activeSnapshot: ParsedAuthSnapshot, - incomingSnapshot: ParsedAuthSnapshot, - ): boolean { - const activeEmail = activeSnapshot.email?.trim().toLowerCase(); - const incomingEmail = incomingSnapshot.email?.trim().toLowerCase(); - - if (!activeEmail || !incomingEmail || activeEmail !== incomingEmail) { - return false; - } - - try { - return activeName === this.normalizeAccountName(incomingEmail); - } catch { - return false; - } + private snapshotsShareEmail(a: ParsedAuthSnapshot, b: ParsedAuthSnapshot): boolean { + const aEmail = a.email?.trim().toLowerCase(); + const bEmail = b.email?.trim().toLowerCase(); + return Boolean(aEmail && bEmail && aEmail === bEmail); } private renderSnapshotIdentity(snapshot: ParsedAuthSnapshot, fallbackEmail: string): string { diff --git a/src/tests/save-account-safety.test.ts b/src/tests/save-account-safety.test.ts index da2b030c..a5e6a789 100644 --- a/src/tests/save-account-safety.test.ts +++ b/src/tests/save-account-safety.test.ts @@ -283,7 +283,7 @@ test("resolveLoginAccountNameFromCurrentAuth reuses active canonical email snaps }); }); -test("resolveLoginAccountNameFromCurrentAuth ignores active alias and infers canonical email snapshot", async (t) => { +test("resolveLoginAccountNameFromCurrentAuth reuses active alias for same-email relogin", async (t) => { await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => { const service = new AccountService(); const activeName = "team-primary"; @@ -309,8 +309,9 @@ test("resolveLoginAccountNameFromCurrentAuth ignores active alias and infers can const resolved = await service.resolveLoginAccountNameFromCurrentAuth(); assert.deepEqual(resolved, { - name: email, - source: "inferred", + name: activeName, + source: "active", + forceOverwrite: true, }); }); }); @@ -688,7 +689,7 @@ test("syncExternalAuthSnapshotIfNeeded disables auto-switch and snapshots extern }); }); -test("syncExternalAuthSnapshotIfNeeded re-keys active alias to inferred email name when external login identity matches", async (t) => { +test("syncExternalAuthSnapshotIfNeeded refreshes active alias instead of re-keying to email name", async (t) => { await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => { const service = new AccountService(); const activeAlias = "team-primary"; @@ -718,13 +719,139 @@ test("syncExternalAuthSnapshotIfNeeded re-keys active alias to inferred email na const result = await service.syncExternalAuthSnapshotIfNeeded(); assert.deepEqual(result, { synchronized: true, - savedName: incomingEmail, + savedName: activeAlias, autoSwitchDisabled: false, }); - assert.equal((await fsp.readFile(currentPath, "utf8")).trim(), incomingEmail); - const inferredSnapshot = await parseAuthSnapshotFile(path.join(accountsDir, `${incomingEmail}.json`)); - assert.equal(inferredSnapshot.email, incomingEmail); + assert.equal((await fsp.readFile(currentPath, "utf8")).trim(), activeAlias); + const aliasSnapshotRaw = await fsp.readFile(path.join(accountsDir, `${activeAlias}.json`), "utf8"); + assert.match(aliasSnapshotRaw, /token-post-login/); + await assert.rejects(() => fsp.access(path.join(accountsDir, `${incomingEmail}.json`))); + }); +}); + +test("syncExternalAuthSnapshotIfNeeded refreshes active same-identity snapshot after relogin", async (t) => { + await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => { + const service = new AccountService(); + const activeName = "admin@mite.hu"; + const currentPath = path.join(codexDir, "current"); + const registryPath = path.join(accountsDir, "registry.json"); + + await fsp.writeFile( + path.join(accountsDir, `${activeName}.json`), + buildAuthPayload(activeName, { + accountId: "acct-admin", + userId: "user-admin", + tokenSeed: "pre-login", + }), + "utf8", + ); + await fsp.writeFile(currentPath, `${activeName}\n`, "utf8"); + await fsp.writeFile( + registryPath, + `${JSON.stringify( + { + version: 1, + autoSwitch: { + enabled: true, + threshold5hPercent: 10, + thresholdWeeklyPercent: 5, + }, + api: { + usage: true, + }, + activeAccountName: activeName, + accounts: { + [activeName]: { + name: activeName, + createdAt: new Date().toISOString(), + email: activeName, + accountId: "acct-admin", + userId: "user-admin", + planType: "team", + }, + }, + }, + null, + 2, + )}\n`, + "utf8", + ); + await fsp.writeFile( + authPath, + buildAuthPayload(activeName, { + accountId: "acct-admin", + userId: "user-admin", + tokenSeed: "post-login", + }), + "utf8", + ); + + const result = await service.syncExternalAuthSnapshotIfNeeded(); + assert.deepEqual(result, { + synchronized: true, + savedName: activeName, + autoSwitchDisabled: false, + }); + + const refreshedSnapshotRaw = await fsp.readFile(path.join(accountsDir, `${activeName}.json`), "utf8"); + assert.match(refreshedSnapshotRaw, /token-post-login/); + assert.equal((await fsp.readFile(currentPath, "utf8")).trim(), activeName); + + const registry = JSON.parse(await fsp.readFile(registryPath, "utf8")) as { + autoSwitch: { enabled: boolean }; + }; + assert.equal(registry.autoSwitch.enabled, true); + }); +}); + +test("syncExternalAuthSnapshotIfNeeded reuses a saved alias that matches relogin identity", async (t) => { + await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => { + const service = new AccountService(); + const activeName = "primary@edixai.com"; + const savedAlias = "team-primary"; + const incomingEmail = "admin@kozpontihusbolt.hu"; + const currentPath = path.join(codexDir, "current"); + + await fsp.writeFile( + path.join(accountsDir, `${activeName}.json`), + buildAuthPayload(activeName, { + accountId: "acct-primary", + userId: "user-primary", + }), + "utf8", + ); + await fsp.writeFile( + path.join(accountsDir, `${savedAlias}.json`), + buildAuthPayload(incomingEmail, { + accountId: "acct-team", + userId: "user-team", + tokenSeed: "pre-login", + }), + "utf8", + ); + await fsp.writeFile(currentPath, `${activeName}\n`, "utf8"); + await fsp.writeFile( + authPath, + buildAuthPayload(incomingEmail, { + accountId: "acct-team", + userId: "user-team", + tokenSeed: "post-login", + }), + "utf8", + ); + + const result = await service.syncExternalAuthSnapshotIfNeeded(); + assert.deepEqual(result, { + synchronized: true, + savedName: savedAlias, + autoSwitchDisabled: false, + }); + + assert.equal((await fsp.readFile(currentPath, "utf8")).trim(), savedAlias); + const aliasSnapshotRaw = await fsp.readFile(path.join(accountsDir, `${savedAlias}.json`), "utf8"); + assert.match(aliasSnapshotRaw, /token-post-login/); + await assert.rejects(() => fsp.access(path.join(accountsDir, `${incomingEmail}.json`))); }); }); @@ -814,6 +941,35 @@ test("useAccount writes auth.json as a regular file (never symlink)", async (t) }); }); +test("useAccount records session auth fingerprint for the switch fast path", async (t) => { + await withIsolatedCodexDir(t, async ({ accountsDir }) => { + const service = new AccountService(); + const accountName = "fast-switch"; + const sessionMapPath = path.join(accountsDir, "sessions.json"); + const sessionKey = `ppid:${process.ppid}`; + + await fsp.writeFile( + path.join(accountsDir, `${accountName}.json`), + buildAuthPayload("fast-switch@edixai.com"), + "utf8", + ); + + await service.useAccount(accountName); + + const sessionMap = JSON.parse(await fsp.readFile(sessionMapPath, "utf8")) as { + sessions: Record; + }; + assert.equal(sessionMap.sessions[sessionKey]?.accountName, accountName); + assert.equal(typeof sessionMap.sessions[sessionKey]?.authFingerprint, "string"); + + const result = await service.syncExternalAuthSnapshotIfNeeded(); + assert.deepEqual(result, { + synchronized: false, + autoSwitchDisabled: false, + }); + }); +}); + test("getCurrentAccountName falls back to global current pointer when codex is not active in this terminal", async (t) => { await withIsolatedCodexDir(t, async ({ codexDir, accountsDir }) => { const service = new AccountService();