Skip to content

Scope quota warning state to accounts#2130

Merged
steipete merged 3 commits into
steipete:mainfrom
vincent-peng:codex/account-scope-quota-warnings
Jul 14, 2026
Merged

Scope quota warning state to accounts#2130
steipete merged 3 commits into
steipete:mainfrom
vincent-peng:codex/account-scope-quota-warnings

Conversation

@vincent-peng

@vincent-peng vincent-peng commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • key static quota-warning episodes by a stable account discriminator instead of provider/window alone
  • forward account ownership through ordinary refresh, selected token-account, stacked Codex, and Claude OAuth/CLI publication paths
  • reconcile Claude extra-window warning state per account while preserving provider-wide cleanup when a warning window is disabled
  • add focused regressions for ordinary refresh, selected outcomes, stacked Codex accounts, Claude sibling accounts, and provider-wide cleanup

Root cause

QuotaWarningStateKey previously contained only the provider, warning window, and optional extra-window ID. When users switched accounts, one account's observation could clear, suppress, or re-arm another account's threshold episode. The stacked selected-Codex publication path also did not run the static threshold reducer.

This change separates the opaque state discriminator from the optional display identity. Stable token-account UUIDs and hashed provider ownership keys scope reducer state; notification copy still follows the existing Hide Personal Info setting.

Runtime proof

I ran an identical disposable harness at base c852c135 and current head be7f11c4. It drives the production UsageStore.refreshProvider path through six ordinary selected-account refreshes, with two synthetic token accounts and fixture-only provider outcomes. A notifier spy records the same QuotaWarningEvent passed to the notification layer.

Sequence: A 40%, B 40%, A 55%, B 30%, A 55%, B 55%, with a 50%-remaining threshold.

Base shared-state trace:

step=3 account=Account-A used=55
emitted account=Account-A threshold=50 remaining=45
step=5 account=Account-A used=55
emitted account=Account-A threshold=50 remaining=45
step=6 account=Account-B used=55
summary emitted=Account-A,Account-A

Account B's recovery re-armed A, so A emitted twice; B's own crossing was then suppressed.

Current-head account-scoped trace:

step=3 account=Account-A used=55
emitted account=Account-A threshold=50 remaining=45
step=5 account=Account-A used=55
step=6 account=Account-B used=55
emitted account=Account-B threshold=50 remaining=45
summary emitted=Account-A,Account-B

Each account retained an independent episode: no duplicate A warning and no suppressed B warning. Both runs passed one test in one suite using:

CODEXBAR_SUPPRESS_TEST_KEYCHAIN_ACCESS=1 swift test --filter QuotaWarningAccountRuntimeProofTests

The proof used isolated settings, synthetic labels/tokens, and stubbed provider outcomes. It did not read a real account store, browser data, provider session, or Keychain item; the disposable harness was not added to either branch.

Validation

  • CODEXBAR_SUPPRESS_TEST_KEYCHAIN_ACCESS=1 OS_ACTIVITY_MODE=disable swift test --filter 'UsageStoreAccountQuotaWarningTests|UsageStoreSessionQuotaTransitionTests|CodexSessionQuotaFalseRestoreTests|ClaudeExtraWindowQuotaWarningTests|PredictivePaceWarningTests' - 86 tests in 5 suites passed
  • make check - passed; SwiftFormat reported 0 files requiring formatting and SwiftLint reported 0 violations across 1,426 files
  • make test on this commit combined with the test-only isolation from Isolate Codex CLI account test from local data #2128 - all 639 selections passed across 54/54 groups, with zero first-pass failures, retries, or timeouts
  • disposable merge simulation against current Add external event hooks for quota and provider state changes #2001 head b14629bd - 51 focused tests in 5 suites passed; make check reported 0/1,437 files requiring formatting and 0 SwiftLint violations across 1,436 files; git diff --check passed
  • git diff --check - passed

Coordination with #2001

#2001 now has a dedicated QuotaLowHookUsageKey at head b14629bd, but derives its account key from snapshot.accountEmail. That compiles independently, yet is not a sufficient stable discriminator: Claude OAuth accounts can be email-less, generic token accounts can fall back to mutable or duplicate labels, and Codex owners can share an email across workspace/source/auth contexts.

The refreshed disposable merge simulation produced exactly one conflicted file, UsageStore+QuotaWarnings.swift, with two adjacent regions. The minimal correct union is to keep #2001's dedicated hook key, notification-independent hook dispatch, and payload behavior while feeding this PR's accountContext.discriminator into dispatchQuotaLowHooks(accountKey:); accountContext.displayName remains presentation-only.

The combined tree passed 51 focused warning/hook tests across five suites, including a simulation-only regression proving two email-less Claude owners keep separate hook baselines while native notifications are disabled. It also passed make check and git diff --check. No published branch was changed.

Whichever PR lands second must carry that union and the email-less hook regression. If #2001 lands first, this PR will be rebased and the integration included before merge.

A recording was not available because the host was locked; the exact-head packaged-app runtime log is included above.

Fresh-bundle runtime proof

I also ran the freshly packaged current-head app, not the disposable harness:

head=be7f11c4ffdc5249a7409af5cd7c50b17eb66869
bundle_embedded_commit=be7f11c4
bundle_binary_sha256=6b4392eef2d9574a1b8929eceea8f12b97ae3f8a7bbaaa4bc582d8db9007b8a6
build=CODEXBAR_ALLOW_LLDB=1 ./Scripts/package_app.sh debug

The exact unmodified bundle ran with an isolated HOME/CODEX_HOME/config, both Keychain-access guards set, and only sub2api enabled against a loopback HTTP fixture. The two token accounts had synthetic labels and fixed UUIDs. This does not claim authenticated-provider behavior and did not read a real provider account, browser store, or Keychain item.

Because the host was locked, I drove the same production operations used by the segmented account switcher under LLDB: setActiveTokenAccountIndex, cached-account activation, and refreshProvider(.sub2api). Provider outcomes were not stubbed: the packaged app used its production Sub2API URLSession fetcher against the loopback server, then its production reducer, SessionQuotaNotifier, and QuotaWarningAlertOverlayController.

The redacted server trace was:

[FETCH 01] account=A used=40    # A baseline
[FETCH 02] account=B used=40    # B baseline
[CONTROL] account=A used=55
[FETCH 03] account=A used=55    # A crosses
[FETCH 04] account=B used=40    # B unchanged; no false alert
[FETCH 05] account=A used=55    # repeated low A; no duplicate
[CONTROL] account=B used=55
[FETCH 06] account=B used=55    # B crosses
[FETCH 07] account=B used=55    # repeated low B; no duplicate

The real notifier breakpoint was hit exactly twice:

[NOTIFIER 1] account=Proof Account A provider=sub2api window=session threshold=50 remaining=45
2026-07-14 04:53:54.265 [sessionQuotaNotifications] enqueuing (prefix=quota-warning-sub2api-session-50)
2026-07-14 04:53:54.277 [sessionQuotaNotifications] Presenting quota warning overlay

[NOTIFIER 2] account=Proof Account B provider=sub2api window=session threshold=50 remaining=45
2026-07-14 04:56:29.991 [sessionQuotaNotifications] enqueuing (prefix=quota-warning-sub2api-session-50)
2026-07-14 04:56:29.997 [sessionQuotaNotifications] Presenting quota warning overlay

breakpoint hit count = 2

The debug bundle was not authorized for Notification Center, so its native UNUserNotificationCenter posts were skipped; the enabled on-screen alert path did present twice. The final in-process state contained exactly two independent owner keys:

owner=token-account:11111111-1111-4111-8111-111111111111 window=session fired=50 remaining=45
owner=token-account:22222222-2222-4222-8222-222222222222 window=session fired=50 remaining=45
cached snapshots: Proof Account A=55%, Proof Account B=55%

The source worktree remained clean. The temporary server/app were stopped, the pre-run debug preference snapshot was restored exactly, and the installed release app was left running.

@vincent-peng
vincent-peng marked this pull request as ready for review July 13, 2026 15:22
@vincent-peng

vincent-peng commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

CI follow-up: macOS shard 2 failed only in the unchanged TTYCommandRunnerEnvTests test “sets working directory when provided”, which recorded timedOut after 1.135s.

Evidence:

