Skip to content

fix(host): close the idle, EPERM and in-session gaps of destroy - #216

Merged
ThomasK33 merged 43 commits into
mainfrom
fix/destroy-reach
Oct 30, 2026
Merged

ThomasK33 merged 43 commits into
mainfrom
fix/destroy-reach

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Why

The operator wants every follow-up of #203 landed. destroy must stop what the session started, and it must not report destroyed while the program's process group still runs. This PR fixes three of the seven cases in #203 (3, 6 and 7). A second PR covers the other four.

What changed

  • The idle time-out now hangs up the program and kills its process group 2 seconds later, like shutdown does (case 3). The session still ends exited.
  • An EPERM from a group kill now means that a process of another user remains (case 6). The host no longer crashes on it. The host and reconcile wait until the group is gone before they record destroyed or clear childPid. Until then, destroy fails with the existing HOST_TIMEOUT.
  • A destroy that runs inside the program's session now runs again in a new session (case 7). This covers a destroy run by the program and one typed into its shell. It prints its result, and the session ends destroyed.
  • The lifecycle model check covers all three cases. It adds rules R13 LiveGroupStaysNamed and R14 DestroyOutlivesProgram, plus five guards.

Scope

  • This PR covers cases 3, 6 and 7. The next PR covers cases 1, 2, 4 and 5: leftovers of a normal exit, the start-up gap, setsid escapes and reused group ids.
  • One case 7 gap stays on macOS. A destroy typed into a shell job whose environment lost AGENT_TTY_SESSION_ID still dies there, because macOS has no portable way to read a session id. Linux compares the session id.

Tradeoffs

  • Case 7 runs the command again in a new session. The other option was to print first and kill its own group last. But the kernel sends SIGHUP when the session leader exits, so an in-session CLI dies before its own kill. That order also writes destroyed while the group still runs.

Blast Radius

The change touches destroy, reconcile (list, inspect and gc) and the idle time-out. A session whose group holds a process of another user now stays destroying or running. Each reconcile of such a session takes about 5 seconds. A destroy whose environment carries the target's AGENT_TTY_SESSION_ID runs again in a new session even when the caller is outside the session, which adds about 400 ms. The re-run prints its result to the caller's stdout. When the program itself runs destroy, that stdout is the session's terminal. CLI JSON and error codes stay the same.

Verification

  • I reproduced each case on the real CLI, before and after the fix.
    • Case 3: the idle program still ran at 9 seconds. Now it ends exited by SIGKILL at about 3 seconds.
    • Case 6, with a kill shim that returns EPERM: before, the host crashed and the CLI printed a raw stack. Now destroy returns a HOST_TIMEOUT envelope.
    • Case 7: the inner destroy printed nothing. Now it prints ok: true and destroyed: true.
  • The new unit and integration tests fail on main and pass here. The model check exits 0, with 34 runs and 1,789,073 states. A scratch break of each new guard turns one run UNEXPECTED.

Refs #203


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: xhigh

…unning

The idle time-out only sends SIGHUP. A program that ignores it keeps
running, and the session stays running forever. This test fails on main:
the session is still running 10 s after a 300 ms idle time-out.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
The idle time-out only sent SIGHUP. A program that ignores SIGHUP kept
running, so the session never ended.

The idle time-out now hangs up the program the way shutdown does: SIGHUP
now, and a kill of the program's process group after the 2 s grace.
Both call sites share one helper and one timer. The host clears the
timer when the PTY exits, so a program that exits on SIGHUP keeps its
leftover background processes, as before. The final status of an idle
time-out stays exited.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
When every process left in the program's group belongs to another user,
kill(-pgid) fails with EPERM. The group helpers throw it. The host dies
from the throw in its kill timer or in handlePtyExit, and destroy,
destroy --force and the reconcile after a host crash end with a raw Node
stack and no JSON envelope.

