Repository navigation
feat(daemon): --serve request loop for the shared-process runner - #35
Conversation
First slice of the daemon epic (#14): a long-lived process mode that reads NDJSON status payloads from stdin (one per line) and answers each with one JSON response line ({"text"} or {"error"}), reusing the request-scoped render orchestration (#15) instead of paying process start and module load on every repaint. - src/serve.ts: handleServeLine (one request -> one response, errors never kill the process) + runServeLoop (stdin readline loop); ServeDependencies injectable for hermetic tests. - ccstatusline.ts: --serve flag wired before the CLI/TUI modes. - One request = one loadSettingsFrom + renderStatusLines call, so concurrent sessions (#15) can share one process. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 25eaf9b982
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| export function createProcessServeDependencies(): ServeDependencies { | ||
| return { | ||
| loadSettings: () => loadSettingsFrom(getConfigPath()), | ||
| resolveTerminalWidth: (sessionId, ttlSeconds) => getTerminalWidth({ sessionId, ttlSeconds }), |
There was a problem hiding this comment.
Re-probe terminal width for each served request
When the serve process can discover a numeric terminal width, the first request sets the module-level hasProbed cache in getTerminalWidth; every later request returns that value immediately (src/utils/terminal.ts:197-200). Because this mode keeps the process alive, resizing the terminal leaves truncation and flex layout stuck at the initial width, unlike the one-shot path where each repaint starts with a fresh cache. Reset or otherwise refresh the terminal-width probe between served requests.
Useful? React with 👍 / 👎.
| for await (const line of rl) { | ||
| const response = await handleServeLine(line, deps); |
There was a problem hiding this comment.
Evict expired custom-command cache entries in serve mode
When customCommandCacheTtlSeconds is enabled, each distinct session ID, command, and terminal width creates an entry in the process-global customCommandCache (src/utils/custom-command.ts:59,82-88,331), but expired or inactive keys are never removed. The new long-lived request loop prevents those entries—and cached command output of up to 16 KiB each—from being reclaimed when sessions end, so a daemon serving many sessions can grow without bound. Bound or periodically prune this cache for the lifetime of the serve loop.
Useful? React with 👍 / 👎.
Closes nothing; first slice of #14 (daemon / shared-process runner).
What
--servemode: the process stays alive and serves status-line renders from NDJSON requests on stdin — one JSON payload per line in, one JSON response line out ({"text": "..."}on success,{"error": "..."}on failure; blank lines are skipped). This amortizes process start + module load across repaints, which is the whole premise of the daemon epic: today every repaint is a fresh node process.src/serve.ts—handleServeLine(request → response; parse/validation/render errors answer with{"error"}instead of killing the process) andrunServeLoop(readline loop).ServeDependenciesis injectable, so tests run hermetically with fixed settings and no terminal probe.src/ccstatusline.ts—--servewired before the CLI/TUI modes.loadSettingsFrom→renderStatusLines), so per-request settings/config isolation from Daemon: extract request-scoped render orchestration with output parity #15 holds inside one process.Verification
src/__tests__/serve.test.ts(5 cases): valid payload →{"text"}containing the model, invalid JSON →{"error"}, schema-invalid →{"error"}, blank lines → no response, process-dependency wiring.bun run lintclean.Micro-benchmark (honest scope: synthetic payload, no transcript, real user config, sequential requests)
--serveMeasured with
/usr/bin/time -lon the built bundle; per-request serve cost includes the render itself (git subprocesses run per request). The ~3× CPU-per-request delta is the amortized startup; aggregate-memory extrapolation for N concurrent sessions is left to the harness (scripts/benchmark-render.pybaseline from #26), not claimed here.Known limitations (deliberate, next slices)
null.🤖 Generated with Claude Code