relay agent drive and relay agent watch (= agent attach --mode view) misbehave. A code audit across packages/cli (attach clients), packages/harness-driver, and crates/relay-pty + crates/broker found the following verified defects. Every claim below was checked against the actual code paths.
Structural root cause: snapshot ↔ live stream cannot be synchronized
worker_stream events carry no sequence number and are excluded from the replay buffer (crates/broker/src/listen_api.rs:2520), so an attaching client cannot correlate its HTTP snapshot with a stream position:
- Lost output: everything emitted between the snapshot response and the WS subscription is unrecoverable. A gap that cuts a repaint or escape sequence leaves the screen garbled until the next full redraw. (
packages/cli/src/cli/lib/attach.ts:61-66 claims the window is "≤10ms" — not true for drive.)
- Duplicated output: the snapshot grid is advanced by the PTY reader before chunks are queued for broadcast (
crates/relay-pty/src/pty.rs:328-332), and the 16ms coalescing buffer (crates/broker/src/pty_worker.rs:708-719) is not flushed at snapshot time — bytes can appear in the snapshot and again on the stream.
- Drive widens the gap and injects a repaint into it (
packages/cli/src/cli/lib/attach-drive.ts:740-766): order is snapshot → GET /pending → resize agent PTY (SIGWINCH → immediate TUI repaint) → only then WS connect (:619). The user sees a stale snapshot at the old geometry until the agent next outputs. Predictive echo is seeded from the stale snapshot bytes, so early predictions render at wrong positions.
- Watch loses output forever on lag: one shared 512-slot broadcast channel for all events of all workers (
crates/broker/src/runtime/init.rs:383); on RecvError::Lagged only durable events are backfilled; clients discard replay_gap frames without re-snapshotting (attach-view.ts:189-202, attach-drive.ts:337-361).
Fix direction: per-worker monotonic offset on worker_stream; include offset in snapshot response; clients subscribe first, snapshot second, drop buffered chunks ≤ snapshot offset; move drive's resize after subscription; re-snapshot on replay_gap.
Drive-specific bugs
- Pending counter corrupted by full event replay — drive opens
/ws with no sinceSeq (attach-drive.ts:453); broker replays up to 1000 historical durable events (listen_api.rs:320-328, 2394-2424; replay_buffer.rs:14). Drive seeds from GET /pending (attach-drive.ts:749) then re-counts replayed delivery_queued/agent_pending_drained (:677-684). Agent with 2 pending / 20 lifetime messages shows pending=22.
- Status line splices escape sequences and clobbers DECSC —
paintStatus runs after every chunk (attach-drive.ts:669-674) emitting ESC 7 … ESC 8 (:415-427). Arbitrary chunk boundaries mean it can splice mid-CSI and overwrite the terminal's single saved-cursor register while the agent has a DECSC pending. Same pattern in attach-passthrough.ts:236-240,481-489.
- Multi-byte UTF-8 stdin corruption — each stdin chunk decoded independently via
Buffer.toString('utf-8') (attach-drive.ts:531, attach-passthrough.ts:353); split characters in large pastes become U+FFFD. Output direction already solves this (crates/relay-pty/src/utf8_stream.rs); the input WS also accepts binary frames (listen_api.rs:1664-1667).
- Ctrl+C during attach setup strands the worker in
manual_flush — mode flipped at attach-drive.ts:722 but SIGINT/SIGTERM handlers installed only inside runDriveSessionLoop (:647-650), after three awaited HTTP round-trips. Passthrough has the inverted twin (worker left in auto_inject, attach-passthrough.ts:249-303).
manual_flush doesn't stop worker-side automation — injections already forwarded sit in pending_worker_injections and fire body+\r mid-drive (pty_worker.rs:318,961-1055); try_auto_enter can submit the human's half-typed input (wrap.rs:349-379); prompt auto-responders race the human's keystrokes (pty_worker.rs:741-748).
- Input acks mean "queued", not "written" —
SendInput replies OK once the frame is enqueued to the worker (runtime/api.rs:992-1008; listen_api.rs:1554-1570); actual PTY write failure (pty_worker.rs:535-548) is only a fire-and-forget worker_error. Client rollback path never runs; keystrokes silently lost while reported delivered.
- No settled-guard on WS message handler — output and reverse-video status repaints keep spraying onto the shell prompt after detach (
attach-drive.ts:659-688, attach-passthrough.ts:475-494, predictive-echo.ts:305-309).
- Delivery-mode save/restore get-then-set race between concurrent sessions; failed initial read force-restores
auto_inject, cancelling an explicit hold (attach.ts:216-242).
- Minor: status-line ANSI written to non-TTY stdout (
attach-drive.ts:482-492); stdout backpressure ignored in all three modes.
Rust PTY layer
- Blocking
write_all inside the tokio select loop with a real deadlock cycle — relay-pty/src/pty.rs:364-382 blocks on a 128-slot std channel + ack, called without spawn_blocking from the worker's single task on every drive keystroke and injection (pty_worker.rs:537,977-1024, wrap.rs:374). Child floods output while not reading stdin → kernel PTY buffer fills → drainer wedges → queue fills → select loop blocks → pty_rx (cap 256) fills → reader blocks in blocking_send (pty.rs:332) → permanent hang; all attach clients freeze; watchdog dead.
- Sleeps inside select arms (100–120ms:
pty_worker.rs:970,986,994,1023; wrap.rs:338) stall streaming and keystroke processing — visible drive/watch jank.
- Snapshot omits terminal modes —
snapshot.rs:148-203 re-emits cells/SGR/cursor position only; drops alt-screen, DECTCEM, DECCKM, bracketed paste, mouse reporting, wrap mode, scroll region, saved cursor. After attach, arrow keys send wrong sequences, cursor visibility wrong.
- Resize is last-writer-wins with no ownership (
runtime/api.rs:1035-1083); view never resizes and paints CUP coordinates that garble on smaller terminals.
- Stateless-per-chunk
strip_ansi / suggestion detectors mis-parse sequences split across 4KB reads (ansi.rs:18-69; wrap.rs:381-387; terminal.rs:140-144) — affects injection readiness/timing.
- No-PID watchdog can declare a silently-thinking child dead after ~30s (
pty.rs:491-606).
harness-driver package
- Stale WS close handler clobbers reconnects —
transport.ts:532-539 mutates instance state without checking the closing socket is current; disconnect→reconnect leaves two live sockets feeding the same listeners (every event duplicated), a leaked socket, and a spurious third connection.
- Events-WS message handler uses
data.toString() instead of the existing rawDataToString (transport.ts:507-529 vs :381-389) — fragmented/binary frames silently swallowed. Reconnect loop has no backoff cap (:536-538).
- Headless workers lose newlines —
runtime/headless.rs:246-280 strips terminators via next_line() and emits stripped lines as chunk; node tail concatenates all output onto one line.
- Latent protocol drift: typed
BrokerEvent::Delivery* variants serialize agent w/o event_id (crates/broker/src/protocol.rs:419-434), mismatching protocol.ts:283-362. Dead code today; first use would be silently dropped by clients.
- Minor: unbounded
subscribeWorkerStream queue (client.ts:716-741); input_serializers map never pruned (listen_api.rs:1602-1616); predictive echo suppresses first prediction at prompts ending in a space (predictive-echo.ts:263-266).
Verified clean (ruled out)
utf8_stream.rs output decoding; queue.rs/inject.rs retry & priority logic; write-queue FIFO ordering vs injection \r; EOF/exit ordering in pty_worker.rs:908-946; PtyInputStream FIFO ack correlation; predictive-echo server-chunk ordering; durable-event replay dedup/TOCTOU in handle_dashboard_ws; enum/wire-string spellings between protocol.ts and emitted Rust JSON.
Suggested fix priority
- Stream cursor + subscribe-first attach (fixes lost/dup output for both modes) + drive resize-after-subscribe + WS
sinceSeq cutoff (fixes pending counter)
- Blocking
write_all → async ack; remove sleeps from select arms
- Signal handlers before mode flip; UTF-8-safe stdin forwarding (binary frames)
- Snapshot terminal modes
- Transport stale-close-handler guard; headless newline preservation; protocol type alignment
relay agent driveandrelay agent watch(=agent attach --mode view) misbehave. A code audit acrosspackages/cli(attach clients),packages/harness-driver, andcrates/relay-pty+crates/brokerfound the following verified defects. Every claim below was checked against the actual code paths.Structural root cause: snapshot ↔ live stream cannot be synchronized
worker_streamevents carry no sequence number and are excluded from the replay buffer (crates/broker/src/listen_api.rs:2520), so an attaching client cannot correlate its HTTP snapshot with a stream position:packages/cli/src/cli/lib/attach.ts:61-66claims the window is "≤10ms" — not true for drive.)crates/relay-pty/src/pty.rs:328-332), and the 16ms coalescing buffer (crates/broker/src/pty_worker.rs:708-719) is not flushed at snapshot time — bytes can appear in the snapshot and again on the stream.packages/cli/src/cli/lib/attach-drive.ts:740-766): order is snapshot →GET /pending→ resize agent PTY (SIGWINCH → immediate TUI repaint) → only then WS connect (:619). The user sees a stale snapshot at the old geometry until the agent next outputs. Predictive echo is seeded from the stale snapshot bytes, so early predictions render at wrong positions.crates/broker/src/runtime/init.rs:383); onRecvError::Laggedonly durable events are backfilled; clients discardreplay_gapframes without re-snapshotting (attach-view.ts:189-202,attach-drive.ts:337-361).Fix direction: per-worker monotonic offset on
worker_stream; include offset in snapshot response; clients subscribe first, snapshot second, drop buffered chunks ≤ snapshot offset; move drive's resize after subscription; re-snapshot onreplay_gap.Drive-specific bugs
/wswith nosinceSeq(attach-drive.ts:453); broker replays up to 1000 historical durable events (listen_api.rs:320-328, 2394-2424;replay_buffer.rs:14). Drive seeds fromGET /pending(attach-drive.ts:749) then re-counts replayeddelivery_queued/agent_pending_drained(:677-684). Agent with 2 pending / 20 lifetime messages showspending=22.paintStatusruns after every chunk (attach-drive.ts:669-674) emittingESC 7 … ESC 8(:415-427). Arbitrary chunk boundaries mean it can splice mid-CSI and overwrite the terminal's single saved-cursor register while the agent has a DECSC pending. Same pattern inattach-passthrough.ts:236-240,481-489.Buffer.toString('utf-8')(attach-drive.ts:531,attach-passthrough.ts:353); split characters in large pastes become U+FFFD. Output direction already solves this (crates/relay-pty/src/utf8_stream.rs); the input WS also accepts binary frames (listen_api.rs:1664-1667).manual_flush— mode flipped atattach-drive.ts:722but SIGINT/SIGTERM handlers installed only insiderunDriveSessionLoop(:647-650), after three awaited HTTP round-trips. Passthrough has the inverted twin (worker left inauto_inject,attach-passthrough.ts:249-303).manual_flushdoesn't stop worker-side automation — injections already forwarded sit inpending_worker_injectionsand fire body+\rmid-drive (pty_worker.rs:318,961-1055);try_auto_entercan submit the human's half-typed input (wrap.rs:349-379); prompt auto-responders race the human's keystrokes (pty_worker.rs:741-748).SendInputreplies OK once the frame is enqueued to the worker (runtime/api.rs:992-1008;listen_api.rs:1554-1570); actual PTY write failure (pty_worker.rs:535-548) is only a fire-and-forgetworker_error. Client rollback path never runs; keystrokes silently lost while reported delivered.attach-drive.ts:659-688,attach-passthrough.ts:475-494,predictive-echo.ts:305-309).auto_inject, cancelling an explicithold(attach.ts:216-242).attach-drive.ts:482-492); stdout backpressure ignored in all three modes.Rust PTY layer
write_allinside the tokio select loop with a real deadlock cycle —relay-pty/src/pty.rs:364-382blocks on a 128-slot std channel + ack, called withoutspawn_blockingfrom the worker's single task on every drive keystroke and injection (pty_worker.rs:537,977-1024,wrap.rs:374). Child floods output while not reading stdin → kernel PTY buffer fills → drainer wedges → queue fills → select loop blocks →pty_rx(cap 256) fills → reader blocks inblocking_send(pty.rs:332) → permanent hang; all attach clients freeze; watchdog dead.pty_worker.rs:970,986,994,1023;wrap.rs:338) stall streaming and keystroke processing — visible drive/watch jank.snapshot.rs:148-203re-emits cells/SGR/cursor position only; drops alt-screen, DECTCEM, DECCKM, bracketed paste, mouse reporting, wrap mode, scroll region, saved cursor. After attach, arrow keys send wrong sequences, cursor visibility wrong.runtime/api.rs:1035-1083); view never resizes and paints CUP coordinates that garble on smaller terminals.strip_ansi/ suggestion detectors mis-parse sequences split across 4KB reads (ansi.rs:18-69;wrap.rs:381-387;terminal.rs:140-144) — affects injection readiness/timing.pty.rs:491-606).harness-driver package
transport.ts:532-539mutates instance state without checking the closing socket is current; disconnect→reconnect leaves two live sockets feeding the same listeners (every event duplicated), a leaked socket, and a spurious third connection.data.toString()instead of the existingrawDataToString(transport.ts:507-529vs:381-389) — fragmented/binary frames silently swallowed. Reconnect loop has no backoff cap (:536-538).runtime/headless.rs:246-280strips terminators vianext_line()and emits stripped lines aschunk;node tailconcatenates all output onto one line.BrokerEvent::Delivery*variants serializeagentw/oevent_id(crates/broker/src/protocol.rs:419-434), mismatchingprotocol.ts:283-362. Dead code today; first use would be silently dropped by clients.subscribeWorkerStreamqueue (client.ts:716-741);input_serializersmap never pruned (listen_api.rs:1602-1616); predictive echo suppresses first prediction at prompts ending in a space (predictive-echo.ts:263-266).Verified clean (ruled out)
utf8_stream.rsoutput decoding;queue.rs/inject.rsretry & priority logic; write-queue FIFO ordering vs injection\r; EOF/exit ordering inpty_worker.rs:908-946;PtyInputStreamFIFO ack correlation; predictive-echo server-chunk ordering; durable-event replay dedup/TOCTOU inhandle_dashboard_ws; enum/wire-string spellings betweenprotocol.tsand emitted Rust JSON.Suggested fix priority
sinceSeqcutoff (fixes pending counter)write_all→ async ack; remove sleeps from select arms