A preload fixture makes every group signal fail with EPERM, which stands
for such a group. The integration tests fail on main with an empty
stdout. The unit tests show the probe and the kill throw EPERM, and that
reconcile records the session failed although its group survived the
kill.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
When every process left in the program's group belongs to another user,
kill(-pgid) fails with EPERM. The group helpers threw it, so the host
died in its kill timer or in handlePtyExit, and every CLI path printed a
raw Node stack instead of a JSON envelope.

EPERM now means "a process of the group exists" for the probe, and the
kill ignores it as it ignores ESRCH. A kill that returns 0 is not proof
either: with `sudo cmd &` the group holds sudo, which we may signal, and
a root child, which survives. So every writer of destroyed or of
childPid null first waits until the group is gone:

- handlePtyExit records destroyed only after the wait. If the wait gives
  up, the manifest stays destroying and the host still shuts down.
- reconcileSession returns without a write while the group lives, so a
  later reconcile retries.
- destroy --force needs no change: its wait now times out with
  HOST_TIMEOUT.

`wait` read the exit code from the session record, which handlePtyExit
now fills in later for a destroyed session. It reads the PTY's exit
code instead, so `wait --exit` during a destroy still reports it.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
The test read the manifest as soon as destroy returned. The host gives
up on the group at about the same time as destroy, so a host that wrote
destroyed after a failed wait could still pass. The test now waits for
the host to exit first. It fails if handlePtyExit records destroyed
without the wait, or after a wait that gave up.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
The idle time-out test had its own copy of the group probe that the
lifecycle tests define. The case 7 tests need it too, outside the
describe block that held it. One copy now lives in test/helpers.ts.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
A program that runs `agent-tty destroy "$AGENT_TTY_SESSION_ID"` runs the
CLI inside its own process group. The destroy kills that group, or the
kernel hangs it up when the session leader exits, so the CLI dies before
it prints. With --force the session also ends exited instead of
destroyed. Both tests fail on main with no output.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
A program that runs `agent-tty destroy "$AGENT_TTY_SESSION_ID"` starts
the CLI inside its own process group. --force kills that group, and a
plain destroy ends the program, whose exit makes the kernel hang up the
group. Either way the CLI died before it printed, and with --force the
session ended exited instead of destroyed.

destroy now compares its own process group id with the manifest's
childPid. When they match, it runs the same command again in a new
session, outside the group, waits for it and exits with its exit code.
That copy takes the normal path and prints the result. Linux reads the
group id from /proc/self/stat, other POSIX systems ask ps, and Windows
has no process groups.

Rejected: print first and kill the own group last. A plain destroy shows
that the CLI dies from the kernel's SIGHUP when the session leader dies,
before any self-kill could run, and that order would write destroyed
before the group is gone.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
The case 7 integration test cannot tell the group id from the parent pid
or the session id: in that test all three are the program's pid. This
test compares the lookup with `ps -o pgid=` in the test process, where
they differ. It also runs the ps branch on Linux, since no CI job runs
the tests on macOS.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
lifecycle.ts imports stat from node:fs/promises. The own-group lookup
named its /proc/self/stat text stat too, which hid that import inside
the function.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
Bash runs a typed command as a job in its own process group. So a
destroy that a user types into the session's interactive shell does not
share the program's group, and the group check does not see that it
runs inside the program. The CLI still dies when the shell hangs up its
jobs or the shell's exit hangs up the foreground job. The job's session
id is the program's pid, though.

The new integration tests run only on Linux, where destroy can read the
session id. They fail at the group check with no output. The unit test
now expects the Linux lookup to match the caller's session.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
…sion

A shell with job control puts each typed command in a process group of
its own. So a destroy typed into the session's interactive shell did
not match the program's group, and it died without output as before.

The program leads its session, so every process inside that session
has the program's pid as its session id. On Linux, destroy now compares
its own session id, the field after pgrp in /proc/self/stat, with the
manifest's childPid. That covers both a CLI in the program's group and
a job of the program's shell. The helper is now
cliRunsInsideProgramSession, and the rerun is rerunInNewSession.

Other POSIX systems still compare the process group through
`ps -o pgid=`, because POSIX ps has no session id field. So macOS still
misses a destroy typed into an interactive shell's job. That gap is
documented here, not fixed. Windows is never inside.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
The program gets AGENT_TTY_SESSION_ID at spawn, but its pid reaches the
manifest only with the host's start-up write. A destroy that the program
runs before that write reads childPid null, so cliRunsInsideProgramSession
says it runs outside and destroy does not re-run itself. With --force it
kills the host, the PTY hangs up the program's group, and the CLI dies
before it prints. The session then reads failed.

The integration test holds the host's start-up rename with replay-hold,
so the destroy always runs in that window. It fails with no output. The
unit test expects the variable to mark a caller inside the session.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
…orded

The program has AGENT_TTY_SESSION_ID from its spawn on, before any
manifest names its pid. So cliRunsInsideProgramSession now returns true
when that variable equals the session id, and only then compares the
manifest's childPid with the CLI's session id or process group. A
destroy that the program runs before the host's start-up write now
re-runs in a new session, prints its result, and records destroyed.

The id comparison stays: `create --env AGENT_TTY_SESSION_ID=x` overrides
the injected value. A needless re-run is harmless, because the check only
routes the CLI and never decides a kill. As a side effect, macOS now also
sees a job of the program's interactive shell as inside, as long as the
job kept the variable.

The re-run copy gets an environment without AGENT_TTY_SESSION_ID.
Without that, the copy would match the variable again and start yet
another copy, without end.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
The lifecycle model no longer matched the code after the fixes for
#203 cases 3, 6 and 7. KillsProgramGroup read FALSE
because recordDestroyed moved out of handlePtyExit's destroying block,
so `current` failed.

The model now has a process of another user in the program's group,
which our SIGKILL does not reach. The host, reconcile and destroy --force
wait for the group, and those waits can give up. A destroy can also run
inside the program's session. New guards: ProbeCountsDeniedGroup,
HostWaitsForGroup, ReconcileWaitsForGroup, ReRunsInNewSession and
InsideCheckReadsEnv. New rules: R13 LiveGroupStaysNamed and R14
DestroyOutlivesProgram. R11 now counts the process of another user.
Without KillsProgramGroup the waits read the group gone, as with the pid
waits before the group kill, so its mutant still breaks R11.

coverage.json is not updated here, so check.sh exits 3 until the sync
commit.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
…PtyExit

handlePtyExit has two blocks under `currentStatus === 'destroying'`: the
group kill and, after the wait, recordDestroyed. The KillsProgramGroup
pattern kept only the last block it visited. So a scratch edit that left
recordDestroyed under the bare status check also turned
KillsProgramGroup FALSE, and that run did not show HostWaitsForGroup on
its own. The pattern now reads TRUE when any such block kills the group.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
The skill now lists R13 and R14, the new covered code, the five new
guards with the race each one prevents and a replay of it, and the run
counts. Scope says what the model assumes for #203 cases
3, 6 and 7, and it keeps cases 1, 2, 4 and 5 as open. Each replay was
run against this tree and showed the safe result in its last comment.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
origin/main is at ce85b3f. #212 changed the Scope section of the
lifecycle skill: gc's rm now retries, and the artifact commands no
longer bring back a removed session folder. The conflict was in the
"It leaves out" paragraph. The resolution keeps this branch's open
cases of #203 and takes #212's artifact item and its new
paragraph. The gc rm bullet and the GcRemoves comment merged cleanly.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
…ixes

The model describes the merged tree: the destroy reach fixes for
#203 cases 3, 6 and 7, and #212's gc rm retries, which the
model still takes as one step. Each of the five new guards was broken
once in a scratch edit of the code, and each time `current` turned
UNEXPECTED on the rule that the guard protects. This commit becomes the
sync commit.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T18:29:06.089938Z 248dad3 Manual request
🔒 Security Review ✅ Completed 2026-10-30T18:27:30.836562Z 248dad3 Manual request

Security findings

Advisory findings (2)

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9fbd31ed2f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/host/hostMain.ts Resolved
@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 9fbd31ed2f

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

When the idle time-out hangs up a program that exits on SIGHUP at once,
a background process in its group that ignores SIGHUP keeps running
after the session ends exited. This test reproduces that case and fails
on 9fbd31e.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
…hang-up

handlePtyExit cleared the 2 s group kill and recorded exited, so a
background process that ignored SIGHUP outlived the idle time-out. The
exit after an idle hang-up now kills the group, as the destroying branch
does. A program that exits on its own still keeps its background
processes.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
…oup kill

handlePtyExit now kills the program's group when the program exits on
the idle hang-up. The model still describes the code: that kill only
ends processes, and the normal exit drops them in the model anyway. The
Scope bullet, the timer paragraph and the model comments now name the
kill, and coverage.json records runHost at this commit.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 90a630cb0d

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛡️ Codex Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: bbc95b7bcb

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Comment thread src/host/lifecycle.ts Resolved

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bbc95b7bcb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/host/lifecycle.ts Resolved
Comment thread src/host/lifecycle.ts Resolved
With /proc mounted hidepid=2 (or hidepid=invisible), the scan cannot list
the processes of other users. A group that holds a zombie of ours and a
hidden live process of another user then reads gone, so destroy records
destroyed while that process runs. These cases fail now.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
hidepid=2 leaves other users' processes out of the /proc listing, so the
scan cannot tell a zombie-only group from one with a hidden live process
of another user. The probe now reads the /proc mount options and, under
hidepid=2 or an unreadable mountinfo, counts the group alive as it did
before the scan. Codex security review raised this on #216.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
The probe now skips its zombie scan when /proc hides other users, so a
group with a zombie reads alive there, as it does on macOS. The model has
no zombies and no /proc scan, so it still describes the code. Scope says
so, and the covered list gains procHidesOtherUsers.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: b158e2bd2a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛡️ Codex Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: b158e2bd2a

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Comment thread src/host/lifecycle.ts Resolved
The zombie scan tests faked /proc but read the host's real mountinfo. On
macOS that file does not exist, so the probe skipped the scan and three
cases failed in quality-gates-macos. They failed the same way on a Linux
host whose /proc has hidepid=2. The fake /proc now comes with a plain
/proc mount line, and a read of mountinfo with no fake line fails ENOENT,
so no case depends on the host.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
hidepid=ptraceable (hidepid=4) also leaves processes of other users out
of the /proc listing, and with two /proc mounts the last one is in
effect. The probe reads only hidepid=2 and the first /proc line, so in
both cases a zombie of ours beside a hidden live process reads gone.
These cases fail now.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
hidepid=ptraceable (hidepid=4) also leaves processes of other users out
of the /proc listing, so the probe now skips its zombie scan there too.
With several /proc mounts the last mountinfo line is the one in effect,
so the probe reads that one instead of the first.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
…ble check

The probe now also skips its zombie scan under hidepid=ptraceable and
reads the top /proc mount. The model has no zombies and no /proc scan,
so it still describes the code. Scope names both modes.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 248dad38a9

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 248dad38a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/host/lifecycle.ts Resolved
Comment thread src/host/lifecycle.ts Resolved
Comment thread src/cli/commands/destroy.ts Resolved
@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 9ff3fd4 Oct 30, 2026
12 checks passed
@ThomasK33
ThomasK33 deleted the fix/destroy-reach branch October 30, 2026 19:15
ThomasK33 added a commit to pmarreck/tui_test that referenced this pull request Nov 1, 2026
…#221)

* feat(host): record the program's session token in program.json

Every group kill trusts the number in session.json, which can name a group
that the kernel has since given to another program. On Linux, setsid()
gives the program's session an autogroup id that never repeats within a
boot, and fork() passes it on, so the boot id and that id prove which
session a process belongs to. The host now captures both right after it
spawns the program and writes them, with the pid, to a private
program.json before its first manifest write and before it awaits
anything. Nothing reads the record yet.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* test(host): show kills that trust a stale or missing childPid