The harness configures a 10s PTY timeout, while the runner also maps a short-lived command that leaves no captured output to timedOut; the 1.135s result is consistent with that pre-existing output-capture race, not the configured deadline.

I attempted a failed-only rerun through both gh run rerun and the Actions REST endpoint, but GitHub returns 403 because workflow reruns require repository admin rights. Could a maintainer please rerun the failed jobs? No branch change appears warranted for this unrelated runner-timing flake; a separate TTY hardening lane would make sense only if the exact failure repeats.

@clawsweeper

clawsweeper Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codex review: needs maintainer review before merge. Reviewed July 13, 2026, 4:03 PM ET / 20:03 UTC.

Summary
The PR scopes quota-warning reducer state by stable account ownership, forwards that ownership through ordinary and selected-account publication paths, and adds multi-account regression coverage.

Reproducibility: yes. The supplied base/head sequence reproduces duplicate warnings for account A and a suppressed warning for account B on the current base, while the exact-head fresh bundle produces one warning per synthetic account through production paths.

Review metrics: 3 noteworthy metrics.

  • Patch surface: 11 files, +464/-101. The fix spans warning-state keys, multiple publication paths, shared ownership helpers, and focused regression suites.
  • Focused validation: 86 tests across 5 suites. The reported run covers ordinary refresh, selected accounts, session transitions, Claude extra windows, and predictive warnings.
  • Runtime observations: 7 fetches, 2 notifier hits, 2 account keys. The exact-head fresh-bundle trace demonstrates independent episodes without duplicate or suppressed warnings.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

Risk before merge

Maintainer options:

  1. Decide the mitigation before merge
    Land the account-scoped reducer fix, then require the second-landed hooks branch to carry the documented stable-discriminator union and its email-less-account regression.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • [P2] The contributor-facing proof blocker is resolved; this PR now needs ordinary maintainer review and landing-order coordination rather than an automated repair.

Security
Cleared: The patch changes in-memory warning ownership and tests without adding dependencies, workflows, permissions, downloads, secret handling, or process-execution surfaces.

Review details

Best possible solution:

Land the account-scoped reducer fix, then require the second-landed hooks branch to carry the documented stable-discriminator union and its email-less-account regression.

Do we have a high-confidence way to reproduce the issue?

Yes. The supplied base/head sequence reproduces duplicate warnings for account A and a suppressed warning for account B on the current base, while the exact-head fresh bundle produces one warning per synthetic account through production paths.

Is this the best way to solve the issue?

Yes. Extending the existing reducer key with an opaque stable owner while keeping display identity separate is the narrowest maintainable fix and follows the repository's provider-silo and privacy guidance.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against c852c135e10c.

Label changes

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. Exact-head fresh-bundle logs show production account switching, refresh, URLSession fetching, reducer, notifier, and overlay behavior with two isolated synthetic accounts and one observed warning per account.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (logs): Exact-head fresh-bundle logs show production account switching, refresh, URLSession fetching, reducer, notifier, and overlay behavior with two isolated synthetic accounts and one observed warning per account.
  • remove status: 📣 needs proof: Current PR status label is status: 👀 ready for maintainer look.
  • remove rating: 🦪 silver shellfish: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.

Label justifications:

  • P2: The PR fixes cross-account quota-warning duplication and suppression with a bounded user-facing impact.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (logs): Exact-head fresh-bundle logs show production account switching, refresh, URLSession fetching, reducer, notifier, and overlay behavior with two isolated synthetic accounts and one observed warning per account.
  • proof: sufficient: Contributor real behavior proof is sufficient. Exact-head fresh-bundle logs show production account switching, refresh, URLSession fetching, reducer, notifier, and overlay behavior with two isolated synthetic accounts and one observed warning per account.
Evidence reviewed

What I checked:

