Skip to content

fix: a ticket's step that has exited is not taken for running though nothing has reaped it - #983

Merged
kid7st merged 2 commits into
mainfrom
fix/ticket-zombie-step
Nov 8, 2026
Merged

kid7st merged 2 commits into
mainfrom
fix/ticket-zombie-step

Conversation

@kid7st

@kid7st kid7st commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

running(pid) decided whether a ticket's step is alive with kill(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:

  • --check never sees the step gone, so its ticket stays working or need_user_action until 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 state Z and kill(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 /proc says what a process is (Linux), a process kill(pid, 0) answers for is taken for stopped when its state is Z or X, 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 at running says 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 --fork without --mount-proc, and sandboxes set up that way). There /proc/<pid> is not the process kill(pid, 0) names: measured in a Linux container, a live child (pid 8) answered kill(8, 0) while /proc/8/stat was ENOENT, and /proc/self/stat said 18 for a process whose process.pid was 1. The first version of this PR took that ENOENT for gone, so a running step was reported stopped by --check, and stop, a newer attempt and join all passed it by (stop returns at once for a process that is not running, so it was never signalled).

/proc is now read only when /proc/self/stat begins with process.pid (procfsIsOurs); otherwise running is kill(pid, 0) alone, as before this PR. Measured in the same container, a live step's ticket under unshare --pid --fork: failed before this fix, need_user_action after; in the same namespace need_user_action both times, and a zombie's ticket still failed.

Tests

A step whose process is a zombie (sh starts a child that exits, then becomes a sleep that never waits for it), named x) S y so its name holds both, is failed by checkTicket with "it stopped". It runs where /proc/self/stat exists (CI, Linux), and is skipped on macOS. On Linux it failed before the fix (need_user_action). procfsIsOurs is tested on the stat of this process, of another namespace's, and of a pid that begins with this one's digits (12 for 1); 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 a sleep named 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.

@kid7st
kid7st requested a review from Sukitly as a code owner November 8, 2026 18:51
@kid7st kid7st self-assigned this Nov 8, 2026
@github-actions github-actions Bot added the bug Something isn't working label Nov 8, 2026
@kid7st
kid7st merged commit b61773a into main Nov 8, 2026
11 checks passed
@kid7st
kid7st deleted the fix/ticket-zombie-step branch November 8, 2026 19:21
@kid7st kid7st mentioned this pull request Nov 9, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant