Repository navigation
fix: time out keychain reads, drop tput's made-up width, expire future-dated usage cache - #683
Open
eric-engberg wants to merge 3 commits into
Open
eric-engberg wants to merge 3 commits into
eric-engberg wants to merge 3 commits into
Conversation
On macOS the usage credentials come from `security find-generic-password` and, for the default profile on a miss, `security dump-keychain`. Neither call had a timeout, so a `security` that blocks (for example on a keychain unlock prompt) held the whole status line until someone answered it. With a fake `security` that sleeps 12s, every render took 24s. Both calls now time out after 5s, like the git and usage API calls. A lookup that finds no OAuth login was also repeated on every render: an API-key user with a usage widget has nothing to cache, so each render ran both `security` calls, the full keychain dump included. The miss is now remembered for 30s across renders (as it already was within one process), in ~/.cache/ccstatusline/usage-credentials.lock. The record names the credential stores it covers (the keychain service and config directory), so one profile's miss never suppresses another profile's lookup. As with the usage lock, a deadline further ahead than the backoff itself is ignored, so a clock set back cannot stretch it. With the same fake `security`, the first render now takes 10s and the next ones inside the backoff about 0.2s.
The file cache is fresh while `now - mtime` is under 180s. For a file modified in the future that age is negative, so it always passed. After one render under a clock running ahead (since corrected, e.g. by NTP or a VM resume), the cached usage stayed on screen with no refetch until real time passed that mtime plus three minutes: a clock a day fast froze the usage widgets for a day. A file dated ahead of now now counts as fresh only within one cache lifetime. That still covers a concurrent render writing the file just after this one read the clock; anything further ahead cannot be right and is refetched. The usage lock already ignores deadlines too far ahead for the same reason.
When the ancestor walk finds no terminal, the probe fell back to `tput cols`. That runs with piped stdio, so tput cannot see a terminal either: it prints $COLUMNS if exported, otherwise the terminfo default for $TERM, which is 80. So with no terminal found and TERM set, the status line was truncated at 74 columns in full-width mode (40 in full-minus-40) however wide the real terminal was, and flex separators padded to that made-up width. It also meant the probe never returned null in that case, so the per-session "no TTY" cache was never written and the whole `ps` ancestor walk re-ran on every render. The fallback is gone. With no terminal found the probe returns null, which the renderer already handles: no truncation, and flex separators render as plain separators. That result is now cached per session as intended. CCSTATUSLINE_WIDTH still overrides detection for anyone who wants a fixed width.
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.
What
Three small fixes for cases where something outside ccstatusline misbehaves and the status line stalls or shows the wrong thing:
securitycalls behind the usage widgets give up after 5s, and a lookup that finds no OAuth login is not repeated for 30s.Why
Keychain.
security find-generic-passwordand, for the default profile on a miss,security dump-keychainhad no timeout. Ifsecurityblocks, for example on a keychain unlock prompt, the status line waits until someone answers it. With a fakesecuritythat sleeps 12s, every render took 24s under both Bun and Node. Separately, an API-key user with a usage widget has no OAuth login to find, so nothing is cached and every render repeats bothsecuritycalls, the full keychain dump included.Width. When the
pswalk finds no ancestor with a terminal (IDE sessions, some Claude Code spawn paths), the probe fell back totput cols. That runs with piped stdio, so tput can't see a terminal either. It prints$COLUMNSif exported, otherwise the terminfo default for$TERM:So the line was cut at 74 columns with
...however wide the terminal was, and flex separators padded to that width. And since the probe never returned "no width" in that case, the per-session no-width cache was never written and thepswalk ran again on every render.Cache date. The usage cache is fresh while
now - mtime < 180s. For a file modified in the future the difference is negative, so it always passed. After one render under a clock that ran ahead and was then corrected (NTP, a VM resume), the widgets kept showing that cached usage, with no refetch, until real time passed the file's mtime plus three minutes. A clock one day fast froze them for a day. The usage lock already ignores deadlines too far ahead for the same reason.How
execFileSync('security', …)calls gettimeout: 5000, the bound the git and usage API calls already use. A timed-out read is a miss, as anysecurityerror already was. When the whole lookup finds no login,~/.cache/ccstatusline/usage-credentials.lockrecords{ blockedUntil: now + 30, source }, and renders skip the lookup until then. That is the same 30s a single process (the TUI) already cachesno-credentialsfor.sourcenames the keychain service and config directory the lookup used, so one profile's miss never suppresses another profile's lookup. During the backoff the result is what a miss already gave: the stale cache if there is one, else[No credentials]. A login made in that window shows up when it ends, at most 30s later. A deadline more than 30s ahead is ignored, like the usage lock's 24h horizon, so a clock set back can't stretch the backoff. Off macOS the lookup is a single file read; the backoff applies there too, which costs nothing and keeps one rule.tput colsfallback is removed. With no terminal found,getTerminalWidth()returns null, which the renderer already handles: no truncation, and flex separators render as plain separators (the documented no-width behavior). That null is now cached per session (terminalWidthCacheTtlSeconds), so thepswalk runs once per TTL instead of on every render. Detection with a terminal is unchanged (the Linux/procprobe, theps/sttywalk), andCCSTATUSLINE_WIDTHstill overrides it. The one setup that loses a width is no terminal anywhere butCOLUMNSexported, which tput passed through;CCSTATUSLINE_WIDTHcovers that explicitly.|now - mtime| < 180s. A file dated a few seconds ahead still counts, since a concurrent render can write it just after this one read the clock. Anything more than one cache lifetime ahead is refetched. The stale fallback used during API errors and backoffs is unchanged.Docs: DEVELOPMENT.md lists the new lock file and the cache-date rule, and DEVELOPMENT.md and USAGE.md no longer mention tput.
Overlap with open upstream PRs, all textual: #532 (keychain account) edits
readMacKeychainSecret's argument line, next to the options line this changes. #331 (Windows width) adds code right afterprobeTerminalWidth, where the fallback is removed. #266 (per-profile cache paths) rewritesreadStaleUsageCache, which directly follows the new backoff code. #624 (lock clearing) touchesusage-fetch.tsbut none of these hunks.Demo
The status line where no ancestor process has a terminal, as in an IDE session, rendered from the example payload (fake data) in a window about 110 columns wide.
psis stubbed to find no terminal andtputto print 80, which is what the real one prints there. Before, the line is cut at 74 columns and Terminal Width reads 80. After, the line renders whole and Terminal Width, with no width to show, hides itself. The keychain and cache-date fixes change nothing visible.Powerline: before
Powerline: after
Plain: before
Plain: after
Testing
main: every keychain read passes a 5s timeout (usage-token.test.ts). A lookup that found nothing is skipped by a render 10s later, retried after 31s, and doesn't suppress another profile's lookup (usage-fetch.test.ts, real subprocess renders). A cache dated a day ahead is refetched, and one dated 2s ahead is still served (the second case passes onmaintoo; it guards the tolerance). With no ancestor terminal, the width is null andtputis never called (terminal.test.ts, replacing the test that asserted the tput fallback).bun test: 2786 pass, 0 fail.bun run lintpasses.terminal.test.ts22/22 pass. The other two can't run under Node onmaineither:usage-fetch.test.ts's probes import.tssources in a Node subprocess, andusage-token.test.ts'snode:child_processmock lacksspawn. The new tests there fail the same way as their neighbors (36 of 58 probe tests failing onmain, 39 of 61 here; no new failing files), so the behavior was checked under Node through the built CLI instead.security/ps/tput/jjon PATH, no network (a local proxy counted CONNECT attempts and refused them). Both runtimes agreed:securitysleeping 12s:maintook 24.2s on every render. This branch took 10.2s, then 0.13s (Bun) / 0.23s (Node) on the next render, with nosecuritycall.securityfinding nothing (API-key user):mainmade 2securitycalls on every render; this branch made 2, then 0.maincut it to 74 columns and ranpsandtputon every render. This branch rendered the whole line, and the next render ran nops.usage.jsondated a day ahead:mainserved it with no request. This branch tried a refetch (1 CONNECT) and, with the API unreachable, fell back to the same cached value. Dated 5s ahead: served from cache on both.runtime-check.sh: plain and Powerline piped renders agree across Bun and Node and matchmain(baseline config, terminal found). The TUI opens and exits cleanly under both.