Repository navigation
[low] fix(usage): only clear the in-flight lock this render wrote - #624
Merged
Merged
Conversation
A successful fetch removed usage.lock unconditionally. When two renders fetch concurrently and one receives a 429, the other's later success deleted the Retry-After lock, so the next render after the 180s cache expiry fetched again inside the server's backoff window. Clear the lock only while it still holds this render's own in-flight record. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This was referenced Sep 30, 2026
This was referenced Oct 10, 2026
Closed
pcvelz
added a commit
to pcvelz/ccstatusline-usage
that referenced
this pull request
Oct 11, 2026
Upstream: label prefixes editable with e (sirmalloc#601, sirmalloc#666), Session Cost Rate (sirmalloc#671) and Extra Usage Daily Budget (sirmalloc#672) widgets, own-lock-only usage lock clearing (sirmalloc#624), NO_PROXY and request deadlines, terminal control-sequence sanitizing, git hardening and linear-time parsing, shared searchable option editor (sirmalloc#649) and widget base classes (sirmalloc#646-sirmalloc#654), node: imports (sirmalloc#657), Number.* globals lint (sirmalloc#645), test waits on redraw instead of fixed delays (sirmalloc#652). Hand edits outside conflicts: - src/widgets/shared/searchable-option-editor.tsx: compat fix - ports the fork's ref-based input handling (Enter picks from what was just typed) into upstream's new shared editor - src/utils/usage-fetch.ts: compat fix - fork's fetchFreshUsageData uses upstream's clearOwnUsageLock (sirmalloc#624) instead of the removed clearUsageLock - src/widgets/ApiUsage.tsx, src/widgets/Battery.ts: compat fix - Number.isNaN/Number.parseInt for upstream's no-restricted-globals rule - src/utils/__tests__/usage-prefetch.test.ts: compat fix - mock renamed to upstream's mockFetchUsage - src/tui/components/__tests__/PowerlineSetup.test.ts, src/tui/components/__tests__/StatusLinePreview.test.ts, src/tui/components/__tests__/TerminalWidthMenu.test.ts: compat fix - drop the fork's flushInk import, unused after upstream's waitFor switch - README.md: upstream notes linked docs/USAGE.md (deleted in the fork); inlined the proxy and workspace-trust text instead; upstream's own release-note entries not taken - .fork-keep-deleted: src/widgets/FableWeeklyUsage.ts (upstream widget, unregistered in the fork, which ships WeeklyFableUsage.ts) - src/widgets/WeeklyFableUsage.ts: compat fix - adopts upstream's shared getUsagePercentWidgetEditorDisplay, which carries the time-cursor modifier - src/tui/__tests__/App.test.ts: fork deviation - one extra down-arrow for the fork's Off Hours main-menu row - src/widgets/__tests__/CacheTimer.test.ts, src/widgets/CacheTimer.ts: compat fix - emoji toggle moved from e to o because upstream reserves e for edit-label - src/widgets/AgentAddress.ts: split the session-title guard into its own if (same behaviour) so the .fork-invariants nameSource guard matches again - docs/test-retirements.md: ledger for tests upstream renamed or replaced
axisrow
added a commit
to axisrow/ccstatusline
that referenced
this pull request
Oct 12, 2026
Upstream 0bcc7eb (sirmalloc#624) makes a successful fetch clear usage.lock only when it still holds that render's own in-flight record: previously a success unconditionally removed the lock, so with two concurrent renders and one 429 the other's later success deleted the rate-limit lock and the next render fetched inside the server's backoff window. Upstream CI only runs Linux; this proves the fix holds on macOS. Red: against the pre-fix tree (475e401) the probe's rate-limit lock (written by a "concurrent render" mid-request) is gone after the observed success — lockExistsAfter expected true, received false. Green: bun test 1/1 and vitest/Node 5/5 (three batch files). Mutation: usage-fetch.ts is exercised through the subprocess harness, whose coverage the mutation runner cannot see (the probe is a child process), so the fix lines report as uncovered rather than surviving; the red/green pair above is the regression proof for this fix. Upstream commit: sirmalloc@0bcc7eb Co-Authored-By: Claude Code <[email protected]>
axisrow
added a commit
to axisrow/ccstatusline
that referenced
this pull request
Oct 12, 2026
…xes on macOS + isolate the mutation sandbox (#58) * fix(verify): isolate the mutation sandbox from the real home directory Running Stryker over config.ts/claude-settings.ts/powerline.ts executed upstream's claude-settings.test.ts, whose install tests reset the config path to the real ~/.config/ccstatusline/settings.json and, after one test deletes CLAUDE_CONFIG_DIR without restoring it, write the real ~/.claude/settings.json too. A mutant that broke config validation materialised default settings over the live statusline config, and an install test pointed statusLine at a client inside .stryker-tmp — which Stryker deletes afterwards, so the Claude Code status line went empty (upstream issue sirmalloc#716). globalSetup now redirects HOME and CLAUDE_CONFIG_DIR to a fresh temp directory for every sandboxed run. Verified by rerunning the same mutation scope with mtime guards on both real config files: stryker exit 0, both untouched ("BOTH-REAL-CONFIGS-UNTOUCHED"). Co-Authored-By: Claude Code <[email protected]> * test(settings): reproduce upstream 4c62e31 on macOS Upstream 4c62e31 keeps private settings private when saving and backing up: the atomic-save temp file inherits the replaced file's permissions (a 0600 settings came back 0644), and the .bak/.orig backups of Claude Code's settings.json get the settings' permissions instead of the umask default. Upstream CI only runs Linux; this proves the fix holds on macOS. Red: against the pre-fix tree (75f07d6) both cases fail — a 0600 ccstatusline settings comes back 0644 after a save, and the .orig backup of a 0600 Claude settings is 0644. Green: bun test 4/4 and vitest/Node 4/4 (three upstream-repro files). Mutation (StrykerJS over config.ts/claude-settings.ts/powerline.ts): the backup-mode lines' survivors are killed by covering the .bak existence after a save, the recreation of an existing .orig with loose permissions (writeFile alone keeps 0777), and a re-install over an existing status line. Remaining survivors on fix lines are equivalent: writeFile with encoding '' writes the same utf-8 bytes, and readFile returning a Buffer rounds-trips identically. Note: this file family runs installs against real home directories in upstream's claude-settings.test.ts (issue sirmalloc#716); the mutation sandbox is isolated from the real home by the preceding commit's globalSetup. Upstream commit: sirmalloc@4c62e31 Co-Authored-By: Claude Code <[email protected]> * test(install): reproduce upstream 475e401 on macOS Upstream 475e401 (sirmalloc#627) makes installStatusLine abort when the existing Claude settings cannot be read or parsed, instead of overwriting them with a settings.json holding only statusLine (permissions, hooks, env lost). Upstream CI only runs Linux; this proves the fix holds on macOS. Red: against the pre-fix tree (baf37e1) the install promise resolves (the overwrite happens) where the fixed code rejects. Green: bun test 3/3 and vitest/Node 8/8 (three upstream-repro files). Mutation: the abort path's survivors are killed by asserting the exact abort reason ("left unchanged") and covering both keepExisting branches (keep-existing over an unreadable file, keep-existing over a valid statusLine). Remaining survivors on fix lines are equivalent (logErrors: false → true only changes console noise; a truncated message tail). Upstream commit: sirmalloc@475e401 Co-Authored-By: Claude Code <[email protected]> * test(powerline): reproduce upstream 6aefa6b on macOS Upstream 6aefa6b downloads Powerline fonts into an mkdtemp folder — a random name created 0700 — instead of the predictable os.tmpdir()/ccstatusline-powerline-fonts-<timestamp>, which another account on a shared /tmp could create first and repopulate with its own install.sh. Upstream CI only runs Linux; this proves the fix holds on macOS. Red: against the pre-fix tree (9246095) the observed download folder is the predictable timestamp name (ccstatusline-powerline-fonts-<13 digits>), failing the random-name assertion. The mocked clone recreates what git clone would, so the folder's name and permissions are observed mid-install (the installer removes the folder in its finally). Green: bun test 1/1 and vitest/Node 8/8 (three upstream-repro files). Mutation: the mkdtemp line has zero surviving mutants in the batch scope (powerline.ts:19x). The file's broader low score comes from code paths this fix does not touch (font directory selection, install script variants), outside the fix-line criterion. Upstream commit: sirmalloc@6aefa6b Co-Authored-By: Claude Code <[email protected]> * test(usage): shared subprocess probe harness for usage-fetch tests The usage fetch code paths (proxy selection, lock handling, request deadlines) can only be exercised in a child process: bun's os.homedir() ignores HOME, so in-process tests would read and write the live ~/.cache/ccstatusline, and node:https is not patchable through bun's ESM namespace. The harness bundles a probe script with bun (extensionless imports resolve) and runs it with node, whose homedir() honors HOME; the https.request spy is installed by a --require patch that runs before the bundle (a CJS-require mutation alone would not reach the bundle's ESM import bindings). Subprocesses go through Bun.spawnSync under bun (sibling suites mock node:child_process and that mock leaks across files in the shared runner); vitest/Node keeps file isolation and takes the real spawnSync. Co-Authored-By: Claude Code <[email protected]> * test(usage): reproduce upstream e8ef3cb on macOS Upstream e8ef3cb connects usage and status-page requests directly to hosts NO_PROXY excludes: the usage request carries the account's OAuth token, and a user keeping api.anthropic.com off a proxy (for example a TLS-inspecting corporate one) had it sent through the proxy anyway. Upstream CI only runs Linux; this proves the fix holds on macOS. Red: against the pre-fix tree (4c62e31) the probe observes a proxy agent configured for the request even though NO_PROXY excludes the host (both spellings covered); the control case without an exclusion still sees the agent. The probe runs in a subprocess (usage-probe.ts harness). Green: bun test 3/3 and vitest/Node 5/5 (three batch files). Mutation (StrykerJS over no-proxy.ts): 94.23%, the 3 survivors are equivalent — splitting on one whitespace/comma character instead of runs yields empty entries that the `domain !== ''` check already rejects, and empty-domain mutants compare against a target that is never empty. Upstream commit: sirmalloc@e8ef3cb Co-Authored-By: Claude Code <[email protected]> * test(usage): reproduce upstream 0bcc7eb on macOS Upstream 0bcc7eb (sirmalloc#624) makes a successful fetch clear usage.lock only when it still holds that render's own in-flight record: previously a success unconditionally removed the lock, so with two concurrent renders and one 429 the other's later success deleted the rate-limit lock and the next render fetched inside the server's backoff window. Upstream CI only runs Linux; this proves the fix holds on macOS. Red: against the pre-fix tree (475e401) the probe's rate-limit lock (written by a "concurrent render" mid-request) is gone after the observed success — lockExistsAfter expected true, received false. Green: bun test 1/1 and vitest/Node 5/5 (three batch files). Mutation: usage-fetch.ts is exercised through the subprocess harness, whose coverage the mutation runner cannot see (the probe is a child process), so the fix lines report as uncovered rather than surviving; the red/green pair above is the regression proof for this fix. Upstream commit: sirmalloc@0bcc7eb Co-Authored-By: Claude Code <[email protected]> * test(usage): reproduce upstream 610dcc9 on macOS Upstream 610dcc9 bounds the usage fetch with an absolute deadline: the request's `timeout` only covers an idle socket, and behind HTTPS_PROXY the request has no socket until the proxy answers CONNECT — a proxy that accepts and never answers held the render forever, and its agent kept the process alive afterwards. Upstream CI only runs Linux; this proves the fix holds on macOS. Red: against the pre-fix tree (ae9721e) the probe against a local TCP proxy that accepts and never answers never finishes — the 20s probe budget kills it (spawnSync ETIMEDOUT), where the fixed code finishes with an api-error in about 6s (the 5s deadline plus overhead). Green: bun test 1/1 and vitest/Node 5/5 (three batch files). Mutation: usage-fetch.ts is exercised through the subprocess harness, whose coverage the mutation runner cannot see (the probe is a child process); proxy-agent.ts survivors are equivalent in any environment that has AbortController (the typeof fallback targets ancient runtimes, where controller?.abort() and controller.abort() are the same). Upstream commit: sirmalloc@610dcc9 Co-Authored-By: Claude Code <[email protected]> * test(usage): honor probe timeoutMs under the bun runner The Bun branch of runProcess dropped options.timeout, so a hung probe failed the stalled-proxy contract by hanging instead of failing; pass it through like the node branch does. Bun.spawnSync kills the process with SIGTERM on timeout (verified on Bun 1.3.13), which surfaces as a non-zero exit here. Co-Authored-By: Claude Code <[email protected]> * test(powerline): scope font-dir observation to the clone target Prefix-scanning the shared tmpdir broke under leftovers from killed runs (they landed in observed and failed toHaveLength(1) on every later run) and the blind rmSync could delete other accounts' directories. Observe and clean up only the target the execFileSync mock already extracts from the installer's own clone call; drop the dead git-clone branch in the execSync mock, since powerline.ts clones via execFileSync. Co-Authored-By: Claude Code <[email protected]> * test(sandbox): fix setup comment and clean up the mutation home globalSetup runs once per run, not per test file — say so; return a teardown that removes the sandbox home after the run. Co-Authored-By: Claude Code <[email protected]> --------- Co-authored-by: axisrow <[email protected]> Co-authored-by: Claude Code <[email protected]>
This was referenced Oct 17, 2026
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
BLUF
clearUsageLock(), which deletes the 429rate-limitedlock. The next render after the 180 s cache expiry then fetches again inside the server's Retry-After window.usage.lockonly while it still holds the in-flight record this render wrote (content compare).rate-limitedlock while the request is in flight. It fails onmainand passes with the fix.Details
The lock check (
readActiveUsageLock) and the in-flight write (writeUsageLock(now + 30, 'timeout')) are two separate syscalls, so two renders can both pass the check. The failing sequence:{ blockedUntil: now + retryAfter, error: 'rate-limited' }.clearUsageLock(), an unconditionalrmSync, which deletes B's lock.CACHE_MAX_AGE(180 s, shorter than the 300 s default backoff), the next render finds a stale cache and no lock, and fetches again.I found this with a TLA+ model of the lock protocol:
A replay of the same interleaving against the built
dist/bundle (network faked, 300 ms pause injected after A's lock check) shows:The window is narrow: two renders must pass the lock check at almost the same moment, and only sessions that need API-only fields fetch at all. So this is low priority. The fix is small and keeps #487's intent: a render still removes its own in-flight lock after a satisfying success.
Checks
bun run lint: clean.bun run build: clean.bun test: the new test passes.fetchUsageData error handling > preserves root errors…times out at 5 s on this loaded machine, identically onmain.🤖 Generated with Claude Code