Skip to content

refactor(daemon-client): one socket round-trip primitive, budgets as caller values (6gd)#101

Merged
brandon-fryslie merged 1 commit into
mainfrom
brandon-daemon-client-6gd
Jun 10, 2026
Merged

refactor(daemon-client): one socket round-trip primitive, budgets as caller values (6gd)#101
brandon-fryslie merged 1 commit into
mainfrom
brandon-daemon-client-6gd

Conversation

@brandon-fryslie

Copy link
Copy Markdown
Contributor

Summary

Closes sheriff finding #6 (ticket brandon-daemon-client-6gd, epic 58c): the daemon socket round-trip was implemented three times — client.ts (connectWithTimeout, 50/150ms), client-stats.ts (connect, 200/500ms), client-debug.ts (connect, 200/500ms) — and the stats/debug copies had drifted from client.ts's outcome classification.

Locked decision (from the ticket): one connect+round-trip primitive parameterized by (connectMs, budgetMs); the three callers pass their budgets as VALUES. Render keeps 50/150, stats/debug keep 200/500.

What changed

  • New src/daemon/client-transport.ts — the single round-trip implementation: connect with timeout, one framed request/response, socket teardown, and the transient/permanent failure classification (moved verbatim from client.ts, the maintained post-2l6 copy). Callers differ only in values: request, RoundTripBudgets, and a payload projector (output / stats / debug). [LAW:one-type-per-behavior] [LAW:dataflow-not-control-flow]
  • PROTOCOL_VERSION stamped at one boundary — the transport stamps v on every outbound request (UnversionedRequest); callers cannot mis-stamp or omit it. [LAW:single-enforcer]
  • client.ts shrinks to the render/click vocabulary: budget consts (check-protocol anchors untouched — all 12 mirrored constants verified), ClientOutcome = RoundTripOutcome<string>, and the output projector.
  • client-stats.ts / client-debug.ts drop their private connect copies and ad-hoc thrown errors, inheriting the typed classification they had drifted away from. Observable improvement: the "daemon may not be running" hint now prints only on transient failures — previously it printed even on VERSION_MISMATCH, where the daemon is demonstrably running.
  • The transport union is named RoundTripOutcome<T> to avoid colliding with the provider-fetch Outcome<T> in src/utils/outcome.ts — the two unions carry genuinely different failure semantics (kick-vs-no-kick vs absent-vs-failed) and stay distinct types.

Verification

  • pnpm typecheck, pnpm lint, pnpm check:protocol (12/12 mirrored constants) all clean
  • Full suite: 68 suites, 1123 tests passing (includes new test/client-transport.test.ts pinning the hint-only-on-transient contract)
  • Residue grep: exactly one net.createConnection and one sendOne callsite remain on the client side

…caller values (6gd)

Sheriff finding #6: the connect-with-timeout + frame round-trip was
implemented three times (client.ts 50/150ms, client-stats.ts and
client-debug.ts 200/500ms), and the stats/debug copies had drifted from
client.ts's outcome classification.

[LAW:one-type-per-behavior] src/daemon/client-transport.ts is now the
single implementation: connect, one framed round-trip, socket teardown,
and the transient/permanent failure classification (moved verbatim from
client.ts, the maintained post-2l6 copy). The three clients pass their
timeout budgets and payload projection as VALUES — render/click keep
50/150/200, stats/debug keep 200/500. No flags, no modes.

[LAW:single-enforcer] PROTOCOL_VERSION is stamped on every outbound
request inside the transport; callers cannot mis-stamp it. ClientOutcome
becomes RoundTripOutcome<string>; stats/debug inherit the typed
classification, so the "daemon may not be running" hint now appears only
on transient failures (on permanent ones the daemon is demonstrably up).

check-protocol anchors (CONNECT_TIMEOUT_MS / TOTAL_BUDGET_MS in
client.ts) stay in place; all 12 mirrored constants verified.

@github-actions github-actions 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.

Z.ai Coding Agent Review

This refactoring extracts the duplicated socket round-trip (connect/send/classify/destroy) from three files into a single requestOutcome primitive in client-transport.ts. Callers pass their timeout budgets and payload projector as values — no caller-identity branching in the transport layer. This is a textbook application of [LAW:one-type-per-behavior] and [LAW:dataflow-not-control-flow].

No must-change issues found. Specifics examined:

  • Type relocation: TransientOutcome, PermanentOutcome, and FailureOutcome moved from client.ts to client-transport.ts. All consumers updated. client.ts re-exports ClientOutcome = RoundTripOutcome<string>. Clean.
  • Field rename: ok-variant outputvalue (generic name for the parameterized RoundTripOutcome<T>). All access sites updated (outcome-plan.ts, tests). No stale references.
  • Protocol check script: check-protocol.mjs anchors on CONNECT_TIMEOUT_MS and TOTAL_BUDGET_MS in client.ts, which still exist as named consts. No breakage.
  • Behavioral fix (good): client-debug.ts and client-stats.ts previously always showed the "daemon may not be running" hint via .catch(), even on permanent failures. Now describeFailure conditionally includes the hint only for transient outcomes — the correct behavior, matching the render path's kick-vs-no-kick semantics.
  • describeFailure exhaustiveness: The switch over PermanentOutcome.cause has no default branch, so TypeScript enforces exhaustiveness at compile time. Adding a new cause is a compile error.
  • async removal on forwarding functions: tryRenderViaDaemon and tryClickViaDaemon no longer use async since they just forward the promise from requestOutcome. Correct — avoids an unnecessary microtask tick.
  • Tests: New test/client-transport.test.ts covers describeFailure's contract (hint only on transient, version formatting, message surfacing). Existing daemon-version-mismatch.test.ts covers the outcome typing and planOutcome.

Pre-existing note (not from this diff): interpretException classifies OS errors by substring-matching error messages (message.includes("ECONNREFUSED") etc.). This is a known limitation — Node.js doesn't provide structured error codes for all socket errors. The ProtocolError/SyntaxError paths use structural matching (instanceof) as the comment explains.

✅ Approved

@brandon-fryslie brandon-fryslie merged commit f073bb7 into main Jun 10, 2026
7 checks passed
@brandon-fryslie brandon-fryslie deleted the brandon-daemon-client-6gd branch June 10, 2026 09:20
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.

2 participants