Repository navigation
fix(core): report shell exit only after output is captured - #56321
Conversation
There was a problem hiding this comment.
I found no blocking problems. On the base I reproduced the race with a real sh command: polling shell.get until the shell was no longer running and then reading shell.output gave empty or partial output in 44 of 60 runs. With this change it was 0 of 60. The new retention test fails on the base and passes here. The nearby shell tests (shell, shell-cleanup, shell-retention, tool-shell, session-shell) and the packages/core type check pass.
The finishing flag still makes the exit and timeout paths run finish() only once. The early return in command.timeout avoids interrupting a timeout fiber while it is finishing. wait, result and the shell.exited event behave as before.
Public API: nothing is added, removed or renamed. GET /api/shell/:id and the shell list now report running until captured output is complete, instead of switching as soon as the process exits. That is backwards compatible.
Problem
Shell.finish()set the terminal status (exited/timeout) before awaiting the output pump. During that window,GET /api/shell/:idalready reportedexitedwhile the output file was still empty or partial, so a client that pollsgetand then readsoutputcould get nothing back.Reproduced over HTTP against the in-process server: polling
shell.getuntil non-running, then readingshell.outputforprintf hello-world, returned""in 10 of 40 runs.Fix
Split "how did it end" from "publish that it ended":
endedDeferredrecords the outcome. The process exit and the timeout timer both complete it, and the first one wins. The timer records the timeout and starts the kill in one step that cannot be interrupted. The kill itself runs detached, soremoveand teardown never wait on a dying process.ended, then waits for captured output to drain, and only then updatesinfo, resolves waiters, publishesshell.exited, and runs retention.time.completedis still the moment the process ended or the timeout fired.Behavior change: when a timed-out process ignores SIGTERM,
getandlistkeep reportingrunninguntil it is force-killed (up to about 3s), and retiming is ignored during that window. Previously status switched totimeoutas soon as the timer fired, even though the process was still alive and its output incomplete.Verification
getstaysrunninguntil the chunk arrives. The same check fails ondev.waitwithtimeout.timeout: 200command reportstimeoutafter about 220ms. With a process that ignores SIGTERM,time.completedmatches when the timeout fired, andremoveduring the kill returns in a few ms.dev. The 7 bash-syntax failures on macOS also fail ondev.bun typecheckinpackages/coreis clean.