Problem
bin/hud-daemon.js (1,099 lines, zero tests) implements the ADR-003 transport cascade: producers emit via named pipe -> HTTP -> JSONL, and the daemon fans events out to browser clients over a hand-rolled WebSocket server.
The fan-out path has no backpressure. broadcast() (bin/hud-daemon.js:798) does wsClients.forEach(s => wsFrame(s, msg)) and wsFrame() (:787) calls socket.write() with no bufferedAmount / drain / highWaterMark handling. A single slow or disconnected browser client lets the Node socket buffer grow unboundedly, so an AFK loop that keeps emitting events can slowly exhaust the daemon's memory — the exact AFK scenario ADR-003 was built to observe. A dead client is only cleaned up when the next write() throws and wsClients.delete() runs (:795).
Two related hardening items belong in the same slice because they share the transport's write/timer lifecycle:
- The heartbeat
setInterval (:1053) and pipe-retry setTimeout (:629) never .unref(), so the daemon keeps the process alive around shutdown.
eventBuffers/violationsMap prune with .shift() per write (hot path), a minor inefficiency that a test seam would force into a bounded structure.
Architecture impact
Scope
- Cap per-socket buffering in
wsFrame/broadcast: check socket.bufferedAmount (or writableLength) against a watermark and drop/close slow clients rather than buffering without bound; backpressure or process-and-discard with a HUD_MAX_CLIENTS-style bound consistent with the existing HUD_MAX_PROJECTS knob.
.unref() the heartbeat interval and pipe-retry timers so the daemon can exit cleanly on shutdown.
- Extract the transport write path into an injectable module with a unit test (using
node:test/node:assert, matching the zero-dependency Node-only design in ADR-003) covering: frame construction (len <126 / <65536 / >=65536), write-throw -> client removed, bufferedAmount cap, and the JSONL fallback path.
- Add a transport-level regression test asserting the daemon never calls
write() on an errored socket.
Acceptance criteria
node --test bin/hud-daemon.test.js (or equivalent) passes: >=1 test each for frame framing, slow-client cap, write-error cleanup, and JSONL fallback.
- A slow-client simulation shows a bounded per-socket buffer (no unbounded
writableLength growth).
- Daemon exits promptly on SIGTERM (timers unref'd).
- No observable behavior change for healthy clients; no producer changes.
Non-goals
Proposed by: automated source:architecture-review pass.
Problem
bin/hud-daemon.js(1,099 lines, zero tests) implements the ADR-003 transport cascade: producers emit via named pipe -> HTTP -> JSONL, and the daemon fans events out to browser clients over a hand-rolled WebSocket server.The fan-out path has no backpressure.
broadcast()(bin/hud-daemon.js:798) doeswsClients.forEach(s => wsFrame(s, msg))andwsFrame()(:787) callssocket.write()with nobufferedAmount/drain/highWaterMarkhandling. A single slow or disconnected browser client lets the Node socket buffer grow unboundedly, so an AFK loop that keeps emitting events can slowly exhaust the daemon's memory — the exact AFK scenario ADR-003 was built to observe. A dead client is only cleaned up when the nextwrite()throws andwsClients.delete()runs (:795).Two related hardening items belong in the same slice because they share the transport's write/timer lifecycle:
setInterval(:1053) and pipe-retrysetTimeout(:629) never.unref(), so the daemon keeps the process alive around shutdown.eventBuffers/violationsMapprune with.shift()per write (hot path), a minor inefficiency that a test seam would force into a bounded structure.Architecture impact
source:architecture-reviewissue covers the fan-out/backpressure path.Scope
wsFrame/broadcast: checksocket.bufferedAmount(orwritableLength) against a watermark and drop/close slow clients rather than buffering without bound; backpressure or process-and-discard with aHUD_MAX_CLIENTS-style bound consistent with the existingHUD_MAX_PROJECTSknob..unref()the heartbeat interval and pipe-retry timers so the daemon can exit cleanly on shutdown.node:test/node:assert, matching the zero-dependency Node-only design in ADR-003) covering: frame construction (len <126 / <65536 / >=65536), write-throw -> client removed, bufferedAmount cap, and the JSONL fallback path.write()on an errored socket.Acceptance criteria
node --test bin/hud-daemon.test.js(or equivalent) passes: >=1 test each for frame framing, slow-client cap, write-error cleanup, and JSONL fallback.writableLengthgrowth).Non-goals
Proposed by: automated
source:architecture-reviewpass.