Repository navigation
fix(windows): stop killing orphan workers behind reused parent PIDs (0.7.2) - #23
Merged
Merged
Conversation
On Windows a detached worker keeps its exited dispatcher's PID in ParentProcessId. Once that PID is reused by another job's agy process, both tree walks used at cleanup (tree() in stream-worker and taskkill /T) took the worker for a descendant and terminated it with no chance to write its status file, so wait reported "crashed" with an empty log. Every Windows CI run since process-tree cleanup landed (#18) had a roughly even chance of losing one worker to this; which test broke was whichever job happened to hold the reused PID. - tree() follows a parent link only when the child was created after its parent; a row born before its recorded parent is an orphan behind a reused PID. PowerShell now reports CreationDate in round-trip ("o") format so the comparison has 100 ns precision instead of one second. - taskkill runs without /T on Windows; descendants are terminated one by one after passing the identity and birth-order checks. - stopExecution never signals its own process and accepts the table source so a test can inject the one thing no kernel lets it choose: a stale parent link over real processes. - Regression test with real processes and real signals, plus unit tests for parseBorn, tree() and the taskkill arguments. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RKgBCYMWQru5SawpfCLxFY
keli-wen
had a problem deploying
to
manual-tests
September 13, 2026 12:49 — with
GitHub Actions
Failure
keli-wen
had a problem deploying
to
manual-tests
September 13, 2026 12:49 — with
GitHub Actions
Failure
keli-wen
had a problem deploying
to
manual-tests
September 13, 2026 12:49 — with
GitHub Actions
Failure
keli-wen
had a problem deploying
to
manual-tests
September 13, 2026 12:49 — with
GitHub Actions
Failure
…pers Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RKgBCYMWQru5SawpfCLxFY
Owner
Author
|
Windows pass-rate measurement on this branch (each run approved through the
Across the 8 runs the original signature (a worker vanishing with a 178-byte log and |
… tests Follow-ups from the second agy review of the diff: - stopExecution adopts members that appear between its two snapshots (a tool started during the grace period) from a still-live, identity-matched member under the same birth-order rule as tree(). Without /T this window was otherwise left to leak. - cancel compares worker identities with sameBirth(), which matches a 0.7.1 locale-rendered stamp against the round-trip table at the coarser precision, so a job that spans the upgrade can still be canceled. - Tests: the real-process test registers child cleanup before polling and polls at most 30 times (each table query costs seconds on Windows); the unreadable-stamp assertion now pairs an unreadable child with a readable parent; a POSIX real-process test covers late-child adoption. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RKgBCYMWQru5SawpfCLxFY
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RKgBCYMWQru5SawpfCLxFY
…ndler On a fast Linux runner ps saw the root before Node had registered the handler, so SIGTERM took its default action and no late child appeared. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RKgBCYMWQru5SawpfCLxFY
Worker identities are compared as the exact strings the table produced, as before. A job started by 0.7.1 and still running through the upgrade is the only case affected; the release note says to let jobs finish first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RKgBCYMWQru5SawpfCLxFY
This branch was successfully deployed
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.
Problem
Every Windows CI run since process-tree cleanup landed in #18 had roughly a 50% chance of failing, each time on a different test (
a9887f1: dx.test staffer prompt;07aaca0: recovery-regressions hard expiry; PR runs: hard stop escalates, group membership, continue --job, npm archive, ...). PRs were merged after their last run happened to be green; master gets one roll of the dice.The common signature: a detached worker vanishes with no status file and a log of exactly 178 bytes (the two startup lines and nothing else), so
waitreportscrashed(exit 3). No internal error path can produce that: the top-level catch always writesagy-staff error:to the log. OnlyTerminateProcessfrom outside does.Root cause
On Windows a detached worker keeps its exited dispatcher's PID in
ParentProcessId. Windows reuses PIDs quickly, and with three test files running in parallel one of them soon spawns an agy process that receives that PID. When that job finishes, two independent tree walks both take our worker for its descendant and kill it:tree()incompanion/stream-worker.mjsfollows parent links with no check that the child is younger than the parent.taskkill /PID <root> /T /Fdoes its own walk over the same staleParentProcessIdlinks.Neither happened before #18 (Windows never walked process trees at all), which is why six earlier master builds were green. Diagnosis and fix design were independently reviewed by an agy review job (verdict: confirmed; it found the second,
taskkill /T, path).Fix
tree()follows a parent link only whenchild.born >= parent.born. A process cannot be older than its parent; a row born before its recorded parent is an orphan behind a reused PID. This is sound: the reuser can only be born after the dispatcher exited, and the worker was born before that.CreationDateviaToString('o')so the comparison has 100 ns precision; the default locale rendering is second-granular and would miss a reuse within the same second.parseBornputs ISO-8601, WMIC and legacy stamps on one BigInt tick scale.taskkillruns without/Ton Windows. Descendants are terminated one by one after passing the identity and birth-order checks, asstopExecutionalready did for the JS-side members.stopExecutionnever signals its own PID and takes an injectable table source.docs/REFERENCE.md, zh-CN).Tests
ParentProcessIdin the table, because no kernel lets a test choose the next PID it hands out (and POSIX reparents orphans to init). With the birth-order check disabled the test fails; with it, the root and its child are stopped and the orphan survives.parseBorn(ISO with offsets, WMIC, legacy),tree()stale-link pruning, the round-trip PowerShell query and the newtaskkillarguments.🤖 Generated with Claude Code
https://claude.ai/code/session_01RKgBCYMWQru5SawpfCLxFY