Summary
When a media task fails (e.g. an upstream 4xx, missing provider key, or unsupported parameter), the daemon CLI (media generate / media wait) prints the failure JSON and then crashes on exit with a native libuv assertion on Windows, returning a garbage exit code (-1073740791 = 0xC0000409) instead of a clean non-zero exit.
Because the process aborts, any harness that captures the CLI's stdout/stderr (redirect to a file, or $out = & node cli …) ends up with empty output — the buffered streams are lost. This makes a genuine upstream failure look like a "silent empty success" (exit 0/garbage, empty STDOUT/STDERR, no file written), which is very hard to diagnose and easily misattributed to the provider.
Environment
- Open Design 0.15.0 (packaged app)
- Windows x64, Node 24
- CLI:
prebundled/daemon/daemon-cli.mjs (media generate / media wait)
Observed output (verbatim)
task <id> queued (running)
task failed: custom image 400: { … 'response_format' … } # or: "no Fal API key — … set FAL_KEY"
{"taskId":"<id>","status":"failed","error":{ … , "status":400}}
Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76
# process exit code: -1073740791 (0xC0000409)
Reproduced with two independent failure causes:
media generate --model flux-pro-ultra … with no FAL_KEY → no Fal API key → crash.
media generate --model gpt-image-2 --image ref.png … → upstream 400 (response_format, see companion issue) → crash.
The happy path does not crash: a successful gpt-image-2 text-to-image run exits 0 with the {"file":{…}} JSON and writes the PNG normally.
Steps to reproduce
- Run any
media generate that will fail upstream (missing key, unsupported param, 4xx).
- Capture output, e.g.
& $node $cli media generate … *> out.txt or $out = & $node $cli ….
- The captured output is empty and the process aborts with the
UV_HANDLE_CLOSING assertion instead of a clean exit 5 + preserved error JSON.
Expected
On task failure: clean exit code 5 (task failed) with the error JSON preserved on stdout/stderr. No native crash, no lost output.
Likely cause
An async handle (the SSE/poll socket or a stdout stream) appears to be closed twice / after the event loop is torn down on the failure branch on Windows (libuv uv_close on an already-closing handle). The success branch tears down cleanly; only the error/abort path trips the assertion.
Impact
- Masks all upstream provider failures behind an unhelpful crash + empty output.
- Breaks agent runtimes that rely on the documented contract (exit
0 with {file|taskId}, exit 5 on failure): the crash means the agent cannot distinguish "failed" from "produced nothing," and cannot surface the real error to the user.
Summary
When a media task fails (e.g. an upstream 4xx, missing provider key, or unsupported parameter), the daemon CLI (
media generate/media wait) prints the failure JSON and then crashes on exit with a native libuv assertion on Windows, returning a garbage exit code (-1073740791=0xC0000409) instead of a clean non-zero exit.Because the process aborts, any harness that captures the CLI's stdout/stderr (redirect to a file, or
$out = & node cli …) ends up with empty output — the buffered streams are lost. This makes a genuine upstream failure look like a "silent empty success" (exit 0/garbage, empty STDOUT/STDERR, no file written), which is very hard to diagnose and easily misattributed to the provider.Environment
prebundled/daemon/daemon-cli.mjs(media generate/media wait)Observed output (verbatim)
Reproduced with two independent failure causes:
media generate --model flux-pro-ultra …with noFAL_KEY→no Fal API key→ crash.media generate --model gpt-image-2 --image ref.png …→ upstream 400 (response_format, see companion issue) → crash.The happy path does not crash: a successful
gpt-image-2text-to-image run exits0with the{"file":{…}}JSON and writes the PNG normally.Steps to reproduce
media generatethat will fail upstream (missing key, unsupported param, 4xx).& $node $cli media generate … *> out.txtor$out = & $node $cli ….UV_HANDLE_CLOSINGassertion instead of a clean exit 5 + preserved error JSON.Expected
On task failure: clean exit code
5(task failed) with the error JSON preserved on stdout/stderr. No native crash, no lost output.Likely cause
An async handle (the SSE/poll socket or a stdout stream) appears to be closed twice / after the event loop is torn down on the failure branch on Windows (libuv
uv_closeon an already-closing handle). The success branch tears down cleanly; only the error/abort path trips the assertion.Impact
0with{file|taskId}, exit5on failure): the crash means the agent cannot distinguish "failed" from "produced nothing," and cannot surface the real error to the user.