fix(daemon): flush stdout/stderr before exit in od media wait/generate - #6800
fix(daemon): flush stdout/stderr before exit in od media wait/generate#6800lorenzozanee wants to merge 1 commit into
Conversation
`od media wait`/`od media generate` write their terminal JSON line and then call process.exit() in the same synchronous block. process.exit() terminates the process without waiting for pending async writes, so when stdout is a pipe (Windows named pipes, or output larger than the pipe buffer on any platform) the final line is dropped: the task succeeds and the daemon records a valid file, but the caller sees exit 0 with empty stdout and reports a failure - often re-submitting a duplicate generation job. Drain both streams before exiting so the terminal JSON always reaches the caller. The exit-code contract (0 done, 2 still running, 5 failed) is unchanged. Regression spec spawns the real CLI against a fake daemon with a multi-megabyte terminal snapshot to force the write/exit race, and asserts exit code plus complete JSON/stderr delivery. Red on main, green here.
|
Hey @lorenzozanee — the reproduction notes here are very strong, especially the pipe-buffer race setup and the red/green verification against the real CLI. I'm queuing maintainer review next and handling the validation bookkeeping for this daemon path.
|
|
🧪 Queued for QA validation — this PR has changes that need a manual QA pass before it is merged. Nothing needed from you; we’ll update here once it is validated. Thanks for the contribution! 🙏 |
mrcfps
left a comment
There was a problem hiding this comment.
@lorenzozanee thank you for pinning this down so carefully. This is a nasty Windows/pipe race, and the red spec makes the fix trustworthy.
I reviewed the pollUntilDoneOrBudget exit paths in apps/daemon/src/cli.ts and the new apps/daemon/tests/media-wait-stdout-flush.test.ts. Replacing the same-tick process.exit() calls with flushStreamsAndExit() is the right invariant: writing an empty chunk with a callback only resolves after previously queued stdout/stderr writes have been handed to the kernel, which is exactly the userspace-buffer loss described in #6540. The helper covers every terminal branch in the poll loop (done, failed, interrupted, non-2xx, 404, fetch error, and the still-running handoff), and it still exits after the flush so the top-level dispatcher cannot race a second unflushed process.exit().
The 6 MB / 2 MB throttled-reader spec is a real red test, not a tautology: it forces the pipe-buffer race, asserts full JSON/stderr delivery plus the matching exit codes, and would reject a short sleep-before-exit fake fix. The covered branches match the user-visible stdout contract.
No correctness, safety, or test issues in the changed ranges. Nice work.
🔁 Powered by Looper · runner=reviewer · agent=grok-build · An autonomous AI dev team for your GitHub repos.
|
Thanks for the contribution. I completed QA validation for this PR. QA Acceptance Record Scope:
Verified:
Not verified:
Risks / notes:
Conclusion:
|
Fixes #6540
Why
od media waitandod media generatewrite their terminal result as a single JSON line on stdout and then callprocess.exit()in the same synchronous block.process.exit()terminates the process without waiting for pending async writes, so when stdout is a pipe the final line can be dropped: the task completes and the daemon records a valid file, but the caller sees exit 0 with empty stdout and reports the generation as failed — often re-submitting a duplicate job. This is reported on Windows (named-pipe writes are always async there) and is also reachable on any platform once a single write exceeds the pipe buffer, which large progress logs or result payloads do.What users will see
No UI or CLI surface changes. Scripts and agents that capture
od media wait/od media generateoutput through a pipe now reliably receive the final{"file": ...}/ handoff JSON line on stdout before the process exits, so successful tasks are no longer misreported as failures.Surface area
Screenshots
N/A — CLI behavior change, no UI.
Bug fix verification
apps/daemon/tests/media-wait-stdout-flush.test.tsspawns the real CLI against a fake daemon and returns a terminal snapshot whose payload (6 MB stdout / 2 MB stderr) exceeds the pipe buffer, forcing the write/exit race deterministically. It asserts exit code plus complete JSON/stderr delivery via deep-equality.mainand green on this branch? Yes — 4/4 failed on the base commit (stdout/stderr truncated at the pipe-buffer boundary), 4/4 pass here.Validation
pnpm --filter @open-design/daemon typecheck— PASSpnpm exec vitest run -c vitest.config.ts tests/media-wait-stdout-flush.test.ts— 4/4 PASSpnpm exec vitest run -c vitest.config.ts tests/media-generate-prompt-file.test.ts tests/media-generate-multi-image.test.ts— 4/4 PASSpnpm guard— PASS