Skip to content

fix(usage): render the quota panel again and restore reset countdowns - #4656

Merged
Yeachan-Heo merged 2 commits into
Yeachan-Heo:devfrom
kook-oh:fix/usage-multi-account-reset
Aug 18, 2026
Merged

fix(usage): render the quota panel again and restore reset countdowns#4656
Yeachan-Heo merged 2 commits into
Yeachan-Heo:devfrom
kook-oh:fix/usage-multi-account-reset

Conversation

@kook-oh

@kook-oh kook-oh commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #4661.

What

Makes packages/coding-agent/test/account-inventory-usage.test.ts pass regardless of what credentials exist on the machine running it. Test-only change; no product file touched.

  • Stub the hooks the synthetic-row path calls: peekCachedCredentialHealthForSource, recordCredentialHealthForSource, peekApiKey, checkApiKeyCredential.
  • Address the stored credential by identity (source === "stored" + provider) instead of rows[0].

Why

The suite fails 2/2 on dev tip 2bd7b4a48 with no changes applied:

TypeError: undefined is not an object (evaluating 'authStorage.peekCachedCredentialHealthForSource.call')
      at sourceHealth (packages/coding-agent/src/session/account-inventory.ts:332:68)
      at addSyntheticRows (packages/coding-agent/src/session/account-inventory.ts:463:12)
      at buildAccountInventorySnapshot (packages/coding-agent/src/session/account-inventory.ts:475:2)

providerSet() adds every provider from listProvidersWithEnvKey() whose key resolves — 62 providers are scanned — and each one produces a synthetic row whose sourceHealth() call the minimal stub cannot answer. Once the crash is out of the way, a second failure surfaces: rows[0] is the synthetic row, not the stored credential, so rows[0]?.usage is undefined.

Two details make this broader than an exported-variable problem, and correct the reproduction note on #4635:

  • env -i does not avoid it. $credentialEnv() resolves through $inheritedEnv → live credential store → ~/.gjc/agent/.env → piEnv → ~/.env → shell-rc parsing, so a cleared process environment still sees file-backed credentials.
  • Some resolvers never consult a variable. amazon-bedrock falls through AWS_BEARER_TOKEN_BEDROCK to hasResolvableAwsProfileSource(), so a plain ~/.aws/config + ~/.aws/credentials is enough. That is what triggers it on the machine where this was found — measured with listProvidersWithEnvKey().filter(getEnvApiKey) returning ["amazon-bedrock"] under both a normal shell and env -i.

CI stays green because runners carry no credentials and therefore build no synthetic rows, so the failure only ever reaches developer machines.

I first filed #4661 as a product defect and then, in a follow-up, attributed the trigger to a specific API-key variable. Both were wrong and are corrected on the issue. A type probe confirms peekCachedCredentialHealthForSource and checkApiKeyCredential are declared on the exported AuthStorage type, so the runtime contract is intact and the fault is the stub lying through as unknown as AuthStorage. Guarding the call sites in account-inventory.ts would mask genuinely missing methods on a real storage, so the fix belongs in the test.

Testing

bun test packages/coding-agent/test/account-inventory-usage.test.ts2 pass / 0 fail in three configurations:

  • with a resolvable ~/.aws profile present (the original failing case)
  • under env -i (cleared process environment)
  • with OPENAI_API_KEY and ANTHROPIC_API_KEY exported

bun --cwd=packages/coding-agent run check — clean (biome 2836 files + tsc --noEmit).

GJC verdict

gajae.pr-review-verdict.v1 merge-approved sha256:e3819dca7d508b2bf9818387c49f4417176c6a9be2946e92d1352687e72eef1f reviewer:human reviewer-id:Yeachan-Heo evidence:authenticated APPROVED review on exact head 55469dae4c8295d8bd4f2ae7ff96586e93dedb36 by Yeachan-Heo (maintainer, non-author; no prior approvals dismissed or reused); canonical digest matches body; hermetic bounded validation in the review comment

  • Target branch is dev
  • bun check passes (coding-agent package check on exact head 6b92e97c8)
  • Tested locally
  • CHANGELOG updated (if user-facing) — test-only, no user-facing change
  • Verdict above matches the exact PR head, not an earlier commit

@kook-oh kook-oh changed the title fix(tui): restore reset countdown and account identity in /usage fix(usage): render the quota panel again and restore reset countdowns Aug 18, 2026
@kook-oh
kook-oh force-pushed the fix/usage-multi-account-reset branch from 46525ad to 337c46e Compare August 18, 2026 06:48
@kook-oh

kook-oh commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto dev 9f29ee590 after #4635 merged (50b032e9b). Head is now 337c46e72; conflicts resolved and mergeable: MERGEABLE.

Conflict: CHANGELOG [Unreleased] sibling ordering only — #4635's entry and this PR's entry, both kept, no other file touched by the rebase. Code and tests are unchanged from the pre-rebase head.

Interaction with #4635 — checked, and it strengthens this PR: collectCachedUsageReports() reads row.usage.report from buildAccountInventorySnapshot(), which #4635 taught to resolve the provider base URL. Before that fix the baseUrl-less key hit the default bucket and lost rows, so the panel would have rendered a subset of accounts. Redaction is also compatible: redactUsageWindow() preserves resetsAt (:238) and redactUsageReport() allowlists email (:289), so per-account labels and reset lines survive the inventory path intact.

Validation on 337c46e72

  • bun test packages/coding-agent/test/usage-report-columns.test.ts packages/coding-agent/test/status-line-usage.test.ts15 pass / 0 fail
  • bun --cwd=packages/coding-agent run check — clean (biome 2836 files + tsc --noEmit)

Pre-existing failure, not from this branch: account-inventory-usage.test.ts (added by #4635) fails 2/2 on dev tip 9f29ee590 with no changes applied, on any machine exporting a provider key — here AI_HUB_API_KEY, with OPENAI_API_KEY unset. The proximate cause is an unguarded optional-hook call in session/account-inventory.ts, so it is a product defect rather than the operator-env contamination #4635 recorded. Filed separately as #4661 with the full trace and a suggested shape; kept out of this PR to avoid reaching into that feature's files.

@kook-oh

kook-oh commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my earlier comment on this PR, body updated to match: I described the pre-existing account-inventory-usage.test.ts failure as a product defect triggered by an exported API-key variable. Both halves were wrong.

It is test hermeticity — the hooks are declared on the exported AuthStorage type, so the stub is what lies. And the trigger is not an exported variable: $credentialEnv() falls back to ~/.gjc/agent/.env, ~/.env, and shell-rc parsing, while amazon-bedrock resolves through ~/.aws with no variable at all, so env -i reproduces it identically. #4661 is rewritten around the measured evidence and #4663 carries the fix.

Nothing here changes this PR: its files, tests, and results are unaffected — the note only clarified why an unrelated suite is red on developer machines while green in CI. Head remains 337c46e72; usage-report-columns + status-line-usage still 15 pass / 0 fail, and Affected path validation / native-build has since passed on the re-run.

오승국 added 2 commits August 18, 2026 18:18
The multi-account usage panel dropped the `resets in …` line entirely:
it was gated on a window having a single limit, so the moment a second
credential appeared the only reset signal left was a parenthetical that
competed with the account label for column width. Coarse single-unit
durations made it worse -- a weekly window read `7d` whether 6.6 or 7.4
days remained -- and per-account labels right-truncated into identical
stubs, so the panel could not answer either question it exists for:
when does my quota come back, and for which account.

Always resolve the reset range, render it with hour precision plus an
absolute local reset time, and fit account labels as a set so the
columns stay mutually distinguishable.

Lore-id: 7c1a9f3e
Constraint: keep formatDuration untouched -- job/elapsed rendering shares it
Rejected: widen columns until labels fit | wraps or overflows narrow terminals
Rejected: drop the reset suffix from the header row | loses per-account skew
Confidence: high
Scope-risk: narrow
Reversibility: safe
Tested: multi-account reset line, divergent/identical reset windows, 48h+ precision, label uniqueness at 80 cols
Not-tested: live provider fetch against real credentials
Canonicalizing multi-account management (364f140) rewired the
interactive /usage handler from the graphical panel to the account
inventory text view. That view prints only `label: N% used (M% left)`,
so bars, per-account columns, and every reset signal disappeared from
the command whose entire job is answering "how much is left, and when
does it come back" -- and handleUsageCommand/renderUsageReports became
unreachable code that no surface could reach.

Plain /usage renders the panel again, fed by the same cache-only
inventory snapshot the text view reads, so the cache-only contract that
motivated the rewire is preserved and no fetch or probe returns. `/usage
check` stays on the text path, where the per-credential health verdict
is the point. Account rows on every surface now carry the reset
countdown and its absolute time.

Lore-id: 7c1a9f3e
Constraint: plain /usage must remain cache-only -- no fetch, no probe
Constraint: keep formatDuration untouched -- job/elapsed rendering shares it
Rejected: add resets to the text rows only | leaves the panel dead and the bars gone
Rejected: revive the panel via session.fetchUsageReports | reintroduces the network call 364f140 removed
Confidence: high
Scope-risk: narrow
Reversibility: safe
Tested: panel reset lines, divergent/identical windows, 48h+ precision, label uniqueness at 80 cols, text-row reset detail incl. expired/absent windows
Not-tested: live provider fetch against real credentials
@kook-oh
kook-oh force-pushed the fix/usage-multi-account-reset branch from 337c46e to 55469da Compare August 18, 2026 09:20

@Yeachan-Heo Yeachan-Heo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Adversarial exact-head review — APPROVE on 55469dae4c8295d8bd4f2ae7ff96586e93dedb36

Reviewer: Yeachan-Heo (maintainer; independent of PR author kook-oh). Base 2bd7b4a48c (immutable event base, current origin/dev). Canonical source digest e3819dca7d508b2bf9818387c49f4417176c6a9be2946e92d1352687e72eef1f — recomputed locally with git diff --binary --full-index --no-ext-diff 2bd7b4a48c...55469dae4c, byte-identical to the verdict line in the PR body.

Authorship and reconstruction

  • Both commits authored and committed by 오승국 <kook@oseung-gug-ui-MacBookAir-2.local> (kook-oh), original author dates 2026-08-18 preserved. The contributor performed their own rebase onto 9f29ee590337c46e7255469dae4c; no maintainer force-push was needed on this branch.
  • PR code/test paths are byte-identical to the original head 46525ad98a (path-scoped diff over command-controller.ts, builtin-registry.ts, usage-report.ts, usage-report-columns.test.ts is empty); the only per-head delta vs 27afb732b3 is CHANGELOG [Unreleased] sibling ordering, both entries kept.

Semantics — checked against merged #4635 and in-flight #4663

  1. Cache-only contract preserved. collectCachedUsageReports() reads row.usage.report out of buildAccountInventorySnapshot() — the same redacted snapshot the text view reads. No fetch, no probe, no session.fetchUsageReports. redactUsageWindow() at account-inventory.ts:238 preserves resetsAt, and redactUsageReport() allowlists email, so both the per-account labels and the reset lines survive the inventory path intact. Interaction with #4635 is complementary: #4635 taught the snapshot to resolve the provider base URL (without it this panel would render a subset of accounts); this PR renders resets.
  2. /usage check stays text. Plain /usage → panel when cached reports exist, text fallback otherwise; check always goes through buildUsageReportText({check:true}) with the health verdicts. Both surfaces now carry resets in <countdown> (<absolute time>) via formatLimitDetail().
  3. Reset range fix is real. The old gate sortedLimits.length <= 1 ? resolveResetRange(...) : null suppressed the only reset signal exactly when multiple credentials existed. Now always resolved; divergent resets render min–max (first <abs>), identical resets one value. Expired/window-less limits emit no reset text (filtered by value > nowMs / early return).
  4. Two-unit countdowns are local. formatResetCountdown/formatLimitReset/formatResetAt are new local helpers; formatDuration is untouched, so job/elapsed rendering keeps its coarse label. formatResetShort (status-line path) unchanged.
  5. Label set-fitting is monotone and terminating. truncateAccountLabels tries full → local-part → head-truncated → middle-squeezed, keeping the first mutually-unique variant; the #N fallback guarantees uniqueness even when all representations collide. Padding uses Math.max(0, ...) so narrow terminals can't produce negative repeats (the pre-existing code would have).
  6. Panel is reachable again, dead code is not accumulated. handleUsageCommand/renderUsageReports regain their only interactive caller; the fallback text path remains for empty snapshots.

Pre-existing red, correctly out of scope

account-inventory-usage.test.ts (from #4635) fails 2/2 on this machine and on clean origin/dev — test hermeticity against developer-shell credential resolution, tracked as #4661 with the fix in #4663. This PR does not touch account-inventory.ts and correctly declines to reach into that feature's files.

Verification on this exact head (hermetic, local)

  • bun test ./packages/coding-agent/test/usage-report-columns.test.ts ./packages/coding-agent/test/status-line-usage.test.ts15 pass / 0 fail; adding session-manager/usage-statistics18 pass / 0 fail
  • bun --cwd=packages/coding-agent run check — clean (biome 2837 files, tsc --noEmit)
  • bun scripts/verify-gjc-state-writers.ts --fail --root . — 0 write sites outside the sanctioned writer
  • bun scripts/changelog-history-guard.ts — no released sections removed
  • git diff --check 2bd7b4a48c 55469dae4c — clean
  • CLI smoke from source: --versiongjc/0.14.0; accounts list --json → valid inventory JSON
  • Canonical binaries: dev 2bd7b4a48c94653606…, head 55469dae4c5dcb965a6a2ed3476fdc9e5d31f6ef842fae35be449831318df000809ee027a1; head binary --version/--help/--smoke-test (smoke-test: ok) all green
  • Product CI on this exact head: 15 success / 6 skipped — including native-build, check:@gajae-code/coding-agent, cli-smoke, ts-build, targeted usage-report-columns shard, all five gjc-state-gates shards, evidence producer, Virtual integration, Public site sync. The only reds are the two PR-contract jobs, whose sole error is Verdict needs-human intentionally blocks merge — the designed gate.

No blockers. Approving this exact head.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo
Yeachan-Heo merged commit b01f345 into Yeachan-Heo:dev Aug 18, 2026
74 of 87 checks passed
@Yeachan-Heo

Copy link
Copy Markdown
Owner

Terminal disposition — MERGED to dev

PR #4656 fix(usage): render the quota panel again and restore reset countdowns was squash-merged into dev as b01f34527c703a33b77edec91d5d36c215134cf1 by Yeachan-Heo at 2026-08-18T12:28:59Z (parent 62b5688d92, #4664).

Exact-head merge basis (head 55469dae4c8295d8bd4f2ae7ff96586e93dedb36, base 2bd7b4a48c)

  • Canonical source digest e3819dca7d508b2bf9818387c49f4417176c6a9be2946e92d1352687e72eef1f — validated by PR-contract runs 32129444793 (PR contract valid: merge-approved e3819dca…) and earlier 32127648250; both re-verified the single-body-verdict, exact-head digest, immutable-base ancestry, and the authenticated non-author approval (review id 4960023897, Yeachan-Heo APPROVED @ 55469dae4c).
  • Product Dev CI run 32129444270 on the exact head: terminal success — PR contract bootstrap, all five gjc-state-gates shards, native-build, check:@gajae-code/coding-agent, ts-build, cli-smoke, targeted usage-report-columns shard, evidence producer, Virtual integration, Public site sync.
  • Two intermediate contract failures (32127346075 attempt-reruns, 32129344332) were stale-event metadata: the pull_request_review event carried a pre-edit body snapshot (and one cross-PR body-settlement race read test(usage): make account-inventory usage tests hermetic #4663's digest d62d353a). Not rerun; superseded by the two green pull_request_target runs above. A maintainer-side body-write race during lane reconciliation briefly overwrote this body with test(usage): make account-inventory usage tests hermetic #4663's text and was fully restored with the exact merge-approved verdict — verified post-restore by contract run 32129444793.
  • Contributor authorship preserved end-to-end: 오승국 <kook@oseung-gug-ui-MacBookAir-2.local> (kook-oh) authored both commits; all rebases onto dev (9f29ee590337c46e7255469dae4c) were performed by the contributor. No maintainer force-push to this branch was ever needed or made.
  • Verification carried in the approval review: 15/15 (+18/18 with session-manager usage stats) focused tests, coding-agent check clean, state-writer gate 0 violations, changelog-history-guard clean, git diff --check clean, canonical head binary 5dcb965a6a2ed3476fdc9e5d31f6ef842fae35be449831318df000809ee027a1 with --version/--help/--smoke-test green.

Postmerge dogfood on fresh dev (b01f34527c)

  • Fast-forward verified; bun run build — exit 0 (natives + coding-agent compile, dist/gjc produced).
  • Fresh binary dogfood: --versiongjc/0.14.0; --smoke-testsmoke-test: ok; accounts list --json → valid inventory JSON. Fresh-dev binary digest 51573d0b127ce735bb9dff93aa8007599d5d52aa19840da4581182a47ba42b5a.
  • usage-report-columns + status-line-usage on fresh dev: 15 pass / 0 fail.

Postmerge CI reconciliation

  • The Dev CI run at the merge commit (32137058220) was cancelled mid-flight after one unrelated flake: sdk-slack-daemon.test.ts (2 cases; 69/69 pass locally on the same tree; zero file overlap with this PR).
  • Superseded by green Dev CI on every later dev tip containing this merge — 9378f05351 (run 32140406661: all 8 test shards + evidence producer + affected-path aggregate success), ded067a372 (32143538804 success), and current tip ceb31349c2 (32146403507 all-green). Public site sync green throughout.

Linked issues

None open against this PR. #4634 was already closed by #4635 before this lane began; the pre-existing account-inventory-usage.test.ts red on developer machines is owned by open #4663/#4661 and untouched by this merge. No release, tag, or publish performed.

Lane retired. Terminal state: dev merge complete.


[repo owner's gaebal-gajae (clawdbot) 🦞]

pull Bot pushed a commit to nenyatech-mirror/gajae-code that referenced this pull request Aug 18, 2026
…Yeachan-Heo#4656)

* fix(tui): restore reset countdown and account identity in /usage

The multi-account usage panel dropped the `resets in …` line entirely:
it was gated on a window having a single limit, so the moment a second
credential appeared the only reset signal left was a parenthetical that
competed with the account label for column width. Coarse single-unit
durations made it worse -- a weekly window read `7d` whether 6.6 or 7.4
days remained -- and per-account labels right-truncated into identical
stubs, so the panel could not answer either question it exists for:
when does my quota come back, and for which account.

Always resolve the reset range, render it with hour precision plus an
absolute local reset time, and fit account labels as a set so the
columns stay mutually distinguishable.

Lore-id: 7c1a9f3e
Constraint: keep formatDuration untouched -- job/elapsed rendering shares it
Rejected: widen columns until labels fit | wraps or overflows narrow terminals
Rejected: drop the reset suffix from the header row | loses per-account skew
Confidence: high
Scope-risk: narrow
Reversibility: safe
Tested: multi-account reset line, divergent/identical reset windows, 48h+ precision, label uniqueness at 80 cols
Not-tested: live provider fetch against real credentials

* fix(usage): render the quota panel again and restore reset countdowns

Canonicalizing multi-account management (364f140) rewired the
interactive /usage handler from the graphical panel to the account
inventory text view. That view prints only `label: N% used (M% left)`,
so bars, per-account columns, and every reset signal disappeared from
the command whose entire job is answering "how much is left, and when
does it come back" -- and handleUsageCommand/renderUsageReports became
unreachable code that no surface could reach.

Plain /usage renders the panel again, fed by the same cache-only
inventory snapshot the text view reads, so the cache-only contract that
motivated the rewire is preserved and no fetch or probe returns. `/usage
check` stays on the text path, where the per-credential health verdict
is the point. Account rows on every surface now carry the reset
countdown and its absolute time.

Lore-id: 7c1a9f3e
Constraint: plain /usage must remain cache-only -- no fetch, no probe
Constraint: keep formatDuration untouched -- job/elapsed rendering shares it
Rejected: add resets to the text rows only | leaves the panel dead and the bars gone
Rejected: revive the panel via session.fetchUsageReports | reintroduces the network call 364f140 removed
Confidence: high
Scope-risk: narrow
Reversibility: safe
Tested: panel reset lines, divergent/identical windows, 48h+ precision, label uniqueness at 80 cols, text-row reset detail incl. expired/absent windows
Not-tested: live provider fetch against real credentials

---------

Co-authored-by: 오승국 <kook@oseung-gug-ui-MacBookAir-2.local>
(cherry picked from commit b01f345)
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