Reconcile and destroy --force kill whatever group the manifest's childPid
names. These tests point the manifest and program.json at an unrelated
group that took the program's group id, and reconcile and destroy --force
kill it. A host that dies before its first manifest write leaves a program
that no manifest names, and destroy --force leaves it running, although
program.json names it.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* fix(host): kill a program group only when program.json proves it

Every group kill now asks one module whether the group is the program's.
A record with a token proves it when a live member of the group has the
program's pid as its session id and the recorded autogroup of the same
boot. Without that proof the kill does not happen, so reconcile and
destroy --force leave an unrelated group that took the id alone. The
record also names a program whose host died before its first manifest
write. The module reads the witness again right before the kill.

A record without a token, or no record, keeps the kill of the manifest's
childPid. The raw group kill moves into the module, so no other code can
call it.

The kill after the program exits on an idle hang-up goes through the
module too.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* test(cli): show destroy leaves the processes of a program that exited

A program that exits on its own can leave a process in its group. destroy
of the exited session records it destroyed and leaves that process
running, although program.json proves it is the program's.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* fix(cli): stop the processes left by a program that exited on destroy

destroy of a session whose program exited, failed or ended during the
destroy now kills the program's group first when program.json proves the
group is the program's, and waits for it to end. If it does not end in
the destroy wait, destroy fails with HOST_TIMEOUT and does not record the
session destroyed. Without a token destroy kills nothing, as before, and
it never kills the hostPid of a finished session.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* refactor(storage): make the sync record write state createParentFolders: false

Since coder#212 the shared writers create a missing parent folder unless the
caller passes createParentFolders: false. writeValidatedJsonFileSync never
creates one, so its type now requires that option, and the program.json
write states it like the other session-folder writes.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* docs: describe what destroy stops and where its proof ends

docs/USAGE.md gets a destroy section: destroy and reconcile kill only a
group that program.json proves is the session's, and they keep the plain
childPid kill where no token exists. It names the limits: processes that
leave the group with setsid or job control, platforms and sessions without
a token, the microseconds between the check and the kill, the startup gap
before program.json exists (3 to 6 ms median, up to 80 ms under load in
this measurement) and gc removing the record. The host and storage
contracts list the new module, path and sync writer.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* refactor(host): name the proven-group read and drop restating comments

Destroy of a finished session now reads its group through
readProvenProgramGroup instead of passing a null childPid, and the helper
that kills leftovers before the destroyed write says so in its name.
ownLinuxSessionId reuses parseProcStat, parseAutogroup refuses text that
names no autogroup, and comments that restated the code or claimed the
program stays unreaped go.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* test(host): show an unexpected autogroup file crashes capture and kill checks

A kernel that prints the autogroup file in another format, or a member
whose file cannot be read, makes parseAutogroup throw. That throw would
end the host at start-up and make destroy or reconcile fail. The tests
expect such a file to carry no token instead.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* fix(host): read an autogroup file it cannot parse as no token

parseAutogroup threw on text in an unexpected format, so a kernel that
prints the file differently would end the host at start-up and make
destroy and reconcile fail. Now such text, and a file that cannot be
read, names no autogroup. At start-up the record then has no token and
today's kill applies. At a check the member is no witness, so the group
reads unproven and gets no kill.

The capture also stops as soon as the child leads its session without
a readable autogroup, instead of polling for the rest of its 250 ms.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* test(cli): show a corrupt program.json hides the session and fails destroy

A program.json that does not parse, or that another version wrote,
makes reconcile and destroy --force throw MANIFEST_VALIDATION_ERROR.
list then skips the session, and destroy --force cannot record it. The
tests expect such a record to prove nothing: no kill, no throw.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* fix(host): treat a program.json that does not validate as unproven

A record that does not parse, or that a newer version wrote, made
reconcile and destroy throw, so list hid the session and destroy could
not record it. Such a record proves nothing about the group, so it now
gets no kill and no throw. A missing record still falls back to the
manifest's childPid, as before program.json existed.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* docs: list program.json among the files of a session folder

The host now writes program.json into every session folder, so the
README line that lists those files names it too.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* test(host): show a reconcile kills a stale childPid after gc removed the folder

When gc removes the session folder between the re-read of session.json
and the read of program.json, the record reads missing, and the kill
falls back to the childPid of the copy that reconcile read before. By
then the group can be gone and its id can name an unrelated group. The
test expects no kill once the session's files are gone.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* docs(skills): model the program's session token in the lifecycle check

The model now has program.json and its token (rec), an environment step
that gives the group id of an ended group to an unrelated group
(reused), a ghost for a kill that reaches that group, the processes a
program leaves when it exits on its own, and the window between the
spawn and the record. It adds the record step of the host, the kill of
proven leftovers in destroy of a terminal session, and the program.json
read before each CLI group kill.

R15 NoUnrelatedGroupKilled says no kill reaches the unrelated group. R16
DestroyedSessionStopped says a destroyed session runs no process of the
program's group, also none it left, unless the host died before
program.json named the program. Four guards protect them:
RecordsProgramSync, ProvesGroupOwnership, DestroyStopsLeftovers and
FallbackNeedsFolder. The last one reads FALSE on this code: the model
found that a kill without a token can use a childPid that gc's removal
left stale.

facts.mjs follows the group helpers into programGroup.ts. With a token
the token check, not the EPERM probe, reads a process of another user
as alive, so the ProbeCountsDeniedGroup mutant now passes.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* fix(host): kill a group that only a childPid names only while its folder exists

Without a token, readProgramGroup falls back to the childPid of a
manifest read made before it. When gc removed the session folder in
between, program.json reads missing too, and the kill used a childPid
whose group can have ended and whose id can name an unrelated group by
then. A session with a record names its childPid only after the record,
so a record missing from a folder that still exists marks a session that
never had one. The fallback now carries the session folder, and
stopProgramGroup and isProgramGroupAlive check it in the same
synchronous block as the kill or the probe.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* docs(skills): read the record and the stop of the leftover helper apart

A scratch break that dropped the stop from stopLeftoversThenRecordDestroyed
also turned DestroyRecordsDestroyed FALSE, though the helper still
records destroyed. DestroyRecordsDestroyed now asks only that the helper
ends with recordDestroyed, and DestroyStopsLeftovers asks for the stop
before it.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* docs(skills): let a program leave a process only while its group runs

mutant-CreateRecordsHostPid ended in a TypeOK error: destroy --force
killed the group of a host it could not name, and the host's exit step
then made up a leftover in a group whose id another group had taken. A
program that exits on its own leaves a process now only when one of its
group still runs.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* docs: describe the unproven record and the folder check of the fallback kill

A program.json that does not validate now proves nothing instead of
failing reconcile and destroy, and a group that only a childPid names
gets the kill only while its session folder exists. The host README and
the destroy section of USAGE.md say so.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* refactor: drop comments that restate the code of the program group

A review of the comments that this branch adds found several that only
restate the code next to them, and one that called a write durable
without an fsync. Those go. The tests name the platform gate once as
capturesToken, clean up a group through one helper that ignores only
ESRCH, and check that the unrelated group is not a zombie instead of
relying on init to reap it.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* docs(skills): record the lifecycle model sync for the program's session token

The check passes at this commit: 42 runs, 4,304,893 states of the
current model, every result as expected. The scratch breaks of
RecordsProgramSync, ProvesGroupOwnership and DestroyStopsLeftovers, and
the code before the folder check of the fallback kill, each turned the
current run UNEXPECTED on the rule that the guard protects.

SKILL.md lists R15 and R16, the covered code of programGroup.ts, one
race section per new guard with a replay against the real CLI, and the
limits that stay: the microsecond gap between the check and the kill,
the start-up gap before program.json names the program, case 4, and
sessions without a token.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* test(host): show the token check crashes on a refused read and trusts a zombie under hidepid=2

PR 1 counts a /proc read refused with EPERM or EACCES as alive, and under
hidepid=2 it does not let a zombie prove the group gone. The token check
throws on a refused read in the scan and in the capture, and reads a group
with only a zombie in view as gone while /proc hides other users, so a
process of another user that /proc hides outlives a destroy that records
destroyed. The tests fake /proc and fail on this code.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* fix(host): give the token check the probe's refused-read and hidepid=2 rules

A /proc read that hidepid=1 or a security module refuses now hides that
process from the token scan and the capture instead of throwing, so it
never witnesses the token. When no live member of the group is in view, a
zombie or a hidden process of another user can stand for one, so the
token check then reads the group as isProcessGroupAlive does: alive while
kill(-pgid, 0) finds a process and /proc hides or refuses other users,
gone when every process in view is a zombie. A live member in view still
decides alone, because the members of a group share one session.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* test(cli): show destroy times out on a member that outlives an idle exit

The coder#216 review found that an idle time-out records exited while a group
member that the kill cannot stop runs, and that a later destroy then
records destroyed. The EPERM preload stands for that member. The session
still ends exited, and on Linux the token now makes destroy wait for the
member and fail with HOST_TIMEOUT instead. On PR 1's code the same test
fails: destroy reports ok and records destroyed.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* docs(skills): check the lifecycle model without the program's session token too

The model took the token as always captured, so it no longer checked the
childPid fallback that macOS, kernels without autogroups and sessions of
older versions use, and mutant-ProbeCountsDeniedGroup passed. A Token
constant now picks the path. current keeps every rule with the token.
current-no-token checks R1 to R14 without it, because R15 and R16 are what
the token adds. There the host kills and probes the program's pid as it is
("bare"), and readProgramGroup falls back to the childPid. The EPERM probe
and the group kill, host wait and reconcile wait mutants break R11 and R13
on that path again, and six witnesses show it reaches the fallback.
facts.mjs also covers the /proc reads of the token check.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* docs: give one measured start-up gap for program.json

USAGE.md said 3 to 6 ms median and up to 80 ms, and the skill said p50
2.5 ms and at most 74 ms, from two different measurements. Both now give
one measurement with its method: 150 sequential create calls on Linux,
timed with performance.now() in a scratch build of runHost from the
return of node-pty's spawn to the renamed program.json, at 1-minute load
averages of 38 to 83. The median was 2.8 ms, the 90th percentile 4.3 ms
and the longest 66 ms. The skill adds that captureProgram took a median
of 1.3 ms and at most 7.3 ms of it.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* docs(skills): record the lifecycle model sync for the token check's hiding rules

The model now runs with and without the program's session token, and the
token check reads a group with no live member in view as the probe does.
check.sh ran all 52 runs as expected: current has 4,304,893 distinct
states and current-no-token 5,218,828. coverage.json records the new
hashes and is formatted, because --update-coverage writes arrays that
oxfmt joins.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* test(host): wait for the killed group in the token kill test

The test read the group right after stopProgramGroup sent SIGKILL. The
process is still running at that moment: 198 of 200 sleeps still showed
a live state in /proc right after kill(-pgid, SIGKILL). Locally the full
/proc scan before the read took long enough, so the test passed, but it
failed in CI test-unit. It now waits until the group reads gone.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* test(host): capture with a boot id that /proc refuses to show

A hardened /proc can refuse the boot id with EACCES or EPERM while it
shows the autogroups. captureProgram then throws, and the host dies at
start-up, so every create fails with HOST_UNREACHABLE. This test fails
until the capture records no token instead.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* fix(host): record no token when /proc refuses to show the boot id

readBootId rethrew EACCES and EPERM, so on a hardened /proc that refuses
the boot id but shows the autogroups, captureProgram crashed the host at
start-up and every create failed with HOST_UNREACHABLE. A refused read now
reads as no boot id, so the capture records no token and the session falls
back to the childPid kill, as on a kernel without autogroups.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* test(host): a member whose token cannot be read reads alive

When /proc refuses the autogroup of a live member that leads the
program's group and session, or refuses the boot id, the token check
cannot tell whether the group is ours. Today it reads that group gone,
so destroy of an exited session records destroyed while the process that
the program left still runs. These tests fail until such a group gets no
kill and reads alive, so the wait ends in HOST_TIMEOUT. A member whose
autogroup reads as another id still reads gone.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* fix(host): read a group whose token cannot be read as alive

judgeProgramGroup gave unproven both to a group that shows another
autogroup or boot id and to one whose token /proc refuses to show. The
second can be the program's own group, and isProgramGroupAlive read it
gone, so destroy or a reconcile could record a terminal status while a
process of the session ran.

A new verdict, unknown, covers a boot id that cannot be read and a member
of the program's session without a readable autogroup. It gets no kill
and reads alive, so the wait ends in HOST_TIMEOUT. A member whose
autogroup or boot id reads as another one stays unproven and reads gone.
facts.mjs now finds the boot id check as its own early return.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* test: run the token tests only where the host can capture a token

The tests that need program.json to hold a token ran on every Linux. On a
kernel without autogroups, or under a /proc that refuses the autogroup or
the boot id, the host rightly records no token, so those tests expected a
kill or a time-out that production skips, and 17 of them failed. They now
run only where /proc/self/autogroup names an autogroup and the boot id
reads, through one capturesToken flag in test/helpers.ts.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* test(host): a zombie leader with a live thread stays alive

When the main thread of the program calls pthread_exit while another
thread runs, /proc/<pid>/stat shows Z, but /proc/<pid>/task shows the
running thread. Both the token check and the scan without a token skip
such a process as a zombie, so destroy never kills it and ends with
HOST_TIMEOUT while the status stays destroying. These tests fail until a
zombie leader with a live thread counts as live, so the token check can
take it as the witness. The fake /proc of the unit tests now lists the
threads of each process.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* fix(host): count a zombie leader with a live thread as live

/proc/<pid>/stat shows Z for a process whose main thread called
pthread_exit while another thread runs. judgeProgramGroup and the scan
without a token skipped it as a zombie, so with a token stopProgramGroup
never killed it, and destroy ended with HOST_TIMEOUT while the program
ran on. readProcStat now gives such a process the state of its first
live thread from /proc/<pid>/task, so it is a member that can carry the
token and gets the kill. A thread read that /proc refuses, or that
vanished, counts as a refused or vanished stat read does.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* test(host): use the shared stat tail in the fake process table

The thread fake added STAT_TAIL, and procShowsGroup kept a local alias
of it. Both helpers now use the constant directly.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* test(host): destroy ends a zombie-leader child that outlives the program

The worse form of the zombie leader: `sh -c 'python3 zombie-leader.py &
wait'`. The SIGHUP of destroy ends sh, the python child ignores it and
shows Z while its thread runs, so the group read empty. Before the fix,
destroy reported destroyed while that process still ran. The test runs
both forms, the zombie-leader program and the child that outlives it, and
waits until /proc shows Z before the destroy. On 0679782, before the fix,
both fail: the first with HOST_TIMEOUT, the second because the group
outlives a destroy that reported success. Both pass since 9e5fc6a.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

* docs(skills): sync the lifecycle model with the unknown verdict and live threads

The model has no refused /proc reads and no zombies, so its actions stay
as they are. The comments of ProvenKill and GroupSeen, and the Scope, now
say what the code does when /proc refuses the boot id or a member's
autogroup (unknown: no kill, reads alive, destroy ends with HOST_TIMEOUT)
and that a process whose main thread exited counts as live while another
of its threads runs. facts.mjs finds the boot id check as its own early
return and covers readBootId and readLiveThreadState. docs/USAGE.md and
src/host/README.md describe the new verdict, the no-token cases and the
thread reads.

check.sh --update-coverage: 52 runs, all as expected, exit 0. current has
4,304,893 distinct states and current-no-token 5,218,828.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant