[GH-ISSUE #6399] [Bug] A stdio tool call in flight during shutdown can reopen the session after it closes #3833

Closed
opened 2026-10-31 00:44:28 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @Adolanium on GitHub (Oct 22, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6399

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

MCP or agent integration

Observed behavior

NOT_ENOUGH_INFO for a runtime observation: this was not reproduced against a real provider. The following is source-derived.

On stdio, process shutdown (SIGINT, SIGTERM, or stdin EOF) does not wait for tool calls already in flight. createShutdown in src/main/shutdown.ts awaits handle.prepareShutdown?.(), but only the Streamable HTTP transport provides that drain. For stdio the transport handle and the session then close concurrently. A tool call that the server admitted before shutdown and that reaches session.open() after the shutdown's session.close() has been queued can reopen the shared BinarySession behind that close. Nothing closes the session again, so the provider child it started can outlive REA.

Reproduction

Source-derived; not run against a real provider.

  1. Start REA as a stdio MCP server.
  2. Send open_binary (or another tool that opens the session) and deliver SIGINT while the call is still before its session.open().
  3. Shutdown queues session.close({ retainProviderDocuments: true }) and closes the stdio handle. The admitted call's session.open() then runs after the close.

A boundary test of the same race was written for the startup window in #4884 (a session reopened from inside serve() after a raced shutdown). That PR closes the reopen cases it can reach before its follow-up close is queued, but not this steady-state window.

Expected behavior

Once process shutdown begins, a tool call cannot leave a session open after shutdown completes. Either new opens are rejected, or shutdown waits for in-flight calls before closing the session.

Impact and scope

Any stdio client that sends a session-opening tool call while REA is being stopped. The window is narrow, but when it is hit, detached provider children (detached: platform !== "win32" in src/process/ProviderProcess.ts) can survive REA's exit. The HTTP transport is not affected because it drains requests first.

REA version

6.7.0, revision 6d73515bf

Runtime and platform

NOT_ENOUGH_INFO: source-derived. The window applies on every platform; the orphaned child applies where provider children are detached (non-Windows).

Target and artifact context

None; any target that a session can open.

Interface

MCP

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

src/main/shutdown.ts (upstream/main):

// HTTP handlers can still own provider resources after cancellation.
// A failed drain keeps every shared owner available for a later retry.
await handle.prepareShutdown?.();
const results = await Promise.allSettled(
  pending.map(async (owner) => {
    await owner.close();
    owner.closed = true;
  }),
);

prepareShutdown is implemented only by src/server/http/serveStreamableHttp.ts (and passed through by serveEnvironment.ts); the stdio handle has none.

Typed outcome or recovery expected

A session-opening call that loses the race with shutdown should fail with a typed error (for example, an unavailable or cancelled session) instead of reopening the session.

Analysis or suspected cause

Shutdown closes the session once, but the session can be reopened by any request that is still running, and stdio has no barrier that stops or waits for those requests. Two possible fixes:

  1. A terminal shutdown state in BinarySession, so open() is rejected once process shutdown has begun. This fixes it at the resource owner and covers every transport.
  2. A stdio request drain equivalent to HTTP's prepareShutdown.

Option 1 looks smaller and also covers future transports. Both change steady-state shutdown behavior for every stdio request, so a maintainer decision on the approach would help before implementation.

Additional context

Found while addressing review on #4884, where it is recorded under "Not included".

Originally created by @Adolanium on GitHub (Oct 22, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6399 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area MCP or agent integration ### Observed behavior NOT_ENOUGH_INFO for a runtime observation: this was not reproduced against a real provider. The following is source-derived. On stdio, process shutdown (SIGINT, SIGTERM, or stdin EOF) does not wait for tool calls already in flight. `createShutdown` in `src/main/shutdown.ts` awaits `handle.prepareShutdown?.()`, but only the Streamable HTTP transport provides that drain. For stdio the transport handle and the session then close concurrently. A tool call that the server admitted before shutdown and that reaches `session.open()` after the shutdown's `session.close()` has been queued can reopen the shared `BinarySession` behind that close. Nothing closes the session again, so the provider child it started can outlive REA. ### Reproduction Source-derived; not run against a real provider. 1. Start REA as a stdio MCP server. 2. Send `open_binary` (or another tool that opens the session) and deliver SIGINT while the call is still before its `session.open()`. 3. Shutdown queues `session.close({ retainProviderDocuments: true })` and closes the stdio handle. The admitted call's `session.open()` then runs after the close. A boundary test of the same race was written for the startup window in #4884 (a session reopened from inside `serve()` after a raced shutdown). That PR closes the reopen cases it can reach before its follow-up close is queued, but not this steady-state window. ### Expected behavior Once process shutdown begins, a tool call cannot leave a session open after shutdown completes. Either new opens are rejected, or shutdown waits for in-flight calls before closing the session. ### Impact and scope Any stdio client that sends a session-opening tool call while REA is being stopped. The window is narrow, but when it is hit, detached provider children (`detached: platform !== "win32"` in `src/process/ProviderProcess.ts`) can survive REA's exit. The HTTP transport is not affected because it drains requests first. ### REA version 6.7.0, revision `6d73515bf` ### Runtime and platform NOT_ENOUGH_INFO: source-derived. The window applies on every platform; the orphaned child applies where provider children are detached (non-Windows). ### Target and artifact context None; any target that a session can open. ### Interface MCP ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence `src/main/shutdown.ts` (upstream/main): ```ts // HTTP handlers can still own provider resources after cancellation. // A failed drain keeps every shared owner available for a later retry. await handle.prepareShutdown?.(); const results = await Promise.allSettled( pending.map(async (owner) => { await owner.close(); owner.closed = true; }), ); ``` `prepareShutdown` is implemented only by `src/server/http/serveStreamableHttp.ts` (and passed through by `serveEnvironment.ts`); the stdio handle has none. ### Typed outcome or recovery expected A session-opening call that loses the race with shutdown should fail with a typed error (for example, an unavailable or cancelled session) instead of reopening the session. ### Analysis or suspected cause Shutdown closes the session once, but the session can be reopened by any request that is still running, and stdio has no barrier that stops or waits for those requests. Two possible fixes: 1. A terminal shutdown state in `BinarySession`, so `open()` is rejected once process shutdown has begun. This fixes it at the resource owner and covers every transport. 2. A stdio request drain equivalent to HTTP's `prepareShutdown`. Option 1 looks smaller and also covers future transports. Both change steady-state shutdown behavior for every stdio request, so a maintainer decision on the approach would help before implementation. ### Additional context Found while addressing review on #4884, where it is recorded under "Not included".
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3833
No description provided.