fix(daemon): report the /rpc backstop through the runtime's injected sinks

Merged
TH
8d ago
+410-59

Summary

#3505 gave the detached /rpc flow a last-resort .catch. Two things were wrong with it:

  • it reported through process.stderr.write and a scope-less emitDiagnostic, so a daemon with an injected error stream got the reason on the wrong stream and dropped the diagnostic;
  • it normalized the reason while building its answer, and both of the flow's own catches normalize the same way — while evaluating their answer. A reason that cannot be read (a details getter that throws) fails all three, and that second throw escaped the .catch as the fatal unhandledRejection #3505 existed to prevent.

createDaemonHttpServer now takes onUnownedRequestFailure, and openDaemonServers wires it to the pair every other out-of-request daemon event uses: the runtime's injected stderr plus emitDaemonDiagnostic. The server only answers, normalizes defensively, and an unreadable reason reports as the unknown-error failure.

Possible follow-up, not taken here: parsing the envelope with the kernel's jsonRpcRequestSchema would replace the hand-rolled isRecord check and the as JsonRpcRequest cast. It also changes the answer for a non-scalar id, so it owes a CHANGELOG line.

Validation

  • http-server-rpc-unowned-failure.test.ts: an unreadable thrown reason answers 500 with id: null, the sink hears it, the next call answers 200. The sink throws on its first event, so a broken sink cannot cost the answer.
  • daemon-runtime-rpc-flow-failure.test.ts: boots the real runtime; daemon_rpc_request_flow_failed reaches the injected stderr and daemon.log.
  • Mutation-checked: no .catch, direct process.stderr, or no defensive normalize turns both red.
  • Changed-line coverage 12/12; CRAP, eager-closure, DI-seam and layering guards pass.