Repository navigation
fix: a ticket's step that has exited is not taken for running though nothing has reaped it - #983
Merged
Merged
Conversation
…nothing has reaped it
Merged
kid7st
added a commit
that referenced
this pull request
Nov 9, 2026
## Release v0.30.2 Bump `@fastagent-sh/fastagent` from 0.30.1 to 0.30.2. ## Fixed - **A ticket's step that had exited could be taken for running** (#983). Whether a step (`login`, `channels setup`, `deploy`'s sign-in) still ran was asked with signal 0, which a zombie answers too: in a container whose PID 1 does not reap orphans, a step that had exited stayed a zombie, so `--check` never saw it gone and a stop waited its full 5 s. Where `/proc` belongs to the process's own PID namespace (Linux), a process that has exited is now taken for stopped; where it does not (a sandbox that leaves a process the host's `/proc`), signal 0 alone decides, as before. ## Dependencies - `effect`, `@effect/platform-node` and `@effect/platform-bun` 4.0.7 → 4.1.2 (#937). The breaking changes of Effect 4.1 (`effect/Crypto`, Bun's default idle timeout, `MaxBodySize` on buffered body reads) do not touch the APIs FastAgent uses, as it sets the Bun idle timeout and the Agent API's body limit itself. `effect` was already `^4.0.7`, so an install of 0.30.1 already resolved 4.1.x; this release is built and tested with it. - Patch releases of `@smithy/signature-v4`, `@hono/node-server`, `fastify`, `knip`, and `vitest` 5.1.0. ## Verification - `npm run lint`, `npm run typecheck`, `npm test` (195 files, 2383 passed, 3 skipped), `npm pack --dry-run --json`. - With the new dependencies: the build, the public imports and the CLI, and every Bun check (`test/bun/*.ts`) on Bun 1.4.0 and 1.4.4. ## After merge Create and publish the GitHub Release `v0.30.2` from the merged commit, with the sections above as its notes. The protected publish workflow verifies and publishes the package to npm via Trusted Publishing. This PR itself does not tag or publish.
This was referenced Nov 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
running(pid)decided whether a ticket's step is alive withkill(pid, 0), which also succeeds for a zombie: a process that has exited and that its parent has not reaped.When it matters
A step is started detached by the command that starts it, which then exits, so the step's parent becomes PID 1. Where PID 1 does not reap orphans (a Docker container without
--init, a coding agent's sandbox that runs its shell as PID 1) a step that has exited stays a zombie for good. Then:--checknever sees the step gone, so its ticket staysworkingorneed_user_actionuntil its deadline instead of failing with "it stopped";stop(a stop on request, a newer attempt, a start given up on) waits its full 5 s for a process that is already dead.Measured in a Linux container whose PID 1 was
sleep: after the step's group was sent SIGTERM, the step was in stateZandkill(pid, 0)still answered. CI's runner reaps, so it does not fail there. macOS's launchd reaps an orphan at once, so a step is a zombie there only for an instant.The fix
Where
/procsays what a process is (Linux), a processkill(pid, 0)answers for is taken for stopped when its state isZorX, or when it is gone since. The state is read after the last)of/proc/<pid>/stat, since the command name before it is in parentheses and may hold spaces and parentheses. macOS is not asked, for the reason above, and the comment atrunningsays so.Found in review: a /proc that is not this process's
A process in a PID namespace of its own can be left the host's /proc (
unshare --pid --forkwithout--mount-proc, and sandboxes set up that way). There/proc/<pid>is not the processkill(pid, 0)names: measured in a Linux container, a live child (pid 8) answeredkill(8, 0)while/proc/8/statwasENOENT, and/proc/self/statsaid 18 for a process whoseprocess.pidwas 1. The first version of this PR took thatENOENTfor gone, so a running step was reported stopped by--check, andstop, a newer attempt andjoinall passed it by (stopreturns at once for a process that is not running, so it was never signalled)./proc is now read only when
/proc/self/statbegins withprocess.pid(procfsIsOurs); otherwiserunningiskill(pid, 0)alone, as before this PR. Measured in the same container, a live step's ticket underunshare --pid --fork:failedbefore this fix,need_user_actionafter; in the same namespaceneed_user_actionboth times, and a zombie's ticket stillfailed.Tests
A step whose process is a zombie (
shstarts a child that exits, then becomes asleepthat never waits for it), namedx) S yso its name holds both, is failed bycheckTicketwith "it stopped". It runs where/proc/self/statexists (CI, Linux), and is skipped on macOS. On Linux it failed before the fix (need_user_action).procfsIsOursis tested on the stat of this process, of another namespace's, and of a pid that begins with this one's digits (12for1); mutations dropping the space after the pid, or trusting any /proc, fail it. Mutations of the zombie check, each failing it on Linux: zombies taken for running again, the state read by splitting on spaces (survived at first, with asleepnamed child; the name now holds a space and a parenthesis), the state read after the first). The whole suite passed in a Linux container (Node 26.13.0) and on macOS.