Repository navigation
Conversation
…ore the render Each custom-command widget ran synchronously during the render, and each run started a second node/bun runtime (spawnSync(process.execPath, ['-e', capture])) that then spawned `sh -c command`. The helper exists so the synchronous renderer can bound stdout, enforce the deadline, kill the process group and not hang on a background descendant that keeps the stdout pipe open (sirmalloc#539). With the default customCommandCacheTtlSeconds of 0, N widgets meant N extra runtimes started one after another on every repaint. Run every custom command in a prefetch phase instead, alongside the transcript, usage and service-status prefetches, with async spawn in the main process. The capture function is unchanged apart from handing its result to a callback, so the in-process path and the helper share one implementation of the deadline, the process-group SIGKILL, the output cap and the destroy-the-pipe-on-delivery protection. A timeoutMs + 1000 backstop mirrors the spawnSync timeout. The widget passes the prefetched results from the render context to runCustomCommand, which falls back to the synchronous helper path for anything the prefetch did not cover. Identical requests in one render run once. Measured with bench2 (20 interleaved passes, node, config of model + N x `sleep 0.05; echo x`, host load1 51-70 on 6 cores), CPU/wall medians: 1 command: 1398/10994 ms -> 1245/9457 ms (-11% CPU, -14% wall) 3 commands: 1812/14500 ms -> 1329/9666 ms (-27% CPU, -33% wall) 5 commands: 2160/17036 ms -> 1435/10223 ms (-34% CPU, -40% wall) 1 command + 1 that backgrounds a child holding stdout (timeout 300 ms): 1551/12313 ms -> 1297/10893 ms (-16% CPU, -12% wall) execve count for 3 commands drops from 10 to 7 (no helper runtimes). Co-Authored-By: Claude Opus 5.5 <[email protected]>
whycantfindaname
pushed a commit
to whycantfindaname/ccstatusline
that referenced
this pull request
Oct 1, 2026
Merged from the jason/beta4-local-fixes Trellis build (compat-repair base): bounded stdin reads in shared hooks (sirmalloc#590), research dispatch may write the task research dir (sirmalloc#634), scoped archive commits (sirmalloc#622, sirmalloc#630), list filter traversal (sirmalloc#631), remove-subtask link check (sirmalloc#632), hooks.local.json ignore (sirmalloc#633).
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The background job in the test writer now lives 3s (from main), so the prefetch test's fixed 1.3s sleep ran out before the job wrote its file. Poll for the file with waitForFile, as the capture test already does. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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
spawnin the main process. This replaces running them one after another during the render, where each run didspawnSync(process.execPath, ['-e', capture])and thensh -c command.SIGKILL, 1 MiB cap, and pipe destroy on delivery. A background child that keeps stdout open still cannot hang the render or keep the process alive.Details
custom-command-capture.ts:captureCustomCommandnow takes adeliver(result)callback and no longer callsprocess.stdout.writeandprocess.exititself. The-ehelper passes a callback that does exactly that, so the synchronous path behaves as before. The function is still self-contained for the release bundle.custom-command.ts:runCustomCommandAsync, which uses the same memory and persistent cache logic, now factored intoreadCachedResult/storeResult. It never rejects, and atimeoutMs + 1000backstop mirrors the oldspawnSynctimeout.prefetchCustomCommandsIfNeeded(lines, context), which runs every custom-command widget's request at once. Identical requests (same command, timeout, TTL and payload) run once per render and share the result.runCustomCommand(request, prefetched?)returns the prefetched result when there is one. Otherwise it falls back to the existing synchronous helper path.ccstatusline.ts:getTerminalWidthmoves above thePromise.all, because the piped payload and the cache key depend on it. The prefetch is added next to the transcript, usage and service-status prefetches, and its results go onRenderContext.customCommandResults.CustomCommand.tsx: a one-line change that passescontext.customCommandResultstorunCustomCommand. The request-building andmaxWidthcode is untouched.backgroundtest.timeoutMs + 1000backstop. On a loaded host the old path therefore sometimes rendered[Timeout]for asleep 0.05command, and the new path does not.Suggestion (not in this PR)
customCommandCacheTtlSecondsstill defaults to0, so every repaint runs every command. A small default, like the 5 s used forgitCacheTtlSeconds, would remove most of the remaining cost. That is a UX decision, so it is left to you.Measurements
Setup:
bench2round-robin fork/exec with CPU = user+sys including reaped children, node 24, all arms interleaved in one run. Config:model+ N ×sleep 0.05; echo xNcustom commands at TTL 0, with a payload that has no transcript.ccbg= one normal command +(sleep 2 & ) ; echo earlywithtimeout: 300. 20 passes. load1 during the run: min 51.2 / median 60.3 / max 69.8 on 6 cores. The host was heavily loaded, so absolute times are inflated; compare the ratios.node -e 0[Timeout]under load, as noted above.Checks
bun run lint(tsc + eslint): clean.bun run build: OK.bun test:sleep 0.5in under 1.4 s), dedupe of identical requests, and a background job holding stdout. For that last case the result comes at the deadline, the probe process does not stay alive afterwards (checked via alingeredexit metric), and the background job survives.mainfailed 10 of its 24 existing process tests in the same conditions. On this branch the unchanged sync path failed 15/24, the new async path 11/24, and prefetch 2/6. Every failure was a[Timeout]instead of the expected result, or a timing bound missed.fetchUsageData error handling5 s timeouts and several TUI menu tests.CustomCommand.tsxmaxWidth) merges cleanly with this branch; checked withgit merge-tree. This PR changes only therunCustomCommand(...)call line in that file.ccstatusline.tsandRenderContext.ts, and also merges cleanly.🤖 Generated with Claude Code