Likely related people:

  • Alekstodo: Authored the merged configurable quota-warning implementation that created the reducer and notification behavior corrected here. (role: introduced behavior; confidence: high; commits: 0769b9e, 18eb73d; files: Sources/CodexBar/UsageStore.swift, Tests/CodexBarTests/UsageStoreSessionQuotaTransitionTests.swift)
  • steipete: Reviewed, adjusted, live-tested, and merged the original quota-warning feature and contributed to subsequent account-routing work. (role: merger and recent area contributor; confidence: high; commits: 18eb73d, 49ddb04, dca9c46; files: Sources/CodexBar/UsageStore.swift, Sources/CodexBar/UsageStore+TokenAccounts.swift, Sources/CodexBar/UsageStore+Refresh.swift)
  • ThiagoCAltoe: Authored the merged selected-Codex account refresh routing that this PR extends with quota-warning ownership. (role: account-routing feature owner; confidence: high; commits: bcf7bb0, 23ac4cf, 2af7f55; files: Sources/CodexBar/UsageStore+TokenAccounts.swift, Sources/CodexBar/UsageStore+Refresh.swift)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (2 earlier review cycles)
  • reviewed 2026-07-13T15:54:46.556Z sha be7f11c :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-13T18:08:52.266Z sha be7f11c :: needs real behavior proof before merge. :: none

@vincent-peng

Copy link
Copy Markdown
Contributor Author

Added the requested redacted current-head runtime proof to the PR body.

The identical disposable harness drives the production ordinary-refresh path with two synthetic selected accounts:

  • Base c852c135: emitted Account-A,Account-A — B's recovery re-armed A, and B's own crossing was suppressed.
  • Head be7f11c4: emitted Account-A,Account-B — each account retained one independent warning episode.

Both traces passed one focused test in one suite. The harness used isolated settings, stubbed outcomes, synthetic labels/tokens, and CODEXBAR_SUPPRESS_TEST_KEYCHAIN_ACCESS=1; it read no real provider, browser, account-store, or Keychain data and was removed afterward.

The separate CI failure remains the unchanged PTY harness timeout documented above; a maintainer rerun is still needed because contributor reruns return 403.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 13, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal priority bug or improvement with limited blast radius. labels Jul 13, 2026
@vincent-peng

Copy link
Copy Markdown
Contributor Author

Added the requested fresh-bundle runtime proof to the PR body for exact head be7f11c4.

  • Freshly packaged, unmodified debug app; embedded commit be7f11c4, binary SHA-256 6b4392eef2d9574a1b8929eceea8f12b97ae3f8a7bbaaa4bc582d8db9007b8a6.
  • The packaged app used the production account-selection, refreshProvider, Sub2API URLSession, reducer, notifier, and on-screen overlay paths against an isolated loopback fixture.
  • Seven fetches covered A/B baselines, A crossing, unchanged B, repeated-low A, B crossing, and repeated-low B.
  • The real notifier breakpoint hit exactly twice: Proof Account A, then Proof Account B; the overlay presented exactly twice. Repeated low readings produced no duplicate, and B was not suppressed.
  • Final reducer state had two independent fixed-UUID owner keys, each with threshold 50 fired.
  • No authenticated provider account, browser data, or Keychain item was read. Native Notification Center was not authorized for the debug bundle, so the proof claims the enabled on-screen alert plus runtime logs, not native-center delivery.
  • The proof app/server were stopped, the pre-run debug preferences were restored exactly, and both source worktrees remain clean.

@clawsweeper re-review

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 13, 2026
@steipete
steipete force-pushed the codex/account-scope-quota-warnings branch from be7f11c to 746b488 Compare July 14, 2026 10:21

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 746b488c38

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +57 to +60
return self.warningClaudeAccountDiscriminator(
strategyKind: result.strategyKind,
observation: context.claudeOAuthActiveAccountObservation,
oauthHistoryOwnerIdentifier: result.claudeOAuthHistoryOwnerIdentifier)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep Claude OAuth quota keys on the credential owner

For Claude OAuth results this new quota-warning path reuses warningClaudeAccountDiscriminator, which prefers the active .claude.json account identity and only falls back to claudeOAuthHistoryOwnerIdentifier when that metadata is unavailable. If a refresh first records a warning under claude-oauth-owner:<owner> while the active account file is absent/unreadable, and a later refresh for the same OAuth credential sees the active account identity, the key changes to claude-account:<identity>; because the reducer fires on a first observation already below the threshold, the same account can get a duplicate low-quota notification without crossing again. Use the OAuth owner identifier consistently for OAuth quota-warning state when it is present, or migrate the fallback key.

Useful? React with 👍 / 👎.

@steipete
steipete merged commit 7ccfd6c into steipete:main Jul 14, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants