Skip to content

feat(daemon): --serve request loop for the shared-process runner - #35

Merged
axisrow merged 1 commit into
mainfrom
feat/serve-request-loop
Sep 27, 2026
Merged

axisrow merged 1 commit into
mainfrom
feat/serve-request-loop

Conversation

@axisrow

@axisrow axisrow commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Closes nothing; first slice of #14 (daemon / shared-process runner).

What

--serve mode: 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) and runServeLoop (readline loop). ServeDependencies is injectable, so tests run hermetically with fixed settings and no terminal probe.
  • src/ccstatusline.ts — --serve wired before the CLI/TUI modes.
  • Each request goes through the existing request-scoped path (loadSettingsFrom → renderStatusLines), so per-request settings/config isolation from Daemon: extract request-scoped render orchestration with output parity #15 holds inside one process.

Verification

  • New 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.
  • Full suite green (2556 tests), bun run lint clean.
  • Live smoke through the built bundle: 3 NDJSON requests (2 valid, 1 malformed-typed) → 3 JSON responses, process stays up across requests and exits cleanly on stdin EOF.

Micro-benchmark (honest scope: synthetic payload, no transcript, real user config, sequential requests)

Mode Requests CPU total CPU / request Wall / request Peak RSS
one-shot (current model) 20 3.51 s ~176 ms ~169 ms 82 MB × process
--serve 200 11.15 s ~56 ms ~63 ms 110 MB total

Measured with /usr/bin/time -l on 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.py baseline from #26), not claimed here.

Known limitations (deliberate, next slices)

  • One serve process = one cwd / one config path; per-session variation arrives with the real transport.
  • Terminal width is resolved per request via the existing memoized probe; a daemon with no TTY will see null.
  • No socket/supervisor transport yet — this PR is the in-process request loop; how Claude Code reaches the daemon is the next slice.

🤖 Generated with Claude Code

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>
@axisrow

axisrow commented Sep 27, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/serve.ts
export function createProcessServeDependencies(): ServeDependencies {
return {
loadSettings: () => loadSettingsFrom(getConfigPath()),
resolveTerminalWidth: (sessionId, ttlSeconds) => getTerminalWidth({ sessionId, ttlSeconds }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/serve.ts
Comment on lines +79 to +80
for await (const line of rl) {
const response = await handleServeLine(line, deps);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@axisrow
axisrow merged commit 4442826 into main Sep 27, 2026
6 checks passed
@axisrow
axisrow deleted the feat/serve-request-loop branch September 27, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant