Skip to content

feat(codex): add multi-account Codex home aggregation#870

Open
jackcpku wants to merge 4 commits intoryoppippi:mainfrom
jackcpku:feat/codex-multi-account
Open

feat(codex): add multi-account Codex home aggregation#870
jackcpku wants to merge 4 commits intoryoppippi:mainfrom
jackcpku:feat/codex-multi-account

Conversation

@jackcpku
Copy link
Contributor

@jackcpku jackcpku commented Mar 1, 2026

Summary

  • add multi-account source resolution for @ccusage/codex via --codex-home (supports comma-separated paths and label=path)
  • add --by-account report grouping for daily/monthly and account-aware session rendering
  • keep session IDs stable while preventing collisions across accounts by keying session aggregation with (account, sessionId)
  • document multi-account usage in codex README and codex guide pages

Implementation details

  • new session-sources.ts parser resolves sources from CLI or CODEX_HOME
  • loadTokenUsageEvents now accepts sessionSources and annotates each event with account
  • daily/monthly builders can optionally group rows by account
  • session builder separates identical session IDs across accounts
  • added tests for parser behavior, multi-source loading, and account grouping

Validation

  • pnpm --filter @ccusage/codex format
  • pnpm --filter @ccusage/codex test
  • pnpm --filter @ccusage/codex typecheck

Related: #839

Summary by CodeRabbit

  • New Features

    • Multi-account support: specify multiple Codex homes via --codex-home (comma-separated, optional label=path) and automatic aggregation.
    • New --by-account flag to split daily, monthly, and session reports by account; reports add an Account column and per-account totals when enabled.
    • Improved responsive table rendering with dynamic columns, aligned totals, and compact mode.
  • Documentation

    • Updated guides and environment variable docs (CODEX_HOME, LOG_LEVEL) with examples and multi-account usage.
  • Tests

    • Added tests covering multi-account loading, parsing, and per-account report grouping.

@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

📝 Walkthrough

Walkthrough

Adds multi-account Codex support: new CLI flags (--codex-home, --by-account), session-source resolution, account-aware data loading/types, account-grouped daily/monthly/session reports, a reusable responsive usage table factory, and docs/README updates.

Changes

Cohort / File(s) Summary
Types & Shared Args
apps/codex/src/_types.ts, apps/codex/src/_shared-args.ts
New SessionSource type; optional account added to events/summaries/rows; models changed from MapRecord; added codexHome and byAccount CLI args.
Session Sources
apps/codex/src/session-sources.ts
New resolveSessionSources() module: parses --codex-home/env entries, supports label=path, tilde expansion, produces unique {account,directory} sources.
Data Loader
apps/codex/src/data-loader.ts
loadTokenUsageEvents accepts sessionSources; iterates per-source, assigns normalized account labels, resolves directories, and emits events augmented with account.
Report Generators
apps/codex/src/daily-report.ts, apps/codex/src/monthly-report.ts, apps/codex/src/session-report.ts
Added byAccount option; createSummary accepts optional account; grouping keys include account when byAccount is true; output rows include account; sorting updated; tests added/updated.
Commands & Table Rendering
apps/codex/src/commands/daily.ts, apps/codex/src/commands/monthly.ts, apps/codex/src/commands/session.ts, apps/codex/src/commands/usage-table.ts
Introduced createUsageResponsiveTable factory; commands call resolveSessionSources and pass sessionSources to loaders; table layout optionally includes Account column; rows/totals adapt to schema; informational log when multiple accounts exist without --by-account.
CLI Surface & README
apps/codex/README.md, docs/guide/codex/index.md, docs/guide/codex/daily.md, docs/guide/codex/monthly.md, docs/guide/codex/session.md
Documented --codex-home and --by-account, updated CODEX_HOME env var description and examples, added usage examples and notes about per-account behavior and responsive table layout.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as Command Handler
    participant Resolver as resolveSessionSources
    participant Loader as loadTokenUsageEvents
    participant Builder as Report Builder
    participant Table as createUsageResponsiveTable / Renderer

    User->>CLI: Run command (--codex-home, optional --by-account)
    CLI->>Resolver: resolveSessionSources(codexHomeArg)
    Resolver->>Resolver: Parse entries, expand ~, assign unique accounts
    Resolver-->>CLI: SessionSource[] {account,directory}
    CLI->>Loader: loadTokenUsageEvents({sessionSources})
    loop per SessionSource
        Loader->>Loader: Read sessions in directory
        Loader-->>CLI: Emit TokenUsageEvent (with account)
    end
    CLI->>Builder: buildReport(events, {byAccount})
    Builder->>Builder: Group by period (+ account if byAccount)
    Builder-->>CLI: ReportRows[] (include account)
    CLI->>Table: createUsageResponsiveTable({mode, includeAccountColumn})
    Table-->>CLI: table, tableColumnCount
    CLI->>Table: Render rows and totals (account column if present)
    Table-->>User: Terminal table output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ryoppippi

Poem

🐇 Hop, hop — Codex finds each home and name,
Paths split by commas, labeled without shame,
Sessions now carry which account they came,
Tables stretch or compact, totals call each name,
A rabbit cheers: "Many homes, one tidy frame!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(codex): add multi-account Codex home aggregation' clearly and concisely summarizes the main feature addition in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
apps/codex/src/session-sources.ts (1)

105-107: Isolate CODEX_HOME env mutation in tests.

This test deletes process.env[CODEX_HOME_ENV] and does not restore it, which can leak state into later tests in the same process.

✅ Suggested test hardening
 if (import.meta.vitest != null) {
 	describe('resolveSessionSources', () => {
+		const originalCodexHome = process.env[CODEX_HOME_ENV];
+		afterEach(() => {
+			if (originalCodexHome == null) {
+				delete process.env[CODEX_HOME_ENV];
+				return;
+			}
+			process.env[CODEX_HOME_ENV] = originalCodexHome;
+		});
+
 		it('uses default CODEX_HOME when no override is provided', () => {
 			delete process.env[CODEX_HOME_ENV];
 			const sources = resolveSessionSources();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/session-sources.ts` around lines 105 - 107, The test mutates
process.env[CODEX_HOME_ENV] without restoring it; update the test around
resolveSessionSources (the 'uses default CODEX_HOME when no override is
provided' test) to snapshot the original value of process.env[CODEX_HOME_ENV]
before deleting it, perform the delete and run resolveSessionSources, and then
restore the original value (or undefined) after the assertion (or use
beforeEach/afterEach to manage setup/teardown) so no global env state leaks to
other tests.
apps/codex/src/commands/daily.ts (1)

126-165: Extract table schema construction into a shared helper to reduce drift.

Line [127] onward mirrors near-identical header/alignment/compact setup in apps/codex/src/commands/monthly.ts and apps/codex/src/commands/session.ts. Centralizing this avoids future column-order mismatches.

♻️ Suggested direction
+// e.g. in a shared command table helper module
+export function buildUsageTableConfig(
+  mode: "daily" | "monthly" | "session",
+  includeAccountColumn: boolean,
+): {
+  head: string[];
+  colAligns: TableCellAlign[];
+  compactHead: string[];
+  compactColAligns: TableCellAlign[];
+} {
+  // centralize schema selection by mode + account flag
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/commands/daily.ts` around lines 126 - 165, Extract the
repeated table schema construction (head, colAligns, compactHead,
compactColAligns, tableColumnCount and ResponsiveTable init) into a shared
helper function (e.g., buildCostTableSchema or createResponsiveTableConfig) and
call it from daily.ts, monthly.ts, and session.ts; the helper should accept a
boolean byAccount and return the head, colAligns, compactHead, compactColAligns,
compactThreshold and tableColumnCount (or even return a preconfigured
ResponsiveTable instance) so you remove the duplicated blocks surrounding the
includeAccountColumn logic and the ResponsiveTable constructor while preserving
existing symbols like ResponsiveTable, head, colAligns, compactHead,
compactColAligns, compactThreshold and tableColumnCount.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/codex/src/data-loader.ts`:
- Around line 199-208: The derived account labels from options.sessionDirs use
only the basename (variable basename) which can produce collisions (e.g., two
different paths both named "sessions"); change the mapping logic that builds {
account, directory } (the options.sessionDirs.map block) to ensure uniqueness by
disambiguating duplicate account names — for example, compute accountCandidate =
allDirs.length <= 1 ? 'default' : (basename === '' ? fallbackAccount :
basename), then if accountCandidate already exists append a suffix (like
`-${index}`) or a short hash of path.resolve(dir) to form account, or
deduplicate in a post-pass that renames collisions consistently; update the code
which uses account (the account variable returned in the mapping) accordingly so
account labels are guaranteed unique.

In `@apps/codex/src/session-sources.ts`:
- Around line 70-73: The code passes codexHome into path.resolve without
expanding a leading tilde, so inputs like "~/.codex-work" are treated literally;
update the code that uses codexHome (the occurrences that compute baseName via
path.resolve(codexHome) and the other resolve at the other occurrence around
line 98) to first expand a leading "~" to the user's home directory (e.g.
codexHome = codexHome.replace(/^~(?=$|[\\/])/, os.homedir())), ensure you
import/require the os module, then call path.resolve on the expanded codexHome
before computing path.basename or other path operations.

In `@docs/guide/codex/index.md`:
- Line 70: The description for LOG_LEVEL contains a typo: replace the word
"consola" with "console" in the LOG_LEVEL table entry (the row referencing
`LOG_LEVEL`) so it reads "Adjust console verbosity (0 silent … 5 trace)"; update
only that string in the docs/guide/codex/index.md content where the `LOG_LEVEL`
table row is defined.

---

Nitpick comments:
In `@apps/codex/src/commands/daily.ts`:
- Around line 126-165: Extract the repeated table schema construction (head,
colAligns, compactHead, compactColAligns, tableColumnCount and ResponsiveTable
init) into a shared helper function (e.g., buildCostTableSchema or
createResponsiveTableConfig) and call it from daily.ts, monthly.ts, and
session.ts; the helper should accept a boolean byAccount and return the head,
colAligns, compactHead, compactColAligns, compactThreshold and tableColumnCount
(or even return a preconfigured ResponsiveTable instance) so you remove the
duplicated blocks surrounding the includeAccountColumn logic and the
ResponsiveTable constructor while preserving existing symbols like
ResponsiveTable, head, colAligns, compactHead, compactColAligns,
compactThreshold and tableColumnCount.

In `@apps/codex/src/session-sources.ts`:
- Around line 105-107: The test mutates process.env[CODEX_HOME_ENV] without
restoring it; update the test around resolveSessionSources (the 'uses default
CODEX_HOME when no override is provided' test) to snapshot the original value of
process.env[CODEX_HOME_ENV] before deleting it, perform the delete and run
resolveSessionSources, and then restore the original value (or undefined) after
the assertion (or use beforeEach/afterEach to manage setup/teardown) so no
global env state leaks to other tests.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c40ea6e and c4bf8d0.

📒 Files selected for processing (15)
  • apps/codex/README.md
  • apps/codex/src/_shared-args.ts
  • apps/codex/src/_types.ts
  • apps/codex/src/commands/daily.ts
  • apps/codex/src/commands/monthly.ts
  • apps/codex/src/commands/session.ts
  • apps/codex/src/daily-report.ts
  • apps/codex/src/data-loader.ts
  • apps/codex/src/monthly-report.ts
  • apps/codex/src/session-report.ts
  • apps/codex/src/session-sources.ts
  • docs/guide/codex/daily.md
  • docs/guide/codex/index.md
  • docs/guide/codex/monthly.md
  • docs/guide/codex/session.md

@jackcpku
Copy link
Contributor Author

jackcpku commented Mar 1, 2026

Addressed the review feedback in commit 1f2e604:

  • Fixed account label collisions when sessionDirs share the same basename by adding deterministic deduplication (sessions, sessions-2, ...).
  • Added ~ expansion for --codex-home / CODEX_HOME resolution before path.resolve.
  • Hardened session-sources tests to restore process.env.CODEX_HOME after each test.
  • Extracted repeated table schema/config construction into a shared helper (apps/codex/src/commands/usage-table.ts) and wired daily/monthly/session commands to it.
  • Fixed docs typo: consola -> console (docs + README).

Validation run:

  • pnpm --filter @ccusage/codex format
  • pnpm --filter @ccusage/codex test
  • pnpm --filter @ccusage/codex typecheck
    (all passing)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/codex/src/data-loader.ts`:
- Around line 236-241: The fallback logic for computing sessionSources builds a
single path from CODEX_HOME (using CODEX_HOME_ENV, DEFAULT_CODEX_DIR,
DEFAULT_SESSION_SUBDIR) and therefore skips the multi-account parsing used
elsewhere; update this to delegate to the same resolver used by the rest of the
code (e.g., call resolveSessionSources or the existing session-parsing utility)
so comma-separated and label=path entries in CODEX_HOME are parsed the same way.
Concretely, when providedSources and providedDirs are both undefined, compute
the fallback input as the resolved CODEX_HOME (defaulting to DEFAULT_CODEX_DIR)
+ DEFAULT_SESSION_SUBDIR and pass that through resolveSessionSources (or the
same parsing function) to produce sessionSources, instead of building a single
[{ account: 'default', directory: ... }] entry. Ensure you reference
CODEX_HOME_ENV, DEFAULT_CODEX_DIR, DEFAULT_SESSION_SUBDIR,
resolveSessionSources, and loadTokenUsageEvents to locate the change.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4bf8d0 and 1f2e604.

📒 Files selected for processing (8)
  • apps/codex/README.md
  • apps/codex/src/commands/daily.ts
  • apps/codex/src/commands/monthly.ts
  • apps/codex/src/commands/session.ts
  • apps/codex/src/commands/usage-table.ts
  • apps/codex/src/data-loader.ts
  • apps/codex/src/session-sources.ts
  • docs/guide/codex/index.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/codex/src/commands/monthly.ts

@jackcpku
Copy link
Contributor Author

jackcpku commented Mar 1, 2026

Addressed the additional CodeRabbit finding in commit d6c0eb8:

  • loadTokenUsageEvents now uses resolveSessionSources() for fallback source resolution when sessionSources and sessionDirs are not provided.
  • This ensures CODEX_HOME supports the same multi-account parsing (a=path,b=path) in loader fallback paths too, instead of being treated as a single path.
  • Added a regression test that sets comma-separated CODEX_HOME and verifies both accounts are loaded.

Validation:

  • pnpm --filter @ccusage/codex format
  • pnpm --filter @ccusage/codex test
  • pnpm --filter @ccusage/codex typecheck
    (all passing)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/codex/src/data-loader.ts`:
- Around line 179-187: The current makeUniqueAccountLabel(base, usedAccounts)
can produce labels that collide with existing explicit labels (e.g., "work-2");
change the logic to generate a candidate label and loop while
usedAccounts.has(candidate) incrementing a counter (start at 1 or 2 as
appropriate) until you find a candidate not present in usedAccounts, then set
usedAccounts.set(candidate, 1) and also update the base's counter
(usedAccounts.set(base, newCount)) so future generations continue from the
correct number; apply the same fix pattern to the analogous dedup
functions/blocks at the other ranges referenced (lines ~203-209 and ~218-226)
using the same candidate loop and usedAccounts.has(...) checks.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f2e604 and d6c0eb8.

📒 Files selected for processing (1)
  • apps/codex/src/data-loader.ts

@jackcpku
Copy link
Contributor Author

jackcpku commented Mar 1, 2026

Addressed the new account-label collision finding in commit d5c1a42.

Root cause:

  • The prior dedup strategy (base, base-2, base-3 via counter map) could still collide when explicit labels already used suffixes (e.g. work, work, work-2).

What changed:

  • Replaced dedup logic with candidate probing (base, base-2, base-3, ...) and collision checks.
  • Added reservation logic so generated suffixes do not preempt raw labels that appear later in the same input list.
  • Applied this in both:
    • apps/codex/src/data-loader.ts
    • apps/codex/src/session-sources.ts

Regression tests added:

  • session-sources: explicit suffix collision case (work, work, work-2) now resolves uniquely without duplicates.
  • data-loader: same collision case for sessionSources path.

Validation:

  • pnpm --filter @ccusage/codex format
  • pnpm --filter @ccusage/codex test
  • pnpm --filter @ccusage/codex typecheck
    (all passing)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/codex/src/data-loader.ts (1)

174-201: Extract account-label deduplication into a shared helper.

makeUniqueAccountLabels is now effectively duplicated in both apps/codex/src/data-loader.ts and apps/codex/src/session-sources.ts. Centralizing this logic would reduce drift risk in future suffix-collision fixes.

Also applies to: 214-245

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/data-loader.ts` around lines 174 - 201, Extract the duplicated
account-label deduplication into a single exported helper and replace both
copies with imports: create a shared function file that exports
makeUniqueAccountLabels (keeping the existing reservedLabels/usedLabels and
suffix logic) and optionally export normalizeAccountLabel if also duplicated,
then update callers in the two locations to import and use the shared
makeUniqueAccountLabels (and normalizeAccountLabel) instead of their local
implementations; ensure the exported names match (makeUniqueAccountLabels,
normalizeAccountLabel) and existing behavior/tests remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/codex/src/data-loader.ts`:
- Around line 174-201: Extract the duplicated account-label deduplication into a
single exported helper and replace both copies with imports: create a shared
function file that exports makeUniqueAccountLabels (keeping the existing
reservedLabels/usedLabels and suffix logic) and optionally export
normalizeAccountLabel if also duplicated, then update callers in the two
locations to import and use the shared makeUniqueAccountLabels (and
normalizeAccountLabel) instead of their local implementations; ensure the
exported names match (makeUniqueAccountLabels, normalizeAccountLabel) and
existing behavior/tests remain unchanged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6c0eb8 and d5c1a42.

📒 Files selected for processing (2)
  • apps/codex/src/data-loader.ts
  • apps/codex/src/session-sources.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant