Skip to content

fix: preserve workspace-specific usage quotas + harden test isolation#161

Merged
ndycode merged 7 commits into
ndycode:mainfrom
hawkff:fix/usage-workspace-dedupe
May 30, 2026
Merged

fix: preserve workspace-specific usage quotas + harden test isolation#161
ndycode merged 7 commits into
ndycode:mainfrom
hawkff:fix/usage-workspace-dedupe

Conversation

@hawkff
Copy link
Copy Markdown
Contributor

@hawkff hawkff commented May 29, 2026

Summary

  • What changed?

    • Workspace-aware usage dedupe. codex-limits and TUI usage discovery deduplicated accounts purely by refresh token. Multiple ChatGPT workspaces under one login share a single refresh token while exposing distinct accountId/organizationId, so same-token workspaces collapsed into one row. Usage accounts are now deduplicated by workspace identity (accountId + organizationId) when available, falling back to refresh token only when no workspace identity exists. Dedupe keys are JSON.stringify arrays so delimiter characters can't cause collisions. Disabled accounts are skipped, and the active-account marker tracks the active workspace identity rather than the shared refresh token alone.
    • Sparse-slot guard. resolveCodexUsageActiveAccount no longer throws on an undefined account slot; empty slots are treated as disabled, matching the selection loop.
    • Test isolation fix. test/rotation-integration.test.ts could leak fixture accounts into the real account store (~/.opencode/oc-codex-multi-auth-accounts.json): it redirects storage with setStoragePathDirect(TEST_STORAGE_PATH) and resets to null in afterAll, but a pending saveToDiskDebounced() timer could fire after the reset. The suite now drains shutdown-flush handlers via runCleanup() before resetting, and flushes/disposes managers in the debounce tests while the test path is still active.
    • Docstrings for the new/changed usage helpers and the codex-limits tool factory.
  • Why is this needed? Users running multiple workspaces under one login saw only one workspace's quota in codex-limits/TUI. This restores correct per-workspace visibility. The test leak is a safety issue: running the suite could overwrite a developer's real local account store.

    Workspaces that share one ChatGPT login still share one server-side Codex quota. This fixes how distinct workspace entries are displayed and deduplicated; it does not create independent quotas.

Testing

  • npm run lint
  • npm run build
  • npm test

Local results on this branch: lint clean, typecheck clean, npm test 85 files / 2386 tests passing. Added regression tests for distinct same-token workspaces, skipped disabled accounts, delimiter-collision safety, sparse/undefined slots, and all-empty/disabled storage returning null. Verified the live account store checksum/mtime is unchanged before/after the full suite.

Summary by CodeRabbit

  • Bug Fixes

    • Improved account deduplication to prefer workspace identity, skip accounts lacking a derived key, keep first-seen ordering, and retain the most recent record per identity.
    • Refined active-account selection: ignores disabled/missing slots, treats absent timestamps consistently, returns null only when all accounts are missing/disabled, and preserves the active marker when an active entry is deduplicated.
  • Tests

    • Expanded coverage for deduplication, delimiter-safe keys, sparse storage, active-selection edge cases, and teardown flushing of debounced saves.

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

fixes workspace-aware dedupe so multiple chatgpt workspaces sharing one refresh token are shown as separate rows in codex-limits/tui, and hardens test isolation so the debounced-save tests can't leak fixture data into the developer's real account store.

  • deduplicateUsageAccountIndices now keys on accountId+organizationId first, falling back to refresh token, using json-serialised tagged arrays to prevent delimiter collisions; per-key it keeps the last occurrence so a re-issued refresh token wins over the stale original.
  • resolveCodexUsageActiveAccount no longer throws on sparse account slots; disabled accounts are skipped throughout both helpers.
  • rotation-integration.test.ts debounced-save tests now flush and dispose their own AccountManager instances before afterAll resets the storage path, removing the runCleanup() call that could have drained shutdown handlers from other workers.

Confidence Score: 5/5

the dedup and active-marker changes are correct and well-tested; no data loss or token-leak paths introduced

core logic in deduplicateUsageAccountIndices and resolveCodexUsageActiveAccount is sound — last-occurrence preference correctly surfaces the freshest credential, the workspace-identity key prevents delimiter collisions, and the sparse-slot guard behaves correctly. the two stale inline comments are documentation noise and do not affect runtime behaviour. test isolation fix is clean.

the logWarn comment in lib/tools/codex-limits.ts and the test comment in test/index.test.ts both describe the old first-occurrence dedup direction; worth correcting before this ships to avoid future confusion

Important Files Changed

Filename Overview
lib/codex-usage.ts adds getUsageAccountDedupeKey and rewrites deduplicateUsageAccountIndices to key on workspace identity first, keep the last (freshest) occurrence per key, and skip disabled accounts; resolveCodexUsageActiveAccount hardened against sparse slots and disabled active accounts — logic is sound and well-tested
lib/tools/codex-limits.ts active-marker matching upgraded to workspace-identity key comparison with token-match fallback; logWarn on deduped-out active index added, but the prose explanation above the warn describes the inverted (old first-occurrence) scenario
test/codex-usage.test.ts new regression tests cover distinct same-token workspaces, disabled-skip, delimiter-collision safety, sparse slots, all-empty/disabled null return, and missing lastUsed — good coverage of the changed paths
test/index.test.ts integration test updated for 3-account distinct-workspace display; new re-issued-token test is correct but the inline comment describes old first-occurrence dedupe direction (now inverted)
test/rotation-integration.test.ts debounced-save tests now flush and dispose their own managers before afterAll resets the storage path; runCleanup() correctly removed to avoid draining the global shutdown queue across workers

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[storage.accounts] --> B[deduplicateUsageAccountIndices]
    B --> C{account exists?}
    C -- no --> D[skip sparse slot]
    C -- yes --> E{enabled === false?}
    E -- yes --> F[skip disabled]
    E -- no --> G[getUsageAccountDedupeKey]
    G --> H{accountId or organizationId?}
    H -- yes --> I["key = JSON.stringify(['workspace', accountId, orgId])"]
    H -- no --> J{refreshToken?}
    J -- yes --> K["key = JSON.stringify(['refresh', token])"]
    J -- no --> L[skip: no identity]
    I --> M["indexByIdentity.set(key, i) - overwrites → last occurrence wins"]
    K --> M
    M --> N["[...indexByIdentity.values()] - first-appearance order"]
    N --> O[uniqueIndices for codex-limits display]
    O --> P{i === activeIndex or accountUsageKey === activeUsageKey?}
    P -- yes --> Q[show active marker]
    P -- no --> R[no marker]
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
test/index.test.ts:1158-1162
**stale dedup-direction comment**

the inline comment says "dedupe keeps the first occurrence (older token)" and "the active marker must still attach to that surviving entry via workspace-identity match" — both are backwards. `deduplicateUsageAccountIndices` now keeps the **last** occurrence (`indexByIdentity.set(key, i)` overwrites on every pass), so both entries share `acc-1/org-1`, the Map keeps index 1 (`rt_reissued`), and `activeIndex === 1` means the marker lands via a direct `i === activeIndex` hit, not via workspace-identity recovery. the comment describes the old first-occurrence behaviour that was explicitly replaced by this PR; a future engineer reading it would come away with the wrong mental model of the dedup direction.

### Issue 2 of 2
lib/tools/codex-limits.ts:122-135
**`logWarn` comment describes inverted scenario**

the explanation above the `logWarn` says the warn fires when "the active account is a later occurrence of a workspace whose first occurrence carries a re-issued refresh token" — but that's the old first-occurrence world. with last-occurrence preference, the **last** occurrence is the one kept in `uniqueIndices`. the warn therefore fires when `activeIndex` points at an **earlier** occurrence while a newer duplicate at a higher index has taken its place, or when the active account is excluded due to `enabled === false`. the written rationale is inverted; a developer chasing this log line will look for the wrong thing.

Reviews (3): Last reviewed commit: "fix: prefer freshest occurrence in usage..." | Re-trigger Greptile

Your Name added 5 commits May 29, 2026 18:36
… store

The suite redirects storage via setStoragePathDirect(TEST_STORAGE_PATH) and
resets it to null in afterAll. A pending saveToDiskDebounced() timer could
fire after the reset and write fixture accounts into the real default store
(~/.opencode/oc-codex-multi-auth-accounts.json).

- Drain registered shutdown-flush handlers with runCleanup() before resetting
  the storage path in afterAll.
- Flush the pending debounced save and dispose the shutdown handler in the
  debounce-specific tests while the test path is still active.

No production code changes; test isolation only.
Add JSDoc explaining workspace-identity dedupe and the active-account
marker rule. Docs only; no behavior change.
resolveCodexUsageActiveAccount's every() guard accessed account.enabled
directly, which throws when storage contains an undefined slot. Treat
null/undefined slots as disabled, matching the selection loop below.

Adds regression tests for sparse slots and all-empty/disabled storage.
Addresses CodeRabbit review on fork PR #1.
Document getUsageAccountDedupeKey, deduplicateUsageAccountIndices,
resolveCodexUsageActiveAccount, and normalizeUsageIdentityPart to explain
the workspace-identity-first dedupe strategy and collision-safe keys.

Docs only; no behavior change.
@hawkff hawkff requested a review from ndycode as a code owner May 29, 2026 23:14
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

This PR changes account deduplication to prefer workspace identity (accountId+organizationId) with refresh-token fallback, rewrites deduplication to skip disabled/empty accounts and preserve first-seen order, updates active-account selection to handle missing/invalid lastUsed and to return null when no enabled accounts exist, integrates the dedupe key into codex-limits active-marker logic, and adds/tests teardown improvements.

Changes

Workspace-identity deduplication and account selection

Layer / File(s) Summary
Deduplication key creation and dedupe implementation
lib/codex-usage.ts
getUsageAccountDedupeKey emits a stable key preferring workspace identity and falling back to refresh token; deduplicateUsageAccountIndices skips disabled/undefined accounts, drops entries with no key, deduplicates by that key, and preserves first-seen order.
Active account resolution logic and documentation
lib/codex-usage.ts
resolveCodexUsageActiveAccount JSDoc updated; selection returns null when no enabled accounts exist, otherwise normalizes lastUsed (missing/invalid -> 0), initializes baseline properly, and picks the enabled account with greatest normalized lastUsed.
Codex-limits tool integration with workspace deduplication
lib/tools/codex-limits.ts, test/index.test.ts
Import getUsageAccountDedupeKey, derive activeUsageKey, compute per-account accountUsageKey, and prefer workspace-key equality when attributing [active] (refresh-token equality used only when workspace key is absent); tests updated/added to reflect retained same-token workspace accounts and active-marker preservation.
Unit tests for deduplication and active selection
test/codex-usage.test.ts
New and updated tests cover same-refresh-token-but-different-workspace separation, delimiter-safe workspace-key generation, sparse/undefined account slots handling, returning null when all accounts disabled, preserving configured active when lastUsed missing, and excluding accounts with neither workspace identity nor refresh token.
Rotation integration test cleanup and teardown
test/rotation-integration.test.ts
Clarify afterAll intentionally does not call shared runCleanup; tests now await flushPendingSave() and call disposeShutdownHandler() to ensure debounced writes complete before teardown.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • ndycode/oc-codex-multi-auth#70: Main PR updates the codex-usage/codex-limits account deduplication behavior to use a workspace-scoped identity dedupe key (and adjust active-account selection accordingly), whereas PR #70 initially added codex-limits and deduplicated accounts by refreshToken—both directly change the same deduplication behavior feeding codex-limits output.
  • ndycode/oc-codex-multi-auth#146: Both PRs touch the Codex usage-account selection logic (e.g., the lib/codex-usage.ts helpers like deduplicateUsageAccountIndices / resolveCodexUsageActiveAccount), with this PR refining workspace-identity dedupe and active-account resolution.

Suggested reviewers

  • ndycode

Poem

🐰 I hop through keys and tokens bright,

Workspace IDs now hold the light,
Duplicates parted, active stays true,
Tests and flushes tidy the queue,
Night falls on a stable view.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: workspace-specific usage quota preservation and test isolation hardening.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR description is comprehensive and well-structured, covering all required sections with clear explanations of changes, rationale, testing, and compliance.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Comment thread test/rotation-integration.test.ts Outdated
Comment thread lib/codex-usage.ts Outdated
Comment thread lib/tools/codex-limits.ts Outdated
Copy link
Copy Markdown
Contributor

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/codex-usage.ts`:
- Around line 590-609: The active account selection treats a missing
activeAccount.lastUsed as -1 while other enabled accounts default to 0, causing
enabled active accounts with undefined lastUsed to lose to other enabled
accounts; update the activeLastUsed initialization in lib/codex-usage.ts
(symbol: activeLastUsed) to use the same fallback as other accounts (0) when the
active account is enabled so newestLastUsed/newestIndex logic (symbols:
newestLastUsed, newestIndex, storage.accounts, activeAccount, index) compares
like-for-like and preserves the active marker correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cdea7d64-69be-4e85-a464-fc98db47fe79

📥 Commits

Reviewing files that changed from the base of the PR and between 5a28241 and 9ff8d87.

📒 Files selected for processing (5)
  • lib/codex-usage.ts
  • lib/tools/codex-limits.ts
  • test/codex-usage.test.ts
  • test/index.test.ts
  • test/rotation-integration.test.ts

Comment thread lib/codex-usage.ts
- resolveCodexUsageActiveAccount: an enabled active account with a
  missing/invalid lastUsed now falls back to 0 (not -1), so a lower-index
  enabled account with lastUsed 0 can no longer steal the active marker
  (CodeRabbit).
- codex-limits: match the active account by workspace identity first, so the
  [active] marker survives when the active entry is a same-workspace duplicate
  carrying a re-issued refresh token; warn when the active index is deduped out
  (Greptile).
- deduplicateUsageAccountIndices: document that accounts with no workspace
  identity and no refresh token are dropped (Greptile).
- rotation-integration test: drop the global runCleanup() in afterAll, which
  could drain other files' shutdown-flush handlers under non-default vitest
  pools; debounce tests already flush/dispose their own managers (Greptile).
- Add regression tests for each case.
Comment thread lib/codex-usage.ts Outdated
deduplicateUsageAccountIndices kept the first occurrence of each workspace
key. After a token re-issue (re-add account), the surviving first occurrence
carried the invalidated refresh token, so the active workspace showed an
error right after re-authenticating. Keep the last (most recently added)
occurrence instead; the [active] marker already matches by workspace identity,
so this is safe. Display order still follows first appearance.

Addresses Greptile P1 follow-up on PR ndycode#161.
hawkff pushed a commit to hawkff/oc-codex-multi-auth that referenced this pull request May 30, 2026
- resolveCodexUsageActiveAccount: an enabled active account with a
  missing/invalid lastUsed now falls back to 0 (not -1), so a lower-index
  enabled account with lastUsed 0 can no longer steal the active marker
  (CodeRabbit).
- codex-limits: match the active account by workspace identity first, so the
  [active] marker survives when the active entry is a same-workspace duplicate
  carrying a re-issued refresh token; warn when the active index is deduped out
  (Greptile).
- deduplicateUsageAccountIndices: document that accounts with no workspace
  identity and no refresh token are dropped (Greptile).
- rotation-integration test: drop the global runCleanup() in afterAll, which
  could drain other files' shutdown-flush handlers under non-default vitest
  pools; debounce tests already flush/dispose their own managers (Greptile).
- Add regression tests for each case.
hawkff pushed a commit to hawkff/oc-codex-multi-auth that referenced this pull request May 30, 2026
deduplicateUsageAccountIndices kept the first occurrence of each workspace
key. After a token re-issue (re-add account), the surviving first occurrence
carried the invalidated refresh token, so the active workspace showed an
error right after re-authenticating. Keep the last (most recently added)
occurrence instead; the [active] marker already matches by workspace identity,
so this is safe. Display order still follows first appearance.

Addresses Greptile P1 follow-up on PR ndycode#161.
@ndycode ndycode merged commit 15988d2 into ndycode:main May 30, 2026
2 checks passed
@ndycode ndycode mentioned this pull request May 30, 2026
ndycode added a commit that referenced this pull request May 30, 2026
Bump to 6.2.0 (TUI email masking #160, workspace-aware usage dedupe + test isolation #161) with review polish and manifest sync.
@hawkff hawkff deleted the fix/usage-workspace-dedupe branch May 30, 2026 13:14
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.

2 participants