Skip to content

fix(orchestration): keep a stopping worker from wedging dispatch, and certify legacy exits from the owner - #19347

Closed
Jinwoo-H wants to merge 13 commits into
mainfrom
fix/lifecycle-wedge
Closed

Jinwoo-H wants to merge 13 commits into
mainfrom
fix/lifecycle-wedge

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1680 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1680
Prod 17 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​696 $\color{#cf222e}{\Huge{\mathbf{−}}}$​19 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​677

Summary

P0 (lifecycle-federation lane) and P1 (mailbox-liveness lane) from the #16904 audit.

P0. Three places assumed a worker state they never rechecked. The Task guard ran its live-supervised-worker check only for terminal statuses, so task-update --status dispatched re-opened a Task under a stopping worker. Settlement then transitioned the worker from a hardcoded ready and threw, and both callers that run it inside a batch (federated relay import, coordinator message sweep) roll the whole batch back: a relay whose cursor can never advance, and a run marked failed with its messages left unread. beginWorkerStop refused a re-issue from stopping, so an operator had no CLI-reachable escape.

P1. Legacy worker terminal recovery read exited off one controller listing. A restarted relay omits every id the previous one minted, and a daemon session this process never attached was never in the cache, so absence from a listing is not an observation of exit.

Fix

  • Every status the Task guard lets past the active-Dispatch check now clears the same worker check, scoped to a genuine re-open so a no-op re-assert of dispatched stays legal.
  • Settlement reads the worker's actual state, asks the lifecycle graph through a new isLegalLifecycleTransition predicate, and returns a structured worker_not_settleable rejection instead of throwing. Relay import and the coordinator sweep already record rejections and advance. Wire-safe: the relay schema types the code as a string.
  • beginWorkerStop accepts a re-issue from stopping; the chain re-issue, stop_unknown, abandon is a complete operator exit and never asserts an exit the runtime did not observe.
  • All three listing-omission branches in recovery now call the runtime's existing isLeafPtyProvenAbsent (cache proves presence, only the owning provider's readback proves absence, remote: ids answer unverifiable). Doubt defers to the next sweep.

Tests

db-lifecycle-state-space-settlement.test.ts (BFS over reachable task/dispatch/worker triples asserts zero throwing settlements; reports the audit's four on main), db-stopping-worker-report-settlement.test.ts, runtime-legacy-worker-terminal-exit-certification.test.ts. Two recovery fixtures that asserted exits from an empty listing now supply the owner readback their names claimed.

A/B on unpatched main: 9 of 15 fail (all the bug-characterizing cases), 6 controls pass. 15 of 15 here.

Gates: orchestration + rpc/orchestration + CLI orchestration + legacy recovery + runtime barrel (187 files, 2856 tests), tc:node, tc:cli, changed-code quality (14 files, 0 findings).

Part of the fix-forward set for #16904 with #19337, #19339, #19344. Not to be merged without explicit sign-off.

Round-1 review follow-ups (pushed)

  • Coverage gaps from Codex mutation sweep closed: failed-report settlement on the wedged triple, pre-adoption listing-omission deferral; BFS gains taskUpdate:pending and an honest scope comment.
  • Opus high finding fixed at the provider boundary: SshPtyProvider had no probePtyLiveness, so recovery could never certify an SSH legacy worker's exit and deferred forever. New probe uses pty.listProcesses (the relay re-probes pids before listing) plus the id's mint epoch versus the relay's current epoch from pty.getCapabilities (one additive optional field): listed → live; unlisted with matching epoch → exited; legacy id, epoch mismatch, relay without the field, timeout, wrong target → unverifiable. Deliberately not pty.attach, which would disturb a live consumer. Old relays keep today's behavior (every absence unverifiable).
  • Opus medium fixed: a stop re-issue is accepted from stopping only when the row's runtime_epoch differs from this runtime's (stranded by a dead runtime). A same-epoch duplicate keeps the refusal, so an in-flight clean stop can no longer be recorded as stop_unknown and then escalated as a crash by failActiveDispatchOnExit. Both mutation directions are fenced by tests.

Round-2 review follow-ups (pushed)

  • SSH liveness readback removed from this PR. Codex round 2 reproduced a P0 in the round-1 probe: the relay's shutdown path removes a PTY record after an exit-wait timeout without observing exit, keeps serving under the same mint epoch when a sibling kill fails, so "unlisted and same epoch" certified a live process as exited. The probe, the shared mint-epoch parser, its capability field and all their tests are deleted; the relay is byte-identical to main. SSH legacy workers now answer unverifiable and defer, exactly like remote: ids; the operator exit chain this PR adds (stop re-issue, stop_unknown, abandon) is the escape. A relay-owned exact-id liveness answer is a separate follow-up.
  • Opus round 2 nits: dedupeWorkerStop docstring corrected; BFS integrity floor raised to the measured count.
  • Net change since round 1: +18/−400.

Round-3 review follow-ups (pushed)

Codex round 3 left one P1: with the readback removed, a supervised SSH worker that exits while the
client is disconnected can never be certified. Recovery defers on every sweep, the durable worker
stays active, and the Task refuses both reuse and a replacement worker. The user's decision is to
ship the relay-owned probe, so the round-2 removal is reverted and the owner contract completed.

What the probe certifies. pty.probeLiveness answers one exact id from the owning relay's own
lifecycle state, with no side effect on any consumer. live = this relay holds a non-disposed
record whose pid still answers signal 0. exited = this relay watched the process end — exactly
two lines produce that evidence, node-pty's onExit and the pid probe that answered ESRCH, and each
records the id at the line that made the observation rather than in the shared reap they both pass
through. SshPtyProvider.probePtyLiveness forwards and maps only those two; anything else is null.

What it never certifies. Everything else is unknown: a shutdown that deleted the record after
its 8 s exit wait timed out (with or without a sibling kill failure keeping the relay serving under
the same mint epoch), the torn-down-record sweep, a record mid-teardown, a pty.revive in flight
holding the id, an id this relay never held (another generation's, a legacy pty-N, empty,
malformed), an observation evicted at the 4,096 cap, and every id after a relay restart — the ledger
is in memory per relay process on purpose, because forgetting is the fail-closed direction. A
worktree removal or a refused force kill leaves the record in place and the answer live. No
capability negotiation, no persisted ledger, no client-side epoch comparison, no listing inference.

Old relays. A host predating the method answers JSON-RPC -32601, which arrives as a rejection
and maps to null — indistinguishable from a timeout or a disposed multiplexer — so recovery defers
and pre-PR behaviour is preserved. Nothing was added to pty.getCapabilities, so there is no field
an old host must publish and none a new host publishes that an old client can misread.

Removal-path census. this.ptys.delete(...) appears at one line, inside removePty, which has
exactly three callers. That closes the enumeration.

Path in src/relay/pty-handler.ts Removes the record Watched the end Records Test
node-pty onExit → removePty (:1070) yes yes yes (:1069) certifies the exit node-pty reported
reapPtyProvenExited → reapExitedPty('exited') → removePty (:2496→:2503→:2453) yes yes (ESRCH) yes (:2502) certifies an exit it proves by probing the pid, and keeps certifying it
Torn-down-record sweep in listProcesses → reapExitedPty('record-torn-down') (:2889) yes no no stays unverifiable after the listing sweeps a torn-down record away
disposePtyForRelayShutdown → removePty (:3269) yes no no stays unverifiable after a shutdown removes a record it never saw exit; both convergence controls
disposePtyForRelayShutdown with a refused force kill no no no keeps a record a shutdown could not kill live, rather than retiring it on paper
pty.shutdown (:2275) and shutdownForWorktreePath (:715) no no no never certifies an exit from a worktree removal it could not prove

One claim in the restored implementation was wrong and is corrected: ids embed the mint epoch, but
pty.revive re-creates a process under a serialized id, so a previous generation's id can enter
the ledger. The provable statement is narrower — only ids of records this relay itself held, minted
here or revived here — and the foreign-generation test now runs against a populated ledger.

Tests. The round-3 reproduction is checked in unmodified as
src/relay/pty-handler-ssh-recovery-convergence.test.ts (real handler + real provider + real
registry/controller readback + real recovery candidate + in-memory orchestration DB). It and the
round-2 certification file pass unchanged. New relay cases cover every row above plus revive,
eviction, restart and id shapes; a new recovery case covers the -32601 host. All six files are
registered as reliability gate ssh-liveness.relay-owned-exit-certification.

Six mutations of the probe and its forwarder each fail at least one checked-in test, zero survivors:
record in the shared reap (1/47), dropped disposed check (1/47), dropped pending-revive check
(1/47), absent record as exited (14/47), provider unknown as false (10/47), provider rejection as
false (4/47).

Gates: src/relay + src/main/providers + src/main/ipc/pty + src/main/daemon (504 files, 5,121
tests), src/main/runtime (710 files, 7,359 tests), tc:node, tc:cli, changed-code quality (24
files, 0 findings), reliability-gate manifest (110 gates), max-lines ratchet.

Also reuses the existing tested BoundedMap for the ledger instead of a hand-rolled cap-and-evict.

Round-4 review follow-ups

The review reproduced three owner-evidence bugs on top of a recovery path and removal census it
found correct. Full write-up in fix-reports/19347-round5.md.

F1 (P0) and F2 (P1) are one root cause: the ledger keyed exit evidence by id, and pty.revive
reuses ids, so an entry did not say which incarnation it described.
Admission (wireAndStore, the
single this.ptys.set site) now clears the id's prior observation and records the incarnation that
owns the id; the node-pty onExit observer records the exit before the if (managed.disposed)
return, and only when that incarnation still owns the id. F1 was a stale entry certifying a revived
process that never exited, once a shutdown removed the new record without proving anything. F2 was a
real exit delivered after disposal being dropped, leaving the id unverifiable forever.

The ownership check is this.ptys.get(id) === managed || ptyIdIncarnationOwners.peek(id) === incarnationId,
not the reviewer's !this.ptys.has(id) || .... An empty pool is not evidence that a late callback
still speaks for the id: with the empty-pool clause, an id torn down, revived, and torn down again
lets the first process's callback certify the second one's exit. ptyIdIncarnationOwners is a
second bounded map, written at the same single admission site; forgetting an owner withholds an
exit, never invents one, and the live-record disjunct keeps eviction off the normal path.

F3 (P1): ptyIdMintEpoch is no longer published by pty.getCapabilities, restoring base
9fed61e5c246 exactly, and src/shared/relay-pty-mint-epoch.ts no longer equates same-epoch
omission with an exit. toRelayPtyIdWithMintEpoch stays — ids still carry a generation so they
cannot collide. The round-two client's list/epoch inference is checked in as an oracle (reproduced
inline, old client source not shipped) and reaches no verdict against this host.

F4 (P2): the ledger-eviction test now reads the oldest entry before the overflow, which kills
the surviving BoundedMap.has()-reorders mutation.

P3: pty.probeLiveness answers live | exited | unverifiable, matching the execution-boundary
vocabulary. No released client consumes the RPC, and the provider maps anything that is not live
or exited to null in either direction.

Twelve mutations now each fail at least one checked-in test, zero survivors: the round-4 six, the
BoundedMap.has() reorder, dropped admission invalidation (2/56), dropped incarnation guard (1/56),
that guard weakened to the empty-pool form (1/56), recording moved back below the disposed guard
(2/56), and a republished mint epoch (3/56). All four reviewer scripts pass at this head; the owner
probes needed only 'unknown' → 'unverifiable' in their appended assertions for the P3 rename.

Gates: src/relay src/main/providers src/main/runtime src/main/daemon src/main/ipc/pty (1,214
files, 12,489 tests), the six-file reliability command (56 tests, was 47), tc:node, tc:cli,
changed-code quality (24 files, 0 findings), reliability-gate manifest (110 gates), max-lines
ratchet.

…the relay

`updateTaskStatus` ran its live-supervised-worker guard only for terminal
statuses, so `task-update --status dispatched` re-opened a Task under a worker
that was already stopping. The worker's own report then had nowhere to land:
settlement transitioned the worker from a hardcoded `ready`, so it threw
`lifecycle_conflict` out of the federated relay import (rolling back the batch
and freezing `to_home_imported_sequence`, which the contiguity check then used
to refuse every later sequence forever) and out of the coordinator's message
sweep (marking the run failed with its batch unread).

Three edges, one rule each:
- every status the Task guard lets past the active-Dispatch check now clears the
  same live-worker check, so a genuine re-open cannot happen under a live worker;
- settlement reads the worker's actual state and asks the lifecycle graph, then
  rejects with `worker_not_settleable` instead of throwing, so already-wedged
  databases drain rather than stall;
- `beginWorkerStop` accepts a re-issue from `stopping`, giving the operator a
  legal exit whose outcome is the honest `stop_unknown` rather than an asserted
  exit.

The state-space test drives the real public operations and proves no reachable
(Dispatch, Task, worker) triple can make a worker report throw.
…a listing

Legacy worker terminal recovery read `exited` straight off a PTY id missing from
one controller inventory. `listProcesses` returns the host's CURRENT session map,
so a restarted relay omits every id the previous one minted and a daemon session
this process never attached was never cached at all — neither shell necessarily
died. On that inference recovery abandoned the worker, failed and circuit-broke
its dispatch, closed its questions and reset the Task to ready, so the same Task
could be dispatched twice while the original agent kept running.

All three listing-omission branches now ask the runtime's existing absence
resolver, which routes to the provider that owns the PTY and answers absent only
on its readback. Doubt defers to the next sweep instead of certifying an exit,
which is what the sibling inventory refresh already does for a restarted relay.

The recovery fixtures asserted exits from an empty listing alone; they now supply
the owner readback their test names already claimed.
@coderabbitai

coderabbitai Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds relay-owned PTY liveness checks to legacy worker recovery. Unverifiable PTY absence now defers reconciliation. Relay PTY ids include mint epochs, and observed exits are tracked for exact liveness responses. Worker settlement now validates lifecycle transitions, blocks task reopening while supervised workers stop, and distinguishes stranded stops by runtime epoch. New tests cover PTY recovery, relay behavior, lifecycle state exploration, stopping workers, federated reports, and repeated stop requests.

Merge Risk: 🟠 High · up to 84791

The relay can retain oversized PTY identifiers and may certify stale exit evidence, risking relay exhaustion or incorrect worker retirement. Recovery tests can also mask unverifiable PTYs as exited. These issues should be fixed before merge.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description gives detailed context, changes, rationale, tests, and gate results. However, it omits required template sections and information, including an explicit Fixes #... linked issue, the … Add the missing template sections. Provide an explicit linked issue using Fixes #...; add ## Visual Proof with N/A and a brief reason if there is no UI or interaction change; complete the Testing checkboxes; complete AI Disclosure if …
Docstring Coverage ⚠️ Warning Docstring coverage is 44.74% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 38 functions across 25 files. (2 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies both primary changes: preventing stopping-worker dispatch wedging and certifying legacy exits from the owning provider.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description gives detailed context, changes, rationale, tests, and gate results. However, it omits required template sections and information, including an explicit Fixes #... linked issue, the required Visual Proof section with N/A and a reason, the checklist confirmations, the Agent skill upstream boundary checkbox, and the remaining review and notes sections.

Resolution

Add the missing template sections. Provide an explicit linked issue using Fixes #...; add ## Visual Proof with N/A and a brief reason if there is no UI or interaction change; complete the Testing checkboxes; complete AI Disclosure if applicable; select the Agent skill upstream boundary option; and complete the Review, Notes, and Checklist sections.

Full details: Docstring Coverage

Explanation

Docstring coverage is 44.74% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 38 functions across 25 files. (2 skipped: 2 unsupported.)

✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • Worker-report settlement — settleWorkerReportInTransaction now asks the lifecycle graph via isLegalLifecycleTransition before writing and returns a structured worker_not_settleable rejection instead of throwing; the worker transition's from is the worker's actual state, not a hardcoded 'ready'.
  • Task re-open guard — updateTaskStatus runs the live-supervised-worker check on a genuine re-open to dispatched, keeping a no-op re-assert legal.
  • Stop re-issue escape — beginWorkerStop accepts a re-issue from stopping, completing the stopping → stop_unknown → abandon operator chain.
  • Legacy exit certification — all three listing-omission branches in legacy recovery now defer unless the owning provider's readback (isPtyProvenAbsent) certifies absence.
  • Tests — a BFS state-space settlement proof, targeted stopping-worker settlement tests, and legacy exit-certification tests, plus updated recovery fixtures.

I traced the load-bearing paths behind the change and found the logic sound. The worker lifecycle edges reused by the new guard are pre-existing in LEGAL_TRANSITIONS, so no new reachable state is introduced; settlement runs inside a single transaction, so the reportingWorker.state read is stable through the write; and neither importFederatedRelayItem nor the coordinator sweep calls updateTaskStatus('dispatched'), so the stricter throw cannot wedge a relay batch. The worker_not_settleable code is string-typed on the wire and is parsed defensively (typeof code === 'string'), so it's safe for mixed-version clients. The pre-existing-wedge-row case (task dispatched under a stopping worker written by an older binary) is correctly handled by rejection rather than a throw.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

… left green

A Codex review of #19347 found two mutants the new tests did not kill.

Restoring settlement's hardcoded `ready`/`start_unknown` precondition left all
ten lifecycle tests green: every old-database case reported success, which the
graph guard rejects before the transition runs. The graph does accept
stopping -> failed, so a failed report on that same persisted triple must settle;
under the hardcoded precondition it throws and wedges the relay batch again.

Restoring the unconditional `exited` on the pre-adoption omission branch left the
certification suite green: it covered the initial and final listings but not the
re-list inside the adoption mutation, which is exactly where a relay restart
drops ids whose shells are still running.

Also narrows the state-space test's own claim. It adds the missing
`taskUpdate:pending` operation and replaces the "state-space proof" wording with
what the search actually enumerates: one federated dispatch, settlement called
directly rather than through its two batch callers, a memo key of three statuses,
and a depth-five cutoff it does not re-derive.
…lers

The comment justified the coalescing by a DB refusal that no longer happens:
`beginWorkerStop` accepts a re-issue from `stopping`, since that is the
operator's escape. The mechanism is now more load-bearing, not less — without
it both callers close the same terminal and race to settle a row only one of
them can still find — so say that instead of the stale reason.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

The delta since the prior review (4caaa6c test + cb0657a docs) widens regression coverage and clarifies docs; there is no production-code change.

  • State-space search scope — added a taskUpdate:pending operation so the BFS also reaches task states with no active dispatch, and rewrote the block comment to spell out exactly what the search enumerates and what it leaves to the targeted tests.
  • Failed-report settlement — added a regression test proving a failed worker report settles from a stopping worker, the other half of the from: reportingWorker.state guard that the reject case already covered.
  • Pre-adoption refresh deferral — added a legacy-recovery test for the pre-adoption re-list losing the PTY without an owner absence proof (relay restart between the plan inventory and adoption).
  • dedupeWorkerStop doc — corrected the JSDoc to match the now-accepted re-issue from stopping.

I ran the three changed test files (17 tests; the state-space BFS finishes in ~17s against a 300s timeout). The added tests fail on unpatched main and pass here, so they are real regression coverage rather than theatre. The stopping → failed (legal) versus stopping → succeeded (illegal) split is confirmed against LEGAL_TRANSITIONS in lifecycle-transition.ts, and both comment corrections state the behavior accurately.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 7947cf88-9d80-4ad6-a7c6-be51e919ca05

📥 Commits

Reviewing files that changed from the base of the PR and between 1d1b73c and cb0657a.

📒 Files selected for processing (15)
  • src/main/runtime/orca-runtime-preserved-branch-cleanup.ts
  • src/main/runtime/orca-runtime-test-scenario-builders.spec.ts
  • src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-04.spec.ts
  • src/main/runtime/orchestration/db-lifecycle-state-space-settlement.test.ts
  • src/main/runtime/orchestration/db-stopping-worker-report-settlement.test.ts
  • src/main/runtime/orchestration/db/dispatch-context/worker-report-settlement.ts
  • src/main/runtime/orchestration/db/lifecycle-transition.ts
  • src/main/runtime/orchestration/db/tasks/task-status-transition.ts
  • src/main/runtime/orchestration/db/worker-dispatch/worker-dispatch-stop.ts
  • src/main/runtime/orchestration/lifecycle-reconciliation.ts
  • src/main/runtime/orchestration/types.ts
  • src/main/runtime/rpc/methods/orchestration/worker/worker-stop.ts
  • src/main/runtime/runtime-legacy-worker-terminal-exit-certification.test.ts
  • src/main/runtime/runtime-legacy-worker-terminal-recovery-candidate.ts
  • src/main/runtime/runtime-legacy-worker-terminal-recovery-types.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.

getForegroundProcess: async () => null,
hasPty,
// Absence is only an exit when the owning provider says so; these fixtures own no other PTY.
probePtyLiveness: async (candidate: string) => hasPty(candidate) === true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve unknown PTY liveness

When a harness caller supplies null, return hasPty(candidate) unchanged. The current comparison converts null to false, and the recovery path records that result as proven PTY absence instead of unknown liveness.

Legacy worker recovery now retires a worker only on the owning provider's
readback, but `SshPtyProvider` had none, so the runtime controller answered null
for every `ssh:` id and absence was permanently unverifiable. An SSH worker whose
shell died while Orca was closed has no exit event and recovery is its only
settler, so it deferred on a 30 s backoff forever: the pane stayed fenced, the
dispatch stayed dispatched, and the Task could not be re-opened. Removing a false
`exited` had left no path to a true one.

The relay can answer, because a PTY id already carries the mint epoch of the
relay process that allocated it. `pty.listProcesses` is that relay's live set: it
reaps a torn-down record and re-probes the pid before reporting. So an unlisted
id whose mint epoch is the relay's current one is an exit this host observed,
while an unlisted id from any other epoch is the restart case that was never
evidence. `pty.getCapabilities` gains the epoch as an additive field so the
comparison holds even when the relay now lists nothing, which is the reported
case; a relay that omits it leaves every absence unverifiable, as today.

Deliberately not `pty.attach`, the only refusal carrying the proven-exited
marker: a successful attach opens a delivery, retires the previous one, and
restores retired pane surfaces, so probing with it would disturb a live consumer
in precisely the case where the answer is "live".

The certification suite's PTY ids were not parseable SSH ids, so it claimed
coverage of the population that regressed and drove none of it. They are real
ids now, with two cases running the recovery branch through this probe.
Accepting `stopping` from any caller re-opened the window the worker-stop
coalescing exists to close. A second pass over this runtime's own in-flight stop
re-inspects a terminal that is already going away and records `stop_unknown`.
The exit event that follows claims a clean stop only from `stopping` under its
own epoch, so from `stop_unknown` it falls through to a dispatch failure with
`workerProcessExited` and posts an "Agent exited unexpectedly" escalation: the
operator's own stop reported as a crash.

The escape was only ever justified for a row whose owning runtime died, and the
runtime id is a per-process value, so the epoch names exactly that population.
`failActiveDispatchOnExit` already draws the same line to decide whose stop an
exit may settle; both sides now use it, so they cannot disagree about whose stop
is in flight.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

The delta since the prior review (6dea9aa) is the SSH half of the P1 fix: SshPtyProvider gains a probePtyLiveness readback and the relay publishes its ptyIdMintEpoch, so a legacy SSH worker can finally be certified exited by its own owner instead of deferring forever.

  • SSH liveness readback — SshPtyProvider.probePtyLiveness delegates to a new probeSshPtyLiveness, which answers live when the relay lists the id, exited only when the same relay that minted it (matched ptyIdMintEpoch) no longer lists it, and null otherwise.
  • Relay mint-epoch capability — pty.getCapabilities gains an additive ptyIdMintEpoch field, and id minting is extracted into a shared toRelayPtyIdWithMintEpoch whose output is byte-identical to the prior pty2:<epoch>:<seq> format.
  • Shared epoch module — relay-pty-mint-epoch.ts keeps the id shape in one place so the minting relay and the parsing client cannot drift.
  • Recovery integration tests — the legacy recovery suite now drives a real SSH id and asserts both certification (same-epoch absence) and deferral (a restarted relay that merely disowns the id).

I traced the load-bearing claims behind the readback: the epoch is a per-process randomUUID() (production constructs PtyHandler without a persisted epoch), pty.listProcesses reaps disposed/proven-exited PTYs before answering so the listing is genuinely the live set, includeForegroundProcessEvidence: false skips the host process-table work without skipping the reap, and the probe never throws — every doubt case (legacy pty-N id, missing epoch, epoch mismatch, transport failure, non-array listing) resolves to null. An old relay that omits ptyIdMintEpoch leaves every absence unverifiable, preserving the shipped mixed-version behavior. Ran the four changed/new test files (29 tests) — all pass.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

The SSH liveness probe read "absent from pty.listProcesses and minted by this
relay's generation" as an observed exit. That implication is false. Relay
shutdown waits eight seconds for a physical exit, catches the timeout, and
deletes the record anyway; a sibling PTY's failed kill rejects the aggregate, the
grace lifecycle defers shutdown, and the relay keeps serving under the same mint
epoch. A provider that never attached the PTY then received a death certificate
for a running process, and recovery queued its worker as exited.

The census of every record removal splits cleanly: node-pty's onExit and the
ESRCH pid probe watched the process end; the shutdown teardown and the
torn-down-record sweep did not. Only the first two now record the id, at the line
that made the observation rather than in the shared reap they both pass through.

pty.probeLiveness answers an exact id from that state: a live record, or exited
when its pid probes absent or the id is in the ledger. A record mid-teardown, a
revive in flight, and every id the relay never watched end are unknown. It fails
closed, so a removal path added later without a record defers instead of
certifying. SshPtyProvider forwards and maps three statuses; an older relay
answers -32601, which arrives as a rejection and maps to null, so no capability
negotiation is needed.

Two client-side gates went with it. The mint-epoch comparison is subsumed by
ledger keys that already embed the generation, and a blanket shutdown-fenced
refusal only masked the ledger in the one path where teardown runs.
Reverts 6dea9aa and 1dcae18. The relay-owned probe those commits landed is
correct, but it is a relay lifecycle change with its own census, capability
surface, and shutdown semantics, and it does not belong in a dispatch-lifecycle
PR. It moves to fix/ssh-pty-liveness-owner.

What this PR keeps is the safety half: recovery never certifies an exit from a
listing omission. SSH legacy workers therefore answer unverifiable and defer,
exactly as `remote:` ids already do, because no SSH provider implements the owner
readback. That is an availability cost, not a correctness one — a worker whose
shell really died is cleared by worker-stop, stop_unknown, then worker-abandon,
which this PR makes reachable. Retiring it on a listing omission is what orphaned
live agents.

The certification suite keeps its parseable SSH ids and gains a case naming the
deferral, so the limitation is documented where someone will find it.
The dedupe docstring was rewritten before the epoch gate landed and now says the
opposite of what happens: both coalesced callers are in one runtime and share one
epoch, so `beginWorkerStop` refuses the second. The coalescing still earns its
place by keeping one of them from closing the terminal, which is what it now says.

The state-space floor stayed at 50 after `taskUpdate:pending` grew the reachable
set from 59 to 71, so the harness-integrity guard tolerated losing a third of the
space in silence. Raised to just under the measured count.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

The delta since 1e89522 reverts the SSH liveness readback out of the PR, leaving the P0 dispatch fix plus the P1 safety half, and corrects two comments.

  • SSH liveness readback removed — f0f8858964 reverts both the relay-owned pty.probeLiveness ledger (1dcae188bf) and the earlier SshPtyProvider.probePtyLiveness / mint-epoch capability, restoring pty-handler.ts and ssh-pty-provider.ts to byte-identical with main. SSH legacy workers now answer unverifiable and defer, exactly like remote: ids, and are cleared through the now-reachable stop re-issue → stop_unknown → abandon chain.
  • Stop re-issue doc corrected — 0be1813781 rewrites the dedupeWorkerStop docstring to match the epoch-scoped re-issue (same-runtime callers coalesce; the accepted epoch belongs only to a row a dead runtime stranded).
  • State-space floor raised — the BFS integrity floor moves to the measured count so a real regression in coverage is visible.
  • SSH deferral documented — the recovery cert suite keeps its parseable SSH ids and names the "no owner can certify" deferral case in a comment, recording the limitation next to the code that enforces it.

I verified the revert is clean rather than assuming it: git diff --merge-base main on pty-handler.ts, ssh-pty-provider.ts, and the deleted relay-pty-mint-epoch.ts / ssh-pty-liveness-probe.ts modules is empty, and a repo-wide grep for the removed symbols turns up only the pre-existing pty-handler.ts mint-epoch field (used as authorityGeneration) and two accurate comments. SshPtyProvider omits the optional probePtyLiveness from IPtyProvider, so probePtyLivenessFromRuntimeController returns null for SSH and resolvePtyAbsentFromListing defers to the next sweep — the intended safety behavior rather than a dangling reference.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

The probe answers `exited` from an observation alone, so the safety argument is
the census of every path that makes a record disappear and whether that path
watched anything. Three do not: the relay-shutdown teardown that stopped waiting
for an uninterruptible child, the listing's torn-down-record sweep, and a
worktree removal whose kill nothing confirmed. Each now has a test asserting the
id stays unverifiable afterwards, alongside the states that observed nothing
either — a revive holding the id while it puts a different process on it, a
record mid-teardown, an evicted observation at the ledger cap, a relay that
restarted since, an id another generation minted, and every malformed shape.

The round-3 reproduction is checked in as it was written: the reachable relay
observed the worker's exit while the client was absent, and recovery has to
converge instead of deferring every sweep and holding the Task fenced. Its two
shutdown controls and the round-2 certification file ride with it, plus a
recovery case for a host too old to know the method, which rejects with -32601
and defers with no capability handshake.

Two corrections fell out of writing them. The ledger claimed an id another
generation minted could never be in it; `pty.revive` re-creates a process under
a serialized id, so the true statement is narrower and now says so. And the
eviction is the pattern `BoundedMap` already implements and tests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: a3d56ddd-4007-44f7-8e28-69a08db1fb04

📥 Commits

Reviewing files that changed from the base of the PR and between 0be1813 and 03af416.

📒 Files selected for processing (12)
  • config/reliability-gates.jsonc
  • fix-reports/19347-round2.md
  • src/main/providers/ssh-pty-liveness-probe.test.ts
  • src/main/providers/ssh-pty-liveness-probe.ts
  • src/main/providers/ssh-pty-provider-liveness-readback.test.ts
  • src/main/providers/ssh-pty-provider.ts
  • src/main/runtime/runtime-legacy-worker-terminal-exit-certification.test.ts
  • src/relay/pty-handler-liveness-probe.test.ts
  • src/relay/pty-handler-ssh-exit-certification.test.ts
  • src/relay/pty-handler-ssh-recovery-convergence.test.ts
  • src/relay/pty-handler.ts
  • src/shared/relay-pty-mint-epoch.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.

generation, therefore exited". The implication is false. The relay has removal paths that never
observe the process end: `disposePtyForRelayShutdown` waits `IMMEDIATE_PTY_EXIT_TIMEOUT_MS` for a
physical exit, catches the timeout, and deletes the record anyway; `disposePtys` settles with
`Promise.allSettled`, so another PTY's failed kill rejects the aggregate while that record is

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 12 '\bdisposePtys\b|Promise\.allSettled' src

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- report ---'
sed -n '1,18p' fix-reports/19347-round2.md
printf '%s\n' '--- implementation ---'
sed -n '3210,3248p' src/relay/pty-handler.ts

Repository: stablyai/orca

Length of output: 2398


Correct the Promise.allSettled failure description.

Promise.allSettled fulfills with rejected result entries. disposePtys explicitly throws rejected.reason after settlement, so state that disposePtys rejects rather than the Promise.allSettled aggregate.

Comment thread src/relay/pty-handler.ts Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No critical issues — one minor doc/dead-code note inline.

Reviewed changes

The delta since the prior review (0be1813) re-lands the relay-owned SSH liveness readback that was reverted in round 2, completing the owner contract so a supervised SSH worker that exits while the client is disconnected can finally be certified.

  • Relay-owned pty.probeLiveness restored — the relay now answers an exact id from an observedPtyExitIds ledger, certifying exited only where it watched the process end (node-pty onExit, or a pid probe answering ESRCH); every other record disappearance is unknown.
  • Owner readback forwarder — SshPtyProvider.probePtyLiveness carries the question to the relay via probeSshPtyLiveness, mapping live/exited/unknown to true/false/null; a foreign target or an old relay (-32601) answers null with no capability handshake.
  • Bounded ledger — the hand-rolled cap-and-evict is replaced with the existing tested BoundedMap (4,096 entries); eviction fails closed to unknown.
  • Shared id module — relay id minting extracts to relay-pty-mint-epoch.ts so the minting relay and any reader cannot drift.
  • Tests + gate — six new/updated files (relay probe, exit-certification, recovery-convergence, provider readback/forwarder, legacy certification) and a ssh-liveness.relay-owned-exit-certification reliability-gate entry.

I traced the load-bearing paths and find the design sound: the two observation sites record the id at the line that observed the end (onExit at pty-handler.ts:1069, pid-probe ESRCH at :2502), not inside the shared reap; the disposed and pendingRevive guards plus ledger eviction all answer unknown; and probeLiveness's reap of a live-but-dead-pid record is the same pre-existing reapPtyProvenExited path listProcesses already drives, so the notification it publishes is correct, not spurious. Ran the six changed/new test files (47 tests) — all pass.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Comment thread src/relay/pty-handler.ts Outdated
The observed-exit ledger keyed evidence by id, but pty.revive reuses ids,
so an entry did not say which incarnation it described. Admission now
clears the id's prior observation and records its owning incarnation, and
the node-pty onExit observer records before the disposed guard - a
shutdown that stopped waiting is bookkeeping, and the process ending
afterwards is still this relay watching it end - but only while that
incarnation still owns the id.

Ownership outlives the record because the observation can too: an empty
pool is not evidence that a late callback still speaks for the id, so a
bounded id -> incarnation map answers it after teardown removed the
record. Forgetting either map withholds an exit, never invents one.

Also drops ptyIdMintEpoch from pty.getCapabilities, restoring the base
host exactly: pairing it with pty.listProcesses is the list-absence
inference this PR removed from the client, and a host that publishes it
hands an older client the second half of it. Renames the probe's doubt
status to unverifiable, the execution-boundary vocabulary, and pins FIFO
ledger eviction with a read before the overflow.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

The delta since the prior review (03af416) binds PTY exit evidence to the incarnation that earned it, closing the three owner-evidence bugs the round-4 review reproduced, and corrects the FIFO-eviction test.

  • Incarnation-bound exit evidence — wireAndStore (the single this.ptys.set site) now clears a reused id's prior observation and records the owning incarnationId in a second bounded ptyIdIncarnationOwners map, so a pty.revive re-admission no longer lets a predecessor's exit certify the replacement (F1).
  • Late-exit certification — node-pty onExit records the observation before the disposed guard, so an exit delivered after a shutdown gave up waiting still certifies instead of leaving the id unverifiable forever (F2).
  • Ownership gate — stillOwnsPtyId (this.ptys.get(id) === managed || ptyIdIncarnationOwners.peek(id) === incarnationId) rejects a superseded incarnation's callback once its record left the pool; peek (not get) keeps reads from renewing retention.
  • Mint epoch unpublished — pty.getCapabilities no longer advertises ptyIdMintEpoch, restoring base behavior exactly and making the rejected round-1 list/epoch inference unreachable for old clients (F3).
  • unknown → unverifiable — pty.probeLiveness adopts the execution-boundary vocabulary; the provider forwarder already mapped non-live/exited to null, so the rename is wire-safe in both directions.
  • Eviction order pinned — the FIFO-eviction test now reads the oldest entry before overflow, so the BoundedMap.has()-reorders mutation is caught (F4).

I traced the load-bearing paths and find the incarnation binding sound: the incarnationId is a fresh randomUUID() at both admission sites, so the owners map always names the latest incarnation and a late callback from a superseded record cannot write; BoundedMap.has and peek do not reorder, and set overwrites in place; and the divergence between the two bounded maps under eviction fails closed (forgetting withholds an exit, never invents one). The prior inline note about the dead ptyIdMintEpoch field is addressed — the field is unpublished and the parser remains only as the test oracle that proves the old client's inference is unreachable.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/runtime/orca-runtime-test-scenario-builders.spec.ts (1)

241-242: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Return the nullable liveness result from the recovery harness

makePostRevealWorkerRecoveryHarness accepts boolean | null, but probePtyLiveness converts null to false. Production records absence only for explicit false; this conversion can make recovery report exited instead of deferring. Return hasPty(candidate) directly.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 6044437f-e2c1-4fe8-bfef-f3939ea74333

📥 Commits

Reviewing files that changed from the base of the PR and between 03af416 and 847912e.

📒 Files selected for processing (10)
  • config/reliability-gates.jsonc
  • fix-reports/19347-round5.md
  • src/main/providers/ssh-pty-liveness-probe.test.ts
  • src/main/providers/ssh-pty-provider-liveness-readback.test.ts
  • src/main/runtime/runtime-legacy-worker-terminal-exit-certification.test.ts
  • src/relay/pty-handler-liveness-probe.test.ts
  • src/relay/pty-handler-ssh-exit-certification.test.ts
  • src/relay/pty-handler-ssh-recovery-convergence.test.ts
  • src/relay/pty-handler.ts
  • src/shared/relay-pty-mint-epoch.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/shared/relay-pty-mint-epoch.ts
  • config/reliability-gates.jsonc

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.

Comment on lines +75 to +76
stays unverifiable; and the owners map is bounded by the same `OBSERVED_PTY_EXIT_HISTORY` entry cap
as the ledger, so eviction can only withhold an exit, never invent one. The live-record disjunct

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'ptyIdIncarnationOwners|observedPtyExitIds|OBSERVED_PTY_EXIT_HISTORY|probeLiveness|recordObservedPtyExit|wireAndStore' \
  src/relay/pty-handler.ts

rg -n -C 8 \
  'OBSERVED_PTY_EXIT_HISTORY|ptyIdIncarnationOwners|observedPtyExitIds|unverifiable|evict' \
  src/relay/pty-handler-liveness-probe.test.ts

Repository: stablyai/orca

Length of output: 24366


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 12 \
  'class BoundedMap|type BoundedMap|new BoundedMap|bounded-map' \
  src

Repository: stablyai/orca

Length of output: 47919


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n src/shared/bounded-map.ts | sed -n '45,180p'

Repository: stablyai/orca

Length of output: 5791


Couple owner eviction with exit-ledger eviction.

ptyIdIncarnationOwners and observedPtyExitIds are separate BoundedMap instances and evict independently. After pty-A is observed, admitting more than OBSERVED_PTY_EXIT_HISTORY other ids can evict its owner entry while retaining its exit-ledger entry. After the live record is removed, probeLiveness('pty-A') checks only observedPtyExitIds and can return exited from stale evidence.

Add a regression test for this sequence and assert unverifiable, or remove the corresponding exit-ledger entry when the owner entry is evicted.

it('defers an SSH candidate the owning relay will not certify', async () => {
const { pendingResolutions, deferredDispatchIds } = await reconcile({
inventory: listingWithoutThePty,
isPtyProvenAbsent: provenAbsentViaRelay(async () => ({ status: 'unverifiable' }))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Assert the converted relay PTY ID in this recovery test.

The helper test checks pty.probeLiveness separately, but this request stub ignores both arguments. Capture the request here and assert the method and { id: toRelaySshPtyId(CONNECTION_ID, PTY_ID) }. Otherwise, a wrong connection or mint-epoch conversion can still return unverifiable and pass this test.

Comment thread src/relay/pty-handler.ts
Comment on lines +572 to +574
private readonly ptyIdIncarnationOwners = new BoundedMap<string, string>({
maxEntries: OBSERVED_PTY_EXIT_HISTORY
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- pty-handler map declarations and revive/exit paths ---'
rg -n -C 12 'ptyIdIncarnationOwners|observedPtyExitIds|pty\.revive|revive|entry\.id|unverifiable' src/relay/pty-handler.ts
printf '%s\n' '--- bounded-map options and call sites ---'
rg -n -C 8 'new BoundedMap|interface BoundedMapOptions|type BoundedMapOptions|sizeOf|maxEntryBytes|maxBytes' src/relay src/shared

Repository: stablyai/orca

Length of output: 50370


Denial of Service (CWE-400): Uncontrolled Resource Consumption

Reachability: External · Exploitability: Moderate

Bound retained PTY IDs by bytes.

pty.revive accepts an unbounded entry.id, and both PTY history maps retain that ID after removal. Configure maxBytes, maxEntryBytes, and UTF-8 key measurement through sizeOf for both maps. Add a regression test that rejects an oversized revived ID and reports it as unverifiable.

…eviction

The resident disjunct in stillOwnsPtyId let a PTY that outlives 4,096
admissions certify its own exit after its owner entry was evicted; nothing
checked in failed when it was removed. Round-5 review found the survivor.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

The delta since the prior review (847912e) is a single test commit that pins the live-record ownership fallback across owner-map eviction; no production code changed.

  • Owner-map eviction coverage — two new tests drive ptyIdIncarnationOwners past its 4,096 cap and assert that a still-resident record still certifies its own exit via the this.ptys.get(id) === managed disjunct, while a late callback with neither the record nor the owner entry present is withheld (unverifiable).
  • Reliability-gate bookkeeping — the gate entry gains the two assertion names, a corrected summary (58 tests, thirteen mutations), and a budget-scope note.

I ran src/relay/pty-handler-liveness-probe.test.ts (25 tests) — all pass, including the two new cases. Both assertions are exact (live / exited / unverifiable, not truthiness), so they are regression coverage rather than theatre: they exercise the two disjuncts of stillOwnsPtyId in the exact scenario where the live-record and owner-map halves diverge under eviction.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@Jinwoo-H

Jinwoo-H commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Closing. The lifecycle half (Task re-open guard + worker-stop re-issue) landed as #19551. The relay exit-evidence half is being redone at the root: a single tagged retirement record (exited vs torn-down) replacing the two-meaning tombstone map, instead of a second ledger and a new probe RPC beside it. A fresh PR will replace this one when that is ready.

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