fix(orchestrate): never put a second agent on one worktree (re-entry) - #26
Merged
Conversation
SKILL.md's re-entry path restarts a worker the coordinator believes is dead by calling orca-worker-start.sh again with --worktree/--agent. For a dev-loop worker GROUNDWORK_ESCALATION_DIR is always set, so that lands in worker mode, which ran `orca terminal create` unconditionally. If the liveness read was wrong — and this run proved a worker can be ALIVE yet wedged — that puts a second agent on the same checkout with the Dispatch bound to only one of them. The tmux path has no such hazard: launch-session.sh detects an existing session and reuses it, loudly. Worker mode now probes first, automatically, with no flag to pass: it joins `orchestration dispatch-show --task` against `terminal list --worktree` and binds the recorded assignee when that terminal is still live on this worktree, reporting the reuse on stderr. Fails toward the safe side. Creating the second agent is the damaging direction, so a probe that cannot answer is UNKNOWN, never "no agent": an unreadable dispatch or terminal list exits 6 and creates nothing. Only `ok:true` with `dispatch:null` counts as a definite first start. Both probe calls take `</dev/null` so a headless coordinator cannot block on a prompt. Reuse also skips the tui-idle wait — that exists to pass a new CLI's trust screen, and a mid-task agent may never report idle. Field names verified against the live runtime, not assumed: `terminal list` exposes `orphaned` and `connected`; `dispatch-show` exposes `assignee_handle`. Every pre-existing fail-closed guard is untouched. Tests 134 -> 147: reuse, first start, orphaned, disconnected, empty list, assignee on another worktree, both probes returning ok:false, both returning malformed JSON, a blank payload, and proof the probe never runs in --terminal or composed mode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
The gap
SKILL.md's "Re-entry (resume)" restarts a proven-dead worker with--worktree id:<…> --agent claude.GROUNDWORK_ESCALATION_DIRis always set for a dev-loop worker, so that lands in WORKER MODE, whose body ranorca terminal createunconditionally.If the coordinator's liveness read was wrong — or the worker was only briefly unreachable — that put a second Claude agent on the same checkout, with the Dispatch bound to only one of them. Two agents editing one working tree corrupt each other's edits silently. The tmux path has no such hazard:
launch-session.shdetects the existing session and reuses it, loudly, on stderr.The fix
Worker mode is now idempotent on re-entry, automatically — no flag to pass. Before creating anything it asks Orca whether this task's Dispatch already names a terminal that is still live on the target worktree:
orchestration dispatch-show --task <id>→.result.dispatch.assignee_handleterminal list --worktree <selector>, requiringorphaned != trueandconnected == true--perm/--nameare inert)terminal create+tui-idlewait + bindThe reuse target is identified by the Dispatch record rather than a title/preview heuristic because a single worktree can hold both an agent terminal and a plain shell — binding a Dispatch to the user's shell would type the worker preamble into
bash.Why "unknown" refuses instead of creating
The two error directions are asymmetric. A wrong create = two agents on one checkout: silent and unrecoverable by the time anyone notices. A wrong refusal = one restart stalls, loudly and exit-coded, and the coordinator can
orca orchestration worker-stopand re-run. This mirrorsorca-worktree-alive.sh's existing rule that "unknown" is never a definite answer that causes damage.Known limitation (stated, not hidden)
This is a check-then-create, so it protects a sequential re-entry, not two coordinators racing on one worktree. Closing that window needs resource-side exclusivity (Orca refusing a second agent terminal per worktree, or a fencing token on the Dispatch).
Tests
tests/orca-worker-start.bats: 29 → 42 tests (13 new — 2 normal, 5 error, 6 boundary). All 29 pre-existing tests are byte-identical (0 removed lines); every pre-existing fail-closed guard still holds.npx bats tests/→ 147 passing, 0 failuressh -n/dash -nclean, no bashisms (POSIX sh,set -upreserved)[ "$live" -ge 1 ]→-ge 0turns the 4 boundary tests red; making the refusal guards unreachable turns all 5 exit-6 tests redtest-quality-auditoraudit: VERDICT: PASS (re-ran both mutants itself)Also included: a test-isolation fix
setup()now doesunset GROUNDWORK_ESCALATION_DIR GROUNDWORK_TASK_ID. A dev-loop worker session exports that variable and bats'run env FOO=bar …inherits the caller's environment, so 6 pre-existing tests failed at baseline when the suite was run inside a worker session (verified by stashing the test file and re-running). CI never saw it because CI's environment is clean. This is an addition tosetup()only — no existing test body was touched.Follow-ups (deliberately out of scope)
</dev/nullon the three pre-existingorcacalls in this file (the new probe calls have it)orca-spawn.sh, which has the same unconditional-create shape🤖 Generated with Claude Code