Skip to content

[WIP prerequisite] finalize worker shutdown with PID identity fencing - #1202

Draft
sethkarten wants to merge 28 commits into
mainfrom
v080/prereq-851-ci-candidate
Draft

[WIP prerequisite] finalize worker shutdown with PID identity fencing#1202
sethkarten wants to merge 28 commits into
mainfrom
v080/prereq-851-ci-candidate

Conversation

@sethkarten

@sethkarten sethkarten commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Purpose

Draft upstream prerequisite validation for the unique #851 shutdown-finalization semantics rebased onto accepted #850. This is not a v0.8 stack deliverable and not ready for human merge review.

Exact candidate

Safety corrections

  • Stop and finalization signals bind the exact descriptor/PID/start-ID/stop revision/tombstone tuple.
  • Missing legacy process identity fails closed and becomes an explicit manual-recovery state; it is never learned during stop or recovery.
  • Known identity observation outages retry boundedly without connecting, adopting, launching, or signalling.
  • Direct launch rollback uses the exact ChildProcess handle without weakening descriptor-only PID fencing.
  • OAuth/MCP and v0.8 stack work are not part of this Draft.

Evidence and hold

  • Focused supervisor monitor + child-process: 73/73 passed with trapped exact dependencies and recursive cleanup.
  • Independent P1 review: PASS; no coverage replacement or unrelated test weakening found.
  • Local process-smoke comparison is environment-blocked: candidate and accepted fix(coding-agent): report accurate worker lifecycle and hide stopping workers #850 baseline each fail the same eight supervisor-handshake tests under the local daemon environment. Candidate log SHA-256 1b8569a20a51b98c2d24f9d6c2677d52fe80b582ab3daf94e3900bd8a3b28828; baseline log SHA-256 ff9ee41c2fd3d7918a37dc3cc63d8cf9a20dc09689988e0e47ad87fe07a1cab8. Earlier overlapping attempts are preserved but invalid.

This Draft exists to obtain clean GitHub CI/process-smoke evidence. Do not merge, start #852, or present this as human-ready until the remote run and review gates pass.

Note

Add PID identity fencing to worker shutdown to prevent signalling recycled processes

  • Introduces processIdExists, isZombieProcess, and isProcessAlive utilities in child-process.ts for Unix-aware process liveness and zombie detection.
  • Adds signalCurrentWorker and processIdentity helpers to DaemonSupervisor that verify a stored processStartId matches the live process before sending SIGTERM or SIGKILL, preventing signals from reaching recycled PIDs.
  • Reworks stopWorker to bind stop operations to an exact descriptor/stop-revision tuple; timed-out stops now schedule background finalization via finalizeTimedOutWorkerStop rather than leaving tombstones indefinitely.
  • Adds effectiveWorkerState to expose a 'stopping' state and map disconnected 'ready' workers to 'recovering' in public summaries; daemon protocol bumped to revision 16.
  • Legacy worker descriptors without a processStartId are marked 'failed' with a manual-recovery error instead of being silently mutated.
  • Risk: workers that lack a stored processStartId (legacy descriptors) can no longer be automatically recovered and require manual intervention.
📊 Macroscope summarized f7ccf5e. 7 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

snimu and others added 24 commits August 10, 2026 22:09
… workers

A worker with a durable stop intent is now reported as "stopping" and a
disconnected worker is never reported as "ready". Stopping workers are
excluded from live session lists and daemon-wide fan-out commands.
…y checks

The list response feeds shutdownStaleDaemonIfNotBusy and
probeRunningDaemonSessions, so hiding stopping workers made a
tombstoned-but-running worker look idle and let a stale-daemon replace
terminate it silently. Stopping workers stay listed with an honest
"stopping" workerState; command fan-outs still skip them.
…ates

Revision 14 records the workerState wire-semantics change ("stopping"
state; disconnected workers no longer report "ready") so version probes
can tell old and new daemons apart. The field stays optional and
backward-tolerant, so no capability gate is needed.
… workers

A worker with a durable stop intent is now reported as "stopping" and a
disconnected worker is never reported as "ready". Stopping workers are
excluded from live session lists and daemon-wide fan-out commands.
…y checks

The list response feeds shutdownStaleDaemonIfNotBusy and
probeRunningDaemonSessions, so hiding stopping workers made a
tombstoned-but-running worker look idle and let a stale-daemon replace
terminate it silently. Stopping workers stay listed with an honest
"stopping" workerState; command fan-outs still skip them.
…ates

Revision 14 records the workerState wire-semantics change ("stopping"
state; disconnected workers no longer report "ready") so version probes
can tell old and new daemons apart. The field stays optional and
backward-tolerant, so no capability gate is needed.
…ng registrations

When a worker does not exit within the stop deadline, the supervisor now
keeps watching the process, escalates to SIGKILL, and completes the
interrupted cleanup once the process dies. Process liveness checks also
treat zombie processes as dead so cleanup is not deferred forever.
… generation

The background finalizer now snapshots pid, processStartId, and
stopRevision when scheduled and aborts if the stop is rescinded or the
worker is relaunched, so it can never SIGKILL a retried worker or an
unrelated process that reused the pid. stopWorker signalling is likewise
identity-aware.
- Fail closed when a recorded processStartId cannot be observed, so a
  recycled pid is never signalled even if identity observation fails.
- Record a schedule-time identity for workers that never had one.
- Retry transient finalization cleanup failures instead of stranding the
  dead registration permanently.
- Probe liveness with a cheap kill(0) on every poll and throttle the
  ps-backed zombie/identity checks so wedged workers cannot saturate the
  supervisor event loop.
…gone

stopWorker used the identity check as a liveness predicate, so a
transient getProcessStartId failure could skip signalling and delete the
registration of a still-running worker. Identity verdicts are now
directional: only a confirmed-current pid is signalled, only a
confirmed-gone/replaced pid is cleaned up, and an unknown verdict keeps
waiting.
…ched mid-await

stopWorker can yield during archival while a retry rescinds the stop and
relaunches the worker on the same registration. The cleanup tail now
verifies the registered process is still the one it stopped before
removing the registration or descriptor, so a relaunched worker is never
orphaned by a stale stop invocation.
The throttled identity cache can be up to 500ms old, long enough for a
pid to be recycled. Both SIGKILL sites (stopWorker force escalation and
the stop finalizer) now run a fresh identity check immediately before
signalling; the cache remains only for read-only wait-loop polling.
…ages

A transiently unobservable identity at the escalation deadline now skips
that attempt without marking the kill done, so a later pass that
re-verifies the original process still escalates instead of leaving a
wedged worker registered forever.
…scinded stops

All stopWorker polling and signalling now use the pid and start identity
captured at entry, so a retry relaunching the worker mid-stop can never
be SIGKILLed through the mutable descriptor. The cleanup guard also
aborts when a removeDescriptor stop lost its tombstone, catching a
rescission that lands before the successor pid does.
Comment thread packages/coding-agent/src/modes/daemon/daemon-supervisor.ts
Comment thread packages/coding-agent/src/modes/daemon/daemon-supervisor.ts
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.

3 participants