fix: never let an umwelt child inherit the JSON-RPC stdin #14
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/stdin-inheritance"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Follow-up to #13 (merged as
0b3b77c). #13 fixed a real problem — a serialised front door — but it did not fix the hangs users were still seeing. This does.Problem
umwelt's children inherited the MCP server's stdin.
podman()calledsubprocess.run(..., input=stdin)withstdin=None. That does not mean "no input" — it means inherit fd 0, and fd 0 of the MCP server is the JSON-RPC pipe from opencode. Six call sites then runpodman exec -i, which keeps stdin attached and forwards it into the container for the whole call. So while aread_file,edit_file,read_imageorread_chunkexec is alive, podman is reading the same pipe as the front door's reader. A request line that arrives in that window is swallowed by podman and never reaches the server: no response, ever. The client waits, the harness shows nothing, and the tool part staysrunningindefinitely.It needs a concurrent sender to leave a request pending while a
-iexec is alive — which is exactly why it always hurt more with subagents. It predates #13; #13's concurrency widened the window rather than creating the bug.Evidence
/home/troed/dev/onmymind, sessionses_e481de92…: anumwelt_edit_fileonsrc/plugin.tsissued at 15:10:33 was stillrunningat 15:18, with no client-side transport error. Its three siblings from the same message completed.Invalid MCP stdio message from umwelt … "id": null … Unrecognized key: "error"— one of the server's parse-error frames, which can only occur when a split line reaches the reader. A line split in two is what a reader stealing from the middle of the pipe produces.podman exec -iconsuming the caller's stdin is long-standing, documented behaviour (the classicwhile read line; do podman exec -i …; donethat skips every other line).Fix
podman()passesstdin=subprocess.DEVNULLwhenever no payload is supplied — no child inherits fd 0. An explicit payload still goes in as a pipe.-idropped from the four exec sites that never feed stdin (edit_file,read_file,read_image,read_chunk); kept forwrite_file/write_chunk, which do.gitandkeytoolchildren, and for therun.pylaunch itself, which is the control plane every sandbox child inherits from.Tests
test_podman_never_inherits_the_servers_stdin— pinsstdin=DEVNULLfor the no-payload path andinput=for the payload path.-i.python3 -m pytest -m "not integration and not root and not mitmproxy" -q→ 1312 passed, 44 deselected (8 warnings pre-existing).After merge
Needs a rebuild +
uv tool install --forceand an opencode restart; a session that is already wedged keeps its stuck call until it restarts.