Skip to content

fix: on Windows, stop a late host after a failed create in more cases, and name the session in text mode - #324

Merged
ThomasK33 merged 28 commits into
mainfrom
fix/round3-lifecycle
Nov 4, 2026
Merged

ThomasK33 merged 28 commits into
mainfrom
fix/round3-lifecycle

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Why

ThomasK33 asked to ship every valid review finding of this autopilot run in a follow-up PR, with no parking issues. This PR ships the lifecycle follow-ups from #284, #288, #294 and #312 (#264):

  1. On Windows, a create that gives up sends no SIGTERM while the manifest names no program, so two cases left a host that started late serving the failed session. Both are closed here:
  2. fix: create stops its host and names the session on every error of its readiness wait #294: in text mode, a failed create did not name its session. JSON mode has details.sessionId.
  3. fix(host): read a terminal session next to the destroy marker as destroyed #284: the lifecycle model had no reconciler that dies between its write and its repair. The marked repair had no comment on why it removes both sockets without a liveness check.
  4. fix(host): keep sockets under a per-uid root and remove a dead host socket #288: the comments on the old socket-path fallback did not name the release that can drop it. Hosts of 0.5.0 and earlier listen at the old path, and 0.6.0 (chore(release): 0.6.0 #172) is the first release with the per-uid root, so the fallback can go in 0.7.0.

What changed

  • The host's marker watch (runHost in src/host/hostMain.ts, watchCreateFailed). Right after the start-up check, the host checks every 100 ms whether its creator still runs and then whether the create-failed marker exists. On the marker it calls startShutdown, as for a SIGTERM. One check after the creator has exited finds no marker, and then the watch stops. A probe that throws, such as EPERM from a process of another user that took the creator's pid, and a zombie creator (readProcStat shows Z on Linux) count as an exited creator. This closes the case where the host's start-up check came before create gave up and create's manifest read then failed.
  • Marker retries in create (src/cli/commands/create.ts). The give-up catch writes the marker through the new writeMarkerOrLog, which logs a failed write and reports whether it landed. On Windows, while the write fails, the manifest names no program and the host runs (hostRuns, where a probe that throws counts as an exited host), create waits 100 ms, writes the marker again and reads the manifest, up to 50 times. The stop after the loop ends a host that the last read names. A marker that lands reaches the host by its start-up check or its watch, because create still runs.
  • Text mode: a failed create first writes Session <id> failed to start. to stderr (writeHumanErrorLines in src/cli/output.ts), then the error line.
  • Comments: the old-path fallback names 0.7.0 as its removal release in socketRoot.ts, lifecycle.ts, sessionPaths.ts and rpcClient.ts. The marked repair in reconcileSession says that only recordDestroyed writes the marker, for a terminal manifest or once the host and the program's group are dead, so the host has ended or is ending and the repair needs no liveness check.
  • Lifecycle check (.agents/skills/verify-session-lifecycle):
    • ListDiesBeforeRepair: a reconciling list can die between its write and its repair. R18 allows the destroyed record to be lost only until the next reconcile reads the manifest. mutant-ReconcileRepairsMarked now breaks R18, and WitnessDeadRepairRepaired shows the heal.
    • New guard HostPollsCreateFailed (the watch), with the Windows manifest read that can fail. mutant-HostPollsCreateFailed-win32 breaks R17.
    • New guard CreateRetriesMarker (the retries). On Windows the marker write can fail, CreateStopsHostOnWindows goes on to "retry", and CreateRewritesMarker writes and reads again. A create whose last write failed ends as "exhausted", and R17 does not cover it (SKILL.md Scope). mutant-CreateRetriesMarker-win32 breaks R17, and witness-WitnessMarkerRetried-win32 reaches the retry.
  • Docs: docs/USAGE.md, SKILL.md, src/cli/commands/README.md, src/cli/README.md and src/host/README.md.

Behavior changes for CLI and JSON users

  • No JSON shape, error code or exit code changes.
  • Text mode: a failed create prints Session <id> failed to start. on stderr before the error line.
  • On Windows, a failed create whose marker write fails can take up to about 5 s longer (50 retries, 100 ms apart) while its host runs.
  • On Windows, a host that started late after a failed create ends the session in more cases: when create's manifest read fails, and when its first marker writes fail.
  • For contributors: summed over all runs, the model now has about 2.1 times as many states as main's. So each CI lifecycle-model share takes longer than the 13 to 17 min that the comment in ci.yml states, and I expect about 27 to 36 min, within the job's 50 min timeout. A follow-up PR will put the times measured on this PR into that comment and into SKILL.md.

Not verified

Real Windows behavior is not verified. CI has no Windows job. The Windows paths run on Linux with preloads in the create process only: test/fixtures/fake-win32-create.ts makes process.platform read win32, and ChildProcess.kill sends SIGKILL, as TerminateProcess does. fail-manifest-read-after-marker.ts fails the manifest reads once the marker exists, and fail-first-marker-write.ts fails the first marker write with EIO.

When all 51 marker writes fail, for example on a full disk, a Windows host that the manifest does not name yet can still serve the session. docs/USAGE.md and the SKILL.md Scope section state this.

Validation

  • Red first. Each fix commit follows a test commit, and I ran each test at its test commit:
    • 00b3d5ee (text mode): create.test.ts "names the session in text mode when the host never answers" and output.test.ts "writes the lines to stderr, without color when color is off" fail.
    • bfbf4664 (the watch): the integration case a create on Windows whose manifest read fails after it gave up fails. The host and the program are still alive 20 s after create exited.
    • 6d2c217a (the retries): the 4 new create.test.ts cases fail: they find 1 marker write where 2, 3 or 51 were due, or no liveness check of the host. The give-up case on Windows, when its first marker write fails fails: the host and the program are still alive 20 s later.
  • test/unit/host/hostMarker.test.ts runs runHost in the test process. Without watchCreateFailed(), "stops a serving host when the marker appears while its creator runs" fails. With a watch that never stops, "keeps serving when the marker appears after its creator has exited" fails.
  • facts.mjs mutation checks of CreateRetriesMarker: the read before the write in the loop, no isProcessAlive(host.pid), no loop, a throw before the stop, the first read before the first write, and a writeMarkerOrLog that always reports a failure. Each reads FALSE (the last two also make HostChecksCreateFailed FALSE, and the throw also makes CreateStopsOnGiveUp FALSE).
  • check.sh --update-coverage on the head, after the merges of fix(gc): keep a session whose program group still runs, and model-check the lifecycle in CI #302 and fix(gc): freeze a fork chain behind its zombies before gc reads it ended #326: all 85 runs as expected. current 64,052,288 distinct states, current-no-token 63,127,914, current-win32 53,200,220. No guard reads FALSE (38 guards). Eleven runs explore more than 20 million states each. New and changed runs of this PR:
    • mutant-ReconcileRepairsMarked breaks R18 in 24 states, and witness-WitnessDeadRepairRepaired is reached in 21.
    • mutant-HostPollsCreateFailed-win32 breaks R17 in 11 states, and witness-WitnessMarkerPolled is reached in 11.
    • mutant-CreateRetriesMarker-win32 breaks R17 in 6 states, and witness-WitnessMarkerRetried-win32 is reached in 6.
    • mutant-CreateStopsOnGiveUp still breaks R17 (5 states). An earlier run of this branch had it pass, because the model let the marker write fail only on Windows, and with the watch a marker that always landed made the SIGTERM redundant. 9658e3d4 lets the write fail on every platform.
  • Coverage synced in 60006876. check.sh --drift-only: covered code matches the sync record.
  • Gates on the head: typecheck, lint and format pass. Unit 2033/2033. lifecycle and cli integration 148/148 on the merge of fix(gc): freeze a fork chain behind its zombies before gc reads it ended #326 (vitest run --maxWorkers=1 --retry=2). CI runs the full suite.
  • Round 1: Codex clean in both lanes on c8f43008. CI on macOS failed in hostMarker.test.ts: the host resolves its Home to the real path and derives the socket path from it, and on macOS tmpdir() is a symlink. So the two cases that need a serving host looked for the socket at another path. 1159cb73 gives the tests the real Home path. A TMPDIR that is a symlink reproduces the failure on Linux, and the cases pass with the fix. The same macOS run also failed test/unit/evals/scheduler.test.ts (input-order settlement), which this PR does not touch.
  • Round 2: Codex P2 on 1159cb73 (security lane clean): the watch's liveness probe throws EPERM when a process of another user takes the creator's pid, and that uncaught error in the timer ends the host. The red test c4490868 makes the probe answer EPERM. Before the fix, vitest reported 3 unhandled EPERM errors from the timer. The fix e3b15622 counts any probe error as an exited creator, and facts.mjs reads the new watch. A catch that keeps the creator alive, and a marker branch without the shutdown, both read FALSE.
  • Round 3: Codex P2 on e3b15622. The probe of the host in create's retry loop can throw EPERM in the same way, and then the error left the give-up catch, so create reported INTERNAL_ERROR without the session id. The red test cb5c98e0 got the raw EPERM error. The fix af541001 adds hostRuns, which counts any probe error as an exited host, and facts.mjs reads it for CreateRetriesMarker. A hostRuns that returns true on an error reads FALSE. Unit 2016/2016. test/unit/evals/scheduler.test.ts failed once in a full local unit run under load and passed 5 of 5 times alone. This PR does not touch it.
  • Round 4: Codex P2 on af541001: coverage.json lacked the hostRuns fingerprint. The sync commit was still due, and the next sync commit added it.
  • Round 5: Codex clean in both lanes on 8d742ffc, which merges fix(gc): keep a session whose program group still runs, and model-check the lifecycle in CI #302. f2f2a7ce replaced the fixed 300 ms and 500 ms waits in hostMarker.test.ts: the cases spy on the timers and wait until clearInterval runs on the host's 100 ms watch.
  • Verifier notes on 8d742ffc, folded in before round 6:
    • f64e17a1 corrects the marked-repair comment: recordDestroyed also writes the marker for a terminal manifest, where destroy stops no host.
    • 7563fe19 adds the order test. The creator writes the marker and exits between the watch's probe and its check. With the check before the probe, the case fails. It also adds a zombie-creator case, which fails on 8da31541.
    • e030d75d counts a zombie creator as exited, and facts.mjs reads the new probe. A probe that tests another state reads FALSE.
    • fix(gc): freeze a fork chain behind its zombies before gc reads it ended #326 then changed the model again, and I merged main (7c9e06f8).

Refs #284, #288, #294, #264


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

A marker that appears while the creator runs stops a serving host. One
that appears after the creator has exited does not: the watch has
stopped. Each case fails without the code that it covers.
…t serving

The new give-up case fails its first marker write with EIO. create then
sees no program in the manifest, sends nothing, and the host that starts
late finds no marker and serves. The unit cases ask create to retry the
write, to stop the host once the manifest names the program, and to stop
retrying after 50 writes or once the host has exited.
… the program record lands

A create that gives up on Windows sends no SIGTERM while the manifest
names no program, so only the marker can stop a host that starts late.
When the marker write fails, create now writes it again every 100 ms,
up to 50 times, and reads the manifest after each write. It stops when
the marker lands, stops the host once the manifest names the program,
and stops retrying once the host has exited.

The unit case for the stop now finds the program by the number of
marker writes, because the readiness loop also reads the manifest.
On Windows the marker write in CreateGivesUp can now fail. With the new
guard CreateRetriesMarker, which facts.mjs reads from the give-up catch,
CreateStopsHostOnWindows goes on to "retry", and CreateRewritesMarker
writes and reads again until the marker lands, the read names the
program, or the host is gone. A create whose last write failed ends as
"exhausted", which R17 does not cover. New runs: the win32 mutant breaks
R17, and a win32 witness reaches the retry. Docs: SKILL.md, USAGE and
the commands README.
With the host's watch, a marker that always landed off Windows made the
SIGTERM redundant, so mutant-CreateStopsOnGiveUp passed. The write can
fail on any platform, and off Windows the SIGTERM is then what stops the
host: the mutant breaks R17 again.
@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 Nov 4, 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-11-04T12:55:06.640579Z 6000687 Manual request
🔒 Security Review ✅ Completed 2026-11-04T12:54:31.510951Z 6000687 Manual request
ℹ️ 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

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: c8f43008f8

ℹ️ 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

Copy link
Copy Markdown

🛡️ Codex Security Review

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

Reviewed commit: c8f43008f8

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.

The host resolves its Home to the real path and derives the socket path
from it. On macOS tmpdir() is a symlink, so the tests looked for the
socket at another path, and the two cases that need a serving host
failed there. A TMPDIR that is a symlink reproduces it on Linux.
@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: 1159cb73b7

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: 1159cb73b7

ℹ️ 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
When a process of another user takes the creator's pid, the liveness
probe of the marker watch throws EPERM inside its timer. In the host
that is an uncaught exception, and here vitest reports it as unhandled
errors, so the run fails.
The marker watch probes the creator's pid inside its timer. A process of
another user that took that pid answers EPERM, isProcessAlive throws it,
and the uncaught error ended the host. The watch now counts any probe
error as an exited creator, checks for the marker once more and stops.
facts.mjs reads the new watch, and the docs say how it treats the error.
@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: e3b156226b

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: e3b156226b

ℹ️ 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/cli/commands/create.ts Resolved
The two cases where the watch must not stop the host waited 300 ms and
500 ms. They now spy on the timers, wait until clearInterval runs on the
host's 100 ms watch, write the marker and check at once that the host
still serves: a stopped watch never checks again. With a watch that
never stops, or a probe error that leaves the timer, they fail.
# Conflicts:
#	.agents/skills/verify-session-lifecycle/SKILL.md
#	.agents/skills/verify-session-lifecycle/coverage.json
#	.agents/skills/verify-session-lifecycle/scripts/check.sh
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

# Conflicts:
#	.agents/skills/verify-session-lifecycle/SKILL.md
#	.agents/skills/verify-session-lifecycle/coverage.json
#	.agents/skills/verify-session-lifecycle/scripts/check.sh
@chatgpt-codex-connector

Copy link
Copy Markdown

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

Reviewed commit: 8d742ffcee

ℹ️ 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

Copy link
Copy Markdown

🛡️ Codex Security Review

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

Reviewed commit: 8d742ffcee

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.

…ir comment

recordDestroyed writes it for a terminal manifest too, where destroy
stops no host, so the host can still be in its shutdown tail. It is
ending either way, so the repair needs no liveness check.
… zombie creator

A creator that writes the marker and exits between the watch's probe and
its check must still stop the host: with the check before the probe, the
watch stops without the marker. A zombie creator has exited, but
kill(pid, 0) still finds it, so the watch ticks for as long as its
parent does not reap it. The zombie case fails on the head.
kill(pid, 0) finds a zombie until its parent reaps it, so the watch
ticked at 10 Hz for as long as a parent that never waits lived. The
watch now also counts a creator that readProcStat shows as Z as exited.
facts.mjs reads the new probe, and the docs name it.
@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: 600068767c

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: 600068767c

ℹ️ 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
ThomasK33 added a commit that referenced this pull request Nov 4, 2026
…runs at once

A timeout raise alone leaves #324's 48 min share at the merge queue's
60 min limit. check.sh waits for each batch of CHECK_JOBS runs, so a
share took the sum of its longest runs. With 8 shares and CHECK_JOBS 12,
every share starts all its runs at once and takes about as long as its
longest run. The job timeout goes to 45 min.
ThomasK33 added a commit that referenced this pull request Nov 4, 2026
…share times

On the model of #324 the 8 shares took 15 to 43 min. The 43 min share
holds only the longest run, which TLC checks with 1 worker, so no share
split makes it shorter, and 45 min left too little room.
@ThomasK33
ThomasK33 added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 99cb181 Nov 4, 2026
21 of 22 checks passed
@ThomasK33
ThomasK33 deleted the fix/round3-lifecycle branch November 4, 2026 15:43
@github-actions github-actions Bot mentioned this pull request Nov 4, 2026
ThomasK33 added a commit that referenced this pull request Nov 4, 2026
check.sh --update-coverage rewrote coverage.json from facts.mjs at the merge
of main (#324) into #327. All 41 constants hold, and the 39 guards read TRUE.
77 of the 90 TLC runs finished locally as expected before the host rebooted.
CI runs all 90.
ThomasK33 added a commit that referenced this pull request Nov 4, 2026
The times of 996c875 were measured with the model of #324. #327 adds
runs, and a share still holds 10 to 12 runs, of which at most 2 expect
pass.
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