Skip to content

dapi aborts instead of exiting 1 when a tool returns an error (Windows) #75

Description

@rafaelfiguereod-stack

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions