Skip to content

fix(agent): don't peek past a completed stream on a half-open socket (#56) - #215

Open
justrach wants to merge 1 commit into
mainfrom
fix/56-peek-after-terminal
Open

fix(agent): don't peek past a completed stream on a half-open socket (#56)#215
justrach wants to merge 1 commit into
mainfrom
fix/56-peek-after-terminal

Conversation

@justrach

Copy link
Copy Markdown
Owner

Problem

The SSE streaming reader's post-line probe, reader.peekByte() (agent_stream.zig), ran before the terminal-event check that breaks on [DONE] / response.completed. On the final line of a response the peek's result is discarded anyway (the loop breaks at the terminal check), but its fill can block forever on a half-open socket — server gone, no FIN/RST — which the idle-stall watchdog does not guard. That hangs an already-complete turn: part of #56's "reconnect doesn't reconnect".

Fix

Move the peek to after the terminal-event break, which matches this loop's own documented intent — "stop instead of waiting for the socket to close". A completed response now breaks before it ever peeks; the non-terminal path is byte-for-byte unchanged (peek → if (!more) break → toss the buffered '\n').

Safe by inspection: on a terminal line the loop already broke at the isStreamEnd check, so more was never read there — moving the peek only removes a useless, hang-prone fill on completed responses.

Verification

  • zig build test green.
  • Streaming PTY suite green — test-pty-codex-ws, test-pty-repl, test-pty-markdown, test-pty-spinner (these stream model output through this exact reader), so no regression in the hot read path.

I explored a mock hang-test but it can't reliably distinguish old-from-new: a complete SSE line always leaves its terminating \n buffered (that's the byte reader.toss(1) consumes), so the old peekByte reads from the buffer without blocking in every reproducible case. A test both versions pass isn't a meaningful guard, so I rely on the inspection argument + regression suite instead.

Scope (honest)

  • This PR closes the post-completion subcase (the documented "don't wait on the socket" intent).
  • The mid-stream half-open subcase — peekByte blocking on a non-terminal line — still needs a watchdog-guarded peek (a streamPeekTask select-arm) plus a half-open-socket mock harness (accept, send partial SSE, then network-drop without closing) to verify. Left as a follow-up.
  • Fix-B from the issue (retryable stalls via watchdogError: Esc→Interrupted, deadline→HungRequest/StreamStalled) is already implemented and tested (Stream stall incorrectly reported as user Esc interruption #134).

Pre-existing zig fmt debt in this file (unrelated one-liners) left untouched to avoid churn.

Refs #56

…56)

The SSE reader's post-line reader.peekByte() ran BEFORE the terminal-event check,
which then breaks on [DONE]/response.completed. So on the final line the peek's
result was discarded anyway, but its fill could block forever on a half-open
socket (server gone, no FIN) that the idle-stall watchdog doesn't guard — hanging
an already-complete turn. That is part of #56's "reconnect doesn't reconnect".

Move the peek to AFTER the terminal-event break, matching the loop's documented
intent ("stop instead of waiting for the socket to close"). A completed response
now breaks before peeking; the non-terminal path is byte-for-byte unchanged
(peek -> if (!more) break -> toss the buffered '\n').

Safe by inspection: on a terminal line the loop already broke at the isStreamEnd
check, so `more` was never read there — skipping the peek only removes a useless,
hang-prone fill. Verified: zig build test green + the streaming PTY suite
(test-pty-codex-ws/repl/markdown/spinner) green — no regression in the hot read
path.

Scope: this closes the post-completion subcase. The mid-stream half-open subcase
(peekByte blocking on a NON-terminal line) still needs a watchdog-guarded peek
plus a half-open-socket mock harness to verify — left as a follow-up. Fix-B
(retryable stalls via watchdogError, #134) is already done.

Pre-existing zig-fmt debt in this file (unrelated one-liners) left untouched to
avoid churn.

Refs #56

Co-Authored-By: blackfloofie <265516171+blackfloofie@users.noreply.github.com>
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