-
Notifications
You must be signed in to change notification settings - Fork 5.7k
fix(server): correctly report usage limits with multi-account Codex routing #11625
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| import { describe, expect, it } from "@effect/vitest"; | ||
| import * as Effect from "effect/Effect"; | ||
| import * as Path from "effect/Path"; | ||
| import * as Schema from "effect/Schema"; | ||
|
|
||
| import { | ||
| CodexSettings, | ||
| ProviderDriverKind, | ||
| } from "@t3tools/contracts"; | ||
| import { resolveCodexHomeLayout } from "../provider/Drivers/CodexHomeLayout.ts"; | ||
|
|
||
| const decodeCodexSettings = Schema.decodeSync(CodexSettings); | ||
| const codexDriverKind = ProviderDriverKind.make("codex"); | ||
|
|
||
| /** | ||
| * Regression test for #11515: UsageService.resolveTranscriptDirs must scan | ||
| * every configured Codex instance, not just the legacy single-instance config. | ||
| * | ||
| * We test the underlying logic (resolveCodexHomeLayout + deduplication) rather | ||
| * than the full service because the service requires a live settings layer and | ||
| * filesystem. The bug was that `settings.providerInstances` entries with | ||
| * `driver === "codex"` were ignored entirely, so their session directories | ||
| * never appeared in the scan list. | ||
| */ | ||
| it.layer(NodeServices.layer)("UsageService multi-account Codex routing (#11515)", (it) => { | ||
| describe("resolveTranscriptDirs multi-instance enumeration", () => { | ||
| it.effect("resolves distinct session dirs for multiple Codex instances", () => | ||
| Effect.gen(function* () { | ||
| const path = yield* Path.Path; | ||
|
|
||
| // Simulate what resolveTranscriptDirs does: collect unique session dirs | ||
| // from both the legacy config and providerInstances. | ||
| const seenCodexDirs = new Set<string>(); | ||
| const codexDirs: string[] = []; | ||
|
|
||
| const addCodexDir = (layout: { readonly sharedHomePath: string }) => { | ||
| const sessionDir = path.join(layout.sharedHomePath, "sessions"); | ||
| if (seenCodexDirs.has(sessionDir)) return; | ||
| seenCodexDirs.add(sessionDir); | ||
| codexDirs.push(sessionDir); | ||
| }; | ||
|
|
||
| // Legacy config: default ~/.codex | ||
| const legacyConfig = decodeCodexSettings({}); | ||
| const legacyLayout = yield* resolveCodexHomeLayout(legacyConfig); | ||
| addCodexDir(legacyLayout); | ||
|
|
||
| // Multi-instance configs: two additional accounts with different homes | ||
| const personalHome = path.resolve("/home/user/.codex-personal"); | ||
| const workHome = path.resolve("/home/user/.codex-work"); | ||
| const instances: Record<string, { driver: string; config: Record<string, unknown> }> = { | ||
| codex_personal: { | ||
| driver: "codex", | ||
| config: { homePath: personalHome }, | ||
| }, | ||
| codex_work: { | ||
| driver: "codex", | ||
| config: { homePath: workHome }, | ||
| }, | ||
| claude_primary: { | ||
| driver: "claudeAgent", | ||
| config: {}, | ||
| }, | ||
| }; | ||
|
|
||
| for (const instance of Object.values(instances)) { | ||
| if (instance.driver !== codexDriverKind) continue; | ||
| const decoded = decodeCodexSettings(instance.config); | ||
| const layout = yield* resolveCodexHomeLayout(decoded); | ||
| addCodexDir(layout); | ||
| } | ||
|
|
||
| // Should have 3 distinct Codex session directories | ||
| expect(codexDirs).toHaveLength(3); | ||
| expect(codexDirs).toContain(path.join(legacyLayout.sharedHomePath, "sessions")); | ||
| expect(codexDirs).toContain(path.join(personalHome, "sessions")); | ||
| expect(codexDirs).toContain(path.join(workHome, "sessions")); | ||
| }), | ||
| ); | ||
|
|
||
| it.effect("deduplicates when two instances share the same home path", () => | ||
| Effect.gen(function* () { | ||
| const path = yield* Path.Path; | ||
|
|
||
| const seenCodexDirs = new Set<string>(); | ||
| const codexDirs: string[] = []; | ||
|
|
||
| const addCodexDir = (layout: { readonly sharedHomePath: string }) => { | ||
| const sessionDir = path.join(layout.sharedHomePath, "sessions"); | ||
| if (seenCodexDirs.has(sessionDir)) return; | ||
| seenCodexDirs.add(sessionDir); | ||
| codexDirs.push(sessionDir); | ||
| }; | ||
|
|
||
| // Two instances pointing at the same home should produce one entry | ||
| const sharedHome = path.resolve("/shared/codex-home"); | ||
| const instances: Record<string, { driver: string; config: Record<string, unknown> }> = { | ||
| codex_a: { | ||
| driver: "codex", | ||
| config: { homePath: sharedHome }, | ||
| }, | ||
| codex_b: { | ||
| driver: "codex", | ||
| config: { homePath: sharedHome }, | ||
| }, | ||
| }; | ||
|
|
||
| for (const instance of Object.values(instances)) { | ||
| if (instance.driver !== codexDriverKind) continue; | ||
| const decoded = decodeCodexSettings(instance.config); | ||
| const layout = yield* resolveCodexHomeLayout(decoded); | ||
| addCodexDir(layout); | ||
| } | ||
|
|
||
| expect(codexDirs).toHaveLength(1); | ||
| expect(codexDirs[0]).toBe(path.join(sharedHome, "sessions")); | ||
| }), | ||
| ); | ||
|
|
||
| it.effect("skips non-codex provider instances", () => | ||
| Effect.gen(function* () { | ||
| const path = yield* Path.Path; | ||
|
|
||
| const seenCodexDirs = new Set<string>(); | ||
| const codexDirs: string[] = []; | ||
|
|
||
| const addCodexDir = (layout: { readonly sharedHomePath: string }) => { | ||
| const sessionDir = path.join(layout.sharedHomePath, "sessions"); | ||
| if (seenCodexDirs.has(sessionDir)) return; | ||
| seenCodexDirs.add(sessionDir); | ||
| codexDirs.push(sessionDir); | ||
| }; | ||
|
|
||
| const instances: Record<string, { driver: string; config: Record<string, unknown> }> = { | ||
| claude_main: { | ||
| driver: "claudeAgent", | ||
| config: {}, | ||
| }, | ||
| cursor_default: { | ||
| driver: "cursor", | ||
| config: {}, | ||
| }, | ||
| }; | ||
|
|
||
| for (const instance of Object.values(instances)) { | ||
| if (instance.driver !== codexDriverKind) continue; | ||
| const decoded = decodeCodexSettings(instance.config); | ||
| const layout = yield* resolveCodexHomeLayout(decoded); | ||
| addCodexDir(layout); | ||
| } | ||
|
|
||
| expect(codexDirs).toHaveLength(0); | ||
| }), | ||
| ); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,8 @@ | |
| import * as NodeOS from "node:os"; | ||
|
|
||
| import { | ||
| CodexSettings, | ||
| ProviderDriverKind, | ||
| USAGE_CONTRACT_VERSION, | ||
| type UsageProviderKind, | ||
| type UsageSource, | ||
|
|
@@ -217,12 +219,42 @@ export const make = Effect.gen(function* () { | |
|
|
||
| const claudeHome = yield* resolveClaudeHomePath(settings.providers.claudeAgent); | ||
| const claudeDir = yield* resolveClaudeTranscriptDir(claudeHome); | ||
| const codexLayout = yield* resolveCodexHomeLayout(settings.providers.codex); | ||
|
|
||
| return [ | ||
| { provider: "claude" as const, dir: claudeDir }, | ||
| { provider: "codex" as const, dir: path.join(codexLayout.sharedHomePath, "sessions") }, | ||
| // Collect Codex session directories from every configured instance. The | ||
| // legacy single-instance config (`settings.providers.codex`) is always | ||
| // scanned for backward compatibility. Additional instances declared in | ||
| // `settings.providerInstances` each contribute their own shared home so | ||
| // multi-account setups (e.g. codex_personal + codex_work) are fully | ||
| // covered. Duplicate shared homes are de-duplicated so two instances | ||
| // pointing at the same directory do not double-count transcripts. | ||
| const codexDriverKind = ProviderDriverKind.make("codex"); | ||
| const seenCodexDirs = new Set<string>(); | ||
| const dirs: Array<{ provider: UsageProviderKind; dir: string }> = [ | ||
| { provider: "claude", dir: claudeDir }, | ||
| ]; | ||
|
|
||
| const addCodexDir = (layout: { readonly sharedHomePath: string }) => { | ||
| const sessionDir = path.join(layout.sharedHomePath, "sessions"); | ||
| if (seenCodexDirs.has(sessionDir)) return; | ||
| seenCodexDirs.add(sessionDir); | ||
| dirs.push({ provider: "codex", dir: sessionDir }); | ||
| }; | ||
|
|
||
| const legacyLayout = yield* resolveCodexHomeLayout(settings.providers.codex); | ||
| addCodexDir(legacyLayout); | ||
|
|
||
| for (const instance of Object.values(settings.providerInstances)) { | ||
| if (instance.driver !== codexDriverKind) continue; | ||
| const instanceConfig = | ||
| typeof instance.config === "object" && instance.config !== null | ||
| ? (instance.config as Record<string, unknown>) | ||
| : {}; | ||
| const decoded = Schema.decodeSync(CodexSettings)(instanceConfig); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High A malformed Codex entry causes 🤖 Copy this AI Prompt to have your agent fix this:There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
fd -t f 'UsageService|ServerSettings|CodexSettings|Settings' apps/server/src | head -80
printf '%s\n' '--- relevant symbols ---'
rg -n -C 5 'decodeSync\(CodexSettings\)|providerInstances|CodexSettings|UsageReadError|ServerSettings' apps/server/srcRepository: pingdotgg/t3code Length of output: 50373 🤖 get_repo_knowledge executed:
Length of output: 26196 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- named files ---'
git ls-files | rg '(^|/)(UsageService|serverSettings|.*contracts.*|.*settings.*)\.(ts|tsx)$' | head -120
printf '%s\n' '--- UsageService focused section ---'
cat -n apps/server/src/usage/UsageService.ts | sed -n '1,320p'
printf '%s\n' '--- server settings references ---'
rg -l 'export .*ServerSettings|ServerSettingsService|providerInstances' apps/server/src packages --glob '*.ts' --glob '*.tsx' | head -80Repository: pingdotgg/t3code Length of output: 24202 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- apps/server/src/serverSettings.ts ---'
cat -n apps/server/src/serverSettings.ts | sed -n '1,280p'
printf '%s\n' '--- packages/contracts/src/settings.ts ---'
cat -n packages/contracts/src/settings.ts | sed -n '1,300p'
printf '%s\n' '--- packages/contracts/src/providerInstance.ts ---'
cat -n packages/contracts/src/providerInstance.ts | sed -n '1,280p'
printf '%s\n' '--- UsageService readSummary flow ---'
cat -n apps/server/src/usage/UsageService.ts | sed -n '320,620p'Repository: pingdotgg/t3code Length of output: 43663 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- relevant declarations ---'
rg -n -C 12 'CodexSettings|export const ServerSettings|providerInstances|decodeServerSettingsJsonExit|getSettings|updateSettings|load|decodeUnknownEffect' packages/contracts/src/settings.ts packages/contracts/src/providerInstance.ts apps/server/src/serverSettings.tsRepository: pingdotgg/t3code Length of output: 50372 Map invalid instance settings to If Confirm that 🤖 Prompt for AI Agents |
||
| const layout = yield* resolveCodexHomeLayout(decoded); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a Codex instance leaves Useful? React with 👍 / 👎. |
||
| addCodexDir(layout); | ||
| } | ||
|
|
||
| return dirs; | ||
| }); | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This regression test reconstructs the enumeration and deduplication logic locally instead of invoking
UsageService, so it remains green ifresolveTranscriptDirsstops iteratingproviderInstances—the exact regression it claims to prevent—or if the production decoder and settings wiring behave differently. Cover the service through focused layers or extract and directly test the production helper.AGENTS.md reference: AGENTS.md:L106-L109
Useful? React with 👍 / 👎.