Repository navigation
Description
Finding
PR #679 (fixes #678, head f935ce0e) judges a failed resume's "no output" condition inconsistently between the two modes of bin/agy-proxy.mjs, because the stream close handler checks hasOutput before flushing the final unterminated line, while the non-stream handler parses the whole stdout buffer before its retry gate:
- Stream (
bin/agy-proxy.mjson PR head):child.on('close', (code, signal) => …)runs the resume-retry check first —if (code !== 0 && !hasOutput && allowRetry && targetConvId)at :471 — and only afterwards flushes the tail line (if (lineBuffer.trim()) { processJsonLine(lineBuffer.trim()); }at :479-481) and the new exit-code failure check (:489). Astep_updatetext delta that arrives as the child's final stdout bytes without a trailing newline is still sitting inlineBufferat the retry decision, sohasOutputisfalseeven though the run produced output. - Non-stream (same file, PR head):
stdoutBufferis fully parsed intoresponseTextbefore theif (code !== 0)block (:580), and the retry inside it is gated on!responseText— so the same run shape is correctly treated as "produced output": no retry, HTTP 502.
Consequence in stream mode: a resumed conversation whose run emits a partial delta in an unterminated final line and then exits non-zero is silently retried as a fresh conversation, and the client receives the fresh run's answer with finish_reason: "stop" and the resumed conversation's id — the failure is never reported, the partial output is discarded, and a second agy run executes even though the retry's own precondition ("failed without output", #678's acceptance criteria) does not hold. Whether the failure is reported or silently swapped for a fresh-conversation answer depends only on whether agy newline-terminated its last JSON line. The sibling proxies flush first: bin/muse-proxy.mjs and bin/codex-proxy.mjs both process a non-empty lineBuffer as the first step of their close handlers before judging the run.
Note the success path already relies on the flush at :479-481 to deliver an unterminated final line, so unterminated tails are a shape this file explicitly handles — just not at the retry decision.
Evidence (reproduced on PR #679 head f935ce0e1a5e4a2caf1880ee4f1badeec1638865)
Ran the PR-head bin/agy-proxy.mjs with a stub agy at $HOME/.local/bin/agy (the proxy's own binary resolution; the bin/agy-proxy.test.mjs pattern from the PR), requests carrying x-conversation-id: conv-123:
- Stub, on
--conversation: emits{"event":"step_update","step_update":{"text_delta":"partial answer"}}with no trailing newline (non-stream shape:{"response":"partial answer"}likewise unterminated), printsstub agy: run failed midwayto stderr, exits 3; a fresh (no-conversation) run succeeds withHello world:- stream → 200, fresh run's
Hello worldchunk +finish_reason: "stop"+[DONE]; proxy log shows exactly 1retrying as fresh conversationline — no error event, partial output never sent - non-stream → 502
{"error":"agy process exited with non-zero code","code":3,"stderr":"stub agy: run failed midway"}(correct — the control)
- stream → 200, fresh run's
- Control, same stub but the partial line is newline-terminated:
- stream →
partial answerchunk, thendata:-framedagy execution failed: stub agy: run failed midwayerror event +[DONE], nostopchunk (the PR's intended new behaviour) - non-stream → 502, same as above
- stream →
So the two modes disagree on identical run shapes, and stream mode disagrees with itself based on a trailing newline. PR #679's new tests cover the resume-retry case only with no output at all (RETRYME) and the partial-output case only without a conversation (FAILPARTIAL, newline-terminated) — the resume + unterminated-partial shape is untested. This is filed against the PR as a follow-up; fixing it inside PR #679 before merge also satisfies this issue.
Dedup: issue searches for agy-proxy return only #678 (OPEN, the exit-code masking this PR fixes — its acceptance requires the retry be preserved for a failed resume with no output, which is exactly the precondition misjudged here) and unrelated port/discovery issues; searches for lineBuffer / unterminated / resume retry return only #678. Cross-repo: bin/agy-proxy.mjs exists only in this repo (no copy in developing-today/opencode, developing-today/openchamber, dezren39/nix — verified by git grep on each clone's origin/main).
Why it matters
The retry exists to recover from a stale conversation id, not to replace a run that already answered in part. Triggering it on a run that produced output hides a real failure (expired auth, CLI crash) behind an unrelated fresh conversation's answer, presented under the resumed conversation's id as a clean stop — the same silent-substitution failure mode #678 was filed for, reachable only in stream mode (the mode OpenCode actually consumes) and only when the final line is unterminated.
Acceptance criteria
- The stream close handler flushes any non-empty
lineBuffer(viaprocessJsonLine) before the resume-retry decision, sohasOutputreflects all output the child produced — mirroring the non-stream ordering in the same file and the flush-first ordering inbin/muse-proxy.mjs/bin/codex-proxy.mjs. - A failed resume (non-zero exit or signal) whose only output is an unterminated final
step_updatedelta is reported as a failure in stream mode —data:-framed error event naming the failure +data: [DONE], no chunk claimingfinish_reason: "stop", no fresh-conversation retry — matching the non-stream 502 for the same shape. - A failed resume with genuinely no output still retries once as a fresh conversation in both modes (the existing
RETRYMEbehaviour), and successful runs with an unterminated final line still deliver that line before thestopchunk. - A new stub case in
bin/agy-proxy.test.mjspins the resume + unterminated-partial + non-zero-exit shape in stream mode (and the non-stream control); ablating the flush-reorder fails exactly that case, and theagy-proxy+muse-proxy+codex-proxy+proxy-portsuites pass.
Activity
dezren39 commentedon Oct 26, 2026
Intake review
Still live (verified this run, fleet monitor):
bin/agy-proxy.mjson current main still runs the stream-mode resume-retry check at line 471 (if (code !== 0 && !hasOutput && allowRetry && targetConvId)) before the final-line flush at lines 479–480 (if (lineBuffer.trim()) { processJsonLine(...) }) — the exact ordering the issue describes. The sibling proxies (muse-proxy, codex-proxy) flush first.Related: PR #679 / closed #678 (the resume-retry feature and its "failed without output" precondition).
Notes / risks: this is a correctness bug, not just a test gap: the silent fresh-conversation retry discards partial output and misattributes the new run's answer to the resumed conversation. The fix is an ordering change, but it must preserve the success path's reliance on the same flush.
Suggested approach: move the
lineBufferflush to the top of the stream close handler (matching the sibling proxies), before the retry decision; add the stub-agy regression case from the issue's evidence (unterminated final delta + non-zero exit must NOT retry).Labels normalized by the fleet monitor: kind/bug, priority/normal, effort/s.
dezren39 commentedon Oct 31, 2026
agent-note:v1 — triage task 1066 verdict: CONFIRMED on current main (fresh read via gh api contents @main, 2026-10-31). The file was rewritten since filing (resident SessionWorker, PR #1759) but the flaw carried over and is now structural:
lineBufferis only appended/split in_onStdout— no flush exists anywhere; the close handler (:530) calls:955,_faildirectly and_failsetserr.hasOutputfrom newline-terminated deltas only; the retry gate (allowRetry && resumed && !err.hasOutput) then retries as a fresh conversation. Fix follow-up enqueued (flush the tail through the line path before judging; test in bin/agy-proxy.test.mjs). Triage completing as triaged.dezren39 commentedon Oct 31, 2026
🤖 Agent note · picked up
Merge pull request #3173 from developing-today/queue/4595-683-agy-tai…
dezren39 commentedon Oct 31, 2026
🤖 Agent note · done
fixed_mergednode --test bin/agy-proxy.test.mjs11/11 on the laptop with the stub reachable (the new FAILTAIL test fails against the pristine proxy — 10/11 — proving regression validity; the laptop's system agy shadows the stub in-place, a pre-existing quirk that fails the unmodified suite identically on clean main). The two failed push runs on the branch are the repo-wide jules-daily workflow startup failures that occur on every push including main (zero jobs, unrelated files).