Skip to content

Conversation

veracioux
Copy link
Contributor

@veracioux veracioux commented Oct 10, 2025

Reproduced using MCP config from: #1717

The easiest way to reproduce (and verify the fix) is to run opencode run hi.

Investigation:

  • The MCP servers from the issue are run via docker run
  • When opencode exits, the ai library sends SIGTERM to all child MCP processes that use stdio transports
  • The problem is that the child processes are the docker run client, and SIGTERM isn't properly delivered, unless docker run --init was used. So the subprocesses stick around, preventing opencode from exiting. I verified this with a minimalistic subprocess-spawning bun program and strace
  • What does work though, is killing the opencode process itself using process.exit, in which case SIGTERM is delivered correctly
  • Fix: I added a process.exit() when the CLI is done with everything.

NOTE: Issue also happens in (some, maybe all) MCP servers wrapped by shell scripts, which this also fixes.


Edit:
I also found some places that don't properly await Promises and are liable to cause hangs before process exit. I changed those. I also replaced some for () { await ... } with await Promise.all(...) to potentially speed up cleanups of unrelated resources (which hence can be performed concurrently).

Improved INFO logs for ensureTitle - it's now awaited and logged so it can be more obvious from --print-logs that that is the reason why OC hangs before exiting.


Closes #854.
Closes #1001.
Closes #1195.
Closes #1431.
Closes #1717.
Closes #1810.

Might fix:

Possibly related, but I'm not very confident this is fixed by the PR:

@veracioux
Copy link
Contributor Author

Rebased to remove unintended extra commit.

state().queued.delete(input.sessionID)
SessionCompaction.prune(input)
log.info("waiting for session title to be generated")
await ensureTitlePromise
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure we wanna await here, doesnt that make title generation blocking

Copy link
Contributor Author

@veracioux veracioux Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, definitely. The promise will be running either way, and if we don't await it here, it will hang around until either it resolves or the process exits.

I think it makes sense to await it here, as it is the end of the LLM's response to the user's input, meaning it can run concurrently with the LLM's response.

Also, since I do process.exit() at the end of index.ts to work around those dockerized MCP servers, if I don't await this here, the process.exit() will be reached before the ensureTitlePromise resolves, so it will be interrupted and the title won't be saved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rekram1-node Tweaked slightly to address your concern. See 1e0846f.

@rekram1-node rekram1-node self-assigned this Oct 17, 2025
@rekram1-node rekram1-node force-pushed the bugfix/opencode-hangs-after-exit branch from 1e0846f to 7da8a66 Compare October 20, 2025 04:01
// Most notably, some docker-container-based MCP servers don't handle such signals unless
// run using `docker run --init`.
// Explicitly exit to avoid any hanging subprocesses.
process.exit();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there any other cases besides the docker mcps?

Can't we just track the pid from transports and send a SIGKILL if they are still running post close?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a shell script wrapping an stdio MCP that has the same behavior. I can send it to you later today. I can imagine there can be other cases where an MCP misbehaves in a similar way. I have seen a few MCP server implementations and the general vibe is not high quality or well thought out.

I would like to avoid using SIGKILL. Fact of the matter is, the actual MCP server process receives SIGTERM when process.exit() is called. I confirmed that by running strace on the container's PID. But from the POV of OC, the child process is not the MCP server, but rather the docker client managing the MCP server.

The PID from the transports is an implementation detail - I would like to avoid using that too.

Any subprocess not under our control can cause the hang. The process.exit is a panacea, and as long as we do proper awaits, it won't cause any issues. Which we do for the most part.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shell script + how I use it in opencode.json, as promised: https://gist.github.com/veracioux/3a9fe2ea06fca0d16852481030d9297e


return {
queued,
ensureTitlePromise: undefined as Promise<void> | undefined,
Copy link
Collaborator

@rekram1-node rekram1-node Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if I have multiple calls to prompt at once?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep in mind that opencode server isn't tied just to the normal server + tui, it can also have other use cases ex:

https://github.com/sst/opencode/blob/dev/packages/sdk/js/example/example.ts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, I'll make some tweaks. Thanks for pointing out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See newest commits. They should address both these concerns.

- Track cleanup promises (LSP clients, MCP clients, file watchers) to
  prevent premature disposal
- Add timeout warnings to alert users when disposal is taking longer
  than expected
- Use allSettled instead of Promise.all to handle disposal errors gracefully
- Move session title generation to tracked promises, let State.dispose
  handle it in unified way
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants