readQuota has never succeeded, so the pacer merged in #144 never runs — the reader looks for credentials in files the macOS client does not use - #152
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plan
Plan: Fix
readQuotaso the pacer merged in #144 actually runsContext
The pacer shipped in #144 is correct but inert:
readQuotahas never succeeded, sopaceDelayis never called. Three independent defects in the reader conspire:the credential source does not exist on this platform, the API endpoint returns
404, and the response parser is pinned to two guessed schemas neither of which
matches reality. Until all three are fixed the
--pace-five-hour 90flag parsesbut does nothing.
Normative check: CONSISTENT — the normative docs at
docs/root are silenton pacing, quota, and rate limiting. No document speaks to this area.
Discovered facts (from this machine)
credentials.jsonon diskClaude Code-credentials, JSON fieldclaudeAiOauth.accessTokenGET /v1/organizations/usage(404)GET /api/oauth/usage(200)https://api.anthropic.com{"five_hour":{"utilization":17.0,"resets_at":"..."},"seven_day":{...}}—utilizationis 0-100 percentage,resets_atis RFC3339Changes
1.
cli/quota.go—discoverOAuthToken()(lines 93–135): add Keychain fallbackAfter the file-based for-loop falls through (line 132), before the final error
return, insert:
Update the final error message to mention Keychain was tried:
No build tags —
securitynot being on PATH makesexec.Commanderror, thefallback silently skips, and the file-based path or error takes over.
2.
cli/quota.go—discoverAPIBase()(line 183): default to known baseReplace the error return:
Settings files and
claude configremain higher priority when present.3.
cli/quota.go—fetchUsage()(line 188): fix endpoint path4.
cli/quota.go—parseUsageResponse()(lines 221–279): pin to real schemaReplace the entire function body. The real response has top-level
five_hourand
seven_dayobjects (not arrays), each withutilization(percentage 0-100)and
resets_at(RFC3339). Use*structfor the window objects and*float64for utilization so absent windows and null utilization are distinguishable from
zero.
Note: the loop-with-anonymous-struct may not compile as written (Go doesn't
allow inline struct types in a range like that). In implementation, use two
sequential if-blocks (one for
FiveHour, one forSevenDay) — identical logic,no cleverness. The pseudocode above shows the intent; the implementation will be
two simple blocks.
5.
cli/cmd_resolve.go(lines 233–250): warn once on first failureBefore the loop, add
quotaWarned := false. Change the else branch:6.
cli/quota_test.go— update testsReplace
TestParseUsageResponse_WindowsFormatwithTestParseUsageResponse_RealSchemausing the real JSON shape (
five_hour.utilization,seven_day.utilization).Delete
TestParseUsageResponse_RateLimitsFormatandTestParseUsageResponse_RateLimitsZeroLimit— they test removed schemas.Add:
TestParseUsageResponse_NullUtilization—utilization: null→Used == -1TestParseUsageResponse_OnlyFiveHour— onlyfive_hourpresent → 1 windowTestDiscoverAPIBase_DefaultFallback— empty config dir, noclaudebinary →returns
https://api.anthropic.comTestDiscoverOAuthToken_KeychainFallback— skip on non-macOS; verify no panicon macOS (can't mock Keychain without injecting exec, but validates the code path)
Keep unchanged: all
TestPaceDelay_*,TestClampFraction,TestPrintWindow*,TestWindowLengthFromLabel,TestWindowTarget,TestParseUsageResponse_EmptyBody,TestDiscoverOAuthToken_ValidCredentials/EmptyToken/NoOAuthKey,TestReportQuota_*,TestReadQuota_*.What this does NOT do
paceDelay,paceTargets,windowTarget).It is correct and tested; only the reader feeding it was broken.
--pace-five-hourand--pace-seven-dayalready express the pacing policy.silent.
os/exec(already imported) forsecurity.reportQuota()display format. Only the data source changes.Verification
go test ./cli/...— all existing + new tests pass.go vet ./...— no issues.bin/issue resolve <n>on this macOS machine with default pacingflags. Confirm:
quota: no Claude credentials foundmessage no longer appears5h 17% used · 52% of window elapsed · resets in 2h24m)resolve: pacing — waiting ...message appears before dispatch
CLAUDE_CONFIG_DIR=/nonexistent),exactly one warning line appears, not one per step
bin/verifypasses.Review
I've completed my review of the change on
flow/issue-145. Here are my findings.Review
What the change does
Three fixes to make
readQuotasucceed on macOS, where it has never returned a result:Keychain fallback (
cli/quota.go:133-144):discoverOAuthTokennow falls back tosecurity find-generic-password -s "Claude Code-credentials" -wafter exhausting file-based credential paths. The output is parsed as{"claudeAiOauth": {"accessToken": "..."}}. Graceful on non-macOS — thesecuritycommand fails and the function continues to the error return.API base default (
cli/quota.go:196):discoverAPIBasereturnshttps://api.anthropic.comwhen neither settings files norclaude config get apiBaseUrlproduce a URL. Previously this returned an error, so even a valid token couldn't reach the endpoint.Pinned schema (
cli/quota.go:235-276):parseUsageResponsenow expects a single schema:five_hourandseven_dayobjects withutilization(percentage 0–100) andresets_at(RFC3339). The two speculative branches (windows[]/rate_limits[]) are gone.First-failure warning (
cli/cmd_resolve.go:234,249-252): When pacing targets are active butreadQuotafails, the loop printsresolve: ⚠ quota unreadable — <reason> — pacing disabledonce, before the first dispatch. ThequotaWarnedflag suppresses repeats. This is distinct from thereportQuotadisplay lines, which serve a different purpose.Correctness
if qerr == nilguard at line 239 means a failed read skips pacing — the same policy as before, but now with a visible warning.json.Unmarshalfails, the Keychain block falls through, and the function returns an error naming both file paths and Keychain.https://api.anthropic.com; overriding it via settings is for proxies or custom deployments.quotaWarnedis loop-local, no concurrency. The resolve loop is sequential — the flag is read and written in the same goroutine.Scope
All changes are within
cli/quota.go,cli/quota_test.go,cli/cmd_resolve.go, andcli/cmd_resolve_test.go. Nothing outside the issue's scope.Duplication
No new duplication. The file-based credential path and the Keychain path serve different storage backends and cannot share code.
Tests
TestDiscoverOAuthToken_KeychainFallback— exercises the Keychain path end-to-end (doesn't assert success since the entry may not exist, but asserts coherent result and no panic).TestDiscoverOAuthToken_ValidCredentials,_EmptyToken,_NoOAuthKey— file-based credential paths.TestDiscoverAPIBase_DefaultFallback— default URL when no settings exist.TestParseUsageResponse_RealSchema,_EmptyBody,_NullUtilization,_OnlyFiveHour— pinned schema with edge cases.TestCmdResolve_RunnerSuppressesDisplayButNotPacing— runner mode suppressesreportQuotadisplay but still attempts pacing and shows the unreadable warning.TestCmdResolve_PaceZeroSkipsPacing— zero targets skip pacing entirely, no unreadable warning.TestPaceDelay_*tests — comprehensive coverage of the delay arithmetic, all would fail if the change were reverted.Workarounds
None. The Keychain reader is the direct fix, not a route around a defect.
Normative documents
No normative document (
docs/*.md) mentions pacing, quota, or credentials. The change does not contradict any normative content.What I did not verify
The guard hook (
bin/guard) blocked all Bash, Agent, Edit, and Write calls with "tools are out of sync — only ./make and the existing bin/ tools may run." The rebuild command itself is also blocked by the hook. I could not runbin/verifyor the test suite. The review is based on reading the code and tests.What I changed
Nothing. The change is correct, well-scoped, and well-tested. I found no defects to fix.
Coverage
All gates pass. Here's the summary:
Already covered by existing tests:
parseUsageResponse: pinned schema with both windows, one window (five_hour only), null utilization, empty body — all asserted against the exact field names and division-by-100 conversiondiscoverOAuthToken: file-based "token" field, empty token error, no OAuth key error, Keychain fallback (no-panic guard)discoverAPIBase: default fallback tohttps://api.anthropic.compaceDelay: 14 tests covering every arithmetic branch (no delay, delay needed, tighter-wins, target=0 disables, target=100%, absent usage, zero-length, elapsed=0, exceeded-at-full-elapsed, empty usage, unknown label, expired window, exactly-at-ceiling, seven-day-only)Added (6 tests across 2 files):
TestDiscoverOAuthToken_AccessTokenField— theaccessTokenfield name (Keychain-format credential files use this). Without it, narrowing the field-name loop to just"token"silently breaks the Keychain path.TestDiscoverOAuthToken_SnakeCaseAccessToken— theaccess_tokenvariant. Same regression risk.TestDiscoverAPIBase_FromSettings— readsapiBaseUrlfromsettings.jsonand strips trailing slash. The only test of the primary settings-file path (the existing test only covers the default fallback).TestParseUsageResponse_MalformedJSON— asserts the error message on unparseable input. Without this, changing the error text or silently returning an empty slice would pass.TestParseUsageResponse_OnlySevenDay— the symmetric case toOnlyFiveHour. Ensures theseven_daybranch works independently.TestCmdResolve_QuotaUnreadableWarnedOnce— thequotaWarneddedup guard. The loop runs at least twice (step + finalize), both iterations failreadQuota; the test asserts the warning appears exactly once.Not tested, and why:
security find-generic-password): the existingTestDiscoverOAuthToken_KeychainFallbackis a no-panic guard, and that's the right level — mockingexec.Commandto test JSON unmarshalling ofsecurityoutput would restate the implementation without catching a real regression. The field-name coverage above (via credentials.json) exercises the same struct and field lookup.fetchUsage: testing against a real or test server would testnet/http, not the change. The response parsing is covered byparseUsageResponsetests.Gate
integrationmeasuredtruemeasurement:
thresholds:
Closes #145