What happens
On Windows, any dapi command that surfaces a tool error prints the right message and then aborts instead of exiting:
> dapi capture nosuchid
No project open — open one first (`dapi open <dir>`).
Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76
The exit code is -1073740791, which is 0xC0000409, the Windows fatal abort status. The README says:
errors go to stderr with exit code 1. Everything is built to be piped, grepped, and driven by a program.
A caller testing for nonzero still sees a failure, so this is not silent. A caller testing for 1, or any CI or wrapper that treats 0xC0000409 as a crash rather than a handled error, reads it wrong. The assertion line is also extra stderr that a program parsing the error has to know to ignore.
Reproduced
Windows 11, Node 24.1.0, Electron 43, on main at b317412, against a running app.
| command |
exit |
stderr |
dapi context |
0 |
clean |
dapi capture nosuchid |
-1073740791 |
error message, then the assertion |
dapi check nosuchid |
-1073740791 |
error message, then the assertion |
dapi report "x" --logs 3, with gh absent |
-1073740791 |
error message, then the assertion |
dapi media probe C:\nope.mp4 |
1 |
clean |
The last row is the useful one. media probe on a missing file fails in assetPath before any session is opened, and it exits 1 exactly as documented. Every row that aborts is a row where an MCP session was opened first. Renderer tools (capture, check) and a main tool (report) all do it, so it does not look tool specific.
The app itself is unaffected. I checked liveness before and after in the same shell, and it kept answering.
Where I think it comes from
fail() calls process.exit(1):
function fail(message: string): never {
console.error(message);
process.exit(1);
}
On the success path nothing calls that. run() returns, the loop drains, and the process exits on its own with everything closed. On the error path call() rejects, appError runs, and process.exit tears the process down while the Streamable HTTP transport's handle is still closing, which is the state the assertion names.
That also fits media probe exiting cleanly: it calls the same fail(), but no session was ever opened, so there is no handle mid-close.
I did not confirm the exact handle, and I could only test Windows. The assertion lives in src\win\async.c, so the abort is Windows only even if the underlying race is not.
Possible direction
Letting the loop drain rather than forcing the exit would avoid it. check already does this for its own nonzero case:
if (output.issues.some((issue) => issue.severity === "error")) process.exitCode = 1;
fail() is typed never and callers lean on that, so swapping in process.exitCode is not a drop-in, which is why I am filing this rather than sending a patch. If you would like it as a PR, say which shape you would prefer and I will send it.
🤖 Generated with Claude Code
What happens
On Windows, any
dapicommand that surfaces a tool error prints the right message and then aborts instead of exiting:The exit code is
-1073740791, which is0xC0000409, the Windows fatal abort status. The README says:A caller testing for nonzero still sees a failure, so this is not silent. A caller testing for
1, or any CI or wrapper that treats0xC0000409as a crash rather than a handled error, reads it wrong. The assertion line is also extra stderr that a program parsing the error has to know to ignore.Reproduced
Windows 11, Node 24.1.0, Electron 43, on
mainat b317412, against a running app.dapi context0dapi capture nosuchid-1073740791dapi check nosuchid-1073740791dapi report "x" --logs 3, withghabsent-1073740791dapi media probe C:\nope.mp41The last row is the useful one.
media probeon a missing file fails inassetPathbefore any session is opened, and it exits1exactly as documented. Every row that aborts is a row where an MCP session was opened first. Renderer tools (capture,check) and a main tool (report) all do it, so it does not look tool specific.The app itself is unaffected. I checked liveness before and after in the same shell, and it kept answering.
Where I think it comes from
fail()callsprocess.exit(1):On the success path nothing calls that.
run()returns, the loop drains, and the process exits on its own with everything closed. On the error pathcall()rejects,appErrorruns, andprocess.exittears the process down while the Streamable HTTP transport's handle is still closing, which is the state the assertion names.That also fits
media probeexiting cleanly: it calls the samefail(), but no session was ever opened, so there is no handle mid-close.I did not confirm the exact handle, and I could only test Windows. The assertion lives in
src\win\async.c, so the abort is Windows only even if the underlying race is not.Possible direction
Letting the loop drain rather than forcing the exit would avoid it.
checkalready does this for its own nonzero case:fail()is typedneverand callers lean on that, so swapping inprocess.exitCodeis not a drop-in, which is why I am filing this rather than sending a patch. If you would like it as a PR, say which shape you would prefer and I will send it.🤖 Generated with Claude Code