diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 8a1349a7852..8c6a4646386 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -13658,6 +13658,7 @@ "invariant": "Starting a worker in the coordinator's current workspace must materialize one inactive terminal tab before worker-start returns, preserve coordinator focus, and remain exactly once after workspace re-entry. After an app update or restart, an exact live legacy worker must fence automatic provider resume, adopt its original PTY into its original background pane, retain readable output, and clear the resume record without spawning, writing, signalling, interrupting, replacing, or focusing the worker. A current-contract worker whose renderer graph identity is temporarily absent must retain its Dispatch capability and settle exactly once from exact hook-attested handle, pane, and process evidence; otherwise only an exact attested coordinator may take over. A worker_done caller may report success only after the owning runtime returns an explicit lifecycle verdict or authoritative reads prove that the exact Task, Dispatch, and worker report receipt settled the expected outcome. Federated terminal settlement must remain replay-eligible until the worker durably acknowledges it, and identical same-outcome retries must converge idempotently. Independently updated clients and worker servers must preserve the negotiated protocol: current peers use Run-home lifecycle settlement, while protocol v1/v2 peers retain their legacy completion path without receiving newer-only fields. A federated worker may accept only the authority defined by its negotiated protocol. An exact existing target workspace must receive a discoverable tab without stealing coordinator focus; if renderer reveal fails, worker-start must expose that the live worker remains background-only. Run and Dispatch checks must resolve through the caller's stable pane identity when a terminal handle is reminted, while a live handle outranks mismatched pane metadata. A nested worker's creator edge requires the current creator pane, process incarnation, and owning Run generation; reminting and rebinding that pane to another Run must remove the stale edge. Explicit legacy terminal inspection remains handle-scoped, and remote or headless worker presentation remains background-only.", "oracle": "Drive Run create, Task create, and worker-start through production Electron runtimes with a deterministic Codex fixture. Require append-only ledgers with one still-live PID and no interruption, a visible inactive worker tab while the coordinator stays active, Run delivery through stable pane identity, and stable PTY/incarnation, tab, leaf, worktree, Task, and Dispatch across workspace re-entry. In a restart journey, retain the original daemon PTY and PID, remove renderer ownership, retain sleeping-session evidence, mark the Dispatch legacy, relaunch, and require exact inactive tab adoption, readable ACK output, cleared resume state, one spawn, and no resume argv or Conversation interrupted text after another workspace round trip. The service oracle removes renderer lookup identity from current-contract callers while retaining real restored-PTY and hook commitments, replays authenticated completion and takeover across fresh runtimes, and requires one Task, Dispatch, terminal authority, message, mutation, ordinary-mail delivery, remote process fencing, and unchanged fixture marker bytes while foreign pane evidence remains rejected. Unit tests separately remint a creator pane and process from Run A into Run B, require the nested Run A worker to fall back to its current coordinator, require indexed query plans, and bound 300 Task reads with 50,000 retained Runs. They also assert authority-specific legacy affordances, exact identity and owner matching, retained-output fallback, pane-stable routing, federated non-activation, and SSH fallback parity.", "commands": [ + "ORCA_BACKGROUND_LAUNCH=1 npx vitest run --config config/vitest.config.ts src/main/runtime/rpc/methods/orchestration/messaging/check-worker-federated-attachment.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts --reporter=dot", "pnpm exec vitest run --config config/vitest.config.ts src/cli/handlers/orchestration.test.ts src/cli/handlers/orchestration-check-identity.test.ts src/cli/handlers/orchestration-worker-cli.test.ts src/main/runtime/rpc/methods/orchestration/worker/composed-workers.test.ts src/main/runtime/rpc/methods/orchestration/messaging/check.test.ts src/main/runtime/rpc/methods/orchestration/messaging/send.test.ts src/main/ssh/ssh-remote-orca-cli.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/cli/handlers/orchestration-lifecycle-rejection.test.ts src/cli/handlers/orchestration-lifecycle-json-rejection.test.ts src/cli/handlers/orchestration-migration.test.ts", @@ -13672,6 +13673,7 @@ "pnpm run build:cli && SKIP_BUILD=1 pnpm exec playwright test tests/e2e/orchestration-worker-settlement-release-cli.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1" ], "testFiles": [ + "src/main/runtime/rpc/methods/orchestration/messaging/check-worker-federated-attachment.test.ts", "src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts", "src/main/runtime/orchestration/formatter.test.ts", "src/main/runtime/orchestration/orchestration-legacy-worker-terminal-recovery.test.ts", @@ -13695,6 +13697,13 @@ "tests/e2e/orchestration-worker-settlement-release-cli.spec.ts" ], "assertionRefs": [ + { + "file": "src/main/runtime/rpc/methods/orchestration/messaging/check-worker-federated-attachment.test.ts", + "assertions": [ + "replays the coordinator instruction and takes its ack after the app restarts", + "files loopback mail once under the local Dispatch Run without replacing its owner" + ] + }, { "file": "src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts", "assertions": [ diff --git a/config/scripts/orchestration-skill-guidance.test.mjs b/config/scripts/orchestration-skill-guidance.test.mjs index ce501954322..c15e3e93ea8 100644 --- a/config/scripts/orchestration-skill-guidance.test.mjs +++ b/config/scripts/orchestration-skill-guidance.test.mjs @@ -168,9 +168,10 @@ describe('orchestration kernel', () => { expect(kernel).toContain( '`projection.attention` categories, `projection.attention.requiresAction`, and literal `projection.nextAction` argv' ) - expect(kernel).toContain( - 'An `inspect` `nextAction` on a `live` row with `attention.requiresAction` false is informational, not a command to re-run: keep waiting with `check --wait`' - ) + // Unverifiable workers can still owe release; the guide must explain the action itself. + expect(kernel).toContain('A `none` `nextAction` has no argv to run') + expect(kernel).toContain('read `liveness.reason` and keep waiting with `check --wait`') + expect(kernel).toContain('Absence never earns an argv; settlement and pending work still do') expect(kernel).toContain('choose `worker-stop` or `worker-abandon`') }) diff --git a/skill-guides/orchestration.md b/skill-guides/orchestration.md index 4e49a0d84af..d744785ab10 100644 --- a/skill-guides/orchestration.md +++ b/skill-guides/orchestration.md @@ -137,8 +137,8 @@ After three consecutive empty waits, stop waiting blindly and enumerate with `ORCA orchestration worker-list --include-remote --json` (defaults to the bound Run; `--run ` overrides; the receipt's `scope` names which), acting on each row's `projection.attention` categories, `projection.attention.requiresAction`, and literal `projection.nextAction` argv. -An `inspect` `nextAction` on a `live` row with `attention.requiresAction` false -is informational, not a command to re-run: keep waiting with `check --wait`. +A `none` `nextAction` has no argv to run: read `liveness.reason` and keep waiting +with `check --wait`. Absence never earns an argv; settlement and pending work still do. Leave the wait only on positive proof the agent stopped: `exited` liveness, the worker's own observation of process exit, or a transcript whose final agent turn sent no `worker_done`. Then load `references/recovery-and-cleanup.md` and choose diff --git a/src/cli/bundled-skill-guides.ts b/src/cli/bundled-skill-guides.ts index fc30604a264..47b5559f01b 100644 --- a/src/cli/bundled-skill-guides.ts +++ b/src/cli/bundled-skill-guides.ts @@ -66,10 +66,10 @@ const ORCA_PER_WORKSPACE_ENV_SSH_HOST_REFERENCE_MARKDOWN = "# SSH connection mod const ORCA_PER_WORKSPACE_ENV_WINDOWS_SCRIPTS_REFERENCE_MARKDOWN = "# Windows local-side scripts\n\nLoad this when the user's desktop is Windows and you are scaffolding the local-side scripts. A bare\n`.sh` will not execute there. Either require WSL or Git Bash and point `orca.yaml` at a launcher such\nas `bash ./scripts/orca-vm/.sh` through a `.cmd` file, or scaffold PowerShell equivalents.\n\nThe remote-side commands you run inside the Linux environment stay bash regardless of the desktop OS.\n\n```powershell\n#requires -Version 5\n$ErrorActionPreference = 'Stop'\n# resolve env→state→fallback; run the provider CLI / ssh the same way;\n# capture provider output; build the result object for the chosen mode and write ONE line of JSON to stdout.\n# Orca-server mode: @{ schemaVersion=1; pairingCode=$pairingCode; projectRoot=$projectRoot; userData=@{...} }\n# SSH mode: @{ schemaVersion=1; connection=@{ type=\"ssh\"; projectRoot=$projectRoot;\n# target=@{ label=$label; host=$host; port=$port; username=$user } } }\n($result | ConvertTo-Json -Compress -Depth 6)\n# progress/errors → Write-Error / the error stream, never stdout.\n```\n\nThe doctor's executable-bit check is a POSIX concept and is skipped on Windows, so a script that is\nunusable on the user's machine for a different reason still has to be caught by the `--provision`\nself-test.\n" // oxfmt-ignore -const ORCHESTRATION_MARKDOWN = "---\nname: orchestration\ndescription: >-\n Coordinate supervised Orca workers: threaded messages, blocking ask/reply,\n task dispatch, worker_done/escalation waits, task DAGs, decision gates,\n coordinator loops, and decomposing work across agents. Use `orca-cli` for full\n ownership handoffs — \"hand off\", \"handoff\", \"handover\", \"give this to another\n agent\", \"another worktree\" — unless asked to supervise, monitor, or coordinate\n a DAG, and for terminal control, lightweight terminal prompts, shell commands,\n Orca worktree management, and reading or waiting on terminals. Use Computer\n Use for external browser windows, webviews, Orca app UI, or desktop UI outside\n Orca's embedded browser only when the task requires OS/window-level control\n such as focus, menus, dialogs, coordinates, or screenshots. Use `orca-cli` for\n Orca's embedded pages and a page-automation tool such as Playwright or CDP for\n external pages.\n---\n\n# Orca orchestration\n\nOrchestration is Orca's structured coordination layer. It records who owns work,\nwhich attempt is authoritative, and when supervised work has settled.\n\n## Outcome\n\n**Result:** every in-scope Task has one explicit outcome and every settled worker\nterminal has a next owner or cleanup decision. **Next consumer:** the user who\nrequested supervision. **Done:** all expected Dispatches have settled, every\ndelivered message was processed before acknowledgment, each settled worker was\nreused, explicitly retained, or released, and the turn ends only when the report\nto that user names, per Task, its outcome, the evidence behind it, and any\nunresolved blocker.\n\n**Safe failure:** preserve work and authority and report the state as unknown or\n`unverifiable`. Only positive proof of exit authorizes stop, abandon, or retry,\nand only an accepted settlement authorizes release. Every other observation,\nabsence included, is a checkpoint.\n\n## Classify the role\n\n| Current context | Role | Route |\n| ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------ |\n| The user explicitly asks to supervise, monitor, wait for results, track completion, coordinate a DAG, use a decision gate, or manage ask/reply | Coordinator | Use the supervised loop below |\n| The current prompt contains a live injected preamble with Task and Dispatch IDs | Dispatched worker | Follow the preamble and the worker obligations below |\n| The user asks to hand off ownership or start another agent/worktree without supervision | Handoff owner | Use `orca-cli`; create no Run, Task, or Dispatch and do not monitor completion |\n| A message carries a legacy authority label | Compatibility operator | Load the legacy contract reference before any lifecycle mutation |\n| No live preamble and no explicit supervision | Ordinary terminal agent | Do not emit lifecycle messages; use `orca-cli` for terminal/worktree work |\n\nModel or effort selection does not make a handoff supervised. Never substitute a\nnon-Orca subagent tool when Orca orchestration provenance was requested.\n\n## Authority and safety floor\n\n- A Run is a durable namespace and coordinator inbox; it does not schedule or\n place workers. A Task is work. A Dispatch is one authoritative Task attempt.\n- Lifecycle authority comes from the active Dispatch, not a terminal title,\n copied ID, old database row, provider transcript, or visible pane.\n- Workers use the exact executable, handle, capability, Task ID, and Dispatch ID\n in the live preamble. Never reconstruct, translate, or broaden those arguments.\n- After remote start, address the worker by Dispatch ID. The execution host owns\n process, filesystem, transcript, stop, and cleanup facts. Preserve the verdicts\n `live` / `unverifiable` / `exited`; contact loss is not process death.\n- Liveness is layered: `worker-list`'s `projection.liveness` is the fleet verdict\n for the agent; `worker-show`'s `observation.status` is PTY liveness only. A live\n terminal can still hold a dead or stuck agent.\n- Folder workspaces are valid; never require Git or assume a worktree.\n- Clients and remote servers update independently. Treat unknown optional fields\n as absent. A new stream operation requires advertised capability because old\n decoders may silently drop unknown opcodes. Never fall back to local execution\n when remote authority or capability is unproven.\n- Use the executable you used to run `skills get` for the entire run. In the\n examples below, replace `ORCA` with it; do not create a shell variable or run\n `ORCA` literally. If it fails, report that exact error instead of switching.\n- A successful `orchestration send` proves durable enqueue; its wake or nudge is\n best-effort attention only and does not prove the recipient read or accepted it.\n\n## Worker obligations\n\nThe injected preamble is authoritative. A dispatched worker must:\n\n1. Do only the current Task and use the preamble's `ask` command for a blocking\n coordinator question. Never open a local question TUI the coordinator cannot\n answer. Resume the same message ID after an ask timeout.\n2. Send heartbeats only at the cadence in the preamble. A heartbeat proves\n liveness, not completion.\n3. Read coordinator follow-ups at each natural checkpoint — before starting a\n new file, after a test run — and once more immediately before `worker_done`:\n `ORCA orchestration check --terminal --json`.\n4. Send `worker_done` exactly once, from the dispatched terminal, with a\n three-sentence executive summary, both lifecycle IDs, and explicit\n `--outcome succeeded` or `--outcome failed`. Never encode failure only in prose.\n5. Append `--files-modified` and `--report-path` only with real values when\n applicable. After `worker_done`, end the dispatched turn and idle; do not poll\n or start new work.\n\nA direct user instruction after completion starts new user-owned work and takes\nprecedence over the idle rule. Do not reuse the settled lifecycle IDs.\n\n## Canonical supervised loop\n\nConfirm the runtime, bind one Run, and start the full independent wave before\nwaiting. `worker-start --spec` creates the Task and its attempt in one call:\n\n```text\nORCA status --json\nORCA orchestration run-create --objective \"\" --json\nORCA orchestration worker-start --spec \"\" --worktree current --agent codex --json\nORCA orchestration worker-start --spec \"\" --worktree current --agent claude --json\nORCA orchestration check --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nIf `worker-start` exits non-zero, do not relaunch. Read the receipt's\n`failedStage` and `residualResources`, then load\n`references/recovery-and-cleanup.md`.\n\nUse `task-create` plus `worker-start --task ` for planned fan-out with\ndependencies or a retry of a known Task. Use dependencies only for real ordering\nand prefer parallel waves over chains deeper than three or four steps; nested\nworkers obey the depth limit, and a new Run does not reset the caller's depth.\n\nA consuming `check` names its caller with `--terminal `, never `--from`;\nomit it inside the coordinator's own Orca terminal. It returns the bound Run's\noldest FIFO Delivery and replays that batch until acknowledged. Process every\nmessage: reply to questions, validate each `worker_done` against the expected\nactive Dispatch, and decide each settled terminal's next owner before the ack:\n\n```text\nORCA orchestration reply --id --body \"\" --json\nORCA orchestration worker-release --dispatch --json\nORCA orchestration check --ack --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nKeep waiting until every expected Dispatch settles. A timeout or empty result is\na checkpoint, not a failure. Do not stop, retry, release, or launch a duplicate\neditor without the positive proof `## Outcome` requires.\n\nAfter three consecutive empty waits, stop waiting blindly and enumerate with\n`ORCA orchestration worker-list --include-remote --json` (defaults to the bound\nRun; `--run ` overrides; the receipt's `scope` names which), acting on\neach row's `projection.attention` categories, `projection.attention.requiresAction`, and literal `projection.nextAction` argv.\nAn `inspect` `nextAction` on a `live` row with `attention.requiresAction` false\nis informational, not a command to re-run: keep waiting with `check --wait`.\nLeave the wait only on positive proof the agent stopped: `exited` liveness, the\nworker's own observation of process exit, or a transcript whose final agent turn\nsent no `worker_done`. Then load `references/recovery-and-cleanup.md` and choose\n`worker-stop` or `worker-abandon` explicitly. `unverifiable` is absence,\nincluding when `worker-show` reports `agentWait` null. Absence never authorizes\nstop, abandon, retry, or release; keep waiting or inspect.\n\n`worker-start` is the normal path, composing placement, terminal readiness,\nprompt injection, and supervised resource ownership. `dispatch --inject` leaves\nan operator-created process unsupervised and is only for an expressiveness gap.\n\n## Task-spec contract\n\nEvery Task spec must be self-contained and name:\n\n- **Target:** the files, component, or environment in scope.\n- **Change:** the concrete result to produce.\n- **Constraints:** invariants, compatibility rules, and do-not-touch boundaries.\n- **Ownership:** what this worker may edit and any coordination boundary.\n- **Observable acceptance:** the test, output, or evidence that proves completion.\n\n## Completion accounting\n\nAfter an accepted success or failure report, immediately do exactly one:\n\n1. Reuse the same proven agent terminal for an immediate follow-up Dispatch.\n2. Record user-requested retention with `worker-retain`.\n3. Run `worker-release`.\n\nRelease is post-settlement cleanup, not cancellation. Only an accepted\nsettlement authorizes it; no other observation does. If release is uncertain,\nfollow its exact recovery receipt and never substitute `terminal close`.\n\nA valid `worker_done` settles the Task and Dispatch automatically; do not follow\nit with `task-update --status completed`. Enumerate the terminals still owing a\ndecision with `worker-list --run --terminal-state reclaimable --json`,\nand do not end the coordinator turn until it returns none.\n\n## Conditional references\n\nThis compact guide is sufficient for the normal local loop. At an action gate\nbelow, run `ORCA skills get orchestration --reference references/.md` and\nread only that document; `--references` lists the names. If the CLI rejects\n`--reference`, run `ORCA skills get orchestration --full` once instead: it\nreturns this exact kernel and every reference, so read only the named one. If an\nolder CLI rejects `--full`, keep this kernel's safety floor, use that command's\n`--help`, and never guess newer flags.\n\n| Action gate | Bundled reference |\n| ------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |\n| Expanded DAG waves, launch model/effort, same-terminal reuse, or review ownership | `references/coordinator-loop.md` |\n| You are a dispatched worker and the live preamble does not answer your question, or `check` returned an error | `references/worker-contract.md` |\n| New worktree, exact workspace, SSH, WSL, or connected-server placement | `references/placement-and-remote.md` |\n| Inbox replay, follow-up messages, group addresses, or decision gates | `references/messaging-and-gates.md` |\n| Failed/stopped/unknown attempts, retry, stop, abandon, retain, or uncertain release | `references/recovery-and-cleanup.md` |\n| Custom argv or terminal topology that `worker-start` cannot express | `references/low-level-topology.md` |\n| Any legacy label, adopted Run, compatibility receipt, or takeover | `references/legacy-contract-migration.md` |\n\nRetired scheduler commands are not aliases for Run creation. Recovery commands\nmust provide their exact next action; follow it with the same selected executable.\n" +const ORCHESTRATION_MARKDOWN = "---\nname: orchestration\ndescription: >-\n Coordinate supervised Orca workers: threaded messages, blocking ask/reply,\n task dispatch, worker_done/escalation waits, task DAGs, decision gates,\n coordinator loops, and decomposing work across agents. Use `orca-cli` for full\n ownership handoffs — \"hand off\", \"handoff\", \"handover\", \"give this to another\n agent\", \"another worktree\" — unless asked to supervise, monitor, or coordinate\n a DAG, and for terminal control, lightweight terminal prompts, shell commands,\n Orca worktree management, and reading or waiting on terminals. Use Computer\n Use for external browser windows, webviews, Orca app UI, or desktop UI outside\n Orca's embedded browser only when the task requires OS/window-level control\n such as focus, menus, dialogs, coordinates, or screenshots. Use `orca-cli` for\n Orca's embedded pages and a page-automation tool such as Playwright or CDP for\n external pages.\n---\n\n# Orca orchestration\n\nOrchestration is Orca's structured coordination layer. It records who owns work,\nwhich attempt is authoritative, and when supervised work has settled.\n\n## Outcome\n\n**Result:** every in-scope Task has one explicit outcome and every settled worker\nterminal has a next owner or cleanup decision. **Next consumer:** the user who\nrequested supervision. **Done:** all expected Dispatches have settled, every\ndelivered message was processed before acknowledgment, each settled worker was\nreused, explicitly retained, or released, and the turn ends only when the report\nto that user names, per Task, its outcome, the evidence behind it, and any\nunresolved blocker.\n\n**Safe failure:** preserve work and authority and report the state as unknown or\n`unverifiable`. Only positive proof of exit authorizes stop, abandon, or retry,\nand only an accepted settlement authorizes release. Every other observation,\nabsence included, is a checkpoint.\n\n## Classify the role\n\n| Current context | Role | Route |\n| ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------ |\n| The user explicitly asks to supervise, monitor, wait for results, track completion, coordinate a DAG, use a decision gate, or manage ask/reply | Coordinator | Use the supervised loop below |\n| The current prompt contains a live injected preamble with Task and Dispatch IDs | Dispatched worker | Follow the preamble and the worker obligations below |\n| The user asks to hand off ownership or start another agent/worktree without supervision | Handoff owner | Use `orca-cli`; create no Run, Task, or Dispatch and do not monitor completion |\n| A message carries a legacy authority label | Compatibility operator | Load the legacy contract reference before any lifecycle mutation |\n| No live preamble and no explicit supervision | Ordinary terminal agent | Do not emit lifecycle messages; use `orca-cli` for terminal/worktree work |\n\nModel or effort selection does not make a handoff supervised. Never substitute a\nnon-Orca subagent tool when Orca orchestration provenance was requested.\n\n## Authority and safety floor\n\n- A Run is a durable namespace and coordinator inbox; it does not schedule or\n place workers. A Task is work. A Dispatch is one authoritative Task attempt.\n- Lifecycle authority comes from the active Dispatch, not a terminal title,\n copied ID, old database row, provider transcript, or visible pane.\n- Workers use the exact executable, handle, capability, Task ID, and Dispatch ID\n in the live preamble. Never reconstruct, translate, or broaden those arguments.\n- After remote start, address the worker by Dispatch ID. The execution host owns\n process, filesystem, transcript, stop, and cleanup facts. Preserve the verdicts\n `live` / `unverifiable` / `exited`; contact loss is not process death.\n- Liveness is layered: `worker-list`'s `projection.liveness` is the fleet verdict\n for the agent; `worker-show`'s `observation.status` is PTY liveness only. A live\n terminal can still hold a dead or stuck agent.\n- Folder workspaces are valid; never require Git or assume a worktree.\n- Clients and remote servers update independently. Treat unknown optional fields\n as absent. A new stream operation requires advertised capability because old\n decoders may silently drop unknown opcodes. Never fall back to local execution\n when remote authority or capability is unproven.\n- Use the executable you used to run `skills get` for the entire run. In the\n examples below, replace `ORCA` with it; do not create a shell variable or run\n `ORCA` literally. If it fails, report that exact error instead of switching.\n- A successful `orchestration send` proves durable enqueue; its wake or nudge is\n best-effort attention only and does not prove the recipient read or accepted it.\n\n## Worker obligations\n\nThe injected preamble is authoritative. A dispatched worker must:\n\n1. Do only the current Task and use the preamble's `ask` command for a blocking\n coordinator question. Never open a local question TUI the coordinator cannot\n answer. Resume the same message ID after an ask timeout.\n2. Send heartbeats only at the cadence in the preamble. A heartbeat proves\n liveness, not completion.\n3. Read coordinator follow-ups at each natural checkpoint — before starting a\n new file, after a test run — and once more immediately before `worker_done`:\n `ORCA orchestration check --terminal --json`.\n4. Send `worker_done` exactly once, from the dispatched terminal, with a\n three-sentence executive summary, both lifecycle IDs, and explicit\n `--outcome succeeded` or `--outcome failed`. Never encode failure only in prose.\n5. Append `--files-modified` and `--report-path` only with real values when\n applicable. After `worker_done`, end the dispatched turn and idle; do not poll\n or start new work.\n\nA direct user instruction after completion starts new user-owned work and takes\nprecedence over the idle rule. Do not reuse the settled lifecycle IDs.\n\n## Canonical supervised loop\n\nConfirm the runtime, bind one Run, and start the full independent wave before\nwaiting. `worker-start --spec` creates the Task and its attempt in one call:\n\n```text\nORCA status --json\nORCA orchestration run-create --objective \"\" --json\nORCA orchestration worker-start --spec \"\" --worktree current --agent codex --json\nORCA orchestration worker-start --spec \"\" --worktree current --agent claude --json\nORCA orchestration check --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nIf `worker-start` exits non-zero, do not relaunch. Read the receipt's\n`failedStage` and `residualResources`, then load\n`references/recovery-and-cleanup.md`.\n\nUse `task-create` plus `worker-start --task ` for planned fan-out with\ndependencies or a retry of a known Task. Use dependencies only for real ordering\nand prefer parallel waves over chains deeper than three or four steps; nested\nworkers obey the depth limit, and a new Run does not reset the caller's depth.\n\nA consuming `check` names its caller with `--terminal `, never `--from`;\nomit it inside the coordinator's own Orca terminal. It returns the bound Run's\noldest FIFO Delivery and replays that batch until acknowledged. Process every\nmessage: reply to questions, validate each `worker_done` against the expected\nactive Dispatch, and decide each settled terminal's next owner before the ack:\n\n```text\nORCA orchestration reply --id --body \"\" --json\nORCA orchestration worker-release --dispatch --json\nORCA orchestration check --ack --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nKeep waiting until every expected Dispatch settles. A timeout or empty result is\na checkpoint, not a failure. Do not stop, retry, release, or launch a duplicate\neditor without the positive proof `## Outcome` requires.\n\nAfter three consecutive empty waits, stop waiting blindly and enumerate with\n`ORCA orchestration worker-list --include-remote --json` (defaults to the bound\nRun; `--run ` overrides; the receipt's `scope` names which), acting on\neach row's `projection.attention` categories, `projection.attention.requiresAction`, and literal `projection.nextAction` argv.\nA `none` `nextAction` has no argv to run: read `liveness.reason` and keep waiting\nwith `check --wait`. Absence never earns an argv; settlement and pending work still do.\nLeave the wait only on positive proof the agent stopped: `exited` liveness, the\nworker's own observation of process exit, or a transcript whose final agent turn\nsent no `worker_done`. Then load `references/recovery-and-cleanup.md` and choose\n`worker-stop` or `worker-abandon` explicitly. `unverifiable` is absence,\nincluding when `worker-show` reports `agentWait` null. Absence never authorizes\nstop, abandon, retry, or release; keep waiting or inspect.\n\n`worker-start` is the normal path, composing placement, terminal readiness,\nprompt injection, and supervised resource ownership. `dispatch --inject` leaves\nan operator-created process unsupervised and is only for an expressiveness gap.\n\n## Task-spec contract\n\nEvery Task spec must be self-contained and name:\n\n- **Target:** the files, component, or environment in scope.\n- **Change:** the concrete result to produce.\n- **Constraints:** invariants, compatibility rules, and do-not-touch boundaries.\n- **Ownership:** what this worker may edit and any coordination boundary.\n- **Observable acceptance:** the test, output, or evidence that proves completion.\n\n## Completion accounting\n\nAfter an accepted success or failure report, immediately do exactly one:\n\n1. Reuse the same proven agent terminal for an immediate follow-up Dispatch.\n2. Record user-requested retention with `worker-retain`.\n3. Run `worker-release`.\n\nRelease is post-settlement cleanup, not cancellation. Only an accepted\nsettlement authorizes it; no other observation does. If release is uncertain,\nfollow its exact recovery receipt and never substitute `terminal close`.\n\nA valid `worker_done` settles the Task and Dispatch automatically; do not follow\nit with `task-update --status completed`. Enumerate the terminals still owing a\ndecision with `worker-list --run --terminal-state reclaimable --json`,\nand do not end the coordinator turn until it returns none.\n\n## Conditional references\n\nThis compact guide is sufficient for the normal local loop. At an action gate\nbelow, run `ORCA skills get orchestration --reference references/.md` and\nread only that document; `--references` lists the names. If the CLI rejects\n`--reference`, run `ORCA skills get orchestration --full` once instead: it\nreturns this exact kernel and every reference, so read only the named one. If an\nolder CLI rejects `--full`, keep this kernel's safety floor, use that command's\n`--help`, and never guess newer flags.\n\n| Action gate | Bundled reference |\n| ------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |\n| Expanded DAG waves, launch model/effort, same-terminal reuse, or review ownership | `references/coordinator-loop.md` |\n| You are a dispatched worker and the live preamble does not answer your question, or `check` returned an error | `references/worker-contract.md` |\n| New worktree, exact workspace, SSH, WSL, or connected-server placement | `references/placement-and-remote.md` |\n| Inbox replay, follow-up messages, group addresses, or decision gates | `references/messaging-and-gates.md` |\n| Failed/stopped/unknown attempts, retry, stop, abandon, retain, or uncertain release | `references/recovery-and-cleanup.md` |\n| Custom argv or terminal topology that `worker-start` cannot express | `references/low-level-topology.md` |\n| Any legacy label, adopted Run, compatibility receipt, or takeover | `references/legacy-contract-migration.md` |\n\nRetired scheduler commands are not aliases for Run creation. Recovery commands\nmust provide their exact next action; follow it with the same selected executable.\n" // oxfmt-ignore -const ORCHESTRATION_FULL_MARKDOWN = "---\nname: orchestration\ndescription: >-\n Coordinate supervised Orca workers: threaded messages, blocking ask/reply,\n task dispatch, worker_done/escalation waits, task DAGs, decision gates,\n coordinator loops, and decomposing work across agents. Use `orca-cli` for full\n ownership handoffs — \"hand off\", \"handoff\", \"handover\", \"give this to another\n agent\", \"another worktree\" — unless asked to supervise, monitor, or coordinate\n a DAG, and for terminal control, lightweight terminal prompts, shell commands,\n Orca worktree management, and reading or waiting on terminals. Use Computer\n Use for external browser windows, webviews, Orca app UI, or desktop UI outside\n Orca's embedded browser only when the task requires OS/window-level control\n such as focus, menus, dialogs, coordinates, or screenshots. Use `orca-cli` for\n Orca's embedded pages and a page-automation tool such as Playwright or CDP for\n external pages.\n---\n\n# Orca orchestration\n\nOrchestration is Orca's structured coordination layer. It records who owns work,\nwhich attempt is authoritative, and when supervised work has settled.\n\n## Outcome\n\n**Result:** every in-scope Task has one explicit outcome and every settled worker\nterminal has a next owner or cleanup decision. **Next consumer:** the user who\nrequested supervision. **Done:** all expected Dispatches have settled, every\ndelivered message was processed before acknowledgment, each settled worker was\nreused, explicitly retained, or released, and the turn ends only when the report\nto that user names, per Task, its outcome, the evidence behind it, and any\nunresolved blocker.\n\n**Safe failure:** preserve work and authority and report the state as unknown or\n`unverifiable`. Only positive proof of exit authorizes stop, abandon, or retry,\nand only an accepted settlement authorizes release. Every other observation,\nabsence included, is a checkpoint.\n\n## Classify the role\n\n| Current context | Role | Route |\n| ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------ |\n| The user explicitly asks to supervise, monitor, wait for results, track completion, coordinate a DAG, use a decision gate, or manage ask/reply | Coordinator | Use the supervised loop below |\n| The current prompt contains a live injected preamble with Task and Dispatch IDs | Dispatched worker | Follow the preamble and the worker obligations below |\n| The user asks to hand off ownership or start another agent/worktree without supervision | Handoff owner | Use `orca-cli`; create no Run, Task, or Dispatch and do not monitor completion |\n| A message carries a legacy authority label | Compatibility operator | Load the legacy contract reference before any lifecycle mutation |\n| No live preamble and no explicit supervision | Ordinary terminal agent | Do not emit lifecycle messages; use `orca-cli` for terminal/worktree work |\n\nModel or effort selection does not make a handoff supervised. Never substitute a\nnon-Orca subagent tool when Orca orchestration provenance was requested.\n\n## Authority and safety floor\n\n- A Run is a durable namespace and coordinator inbox; it does not schedule or\n place workers. A Task is work. A Dispatch is one authoritative Task attempt.\n- Lifecycle authority comes from the active Dispatch, not a terminal title,\n copied ID, old database row, provider transcript, or visible pane.\n- Workers use the exact executable, handle, capability, Task ID, and Dispatch ID\n in the live preamble. Never reconstruct, translate, or broaden those arguments.\n- After remote start, address the worker by Dispatch ID. The execution host owns\n process, filesystem, transcript, stop, and cleanup facts. Preserve the verdicts\n `live` / `unverifiable` / `exited`; contact loss is not process death.\n- Liveness is layered: `worker-list`'s `projection.liveness` is the fleet verdict\n for the agent; `worker-show`'s `observation.status` is PTY liveness only. A live\n terminal can still hold a dead or stuck agent.\n- Folder workspaces are valid; never require Git or assume a worktree.\n- Clients and remote servers update independently. Treat unknown optional fields\n as absent. A new stream operation requires advertised capability because old\n decoders may silently drop unknown opcodes. Never fall back to local execution\n when remote authority or capability is unproven.\n- Use the executable you used to run `skills get` for the entire run. In the\n examples below, replace `ORCA` with it; do not create a shell variable or run\n `ORCA` literally. If it fails, report that exact error instead of switching.\n- A successful `orchestration send` proves durable enqueue; its wake or nudge is\n best-effort attention only and does not prove the recipient read or accepted it.\n\n## Worker obligations\n\nThe injected preamble is authoritative. A dispatched worker must:\n\n1. Do only the current Task and use the preamble's `ask` command for a blocking\n coordinator question. Never open a local question TUI the coordinator cannot\n answer. Resume the same message ID after an ask timeout.\n2. Send heartbeats only at the cadence in the preamble. A heartbeat proves\n liveness, not completion.\n3. Read coordinator follow-ups at each natural checkpoint — before starting a\n new file, after a test run — and once more immediately before `worker_done`:\n `ORCA orchestration check --terminal --json`.\n4. Send `worker_done` exactly once, from the dispatched terminal, with a\n three-sentence executive summary, both lifecycle IDs, and explicit\n `--outcome succeeded` or `--outcome failed`. Never encode failure only in prose.\n5. Append `--files-modified` and `--report-path` only with real values when\n applicable. After `worker_done`, end the dispatched turn and idle; do not poll\n or start new work.\n\nA direct user instruction after completion starts new user-owned work and takes\nprecedence over the idle rule. Do not reuse the settled lifecycle IDs.\n\n## Canonical supervised loop\n\nConfirm the runtime, bind one Run, and start the full independent wave before\nwaiting. `worker-start --spec` creates the Task and its attempt in one call:\n\n```text\nORCA status --json\nORCA orchestration run-create --objective \"\" --json\nORCA orchestration worker-start --spec \"\" --worktree current --agent codex --json\nORCA orchestration worker-start --spec \"\" --worktree current --agent claude --json\nORCA orchestration check --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nIf `worker-start` exits non-zero, do not relaunch. Read the receipt's\n`failedStage` and `residualResources`, then load\n`references/recovery-and-cleanup.md`.\n\nUse `task-create` plus `worker-start --task ` for planned fan-out with\ndependencies or a retry of a known Task. Use dependencies only for real ordering\nand prefer parallel waves over chains deeper than three or four steps; nested\nworkers obey the depth limit, and a new Run does not reset the caller's depth.\n\nA consuming `check` names its caller with `--terminal `, never `--from`;\nomit it inside the coordinator's own Orca terminal. It returns the bound Run's\noldest FIFO Delivery and replays that batch until acknowledged. Process every\nmessage: reply to questions, validate each `worker_done` against the expected\nactive Dispatch, and decide each settled terminal's next owner before the ack:\n\n```text\nORCA orchestration reply --id --body \"\" --json\nORCA orchestration worker-release --dispatch --json\nORCA orchestration check --ack --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nKeep waiting until every expected Dispatch settles. A timeout or empty result is\na checkpoint, not a failure. Do not stop, retry, release, or launch a duplicate\neditor without the positive proof `## Outcome` requires.\n\nAfter three consecutive empty waits, stop waiting blindly and enumerate with\n`ORCA orchestration worker-list --include-remote --json` (defaults to the bound\nRun; `--run ` overrides; the receipt's `scope` names which), acting on\neach row's `projection.attention` categories, `projection.attention.requiresAction`, and literal `projection.nextAction` argv.\nAn `inspect` `nextAction` on a `live` row with `attention.requiresAction` false\nis informational, not a command to re-run: keep waiting with `check --wait`.\nLeave the wait only on positive proof the agent stopped: `exited` liveness, the\nworker's own observation of process exit, or a transcript whose final agent turn\nsent no `worker_done`. Then load `references/recovery-and-cleanup.md` and choose\n`worker-stop` or `worker-abandon` explicitly. `unverifiable` is absence,\nincluding when `worker-show` reports `agentWait` null. Absence never authorizes\nstop, abandon, retry, or release; keep waiting or inspect.\n\n`worker-start` is the normal path, composing placement, terminal readiness,\nprompt injection, and supervised resource ownership. `dispatch --inject` leaves\nan operator-created process unsupervised and is only for an expressiveness gap.\n\n## Task-spec contract\n\nEvery Task spec must be self-contained and name:\n\n- **Target:** the files, component, or environment in scope.\n- **Change:** the concrete result to produce.\n- **Constraints:** invariants, compatibility rules, and do-not-touch boundaries.\n- **Ownership:** what this worker may edit and any coordination boundary.\n- **Observable acceptance:** the test, output, or evidence that proves completion.\n\n## Completion accounting\n\nAfter an accepted success or failure report, immediately do exactly one:\n\n1. Reuse the same proven agent terminal for an immediate follow-up Dispatch.\n2. Record user-requested retention with `worker-retain`.\n3. Run `worker-release`.\n\nRelease is post-settlement cleanup, not cancellation. Only an accepted\nsettlement authorizes it; no other observation does. If release is uncertain,\nfollow its exact recovery receipt and never substitute `terminal close`.\n\nA valid `worker_done` settles the Task and Dispatch automatically; do not follow\nit with `task-update --status completed`. Enumerate the terminals still owing a\ndecision with `worker-list --run --terminal-state reclaimable --json`,\nand do not end the coordinator turn until it returns none.\n\n## Conditional references\n\nThis compact guide is sufficient for the normal local loop. At an action gate\nbelow, run `ORCA skills get orchestration --reference references/.md` and\nread only that document; `--references` lists the names. If the CLI rejects\n`--reference`, run `ORCA skills get orchestration --full` once instead: it\nreturns this exact kernel and every reference, so read only the named one. If an\nolder CLI rejects `--full`, keep this kernel's safety floor, use that command's\n`--help`, and never guess newer flags.\n\n| Action gate | Bundled reference |\n| ------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |\n| Expanded DAG waves, launch model/effort, same-terminal reuse, or review ownership | `references/coordinator-loop.md` |\n| You are a dispatched worker and the live preamble does not answer your question, or `check` returned an error | `references/worker-contract.md` |\n| New worktree, exact workspace, SSH, WSL, or connected-server placement | `references/placement-and-remote.md` |\n| Inbox replay, follow-up messages, group addresses, or decision gates | `references/messaging-and-gates.md` |\n| Failed/stopped/unknown attempts, retry, stop, abandon, retain, or uncertain release | `references/recovery-and-cleanup.md` |\n| Custom argv or terminal topology that `worker-start` cannot express | `references/low-level-topology.md` |\n| Any legacy label, adopted Run, compatibility receipt, or takeover | `references/legacy-contract-migration.md` |\n\nRetired scheduler commands are not aliases for Run creation. Recovery commands\nmust provide their exact next action; follow it with the same selected executable.\n\n---\n\n# Bundled references\n\nThese references belong to the version-matched guide above. Read only the documents named by its action gates.\n\n\n\n# Coordinator loop\n\nLoad this reference for expanded DAG waves, per-invocation launch preferences,\nsame-terminal reuse, or review ownership. The compact guide remains the source\nof truth for the loop order and completion boundary.\n\n## Ready waves\n\nCreate independent Tasks before the first wait. Encode only real dependencies,\nthen use the ready view as external memory:\n\n```text\nORCA orchestration task-create --spec \"\" --deps --json\nORCA orchestration task-list --ready --brief --json\n```\n\n`--brief` collapses whitespace and caps echoed specs at 160 characters;\n`spec_truncated` identifies shortened rows. Omit it when full specs are needed or\nwhen an older CLI rejects the flag. A nested worker must respect\n`nested_worker_depth_exceeded`; creating another Run does not reset depth.\n\n## Launch preferences\n\nFor a fresh Claude, Codex, or Cursor terminal, `--model` accepts an opaque\nprovider model ID. Pass it only when the user named a model; otherwise omit it\nso the worker inherits the user's configured agent default. Add `--effort` only\nwhen that model supports it:\n\n```text\nORCA orchestration worker-start --task --worktree current --agent claude --model opus --effort high --json\n```\n\n`--effort` requires `--model`; neither option combines with `--terminal`. A\nconnected worker server must advertise launch-preference support before Orca\nforwards either field. Compare `launch.requested` with `launch.effective`; never\nclaim a model or effort from requested arguments alone.\n\n## Reuse after settlement\n\nChoose the terminal's next owner before acknowledging the Delivery. When the\nsame exact agent has immediate follow-up work, recover the proven handle and\ntransfer cleanup ownership to the new Dispatch:\n\n```text\nORCA orchestration worker-show --dispatch --json\nORCA orchestration worker-start --task --terminal --json\n```\n\nOtherwise explicitly retain or release the settled worker. Do not leave it live\nonly to inspect output; archived output remains available through `worker-read`.\n\n## Review ownership\n\nA review-only `worker_done` authorizes synthesis of findings, not coordinator\nfile edits. Dispatch or hand off fixes unless the user explicitly assigned them\nto the coordinator. If the user's plan names a next owner, post-review fixes and\nPR preparation remain with that owner; the coordinator routes and synthesizes.\n\n\n\n# Legacy contract migration\n\nLoad this reference only for an authority label, adopted Run, compatibility or\nrecovery receipt, or explicit legacy takeover. A newly created attempt always\nuses the current grammar.\n\n## Authority labels\n\n- `[LEGACY COMPATIBILITY]` is live and attested. Run only the exact supported\n command printed with the message, using the same selected executable and\n arguments supplied by the original prompt.\n- `[LEGACY RECOVERY REPLAY — MAY HAVE BEEN SEEN]` is one bounded,\n at-least-once cutover replay. Process it idempotently and acknowledge only\n through the exact displayed guidance.\n- `[LEGACY READ-ONLY]` is inspection-only. It has no reply, acknowledgment, or\n lifecycle mutation.\n- An unlabeled current message uses the current guide and grammar.\n\nAn explicitly selected current Run, attested current binding, current Dispatch,\nor federated attachment takes precedence over legacy fallback. A retained\nadoption record alone does not grant mutation authority. If liveness, principal\nownership, capability, or the exact legacy contract is unproven, degrade to\nread-only inspection and never fall back to local execution.\n\nAdoption preserves the live agent process, PTY/session, terminal handle,\ntab/pane, worktree or folder workspace, Task, and Dispatch. It never restarts or\nreplaces the worker and never revives the retired scheduler. Loss of lifecycle\nauthority does not invalidate the existing process, assignment, or filesystem\nwork. Exact recovery may restore the same PTY once in its original inactive\nbackground tab; it must not spawn, write, signal, stop, switch, focus, split, or\ninject a terminal.\n\n## Compatibility recovery\n\nWhen a compatibility response returns structured next-step arguments, execute\nthose exact arguments with the same selected CLI executable. Do not translate\nfrom memory, broaden the recipient, or retry as a current mutation unless the\nreceipt explicitly authorizes it.\n\nA pending ask, reply, final Dispatch settlement, and consuming check have\ndurable recovery identities. Heartbeat and escalation remain at-least-once\nacross a manual contract-boundary retry. If an ask may already have been\nanswered, run the exact non-consuming recovery check printed by Orca before\ncreating any new question. Never guess among identical question threads.\n\nOn packaged Windows, a legacy ask uses a two-step commit/resume protocol. The\ninitial command commits the question, prints its exact\n`ask --resume ` command, and exits with launcher status `75`. Run\nthat exact resume after the launcher or update boundary. For an attested WSL\nlaunch, preserve the printed `orca-ide` executable and distro route. Older WSL\nworkers without launch proof remain lifecycle read-only even while their\nterminal and filesystem work continue.\n\n## Read-only inspection and takeover\n\nRead-only inspection does not consume mail:\n\n```text\nORCA orchestration run-list --json\nORCA orchestration run-show --id run_legacy_local --json\nORCA orchestration run-show --id --json\nORCA orchestration task-list --run --json\nORCA orchestration inbox --full --json\nORCA orchestration check --terminal --peek --format --json\nORCA terminal read --terminal --json\nORCA terminal wait --terminal --for tui-idle --timeout-ms 60000 --json\n```\n\n`run_legacy_local` is an empty audit tombstone after adoption. Find the ordinary\nRun whose objective is `Recovered orchestration work from a contract update`.\n\nOnly when the original coordinator is unavailable or cannot prove retained\nauthority may a new live coordinator take over from its own terminal:\n\n```text\nORCA orchestration run-use --id --takeover-legacy --json\nORCA orchestration check --run --json\n```\n\nTakeover binds the authenticated invoking terminal; `--from` cannot nominate\nanother coordinator. It fences only the old coordinator and moves pending mail\ninto current Run delivery. It preserves live workers, Tasks, Dispatches, processes, and files.\nNever take over while the original coordinator is actively coordinating.\n\nDo not launch a replacement editor merely because Orca updated or authority is\nunclear. Keep the original worker as the only editor until a stable handoff\npoint, then use a fresh current Dispatch in a conflict-free placement.\n\n\n\n# Low-level topology\n\nLoad this reference only when `worker-start` cannot express required custom argv\nor terminal topology. It is not the normal supervised loop and is never a full\nhandoff recipe.\n\n```text\nORCA terminal create --worktree active --title --command \"\" --json\nORCA terminal wait --terminal --for tui-idle --timeout-ms 60000 --json\nORCA orchestration dispatch --task --to --inject --json\n```\n\nWait for readiness only when startup could lose injected input. Prefer\nagent-first `worker-start` whenever its argv and topology are sufficient.\n\n`dispatch --inject` creates authoritative Task/Dispatch context but deliberately\nkeeps an operator-created process unsupervised: it creates no supervised worker\nresource row. `worker-show`, `worker-read`, and `worker-list` report the lane as\n`unsupervised`; `worker-stop` and `worker-abandon` do not close that process, and\nsettled retain/release take no process action.\n\nUse `worker-start --terminal ` when lifecycle ownership of an existing\nagent terminal is required. Never imply that low-level dispatch retroactively\nowns a process, never use it to route around the nested-depth limit, and never\nuse it for an ownership handoff.\n\n\n\n# Messaging and gates\n\nLoad this reference for inbox replay, attempt-specific guidance, group\naddresses, blocking questions, or coordinator-managed DAG decisions.\n\nA successful `send` proves durable enqueue. Wake and nudge are best-effort\nattention only: neither proves the recipient read the message, began a turn, or\naccepted steering.\n\n## Coordinator delivery loop\n\n`check` names its caller with `--terminal ` and is the only verb that\nrejects `--from`. Omit `--terminal` inside an Orca terminal, where Orca resolves\nthe caller; pass it explicitly from anywhere else, including a dispatched\nworker reading coordinator follow-ups.\n\nA consuming coordinator `check` returns the bound Run's oldest FIFO Delivery,\nup to 50 messages, and replays that exact batch until acknowledged. Process\nevery row and required terminal ownership decision before `--ack`. Type filters\ndecide when a waiter wakes; they do not authorize skipping older actionable\nmail. A Delivery therefore always carries the whole FIFO batch whatever its\ntypes, and a `check` without `--wait` hands that batch over unfiltered.\n`--peek` and `--all` are read-only inspection, not progress through the\ncoordinator inbox.\n\nAn empty wait or timeout is a checkpoint. Continue rolling waits until every\nexpected Dispatch settles. Heartbeat or visible activity means alive, not done.\n\n## Addresses\n\nUse a stable Dispatch address for attempt-specific coordinator guidance:\n\n```text\nORCA orchestration send --to dispatch: --subject \"Follow-up\" --body \"\" --json\n```\n\nDo not substitute a remote terminal handle. Omit `--from` for ordinary\ncoordinator calls; a dispatched worker instead copies the exact `--from` and\ncapability arguments in its preamble. `check` is the exception: it identifies\nits caller with `--terminal`, never `--from`.\n\nGroup addresses include `@all`, `@idle`, `@claude`, `@codex`, `@opencode`,\n`@gemini`, `@droid`, `@grok`, `@cursor`, and `@worktree:`. Use them only for\nintentional fan-out status or questions. `worker_done`, heartbeat, and other\nDispatch lifecycle messages never target groups.\n\n## Questions and gates\n\nA worker uses `ask`; its timeout leaves one durable question pending, which the\nworker resumes by message ID. The coordinator answers that message with `reply`.\n\nUse a gate only for a coordinator-owned Task-DAG decision:\n\n```text\nORCA orchestration gate-create --task --question \"\" --options --json\nORCA orchestration gate-resolve --id --resolution \"\" --json\nORCA orchestration gate-list --task --json\n```\n\nPass `json_array` using the quoting rules of the active shell; do not copy POSIX\nsingle-quote syntax into PowerShell or `cmd.exe`.\n\nDo not create a gate merely to answer a worker's `ask`.\n\n\n\n# Placement and remote execution\n\nLoad this reference before creating a new worktree or placing work through SSH,\nWSL, or another connected Orca server.\n\n## Placement choices\n\nA fresh worker means a fresh agent terminal, not a new Git worktree. Use the\ncurrent or an exact existing workspace by default. Create a worktree only when\nthe user requested one or a concrete checkout or filesystem conflict makes\nsharing unsafe.\n\n```text\n# Current workspace; setup is not rerun.\nORCA orchestration worker-start --task --worktree current --agent codex --json\n\n# Stacked child worktree.\nORCA orchestration worker-start --task --worktree new-child --name --agent codex --setup run --json\n\n# Independent top-level worktree.\nORCA orchestration worker-start --task --worktree new-top-level --name --agent codex --setup run --json\n```\n\nCurrent and exact existing workspaces create a fresh terminal unless\n`--terminal` is explicit. Folder workspaces are first-class; do not invoke Git\nor require worktree lineage when the selected workspace is a folder.\n\nRegister a folder workspace through project setup. `repo add --path `\nrequires a valid Git repository and rejects a plain directory:\n\n```text\nORCA project setup-existing-folder --project --host --path --kind folder --json\n```\n\nThen place work on the returned workspace with an exact selector. A worktree\nselector needs the full `::` value Orca returned, passed as\n`id:`; a bare repo id is not a worktree id. `new-child` and\n`new-top-level` are worktree creation and do not apply to a folder.\n\nNew worktrees use agent-first creation and run setup by default. Preserve the\nrepository's startup policy: `start-immediately` can report setup as `running`,\nwhile `wait-for-setup` gates prompt delivery on success. Orca lineage, Git base,\nfilesystem isolation, coordination parentage, UI grouping, and execution host\nare separate decisions.\n\n## Connected servers\n\nThe Run and Tasks remain authoritative on the current server. `--on` selects\nonly the worker's execution server and appears only on `worker-start`:\n\n```text\nORCA orchestration worker-start --task --on --worktree new-top-level --repo --name --agent codex --setup run --json\n```\n\nRemote `current` and `new-child` are invalid because they are ambiguous across\nservers. Use an exact discovered remote workspace, or `new-top-level` with an\nexact remote repository selector. After start, route every follow-up, read,\nstop, and cleanup by Dispatch ID; never repeat `--on` or substitute a remote\nterminal handle.\n\n```text\nORCA orchestration worker-show --dispatch --json\nORCA orchestration worker-read --dispatch --limit 50 --json\nORCA orchestration send --to dispatch: --subject \"Follow-up\" --body \"\" --json\nORCA orchestration worker-list --run --include-remote --json\n```\n\n`worker-list` reads local fleet state only; enumerate remote workers with\n`--include-remote` or every one of them reads `unverifiable`. Scope every list\nwith `--run `: unscoped, it reports every Dispatch this runtime has\nrecorded, and the workers you are waiting on are lost in that history.\n\n## Execution-host and mixed-version floor\n\nThe execution host owns process, filesystem, transcript, stop, and cleanup\nfacts. Render only `live`, `unverifiable`, or `exited`. Connection loss, relay\nabsence, missing client inventory, or timeout yields `unverifiable`, never\nsynthetic exit and never a client-local substitute action.\n\nClients and servers update independently. Optional response fields may be\nabsent. Forward model/effort, transcript reads, cleanup, or another new remote\noperation only when the peer advertises the relevant capability; unknown stream\nopcodes can be silently dropped. A narrow unsupported response may degrade to a\ndocumented older path, but must not broaden the target or cross the execution\nboundary. Changing host-published content reaches old clients even without a\nwire-shape change, so preserve established semantics or negotiate the behavior.\n\nFor WSL, use the exact executable and arguments returned by Orca so the distro\nand packaged launcher remain bound. Do not translate a printed `orca-ide`\nrecovery command into a PATH-resolved local command.\n\n\n\n# Recovery and cleanup\n\nLoad this reference only after a failed/stopped/unknown attempt, explicit retry\ndecision, stop/abandon request, retention request, or uncertain release.\n\n| Proven state | Safe action |\n| ----------------------- | ------------------------------------------------------------------ |\n| `ready` or active | Keep waiting; optionally read bounded output |\n| `failed` or `stopped` | Start a replacement with `--retry-of`; repeat placement explicitly |\n| `outcome_unknown` | Inspect, then choose `worker-stop` or explicit `worker-abandon` |\n| Accepted `worker_done` | Reuse, retain, or release |\n| Remote contact lost | Preserve `unverifiable`; do not stop or retry from absence alone |\n| `unverifiable` liveness | Keep waiting or inspect; never stop, abandon, retry, or release |\n| Proven `exited` agent | Enumerate with `worker-list`; follow its `nextAction` |\n\n## Inspect before acting\n\n```text\nORCA orchestration worker-list --run --json\nORCA orchestration worker-list --run --include-remote --json\nORCA orchestration worker-show --dispatch --json\nORCA orchestration worker-read --dispatch --limit 50 --json\n```\n\n`worker-list` is the enumerating command and the authority on agent liveness:\neach row carries `projection.liveness`, `projection.attention.categories`,\n`projection.attention.requiresAction`, and a literal `projection.nextAction`\nargv to run. Always scope it with `--run `; an unscoped list reports\nevery Dispatch this runtime has ever recorded and buries the live ones.\n`worker-show`'s `observation.status` is PTY liveness only, so a `live` terminal\nwhose agent died at a trust prompt still reads `live` there.\n\nWhen the two disagree, the fleet verdict decides — unless the fleet row is\n`unverifiable` for a reason that names a gap on this client rather than a fact\nabout the worker. `missing_status`, `host_unavailable`, and\n`capability_unsupported` are such gaps: the first means this runtime holds no\nstatus row, the second that it could not ask the execution host at all, and the\nthird that a stale peer answered but lacks the fleet-snapshot capability.\nAgainst any of them, a `worker-show` verdict sourced from the execution host is\nthe better evidence and outranks the row. Only `host_unavailable` is contact\nloss; the other two mean the host was never asked or answered without the\ncapability.\n\nThis never promotes absence. `unverifiable` from either command still authorizes\nnothing — only a positive `live` or `exited` verdict does.\n\nA worker started with `--on ` reads `unverifiable` until you\nenumerate with `--include-remote`, which asks its execution host for the\nverdict. Past 100 rows the response pages, so follow `page.nextCursor` with\n`--cursor ` until `page.hasMore` is false.\n\n## Stall needs positive evidence\n\nLeave the wait only on positive proof the agent stopped: `exited` liveness, the\nworker's own observation of process exit, or a transcript whose final agent turn\nsent no `worker_done`. Only then choose `worker-stop` or `worker-abandon`.\n\n`unverifiable` is always absence — `missing_status`, `stale_status`,\n`restored_unconfirmed`, or a remote worker with no connection — and a null\n`agentWait` or an unchanged `worker-read` tail is that same absence seen again.\nAbsence never authorizes stop, abandon, retry, or release: keep waiting, or\ninspect until you hold one of the positive signals above. A `nextAction` that\nnames an inspecting command is asking for evidence, not for cleanup.\n\n`worker-read --source auto` uses a proven provider transcript when available and\notherwise returns bounded terminal output with a typed `fallbackReason`.\nContinue with its top-level cursor, which is pinned to that source. If Orca\nreports `source_changed`, restart without the old cursor. A bounded initial\ntranscript tail can return an EOF cursor that follows only newly appended records;\nread `contentComplete`, `clipping`, and `warnings` before assuming omitted older\nrecords are pageable. Never guess a provider session ID, transcript path, or\nremote terminal handle.\n\n## Was the mutation applied?\n\nWhen a mutation's response was lost and named no Dispatch, do not replay blind.\nEvery orchestration mutation accepts `--retry-request `, which reuses one\noperation identity so Orca can replay, join, or recover it instead of starting a\nduplicate. Ask what happened first:\n\n```text\nORCA orchestration request-show --request --json\n```\n\n`completed` means the mutation already took effect; read its recorded receipt\ninstead of rerunning. `pending` means the original mutation is still running or\nOrca restarted before recording its outcome; replay the original command with\n`--retry-request `. `absent` means this runtime holds no receipt\nunder your caller identity — that is not proof nothing happened, so inspect the\naffected Task, Dispatch, and terminal before deciding whether to retry.\n\nWhen a worker's terminal accepted input but the submit is unconfirmed, use\n`terminal send --wait-submit `: it observes the accepted prompt for that\nlong and, on timeout, returns the input-accepted receipt without resending.\n\n## Refused starts\n\n`dispatch` and `worker-start` refuse the following preflight cases with a stable\n`error.code`; read it before choosing a recovery, and treat `error.data.nextSteps`\nas the exact recovery text. Older hosts may omit `data`, so treat every field as\noptional.\n\n| Code | Meaning | Recovery |\n| -------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |\n| `task_not_found` | No Task with that id, or not in the bound Run (`data.taskId`, `data.runId`) | Check `task-list --json`; create the Task with `task-create` if it does not exist |\n| `task_not_startable` | Task cannot start now: not `ready`, or invalid `--retry-of` (`data.status`, `data.unmetDependencies`, `data.retryOf`) | Wait for running dependencies with `check --wait`; retry or unblock failed ones; inspect `dispatch-show` if already dispatched |\n| `inject_rejected` | `--inject` refused because no recognized agent runs in the target (`data.terminal`, `data.reason`) | Start a recognized agent there or pick another terminal; or dispatch without `--inject` and use `terminal send` |\n| `runtime_error` | Any other failure, including a target terminal that already owns an active Dispatch | Read the message, inspect state, and do not retry unchanged |\n\n## Retry, stop, and abandon\n\nRetry only a positively proven failed or stopped attempt. Name the failed Task\nwith `--task`, since `--spec` creates a new one. Placement is never silently\ninherited:\n\n```text\nORCA orchestration worker-start --task --retry-of --worktree --agent --json\n```\n\nAfter three consecutive failures for one Task, its dispatch context\ncircuit-breaks and the Task is failed. Do not route around that boundary with a\nnew Run or an unrelated Dispatch.\n\nFor `outcome_unknown`, inspect first, then make an explicit choice:\n\n```text\nORCA orchestration worker-stop --dispatch --json\nORCA orchestration worker-abandon --dispatch --json\n```\n\n`worker-stop` closes only the exact proven supervised agent terminal. It never\ndeletes the worktree, setup terminal, configured tabs, or unrelated processes.\n`worker-abandon` fences orchestration while accepting that resources may remain\nlive; it performs no remote, process, or filesystem action.\n\n## Retain and release\n\n```text\nORCA orchestration worker-retain --dispatch --json\nORCA orchestration worker-release --dispatch --json\n```\n\nRetain only when the user explicitly wants the settled terminal kept live.\nRelease works after succeeded and failed reports, archives readable output, and\ncloses only the exact terminal owned by that settled Dispatch. Replays may call\nrelease again safely. Reused, pre-existing, setup, coordinator, active,\nuser-taken-over, and unproven terminals are retained.\n\nA `worker-start` that failed before its agent was ready still owns the terminal\nit created. Its receipt names `worker-release`, and `worker-list` reports that\nrow as `reclaimable`; release it there rather than closing the terminal by hand.\n\nNever release because of timeout, TUI idle, heartbeat, status, question,\nescalation, or stale/rejected completion. If the receipt says `release_pending`\nor `release_unknown`, follow its exact recovery action. Never substitute\n`terminal close`.\n\n`orchestration reset` is destructive recovery. Do not run it during active\ncoordination unless the user explicitly abandons that state.\n\n\n\n# Worker contract\n\nThe injected preamble is authoritative. Copy its command rather than\nreconstructing flags. In particular, preserve the exact executable, worker\nhandle, Dispatch capability, Task ID, and Dispatch ID.\n\n## Heartbeat\n\nSend heartbeats only at the cadence required by the live preamble. Skip them\nwhile blocked inside `ask` or `check --wait`; those calls are liveness signals.\n\n```text\nORCA orchestration send --from --dispatch-capability --type heartbeat --subject \"alive\" --task-id --dispatch-id --phase \"\"\n```\n\nUse typed lifecycle flags, not a hand-written JSON payload. A heartbeat proves\nliveness, never completion.\n\n## Ask and resume\n\nUse Orca `ask` whenever the coordinator must answer. Never open a local question\nTUI the coordinator cannot answer.\n\n```text\nORCA orchestration ask --from --dispatch-capability --question \"\" --options \",\" --timeout-ms 600000\n\nORCA orchestration ask --from --dispatch-capability --resume --timeout-ms 600000\n```\n\nA timeout or disconnect leaves the original question pending. Resume its\nmessage ID; do not create a duplicate question.\n\n## Reading coordinator follow-ups\n\nThe coordinator steers a running worker with `send --to dispatch:`. That\nenqueue is durable but does not interrupt you, so nothing arrives unless you\nlook:\n\n```text\nORCA orchestration check --terminal --json\n```\n\nRun it at each natural checkpoint — before starting a new file, after a test\nrun — and once more immediately before `worker_done`, so a redirect or a\ncancellation lands before the Task settles. `check` names its caller with\n`--terminal`, never `--from`. Stop checking after `worker_done`.\n\nIf `check` returns `consumer_fenced`, this process no longer owns its Dispatch:\nthe Attempt was re-attached to another worker or settled without you. Stop, do\nnot send `worker_done`, and do not retry the check. An empty `check` never means\nyou were replaced; `consumer_fenced` is the only way you learn that.\n\n## Escalation\n\nEscalate only before completion and only when the coordinator must intervene:\n\n```text\nORCA orchestration send --from --dispatch-capability --type escalation --subject \"Blocked: \" --body \"
\" --task-id --dispatch-id \n```\n\n## Completion\n\nSend exactly one terminal report. `--body` is three sentences: what changed,\nwhat was found, and what remains. Use `--outcome failed` when the requested work\nis not complete; never hide failure in prose or silently exit.\n\nAppend `--files-modified` or `--report-path` only when applicable, using actual\npaths. Do not send documentation placeholders as metadata.\n\n```text\nORCA orchestration send --from --dispatch-capability --type worker_done --subject \"\" --body \"\" --task-id --dispatch-id --outcome succeeded\n```\n\nAfter `worker_done`, end the dispatched turn and idle. Do not poll, close your\nown terminal, or begin unrelated work. A later direct user instruction is new\nuser-owned work and must not reuse settled lifecycle IDs; a supervised follow-up\narrives with a fresh preamble and Task block.\n" +const ORCHESTRATION_FULL_MARKDOWN = "---\nname: orchestration\ndescription: >-\n Coordinate supervised Orca workers: threaded messages, blocking ask/reply,\n task dispatch, worker_done/escalation waits, task DAGs, decision gates,\n coordinator loops, and decomposing work across agents. Use `orca-cli` for full\n ownership handoffs — \"hand off\", \"handoff\", \"handover\", \"give this to another\n agent\", \"another worktree\" — unless asked to supervise, monitor, or coordinate\n a DAG, and for terminal control, lightweight terminal prompts, shell commands,\n Orca worktree management, and reading or waiting on terminals. Use Computer\n Use for external browser windows, webviews, Orca app UI, or desktop UI outside\n Orca's embedded browser only when the task requires OS/window-level control\n such as focus, menus, dialogs, coordinates, or screenshots. Use `orca-cli` for\n Orca's embedded pages and a page-automation tool such as Playwright or CDP for\n external pages.\n---\n\n# Orca orchestration\n\nOrchestration is Orca's structured coordination layer. It records who owns work,\nwhich attempt is authoritative, and when supervised work has settled.\n\n## Outcome\n\n**Result:** every in-scope Task has one explicit outcome and every settled worker\nterminal has a next owner or cleanup decision. **Next consumer:** the user who\nrequested supervision. **Done:** all expected Dispatches have settled, every\ndelivered message was processed before acknowledgment, each settled worker was\nreused, explicitly retained, or released, and the turn ends only when the report\nto that user names, per Task, its outcome, the evidence behind it, and any\nunresolved blocker.\n\n**Safe failure:** preserve work and authority and report the state as unknown or\n`unverifiable`. Only positive proof of exit authorizes stop, abandon, or retry,\nand only an accepted settlement authorizes release. Every other observation,\nabsence included, is a checkpoint.\n\n## Classify the role\n\n| Current context | Role | Route |\n| ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------ |\n| The user explicitly asks to supervise, monitor, wait for results, track completion, coordinate a DAG, use a decision gate, or manage ask/reply | Coordinator | Use the supervised loop below |\n| The current prompt contains a live injected preamble with Task and Dispatch IDs | Dispatched worker | Follow the preamble and the worker obligations below |\n| The user asks to hand off ownership or start another agent/worktree without supervision | Handoff owner | Use `orca-cli`; create no Run, Task, or Dispatch and do not monitor completion |\n| A message carries a legacy authority label | Compatibility operator | Load the legacy contract reference before any lifecycle mutation |\n| No live preamble and no explicit supervision | Ordinary terminal agent | Do not emit lifecycle messages; use `orca-cli` for terminal/worktree work |\n\nModel or effort selection does not make a handoff supervised. Never substitute a\nnon-Orca subagent tool when Orca orchestration provenance was requested.\n\n## Authority and safety floor\n\n- A Run is a durable namespace and coordinator inbox; it does not schedule or\n place workers. A Task is work. A Dispatch is one authoritative Task attempt.\n- Lifecycle authority comes from the active Dispatch, not a terminal title,\n copied ID, old database row, provider transcript, or visible pane.\n- Workers use the exact executable, handle, capability, Task ID, and Dispatch ID\n in the live preamble. Never reconstruct, translate, or broaden those arguments.\n- After remote start, address the worker by Dispatch ID. The execution host owns\n process, filesystem, transcript, stop, and cleanup facts. Preserve the verdicts\n `live` / `unverifiable` / `exited`; contact loss is not process death.\n- Liveness is layered: `worker-list`'s `projection.liveness` is the fleet verdict\n for the agent; `worker-show`'s `observation.status` is PTY liveness only. A live\n terminal can still hold a dead or stuck agent.\n- Folder workspaces are valid; never require Git or assume a worktree.\n- Clients and remote servers update independently. Treat unknown optional fields\n as absent. A new stream operation requires advertised capability because old\n decoders may silently drop unknown opcodes. Never fall back to local execution\n when remote authority or capability is unproven.\n- Use the executable you used to run `skills get` for the entire run. In the\n examples below, replace `ORCA` with it; do not create a shell variable or run\n `ORCA` literally. If it fails, report that exact error instead of switching.\n- A successful `orchestration send` proves durable enqueue; its wake or nudge is\n best-effort attention only and does not prove the recipient read or accepted it.\n\n## Worker obligations\n\nThe injected preamble is authoritative. A dispatched worker must:\n\n1. Do only the current Task and use the preamble's `ask` command for a blocking\n coordinator question. Never open a local question TUI the coordinator cannot\n answer. Resume the same message ID after an ask timeout.\n2. Send heartbeats only at the cadence in the preamble. A heartbeat proves\n liveness, not completion.\n3. Read coordinator follow-ups at each natural checkpoint — before starting a\n new file, after a test run — and once more immediately before `worker_done`:\n `ORCA orchestration check --terminal --json`.\n4. Send `worker_done` exactly once, from the dispatched terminal, with a\n three-sentence executive summary, both lifecycle IDs, and explicit\n `--outcome succeeded` or `--outcome failed`. Never encode failure only in prose.\n5. Append `--files-modified` and `--report-path` only with real values when\n applicable. After `worker_done`, end the dispatched turn and idle; do not poll\n or start new work.\n\nA direct user instruction after completion starts new user-owned work and takes\nprecedence over the idle rule. Do not reuse the settled lifecycle IDs.\n\n## Canonical supervised loop\n\nConfirm the runtime, bind one Run, and start the full independent wave before\nwaiting. `worker-start --spec` creates the Task and its attempt in one call:\n\n```text\nORCA status --json\nORCA orchestration run-create --objective \"\" --json\nORCA orchestration worker-start --spec \"\" --worktree current --agent codex --json\nORCA orchestration worker-start --spec \"\" --worktree current --agent claude --json\nORCA orchestration check --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nIf `worker-start` exits non-zero, do not relaunch. Read the receipt's\n`failedStage` and `residualResources`, then load\n`references/recovery-and-cleanup.md`.\n\nUse `task-create` plus `worker-start --task ` for planned fan-out with\ndependencies or a retry of a known Task. Use dependencies only for real ordering\nand prefer parallel waves over chains deeper than three or four steps; nested\nworkers obey the depth limit, and a new Run does not reset the caller's depth.\n\nA consuming `check` names its caller with `--terminal `, never `--from`;\nomit it inside the coordinator's own Orca terminal. It returns the bound Run's\noldest FIFO Delivery and replays that batch until acknowledged. Process every\nmessage: reply to questions, validate each `worker_done` against the expected\nactive Dispatch, and decide each settled terminal's next owner before the ack:\n\n```text\nORCA orchestration reply --id --body \"\" --json\nORCA orchestration worker-release --dispatch --json\nORCA orchestration check --ack --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nKeep waiting until every expected Dispatch settles. A timeout or empty result is\na checkpoint, not a failure. Do not stop, retry, release, or launch a duplicate\neditor without the positive proof `## Outcome` requires.\n\nAfter three consecutive empty waits, stop waiting blindly and enumerate with\n`ORCA orchestration worker-list --include-remote --json` (defaults to the bound\nRun; `--run ` overrides; the receipt's `scope` names which), acting on\neach row's `projection.attention` categories, `projection.attention.requiresAction`, and literal `projection.nextAction` argv.\nA `none` `nextAction` has no argv to run: read `liveness.reason` and keep waiting\nwith `check --wait`. Absence never earns an argv; settlement and pending work still do.\nLeave the wait only on positive proof the agent stopped: `exited` liveness, the\nworker's own observation of process exit, or a transcript whose final agent turn\nsent no `worker_done`. Then load `references/recovery-and-cleanup.md` and choose\n`worker-stop` or `worker-abandon` explicitly. `unverifiable` is absence,\nincluding when `worker-show` reports `agentWait` null. Absence never authorizes\nstop, abandon, retry, or release; keep waiting or inspect.\n\n`worker-start` is the normal path, composing placement, terminal readiness,\nprompt injection, and supervised resource ownership. `dispatch --inject` leaves\nan operator-created process unsupervised and is only for an expressiveness gap.\n\n## Task-spec contract\n\nEvery Task spec must be self-contained and name:\n\n- **Target:** the files, component, or environment in scope.\n- **Change:** the concrete result to produce.\n- **Constraints:** invariants, compatibility rules, and do-not-touch boundaries.\n- **Ownership:** what this worker may edit and any coordination boundary.\n- **Observable acceptance:** the test, output, or evidence that proves completion.\n\n## Completion accounting\n\nAfter an accepted success or failure report, immediately do exactly one:\n\n1. Reuse the same proven agent terminal for an immediate follow-up Dispatch.\n2. Record user-requested retention with `worker-retain`.\n3. Run `worker-release`.\n\nRelease is post-settlement cleanup, not cancellation. Only an accepted\nsettlement authorizes it; no other observation does. If release is uncertain,\nfollow its exact recovery receipt and never substitute `terminal close`.\n\nA valid `worker_done` settles the Task and Dispatch automatically; do not follow\nit with `task-update --status completed`. Enumerate the terminals still owing a\ndecision with `worker-list --run --terminal-state reclaimable --json`,\nand do not end the coordinator turn until it returns none.\n\n## Conditional references\n\nThis compact guide is sufficient for the normal local loop. At an action gate\nbelow, run `ORCA skills get orchestration --reference references/.md` and\nread only that document; `--references` lists the names. If the CLI rejects\n`--reference`, run `ORCA skills get orchestration --full` once instead: it\nreturns this exact kernel and every reference, so read only the named one. If an\nolder CLI rejects `--full`, keep this kernel's safety floor, use that command's\n`--help`, and never guess newer flags.\n\n| Action gate | Bundled reference |\n| ------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |\n| Expanded DAG waves, launch model/effort, same-terminal reuse, or review ownership | `references/coordinator-loop.md` |\n| You are a dispatched worker and the live preamble does not answer your question, or `check` returned an error | `references/worker-contract.md` |\n| New worktree, exact workspace, SSH, WSL, or connected-server placement | `references/placement-and-remote.md` |\n| Inbox replay, follow-up messages, group addresses, or decision gates | `references/messaging-and-gates.md` |\n| Failed/stopped/unknown attempts, retry, stop, abandon, retain, or uncertain release | `references/recovery-and-cleanup.md` |\n| Custom argv or terminal topology that `worker-start` cannot express | `references/low-level-topology.md` |\n| Any legacy label, adopted Run, compatibility receipt, or takeover | `references/legacy-contract-migration.md` |\n\nRetired scheduler commands are not aliases for Run creation. Recovery commands\nmust provide their exact next action; follow it with the same selected executable.\n\n---\n\n# Bundled references\n\nThese references belong to the version-matched guide above. Read only the documents named by its action gates.\n\n\n\n# Coordinator loop\n\nLoad this reference for expanded DAG waves, per-invocation launch preferences,\nsame-terminal reuse, or review ownership. The compact guide remains the source\nof truth for the loop order and completion boundary.\n\n## Ready waves\n\nCreate independent Tasks before the first wait. Encode only real dependencies,\nthen use the ready view as external memory:\n\n```text\nORCA orchestration task-create --spec \"\" --deps --json\nORCA orchestration task-list --ready --brief --json\n```\n\n`--brief` collapses whitespace and caps echoed specs at 160 characters;\n`spec_truncated` identifies shortened rows. Omit it when full specs are needed or\nwhen an older CLI rejects the flag. A nested worker must respect\n`nested_worker_depth_exceeded`; creating another Run does not reset depth.\n\n## Launch preferences\n\nFor a fresh Claude, Codex, or Cursor terminal, `--model` accepts an opaque\nprovider model ID. Pass it only when the user named a model; otherwise omit it\nso the worker inherits the user's configured agent default. Add `--effort` only\nwhen that model supports it:\n\n```text\nORCA orchestration worker-start --task --worktree current --agent claude --model opus --effort high --json\n```\n\n`--effort` requires `--model`; neither option combines with `--terminal`. A\nconnected worker server must advertise launch-preference support before Orca\nforwards either field. Compare `launch.requested` with `launch.effective`; never\nclaim a model or effort from requested arguments alone.\n\n## Reuse after settlement\n\nChoose the terminal's next owner before acknowledging the Delivery. When the\nsame exact agent has immediate follow-up work, recover the proven handle and\ntransfer cleanup ownership to the new Dispatch:\n\n```text\nORCA orchestration worker-show --dispatch --json\nORCA orchestration worker-start --task --terminal --json\n```\n\nOtherwise explicitly retain or release the settled worker. Do not leave it live\nonly to inspect output; archived output remains available through `worker-read`.\n\n## Review ownership\n\nA review-only `worker_done` authorizes synthesis of findings, not coordinator\nfile edits. Dispatch or hand off fixes unless the user explicitly assigned them\nto the coordinator. If the user's plan names a next owner, post-review fixes and\nPR preparation remain with that owner; the coordinator routes and synthesizes.\n\n\n\n# Legacy contract migration\n\nLoad this reference only for an authority label, adopted Run, compatibility or\nrecovery receipt, or explicit legacy takeover. A newly created attempt always\nuses the current grammar.\n\n## Authority labels\n\n- `[LEGACY COMPATIBILITY]` is live and attested. Run only the exact supported\n command printed with the message, using the same selected executable and\n arguments supplied by the original prompt.\n- `[LEGACY RECOVERY REPLAY — MAY HAVE BEEN SEEN]` is one bounded,\n at-least-once cutover replay. Process it idempotently and acknowledge only\n through the exact displayed guidance.\n- `[LEGACY READ-ONLY]` is inspection-only. It has no reply, acknowledgment, or\n lifecycle mutation.\n- An unlabeled current message uses the current guide and grammar.\n\nAn explicitly selected current Run, attested current binding, current Dispatch,\nor federated attachment takes precedence over legacy fallback. A retained\nadoption record alone does not grant mutation authority. If liveness, principal\nownership, capability, or the exact legacy contract is unproven, degrade to\nread-only inspection and never fall back to local execution.\n\nAdoption preserves the live agent process, PTY/session, terminal handle,\ntab/pane, worktree or folder workspace, Task, and Dispatch. It never restarts or\nreplaces the worker and never revives the retired scheduler. Loss of lifecycle\nauthority does not invalidate the existing process, assignment, or filesystem\nwork. Exact recovery may restore the same PTY once in its original inactive\nbackground tab; it must not spawn, write, signal, stop, switch, focus, split, or\ninject a terminal.\n\n## Compatibility recovery\n\nWhen a compatibility response returns structured next-step arguments, execute\nthose exact arguments with the same selected CLI executable. Do not translate\nfrom memory, broaden the recipient, or retry as a current mutation unless the\nreceipt explicitly authorizes it.\n\nA pending ask, reply, final Dispatch settlement, and consuming check have\ndurable recovery identities. Heartbeat and escalation remain at-least-once\nacross a manual contract-boundary retry. If an ask may already have been\nanswered, run the exact non-consuming recovery check printed by Orca before\ncreating any new question. Never guess among identical question threads.\n\nOn packaged Windows, a legacy ask uses a two-step commit/resume protocol. The\ninitial command commits the question, prints its exact\n`ask --resume ` command, and exits with launcher status `75`. Run\nthat exact resume after the launcher or update boundary. For an attested WSL\nlaunch, preserve the printed `orca-ide` executable and distro route. Older WSL\nworkers without launch proof remain lifecycle read-only even while their\nterminal and filesystem work continue.\n\n## Read-only inspection and takeover\n\nRead-only inspection does not consume mail:\n\n```text\nORCA orchestration run-list --json\nORCA orchestration run-show --id run_legacy_local --json\nORCA orchestration run-show --id --json\nORCA orchestration task-list --run --json\nORCA orchestration inbox --full --json\nORCA orchestration check --terminal --peek --format --json\nORCA terminal read --terminal --json\nORCA terminal wait --terminal --for tui-idle --timeout-ms 60000 --json\n```\n\n`run_legacy_local` is an empty audit tombstone after adoption. Find the ordinary\nRun whose objective is `Recovered orchestration work from a contract update`.\n\nOnly when the original coordinator is unavailable or cannot prove retained\nauthority may a new live coordinator take over from its own terminal:\n\n```text\nORCA orchestration run-use --id --takeover-legacy --json\nORCA orchestration check --run --json\n```\n\nTakeover binds the authenticated invoking terminal; `--from` cannot nominate\nanother coordinator. It fences only the old coordinator and moves pending mail\ninto current Run delivery. It preserves live workers, Tasks, Dispatches, processes, and files.\nNever take over while the original coordinator is actively coordinating.\n\nDo not launch a replacement editor merely because Orca updated or authority is\nunclear. Keep the original worker as the only editor until a stable handoff\npoint, then use a fresh current Dispatch in a conflict-free placement.\n\n\n\n# Low-level topology\n\nLoad this reference only when `worker-start` cannot express required custom argv\nor terminal topology. It is not the normal supervised loop and is never a full\nhandoff recipe.\n\n```text\nORCA terminal create --worktree active --title --command \"\" --json\nORCA terminal wait --terminal --for tui-idle --timeout-ms 60000 --json\nORCA orchestration dispatch --task --to --inject --json\n```\n\nWait for readiness only when startup could lose injected input. Prefer\nagent-first `worker-start` whenever its argv and topology are sufficient.\n\n`dispatch --inject` creates authoritative Task/Dispatch context but deliberately\nkeeps an operator-created process unsupervised: it creates no supervised worker\nresource row. `worker-show`, `worker-read`, and `worker-list` report the lane as\n`unsupervised`; `worker-stop` and `worker-abandon` do not close that process, and\nsettled retain/release take no process action.\n\nUse `worker-start --terminal ` when lifecycle ownership of an existing\nagent terminal is required. Never imply that low-level dispatch retroactively\nowns a process, never use it to route around the nested-depth limit, and never\nuse it for an ownership handoff.\n\n\n\n# Messaging and gates\n\nLoad this reference for inbox replay, attempt-specific guidance, group\naddresses, blocking questions, or coordinator-managed DAG decisions.\n\nA successful `send` proves durable enqueue. Wake and nudge are best-effort\nattention only: neither proves the recipient read the message, began a turn, or\naccepted steering.\n\n## Coordinator delivery loop\n\n`check` names its caller with `--terminal ` and is the only verb that\nrejects `--from`. Omit `--terminal` inside an Orca terminal, where Orca resolves\nthe caller; pass it explicitly from anywhere else, including a dispatched\nworker reading coordinator follow-ups.\n\nA consuming coordinator `check` returns the bound Run's oldest FIFO Delivery,\nup to 50 messages, and replays that exact batch until acknowledged. Process\nevery row and required terminal ownership decision before `--ack`. Type filters\ndecide when a waiter wakes; they do not authorize skipping older actionable\nmail. A Delivery therefore always carries the whole FIFO batch whatever its\ntypes, and a `check` without `--wait` hands that batch over unfiltered.\n`--peek` and `--all` are read-only inspection, not progress through the\ncoordinator inbox.\n\nAn empty wait or timeout is a checkpoint. Continue rolling waits until every\nexpected Dispatch settles. Heartbeat or visible activity means alive, not done.\n\n## Addresses\n\nUse a stable Dispatch address for attempt-specific coordinator guidance:\n\n```text\nORCA orchestration send --to dispatch: --subject \"Follow-up\" --body \"\" --json\n```\n\nDo not substitute a remote terminal handle. Omit `--from` for ordinary\ncoordinator calls; a dispatched worker instead copies the exact `--from` and\ncapability arguments in its preamble. `check` is the exception: it identifies\nits caller with `--terminal`, never `--from`.\n\nGroup addresses include `@all`, `@idle`, `@claude`, `@codex`, `@opencode`,\n`@gemini`, `@droid`, `@grok`, `@cursor`, and `@worktree:`. Use them only for\nintentional fan-out status or questions. `worker_done`, heartbeat, and other\nDispatch lifecycle messages never target groups.\n\n## Questions and gates\n\nA worker uses `ask`; its timeout leaves one durable question pending, which the\nworker resumes by message ID. The coordinator answers that message with `reply`.\n\nUse a gate only for a coordinator-owned Task-DAG decision:\n\n```text\nORCA orchestration gate-create --task --question \"\" --options --json\nORCA orchestration gate-resolve --id --resolution \"\" --json\nORCA orchestration gate-list --task --json\n```\n\nPass `json_array` using the quoting rules of the active shell; do not copy POSIX\nsingle-quote syntax into PowerShell or `cmd.exe`.\n\nDo not create a gate merely to answer a worker's `ask`.\n\n\n\n# Placement and remote execution\n\nLoad this reference before creating a new worktree or placing work through SSH,\nWSL, or another connected Orca server.\n\n## Placement choices\n\nA fresh worker means a fresh agent terminal, not a new Git worktree. Use the\ncurrent or an exact existing workspace by default. Create a worktree only when\nthe user requested one or a concrete checkout or filesystem conflict makes\nsharing unsafe.\n\n```text\n# Current workspace; setup is not rerun.\nORCA orchestration worker-start --task --worktree current --agent codex --json\n\n# Stacked child worktree.\nORCA orchestration worker-start --task --worktree new-child --name --agent codex --setup run --json\n\n# Independent top-level worktree.\nORCA orchestration worker-start --task --worktree new-top-level --name --agent codex --setup run --json\n```\n\nCurrent and exact existing workspaces create a fresh terminal unless\n`--terminal` is explicit. Folder workspaces are first-class; do not invoke Git\nor require worktree lineage when the selected workspace is a folder.\n\nRegister a folder workspace through project setup. `repo add --path `\nrequires a valid Git repository and rejects a plain directory:\n\n```text\nORCA project setup-existing-folder --project --host --path --kind folder --json\n```\n\nThen place work on the returned workspace with an exact selector. A worktree\nselector needs the full `::` value Orca returned, passed as\n`id:`; a bare repo id is not a worktree id. `new-child` and\n`new-top-level` are worktree creation and do not apply to a folder.\n\nNew worktrees use agent-first creation and run setup by default. Preserve the\nrepository's startup policy: `start-immediately` can report setup as `running`,\nwhile `wait-for-setup` gates prompt delivery on success. Orca lineage, Git base,\nfilesystem isolation, coordination parentage, UI grouping, and execution host\nare separate decisions.\n\n## Connected servers\n\nThe Run and Tasks remain authoritative on the current server. `--on` selects\nonly the worker's execution server and appears only on `worker-start`:\n\n```text\nORCA orchestration worker-start --task --on --worktree new-top-level --repo --name --agent codex --setup run --json\n```\n\nRemote `current` and `new-child` are invalid because they are ambiguous across\nservers. Use an exact discovered remote workspace, or `new-top-level` with an\nexact remote repository selector. After start, route every follow-up, read,\nstop, and cleanup by Dispatch ID; never repeat `--on` or substitute a remote\nterminal handle.\n\n```text\nORCA orchestration worker-show --dispatch --json\nORCA orchestration worker-read --dispatch --limit 50 --json\nORCA orchestration send --to dispatch: --subject \"Follow-up\" --body \"\" --json\nORCA orchestration worker-list --run --include-remote --json\n```\n\n`worker-list` reads local fleet state only; enumerate remote workers with\n`--include-remote` or every one of them reads `unverifiable`. Scope every list\nwith `--run `: unscoped, it reports every Dispatch this runtime has\nrecorded, and the workers you are waiting on are lost in that history.\n\n## Execution-host and mixed-version floor\n\nThe execution host owns process, filesystem, transcript, stop, and cleanup\nfacts. Render only `live`, `unverifiable`, or `exited`. Connection loss, relay\nabsence, missing client inventory, or timeout yields `unverifiable`, never\nsynthetic exit and never a client-local substitute action.\n\nClients and servers update independently. Optional response fields may be\nabsent. Forward model/effort, transcript reads, cleanup, or another new remote\noperation only when the peer advertises the relevant capability; unknown stream\nopcodes can be silently dropped. A narrow unsupported response may degrade to a\ndocumented older path, but must not broaden the target or cross the execution\nboundary. Changing host-published content reaches old clients even without a\nwire-shape change, so preserve established semantics or negotiate the behavior.\n\nFor WSL, use the exact executable and arguments returned by Orca so the distro\nand packaged launcher remain bound. Do not translate a printed `orca-ide`\nrecovery command into a PATH-resolved local command.\n\n\n\n# Recovery and cleanup\n\nLoad this reference only after a failed/stopped/unknown attempt, explicit retry\ndecision, stop/abandon request, retention request, or uncertain release.\n\n| Proven state | Safe action |\n| ----------------------- | ------------------------------------------------------------------ |\n| `ready` or active | Keep waiting; optionally read bounded output |\n| `failed` or `stopped` | Start a replacement with `--retry-of`; repeat placement explicitly |\n| `outcome_unknown` | Inspect, then choose `worker-stop` or explicit `worker-abandon` |\n| Accepted `worker_done` | Reuse, retain, or release |\n| Remote contact lost | Preserve `unverifiable`; do not stop or retry from absence alone |\n| `unverifiable` liveness | Keep waiting or inspect; never stop, abandon, retry, or release |\n| Proven `exited` agent | Enumerate with `worker-list`; follow its `nextAction` |\n\n## Inspect before acting\n\n```text\nORCA orchestration worker-list --run --json\nORCA orchestration worker-list --run --include-remote --json\nORCA orchestration worker-show --dispatch --json\nORCA orchestration worker-read --dispatch --limit 50 --json\n```\n\n`worker-list` is the enumerating command and the authority on agent liveness:\neach row carries `projection.liveness`, `projection.attention.categories`,\n`projection.attention.requiresAction`, and a literal `projection.nextAction`\nargv to run. Always scope it with `--run `; an unscoped list reports\nevery Dispatch this runtime has ever recorded and buries the live ones.\n`worker-show`'s `observation.status` is PTY liveness only, so a `live` terminal\nwhose agent died at a trust prompt still reads `live` there.\n\nWhen the two disagree, the fleet verdict decides — unless the fleet row is\n`unverifiable` for a reason that names a gap on this client rather than a fact\nabout the worker. `missing_status`, `host_unavailable`, and\n`capability_unsupported` are such gaps: the first means this runtime holds no\nstatus row, the second that it could not ask the execution host at all, and the\nthird that a stale peer answered but lacks the fleet-snapshot capability.\nAgainst any of them, a `worker-show` verdict sourced from the execution host is\nthe better evidence and outranks the row. Only `host_unavailable` is contact\nloss; the other two mean the host was never asked or answered without the\ncapability.\n\nThis never promotes absence. `unverifiable` from either command still authorizes\nnothing — only a positive `live` or `exited` verdict does.\n\nA worker started with `--on ` reads `unverifiable` until you\nenumerate with `--include-remote`, which asks its execution host for the\nverdict. Past 100 rows the response pages, so follow `page.nextCursor` with\n`--cursor ` until `page.hasMore` is false.\n\n## Stall needs positive evidence\n\nLeave the wait only on positive proof the agent stopped: `exited` liveness, the\nworker's own observation of process exit, or a transcript whose final agent turn\nsent no `worker_done`. Only then choose `worker-stop` or `worker-abandon`.\n\n`unverifiable` is always absence — `missing_status`, `stale_status`,\n`restored_unconfirmed`, or a remote worker with no connection — and a null\n`agentWait` or an unchanged `worker-read` tail is that same absence seen again.\nAbsence never authorizes stop, abandon, retry, or release: keep waiting, or\ninspect until you hold one of the positive signals above. A `nextAction` that\nnames an inspecting command is asking for evidence, not for cleanup.\n\n`worker-read --source auto` uses a proven provider transcript when available and\notherwise returns bounded terminal output with a typed `fallbackReason`.\nContinue with its top-level cursor, which is pinned to that source. If Orca\nreports `source_changed`, restart without the old cursor. A bounded initial\ntranscript tail can return an EOF cursor that follows only newly appended records;\nread `contentComplete`, `clipping`, and `warnings` before assuming omitted older\nrecords are pageable. Never guess a provider session ID, transcript path, or\nremote terminal handle.\n\n## Was the mutation applied?\n\nWhen a mutation's response was lost and named no Dispatch, do not replay blind.\nEvery orchestration mutation accepts `--retry-request `, which reuses one\noperation identity so Orca can replay, join, or recover it instead of starting a\nduplicate. Ask what happened first:\n\n```text\nORCA orchestration request-show --request --json\n```\n\n`completed` means the mutation already took effect; read its recorded receipt\ninstead of rerunning. `pending` means the original mutation is still running or\nOrca restarted before recording its outcome; replay the original command with\n`--retry-request `. `absent` means this runtime holds no receipt\nunder your caller identity — that is not proof nothing happened, so inspect the\naffected Task, Dispatch, and terminal before deciding whether to retry.\n\nWhen a worker's terminal accepted input but the submit is unconfirmed, use\n`terminal send --wait-submit `: it observes the accepted prompt for that\nlong and, on timeout, returns the input-accepted receipt without resending.\n\n## Refused starts\n\n`dispatch` and `worker-start` refuse the following preflight cases with a stable\n`error.code`; read it before choosing a recovery, and treat `error.data.nextSteps`\nas the exact recovery text. Older hosts may omit `data`, so treat every field as\noptional.\n\n| Code | Meaning | Recovery |\n| -------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |\n| `task_not_found` | No Task with that id, or not in the bound Run (`data.taskId`, `data.runId`) | Check `task-list --json`; create the Task with `task-create` if it does not exist |\n| `task_not_startable` | Task cannot start now: not `ready`, or invalid `--retry-of` (`data.status`, `data.unmetDependencies`, `data.retryOf`) | Wait for running dependencies with `check --wait`; retry or unblock failed ones; inspect `dispatch-show` if already dispatched |\n| `inject_rejected` | `--inject` refused because no recognized agent runs in the target (`data.terminal`, `data.reason`) | Start a recognized agent there or pick another terminal; or dispatch without `--inject` and use `terminal send` |\n| `runtime_error` | Any other failure, including a target terminal that already owns an active Dispatch | Read the message, inspect state, and do not retry unchanged |\n\n## Retry, stop, and abandon\n\nRetry only a positively proven failed or stopped attempt. Name the failed Task\nwith `--task`, since `--spec` creates a new one. Placement is never silently\ninherited:\n\n```text\nORCA orchestration worker-start --task --retry-of --worktree --agent --json\n```\n\nAfter three consecutive failures for one Task, its dispatch context\ncircuit-breaks and the Task is failed. Do not route around that boundary with a\nnew Run or an unrelated Dispatch.\n\nFor `outcome_unknown`, inspect first, then make an explicit choice:\n\n```text\nORCA orchestration worker-stop --dispatch --json\nORCA orchestration worker-abandon --dispatch --json\n```\n\n`worker-stop` closes only the exact proven supervised agent terminal. It never\ndeletes the worktree, setup terminal, configured tabs, or unrelated processes.\n`worker-abandon` fences orchestration while accepting that resources may remain\nlive; it performs no remote, process, or filesystem action.\n\n## Retain and release\n\n```text\nORCA orchestration worker-retain --dispatch --json\nORCA orchestration worker-release --dispatch --json\n```\n\nRetain only when the user explicitly wants the settled terminal kept live.\nRelease works after succeeded and failed reports, archives readable output, and\ncloses only the exact terminal owned by that settled Dispatch. Replays may call\nrelease again safely. Reused, pre-existing, setup, coordinator, active,\nuser-taken-over, and unproven terminals are retained.\n\nA `worker-start` that failed before its agent was ready still owns the terminal\nit created. Its receipt names `worker-release`, and `worker-list` reports that\nrow as `reclaimable`; release it there rather than closing the terminal by hand.\n\nNever release because of timeout, TUI idle, heartbeat, status, question,\nescalation, or stale/rejected completion. If the receipt says `release_pending`\nor `release_unknown`, follow its exact recovery action. Never substitute\n`terminal close`.\n\n`orchestration reset` is destructive recovery. Do not run it during active\ncoordination unless the user explicitly abandons that state.\n\n\n\n# Worker contract\n\nThe injected preamble is authoritative. Copy its command rather than\nreconstructing flags. In particular, preserve the exact executable, worker\nhandle, Dispatch capability, Task ID, and Dispatch ID.\n\n## Heartbeat\n\nSend heartbeats only at the cadence required by the live preamble. Skip them\nwhile blocked inside `ask` or `check --wait`; those calls are liveness signals.\n\n```text\nORCA orchestration send --from --dispatch-capability --type heartbeat --subject \"alive\" --task-id --dispatch-id --phase \"\"\n```\n\nUse typed lifecycle flags, not a hand-written JSON payload. A heartbeat proves\nliveness, never completion.\n\n## Ask and resume\n\nUse Orca `ask` whenever the coordinator must answer. Never open a local question\nTUI the coordinator cannot answer.\n\n```text\nORCA orchestration ask --from --dispatch-capability --question \"\" --options \",\" --timeout-ms 600000\n\nORCA orchestration ask --from --dispatch-capability --resume --timeout-ms 600000\n```\n\nA timeout or disconnect leaves the original question pending. Resume its\nmessage ID; do not create a duplicate question.\n\n## Reading coordinator follow-ups\n\nThe coordinator steers a running worker with `send --to dispatch:`. That\nenqueue is durable but does not interrupt you, so nothing arrives unless you\nlook:\n\n```text\nORCA orchestration check --terminal --json\n```\n\nRun it at each natural checkpoint — before starting a new file, after a test\nrun — and once more immediately before `worker_done`, so a redirect or a\ncancellation lands before the Task settles. `check` names its caller with\n`--terminal`, never `--from`. Stop checking after `worker_done`.\n\nIf `check` returns `consumer_fenced`, this process no longer owns its Dispatch:\nthe Attempt was re-attached to another worker or settled without you. Stop, do\nnot send `worker_done`, and do not retry the check. An empty `check` never means\nyou were replaced; `consumer_fenced` is the only way you learn that.\n\n## Escalation\n\nEscalate only before completion and only when the coordinator must intervene:\n\n```text\nORCA orchestration send --from --dispatch-capability --type escalation --subject \"Blocked: \" --body \"
\" --task-id --dispatch-id \n```\n\n## Completion\n\nSend exactly one terminal report. `--body` is three sentences: what changed,\nwhat was found, and what remains. Use `--outcome failed` when the requested work\nis not complete; never hide failure in prose or silently exit.\n\nAppend `--files-modified` or `--report-path` only when applicable, using actual\npaths. Do not send documentation placeholders as metadata.\n\n```text\nORCA orchestration send --from --dispatch-capability --type worker_done --subject \"\" --body \"\" --task-id --dispatch-id --outcome succeeded\n```\n\nAfter `worker_done`, end the dispatched turn and idle. Do not poll, close your\nown terminal, or begin unrelated work. A later direct user instruction is new\nuser-owned work and must not reuse settled lifecycle IDs; a supervised follow-up\narrives with a fresh preamble and Task block.\n" // oxfmt-ignore const ORCHESTRATION_COORDINATOR_LOOP_REFERENCE_MARKDOWN = "# Coordinator loop\n\nLoad this reference for expanded DAG waves, per-invocation launch preferences,\nsame-terminal reuse, or review ownership. The compact guide remains the source\nof truth for the loop order and completion boundary.\n\n## Ready waves\n\nCreate independent Tasks before the first wait. Encode only real dependencies,\nthen use the ready view as external memory:\n\n```text\nORCA orchestration task-create --spec \"\" --deps --json\nORCA orchestration task-list --ready --brief --json\n```\n\n`--brief` collapses whitespace and caps echoed specs at 160 characters;\n`spec_truncated` identifies shortened rows. Omit it when full specs are needed or\nwhen an older CLI rejects the flag. A nested worker must respect\n`nested_worker_depth_exceeded`; creating another Run does not reset depth.\n\n## Launch preferences\n\nFor a fresh Claude, Codex, or Cursor terminal, `--model` accepts an opaque\nprovider model ID. Pass it only when the user named a model; otherwise omit it\nso the worker inherits the user's configured agent default. Add `--effort` only\nwhen that model supports it:\n\n```text\nORCA orchestration worker-start --task --worktree current --agent claude --model opus --effort high --json\n```\n\n`--effort` requires `--model`; neither option combines with `--terminal`. A\nconnected worker server must advertise launch-preference support before Orca\nforwards either field. Compare `launch.requested` with `launch.effective`; never\nclaim a model or effort from requested arguments alone.\n\n## Reuse after settlement\n\nChoose the terminal's next owner before acknowledging the Delivery. When the\nsame exact agent has immediate follow-up work, recover the proven handle and\ntransfer cleanup ownership to the new Dispatch:\n\n```text\nORCA orchestration worker-show --dispatch --json\nORCA orchestration worker-start --task --terminal --json\n```\n\nOtherwise explicitly retain or release the settled worker. Do not leave it live\nonly to inspect output; archived output remains available through `worker-read`.\n\n## Review ownership\n\nA review-only `worker_done` authorizes synthesis of findings, not coordinator\nfile edits. Dispatch or hand off fixes unless the user explicitly assigned them\nto the coordinator. If the user's plan names a next owner, post-review fixes and\nPR preparation remain with that owner; the coordinator routes and synthesizes.\n" diff --git a/src/main/runtime/orca-runtime-subscribe-to-terminal-resize.ts b/src/main/runtime/orca-runtime-subscribe-to-terminal-resize.ts index d610dbb235f..484ea73064e 100644 --- a/src/main/runtime/orca-runtime-subscribe-to-terminal-resize.ts +++ b/src/main/runtime/orca-runtime-subscribe-to-terminal-resize.ts @@ -145,7 +145,7 @@ export class OrcaRuntimeWithSubscribeToTerminalResize extends OrcaRuntimeWithApp exitCause: cause, handle }), - ...(recipient.runId ? { runId: recipient.runId } : {}) + runId: dispatch.run_id }) this.notifyMessageArrived(escalation.to_handle, escalation.type) } catch (error) { diff --git a/src/main/runtime/orca-runtime-tests/lineage-and-scan-cache-part-05.spec.ts b/src/main/runtime/orca-runtime-tests/lineage-and-scan-cache-part-05.spec.ts index 1df978ac165..ad66e89ec7e 100644 --- a/src/main/runtime/orca-runtime-tests/lineage-and-scan-cache-part-05.spec.ts +++ b/src/main/runtime/orca-runtime-tests/lineage-and-scan-cache-part-05.spec.ts @@ -95,7 +95,10 @@ describe('OrcaRuntimeService', () => { return [name, createRootDispatch(db, task.id, handles[name], paneKey(name))] }) ) - const legacyTask = db.createTask({ spec: 'legacy worker' }) + const legacyTask = db.createTask({ + runId: 'run_legacy_local', + spec: 'legacy worker' + }) const legacyDispatch = createRootDispatch( db, legacyTask.id, diff --git a/src/main/runtime/orca-runtime-tests/mobile-creation-and-orchestration-part-02.spec.ts b/src/main/runtime/orca-runtime-tests/mobile-creation-and-orchestration-part-02.spec.ts index 57c1d2c3031..9291c30c1a2 100644 --- a/src/main/runtime/orca-runtime-tests/mobile-creation-and-orchestration-part-02.spec.ts +++ b/src/main/runtime/orca-runtime-tests/mobile-creation-and-orchestration-part-02.spec.ts @@ -216,7 +216,10 @@ describe('OrcaRuntimeService', () => { runtime as unknown as { leaves: Map< string, - { lastAgentStatus: string | null; lastAgentStatusObservedLive: boolean } + { + lastAgentStatus: string | null + lastAgentStatusObservedLive: boolean + } > } ).leaves.values() @@ -415,7 +418,12 @@ describe('OrcaRuntimeService', () => { const [terminal] = (await runtime.listTerminals()).terminals runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07', 100) - db.insertMessage({ from: 'term_worker', to: terminal.handle, subject: 'pending' }) + db.insertMessage({ + runId: 'run_legacy_local', + from: 'term_worker', + to: terminal.handle, + subject: 'pending' + }) runtime.notifyMessageArrived(terminal.handle, 'status') db.close() diff --git a/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-04.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-04.spec.ts index d09b4c64ce8..48f820cee01 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-04.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-04.spec.ts @@ -56,7 +56,10 @@ describe('OrcaRuntimeService', () => { ) const db = new OrchestrationDb(':memory:') try { - const task = db.createTask({ spec: 'continue after missing worker recovery' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'continue after missing worker recovery' + }) const started = db.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -163,7 +166,10 @@ describe('OrcaRuntimeService', () => { ) const db = new OrchestrationDb(':memory:') try { - const task = db.createTask({ spec: 'retry missing worker recovery' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'retry missing worker recovery' + }) const started = db.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, diff --git a/src/main/runtime/orchestration-messages-fake-parity.test.ts b/src/main/runtime/orchestration-messages-fake-parity.test.ts index 72ed8695b5b..5a785fc8602 100644 --- a/src/main/runtime/orchestration-messages-fake-parity.test.ts +++ b/src/main/runtime/orchestration-messages-fake-parity.test.ts @@ -7,9 +7,13 @@ type PointerTarget = { ptyId: string; processIncarnation: string } // The slice of the mailbox store the pointer batch selector depends on. type PointerStore = { - insertMessage(message: { from: string; to: string; subject: string; type?: MessageType }): { - id: string - } + insertMessage(message: { + runId: string + from: string + to: string + subject: string + type?: MessageType + }): { id: string } stageMailboxPointerEnter(ids: string[], target: PointerTarget): boolean markMailboxPointerWriteAttempted(ids: string[], target: PointerTarget): boolean getUndeliveredUnreadMessages( @@ -32,7 +36,12 @@ describe.each(STORES)('mailbox pointer reservations (%s)', (_name, createStore) it('refuses a claim another flight already holds', () => { const store = createStore() - const message = store.insertMessage({ from: 'a', to: 'run:run-1', subject: 'contended' }) + const message = store.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'run:run-1', + subject: 'contended' + }) expect(store.stageMailboxPointerEnter([message.id], rival)).toBe(true) expect(store.stageMailboxPointerEnter([message.id], mine)).toBe(false) @@ -41,8 +50,18 @@ describe.each(STORES)('mailbox pointer reservations (%s)', (_name, createStore) it('rolls the whole batch back when one row is already claimed', () => { const store = createStore() - const free = store.insertMessage({ from: 'a', to: 'run:run-1', subject: 'free' }) - const taken = store.insertMessage({ from: 'a', to: 'run:run-1', subject: 'taken' }) + const free = store.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'run:run-1', + subject: 'free' + }) + const taken = store.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'run:run-1', + subject: 'taken' + }) expect(store.stageMailboxPointerEnter([taken.id], rival)).toBe(true) expect(store.stageMailboxPointerEnter([free.id, taken.id], mine)).toBe(false) @@ -52,8 +71,19 @@ describe.each(STORES)('mailbox pointer reservations (%s)', (_name, createStore) it('applies the exclusion and limit the pointer batch selector relies on', () => { const store = createStore() - store.insertMessage({ from: 'a', to: 'run:run-1', subject: 'reserved', type: 'escalation' }) - const kept = store.insertMessage({ from: 'a', to: 'run:run-1', subject: 'kept' }) + store.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'run:run-1', + subject: 'reserved', + type: 'escalation' + }) + const kept = store.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'run:run-1', + subject: 'kept' + }) expect( store diff --git a/src/main/runtime/orchestration/coordinator-decision-gates.test.ts b/src/main/runtime/orchestration/coordinator-decision-gates.test.ts index 15e0cd2c7b0..3e9934b85ad 100644 --- a/src/main/runtime/orchestration/coordinator-decision-gates.test.ts +++ b/src/main/runtime/orchestration/coordinator-decision-gates.test.ts @@ -12,13 +12,14 @@ describe('coordinator decision-gate authority', () => { it('opens a gate only for the sender-owned active Dispatch', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'owned gate target' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'owned gate target' }) const dispatch = createRootDispatch(db, task.id, 'term_owner', 'tab_owner:leaf_owner') const logs: string[] = [] openDecisionGateFromMessage( db, db.insertMessage({ + runId: 'run_legacy_local', from: 'term_owner', to: 'term_coordinator', subject: 'Need approval', @@ -41,20 +42,27 @@ describe('coordinator decision-gate authority', () => { it('rejects a gate targeting another active Dispatch without mutating either Task', () => { db = new OrchestrationDb(':memory:') - const attackerTask = db.createTask({ spec: 'attacker assignment' }) + const attackerTask = db.createTask({ + runId: 'run_legacy_local', + spec: 'attacker assignment' + }) const attacker = createRootDispatch( db, attackerTask.id, 'term_attacker', 'tab_attacker:leaf_attacker' ) - const victimTask = db.createTask({ spec: 'victim assignment' }) + const victimTask = db.createTask({ + runId: 'run_legacy_local', + spec: 'victim assignment' + }) const victim = createRootDispatch(db, victimTask.id, 'term_victim', 'tab_victim:leaf_victim') const logs: string[] = [] openDecisionGateFromMessage( db, db.insertMessage({ + runId: 'run_legacy_local', from: 'term_attacker', to: 'term_coordinator', subject: 'Block the victim', @@ -79,12 +87,16 @@ describe('coordinator decision-gate authority', () => { it('accepts the canonical sender of an imported federated Dispatch', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'remote gate target' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'remote gate target' + }) const dispatch = createRootDispatch(db, task.id, 'remote-worker') openDecisionGateFromMessage( db, db.insertMessage({ + runId: 'run_legacy_local', from: `dispatch:${dispatch.id}`, to: 'term_coordinator', subject: 'Remote approval required', diff --git a/src/main/runtime/orchestration/coordinator-dispatch-unobserved-prompt.test.ts b/src/main/runtime/orchestration/coordinator-dispatch-unobserved-prompt.test.ts index 5f99e283de3..f0960803988 100644 --- a/src/main/runtime/orchestration/coordinator-dispatch-unobserved-prompt.test.ts +++ b/src/main/runtime/orchestration/coordinator-dispatch-unobserved-prompt.test.ts @@ -66,7 +66,7 @@ describe('coordinator dispatch with an unobserved prompt', () => { it('never re-pastes a preamble whose turn start was not observed', async () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'do the work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'do the work' }) const runtime = createRuntime(new Error('agent_prompt_stalled')) const logs: string[] = [] @@ -88,7 +88,7 @@ describe('coordinator dispatch with an unobserved prompt', () => { it('lets a late worker report settle a dispatch whose prompt was unobserved', async () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'do the work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'do the work' }) await dispatch(createRuntime(new Error('agent_prompt_stalled')), task.id, []) const dispatchId = db.getDispatchContext(task.id)!.id const minted = db.mintDispatchCapability({ @@ -119,7 +119,7 @@ describe('coordinator dispatch with an unobserved prompt', () => { it('still fails the dispatch when the prompt was never delivered', async () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'do the work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'do the work' }) const runtime = createRuntime(new Error('terminal_not_writable')) await expect(dispatch(runtime, task.id, [])).rejects.toThrow('terminal_not_writable') diff --git a/src/main/runtime/orchestration/coordinator-drift-probe-coalescing.test.ts b/src/main/runtime/orchestration/coordinator-drift-probe-coalescing.test.ts index f42713c0dc9..5367af466bf 100644 --- a/src/main/runtime/orchestration/coordinator-drift-probe-coalescing.test.ts +++ b/src/main/runtime/orchestration/coordinator-drift-probe-coalescing.test.ts @@ -44,8 +44,8 @@ describe('Coordinator drift probe coalescing', () => { : { base: 'origin/main', behind: 0, recentSubjects: [] } } } - const first = db.createTask({ spec: 'first task' }) - const second = db.createTask({ spec: 'second task' }) + const first = db.createTask({ runId: 'run_legacy_local', spec: 'first task' }) + const second = db.createTask({ runId: 'run_legacy_local', spec: 'second task' }) const coordinator = new Coordinator(db, runtime, { spec: 'go', coordinatorHandle: 'coord', @@ -65,6 +65,7 @@ describe('Coordinator drift probe coalescing', () => { throw new Error(`missing dispatch for ${task.id}`) } db.insertMessage({ + runId: 'run_legacy_local', from: dispatch.assignee_handle, to: 'coord', subject: 'Done', @@ -105,8 +106,14 @@ describe('Coordinator drift probe coalescing', () => { } } } - const refused = db.createTask({ spec: 'requires a current base' }) - const allowed = db.createTask({ spec: 'can use stale base\nallow-stale-base: true' }) + const refused = db.createTask({ + runId: 'run_legacy_local', + spec: 'requires a current base' + }) + const allowed = db.createTask({ + runId: 'run_legacy_local', + spec: 'can use stale base\nallow-stale-base: true' + }) const coordinator = new Coordinator(db, runtime, { spec: 'go', coordinatorHandle: 'coord', diff --git a/src/main/runtime/orchestration/coordinator-escalation-triage.test.ts b/src/main/runtime/orchestration/coordinator-escalation-triage.test.ts index c020e62dca0..e0bdf75fc98 100644 --- a/src/main/runtime/orchestration/coordinator-escalation-triage.test.ts +++ b/src/main/runtime/orchestration/coordinator-escalation-triage.test.ts @@ -12,20 +12,21 @@ describe('coordinator escalation authority', () => { it('rejects an escalation targeting another active Dispatch', () => { db = new OrchestrationDb(':memory:') - const attackerTask = db.createTask({ spec: 'attacker assignment' }) + const attackerTask = db.createTask({ runId: 'run_legacy_local', spec: 'attacker assignment' }) const attacker = createRootDispatch( db, attackerTask.id, 'term_attacker', 'tab_attacker:leaf_attacker' ) - const victimTask = db.createTask({ spec: 'victim assignment' }) + const victimTask = db.createTask({ runId: 'run_legacy_local', spec: 'victim assignment' }) const victim = createRootDispatch(db, victimTask.id, 'term_victim') const logs: string[] = [] applyEscalationToDispatch( db, db.insertMessage({ + runId: 'run_legacy_local', from: 'term_attacker', to: 'term_coordinator', subject: 'Fail the victim', @@ -43,12 +44,13 @@ describe('coordinator escalation authority', () => { it('accepts the canonical sender of an imported federated Dispatch', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'remote escalation target' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'remote escalation target' }) const dispatch = createRootDispatch(db, task.id, 'remote-worker') applyEscalationToDispatch( db, db.insertMessage({ + runId: 'run_legacy_local', from: `dispatch:${dispatch.id}`, to: 'term_coordinator', subject: 'Remote worker failed', diff --git a/src/main/runtime/orchestration/coordinator-stale-base-flag.test.ts b/src/main/runtime/orchestration/coordinator-stale-base-flag.test.ts new file mode 100644 index 00000000000..818d3f848dc --- /dev/null +++ b/src/main/runtime/orchestration/coordinator-stale-base-flag.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest' +import { parseAllowStaleBaseFromSpec } from './coordinator-stale-base-flag' + +describe('parseAllowStaleBaseFromSpec', () => { + it('matches canonical form on its own line and strips it', () => { + const spec = `Do the work +allow-stale-base: true` + const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec) + expect(allowStale).toBe(true) + expect(strippedSpec).toBe('Do the work\n') + expect(strippedSpec).not.toContain('allow-stale-base') + }) + + it('matches case-insensitively', () => { + const spec = `Do the work +Allow-Stale-Base: TRUE` + const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec) + expect(allowStale).toBe(true) + expect(strippedSpec).not.toMatch(/[Aa]llow-[Ss]tale-[Bb]ase/) + }) + + it('does not match allow-stale-base: false', () => { + const spec = `Do the work +allow-stale-base: false` + const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec) + expect(allowStale).toBe(false) + expect(strippedSpec).toBe(spec) + }) + + it('does not match allow-stale-base: truthy', () => { + const spec = `Do the work +allow-stale-base: truthy` + const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec) + expect(allowStale).toBe(false) + expect(strippedSpec).toBe(spec) + }) + + it('does not match the flag embedded inside a sentence', () => { + const spec = 'we allow-stale-base: true sometimes' + const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec) + expect(allowStale).toBe(false) + expect(strippedSpec).toBe(spec) + }) + + it('handles the flag as the last line with no trailing newline', () => { + const spec = 'line 1\nallow-stale-base: true' + const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec) + expect(allowStale).toBe(true) + expect(strippedSpec).toBe('line 1\n') + expect(strippedSpec.endsWith('allow-stale-base: true')).toBe(false) + }) +}) diff --git a/src/main/runtime/orchestration/coordinator.test.ts b/src/main/runtime/orchestration/coordinator.test.ts index 38701a9d7dd..75ba01ffa27 100644 --- a/src/main/runtime/orchestration/coordinator.test.ts +++ b/src/main/runtime/orchestration/coordinator.test.ts @@ -3,12 +3,11 @@ import { OrchestrationDb } from './db' import { reconcileLifecycleMessage } from './lifecycle-reconciliation' import { Coordinator } from './coordinator' import type { CoordinatorRuntime } from './coordinator-runtime-contract' -import { - DISPATCH_STALE_THRESHOLD, - parseAllowStaleBaseFromSpec -} from './coordinator-stale-base-flag' +import { DISPATCH_STALE_THRESHOLD } from './coordinator-stale-base-flag' import { createRootDispatch } from './db/root-dispatch-test-fixture' +const runId = 'run_legacy_local' + type DriftResult = { base: string behind: number @@ -92,6 +91,7 @@ function insertWorkerDone( } const from = params.from ?? dispatch?.assignee_handle ?? 'term_unknown' db.insertMessage({ + runId, from, to: params.to ?? 'coord', subject: 'Done', @@ -131,7 +131,10 @@ describe('Coordinator', () => { runtime.cliCommand = 'orca-ide' runtime.terminals = [{ handle: 'term_a', worktreeId: 'wt1', connected: true, writable: true }] - const task = db.createTask({ spec: 'implement feature' }) + const task = db.createTask({ + runId, + spec: 'implement feature' + }) // Simulate worker_done arriving after dispatch const coordinator = new Coordinator(db, runtime, { @@ -166,7 +169,10 @@ describe('Coordinator', () => { getTerminalPaneKey: (handle: string) => (handle === 'term_a' ? 'tab_a:leaf_a' : null) }) - const task = db.createTask({ spec: 'implement feature' }) + const task = db.createTask({ + runId, + spec: 'implement feature' + }) const coordinator = new Coordinator(db, withPaneLookup, { spec: 'build it', coordinatorHandle: 'coord', @@ -198,7 +204,10 @@ describe('Coordinator', () => { } : null }) - const task = db.createTask({ spec: 'implement feature' }) + const task = db.createTask({ + runId, + spec: 'implement feature' + }) const coordinator = new Coordinator(db, withAuthority, { spec: 'build it', coordinatorHandle: 'coord', @@ -223,9 +232,13 @@ describe('Coordinator', () => { db = new OrchestrationDb(':memory:') const runtime = createMockRuntime() - const task = db.createTask({ spec: 'send-driven completion' }) + const task = db.createTask({ + runId, + spec: 'send-driven completion' + }) const dispatch = createRootDispatch(db, task.id, 'term_a') const msg = db.insertMessage({ + runId, from: 'term_a', to: 'coord', subject: 'Done', @@ -250,7 +263,10 @@ describe('Coordinator', () => { db = new OrchestrationDb(':memory:') const runtime = createMockRuntime() - const task = db.createTask({ spec: 'duplicate completion' }) + const task = db.createTask({ + runId, + spec: 'duplicate completion' + }) const dispatch = createRootDispatch(db, task.id, 'term_a') const payload = JSON.stringify({ taskId: task.id, @@ -258,6 +274,7 @@ describe('Coordinator', () => { outcome: 'succeeded' }) const first = db.insertMessage({ + runId, from: 'term_a', to: 'coord', subject: 'Done', @@ -265,6 +282,7 @@ describe('Coordinator', () => { payload }) db.insertMessage({ + runId, from: 'term_a', to: 'coord', subject: 'Done again', @@ -289,7 +307,7 @@ describe('Coordinator', () => { db = new OrchestrationDb(':memory:') const runtime = createMockRuntime() - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId, spec: 'work' }) const coordinator = new Coordinator(db, runtime, { spec: 'go', @@ -321,7 +339,10 @@ describe('Coordinator', () => { { handle: 'term_b', worktreeId: 'wt1', connected: true, writable: true } ] - const task = db.createTask({ spec: 'risky work' }) + const task = db.createTask({ + runId, + spec: 'risky work' + }) const coordinator = new Coordinator(db, runtime, { spec: 'go', @@ -339,6 +360,7 @@ describe('Coordinator', () => { const dispatch = db.getDispatchContext(task.id) expect(dispatch).toBeDefined() db.insertMessage({ + runId, from: dispatch?.assignee_handle ?? 'missing-worker', to: 'coord', subject: `Failed attempt ${i + 1}`, @@ -360,7 +382,10 @@ describe('Coordinator', () => { throw new Error('terminal_not_writable') } - const task = db.createTask({ spec: 'cannot dispatch' }) + const task = db.createTask({ + runId, + spec: 'cannot dispatch' + }) const coordinator = new Coordinator(db, runtime, { spec: 'go', coordinatorHandle: 'coord', @@ -379,7 +404,10 @@ describe('Coordinator', () => { const runtime = createMockRuntime() runtime.terminals = [{ handle: 'term_a', worktreeId: 'wt1', connected: true, writable: true }] - const task = db.createTask({ spec: 'needs approval' }) + const task = db.createTask({ + runId, + spec: 'needs approval' + }) const coordinator = new Coordinator(db, runtime, { spec: 'go', @@ -398,6 +426,7 @@ describe('Coordinator', () => { const dispatch = db.getDispatchContext(task.id) expect(dispatch).toBeDefined() db.insertMessage({ + runId, from: 'term_a', to: 'coord', subject: 'Need approval', @@ -441,8 +470,12 @@ describe('Coordinator', () => { const runtime = createMockRuntime() runtime.terminals = [{ handle: 'term_a', worktreeId: 'wt1', connected: true, writable: true }] - const t1 = db.createTask({ spec: 'first' }) - const t2 = db.createTask({ spec: 'second', deps: [t1.id] }) + const t1 = db.createTask({ runId, spec: 'first' }) + const t2 = db.createTask({ + runId, + spec: 'second', + deps: [t1.id] + }) expect(t2.status).toBe('pending') @@ -492,9 +525,9 @@ describe('Coordinator', () => { { handle: 'term_c', worktreeId: 'wt1', connected: true, writable: true } ] - const t1 = db.createTask({ spec: 'one' }) - const t2 = db.createTask({ spec: 'two' }) - const t3 = db.createTask({ spec: 'three' }) + const t1 = db.createTask({ runId, spec: 'one' }) + const t2 = db.createTask({ runId, spec: 'two' }) + const t3 = db.createTask({ runId, spec: 'three' }) const coordinator = new Coordinator(db, runtime, { spec: 'go', @@ -530,7 +563,7 @@ describe('Coordinator', () => { const runtime = createMockRuntime() // No terminals available so dispatchReadyTasks creates one and we can // drive the stale-scan deterministically via SQL backdating. - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId, spec: 'work' }) const ctx = createRootDispatch(db, task.id, 'term_stale') // Backdate dispatched_at and last_heartbeat_at beyond the 10-min threshold @@ -569,7 +602,7 @@ describe('Coordinator', () => { const runtime = createMockRuntime() runtime.terminals = [{ handle: 'term_a', worktreeId: 'wt1', connected: true, writable: true }] - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId, spec: 'work' }) const ctx = createRootDispatch(db, task.id, 'term_a') const coordinator = new Coordinator(db, runtime, { @@ -581,6 +614,7 @@ describe('Coordinator', () => { const runPromise = coordinator.run() db.insertMessage({ + runId, from: 'term_a', to: 'coord', subject: 'alive', @@ -606,12 +640,16 @@ describe('Coordinator', () => { const runtime = createMockRuntime() const logs: string[] = [] - const task = db.createTask({ spec: 'retry-sensitive work' }) + const task = db.createTask({ + runId, + spec: 'retry-sensitive work' + }) const staleCtx = createRootDispatch(db, task.id, 'term_old') db.failDispatch(staleCtx.id, 'retry elsewhere') const activeCtx = createRootDispatch(db, task.id, 'term_current') db.insertMessage({ + runId, from: 'term_old', to: 'coord', subject: 'Late done', @@ -663,11 +701,15 @@ describe('Coordinator', () => { const runtime = createMockRuntime() const logs: string[] = [] - const task = db.createTask({ spec: 'owned work' }) + const task = db.createTask({ + runId, + spec: 'owned work' + }) const leafId = '11111111-1111-4111-8111-111111111111' const ctx = createRootDispatch(db, task.id, 'term_owner', `tab_before:${leafId}`) db.insertMessage({ + runId, from: 'term_reminted', to: 'coord', subject: 'Done after restart', @@ -693,7 +735,7 @@ describe('Coordinator', () => { it('can be stopped', async () => { db = new OrchestrationDb(':memory:') const runtime = createMockRuntime() - db.createTask({ spec: 'never finishes' }) + db.createTask({ runId, spec: 'never finishes' }) const coordinator = new Coordinator(db, runtime, { spec: 'go', @@ -723,7 +765,10 @@ describe('Coordinator', () => { recentSubjects: ['fix A', 'fix B', 'fix C'] }) - const task = db.createTask({ spec: 'do the work' }) + const task = db.createTask({ + runId, + spec: 'do the work' + }) const coordinator = new Coordinator(db, runtime, { spec: 'go', @@ -759,7 +804,10 @@ describe('Coordinator', () => { recentSubjects: ['fix A'] }) - const task = db.createTask({ spec: 'do the work' }) + const task = db.createTask({ + runId, + spec: 'do the work' + }) const coordinator = new Coordinator(db, runtime, { spec: 'go', @@ -799,7 +847,7 @@ describe('Coordinator', () => { const spec = `Investigate issue #42 allow-stale-base: true` - const task = db.createTask({ spec }) + const task = db.createTask({ runId, spec }) const coordinator = new Coordinator(db, runtime, { spec: 'go', @@ -832,7 +880,10 @@ allow-stale-base: true` runtime.terminals = [{ handle: 'term_a', worktreeId: 'wt1', connected: true, writable: true }] runtime.setProbeDrift(null) - const task = db.createTask({ spec: 'do the work' }) + const task = db.createTask({ + runId, + spec: 'do the work' + }) const coordinator = new Coordinator(db, runtime, { spec: 'go', @@ -861,7 +912,10 @@ allow-stale-base: true` runtime.terminals = [{ handle: 'term_a', worktreeId: 'wt1', connected: true, writable: true }] const logs: string[] = [] - const task = db.createTask({ spec: 'do the work' }) + const task = db.createTask({ + runId, + spec: 'do the work' + }) const coordinator = new Coordinator(db, runtime, { spec: 'go', @@ -892,7 +946,10 @@ allow-stale-base: true` runtime.terminals = [{ handle: 'term_a', worktreeId: 'wt1', connected: true, writable: true }] runtime.throwProbeDrift = new Error('boom') - const task = db.createTask({ spec: 'do the work' }) + const task = db.createTask({ + runId, + spec: 'do the work' + }) const coordinator = new Coordinator(db, runtime, { spec: 'go', @@ -915,53 +972,3 @@ allow-stale-base: true` }) }) }) - -describe('parseAllowStaleBaseFromSpec', () => { - it('matches canonical form on its own line and strips it', () => { - const spec = `Do the work -allow-stale-base: true` - const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec) - expect(allowStale).toBe(true) - expect(strippedSpec).toBe('Do the work\n') - expect(strippedSpec).not.toContain('allow-stale-base') - }) - - it('matches case-insensitively', () => { - const spec = `Do the work -Allow-Stale-Base: TRUE` - const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec) - expect(allowStale).toBe(true) - expect(strippedSpec).not.toMatch(/[Aa]llow-[Ss]tale-[Bb]ase/) - }) - - it('does not match allow-stale-base: false', () => { - const spec = `Do the work -allow-stale-base: false` - const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec) - expect(allowStale).toBe(false) - expect(strippedSpec).toBe(spec) - }) - - it('does not match allow-stale-base: truthy', () => { - const spec = `Do the work -allow-stale-base: truthy` - const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec) - expect(allowStale).toBe(false) - expect(strippedSpec).toBe(spec) - }) - - it('does not match the flag embedded inside a sentence', () => { - const spec = 'we allow-stale-base: true sometimes' - const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec) - expect(allowStale).toBe(false) - expect(strippedSpec).toBe(spec) - }) - - it('handles the flag as the last line with no trailing newline', () => { - const spec = 'line 1\nallow-stale-base: true' - const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec) - expect(allowStale).toBe(true) - expect(strippedSpec).toBe('line 1\n') - expect(strippedSpec.endsWith('allow-stale-base: true')).toBe(false) - }) -}) diff --git a/src/main/runtime/orchestration/db-empty-dispatch-shortcircuit.benchmark.test.ts b/src/main/runtime/orchestration/db-empty-dispatch-shortcircuit.benchmark.test.ts index c729a8b1dd7..6407e67bc4b 100644 --- a/src/main/runtime/orchestration/db-empty-dispatch-shortcircuit.benchmark.test.ts +++ b/src/main/runtime/orchestration/db-empty-dispatch-shortcircuit.benchmark.test.ts @@ -64,7 +64,7 @@ describe('orchestration empty-dispatch short-circuit (benchmark)', () => { it('still runs the fan-out once a dispatch exists (correctness preserved)', () => { const db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) createRootDispatch(db, task.id, 'term_5') const handles = Array.from({ length: 10 }, (_, i) => `term_${i}`) @@ -76,7 +76,11 @@ describe('orchestration empty-dispatch short-circuit (benchmark)', () => { it('predicate lifecycle: false when empty, true after dispatch (even completed), false after reset', () => { const db = new OrchestrationDb(':memory:') expect(db.hasAnyDispatchContexts()).toBe(false) - const ctx = createRootDispatch(db, db.createTask({ spec: 'work' }).id, 'term_worker') + const ctx = createRootDispatch( + db, + db.createTask({ runId: 'run_legacy_local', spec: 'work' }).id, + 'term_worker' + ) expect(db.hasAnyDispatchContexts()).toBe(true) // Completed rows still count — recent-completed lookups must stay valid. db.completeDispatch(ctx.id) diff --git a/src/main/runtime/orchestration/db-heartbeat-straggler-guard.test.ts b/src/main/runtime/orchestration/db-heartbeat-straggler-guard.test.ts index 793c218e162..c7743757aa9 100644 --- a/src/main/runtime/orchestration/db-heartbeat-straggler-guard.test.ts +++ b/src/main/runtime/orchestration/db-heartbeat-straggler-guard.test.ts @@ -13,7 +13,7 @@ afterEach(() => { function seedHeartbeatedDispatch(): { d: OrchestrationDb; dispatchId: string } { const d = new OrchestrationDb(':memory:') db = d - const task = d.createTask({ spec: 'work' }) + const task = d.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(d, task.id, 'term_worker') d.recordHeartbeat(dispatch.id, '2026-05-03T00:00:00.000Z') return { d, dispatchId: dispatch.id } diff --git a/src/main/runtime/orchestration/db-message-timestamp.test.ts b/src/main/runtime/orchestration/db-message-timestamp.test.ts index d4d000c48d3..3900904e5b1 100644 --- a/src/main/runtime/orchestration/db-message-timestamp.test.ts +++ b/src/main/runtime/orchestration/db-message-timestamp.test.ts @@ -8,7 +8,12 @@ describe('orchestration message timestamps', () => { it('exposes SQLite timestamps with an explicit UTC designator', () => { db = new OrchestrationDb(':memory:') - const message = db.insertMessage({ from: 'a', to: 'b', subject: 'timestamped' }) + const message = db.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'b', + subject: 'timestamped' + }) expect(message.created_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/) db.markAsDelivered([message.id]) diff --git a/src/main/runtime/orchestration/db-messages.test.ts b/src/main/runtime/orchestration/db-messages.test.ts new file mode 100644 index 00000000000..789797d12f0 --- /dev/null +++ b/src/main/runtime/orchestration/db-messages.test.ts @@ -0,0 +1,194 @@ +import { afterEach, describe, expect, it } from 'vitest' +import type Database from '../../sqlite/sync-database' +import { OrchestrationDb, type MessageType } from './db' + +const runId = 'run_legacy_local' + +describe('OrchestrationDb', () => { + let db: OrchestrationDb | undefined + + afterEach(() => { + db?.close() + }) + + function createDb(): OrchestrationDb { + db = new OrchestrationDb(':memory:') + return db + } + + describe('messages', () => { + it('inserts and retrieves a message', () => { + const d = createDb() + const msg = d.insertMessage({ + runId, + from: 'term_a', + to: 'term_b', + subject: 'hello', + body: 'world' + }) + expect(msg.id).toMatch(/^msg_/) + expect(msg.from_handle).toBe('term_a') + expect(msg.to_handle).toBe('term_b') + expect(msg.subject).toBe('hello') + expect(msg.body).toBe('world') + expect(msg.type).toBe('status') + expect(msg.priority).toBe('normal') + expect(msg.read).toBe(0) + expect(msg.sequence).toBeGreaterThan(0) + }) + + it('returns unread messages in sequence order', () => { + const d = createDb() + d.insertMessage({ runId, from: 'a', to: 'b', subject: 'first' }) + d.insertMessage({ runId, from: 'a', to: 'b', subject: 'second' }) + d.insertMessage({ runId, from: 'a', to: 'c', subject: 'other' }) + + const unread = d.getUnreadMessages('b') + expect(unread).toHaveLength(2) + expect(unread[0].subject).toBe('first') + expect(unread[1].subject).toBe('second') + }) + + it('filters unread by type', () => { + const d = createDb() + d.insertMessage({ + runId, + from: 'a', + to: 'b', + subject: 'status msg', + type: 'status' + }) + d.insertMessage({ + runId, + from: 'a', + to: 'b', + subject: 'done msg', + type: 'worker_done' + }) + + const filtered = d.getUnreadMessages('b', ['worker_done']) + expect(filtered).toHaveLength(1) + expect(filtered[0].type).toBe('worker_done') + }) + + it('excludes already-delivered rows from getUndeliveredUnreadMessages', () => { + const d = createDb() + const m1 = d.insertMessage({ runId, from: 'a', to: 'b', subject: 'one' }) + const m2 = d.insertMessage({ runId, from: 'a', to: 'b', subject: 'two' }) + + d.markAsDelivered([m1.id]) + + // Push delivery query: only undelivered, unread. + const pending = d.getUndeliveredUnreadMessages('b') + expect(pending).toHaveLength(1) + expect(pending[0].id).toBe(m2.id) + + // Explicit `check` still sees both (they are still unread). + const unread = d.getUnreadMessages('b') + expect(unread).toHaveLength(2) + }) + + it('creates the undelivered inbox index used by push delivery', () => { + const d = createDb() + const sqlite = (d as unknown as { db: Database.Database }).db + + const indexes = sqlite + .prepare( + `SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'messages' AND name = 'idx_messages_undelivered_inbox'` + ) + .all() + + expect(indexes).toHaveLength(1) + }) + + it('filters getUndeliveredUnreadMessages by type', () => { + const d = createDb() + d.insertMessage({ + runId, + from: 'a', + to: 'b', + subject: 's', + type: 'status' + }) + const wd = d.insertMessage({ + runId, + from: 'a', + to: 'b', + subject: 'd', + type: 'worker_done' + }) + + const filtered = d.getUndeliveredUnreadMessages('b', ['worker_done']) + expect(filtered).toHaveLength(1) + expect(filtered[0].id).toBe(wd.id) + }) + + it('marks messages as read', () => { + const d = createDb() + const m1 = d.insertMessage({ runId, from: 'a', to: 'b', subject: 'one' }) + const m2 = d.insertMessage({ runId, from: 'a', to: 'b', subject: 'two' }) + + d.markAsRead([m1.id]) + + const unread = d.getUnreadMessages('b') + expect(unread).toHaveLength(1) + expect(unread[0].id).toBe(m2.id) + }) + + it('stores typed payload and thread_id', () => { + const d = createDb() + const payload = JSON.stringify({ taskId: 'task_abc', filesModified: ['src/a.ts'] }) + const msg = d.insertMessage({ + runId, + from: 'a', + to: 'b', + subject: 'done', + type: 'worker_done', + priority: 'high', + threadId: 'thread_1', + payload + }) + + expect(msg.type).toBe('worker_done') + expect(msg.priority).toBe('high') + expect(msg.thread_id).toBe('thread_1') + expect(msg.payload).toBe(payload) + }) + + it('rejects invalid message type', () => { + const d = createDb() + expect(() => + d.insertMessage({ + runId, + from: 'a', + to: 'b', + subject: 'bad', + type: 'invalid' as MessageType + }) + ).toThrow() + }) + + it('getInbox returns all messages across recipients', () => { + const d = createDb() + d.insertMessage({ runId, from: 'a', to: 'b', subject: 'one' }) + d.insertMessage({ runId, from: 'a', to: 'c', subject: 'two' }) + d.insertMessage({ runId, from: 'b', to: 'a', subject: 'three' }) + + const inbox = d.getInbox(10) + expect(inbox).toHaveLength(3) + }) + + it('getMessageById returns the correct message', () => { + const d = createDb() + const msg = d.insertMessage({ + runId, + from: 'a', + to: 'b', + subject: 'test' + }) + const found = d.getMessageById(msg.id) + expect(found?.subject).toBe('test') + expect(d.getMessageById('msg_nonexistent')).toBeUndefined() + }) + }) +}) diff --git a/src/main/runtime/orchestration/db-task-create-readiness.test.ts b/src/main/runtime/orchestration/db-task-create-readiness.test.ts index 019b627da6c..4b661829d02 100644 --- a/src/main/runtime/orchestration/db-task-create-readiness.test.ts +++ b/src/main/runtime/orchestration/db-task-create-readiness.test.ts @@ -33,12 +33,16 @@ describe('task creation dependency readiness', () => { it('creates a late dependent as ready when every dependency is completed', () => { const db = createDb() - const first = db.createTask({ spec: 'first' }) - const second = db.createTask({ spec: 'second' }) + const first = db.createTask({ runId: 'run_legacy_local', spec: 'first' }) + const second = db.createTask({ runId: 'run_legacy_local', spec: 'second' }) db.updateTaskStatus(first.id, 'completed') db.updateTaskStatus(second.id, 'completed') - const child = db.createTask({ spec: 'child', deps: [first.id, second.id] }) + const child = db.createTask({ + runId: 'run_legacy_local', + spec: 'child', + deps: [first.id, second.id] + }) expect(child.status).toBe('ready') }) @@ -49,7 +53,7 @@ describe('task creation dependency readiness', () => { const path = join(directory, 'orchestration.db') const db = createDb(path) const concurrent = createDb(path) - const dependency = db.createTask({ spec: 'dependency' }) + const dependency = db.createTask({ runId: 'run_legacy_local', spec: 'dependency' }) const sqlite = (db as unknown as OrchestrationDbAccess).db const prepare = sqlite.prepare.bind(sqlite) let injected = false @@ -61,7 +65,7 @@ describe('task creation dependency readiness', () => { return prepare(sql) }) - const child = db.createTask({ spec: 'child', deps: [dependency.id] }) + const child = db.createTask({ runId: 'run_legacy_local', spec: 'child', deps: [dependency.id] }) expect(injected).toBe(true) expect(child.status).toBe('ready') @@ -69,10 +73,14 @@ describe('task creation dependency readiness', () => { it('promotes only after every dependency completes', () => { const db = createDb() - const first = db.createTask({ spec: 'first' }) - const second = db.createTask({ spec: 'second' }) + const first = db.createTask({ runId: 'run_legacy_local', spec: 'first' }) + const second = db.createTask({ runId: 'run_legacy_local', spec: 'second' }) db.updateTaskStatus(first.id, 'completed') - const child = db.createTask({ spec: 'child', deps: [first.id, second.id] }) + const child = db.createTask({ + runId: 'run_legacy_local', + spec: 'child', + deps: [first.id, second.id] + }) expect(child.status).toBe('pending') db.updateTaskStatus(second.id, 'completed') @@ -83,11 +91,15 @@ describe('task creation dependency readiness', () => { 'does not unlock a dependent whose dependency is %s', (status) => { const db = createDb() - const terminal = db.createTask({ spec: 'terminal dependency' }) - const completing = db.createTask({ spec: 'completing dependency' }) + const terminal = db.createTask({ runId: 'run_legacy_local', spec: 'terminal dependency' }) + const completing = db.createTask({ runId: 'run_legacy_local', spec: 'completing dependency' }) db.updateTaskStatus(terminal.id, status) - const child = db.createTask({ spec: 'child', deps: [terminal.id, completing.id] }) + const child = db.createTask({ + runId: 'run_legacy_local', + spec: 'child', + deps: [terminal.id, completing.id] + }) expect(child.status).toBe('pending') db.updateTaskStatus(completing.id, 'completed') @@ -98,9 +110,9 @@ describe('task creation dependency readiness', () => { it('rejects missing dependencies without inserting a task', () => { const db = createDb() - expect(() => db.createTask({ spec: 'child', deps: ['task_missing'] })).toThrow( - 'Dependency task task_missing must belong to run' - ) + expect(() => + db.createTask({ runId: 'run_legacy_local', spec: 'child', deps: ['task_missing'] }) + ).toThrow('Dependency task task_missing must belong to run') expect(db.listTasks()).toEqual([]) }) @@ -109,7 +121,7 @@ describe('task creation dependency readiness', () => { const sqlite = (db as unknown as OrchestrationDbAccess).db sqlite.exec('BEGIN IMMEDIATE') - const task = db.createTask({ spec: 'transactional child' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'transactional child' }) sqlite.exec('ROLLBACK') expect(db.getTask(task.id)).toBeUndefined() @@ -120,11 +132,19 @@ describe('task creation dependency readiness', () => { directories.push(directory) const path = join(directory, 'orchestration.db') const before = createDb(path) - const completed = before.createTask({ spec: 'completed' }) - const open = before.createTask({ spec: 'open' }) + const completed = before.createTask({ runId: 'run_legacy_local', spec: 'completed' }) + const open = before.createTask({ runId: 'run_legacy_local', spec: 'open' }) before.updateTaskStatus(completed.id, 'completed') - const ready = before.createTask({ spec: 'ready', deps: [completed.id] }) - const pending = before.createTask({ spec: 'pending', deps: [completed.id, open.id] }) + const ready = before.createTask({ + runId: 'run_legacy_local', + spec: 'ready', + deps: [completed.id] + }) + const pending = before.createTask({ + runId: 'run_legacy_local', + spec: 'pending', + deps: [completed.id, open.id] + }) before.close() databases.splice(databases.indexOf(before), 1) diff --git a/src/main/runtime/orchestration/db-task-dispatch-invariant.test.ts b/src/main/runtime/orchestration/db-task-dispatch-invariant.test.ts index 53340b6dce5..2b81b2f2897 100644 --- a/src/main/runtime/orchestration/db-task-dispatch-invariant.test.ts +++ b/src/main/runtime/orchestration/db-task-dispatch-invariant.test.ts @@ -30,9 +30,17 @@ describe('Task/Dispatch invariant transactions', () => { 'allows a dependency-blocked pending Task to become %s', (status) => { const { db } = createDatabase() - const dependency = db.createTask({ spec: 'unresolved dependency' }) - const task = db.createTask({ spec: 'manual resolution', deps: [dependency.id] }) - const dependent = db.createTask({ spec: 'downstream work', deps: [task.id] }) + const dependency = db.createTask({ runId: 'run_legacy_local', spec: 'unresolved dependency' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'manual resolution', + deps: [dependency.id] + }) + const dependent = db.createTask({ + runId: 'run_legacy_local', + spec: 'downstream work', + deps: [task.id] + }) expect(task.status).toBe('pending') const updated = db.updateTaskStatus(task.id, status, 'manual resolution') @@ -45,7 +53,7 @@ describe('Task/Dispatch invariant transactions', () => { it('surfaces invalid Task lifecycle edges instead of returning the unchanged row', () => { const { db } = createDatabase() - const task = db.createTask({ spec: 'invalid lifecycle edge' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'invalid lifecycle edge' }) db.updateTaskStatus(task.id, 'blocked') expect(() => @@ -67,8 +75,12 @@ describe('Task/Dispatch invariant transactions', () => { 'rolls back a %s Task when Dispatch settlement fails', (status) => { const { db } = createDatabase() - const task = db.createTask({ spec: 'atomic work' }) - const dependent = db.createTask({ spec: 'dependent work', deps: [task.id] }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'atomic work' }) + const dependent = db.createTask({ + runId: 'run_legacy_local', + spec: 'dependent work', + deps: [task.id] + }) const dispatch = createRootDispatch(db, task.id, 'term_worker') const capability = db.mintDispatchCapability({ dispatchId: dispatch.id, @@ -111,7 +123,10 @@ describe('Task/Dispatch invariant transactions', () => { it('does not commit a caller-owned transaction', () => { const { db } = createDatabase() - const task = db.createTask({ spec: 'outer transaction work' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'outer transaction work' + }) const dispatch = createRootDispatch(db, task.id, 'term_worker') const sqlite = sqliteFor(db) @@ -135,7 +150,10 @@ describe('Task/Dispatch invariant transactions', () => { it('keeps Dispatch creation inside a caller-owned transaction', () => { const { db } = createDatabase() - const task = db.createTask({ spec: 'outer transaction dispatch' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'outer transaction dispatch' + }) const sqlite = sqliteFor(db) sqlite.exec('BEGIN IMMEDIATE') @@ -152,7 +170,10 @@ describe('Task/Dispatch invariant transactions', () => { 'settles every active Dispatch left by a pre-fix split when the Task becomes %s', (status) => { const { db } = createDatabase() - const task = db.createTask({ spec: 'legacy split work' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'legacy split work' + }) const first = createRootDispatch(db, task.id, 'term_first') sqliteFor(db).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) const second = createRootDispatch(db, task.id, 'term_second') @@ -169,17 +190,31 @@ describe('Task/Dispatch invariant transactions', () => { expect(db.getActiveDispatchForTerminal('term_first')).toBeUndefined() expect(db.getActiveDispatchForTerminal('term_second')).toBeUndefined() expect(() => - createRootDispatch(db, db.createTask({ spec: 'first later work' }).id, 'term_first') + createRootDispatch( + db, + db.createTask({ runId: 'run_legacy_local', spec: 'first later work' }).id, + 'term_first' + ) ).not.toThrow() expect(() => - createRootDispatch(db, db.createTask({ spec: 'second later work' }).id, 'term_second') + createRootDispatch( + db, + db.createTask({ + runId: 'run_legacy_local', + spec: 'second later work' + }).id, + 'term_second' + ) ).not.toThrow() } ) it('does not requeue a legacy split Task while another Dispatch remains active', () => { const { db } = createDatabase() - const task = db.createTask({ spec: 'legacy split retry' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'legacy split retry' + }) const first = createRootDispatch(db, task.id, 'term_first') sqliteFor(db).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) const second = createRootDispatch(db, task.id, 'term_second') @@ -193,7 +228,10 @@ describe('Task/Dispatch invariant transactions', () => { it('does not block a legacy split Task while another Dispatch remains active', () => { const { db } = createDatabase() - const task = db.createTask({ spec: 'legacy split release' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'legacy split release' + }) const first = createRootDispatch(db, task.id, 'term_first') sqliteFor(db).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) const second = createRootDispatch(db, task.id, 'term_second') @@ -211,7 +249,10 @@ describe('Task/Dispatch invariant transactions', () => { 'rejects moving a Task to %s while a Dispatch remains active', (status) => { const { db } = createDatabase() - const task = db.createTask({ spec: 'guarded work' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'guarded work' + }) const dispatch = createRootDispatch(db, task.id, 'term_worker') expect(() => db.updateTaskStatus(task.id, status, 'must not persist')).toThrowError( @@ -227,7 +268,10 @@ describe('Task/Dispatch invariant transactions', () => { it('rejects moving a Task to dispatched without an active Dispatch', () => { const { db } = createDatabase() - const task = db.createTask({ spec: 'unassigned work' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'unassigned work' + }) expect(() => db.updateTaskStatus(task.id, 'dispatched')).toThrowError( expect.objectContaining({ @@ -241,7 +285,10 @@ describe('Task/Dispatch invariant transactions', () => { it('rejects a Dispatch when failure wins after readiness was observed', () => { const first = createDatabase() const concurrent = createDatabase(first.path) - const task = first.db.createTask({ spec: 'interleaved work' }) + const task = first.db.createTask({ + runId: 'run_legacy_local', + spec: 'interleaved work' + }) const sqlite = sqliteFor(first.db) const prepare = sqlite.prepare.bind(sqlite) let injected = false @@ -264,8 +311,14 @@ describe('Task/Dispatch invariant transactions', () => { it('atomically rejects a same-pane Dispatch that loses the occupancy race', () => { const first = createDatabase() const concurrent = createDatabase(first.path) - const firstTask = first.db.createTask({ spec: 'first terminal claimant' }) - const secondTask = first.db.createTask({ spec: 'second terminal claimant' }) + const firstTask = first.db.createTask({ + runId: 'run_legacy_local', + spec: 'first terminal claimant' + }) + const secondTask = first.db.createTask({ + runId: 'run_legacy_local', + spec: 'second terminal claimant' + }) const sqlite = sqliteFor(first.db) const prepare = sqlite.prepare.bind(sqlite) let winnerId: string | undefined @@ -303,14 +356,20 @@ describe('Task/Dispatch invariant transactions', () => { it('rejects worker authority when another Dispatch owns the pane', () => { const { db } = createDatabase() - const ownerTask = db.createTask({ spec: 'current pane owner' }) + const ownerTask = db.createTask({ + runId: 'run_legacy_local', + spec: 'current pane owner' + }) const owner = createRootDispatch( db, ownerTask.id, 'term_owner', 'tab_old:cccccccc-cccc-4ccc-8ccc-cccccccccccc' ) - const workerTask = db.createTask({ spec: 'competing supervised worker' }) + const workerTask = db.createTask({ + runId: 'run_legacy_local', + spec: 'competing supervised worker' + }) const started = db.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -346,7 +405,10 @@ describe('Task/Dispatch invariant transactions', () => { 'rejects a %s Task update while its supervised worker remains active', (status) => { const { db } = createDatabase() - const task = db.createTask({ spec: 'supervised lifecycle' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'supervised lifecycle' + }) const started = db.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -394,7 +456,10 @@ describe('Task/Dispatch invariant transactions', () => { it('keeps a federated late start authoritative after rejecting Task failure', () => { const { db } = createDatabase() - const task = db.createTask({ spec: 'federated lifecycle' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'federated lifecycle' + }) const started = db.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, diff --git a/src/main/runtime/orchestration/db-task-dispatch-lifecycle-guards.test.ts b/src/main/runtime/orchestration/db-task-dispatch-lifecycle-guards.test.ts index bb6d3472d4e..f4cdcdf63b8 100644 --- a/src/main/runtime/orchestration/db-task-dispatch-lifecycle-guards.test.ts +++ b/src/main/runtime/orchestration/db-task-dispatch-lifecycle-guards.test.ts @@ -29,7 +29,7 @@ afterEach(() => { describe('Task/Dispatch lifecycle guards', () => { it('rejects a worker report while another supervised Dispatch is active', () => { const database = createDatabase() - const task = database.createTask({ spec: 'legacy supervised split' }) + const task = database.createTask({ runId: 'run_legacy_local', spec: 'legacy supervised split' }) const first = startWorker(database, task.id, 'first') sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) const second = startWorker(database, task.id, 'second') @@ -55,7 +55,7 @@ describe('Task/Dispatch lifecycle guards', () => { 'settles context-only legacy siblings after a %s worker report', (outcome) => { const database = createDatabase() - const task = database.createTask({ spec: 'legacy mixed split' }) + const task = database.createTask({ runId: 'run_legacy_local', spec: 'legacy mixed split' }) const contextOnly = createRootDispatch(database, task.id, 'term_context') sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) const worker = startWorker(database, task.id, 'reporter') @@ -78,7 +78,10 @@ describe('Task/Dispatch lifecycle guards', () => { expect(() => createRootDispatch( database, - database.createTask({ spec: 'later context work' }).id, + database.createTask({ + runId: 'run_legacy_local', + spec: 'later context work' + }).id, 'term_context' ) ).not.toThrow() @@ -87,7 +90,10 @@ describe('Task/Dispatch lifecycle guards', () => { it('settles a newer context-only legacy sibling after a worker report', () => { const database = createDatabase() - const task = database.createTask({ spec: 'reversed legacy mixed split' }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: 'reversed legacy mixed split' + }) const worker = startWorker(database, task.id, 'reversed_reporter') sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) const contextOnly = createRootDispatch(database, task.id, 'term_reversed_context') @@ -112,7 +118,10 @@ describe('Task/Dispatch lifecycle guards', () => { 'treats abandon of an already %s worker as stale without a lifecycle conflict', (state) => { const database = createDatabase() - const task = database.createTask({ spec: `already ${state}` }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: `already ${state}` + }) const worker = startWorker(database, task.id, `already_${state}`) if (state === 'failed') { database.failDispatch(worker.dispatchId, 'process exited', { workerProcessExited: true }) @@ -130,7 +139,10 @@ describe('Task/Dispatch lifecycle guards', () => { it('rejects generic failure while a supervised worker remains active', () => { const database = createDatabase() - const task = database.createTask({ spec: 'supervised failure guard' }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: 'supervised failure guard' + }) const worker = startWorker(database, task.id, 'guarded') expect(() => database.failDispatch(worker.dispatchId, 'unsafe retry')).toThrowError( @@ -151,7 +163,10 @@ describe('Task/Dispatch lifecycle guards', () => { it('atomically settles worker state when a proven process exit fails its Dispatch', () => { const database = createDatabase() - const task = database.createTask({ spec: 'exited worker' }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: 'exited worker' + }) const worker = startWorker(database, task.id, 'exited') expect( @@ -168,7 +183,10 @@ describe('Task/Dispatch lifecycle guards', () => { it('settles a stop-unknown worker when a positive PTY exit arrives', () => { const database = createDatabase() - const task = database.createTask({ spec: 'stop-unknown exited worker' }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: 'stop-unknown exited worker' + }) const worker = startWorker(database, task.id, 'stop_unknown_exited') expect(database.beginWorkerStop(worker.dispatchId, 'runtime_test').disposition).toBe('stopping') @@ -198,7 +216,10 @@ describe('Task/Dispatch lifecycle guards', () => { it('keeps a Task dispatched when missing-terminal recovery leaves another worker active', () => { const database = createDatabase() - const task = database.createTask({ spec: 'legacy missing-terminal split' }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: 'legacy missing-terminal split' + }) const missing = startWorker(database, task.id, 'missing') sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) const live = startWorker(database, task.id, 'live') @@ -225,7 +246,10 @@ describe('Task/Dispatch lifecycle guards', () => { 'keeps a Task dispatched when a %s worker start fails beside a live worker', (kind) => { const database = createDatabase() - const task = database.createTask({ spec: `${kind} split start failure` }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: `${kind} split start failure` + }) const failed = database.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -342,7 +366,10 @@ describe('Task/Dispatch lifecycle guards', () => { it('rolls back federated start uncertainty when the Task transition cannot commit', () => { const database = createDatabase() - const task = database.createTask({ spec: 'atomic federated uncertainty' }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: 'atomic federated uncertainty' + }) const started = database.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -383,7 +410,10 @@ describe('Task/Dispatch lifecycle guards', () => { '%s releases the last context-only sibling after a newer worker start fails', (operation) => { const database = createDatabase() - const task = database.createTask({ spec: `${operation} historical sibling` }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: `${operation} historical sibling` + }) const contextOnly = createRootDispatch(database, task.id, `term_${operation}`) sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) const failed = database.createStartingWorkerDispatch({ @@ -411,7 +441,10 @@ describe('Task/Dispatch lifecycle guards', () => { expect(() => createRootDispatch( database, - database.createTask({ spec: `${operation} later work` }).id, + database.createTask({ + runId: 'run_legacy_local', + spec: `${operation} later work` + }).id, `term_${operation}` ) ).not.toThrow() @@ -422,7 +455,10 @@ describe('Task/Dispatch lifecycle guards', () => { '%s records guarded receipts for context-only Dispatch and Task release', (operation) => { const database = createDatabase() - const task = database.createTask({ spec: `${operation} receipt release` }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: `${operation} receipt release` + }) const contextOnly = createRootDispatch(database, task.id, `term_${operation}`) const released = @@ -445,7 +481,10 @@ describe('Task/Dispatch lifecycle guards', () => { it('rolls back both context-only projections when the Task transition fails', () => { const database = createDatabase() - const task = database.createTask({ spec: 'context-only atomic receipt' }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: 'context-only atomic receipt' + }) const contextOnly = createRootDispatch(database, task.id, 'term_context') sqliteFor(database).exec(` CREATE TRIGGER reject_context_release_task_block @@ -470,7 +509,10 @@ describe('Task/Dispatch lifecycle guards', () => { '%s preserves a live worker sibling and lets it report', (operation) => { const database = createDatabase() - const task = database.createTask({ spec: `${operation} legacy worker split` }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: `${operation} legacy worker split` + }) const live = startWorker(database, task.id, `${operation}_live`) sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) const released = startWorker(database, task.id, `${operation}_released`) @@ -502,7 +544,10 @@ describe('Task/Dispatch lifecycle guards', () => { it('blocks a Task when an interleaved stop settles its final active Dispatch', () => { const database = createDatabase() - const task = database.createTask({ spec: 'interleaved legacy worker release' }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: 'interleaved legacy worker release' + }) const stopping = startWorker(database, task.id, 'interleaved_stopping') sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) const abandoned = startWorker(database, task.id, 'interleaved_abandoned') @@ -521,7 +566,10 @@ describe('Task/Dispatch lifecycle guards', () => { it('restores a live sibling after stopping an uncertain worker start', () => { const database = createDatabase() - const task = database.createTask({ spec: 'uncertain legacy worker split' }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: 'uncertain legacy worker split' + }) const live = startWorker(database, task.id, 'uncertain_live') sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) const uncertain = database.createStartingWorkerDispatch({ @@ -552,7 +600,10 @@ describe('Task/Dispatch lifecycle guards', () => { 'restores a live sibling after an uncertain worker start fails through %s', (recovery) => { const database = createDatabase() - const task = database.createTask({ spec: `${recovery} uncertain sibling` }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: `${recovery} uncertain sibling` + }) const live = startWorker(database, task.id, `${recovery}_live`) sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) const uncertain = database.createStartingWorkerDispatch({ @@ -589,7 +640,10 @@ describe('Task/Dispatch lifecycle guards', () => { it('rejects gate creation while a supervised worker remains active', () => { const database = createDatabase() - const task = database.createTask({ spec: 'worker gate guard' }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: 'worker gate guard' + }) const worker = startWorker(database, task.id, 'gate') expect(() => database.createGate({ taskId: task.id, question: 'Proceed?' })).toThrowError( @@ -607,7 +661,10 @@ describe('Task/Dispatch lifecycle guards', () => { it('rolls back gate resolution when an active Dispatch blocks readiness', () => { const database = createDatabase() - const task = database.createTask({ spec: 'corrupt gated task' }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: 'corrupt gated task' + }) const gate = database.createGate({ taskId: task.id, question: 'Proceed?' }) sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id) const dispatch = createRootDispatch(database, task.id, 'term_worker') diff --git a/src/main/runtime/orchestration/db-task-dispatch-races.test.ts b/src/main/runtime/orchestration/db-task-dispatch-races.test.ts index b4c27ac0c64..c671aa4566b 100644 --- a/src/main/runtime/orchestration/db-task-dispatch-races.test.ts +++ b/src/main/runtime/orchestration/db-task-dispatch-races.test.ts @@ -29,7 +29,10 @@ describe('Task/Dispatch concurrency', () => { it('reads a concurrent Task result before applying an explicit status correction', () => { const first = createDatabase() const concurrent = createDatabase(first.path) - const task = first.db.createTask({ spec: 'concurrent status winner' }) + const task = first.db.createTask({ + runId: 'run_legacy_local', + spec: 'concurrent status winner' + }) const sqlite = sqliteFor(first.db) const exec = sqlite.exec.bind(sqlite) let concurrentWon = false @@ -57,7 +60,7 @@ describe('Task/Dispatch concurrency', () => { it('holds the Task status writer reservation through its lifecycle reads', () => { const first = createDatabase() const concurrent = createDatabase(first.path) - const task = first.db.createTask({ spec: 'reserved status winner' }) + const task = first.db.createTask({ runId: 'run_legacy_local', spec: 'reserved status winner' }) const sqlite = sqliteFor(first.db) const exec = sqlite.exec.bind(sqlite) sqliteFor(concurrent.db).pragma('busy_timeout = 0') @@ -86,7 +89,7 @@ describe('Task/Dispatch concurrency', () => { it('rolls back Dispatch failure when Task requeue fails', () => { const { db } = createDatabase() - const task = db.createTask({ spec: 'atomic retry failure' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'atomic retry failure' }) const dispatch = createRootDispatch(db, task.id, 'term_worker') sqliteFor(db).exec(` CREATE TRIGGER reject_task_requeue @@ -113,7 +116,10 @@ describe('Task/Dispatch concurrency', () => { it('does not let stale failure overwrite a completed worker report', () => { const first = createDatabase() const concurrent = createDatabase(first.path) - const task = first.db.createTask({ spec: 'worker completion wins' }) + const task = first.db.createTask({ + runId: 'run_legacy_local', + spec: 'worker completion wins' + }) const started = first.db.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -177,7 +183,10 @@ describe('Task/Dispatch concurrency', () => { it('keeps nested dispatch failure atomic with its caller transaction', () => { const { db } = createDatabase() - const task = db.createTask({ spec: 'nested atomic failure' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'nested atomic failure' + }) const dispatch = createRootDispatch(db, task.id, 'term_worker') const sqlite = sqliteFor(db) @@ -198,8 +207,14 @@ describe('Task/Dispatch concurrency', () => { it('serializes reminted-pane worker authority claims', () => { const first = createDatabase() const concurrent = createDatabase(first.path) - const losingTask = first.db.createTask({ spec: 'losing worker' }) - const winningTask = first.db.createTask({ spec: 'winning worker' }) + const losingTask = first.db.createTask({ + runId: 'run_legacy_local', + spec: 'losing worker' + }) + const winningTask = first.db.createTask({ + runId: 'run_legacy_local', + spec: 'winning worker' + }) const loser = first.db.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, diff --git a/src/main/runtime/orchestration/db-undelivered-mailboxes.test.ts b/src/main/runtime/orchestration/db-undelivered-mailboxes.test.ts index 86bc9fdf46b..b20dd625310 100644 --- a/src/main/runtime/orchestration/db-undelivered-mailboxes.test.ts +++ b/src/main/runtime/orchestration/db-undelivered-mailboxes.test.ts @@ -8,10 +8,20 @@ describe('undelivered orchestration mailboxes', () => { it('lists only mailboxes with undelivered unread messages', () => { db = new OrchestrationDb(':memory:') - const delivered = db.insertMessage({ from: 'a', to: 'delivered', subject: 'done' }) - const read = db.insertMessage({ from: 'a', to: 'read', subject: 'seen' }) - db.insertMessage({ from: 'a', to: 'pending', subject: 'first' }) - db.insertMessage({ from: 'a', to: 'pending', subject: 'second' }) + const delivered = db.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'delivered', + subject: 'done' + }) + const read = db.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'read', + subject: 'seen' + }) + db.insertMessage({ runId: 'run_legacy_local', from: 'a', to: 'pending', subject: 'first' }) + db.insertMessage({ runId: 'run_legacy_local', from: 'a', to: 'pending', subject: 'second' }) db.markAsDelivered([delivered.id]) db.markAsRead([read.id]) @@ -20,7 +30,12 @@ describe('undelivered orchestration mailboxes', () => { it('persists and settles a pending pointer Enter independently of delivery', () => { db = new OrchestrationDb(':memory:') - const message = db.insertMessage({ from: 'a', to: 'run:run_1', subject: 'staged' }) + const message = db.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'run:run_1', + subject: 'staged' + }) expect( db.stageMailboxPointerEnter([message.id], { diff --git a/src/main/runtime/orchestration/db.test.ts b/src/main/runtime/orchestration/db.test.ts index 4825246586a..33af326f34d 100644 --- a/src/main/runtime/orchestration/db.test.ts +++ b/src/main/runtime/orchestration/db.test.ts @@ -4,9 +4,10 @@ import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import Database from '../../sqlite/sync-database' import { LEGACY_RUN_ID, OrchestrationDb } from './db' -import type { MessageType } from './db' import { createRootDispatch } from './db/root-dispatch-test-fixture' +const runId = 'run_legacy_local' + // Overwrites the datetime('now')-seeded timestamps with explicit fixture values // so stale-detection assertions stay deterministic (no wall clock). function setDispatchTimes( @@ -33,154 +34,10 @@ describe('OrchestrationDb', () => { return db } - describe('messages', () => { - it('inserts and retrieves a message', () => { - const d = createDb() - const msg = d.insertMessage({ - from: 'term_a', - to: 'term_b', - subject: 'hello', - body: 'world' - }) - expect(msg.id).toMatch(/^msg_/) - expect(msg.from_handle).toBe('term_a') - expect(msg.to_handle).toBe('term_b') - expect(msg.subject).toBe('hello') - expect(msg.body).toBe('world') - expect(msg.type).toBe('status') - expect(msg.priority).toBe('normal') - expect(msg.read).toBe(0) - expect(msg.sequence).toBeGreaterThan(0) - }) - - it('returns unread messages in sequence order', () => { - const d = createDb() - d.insertMessage({ from: 'a', to: 'b', subject: 'first' }) - d.insertMessage({ from: 'a', to: 'b', subject: 'second' }) - d.insertMessage({ from: 'a', to: 'c', subject: 'other' }) - - const unread = d.getUnreadMessages('b') - expect(unread).toHaveLength(2) - expect(unread[0].subject).toBe('first') - expect(unread[1].subject).toBe('second') - }) - - it('filters unread by type', () => { - const d = createDb() - d.insertMessage({ from: 'a', to: 'b', subject: 'status msg', type: 'status' }) - d.insertMessage({ from: 'a', to: 'b', subject: 'done msg', type: 'worker_done' }) - - const filtered = d.getUnreadMessages('b', ['worker_done']) - expect(filtered).toHaveLength(1) - expect(filtered[0].type).toBe('worker_done') - }) - - it('excludes already-delivered rows from getUndeliveredUnreadMessages', () => { - const d = createDb() - const m1 = d.insertMessage({ from: 'a', to: 'b', subject: 'one' }) - const m2 = d.insertMessage({ from: 'a', to: 'b', subject: 'two' }) - - d.markAsDelivered([m1.id]) - - // Push delivery query: only undelivered, unread. - const pending = d.getUndeliveredUnreadMessages('b') - expect(pending).toHaveLength(1) - expect(pending[0].id).toBe(m2.id) - - // Explicit `check` still sees both (they are still unread). - const unread = d.getUnreadMessages('b') - expect(unread).toHaveLength(2) - }) - - it('creates the undelivered inbox index used by push delivery', () => { - const d = createDb() - const sqlite = (d as unknown as { db: Database.Database }).db - - const indexes = sqlite - .prepare( - `SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'messages' AND name = 'idx_messages_undelivered_inbox'` - ) - .all() - - expect(indexes).toHaveLength(1) - }) - - it('filters getUndeliveredUnreadMessages by type', () => { - const d = createDb() - d.insertMessage({ from: 'a', to: 'b', subject: 's', type: 'status' }) - const wd = d.insertMessage({ from: 'a', to: 'b', subject: 'd', type: 'worker_done' }) - - const filtered = d.getUndeliveredUnreadMessages('b', ['worker_done']) - expect(filtered).toHaveLength(1) - expect(filtered[0].id).toBe(wd.id) - }) - - it('marks messages as read', () => { - const d = createDb() - const m1 = d.insertMessage({ from: 'a', to: 'b', subject: 'one' }) - const m2 = d.insertMessage({ from: 'a', to: 'b', subject: 'two' }) - - d.markAsRead([m1.id]) - - const unread = d.getUnreadMessages('b') - expect(unread).toHaveLength(1) - expect(unread[0].id).toBe(m2.id) - }) - - it('stores typed payload and thread_id', () => { - const d = createDb() - const payload = JSON.stringify({ taskId: 'task_abc', filesModified: ['src/a.ts'] }) - const msg = d.insertMessage({ - from: 'a', - to: 'b', - subject: 'done', - type: 'worker_done', - priority: 'high', - threadId: 'thread_1', - payload - }) - - expect(msg.type).toBe('worker_done') - expect(msg.priority).toBe('high') - expect(msg.thread_id).toBe('thread_1') - expect(msg.payload).toBe(payload) - }) - - it('rejects invalid message type', () => { - const d = createDb() - expect(() => - d.insertMessage({ - from: 'a', - to: 'b', - subject: 'bad', - type: 'invalid' as MessageType - }) - ).toThrow() - }) - - it('getInbox returns all messages across recipients', () => { - const d = createDb() - d.insertMessage({ from: 'a', to: 'b', subject: 'one' }) - d.insertMessage({ from: 'a', to: 'c', subject: 'two' }) - d.insertMessage({ from: 'b', to: 'a', subject: 'three' }) - - const inbox = d.getInbox(10) - expect(inbox).toHaveLength(3) - }) - - it('getMessageById returns the correct message', () => { - const d = createDb() - const msg = d.insertMessage({ from: 'a', to: 'b', subject: 'test' }) - const found = d.getMessageById(msg.id) - expect(found?.subject).toBe('test') - expect(d.getMessageById('msg_nonexistent')).toBeUndefined() - }) - }) - describe('tasks', () => { it('creates a task with no deps as ready', () => { const d = createDb() - const task = d.createTask({ spec: 'do something' }) + const task = d.createTask({ runId, spec: 'do something' }) expect(task.id).toMatch(/^task_/) expect(task.status).toBe('ready') expect(task.deps).toBe('[]') @@ -191,6 +48,7 @@ describe('OrchestrationDb', () => { it('persists explicit task display metadata', () => { const d = createDb() const task = d.createTask({ + runId, spec: 'full details', taskTitle: 'Checkout race', displayName: 'Fix checkout race' @@ -204,6 +62,7 @@ describe('OrchestrationDb', () => { it('persists the creating terminal handle for task-created worktrees', () => { const d = createDb() const task = d.createTask({ + runId, spec: 'spawn related workspace', createdByTerminalHandle: 'term_creator' }) @@ -214,16 +73,16 @@ describe('OrchestrationDb', () => { it('creates a task with deps as pending', () => { const d = createDb() - const parent = d.createTask({ spec: 'parent' }) - const child = d.createTask({ spec: 'child', deps: [parent.id] }) + const parent = d.createTask({ runId, spec: 'parent' }) + const child = d.createTask({ runId, spec: 'child', deps: [parent.id] }) expect(child.status).toBe('pending') expect(JSON.parse(child.deps)).toEqual([parent.id]) }) it('promotes pending tasks when deps complete', () => { const d = createDb() - const t1 = d.createTask({ spec: 'first' }) - const t2 = d.createTask({ spec: 'second', deps: [t1.id] }) + const t1 = d.createTask({ runId, spec: 'first' }) + const t2 = d.createTask({ runId, spec: 'second', deps: [t1.id] }) expect(d.getTask(t2.id)?.status).toBe('pending') @@ -234,9 +93,9 @@ describe('OrchestrationDb', () => { it('does not promote task until ALL deps complete', () => { const d = createDb() - const t1 = d.createTask({ spec: 'a' }) - const t2 = d.createTask({ spec: 'b' }) - const t3 = d.createTask({ spec: 'c', deps: [t1.id, t2.id] }) + const t1 = d.createTask({ runId, spec: 'a' }) + const t2 = d.createTask({ runId, spec: 'b' }) + const t3 = d.createTask({ runId, spec: 'c', deps: [t1.id, t2.id] }) d.updateTaskStatus(t1.id, 'completed') expect(d.getTask(t3.id)?.status).toBe('pending') @@ -247,7 +106,7 @@ describe('OrchestrationDb', () => { it('sets completed_at on completion', () => { const d = createDb() - const task = d.createTask({ spec: 'do it' }) + const task = d.createTask({ runId, spec: 'do it' }) const updated = d.updateTaskStatus(task.id, 'completed', '{"result": true}') expect(updated?.completed_at).toBeTruthy() expect(updated?.result).toBe('{"result": true}') @@ -255,7 +114,7 @@ describe('OrchestrationDb', () => { it('completing a task frees its active dispatch context', () => { const d = createDb() - const task = d.createTask({ spec: 'do it' }) + const task = d.createTask({ runId, spec: 'do it' }) createRootDispatch(d, task.id, 'term_a') d.updateTaskStatus(task.id, 'completed') @@ -266,8 +125,8 @@ describe('OrchestrationDb', () => { it('listTasks filters by status', () => { const d = createDb() - d.createTask({ spec: 'ready task' }) - const t2 = d.createTask({ spec: 'another' }) + d.createTask({ runId, spec: 'ready task' }) + const t2 = d.createTask({ runId, spec: 'another' }) d.updateTaskStatus(t2.id, 'completed') expect(d.listTasks({ status: 'ready' })).toHaveLength(1) @@ -277,15 +136,15 @@ describe('OrchestrationDb', () => { it('listTasks returns all when no filter', () => { const d = createDb() - d.createTask({ spec: 'one' }) - d.createTask({ spec: 'two' }) + d.createTask({ runId, spec: 'one' }) + d.createTask({ runId, spec: 'two' }) expect(d.listTasks()).toHaveLength(2) }) it('listTasksWithDispatch joins active dispatch metadata', () => { const d = createDb() - const ready = d.createTask({ spec: 'ready task' }) - const dispatched = d.createTask({ spec: 'active task' }) + const ready = d.createTask({ runId, spec: 'ready task' }) + const dispatched = d.createTask({ runId, spec: 'active task' }) const ctx = createRootDispatch(d, dispatched.id, 'term_worker') const rows = d.listTasksWithDispatch() @@ -300,7 +159,7 @@ describe('OrchestrationDb', () => { it('listTasksWithDispatch does not surface completed dispatches', () => { const d = createDb() - const task = d.createTask({ spec: 'work' }) + const task = d.createTask({ runId, spec: 'work' }) createRootDispatch(d, task.id, 'term_worker') d.updateTaskStatus(task.id, 'completed') @@ -314,8 +173,8 @@ describe('OrchestrationDb', () => { it('supports parent_id for task decomposition', () => { const d = createDb() - const parent = d.createTask({ spec: 'parent' }) - const child = d.createTask({ spec: 'child', parentId: parent.id }) + const parent = d.createTask({ runId, spec: 'parent' }) + const child = d.createTask({ runId, spec: 'child', parentId: parent.id }) expect(child.parent_id).toBe(parent.id) }) }) @@ -323,7 +182,7 @@ describe('OrchestrationDb', () => { describe('dispatch contexts', () => { it('creates a dispatch context and marks task as dispatched', () => { const d = createDb() - const task = d.createTask({ spec: 'work' }) + const task = d.createTask({ runId, spec: 'work' }) const ctx = createRootDispatch(d, task.id, 'term_worker') expect(ctx.id).toMatch(/^ctx_/) @@ -335,8 +194,8 @@ describe('OrchestrationDb', () => { it('rejects dispatch for non-ready tasks', () => { const d = createDb() - const parent = d.createTask({ spec: 'parent' }) - const child = d.createTask({ spec: 'child', deps: [parent.id] }) + const parent = d.createTask({ runId, spec: 'parent' }) + const child = d.createTask({ runId, spec: 'child', deps: [parent.id] }) expect(() => createRootDispatch(d, child.id, 'term_worker')).toThrow( /only ready tasks can be dispatched/ @@ -345,8 +204,8 @@ describe('OrchestrationDb', () => { it('rejects dispatch to an occupied terminal', () => { const d = createDb() - const t1 = d.createTask({ spec: 'first' }) - const t2 = d.createTask({ spec: 'second' }) + const t1 = d.createTask({ runId, spec: 'first' }) + const t2 = d.createTask({ runId, spec: 'second' }) createRootDispatch(d, t1.id, 'term_worker') expect(() => createRootDispatch(d, t2.id, 'term_worker')).toThrow( @@ -361,8 +220,8 @@ describe('OrchestrationDb', () => { it('rejects dispatch to a reminted handle on a pane with an active dispatch', () => { const d = createDb() - const t1 = d.createTask({ spec: 'first' }) - const t2 = d.createTask({ spec: 'second' }) + const t1 = d.createTask({ runId, spec: 'first' }) + const t2 = d.createTask({ runId, spec: 'second' }) createRootDispatch(d, t1.id, 'term_old', `tab_1:${LEAF_A}`) expect(() => createRootDispatch(d, t2.id, 'term_new', `tab_1:${LEAF_A}`)).toThrow( @@ -372,8 +231,8 @@ describe('OrchestrationDb', () => { it('rejects dispatch when pane keys share a leaf after break-out', () => { const d = createDb() - const t1 = d.createTask({ spec: 'first' }) - const t2 = d.createTask({ spec: 'second' }) + const t1 = d.createTask({ runId, spec: 'first' }) + const t2 = d.createTask({ runId, spec: 'second' }) createRootDispatch(d, t1.id, 'term_old', `tab_1:${LEAF_A}`) expect(() => createRootDispatch(d, t2.id, 'term_new', `tab_2:${LEAF_A}`)).toThrow( @@ -383,8 +242,8 @@ describe('OrchestrationDb', () => { it('allows concurrent dispatches to different panes', () => { const d = createDb() - const t1 = d.createTask({ spec: 'first' }) - const t2 = d.createTask({ spec: 'second' }) + const t1 = d.createTask({ runId, spec: 'first' }) + const t2 = d.createTask({ runId, spec: 'second' }) createRootDispatch(d, t1.id, 'term_a', `tab_1:${LEAF_A}`) expect(() => createRootDispatch(d, t2.id, 'term_b', `tab_1:${LEAF_B}`)).not.toThrow() @@ -392,8 +251,8 @@ describe('OrchestrationDb', () => { it('falls back to handle lock when pane keys are missing', () => { const d = createDb() - const t1 = d.createTask({ spec: 'first' }) - const t2 = d.createTask({ spec: 'second' }) + const t1 = d.createTask({ runId, spec: 'first' }) + const t2 = d.createTask({ runId, spec: 'second' }) createRootDispatch(d, t1.id, 'term_worker') // New dispatch has a pane key but the active row is legacy (no pane key): @@ -403,8 +262,8 @@ describe('OrchestrationDb', () => { it('allows dispatch to a terminal after previous dispatch completes', () => { const d = createDb() - const t1 = d.createTask({ spec: 'first' }) - const t2 = d.createTask({ spec: 'second' }) + const t1 = d.createTask({ runId, spec: 'first' }) + const t2 = d.createTask({ runId, spec: 'second' }) const ctx1 = createRootDispatch(d, t1.id, 'term_worker') d.completeDispatch(ctx1.id) @@ -414,7 +273,7 @@ describe('OrchestrationDb', () => { it('getDispatchContext returns latest for a task', () => { const d = createDb() - const task = d.createTask({ spec: 'work' }) + const task = d.createTask({ runId, spec: 'work' }) const ctx = createRootDispatch(d, task.id, 'term_a') const found = d.getDispatchContext(task.id) expect(found?.id).toBe(ctx.id) @@ -422,7 +281,7 @@ describe('OrchestrationDb', () => { it('getDispatchContext uses insertion order when timestamps tie', () => { const d = createDb() - const task = d.createTask({ spec: 'work' }) + const task = d.createTask({ runId, spec: 'work' }) const ctx1 = createRootDispatch(d, task.id, 'term_a') d.failDispatch(ctx1.id, 'retry') const ctx2 = createRootDispatch(d, task.id, 'term_a') @@ -432,7 +291,7 @@ describe('OrchestrationDb', () => { it('getActiveDispatchForTerminal returns active dispatch', () => { const d = createDb() - const task = d.createTask({ spec: 'work' }) + const task = d.createTask({ runId, spec: 'work' }) createRootDispatch(d, task.id, 'term_a') const active = d.getActiveDispatchForTerminal('term_a') @@ -442,10 +301,16 @@ describe('OrchestrationDb', () => { it('getLatestDispatchForTerminal returns the most recent completed dispatch', () => { const d = createDb() - const firstTask = d.createTask({ spec: 'first' }) + const firstTask = d.createTask({ + runId, + spec: 'first' + }) const first = createRootDispatch(d, firstTask.id, 'term_a') d.completeDispatch(first.id) - const secondTask = d.createTask({ spec: 'second' }) + const secondTask = d.createTask({ + runId, + spec: 'second' + }) const second = createRootDispatch(d, secondTask.id, 'term_a') d.completeDispatch(second.id) @@ -457,7 +322,7 @@ describe('OrchestrationDb', () => { it('circuit breaker trips after 3 failures', () => { const d = createDb() - const task = d.createTask({ spec: 'flaky' }) + const task = d.createTask({ runId, spec: 'flaky' }) const ctx = createRootDispatch(d, task.id, 'term_a') const after1 = d.failDispatch(ctx.id, 'timeout') @@ -480,7 +345,7 @@ describe('OrchestrationDb', () => { it('completeDispatch sets completed_at', () => { const d = createDb() - const task = d.createTask({ spec: 'work' }) + const task = d.createTask({ runId, spec: 'work' }) const ctx = createRootDispatch(d, task.id, 'term_a') d.completeDispatch(ctx.id) @@ -493,7 +358,10 @@ describe('OrchestrationDb', () => { describe('decision gates', () => { it('creates a gate and blocks the task', () => { const d = createDb() - const task = d.createTask({ spec: 'needs approval' }) + const task = d.createTask({ + runId, + spec: 'needs approval' + }) createRootDispatch(d, task.id, 'term_a') const gate = d.createGate({ taskId: task.id, @@ -513,7 +381,7 @@ describe('OrchestrationDb', () => { it('resolves a gate and unblocks the task', () => { const d = createDb() - const task = d.createTask({ spec: 'work' }) + const task = d.createTask({ runId, spec: 'work' }) const gate = d.createGate({ taskId: task.id, question: 'ok?' }) const resolved = d.resolveGate(gate.id, 'yes') @@ -526,7 +394,7 @@ describe('OrchestrationDb', () => { it('times out a gate', () => { const d = createDb() - const task = d.createTask({ spec: 'work' }) + const task = d.createTask({ runId, spec: 'work' }) const gate = d.createGate({ taskId: task.id, question: 'ok?' }) const timedOut = d.timeoutGate(gate.id) @@ -535,8 +403,8 @@ describe('OrchestrationDb', () => { it('lists gates with filters', () => { const d = createDb() - const t1 = d.createTask({ spec: 'a' }) - const t2 = d.createTask({ spec: 'b' }) + const t1 = d.createTask({ runId, spec: 'a' }) + const t2 = d.createTask({ runId, spec: 'b' }) d.createGate({ taskId: t1.id, question: 'q1' }) const g2 = d.createGate({ taskId: t2.id, question: 'q2' }) d.resolveGate(g2.id, 'done') @@ -599,8 +467,13 @@ describe('OrchestrationDb', () => { describe('lifecycle', () => { it('resetAll clears all tables', () => { const d = createDb() - d.insertMessage({ from: 'a', to: 'b', subject: 'test' }) - d.createTask({ spec: 'work' }) + d.insertMessage({ + runId, + from: 'a', + to: 'b', + subject: 'test' + }) + d.createTask({ runId, spec: 'work' }) d.resetAll() @@ -610,8 +483,13 @@ describe('OrchestrationDb', () => { it('resetMessages clears only messages', () => { const d = createDb() - d.insertMessage({ from: 'a', to: 'b', subject: 'test' }) - d.createTask({ spec: 'work' }) + d.insertMessage({ + runId, + from: 'a', + to: 'b', + subject: 'test' + }) + d.createTask({ runId, spec: 'work' }) d.resetMessages() @@ -621,8 +499,13 @@ describe('OrchestrationDb', () => { it('resetTasks clears tasks and dispatch contexts', () => { const d = createDb() - d.insertMessage({ from: 'a', to: 'b', subject: 'test' }) - const task = d.createTask({ spec: 'work' }) + d.insertMessage({ + runId, + from: 'a', + to: 'b', + subject: 'test' + }) + const task = d.createTask({ runId, spec: 'work' }) createRootDispatch(d, task.id, 'term_a') d.resetTasks() @@ -636,6 +519,7 @@ describe('OrchestrationDb', () => { it('insertMessage accepts type = heartbeat', () => { const d = createDb() const msg = d.insertMessage({ + runId, from: 'worker', to: 'coord', subject: 'alive', @@ -647,7 +531,7 @@ describe('OrchestrationDb', () => { it('recordHeartbeat updates last_heartbeat_at on dispatched rows', () => { const d = createDb() - const task = d.createTask({ spec: 'work' }) + const task = d.createTask({ runId, spec: 'work' }) const ctx = createRootDispatch(d, task.id, 'term_a') d.recordHeartbeat(ctx.id, '2026-05-04T00:00:00.000Z') @@ -662,10 +546,10 @@ describe('OrchestrationDb', () => { // (b) dispatched, heartbeated 12 min ago → STALE (expected result) // (c) dispatched, never heartbeated, dispatched 30s ago → not stale (grace) // (d) completed, heartbeated 30 min ago → not stale (status filter) - const taskA = d.createTask({ spec: 'a' }) - const taskB = d.createTask({ spec: 'b' }) - const taskC = d.createTask({ spec: 'c' }) - const taskD = d.createTask({ spec: 'd' }) + const taskA = d.createTask({ runId, spec: 'a' }) + const taskB = d.createTask({ runId, spec: 'b' }) + const taskC = d.createTask({ runId, spec: 'c' }) + const taskD = d.createTask({ runId, spec: 'd' }) const ctxA = createRootDispatch(d, taskA.id, 'term_a') const ctxB = createRootDispatch(d, taskB.id, 'term_b') const ctxC = createRootDispatch(d, taskC.id, 'term_c') @@ -710,15 +594,19 @@ describe('OrchestrationDb', () => { // Fresh worker: dispatched 12:00, heartbeat 12:05 (space-format), both // after the 11:55 threshold → NOT stale. - const fresh = createRootDispatch(d, d.createTask({ spec: 'fresh' }).id, 'term_fresh') + const fresh = createRootDispatch(d, d.createTask({ runId, spec: 'fresh' }).id, 'term_fresh') setDispatchTimes(d, fresh.id, '2026-07-12 12:00:00', '2026-07-12 12:05:00') // Legacy ISO-format fresh row (mixed-format table) stays fresh too. - const legacy = createRootDispatch(d, d.createTask({ spec: 'legacy' }).id, 'term_legacy') + const legacy = createRootDispatch( + d, + d.createTask({ runId, spec: 'legacy' }).id, + 'term_legacy' + ) setDispatchTimes(d, legacy.id, '2026-07-12T12:00:00.000Z', '2026-07-12T12:05:00.000Z') // Genuinely hung: dispatched + heartbeated at 10:00, ~2h before threshold. - const hung = createRootDispatch(d, d.createTask({ spec: 'hung' }).id, 'term_hung') + const hung = createRootDispatch(d, d.createTask({ runId, spec: 'hung' }).id, 'term_hung') setDispatchTimes(d, hung.id, '2026-07-12 10:00:00', '2026-07-12 10:00:00') const stale = d.getStaleDispatches('2026-07-12T11:55:00.000Z') @@ -730,7 +618,7 @@ describe('OrchestrationDb', () => { // Space-format dispatched_at one minute after the threshold, no heartbeat // yet → still inside the grace window, must not be flagged. - const ctx = createRootDispatch(d, d.createTask({ spec: 'x' }).id, 'term_x') + const ctx = createRootDispatch(d, d.createTask({ runId, spec: 'x' }).id, 'term_x') setDispatchTimes(d, ctx.id, '2026-07-12 12:00:00') const stale = d.getStaleDispatches('2026-07-12T11:59:00.000Z') @@ -742,7 +630,11 @@ describe('OrchestrationDb', () => { it('getStaleDispatches keeps a fresh row just after a UTC-midnight threshold (#8452)', () => { const d = createDb() - const ctx = createRootDispatch(d, d.createTask({ spec: 'midnight' }).id, 'term_midnight') + const ctx = createRootDispatch( + d, + d.createTask({ runId, spec: 'midnight' }).id, + 'term_midnight' + ) setDispatchTimes(d, ctx.id, '2026-05-04 00:04:00') const stale = d.getStaleDispatches('2026-05-04T00:00:00.000Z') @@ -755,7 +647,7 @@ describe('OrchestrationDb', () => { it('getStaleDispatches keeps a live worker with a fresh space-format heartbeat (#8452)', () => { const d = createDb() - const ctx = createRootDispatch(d, d.createTask({ spec: 'live' }).id, 'term_live') + const ctx = createRootDispatch(d, d.createTask({ runId, spec: 'live' }).id, 'term_live') setDispatchTimes(d, ctx.id, '2026-07-12 10:00:00', '2026-07-12 11:59:00') const stale = d.getStaleDispatches('2026-07-12T11:55:00.000Z') @@ -765,6 +657,7 @@ describe('OrchestrationDb', () => { it('getThreadMessagesFor returns only same-thread replies to a handle', () => { const d = createDb() const outbound = d.insertMessage({ + runId, from: 'worker', to: 'coord', subject: 'Question', @@ -773,6 +666,7 @@ describe('OrchestrationDb', () => { }) // Reply in the same thread addressed to the worker const reply = d.insertMessage({ + runId, from: 'coord', to: 'worker', subject: 'Re: Question', @@ -781,6 +675,7 @@ describe('OrchestrationDb', () => { }) // Distractor: different thread, same recipient d.insertMessage({ + runId, from: 'coord', to: 'worker', subject: 'other', @@ -789,6 +684,7 @@ describe('OrchestrationDb', () => { }) // Distractor: same thread but not addressed to worker d.insertMessage({ + runId, from: 'coord', to: 'someone_else', subject: 'cc', @@ -902,6 +798,7 @@ describe('OrchestrationDb', () => { // (a) INSERT type='heartbeat' now succeeds expect(() => d.insertMessage({ + runId, from: 'w', to: 'c', subject: 'alive', @@ -911,7 +808,7 @@ describe('OrchestrationDb', () => { ).not.toThrow() // (b) last_heartbeat_at column exists on dispatch_contexts - const task = d.createTask({ spec: 'work' }) + const task = d.createTask({ runId, spec: 'work' }) const ctx = createRootDispatch(d, task.id, 'term_a') d.recordHeartbeat(ctx.id, '2026-05-04T00:00:00.000Z') expect(d.getDispatchContext(task.id)?.last_heartbeat_at).toBe('2026-05-04T00:00:00.000Z') @@ -942,11 +839,12 @@ describe('OrchestrationDb', () => { const d = new OrchestrationDb(path) db = d - const task = d.createTask({ spec: 'work' }) + const task = d.createTask({ runId, spec: 'work' }) const ctx = createRootDispatch(d, task.id, 'term_a', 'tab_1:leaf_1') expect(d.getDispatchContextById(ctx.id)?.assignee_pane_key).toBe('tab_1:leaf_1') const msg = d.insertMessage({ + runId, from: 'w', to: 'c', subject: 'done', @@ -960,6 +858,7 @@ describe('OrchestrationDb', () => { const path = createV1Snapshot() const first = new OrchestrationDb(path) first.insertMessage({ + runId, from: 'w', to: 'c', subject: 'alive', @@ -972,6 +871,7 @@ describe('OrchestrationDb', () => { db = second expect(() => second.insertMessage({ + runId, from: 'w', to: 'c', subject: 'again', diff --git a/src/main/runtime/orchestration/db/attempt-outcome-projection.test.ts b/src/main/runtime/orchestration/db/attempt-outcome-projection.test.ts index eae300a7b20..a8dc098728a 100644 --- a/src/main/runtime/orchestration/db/attempt-outcome-projection.test.ts +++ b/src/main/runtime/orchestration/db/attempt-outcome-projection.test.ts @@ -48,7 +48,7 @@ describe('durable Attempt observation and outcome projection', () => { function createAttempt(): { taskId: string; dispatchId: string } { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'observe outcome' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'observe outcome' }) const dispatch = createRootDispatch(db, task.id, 'term_observed') return { taskId: task.id, dispatchId: dispatch.id } } @@ -162,7 +162,10 @@ describe('durable Attempt observation and outcome projection', () => { const path = join(dir, 'orchestration.sqlite') try { db = new OrchestrationDb(path) - const task = db.createTask({ spec: 'durable observation' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'durable observation' + }) const dispatch = createRootDispatch(db, task.id, 'term_durable') db.recordAttemptObservation( fact(dispatch.id, { @@ -189,7 +192,10 @@ describe('durable Attempt observation and outcome projection', () => { it('keeps worker_done settlement as the atomic success fast path', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'worker_done fast path' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'worker_done fast path' + }) const started = db.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, diff --git a/src/main/runtime/orchestration/db/contract-constants.ts b/src/main/runtime/orchestration/db/contract-constants.ts index 287ce565d5b..47e0cd6165a 100644 --- a/src/main/runtime/orchestration/db/contract-constants.ts +++ b/src/main/runtime/orchestration/db/contract-constants.ts @@ -7,4 +7,4 @@ export const LEGACY_CONTRACT_VERSION = 0 export const CURRENT_CONTRACT_VERSION = ORCHESTRATION_CONTRACT_VERSION // Schema versions: v2 'heartbeat'+last_heartbeat_at, v3 delivered_at, v4 task-creator terminal, v5 task_title/display_name, v6 pane identity, v7 lightweight Runs, v8 crash-safe Run deliveries, v9 durable question threads, v10 Dispatch capabilities, v11 durable mutation receipts, v12 composed worker state, v18 post-v6 version-skew repair, v19 adopted legacy Runs and compatibility receipts, v20 legacy question backfill, v21 legacy scheduler-loss provenance, v22 dispatch assignee lookup, v23 worker terminal resource ownership, v24 creator-incarnation authority, v25 active Dispatch handle lookup, v26 indexed mutation receipt capacity, v27 durable federation acknowledgments, v28 durable local mutation caller identity, v31 dispatch/resource identity links, v32 bounded worker-terminal recovery metadata, v33 durable mailbox pointer Enter state, v34 role-addressed mailbox deliveries, v35 mailbox delivery default and index-predicate repair, v36 dispatch mailbox consumer generation, v37 recorded dispatch creator identity, v39 structured session journal archives. -export const SCHEMA_VERSION = 39 +export const SCHEMA_VERSION = 40 diff --git a/src/main/runtime/orchestration/db/decision-gate-lifecycle.test.ts b/src/main/runtime/orchestration/db/decision-gate-lifecycle.test.ts index 219cf6fe212..9fdc9d9b5fb 100644 --- a/src/main/runtime/orchestration/db/decision-gate-lifecycle.test.ts +++ b/src/main/runtime/orchestration/db/decision-gate-lifecycle.test.ts @@ -9,7 +9,7 @@ describe('decision-gate lifecycle transitions', () => { it('blocks the dispatched Task when creating a gate', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'gate blocks task' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'gate blocks task' }) createRootDispatch(db, task.id, 'term_gate') expect(db.getTask(task.id)?.status).toBe('dispatched') @@ -20,7 +20,7 @@ describe('decision-gate lifecycle transitions', () => { it('rolls back the gate row when the Task transition cannot commit', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'atomic gate creation' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'atomic gate creation' }) const dispatch = createRootDispatch(db, task.id, 'term_gate') db.db.exec(` CREATE TRIGGER reject_gate_task_block diff --git a/src/main/runtime/orchestration/db/decision-gates/decision-gate-store.ts b/src/main/runtime/orchestration/db/decision-gates/decision-gate-store.ts index 532fa52b22a..296bcea42bc 100644 --- a/src/main/runtime/orchestration/db/decision-gates/decision-gate-store.ts +++ b/src/main/runtime/orchestration/db/decision-gates/decision-gate-store.ts @@ -1,6 +1,5 @@ import type { DecisionGateRow, DispatchContextRow, GateStatus } from '../../types' import { OrchestrationError } from '../../orchestration-error' -import { LEGACY_RUN_ID } from '../contract-constants' import { generateId } from '../generated-id' import type { OrchestrationDb } from '../orchestration-db' import { transitionLifecycleWithDb } from '../lifecycle-transition' @@ -18,6 +17,16 @@ export function createGate( ): DecisionGateRow { this.db.exec('SAVEPOINT create_gate') try { + const task = this.getTask(gate.taskId) + if (!task) { + throw new OrchestrationError( + 'lifecycle_not_found', + `Task ${gate.taskId} was not found while creating a decision gate.`, + { taskId: gate.taskId } + ) + } + const runId = task.run_id + this.requireRun(runId) const active = this.db .prepare( `SELECT * FROM dispatch_contexts @@ -65,22 +74,8 @@ export function createGate( .prepare( 'INSERT INTO decision_gates (id, run_id, task_id, question, options) VALUES (?, ?, ?, ?, ?)' ) - .run( - id, - this.getTask(gate.taskId)?.run_id ?? LEGACY_RUN_ID, - gate.taskId, - gate.question, - optionsJson - ) + .run(id, runId, gate.taskId, gate.question, optionsJson) this.completeActiveDispatchesForTask(gate.taskId) - const task = this.getTask(gate.taskId) - if (!task) { - throw new OrchestrationError( - 'lifecycle_not_found', - `Task ${gate.taskId} was not found while creating a decision gate.`, - { taskId: gate.taskId } - ) - } transitionLifecycleWithDb(this.db, { entity: 'task', id: gate.taskId, diff --git a/src/main/runtime/orchestration/db/dispatch-depth.test.ts b/src/main/runtime/orchestration/db/dispatch-depth.test.ts index 97df3f01c51..013cedffe91 100644 --- a/src/main/runtime/orchestration/db/dispatch-depth.test.ts +++ b/src/main/runtime/orchestration/db/dispatch-depth.test.ts @@ -16,7 +16,7 @@ describe('nested worker depth', () => { function coordinatorDispatchesWorker(maxDepth = UNCAPPED) { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'root task' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'root task' }) const worker = db.createDispatchContext({ taskId: task.id, assigneeHandle: 'term_worker', @@ -33,7 +33,7 @@ describe('nested worker depth', () => { it('refuses a worker dispatching a sub-worker at the default cap', () => { coordinatorDispatchesWorker() - const nested = db.createTask({ spec: 'nested task' }) + const nested = db.createTask({ runId: 'run_legacy_local', spec: 'nested task' }) expect(() => db.createDispatchContext({ taskId: nested.id, @@ -51,7 +51,7 @@ describe('nested worker depth', () => { it('tells the refused worker to complete the task itself', () => { coordinatorDispatchesWorker() - const nested = db.createTask({ spec: 'nested task' }) + const nested = db.createTask({ runId: 'run_legacy_local', spec: 'nested task' }) expect(() => db.createDispatchContext({ taskId: nested.id, @@ -64,7 +64,7 @@ describe('nested worker depth', () => { it('permits one more generation when the cap is raised, and records depth 2', () => { coordinatorDispatchesWorker() - const nested = db.createTask({ spec: 'nested task' }) + const nested = db.createTask({ runId: 'run_legacy_local', spec: 'nested task' }) const sub = db.createDispatchContext({ taskId: nested.id, assigneeHandle: 'term_sub', @@ -113,9 +113,9 @@ describe('nested worker depth', () => { db.db .prepare( `INSERT INTO remote_dispatch_attachments - (dispatch_id, task_id, home_peer_fingerprint, protocol_version, runtime_epoch, + (dispatch_id, task_id, home_run_id, home_peer_fingerprint, protocol_version, runtime_epoch, pane_key, process_incarnation, state, depth) - VALUES (?, ?, 'peer', 1, 'epoch', ?, ?, ?, ?)` + VALUES (?, ?, 'run_home', 'peer', 1, 'epoch', ?, ?, ?, ?)` ) .run(`ctx_${state}_${depth}_${paneKey}_${inc}`, 'task_remote', paneKey, inc, state, depth) } @@ -182,7 +182,10 @@ describe('nested worker depth', () => { it('takes the maximum when a process holds both a local and a remote role', () => { // Query order must not decide the answer: the deeper role governs. db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'local role' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'local role' + }) db.createDispatchContext({ taskId: task.id, assigneeHandle: 'term_both', @@ -220,13 +223,19 @@ describe('nested worker depth', () => { it('stamps depth 1 for a root coordinator', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'root work' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'root work' + }) expect(startWorker(task.id, SYSTEM, UNCAPPED).dispatch.depth).toBe(1) }) it('refuses a worker starting a sub-worker at the default cap', () => { coordinatorDispatchesWorker() - const nested = db.createTask({ spec: 'nested work' }) + const nested = db.createTask({ + runId: 'run_legacy_local', + spec: 'nested work' + }) expect(() => startWorker( nested.id, @@ -238,7 +247,10 @@ describe('nested worker depth', () => { it('refuses a worker retrying into a sub-worker at the default cap', () => { coordinatorDispatchesWorker() - const nested = db.createTask({ spec: 'nested retry work' }) + const nested = db.createTask({ + runId: 'run_legacy_local', + spec: 'nested retry work' + }) const first = startWorker(nested.id, SYSTEM, UNCAPPED) db.failWorkerStart(first.dispatch.id, 'accepted', 'first attempt failed') expect(() => @@ -257,7 +269,10 @@ describe('nested worker depth', () => { // Context-only dispatch stores null on purpose; requiring an incarnation // locally would silently drop real parents and fail open. db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'context only' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'context only' + }) const row = db.createDispatchContext({ taskId: task.id, assigneeHandle: 'term_ctx', diff --git a/src/main/runtime/orchestration/db/dispatch-mailbox-consumer-fencing.test.ts b/src/main/runtime/orchestration/db/dispatch-mailbox-consumer-fencing.test.ts index c7d287e8bdb..29a2e468ee8 100644 --- a/src/main/runtime/orchestration/db/dispatch-mailbox-consumer-fencing.test.ts +++ b/src/main/runtime/orchestration/db/dispatch-mailbox-consumer-fencing.test.ts @@ -22,7 +22,7 @@ describe('dispatch mailbox consumer fencing', () => { afterEach(() => db.close()) function dispatchWithMail(subjects: string[]): { id: string; runId: string } { - const task = db.createTask({ spec: 'fenced worker work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'fenced worker work' }) const dispatch = createRootDispatch(db, task.id, 'term_worker', PANE_A) for (const subject of subjects) { db.insertMessage({ @@ -116,7 +116,10 @@ describe('dispatch mailbox consumer fencing', () => { }) it('bumps and fences on the worker-start attach path', () => { - const task = db.createTask({ spec: 'worker-start attach' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'worker-start attach' + }) const started = db.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -149,6 +152,7 @@ describe('dispatch mailbox consumer fencing', () => { it('gives a federated attachment its own generation on the worker host', () => { const dispatchId = 'ctx_remote_fence' db.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId, taskId: 'task_remote', homePeerFingerprint: 'home-peer', @@ -187,7 +191,10 @@ describe('dispatch mailbox consumer fencing', () => { }) it('starts a retry Dispatch on a fresh mailbox address rather than sharing the old one', () => { - const task = db.createTask({ spec: 'work that fails once' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'work that fails once' + }) const first = db.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, diff --git a/src/main/runtime/orchestration/db/dispatch-row-writer.ts b/src/main/runtime/orchestration/db/dispatch-row-writer.ts index 807814a87b1..84862ee343d 100644 --- a/src/main/runtime/orchestration/db/dispatch-row-writer.ts +++ b/src/main/runtime/orchestration/db/dispatch-row-writer.ts @@ -49,8 +49,8 @@ const STARTING_DISPATCH_CONTEXT_SQL = `INSERT INTO dispatch_contexts ( ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', datetime('now'))` const REMOTE_DISPATCH_ATTACHMENT_SQL = `INSERT INTO remote_dispatch_attachments ( - dispatch_id, task_id, home_peer_fingerprint, protocol_version, runtime_epoch, depth - ) VALUES (?, ?, ?, ?, ?, ?)` + dispatch_id, home_run_id, task_id, home_peer_fingerprint, protocol_version, runtime_epoch, depth + ) VALUES (?, ?, ?, ?, ?, ?, ?)` /** Last line of defence: a row that reached here unstamped would read as a root. */ function assertStampedDepth(depth: number): void { @@ -140,6 +140,7 @@ export function insertRemoteDispatchAttachmentRow( db: Database.Database, params: { dispatchId: string + runId: string taskId: string homePeerFingerprint: string protocolVersion: number @@ -151,6 +152,7 @@ export function insertRemoteDispatchAttachmentRow( assertStampedDepth(params.depth) db.prepare(REMOTE_DISPATCH_ATTACHMENT_SQL).run( params.dispatchId, + params.runId, params.taskId, params.homePeerFingerprint, params.protocolVersion, diff --git a/src/main/runtime/orchestration/db/federation/federated-dispatch-observation-fence.test.ts b/src/main/runtime/orchestration/db/federation/federated-dispatch-observation-fence.test.ts index e32bf27a00c..ecc3ca5e2c0 100644 --- a/src/main/runtime/orchestration/db/federation/federated-dispatch-observation-fence.test.ts +++ b/src/main/runtime/orchestration/db/federation/federated-dispatch-observation-fence.test.ts @@ -8,7 +8,10 @@ describe('federated Dispatch observation fence', () => { it('rejects out-of-order epochs and observations captured before release', () => { const database = (db = new OrchestrationDb(':memory:')) - const task = database.createTask({ spec: 'fenced federated observation' }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: 'fenced federated observation' + }) const started = database.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, diff --git a/src/main/runtime/orchestration/db/federation/remote-dispatch-attachment-create.ts b/src/main/runtime/orchestration/db/federation/remote-dispatch-attachment-create.ts index 56d26ccfe3b..d927cf96838 100644 --- a/src/main/runtime/orchestration/db/federation/remote-dispatch-attachment-create.ts +++ b/src/main/runtime/orchestration/db/federation/remote-dispatch-attachment-create.ts @@ -8,6 +8,7 @@ export function createRemoteDispatchAttachment( this: OrchestrationDb, params: { dispatchId: string + runId: string taskId: string homePeerFingerprint: string protocolVersion: number @@ -43,6 +44,16 @@ export function createRemoteDispatchAttachment( `Remote attachment request ${params.mutationReceipt.requestId} already exists.` ) } + if (!params.runId?.trim()) { + throw new OrchestrationError('invalid_argument', 'Missing Run ID') + } + this.db + .prepare( + `INSERT OR IGNORE INTO runs (id, objective, home_database, consumer_generation, legacy) + VALUES (?, ?, 'remote', 0, 0)` + ) + .run(params.runId, `Coordinated from ${params.homePeerFingerprint}`) + this.requireRun(params.runId) ensureMutationReceiptCapacity(this.db) this.db .prepare( @@ -59,6 +70,7 @@ export function createRemoteDispatchAttachment( ) insertRemoteDispatchAttachmentRow(this.db, { dispatchId: params.dispatchId, + runId: params.runId, taskId: params.taskId, homePeerFingerprint: params.homePeerFingerprint, protocolVersion: params.protocolVersion, diff --git a/src/main/runtime/orchestration/db/federation/remote-dispatch-attachment-release.test.ts b/src/main/runtime/orchestration/db/federation/remote-dispatch-attachment-release.test.ts index ab515ffb30c..0865d4b8972 100644 --- a/src/main/runtime/orchestration/db/federation/remote-dispatch-attachment-release.test.ts +++ b/src/main/runtime/orchestration/db/federation/remote-dispatch-attachment-release.test.ts @@ -16,6 +16,7 @@ describe('the remote attachment release guard', () => { function settledAttachment(dispatchId: string): void { db.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId, taskId: `task_${dispatchId}`, homePeerFingerprint: 'home-peer', diff --git a/src/main/runtime/orchestration/db/lifecycle-transition.test.ts b/src/main/runtime/orchestration/db/lifecycle-transition.test.ts index eed4332879f..936208e77ef 100644 --- a/src/main/runtime/orchestration/db/lifecycle-transition.test.ts +++ b/src/main/runtime/orchestration/db/lifecycle-transition.test.ts @@ -8,7 +8,7 @@ describe('guarded lifecycle transitions', () => { it('rejects a stale prior state without changing the projection', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'guarded transition' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'guarded transition' }) expect(() => db!.transitionLifecycle({ @@ -23,7 +23,7 @@ describe('guarded lifecycle transitions', () => { it('composes its projection into the caller-owned transaction', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'caller-owned rollback' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'caller-owned rollback' }) db.db.exec('SAVEPOINT lifecycle_test') expect( @@ -49,7 +49,7 @@ describe('guarded lifecycle transitions', () => { ['completed', 'blocked'] ] as const)('preserves public task updates from %s to %s', (from, to) => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'manual status correction' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'manual status correction' }) db.db.prepare('UPDATE tasks SET status = ? WHERE id = ?').run(from, task.id) expect(db.updateTaskStatus(task.id, to)?.status).toBe(to) diff --git a/src/main/runtime/orchestration/db/messages/message-insert.ts b/src/main/runtime/orchestration/db/messages/message-insert.ts index 2984545a09b..82573305479 100644 --- a/src/main/runtime/orchestration/db/messages/message-insert.ts +++ b/src/main/runtime/orchestration/db/messages/message-insert.ts @@ -1,5 +1,4 @@ import type { MessageType, MessagePriority, MessageDeliveryContract, MessageRow } from '../../types' -import { LEGACY_RUN_ID } from '../contract-constants' import { generateId } from '../generated-id' import { exposeMessageTimestamps } from '../utc-timestamp' import type { OrchestrationDb } from '../orchestration-db' @@ -26,7 +25,10 @@ export type MessageInsert = { } export function insertMessage(this: OrchestrationDb, msg: MessageInsert): MessageRow { - const runId = msg.runId ?? LEGACY_RUN_ID + const runId = msg.runId + if (!runId) { + throw new Error('Run is required') + } const deliveryContract = msg.deliveryContract ?? 'current_delivery' this.requireRun(runId) const id = msg.id ?? generateId('msg') diff --git a/src/main/runtime/orchestration/db/schema/create-graph-tables-sql.ts b/src/main/runtime/orchestration/db/schema/create-graph-tables-sql.ts index 5aed50eb7f9..0897cb852de 100644 --- a/src/main/runtime/orchestration/db/schema/create-graph-tables-sql.ts +++ b/src/main/runtime/orchestration/db/schema/create-graph-tables-sql.ts @@ -38,6 +38,7 @@ CREATE TABLE IF NOT EXISTS federated_dispatches ( ); CREATE TABLE IF NOT EXISTS remote_dispatch_attachments ( + home_run_id TEXT NOT NULL, dispatch_id TEXT PRIMARY KEY, task_id TEXT NOT NULL, home_peer_fingerprint TEXT NOT NULL, diff --git a/src/main/runtime/orchestration/db/schema/federated-home-run-migration.test.ts b/src/main/runtime/orchestration/db/schema/federated-home-run-migration.test.ts new file mode 100644 index 00000000000..b970435223a --- /dev/null +++ b/src/main/runtime/orchestration/db/schema/federated-home-run-migration.test.ts @@ -0,0 +1,26 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { OrchestrationDb } from '../orchestration-db' +import { migrateV40 } from './migrate-v40' +import { importFederatedControlMessage } from '../../federation-control-message' + +describe('federated home Run migration', () => { + const db = new OrchestrationDb(':memory:') + afterEach(() => db.close()) + + it('adds the home Run column and refuses mail for a development placeholder', () => { + db.db.exec('ALTER TABLE remote_dispatch_attachments DROP COLUMN home_run_id') + db.db.exec(`INSERT INTO remote_dispatch_attachments + (dispatch_id, task_id, home_peer_fingerprint, runtime_epoch) + VALUES ('ctx_old', 'task_old', 'home', 'epoch')`) + migrateV40.call(db, 39) + expect(db.getRemoteDispatchAttachment('ctx_old')?.home_run_id).toBe('') + expect(() => + importFederatedControlMessage(db, { + dispatchId: 'ctx_old', + messageId: 'message_old', + payload: JSON.stringify({ from: 'home', subject: 'Instruction', body: '', type: 'message' }) + }) + ).toThrow('Run not found:') + expect(db.getMessageById('message_old')).toBeUndefined() + }) +}) diff --git a/src/main/runtime/orchestration/db/schema/migrate-v40.ts b/src/main/runtime/orchestration/db/schema/migrate-v40.ts new file mode 100644 index 00000000000..50ef46f82cc --- /dev/null +++ b/src/main/runtime/orchestration/db/schema/migrate-v40.ts @@ -0,0 +1,11 @@ +import type { OrchestrationDb } from '../orchestration-db' + +export function migrateV40(this: OrchestrationDb, current: number): void { + if (current >= 40 || this.hasColumn('remote_dispatch_attachments', 'home_run_id')) { + return + } + // Federation is unreleased; any development-only rows fail Run validation until reattached. + this.db.exec( + "ALTER TABLE remote_dispatch_attachments ADD COLUMN home_run_id TEXT NOT NULL DEFAULT ''" + ) +} diff --git a/src/main/runtime/orchestration/db/schema/migrate.ts b/src/main/runtime/orchestration/db/schema/migrate.ts index b8da910722d..582dedf4752 100644 --- a/src/main/runtime/orchestration/db/schema/migrate.ts +++ b/src/main/runtime/orchestration/db/schema/migrate.ts @@ -10,6 +10,7 @@ import { migrateV36 } from './migrate-v36' import { migrateV37 } from './migrate-v37' import { migrateV38 } from './migrate-v38' import { migrateV39 } from './migrate-v39' +import { migrateV40 } from './migrate-v40' // Why: CREATE TABLE IF NOT EXISTS won't alter existing DBs; migrate in a txn that bumps user_version only on success (atomic all-or-nothing). export function migrate(this: OrchestrationDb): void { @@ -30,6 +31,7 @@ export function migrate(this: OrchestrationDb): void { migrateV37.call(this, current) migrateV38.call(this, current) migrateV39.call(this, current) + migrateV40.call(this, current) this.createMailboxDeliveryIndexesIfPossible() this.db.pragma(`user_version = ${SCHEMA_VERSION}`) this.db.exec('COMMIT') diff --git a/src/main/runtime/orchestration/db/tasks/task-store.ts b/src/main/runtime/orchestration/db/tasks/task-store.ts index 4ad3e8e3ffa..af43dc2be12 100644 --- a/src/main/runtime/orchestration/db/tasks/task-store.ts +++ b/src/main/runtime/orchestration/db/tasks/task-store.ts @@ -1,7 +1,6 @@ import type Database from '../../../../sqlite/sync-database' import type { TaskStatus, TaskRow } from '../../types' import { buildOrchestrationTaskDisplayMetadata } from '../../../../../shared/orchestration-task-display' -import { LEGACY_RUN_ID } from '../contract-constants' import { generateId } from '../generated-id' import type { TaskRuntimeLineageRow } from '../run-list-page' import type { OrchestrationDb } from '../orchestration-db' @@ -25,7 +24,10 @@ export function createTask( runId?: string } ): TaskRow { - const runId = task.runId ?? LEGACY_RUN_ID + const runId = task.runId + if (!runId) { + throw new Error('Run is required') + } this.requireRun(runId) if (task.parentId) { const parent = this.getTask(task.parentId) diff --git a/src/main/runtime/orchestration/db/writer-run-required.test.ts b/src/main/runtime/orchestration/db/writer-run-required.test.ts new file mode 100644 index 00000000000..21fcbeb28d6 --- /dev/null +++ b/src/main/runtime/orchestration/db/writer-run-required.test.ts @@ -0,0 +1,40 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { OrchestrationDb } from './orchestration-db' + +describe('writers require a Run', () => { + let db: OrchestrationDb + beforeEach(() => { + db = new OrchestrationDb(':memory:') + }) + afterEach(() => db.close()) + + it('rejects a message without a Run instead of using the legacy Run', () => { + expect(() => db.insertMessage({ from: 'sender', to: 'worker', subject: 'mail' })).toThrow( + 'Run is required' + ) + expect(db.db.prepare('SELECT id FROM messages').all()).toEqual([]) + }) + + it('rejects a Task without a Run instead of using the legacy Run', () => { + expect(() => db.createTask({ spec: 'work' })).toThrow('Run is required') + expect(db.listTasks()).toEqual([]) + }) + + it('rejects a decision gate whose Task has no Run', () => { + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) + vi.spyOn(db, 'getTask').mockReturnValue({ ...task, run_id: undefined } as never) + expect(() => db.createGate({ taskId: task.id, question: 'Proceed?' })).toThrow() + expect(db.listGates()).toEqual([]) + }) + + it('rejects a decision gate without a Task before writing', () => { + db.db.exec(` + CREATE TRIGGER reject_gate_insert BEFORE INSERT ON decision_gates + BEGIN SELECT RAISE(ABORT, 'gate insert reached'); END; + `) + expect(() => db.createGate({ taskId: 'missing', question: 'Proceed?' })).toThrow( + 'Task missing was not found while creating a decision gate.' + ) + expect(db.listGates()).toEqual([]) + }) +}) diff --git a/src/main/runtime/orchestration/dispatch-failure-idempotency.test.ts b/src/main/runtime/orchestration/dispatch-failure-idempotency.test.ts index c6f75723cf3..5704db1490e 100644 --- a/src/main/runtime/orchestration/dispatch-failure-idempotency.test.ts +++ b/src/main/runtime/orchestration/dispatch-failure-idempotency.test.ts @@ -6,7 +6,7 @@ import { createRootDispatch } from './db/root-dispatch-test-fixture' describe('dispatch failure idempotency', () => { it('counts an active dispatch failure only once', () => { const db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_worker') expect(db.failDispatch(dispatch.id, 'exit')?.failure_count).toBe(1) @@ -19,7 +19,7 @@ describe('dispatch failure idempotency', () => { it('does not overwrite a completed dispatch', () => { const db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_worker') db.completeDispatch(dispatch.id) @@ -33,7 +33,7 @@ describe('dispatch failure idempotency', () => { it('rolls back the dispatch when the task update fails', () => { const db = new OrchestrationDb(':memory:') const sqlite = (db as unknown as { db: Database.Database }).db - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_worker') sqlite.exec(` CREATE TRIGGER reject_task_failure_update diff --git a/src/main/runtime/orchestration/failed-start-terminal-adoption.test.ts b/src/main/runtime/orchestration/failed-start-terminal-adoption.test.ts index d38248f9cb8..b980a7f2a25 100644 --- a/src/main/runtime/orchestration/failed-start-terminal-adoption.test.ts +++ b/src/main/runtime/orchestration/failed-start-terminal-adoption.test.ts @@ -17,7 +17,7 @@ describe('a start that fails before authority still owns the terminal it created adoption?: Parameters[3] ): { db: OrchestrationDb; dispatchId: string } { const d = (db = new OrchestrationDb(':memory:')) - const task = d.createTask({ spec: 'residual terminal' }) + const task = d.createTask({ runId: 'run_legacy_local', spec: 'residual terminal' }) const started = d.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -122,7 +122,7 @@ describe('a start that fails before authority still owns the terminal it created const first = d.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, - taskId: d.createTask({ spec: 'owner' }).id, + taskId: d.createTask({ runId: 'run_legacy_local', spec: 'owner' }).id, startOptions: {} }) d.prepareStartingWorkerAuthority({ @@ -138,7 +138,7 @@ describe('a start that fails before authority still owns the terminal it created const second = d.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, - taskId: d.createTask({ spec: 'claimant' }).id, + taskId: d.createTask({ runId: 'run_legacy_local', spec: 'claimant' }).id, startOptions: {} }) d.recordWorkerStage({ diff --git a/src/main/runtime/orchestration/federation-acknowledgment-integrity.test.ts b/src/main/runtime/orchestration/federation-acknowledgment-integrity.test.ts index fd8e32d1a32..e5c452f91a1 100644 --- a/src/main/runtime/orchestration/federation-acknowledgment-integrity.test.ts +++ b/src/main/runtime/orchestration/federation-acknowledgment-integrity.test.ts @@ -14,6 +14,7 @@ describe('federation acknowledgment integrity', () => { db = new OrchestrationDb(':memory:') const dispatchId = `ctx_protocol_${protocolVersion}` db.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId, taskId: `task_protocol_${protocolVersion}`, homePeerFingerprint: 'home_peer', diff --git a/src/main/runtime/orchestration/federation-control-message.ts b/src/main/runtime/orchestration/federation-control-message.ts index bcab04f99b8..2d86ef3c67b 100644 --- a/src/main/runtime/orchestration/federation-control-message.ts +++ b/src/main/runtime/orchestration/federation-control-message.ts @@ -58,11 +58,20 @@ export function importFederatedControlMessage( payload: string } ): { imported: boolean; type: MessageType } { + const attachment = db.getRemoteDispatchAttachment(params.dispatchId) + if (!attachment) { + throw new OrchestrationError( + 'dispatch_not_found', + `Remote Dispatch ${params.dispatchId} was not found.` + ) + } + db.requireRun(attachment.home_run_id) const message = parseFederatedControlMessage(params.payload) const recipient = `dispatch:${params.dispatchId}` const existing = db.getMessageById(params.messageId) if (existing) { if ( + existing.run_id !== attachment.home_run_id || existing.to_handle !== recipient || existing.from_handle !== message.from || existing.subject !== message.subject || @@ -81,6 +90,7 @@ export function importFederatedControlMessage( } db.insertMessage({ id: params.messageId, + runId: attachment.home_run_id, from: message.from, to: recipient, subject: message.subject, diff --git a/src/main/runtime/orchestration/lifecycle-caller-edges.test.ts b/src/main/runtime/orchestration/lifecycle-caller-edges.test.ts index 3bc2eee4ae9..7f445388a25 100644 --- a/src/main/runtime/orchestration/lifecycle-caller-edges.test.ts +++ b/src/main/runtime/orchestration/lifecycle-caller-edges.test.ts @@ -109,7 +109,10 @@ describe('lifecycle graph against its callers', () => { it('settles a stopping worker whose PTY exits during the stop', () => { const database = createDatabase() - const task = database.createTask({ spec: 'stopping exited worker' }) + const task = database.createTask({ + runId: 'run_legacy_local', + spec: 'stopping exited worker' + }) const dispatchId = startWorker(database, task.id, 'stopping_exited') expect(database.beginWorkerStop(dispatchId, 'runtime_test').disposition).toBe('stopping') @@ -127,9 +130,18 @@ describe('lifecycle graph against its callers', () => { it('still lets a coordinator reopen or overturn a settled Task', () => { const database = createDatabase() - const reopened = database.createTask({ spec: 'reopen me' }) - const overturned = database.createTask({ spec: 'overturn me' }) - const retried = database.createTask({ spec: 'retry me' }) + const reopened = database.createTask({ + runId: 'run_legacy_local', + spec: 'reopen me' + }) + const overturned = database.createTask({ + runId: 'run_legacy_local', + spec: 'overturn me' + }) + const retried = database.createTask({ + runId: 'run_legacy_local', + spec: 'retry me' + }) database.updateTaskStatus(reopened.id, 'completed', 'first result') database.updateTaskStatus(overturned.id, 'completed', 'wrong result') database.updateTaskStatus(retried.id, 'failed', 'boom') diff --git a/src/main/runtime/orchestration/lifecycle-reconciliation.test.ts b/src/main/runtime/orchestration/lifecycle-reconciliation.test.ts index 3f0f0a7664f..dedea449629 100644 --- a/src/main/runtime/orchestration/lifecycle-reconciliation.test.ts +++ b/src/main/runtime/orchestration/lifecycle-reconciliation.test.ts @@ -10,10 +10,11 @@ describe('lifecycle reconciliation', () => { it('rejects handle churn when neither side has stable pane identity', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_before_restart') const logs: string[] = [] const message = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_after_restart', to: 'term_coordinator', subject: 'Done', @@ -37,9 +38,10 @@ describe('lifecycle reconciliation', () => { it('completes worker_done from the dispatched pane after a handle remint', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_before_restart', `tab_w:${LEAF_A}`) const message = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_after_restart', to: 'term_coordinator', subject: 'Done', @@ -54,7 +56,7 @@ describe('lifecycle reconciliation', () => { it('completes an exact-authority worker_done after an uncertain worker start', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const started = db.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -82,6 +84,7 @@ describe('lifecycle reconciliation', () => { ).toEqual({ valid: true }) const message = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_worker', to: 'term_coordinator', subject: 'Done after reconnect', @@ -106,9 +109,10 @@ describe('lifecycle reconciliation', () => { it('fails both the dispatch and task from an authenticated failed worker report', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_worker', `tab_w:${LEAF_A}`) const message = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_worker', to: 'term_coordinator', subject: 'Failed: tests cannot start', @@ -139,7 +143,7 @@ describe('lifecycle reconciliation', () => { it('keeps worker report settlement nested in its caller transaction', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_worker') db.db.exec('BEGIN IMMEDIATE') @@ -160,19 +164,16 @@ describe('lifecycle reconciliation', () => { it('replays an identical terminal outcome without mutating settled state', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_worker') const makeMessage = () => db.insertMessage({ + runId: 'run_legacy_local', from: 'term_worker', to: 'term_coordinator', subject: 'Done', type: 'worker_done', - payload: JSON.stringify({ - taskId: task.id, - dispatchId: dispatch.id, - outcome: 'succeeded' - }) + payload: JSON.stringify({ taskId: task.id, dispatchId: dispatch.id, outcome: 'succeeded' }) }) expect(reconcileLifecycleMessage(db, makeMessage()).action).toBe('completed') @@ -199,6 +200,7 @@ describe('lifecycle reconciliation', () => { ])('rejects malformed worker reports with $code', ({ payload, code }) => { db = new OrchestrationDb(':memory:') const message = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_worker', to: 'term_coordinator', subject: 'Done', @@ -215,11 +217,12 @@ describe('lifecycle reconciliation', () => { it('completes worker_done from the same leaf after a pane break-out changed the tab half', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) // Dispatch recorded the post-break-out pane key; the worker shell still // holds the spawn-time key with the old tab id. const dispatch = createRootDispatch(db, task.id, 'term_before_restart', `tab_new:${LEAF_A}`) const message = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_after_restart', to: 'term_coordinator', subject: 'Done', @@ -234,9 +237,10 @@ describe('lifecycle reconciliation', () => { it('rejects mismatched opaque pane keys instead of treating them as legacy', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_owner', `tab_w:${LEAF_A}`) const message = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_reminted', to: 'term_coordinator', subject: 'Done', @@ -251,9 +255,10 @@ describe('lifecycle reconciliation', () => { it('rejects worker_done from a foreign pane that claims the assignee handle', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_owner', `tab_w1:${LEAF_A}`) const message = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_owner', to: 'term_coordinator', subject: 'Done', @@ -294,9 +299,10 @@ describe('lifecycle reconciliation', () => { it('does not let a caller-supplied rejection marker turn completion into success', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_worker', `tab_w:${LEAF_A}`) const message = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_worker', to: 'term_coordinator', subject: 'Done', @@ -323,9 +329,10 @@ describe('lifecycle reconciliation', () => { it('rejects a coordinator completion for a pane-bound dispatch', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_worker', `tab_w:${LEAF_A}`) const message = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_coordinator', to: 'term_coordinator', subject: 'Done', @@ -342,9 +349,13 @@ describe('lifecycle reconciliation', () => { it('uses exact handle equality only for a legacy dispatch without a pane key', () => { db = new OrchestrationDb(':memory:') - const acceptedTask = db.createTask({ spec: 'legacy work' }) + const acceptedTask = db.createTask({ + runId: 'run_legacy_local', + spec: 'legacy work' + }) const acceptedDispatch = createRootDispatch(db, acceptedTask.id, 'term_legacy') const accepted = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_legacy', to: 'term_coordinator', subject: 'Done', @@ -357,9 +368,13 @@ describe('lifecycle reconciliation', () => { }) expect(reconcileLifecycleMessage(db, accepted).action).toBe('completed') - const rejectedTask = db.createTask({ spec: 'other legacy work' }) + const rejectedTask = db.createTask({ + runId: 'run_legacy_local', + spec: 'other legacy work' + }) const rejectedDispatch = createRootDispatch(db, rejectedTask.id, 'term_other_legacy') const rejected = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_foreign', to: 'term_coordinator', subject: 'Done', @@ -379,8 +394,12 @@ describe('lifecycle reconciliation', () => { it('does not release a dependent when a foreign completion wins the arrival race', () => { db = new OrchestrationDb(':memory:') - const parent = db.createTask({ spec: 'parent' }) - const child = db.createTask({ spec: 'child', deps: [parent.id] }) + const parent = db.createTask({ runId: 'run_legacy_local', spec: 'parent' }) + const child = db.createTask({ + runId: 'run_legacy_local', + spec: 'child', + deps: [parent.id] + }) const dispatch = createRootDispatch(db, parent.id, 'term_worker', `tab_w:${LEAF_A}`) const payload = JSON.stringify({ taskId: parent.id, @@ -389,6 +408,7 @@ describe('lifecycle reconciliation', () => { }) const foreign = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_coordinator', to: 'term_coordinator', subject: 'Done', @@ -403,6 +423,7 @@ describe('lifecycle reconciliation', () => { expect(db.getTask(child.id)?.status).toBe('pending') const owner = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_worker_reminted', to: 'term_coordinator', subject: 'Done', @@ -416,7 +437,7 @@ describe('lifecycle reconciliation', () => { it('does not let a foreign replay overwrite an authorized completion', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_worker', `tab_w:${LEAF_A}`) const payload = JSON.stringify({ taskId: task.id, @@ -424,6 +445,7 @@ describe('lifecycle reconciliation', () => { outcome: 'succeeded' }) const owner = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_worker', to: 'term_coordinator', subject: 'Done', @@ -435,6 +457,7 @@ describe('lifecycle reconciliation', () => { const result = db.getTask(task.id)?.result const replay = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_foreign', to: 'term_coordinator', subject: 'Forged replay', @@ -451,10 +474,11 @@ describe('lifecycle reconciliation', () => { it('surfaces worker_done sent from a different pane as rejected', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_owner', `tab_w1:${LEAF_A}`) const logs: string[] = [] const message = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_other_worker', to: 'term_coordinator', subject: 'Done', @@ -474,9 +498,10 @@ describe('lifecycle reconciliation', () => { it('surfaces a heartbeat sent from a different pane without recording liveness', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_owner', `tab_w1:${LEAF_A}`) const heartbeat = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_other_worker', to: 'term_coordinator', subject: 'alive', @@ -508,9 +533,10 @@ describe('lifecycle reconciliation', () => { it('surfaces a foreign heartbeat that claims the assignee handle', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_owner', `tab_w1:${LEAF_A}`) const heartbeat = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_owner', to: 'term_coordinator', subject: 'alive', @@ -528,9 +554,10 @@ describe('lifecycle reconciliation', () => { it('records a heartbeat whose pane key drifted only in the tab half', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_owner', `tab_new:${LEAF_A}`) const heartbeat = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_owner', to: 'term_coordinator', subject: 'alive', @@ -548,12 +575,16 @@ describe('lifecycle reconciliation', () => { it('suppresses same-dispatch heartbeats once worker_done is reconciled', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' }) const dispatch = createRootDispatch(db, task.id, 'term_worker') - const otherTask = db.createTask({ spec: 'other work' }) + const otherTask = db.createTask({ + runId: 'run_legacy_local', + spec: 'other work' + }) const otherDispatch = createRootDispatch(db, otherTask.id, 'term_other') const insertHeartbeat = (dispatchId: string, from: string) => db.insertMessage({ + runId: 'run_legacy_local', from, to: 'term_coordinator', subject: 'alive', @@ -565,6 +596,7 @@ describe('lifecycle reconciliation', () => { reconcileLifecycleMessage(db, staleHeartbeat) reconcileLifecycleMessage(db, otherHeartbeat) const done = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_worker', to: 'term_coordinator', subject: 'Done', diff --git a/src/main/runtime/orchestration/lightweight-run-worker-exit-escalation.test.ts b/src/main/runtime/orchestration/lightweight-run-worker-exit-escalation.test.ts index 2b8d9c90bc2..d37e379e1c5 100644 --- a/src/main/runtime/orchestration/lightweight-run-worker-exit-escalation.test.ts +++ b/src/main/runtime/orchestration/lightweight-run-worker-exit-escalation.test.ts @@ -412,7 +412,7 @@ describe('STA-4604 worker PTY exit escalation reaches the coordinator', () => { } }) - it('falls back to the legacy gate when the dispatch owning Run row is gone', async () => { + it('preserves the dispatch Run when legacy coordinator routing is used', async () => { const { runtime, workerHandle, coordinatorHandle } = makeRuntimeWithTwoPanes() const insertMessage = vi.fn((message: { to: string }) => ({ ...message, @@ -435,8 +435,7 @@ describe('STA-4604 worker PTY exit escalation reaches the coordinator', () => { expect(insertMessage).toHaveBeenCalledWith( expect.objectContaining({ to: coordinatorHandle, type: 'escalation' }) ) - // An orphaned dispatch has no Run mailbox to address, so it must not invent one. - expect(insertMessage.mock.calls[0]?.[0]).not.toHaveProperty('runId') + expect(insertMessage.mock.calls[0]?.[0]).toHaveProperty('runId', 'run-that-no-longer-exists') }) it('still reaches the Run mailbox when the Run has no bound coordinator', async () => { diff --git a/src/main/runtime/orchestration/mailbox-pointer-eligibility.test.ts b/src/main/runtime/orchestration/mailbox-pointer-eligibility.test.ts index 87b090e4e48..2276665fc1c 100644 --- a/src/main/runtime/orchestration/mailbox-pointer-eligibility.test.ts +++ b/src/main/runtime/orchestration/mailbox-pointer-eligibility.test.ts @@ -15,9 +15,9 @@ const MAILBOX = 'dispatch:d1' function seeded(): OrchestrationDb { const db = new OrchestrationDb(':memory:') db.insertMessages([ - { from: 'coordinator', to: MAILBOX, subject: 'a', type: 'status' }, - { from: 'coordinator', to: MAILBOX, subject: 'b', type: 'question' }, - { from: 'coordinator', to: MAILBOX, subject: 'c', type: 'status' } + { runId: 'run_legacy_local', from: 'coordinator', to: MAILBOX, subject: 'a', type: 'status' }, + { runId: 'run_legacy_local', from: 'coordinator', to: MAILBOX, subject: 'b', type: 'question' }, + { runId: 'run_legacy_local', from: 'coordinator', to: MAILBOX, subject: 'c', type: 'status' } ]) return db } diff --git a/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts b/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts index 9573f02fc0c..d76a480ecfd 100644 --- a/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts +++ b/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts @@ -59,7 +59,12 @@ function stageArgs(db: OrchestrationDb, state: OrchestrationMailboxPointerState) describe('mailbox pointer staging watermark', () => { it('leaves no watermark when the reservation claim is lost', () => { const db = new OrchestrationDb(':memory:') - const message = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 's' }) + const message = db.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'run:run-1', + subject: 's' + }) // A concurrent flight already owns the reservation, so this claim cannot succeed. expect( db.stageMailboxPointerEnter([message.id], { ptyId: 'other-pty', processIncarnation: 'inc-x' }) @@ -79,7 +84,12 @@ describe('mailbox pointer staging watermark', () => { it('leaves no watermark when the reservation write throws', () => { const db = new OrchestrationDb(':memory:') - const message = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 's' }) + const message = db.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'run:run-1', + subject: 's' + }) const throwing = new Proxy(db, { get(target, prop, receiver) { if (prop === 'markMailboxPointerWriteAttempted') { @@ -107,7 +117,12 @@ describe('mailbox pointer staging watermark', () => { it('keeps the watermark for the flight that owns the reservation', () => { const db = new OrchestrationDb(':memory:') - const message = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 's' }) + const message = db.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'run:run-1', + subject: 's' + }) const state = new OrchestrationMailboxPointerState() const args = stageArgs(db, state) stageOrchestrationMailboxPointer({ @@ -122,7 +137,12 @@ describe('mailbox pointer staging watermark', () => { it('drains a delivery parked behind the watermark when the write is refused', () => { const db = new OrchestrationDb(':memory:') - const message = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 's' }) + const message = db.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'run:run-1', + subject: 's' + }) const state = new OrchestrationMailboxPointerState() const args = stageArgs(db, state) const redrive = vi.fn() @@ -148,7 +168,7 @@ describe('mailbox pointer staging watermark', () => { it('still points new mail after a delivery lost its reservation claim', async () => { const db = new OrchestrationDb(':memory:') - db.insertMessage({ from: 'a', to: 'run:run-1', subject: 'first' }) + db.insertMessage({ runId: 'run_legacy_local', from: 'a', to: 'run:run-1', subject: 'first' }) let stealNextClaim = true const contended = new Proxy(db, { get(target, prop, receiver) { @@ -172,7 +192,7 @@ describe('mailbox pointer staging watermark', () => { expect(writePty).not.toHaveBeenCalled() // Newer mail must still reach the agent; a leaked watermark used to park it forever. - db.insertMessage({ from: 'a', to: 'run:run-1', subject: 'second' }) + db.insertMessage({ runId: 'run_legacy_local', from: 'a', to: 'run:run-1', subject: 'second' }) delivery.deliver(LEAF, { mailboxHandle: 'run:run-1', skipAbsenceProbe: true }) await new Promise((resolve) => setImmediate(resolve)) diff --git a/src/main/runtime/orchestration/mailbox-pointer-submit.test.ts b/src/main/runtime/orchestration/mailbox-pointer-submit.test.ts index 00126bc237b..02d556204df 100644 --- a/src/main/runtime/orchestration/mailbox-pointer-submit.test.ts +++ b/src/main/runtime/orchestration/mailbox-pointer-submit.test.ts @@ -14,7 +14,12 @@ import type { WriteSettlement } from '../../../shared/pty-write-settlement' describe('orchestration mailbox pointer submit', () => { it('does not settle a replacement reservation after an old Enter write resolves', async () => { const db = new OrchestrationDb(':memory:') - const message = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 'staged' }) + const message = db.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'run:run-1', + subject: 'staged' + }) const ptyId = 'pty-reused' const oldReservation = { ptyId, processIncarnation: 'inc-old' } const replacementReservation = { ptyId, processIncarnation: 'inc-new' } @@ -86,8 +91,18 @@ describe('orchestration mailbox pointer submit', () => { it('does not overwrite a message already reserved by another pointer flight', () => { const db = new OrchestrationDb(':memory:') - const first = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 'first' }) - const second = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 'second' }) + const first = db.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'run:run-1', + subject: 'first' + }) + const second = db.insertMessage({ + runId: 'run_legacy_local', + from: 'a', + to: 'run:run-1', + subject: 'second' + }) const original = { ptyId: 'pty-a', processIncarnation: 'inc-a' } const replacement = { ptyId: 'pty-b', processIncarnation: 'inc-b' } diff --git a/src/main/runtime/orchestration/message-batch-atomicity.test.ts b/src/main/runtime/orchestration/message-batch-atomicity.test.ts index f2e43ed31e4..fda83fad9a9 100644 --- a/src/main/runtime/orchestration/message-batch-atomicity.test.ts +++ b/src/main/runtime/orchestration/message-batch-atomicity.test.ts @@ -108,8 +108,20 @@ describe('message batch atomicity', () => { expect(() => db?.insertMessages([ - { id: 'inner_first', from: 'sender', to: 'recipient', subject: 'first' }, - { id: 'inner_second', from: 'sender', to: 'recipient', subject: 'second' } + { + runId: 'run_legacy_local', + id: 'inner_first', + from: 'sender', + to: 'recipient', + subject: 'first' + }, + { + runId: 'run_legacy_local', + id: 'inner_second', + from: 'sender', + to: 'recipient', + subject: 'second' + } ]) ).toThrow('blocked') sqlite.exec('COMMIT') @@ -133,6 +145,7 @@ describe('message batch atomicity', () => { expect(() => db?.commitWorkerDoneMessageMutation(() => { db?.insertMessage({ + runId: 'run_legacy_local', id: 'inner', from: 'worker', to: 'coordinator', diff --git a/src/main/runtime/orchestration/nested-worker-depth-migration.test.ts b/src/main/runtime/orchestration/nested-worker-depth-migration.test.ts index fa955b7cf08..9d05c0dac07 100644 --- a/src/main/runtime/orchestration/nested-worker-depth-migration.test.ts +++ b/src/main/runtime/orchestration/nested-worker-depth-migration.test.ts @@ -34,6 +34,7 @@ describe('nested worker depth migration (v30)', () => { const oldDb = new Database(dbPath) oldDb.exec('ALTER TABLE dispatch_contexts DROP COLUMN depth') oldDb.exec('ALTER TABLE remote_dispatch_attachments DROP COLUMN depth') + oldDb.exec('ALTER TABLE remote_dispatch_attachments DROP COLUMN home_run_id') oldDb.pragma('user_version = 29') oldDb .prepare( @@ -78,7 +79,7 @@ describe('nested worker depth migration (v30)', () => { ) .run() - const task = db.createTask({ spec: 'post-upgrade nesting attempt' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'post-upgrade nesting attempt' }) expect(() => db!.createDispatchContext({ taskId: task.id, diff --git a/src/main/runtime/orchestration/orchestration-adopted-run-binding.test.ts b/src/main/runtime/orchestration/orchestration-adopted-run-binding.test.ts index 800a7511451..d667267e0a6 100644 --- a/src/main/runtime/orchestration/orchestration-adopted-run-binding.test.ts +++ b/src/main/runtime/orchestration/orchestration-adopted-run-binding.test.ts @@ -47,11 +47,13 @@ function createAdoptedFixture(options: { settleWork: boolean }): AdoptedFixture const before = new OrchestrationDb(dbPath) const task = before.createTask({ + runId: 'run_legacy_local', spec: 'legacy assignment', createdByTerminalHandle: LEGACY_COORDINATOR_HANDLE }) const dispatch = createRootDispatch(before, task.id, LEGACY_WORKER_HANDLE, LEGACY_WORKER_PANE) const recovery = before.insertMessage({ + runId: 'run_legacy_local', from: LEGACY_WORKER_HANDLE, to: LEGACY_COORDINATOR_HANDLE, subject: 'recovered worker outcome', diff --git a/src/main/runtime/orchestration/orchestration-all-start-versions-migration.test.ts b/src/main/runtime/orchestration/orchestration-all-start-versions-migration.test.ts index b4c9281d89b..b62677e465b 100644 --- a/src/main/runtime/orchestration/orchestration-all-start-versions-migration.test.ts +++ b/src/main/runtime/orchestration/orchestration-all-start-versions-migration.test.ts @@ -36,7 +36,12 @@ describe('orchestration migration from every prior version stamp', () => { expect(reopened.db.pragma('user_version', { simple: true }), `reopen v${version}`).toBe( SCHEMA_VERSION ) - expect(() => reopened.createTask({ spec: `migration v${version}` })).not.toThrow() + expect(() => + reopened.createTask({ + runId: 'run_legacy_local', + spec: `migration v${version}` + }) + ).not.toThrow() reopened.close() } }) diff --git a/src/main/runtime/orchestration/orchestration-db-retention-pagination.test.ts b/src/main/runtime/orchestration/orchestration-db-retention-pagination.test.ts index 38eeca64337..c980b8fe02a 100644 --- a/src/main/runtime/orchestration/orchestration-db-retention-pagination.test.ts +++ b/src/main/runtime/orchestration/orchestration-db-retention-pagination.test.ts @@ -103,6 +103,7 @@ describe('OrchestrationDb bounded mutation receipts', () => { insertMutationReceipts(db, MUTATION_RECEIPT_MAX_ROWS, 'completed') db.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId: 'ctx_remote_pruned', taskId: 'task_remote_pruned', homePeerFingerprint: 'caller', @@ -131,6 +132,7 @@ describe('OrchestrationDb bounded mutation receipts', () => { expect(() => db!.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId: 'ctx_remote_overflow', taskId: 'task_remote_overflow', homePeerFingerprint: 'caller', @@ -151,7 +153,7 @@ describe('OrchestrationDb bounded mutation receipts', () => { it('guards atomic worker acceptance without changing task state', () => { db = new OrchestrationDb(':memory:') - const task = db.createTask({ spec: 'capacity check' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'capacity check' }) insertMutationReceipts(db, MUTATION_RECEIPT_MAX_ROWS, 'pending') expect(() => @@ -226,7 +228,10 @@ describe('OrchestrationDb dispatch assignee index migration', () => { tempDir = mkdtempSync(join(tmpdir(), 'orca-dispatch-index-migration-')) const dbPath = join(tempDir, 'orchestration.db') db = new OrchestrationDb(dbPath) - const task = db.createTask({ spec: 'indexed lookup' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'indexed lookup' + }) const dispatch = createRootDispatch(db, task.id, 'term_worker') db.close() db = undefined @@ -245,7 +250,9 @@ describe('OrchestrationDb dispatch assignee index migration', () => { db = new OrchestrationDb(dbPath) const sqlite = sqliteFor(db) expect(sqlite.pragma('user_version', { simple: true })).toBe(SCHEMA_VERSION) - expect(db.getDispatchContextById(dispatch.id)).toMatchObject({ assignee_handle: 'term_worker' }) + expect(db.getDispatchContextById(dispatch.id)).toMatchObject({ + assignee_handle: 'term_worker' + }) expect(db.getTask(task.id)).toMatchObject({ created_by_pane_key: null, created_by_process_incarnation: null, diff --git a/src/main/runtime/orchestration/orchestration-federated-legacy-probe.test.ts b/src/main/runtime/orchestration/orchestration-federated-legacy-probe.test.ts new file mode 100644 index 00000000000..ad08a7383f0 --- /dev/null +++ b/src/main/runtime/orchestration/orchestration-federated-legacy-probe.test.ts @@ -0,0 +1,97 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { LEGACY_RUN_ID, OrchestrationDb } from './db' +import { SCHEMA_VERSION } from './db/contract-constants' +import { resolveOrchestrationMigrationStartVersion } from './orchestration-schema-version-skew' + +describe('federated mailbox legacy-adoption probe', () => { + let db: OrchestrationDb | undefined + let directory: string | undefined + + afterEach(() => { + db?.close() + if (directory) { + rmSync(directory, { recursive: true, force: true }) + } + }) + + function seedMailbox(handle: string, kind: 'message' | 'delivery'): string { + directory = mkdtempSync(join(tmpdir(), 'orca-federated-legacy-probe-')) + const path = join(directory, 'orchestration.db') + db = new OrchestrationDb(path) + db.db.exec(` + INSERT INTO remote_dispatch_attachments ( + dispatch_id, task_id, home_peer_fingerprint, home_run_id, runtime_epoch, state + ) VALUES ('ctx_remote', 'task_remote', 'peer_home', 'run_home', 'epoch', 'ready'); + `) + if (kind === 'message') { + db.db + .prepare( + `INSERT INTO messages ( + id, run_id, delivery_contract, from_handle, to_handle, subject, type + ) VALUES ('msg_probe', ?, 'current_delivery', 'term_home', ?, 'continue', 'dispatch')` + ) + .run(LEGACY_RUN_ID, handle) + } else { + db.db + .prepare( + `INSERT INTO deliveries (id, run_id, mailbox_handle, consumer_generation, message_ids) + VALUES ('delivery_probe', ?, ?, 0, '[]')` + ) + .run(LEGACY_RUN_ID, handle) + } + return path + } + + it.each(['message', 'delivery'] as const)( + 'does not replay adoption for a misfiled federated %s', + (kind) => { + const path = seedMailbox('dispatch:ctx_remote', kind) + expect( + resolveOrchestrationMigrationStartVersion(db!.db, SCHEMA_VERSION, SCHEMA_VERSION) + ).toBe(SCHEMA_VERSION) + db!.close() + db = new OrchestrationDb(path) + expect(db.getLegacyAdoption()).toBeUndefined() + if (kind === 'message') { + expect(db.getMessageById('msg_probe')).toMatchObject({ + run_id: LEGACY_RUN_ID, + delivery_contract: 'current_delivery' + }) + } else { + expect( + db.db.prepare("SELECT status FROM deliveries WHERE id = 'delivery_probe'").get() + ).toEqual({ + status: 'outstanding' + }) + } + } + ) + + it.each(['message', 'delivery'] as const)( + 'still replays adoption for a genuine legacy %s', + (kind) => { + const path = seedMailbox('term_legacy_coordinator', kind) + expect( + resolveOrchestrationMigrationStartVersion(db!.db, SCHEMA_VERSION, SCHEMA_VERSION) + ).toBe(6) + db!.close() + db = new OrchestrationDb(path) + expect(db.getLegacyAdoption()).toBeDefined() + if (kind === 'message') { + expect(db.getMessageById('msg_probe')).toMatchObject({ + run_id: db.getLegacyAdoption()!.adopted_run_id, + delivery_contract: 'legacy_direct' + }) + } else { + expect( + db.db.prepare("SELECT status FROM deliveries WHERE id = 'delivery_probe'").get() + ).toEqual({ + status: 'fenced' + }) + } + } + ) +}) diff --git a/src/main/runtime/orchestration/orchestration-legacy-storage-test-fixture.ts b/src/main/runtime/orchestration/orchestration-legacy-storage-test-fixture.ts index 4cdc8f5ee91..886f2383db5 100644 --- a/src/main/runtime/orchestration/orchestration-legacy-storage-test-fixture.ts +++ b/src/main/runtime/orchestration/orchestration-legacy-storage-test-fixture.ts @@ -54,6 +54,7 @@ export function createLegacyStorageCutoverFixture(): { }) const legacyTask = first.createTask({ + runId: 'run_legacy_local', spec: 'legacy', createdByTerminalHandle: 'term_legacy_coord' }) @@ -76,16 +77,19 @@ export function createLegacyStorageCutoverFixture(): { ) const legacyMessages = [ first.insertMessage({ + runId: 'run_legacy_local', from: 'term_legacy_coord', to: 'term_legacy_worker', subject: 'read worker mail' }), first.insertMessage({ + runId: 'run_legacy_local', from: 'term_legacy_worker', to: 'term_legacy_coord', subject: 'read coordinator mail' }), first.insertMessage({ + runId: 'run_legacy_local', from: 'term_legacy_coord', to: 'term_legacy_worker', subject: 'second worker page' @@ -99,6 +103,7 @@ export function createLegacyStorageCutoverFixture(): { question: 'Retained question?' }) const rejection = first.insertMessage({ + runId: 'run_legacy_local', from: 'term_legacy_worker', to: 'term_legacy_coord', subject: 'Rejected heartbeat', @@ -106,6 +111,7 @@ export function createLegacyStorageCutoverFixture(): { payload: JSON.stringify({ _orcaLifecycleRejection: { code: 'migration', reason: 'cutover' } }) }) const lookalike = first.insertMessage({ + runId: 'run_legacy_local', from: 'term_legacy_worker', to: 'term_legacy_coord', subject: 'Ordinary legacy mail', @@ -115,36 +121,42 @@ export function createLegacyStorageCutoverFixture(): { }) const malformedRejections = [ first.insertMessage({ + runId: 'run_legacy_local', from: 'term_legacy_worker', to: 'term_legacy_coord', subject: 'Invalid JSON marker', payload: '{"_orcaLifecycleRejection":' }), first.insertMessage({ + runId: 'run_legacy_local', from: 'term_legacy_worker', to: 'term_legacy_coord', subject: 'Array marker', payload: JSON.stringify({ _orcaLifecycleRejection: [] }) }), first.insertMessage({ + runId: 'run_legacy_local', from: 'term_legacy_worker', to: 'term_legacy_coord', subject: 'String marker', payload: JSON.stringify({ _orcaLifecycleRejection: 'migration' }) }), first.insertMessage({ + runId: 'run_legacy_local', from: 'term_legacy_worker', to: 'term_legacy_coord', subject: 'Incomplete marker', payload: JSON.stringify({ _orcaLifecycleRejection: { code: 'migration' } }) }), first.insertMessage({ + runId: 'run_legacy_local', from: 'term_legacy_worker', to: 'term_legacy_coord', subject: 'Non-string marker fields', payload: JSON.stringify({ _orcaLifecycleRejection: { code: 19, reason: false } }) }), first.insertMessage({ + runId: 'run_legacy_local', from: 'term_legacy_worker', to: 'term_legacy_coord', subject: 'Array root', @@ -153,6 +165,7 @@ export function createLegacyStorageCutoverFixture(): { ]) }), first.insertMessage({ + runId: 'run_legacy_local', from: 'term_legacy_worker', to: 'term_legacy_coord', subject: 'String root', diff --git a/src/main/runtime/orchestration/orchestration-mutation-question-db.test.ts b/src/main/runtime/orchestration/orchestration-mutation-question-db.test.ts index 5a26448bd98..c4911b7d676 100644 --- a/src/main/runtime/orchestration/orchestration-mutation-question-db.test.ts +++ b/src/main/runtime/orchestration/orchestration-mutation-question-db.test.ts @@ -77,6 +77,7 @@ describe('OrchestrationDb mutation and question state', () => { it('accepts a question message in the fresh canonical schema', () => { const d = createDb() const message = d.insertMessage({ + runId: 'run_legacy_local', from: 'worker', to: 'run:run_1', subject: 'Need input', diff --git a/src/main/runtime/orchestration/orchestration-schema-version-skew.ts b/src/main/runtime/orchestration/orchestration-schema-version-skew.ts index a2767e1e5a7..85e0a78b3ae 100644 --- a/src/main/runtime/orchestration/orchestration-schema-version-skew.ts +++ b/src/main/runtime/orchestration/orchestration-schema-version-skew.ts @@ -42,7 +42,8 @@ const VERSIONED_POST_V6_COLUMNS = [ { version: 36, table: 'dispatch_contexts', column: 'consumer_generation' }, { version: 36, table: 'remote_dispatch_attachments', column: 'consumer_generation' }, { version: 37, table: 'dispatch_contexts', column: 'creator_handle' }, - { version: 37, table: 'dispatch_contexts', column: 'creator_pane_key' } + { version: 37, table: 'dispatch_contexts', column: 'creator_pane_key' }, + { version: 40, table: 'remote_dispatch_attachments', column: 'home_run_id' } ] as const // Why: v34 shipped without these two, so a v34 stamp proves nothing about them; v35 repairs both @@ -122,15 +123,22 @@ function messagesAllowQuestions(db: Database.Database): boolean { function hasConsistentLegacyAdoption(db: Database.Database): boolean { const sourceRunId = 'run_legacy_local' + // Misfiled federated mail is not evidence of a pre-Runs database. + const notFederatedMailbox = (handle: string): string => + `NOT EXISTS (SELECT 1 FROM remote_dispatch_attachments AS attachment + WHERE 'dispatch:' || attachment.dispatch_id = ${handle})` + const deliveryFilter = hasOrchestrationColumn(db, 'deliveries', 'mailbox_handle') + ? ` AND ${notFederatedMailbox('mailbox_handle')}` + : '' const sourceGraph = db .prepare( `SELECT 1 WHERE EXISTS(SELECT 1 FROM tasks WHERE run_id = ?) OR EXISTS(SELECT 1 FROM dispatch_contexts WHERE run_id = ?) OR EXISTS(SELECT 1 FROM decision_gates WHERE run_id = ?) - OR EXISTS(SELECT 1 FROM messages WHERE run_id = ?) + OR EXISTS(SELECT 1 FROM messages WHERE run_id = ? AND ${notFederatedMailbox('to_handle')}) OR EXISTS(SELECT 1 FROM question_threads WHERE run_id = ?) - OR EXISTS(SELECT 1 FROM deliveries WHERE run_id = ?)` + OR EXISTS(SELECT 1 FROM deliveries WHERE run_id = ?${deliveryFilter})` ) .get(sourceRunId, sourceRunId, sourceRunId, sourceRunId, sourceRunId, sourceRunId) const adoption = db diff --git a/src/main/runtime/orchestration/orchestration-settled-worker-resume-fence-db.test.ts b/src/main/runtime/orchestration/orchestration-settled-worker-resume-fence-db.test.ts index ee52bc026d0..5cde241093d 100644 --- a/src/main/runtime/orchestration/orchestration-settled-worker-resume-fence-db.test.ts +++ b/src/main/runtime/orchestration/orchestration-settled-worker-resume-fence-db.test.ts @@ -13,7 +13,7 @@ describe('settled worker terminal resume fence rows', () => { function createReadyWorker(): { db: OrchestrationDb; taskId: string; dispatchId: string } { const d = new OrchestrationDb(':memory:') db = d - const task = d.createTask({ spec: 'settled worker' }) + const task = d.createTask({ runId: 'run_legacy_local', spec: 'settled worker' }) const started = d.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, diff --git a/src/main/runtime/orchestration/orchestration-version-skew-migration.test.ts b/src/main/runtime/orchestration/orchestration-version-skew-migration.test.ts index 12ccf7303ca..f8fa7a5df48 100644 --- a/src/main/runtime/orchestration/orchestration-version-skew-migration.test.ts +++ b/src/main/runtime/orchestration/orchestration-version-skew-migration.test.ts @@ -389,7 +389,10 @@ describe('OrchestrationDb version-skew migration', () => { tempDir = mkdtempSync(join(tmpdir(), 'orca-db-version-skew-v30-reset-')) const dbPath = join(tempDir, 'orchestration.db') db = new OrchestrationDb(dbPath) - const task = db.createTask({ spec: 'reset by an older writer' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'reset by an older writer' + }) const started = db.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, diff --git a/src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts b/src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts index 16a118008de..d6e6038cd65 100644 --- a/src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts +++ b/src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts @@ -15,7 +15,7 @@ describe('OrchestrationDb worker Dispatch state', () => { it('creates and activates a composed worker Dispatch transactionally', () => { const d = createDb() - const task = d.createTask({ spec: 'worker' }) + const task = d.createTask({ runId: 'run_legacy_local', spec: 'worker' }) const started = d.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -82,7 +82,7 @@ describe('OrchestrationDb worker Dispatch state', () => { it('retains an active supervised worker terminal', () => { const d = createDb() - const task = d.createTask({ spec: 'retain active worker' }) + const task = d.createTask({ runId: 'run_legacy_local', spec: 'retain active worker' }) const started = d.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -114,7 +114,7 @@ describe('OrchestrationDb worker Dispatch state', () => { it('requeues an active Task before settling a worker whose terminal is missing', () => { const d = createDb() - const task = d.createTask({ spec: 'recover missing worker' }) + const task = d.createTask({ runId: 'run_legacy_local', spec: 'recover missing worker' }) const started = d.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -153,7 +153,7 @@ describe('OrchestrationDb worker Dispatch state', () => { it('commits worker-start mutation acceptance with the starting Dispatch', () => { const d = createDb() - const task = d.createTask({ spec: 'atomic acceptance' }) + const task = d.createTask({ runId: 'run_legacy_local', spec: 'atomic acceptance' }) const mutationReceipt = { callerFingerprint: 'caller_fingerprint', requestId: 'worker_start_request', @@ -207,7 +207,7 @@ describe('OrchestrationDb worker Dispatch state', () => { it('fails a composed start without losing residual resource receipts', () => { const d = createDb() - const task = d.createTask({ spec: 'worker' }) + const task = d.createTask({ runId: 'run_legacy_local', spec: 'worker' }) const started = d.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -232,7 +232,7 @@ describe('OrchestrationDb worker Dispatch state', () => { it('allows retry only from the Task current terminal Dispatch', () => { const d = createDb() - const task = d.createTask({ spec: 'retry current' }) + const task = d.createTask({ runId: 'run_legacy_local', spec: 'retry current' }) const first = d.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -272,7 +272,7 @@ describe('OrchestrationDb worker Dispatch state', () => { it('treats abandon of a superseded Dispatch as a no-op', () => { const d = createDb() - const task = d.createTask({ spec: 'stale abandon' }) + const task = d.createTask({ runId: 'run_legacy_local', spec: 'stale abandon' }) const first = d.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -317,7 +317,7 @@ describe('OrchestrationDb worker Dispatch state', () => { it('lets the stop fence win before a late worker completion', () => { const d = createDb() - const task = d.createTask({ spec: 'race' }) + const task = d.createTask({ runId: 'run_legacy_local', spec: 'race' }) const started = d.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -350,7 +350,7 @@ describe('OrchestrationDb worker Dispatch state', () => { it('allows explicit stop recovery from uncertain local and remote starts', () => { const d = createDb() - const task = d.createTask({ spec: 'uncertain local start' }) + const task = d.createTask({ runId: 'run_legacy_local', spec: 'uncertain local start' }) const started = d.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, @@ -365,6 +365,7 @@ describe('OrchestrationDb worker Dispatch state', () => { }) d.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId: 'ctx_remote_unknown', taskId: 'task_remote_unknown', homePeerFingerprint: 'home_peer', @@ -398,6 +399,7 @@ describe('OrchestrationDb worker Dispatch state', () => { const paneKey = 'tab_remote:11111111-1111-4111-8111-111111111111' const attach = (dispatchId: string): void => { d.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId, taskId: `task_${dispatchId}`, homePeerFingerprint: 'home_peer', @@ -452,6 +454,7 @@ describe('OrchestrationDb worker Dispatch state', () => { const leafId = '11111111-1111-4111-8111-111111111111' const attach = (dispatchId: string, paneKey: string): void => { d.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId, taskId: `task_${dispatchId}`, homePeerFingerprint: 'home_peer', @@ -499,7 +502,7 @@ describe('OrchestrationDb worker Dispatch state', () => { it('returns already-settled when completion wins before stop', () => { const d = createDb() - const task = d.createTask({ spec: 'race' }) + const task = d.createTask({ runId: 'run_legacy_local', spec: 'race' }) const started = d.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, diff --git a/src/main/runtime/orchestration/r1-identity-migration.test.ts b/src/main/runtime/orchestration/r1-identity-migration.test.ts index bb263d0b9ce..673a72fd844 100644 --- a/src/main/runtime/orchestration/r1-identity-migration.test.ts +++ b/src/main/runtime/orchestration/r1-identity-migration.test.ts @@ -27,7 +27,7 @@ describe('R1 identity migration', () => { tempDir = mkdtempSync(join(tmpdir(), 'orca-r1-identity-')) const dbPath = join(tempDir, 'orchestration.db') db = new OrchestrationDb(dbPath) - const task = db.createTask({ spec: 'legacy supervised worker' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'legacy supervised worker' }) const started = db.createStartingWorkerDispatch({ taskId: task.id, startOptions: { worktree: 'folder:/workspace' }, diff --git a/src/main/runtime/orchestration/types.ts b/src/main/runtime/orchestration/types.ts index 00005443006..85d5dcfc159 100644 --- a/src/main/runtime/orchestration/types.ts +++ b/src/main/runtime/orchestration/types.ts @@ -190,6 +190,7 @@ export type FederatedDispatchRow = { } export type RemoteDispatchAttachmentRow = { + home_run_id: string dispatch_id: string task_id: string home_peer_fingerprint: string diff --git a/src/main/runtime/orchestration/worker-start-unobserved-prompt-settlement.test.ts b/src/main/runtime/orchestration/worker-start-unobserved-prompt-settlement.test.ts index 382ec304bb6..1d123f51b38 100644 --- a/src/main/runtime/orchestration/worker-start-unobserved-prompt-settlement.test.ts +++ b/src/main/runtime/orchestration/worker-start-unobserved-prompt-settlement.test.ts @@ -6,7 +6,7 @@ const INCARNATION = 'runtime_test:term_worker:1' let db: OrchestrationDb function startWorker(spec: string): { taskId: string; dispatchId: string; capability: string } { - const task = db.createTask({ spec }) + const task = db.createTask({ runId: 'run_legacy_local', spec }) const started = db.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federated-message-targeting.test.ts b/src/main/runtime/rpc/methods/orchestration/federation/federated-message-targeting.test.ts index 8e80a8e3925..c28ef94a4a9 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federated-message-targeting.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federated-message-targeting.test.ts @@ -25,6 +25,7 @@ describe('orchestration federated message targeting', () => { vi.spyOn(runtime, 'getTerminalPaneKey').mockReturnValue(paneKey) vi.spyOn(runtime, 'getTerminalProcessIncarnation').mockReturnValue(processIncarnation) db.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId, taskId: 'task_remote_targeting', homePeerFingerprint: 'home_peer', diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federated-release-safety.test.ts b/src/main/runtime/rpc/methods/orchestration/federation/federated-release-safety.test.ts index f3e160244d1..d5150dc052e 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federated-release-safety.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federated-release-safety.test.ts @@ -152,6 +152,7 @@ describe('federated worker release ownership', () => { function createAttachment(dispatchId: string, terminalOwnership?: 'created' | 'external'): void { db.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId, taskId: `task_${dispatchId}`, homePeerFingerprint: HOME_FINGERPRINT, diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-start.ts b/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-start.ts index b577cc87737..a7c59b7064b 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-start.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-start.ts @@ -162,6 +162,7 @@ export async function startFederatedWorker(args: { server.environmentId, 'orchestration.federationAttachStart', { + runId, dispatchId: started.dispatch.id, taskId: taskForRemote.id, taskSpec: taskForRemote.spec, diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federation-agent-launch.test.ts b/src/main/runtime/rpc/methods/orchestration/federation/federation-agent-launch.test.ts index 748e4c55295..ace3bfe407a 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federation-agent-launch.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federation-agent-launch.test.ts @@ -56,6 +56,7 @@ describe('federated worker agent launch', () => { const result = (await method.handler( method.params!.parse({ + runId: 'run-home', dispatchId: 'ctx_remote', taskId: 'task_remote', taskSpec: 'remote cursor worker', diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federation-control-mail.test.ts b/src/main/runtime/rpc/methods/orchestration/federation/federation-control-mail.test.ts index 755f85fd512..c95ec9b5630 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federation-control-mail.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federation-control-mail.test.ts @@ -107,6 +107,7 @@ describe('orchestration federation control mail', () => { homeDb.markWorkerDispatchReady(dispatchId) workerDb.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId, taskId: task.id, homePeerFingerprint: homeFingerprint, diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federation-folder-placement.test.ts b/src/main/runtime/rpc/methods/orchestration/federation/federation-folder-placement.test.ts index b8264bd61a7..68364dac1ed 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federation-folder-placement.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federation-folder-placement.test.ts @@ -27,6 +27,7 @@ describe('orchestration federated folder placement', () => { await expect( method.handler( method.params!.parse({ + runId: 'run-home', dispatchId: 'ctx_folder', taskId: 'task_folder', taskSpec: 'work in folder', diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federation-lifecycle-settlement.test.ts b/src/main/runtime/rpc/methods/orchestration/federation/federation-lifecycle-settlement.test.ts index 3e949383731..e111865d904 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federation-lifecycle-settlement.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federation-lifecycle-settlement.test.ts @@ -445,6 +445,7 @@ describe('orchestration federation lifecycle settlement', () => { const dispatchId = `ctx_persisted_protocol_${protocolVersion}` const taskId = `task_persisted_protocol_${protocolVersion}` workerDb.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId, taskId, homePeerFingerprint: 'run-home-device-token', diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federation-liveness-verdict.test.ts b/src/main/runtime/rpc/methods/orchestration/federation/federation-liveness-verdict.test.ts index 20ae3135ec2..7c75c52eb6c 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federation-liveness-verdict.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federation-liveness-verdict.test.ts @@ -58,6 +58,7 @@ describe('federation host liveness verdicts', () => { status: 'exited' } as never) db.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId: DISPATCH_ID, taskId: 'task_remote', homePeerFingerprint: HOME_FINGERPRINT, @@ -112,6 +113,7 @@ describe('federation host liveness verdicts', () => { throw new Error('Expected the real runtime PTY to be listed') } hostDb.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId: DISPATCH_ID, taskId: 'task_remote', homePeerFingerprint: HOME_FINGERPRINT, diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federation-setup.test.ts b/src/main/runtime/rpc/methods/orchestration/federation/federation-setup.test.ts index c905ddffeb8..83865cf96e4 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federation-setup.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federation-setup.test.ts @@ -49,6 +49,7 @@ describe('orchestration federated setup evidence', () => { } ] db.createRemoteDispatchAttachment({ + runId: 'run-home', dispatchId, taskId: 'task_remote_setup', homePeerFingerprint: 'home_peer', diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federation-start-prompt-budget.test.ts b/src/main/runtime/rpc/methods/orchestration/federation/federation-start-prompt-budget.test.ts index 83b446eb102..d3d5b6d71b1 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federation-start-prompt-budget.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federation-start-prompt-budget.test.ts @@ -29,6 +29,7 @@ describe('federation attach-start prompt budget', () => { await expect( method.handler( method.params!.parse({ + runId: 'run-home', dispatchId: 'ctx_oversized_remote', taskId: 'task_oversized_remote', taskSpec: 'x'.repeat(8 * 1024 * 1024), diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federation-start-schema.ts b/src/main/runtime/rpc/methods/orchestration/federation/federation-start-schema.ts index d5d1874a788..1e7257df27d 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federation-start-schema.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federation-start-schema.ts @@ -3,6 +3,7 @@ import { OptionalFiniteNumber, OptionalString, requiredString } from '../../../s import { OptionalWorkerLaunchPreference } from '../worker/worker-start-schema' export const FederationAttachStartParams = z.object({ + runId: requiredString('Missing Run ID'), dispatchId: requiredString('Missing Dispatch ID'), taskId: requiredString('Missing Task ID'), taskSpec: requiredString('Missing Task spec'), diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federation.ts b/src/main/runtime/rpc/methods/orchestration/federation/federation.ts index 57afd3103a2..785f6a67eec 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federation.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federation.ts @@ -65,6 +65,7 @@ export const ORCHESTRATION_FEDERATION_ATTACH_METHODS: RpcMethod[] = [ const db = runtime.getOrchestrationDb() db.createRemoteDispatchAttachment({ + runId: params.runId, dispatchId: params.dispatchId, taskId: params.taskId, homePeerFingerprint: orchestrationMutation.callerFingerprint, diff --git a/src/main/runtime/rpc/methods/orchestration/messaging/check-worker-federated-attachment.test.ts b/src/main/runtime/rpc/methods/orchestration/messaging/check-worker-federated-attachment.test.ts new file mode 100644 index 00000000000..58e0b3aa0ca --- /dev/null +++ b/src/main/runtime/rpc/methods/orchestration/messaging/check-worker-federated-attachment.test.ts @@ -0,0 +1,188 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { ORCHESTRATION_METHODS } from '../../orchestration' +import type { RpcContext } from '../../../core' +import { OrchestrationDb } from '../../../../orchestration/db' +import { OrcaRuntimeService } from '../../../../orca-runtime' +import { + encodeFederatedControlMessage, + importFederatedControlMessage +} from '../../../../orchestration/federation-control-message' + +const DISPATCH_ID = 'ctx_federated_worker_1' +const WORKER_HANDLE = 'term_federated_worker' +const WORKER_PANE = 'tab_w:eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee' +const INCARNATION = 'runtime_test:term_federated_worker:1' + +type CheckResult = { + runId: string + deliveryId: string | null + messages: { id: string; subject: string }[] + count: number + replayed: boolean + acknowledged: string | null +} + +describe('orchestration.check on a federated attachment across a restart', () => { + let directory: string | undefined + let db: OrchestrationDb | undefined + + afterEach(() => { + db?.close() + db = undefined + if (directory) { + rmSync(directory, { recursive: true, force: true }) + directory = undefined + } + }) + + function launch(path: string): RpcContext { + db = new OrchestrationDb(path) + const runtime = new OrcaRuntimeService() + runtime.setOrchestrationDb(db) + vi.spyOn(runtime, 'getTerminalPaneKey').mockImplementation((handle) => + handle === WORKER_HANDLE ? WORKER_PANE : null + ) + vi.spyOn(runtime, 'getLiveTerminalPaneKey').mockImplementation((handle) => + runtime.getTerminalPaneKey(handle) + ) + vi.spyOn(runtime, 'getTerminalProcessIncarnation').mockImplementation((handle) => + handle === WORKER_HANDLE ? INCARNATION : null + ) + return { runtime } + } + + function check(ctx: RpcContext, params: Record = {}): Promise { + const method = ORCHESTRATION_METHODS.find((entry) => entry.name === 'orchestration.check') + if (!method) { + throw new Error('orchestration.check is not registered') + } + const parsed = method.params + ? method.params.parse({ terminal: WORKER_HANDLE, ...params }) + : undefined + return method.handler(parsed, ctx) as Promise + } + + function attach(store: OrchestrationDb, dispatchId: string, runId: string): void { + store.createRemoteDispatchAttachment({ + dispatchId, + runId, + taskId: 'task_federated_1', + homePeerFingerprint: 'peer_fp', + protocolVersion: 1, + runtimeEpoch: 'epoch_1', + mutationReceipt: { + callerFingerprint: 'peer_fp', + requestId: 'attach_1', + method: 'orchestration.federationAttachStart', + payloadHash: 'attach_payload' + } + }) + expect(store.getRunRaw(runId)).toBeDefined() + store.prepareRemoteAttachmentAuthority({ + dispatchId, + paneKey: WORKER_PANE, + processIncarnation: INCARNATION, + worktreeId: 'folder_workspace', + terminalHandle: WORKER_HANDLE, + setupState: 'not_applicable', + effects: [] + }) + store.markRemoteAttachmentReady(dispatchId) + } + + it('replays the coordinator instruction and takes its ack after the app restarts', async () => { + directory = mkdtempSync(join(tmpdir(), 'orca-federated-check-')) + const path = join(directory, 'orchestration.db') + + const first = launch(path) + attach(db as OrchestrationDb, DISPATCH_ID, 'run_coordinator') + importFederatedControlMessage(db as OrchestrationDb, { + dispatchId: DISPATCH_ID, + messageId: 'msg_federated_1', + payload: encodeFederatedControlMessage({ + from: 'term_coord', + subject: 'continue the task', + body: 'the plan changed', + type: 'dispatch', + priority: 'normal', + threadId: null, + payload: null + }) + }) + + const delivered = await check(first) + expect(delivered.messages.map((message) => message.id)).toEqual(['msg_federated_1']) + expect(delivered.runId).toBe('run_coordinator') + expect(delivered.replayed).toBe(false) + const deliveryId = delivered.deliveryId as string + expect(deliveryId).not.toBeNull() + ;(db as OrchestrationDb).close() + + // The worker's process outlives the app; its instruction is still unacknowledged. + const second = launch(path) + const replayed = await check(second) + expect(replayed.deliveryId).toBe(deliveryId) + expect(replayed.replayed).toBe(true) + expect(replayed.messages.map((message) => message.id)).toEqual(['msg_federated_1']) + + const acknowledged = await check(second, { ack: deliveryId }) + expect(acknowledged.acknowledged).toBe(deliveryId) + expect(acknowledged.count).toBe(0) + }) + + it('files loopback mail once under the local Dispatch Run without replacing its owner', async () => { + const ctx = launch(':memory:') + const store = db as OrchestrationDb + const run = store.createRun({ + objective: 'loopback coordinator', + coordinatorHandle: 'term_coord', + coordinatorPaneKey: 'tab_coord:pane_coord' + }) + const task = store.createTask({ runId: run.id, spec: 'loopback task' }) + const { dispatch } = store.createStartingWorkerDispatch({ + creator: { kind: 'system' }, + maxDepth: Number.MAX_SAFE_INTEGER, + taskId: task.id, + startOptions: {} + }) + attach(store, dispatch.id, run.id) + expect(store.getRemoteDispatchAttachment(dispatch.id)?.home_run_id).toBe(dispatch.run_id) + expect(store.getRun(run.id)).toEqual(run) + const message = { + dispatchId: dispatch.id, + messageId: 'msg_loopback', + payload: encodeFederatedControlMessage({ + from: 'term_coord', + subject: 'continue', + body: 'loopback instruction', + type: 'dispatch', + priority: 'normal', + threadId: null, + payload: null + }) + } + expect(importFederatedControlMessage(store, message).imported).toBe(true) + expect(importFederatedControlMessage(store, message).imported).toBe(false) + expect(store.getMessageById(message.messageId)?.run_id).toBe(run.id) + const delivered = await check(ctx) + expect(delivered.runId).toBe(run.id) + expect(delivered.messages.map((entry) => entry.id)).toEqual([message.messageId]) + expect((await check(ctx, { ack: delivered.deliveryId })).count).toBe(0) + }) + + it('refuses an attachment with no home Run before writing a Delivery', async () => { + const ctx = launch(':memory:') + const store = db as OrchestrationDb + attach(store, DISPATCH_ID, 'run_coordinator') + const attachment = store.getRemoteDispatchAttachment(DISPATCH_ID)! + vi.spyOn(store, 'findActiveRemoteAttachmentForPane').mockReturnValue({ + ...attachment, + home_run_id: undefined + } as never) + await expect(check(ctx)).rejects.toThrow() + expect(store.db.prepare('SELECT id FROM deliveries').all()).toEqual([]) + }) +}) diff --git a/src/main/runtime/rpc/methods/orchestration/messaging/check-worker.ts b/src/main/runtime/rpc/methods/orchestration/messaging/check-worker.ts index 27df8fd2afa..355a12be5c1 100644 --- a/src/main/runtime/rpc/methods/orchestration/messaging/check-worker.ts +++ b/src/main/runtime/rpc/methods/orchestration/messaging/check-worker.ts @@ -3,7 +3,6 @@ import type { OrcaRuntimeService } from '../../../../orca-runtime' import { OrchestrationError } from '../../../../orchestration/orchestration-error' import { formatMessageBanner } from '../../../../orchestration/formatter' import { exposeMessages } from './mailbox-message-receipt' -import { ORCHESTRATION_LEGACY_RUN_ID } from '../../../../../../shared/orchestration-rpc-contract' import { routeAllMailboxPages } from '../schemas' import { asDispatchFence, callerHoldsDispatchPane, dispatchFenced } from './dispatch-mailbox-fence' import type { CheckParams } from '../schemas' @@ -46,13 +45,15 @@ export async function checkWorkerMailbox(args: { : remoteAttachment ? { dispatchId: remoteAttachment.dispatch_id, - runId: undefined, + runId: remoteAttachment.home_run_id, generation: remoteAttachment.consumer_generation } : undefined if (!workerMailbox) { return undefined } + const deliveryRunId = workerMailbox.runId + db.requireRun(deliveryRunId) const address = `dispatch:${workerMailbox.dispatchId}` // Why: a federated worker host has no dispatch_contexts row, so its generation lives on the // remote_dispatch_attachments row instead. @@ -164,7 +165,6 @@ export async function checkWorkerMailbox(args: { } } await revalidateWorkerMailbox() - const deliveryRunId = workerMailbox.runId ?? ORCHESTRATION_LEGACY_RUN_ID let acknowledged try { acknowledged = params.ack diff --git a/src/main/runtime/rpc/methods/orchestration/runs/migration-behavior.test.ts b/src/main/runtime/rpc/methods/orchestration/runs/migration-behavior.test.ts index d372c733246..929dd5c1ee3 100644 --- a/src/main/runtime/rpc/methods/orchestration/runs/migration-behavior.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/runs/migration-behavior.test.ts @@ -31,7 +31,7 @@ describe('orchestration migration behavior', () => { it('lists an explicitly selected legacy Run without binding or mutation', async () => { const { db, runtime } = createRuntime() - const task = db.createTask({ spec: 'pre-upgrade work' }) + const task = db.createTask({ runId: 'run_legacy_local', spec: 'pre-upgrade work' }) const taskList = ORCHESTRATION_METHODS.find( (method) => method.name === 'orchestration.taskList' )! @@ -55,6 +55,7 @@ describe('orchestration migration behavior', () => { it('formats legacy terminal inspection as read-only without consuming mail', async () => { const { db, runtime } = createRuntime() const message = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_worker', to: 'term_coord', subject: 'still working', @@ -79,6 +80,7 @@ describe('orchestration migration behavior', () => { // A consuming check refuses a handle with no live pane before it reads any mail. vi.spyOn(runtime, 'getTerminalPaneKey').mockReturnValue('tab_legacy:leaf_legacy') const message = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_worker', to: 'term_coord', subject: 'still working' @@ -98,6 +100,7 @@ describe('orchestration migration behavior', () => { it('rejects replies to legacy mail without marking or inserting rows', async () => { const { db, runtime } = createRuntime() const message = db.insertMessage({ + runId: 'run_legacy_local', from: 'term_worker', to: 'term_coord', subject: 'legacy question' diff --git a/src/main/runtime/rpc/methods/orchestration/worker/failed-start-residual-terminal.test.ts b/src/main/runtime/rpc/methods/orchestration/worker/failed-start-residual-terminal.test.ts index bdf5daad565..413a397462b 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/failed-start-residual-terminal.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/failed-start-residual-terminal.test.ts @@ -131,7 +131,7 @@ describe('failed worker-start receipt for a residual terminal', () => { function failStart(residual: boolean): { recovery?: string } { const d = (db = new OrchestrationDb(':memory:')) - const task = d.createTask({ spec: 'residual receipt' }) + const task = d.createTask({ runId: 'run_legacy_local', spec: 'residual receipt' }) const started = d.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, diff --git a/src/main/runtime/rpc/methods/orchestration/worker/legacy-dispatch-projection.test.ts b/src/main/runtime/rpc/methods/orchestration/worker/legacy-dispatch-projection.test.ts index 4df73994beb..75ad57f3a12 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/legacy-dispatch-projection.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/legacy-dispatch-projection.test.ts @@ -117,6 +117,6 @@ describe('pre-v3 dispatch rows in worker-list', () => { }) expect(worker.projection.attention.categories).toContain('unverifiable') expect(worker.projection.attention.requiresAction).toBe(true) - expect(worker.projection.nextAction.kind).toBe('inspect') + expect(worker.projection.nextAction).toEqual({ kind: 'none', argv: [] }) }) }) diff --git a/src/main/runtime/rpc/methods/orchestration/worker/manual-dispatch-observation.test.ts b/src/main/runtime/rpc/methods/orchestration/worker/manual-dispatch-observation.test.ts index 4cd8810ad6b..2890fa08938 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/manual-dispatch-observation.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/manual-dispatch-observation.test.ts @@ -242,7 +242,13 @@ describe('manual Dispatch observation', () => { const result = (await workerListMethod.handler( workerListMethod.params?.parse({ run: run.id }), { runtime } - )) as { workers: { dispatchId: string; workerState: string; terminalState: string | null }[] } + )) as { + workers: { + dispatchId: string + workerState: string + terminalState: string | null + }[] + } expect(result.workers).toEqual([ expect.objectContaining({ @@ -262,7 +268,10 @@ describe('manual Dispatch observation', () => { const runtime = new OrcaRuntimeService() runtime.setOrchestrationDb(db) const closeTerminal = vi.spyOn(runtime, 'closeTerminal') - const task = db.createTask({ spec: 'operator-owned lane' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'operator-owned lane' + }) const dispatch = createRootDispatch( db, task.id, diff --git a/src/main/runtime/rpc/methods/structured-worker-stop-receipt.test.ts b/src/main/runtime/rpc/methods/structured-worker-stop-receipt.test.ts index 82cc53ff735..151285ffb1e 100644 --- a/src/main/runtime/rpc/methods/structured-worker-stop-receipt.test.ts +++ b/src/main/runtime/rpc/methods/structured-worker-stop-receipt.test.ts @@ -62,7 +62,10 @@ describe('worker-stop on a structured worker this runtime cannot reach', () => { worktreeId: WORKTREE, hostScope: { kind: 'local', hostId: 'local' } }) - const task = db.createTask({ spec: 'stop a structured worker' }) + const task = db.createTask({ + runId: 'run_legacy_local', + spec: 'stop a structured worker' + }) const started = db.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, diff --git a/src/main/runtime/rpc/orchestration-11745-regression-verification.test.ts b/src/main/runtime/rpc/orchestration-11745-regression-verification.test.ts index 47d585ca244..7a644cc9def 100644 --- a/src/main/runtime/rpc/orchestration-11745-regression-verification.test.ts +++ b/src/main/runtime/rpc/orchestration-11745-regression-verification.test.ts @@ -642,7 +642,11 @@ function createAdoptedDb(options: { settleWork: boolean }): { const dbPath = join(dir, 'orchestration.db') const before = new OrchestrationDb(dbPath) - const task = before.createTask({ spec: 'legacy assignment', createdByTerminalHandle: 'term_old' }) + const task = before.createTask({ + runId: 'run_legacy_local', + spec: 'legacy assignment', + createdByTerminalHandle: 'term_old' + }) createRootDispatch( before, task.id, @@ -650,6 +654,7 @@ function createAdoptedDb(options: { settleWork: boolean }): { 'tab_old:33333333-3333-4333-8333-333333333333' ) const recovery = before.insertMessage({ + runId: 'run_legacy_local', from: 'term_old_worker', to: 'term_old', subject: 'recovered worker outcome', diff --git a/src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher-test-fixture.ts b/src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher-test-fixture.ts index cca68da8f63..faf934a6d66 100644 --- a/src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher-test-fixture.ts +++ b/src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher-test-fixture.ts @@ -53,6 +53,7 @@ export function createHarness(): LegacyCompatibilityDispatcherHarness { const dbPath = join(dir, 'orchestration.db') const before = new OrchestrationDb(dbPath) const task = before.createTask({ + runId: 'run_legacy_local', spec: 'legacy assignment', createdByTerminalHandle: COORDINATOR_HANDLE }) diff --git a/src/main/runtime/rpc/orchestration-legacy-coordinator-race.test.ts b/src/main/runtime/rpc/orchestration-legacy-coordinator-race.test.ts index 3040c37a9ef..71daf09b0e3 100644 --- a/src/main/runtime/rpc/orchestration-legacy-coordinator-race.test.ts +++ b/src/main/runtime/rpc/orchestration-legacy-coordinator-race.test.ts @@ -43,6 +43,7 @@ function createHarness(): Harness { const dbPath = join(dir, 'orchestration.db') const before = new OrchestrationDb(dbPath) const task = before.createTask({ + runId: 'run_legacy_local', spec: 'legacy assignment', createdByTerminalHandle: COORDINATOR_HANDLE }) diff --git a/src/main/runtime/rpc/orchestration-legacy-question-takeover.test.ts b/src/main/runtime/rpc/orchestration-legacy-question-takeover.test.ts index 77d7e2d5a02..82c59f39587 100644 --- a/src/main/runtime/rpc/orchestration-legacy-question-takeover.test.ts +++ b/src/main/runtime/rpc/orchestration-legacy-question-takeover.test.ts @@ -40,12 +40,14 @@ function createHarness(options?: { seedCutoverQuestion?: boolean; seedCutoverAns const dbPath = join(dir, 'orchestration.db') const before = new OrchestrationDb(dbPath) const task = before.createTask({ + runId: 'run_legacy_local', spec: 'legacy assignment', createdByTerminalHandle: COORDINATOR_HANDLE }) const dispatch = createRootDispatch(before, task.id, WORKER_HANDLE, WORKER_PANE) const cutoverQuestion = options?.seedCutoverQuestion ? before.insertMessage({ + runId: 'run_legacy_local', from: WORKER_HANDLE, to: COORDINATOR_HANDLE, subject: 'Question', @@ -60,6 +62,7 @@ function createHarness(options?: { seedCutoverQuestion?: boolean; seedCutoverAns const cutoverAnswer = cutoverQuestion && options?.seedCutoverAnswer ? before.insertMessage({ + runId: 'run_legacy_local', from: COORDINATOR_HANDLE, to: WORKER_HANDLE, subject: 'Re: Question', @@ -308,7 +311,10 @@ describe('legacy question takeover compatibility', () => { resumed as { result: { legacyCompatibility: { - answerAcknowledgement: { questionId: string; answerMessageId: string } + answerAcknowledgement: { + questionId: string + answerMessageId: string + } } } } diff --git a/src/main/runtime/rpc/orchestration-legacy-takeover-delivery.test.ts b/src/main/runtime/rpc/orchestration-legacy-takeover-delivery.test.ts index bcaf5fca11f..feb3017b538 100644 --- a/src/main/runtime/rpc/orchestration-legacy-takeover-delivery.test.ts +++ b/src/main/runtime/rpc/orchestration-legacy-takeover-delivery.test.ts @@ -51,6 +51,7 @@ function createHarness(): Harness { const dbPath = join(dir, 'orchestration.db') const before = new OrchestrationDb(dbPath) const task = before.createTask({ + runId: 'run_legacy_local', spec: 'legacy assignment', createdByTerminalHandle: COORDINATOR_HANDLE }) diff --git a/src/main/runtime/rpc/orchestration-legacy-takeover-dispatcher.test.ts b/src/main/runtime/rpc/orchestration-legacy-takeover-dispatcher.test.ts index 2a2d6b4937b..e5a05fe7d26 100644 --- a/src/main/runtime/rpc/orchestration-legacy-takeover-dispatcher.test.ts +++ b/src/main/runtime/rpc/orchestration-legacy-takeover-dispatcher.test.ts @@ -47,6 +47,7 @@ function createHarness(): Harness { const dbPath = join(dir, 'orchestration.db') const before = new OrchestrationDb(dbPath) const task = before.createTask({ + runId: 'run_legacy_local', spec: 'legacy assignment', createdByTerminalHandle: COORDINATOR_HANDLE }) diff --git a/src/main/runtime/rpc/orchestration-mutation-ledger.test.ts b/src/main/runtime/rpc/orchestration-mutation-ledger.test.ts index d44d3f153d7..b87f595f4b2 100644 --- a/src/main/runtime/rpc/orchestration-mutation-ledger.test.ts +++ b/src/main/runtime/rpc/orchestration-mutation-ledger.test.ts @@ -44,7 +44,7 @@ describe('durable orchestration mutation ledger', () => { const runtime = new OrcaRuntimeService() runtime.setOrchestrationDb(db) const effect = vi.fn((subject: string) => - db.insertMessage({ from: 'caller', to: 'recipient', subject }) + db.insertMessage({ runId: 'run_legacy_local', from: 'caller', to: 'recipient', subject }) ) const dispatcher = new RpcDispatcher({ runtime, @@ -299,7 +299,10 @@ describe('durable orchestration mutation ledger', () => { const db = new OrchestrationDb(':memory:') const runtime = new OrcaRuntimeService() runtime.setOrchestrationDb(db) - const params = { from: 'term_coord', task: db.createTask({ spec: 'restart' }).id } + const params = { + from: 'term_coord', + task: db.createTask({ runId: 'run_legacy_local', spec: 'restart' }).id + } const callerFingerprint = db.getOrCreateLocalMutationCallerFingerprint() const payloadHash = createHash('sha256') .update(JSON.stringify({ method: 'orchestration.workerStart', params })) diff --git a/src/main/runtime/rpc/orchestration-mutation-request-show.test.ts b/src/main/runtime/rpc/orchestration-mutation-request-show.test.ts index cb31ea22736..e64fb5b9640 100644 --- a/src/main/runtime/rpc/orchestration-mutation-request-show.test.ts +++ b/src/main/runtime/rpc/orchestration-mutation-request-show.test.ts @@ -22,7 +22,7 @@ function createHarness() { const runtime = new OrcaRuntimeService() runtime.setOrchestrationDb(db) const effect = vi.fn((subject: string) => - db.insertMessage({ from: 'caller', to: 'recipient', subject }) + db.insertMessage({ runId: 'run_legacy_local', from: 'caller', to: 'recipient', subject }) ) const dispatcher = new RpcDispatcher({ runtime, diff --git a/src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts b/src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts index b2d3d110627..4172097853d 100644 --- a/src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts +++ b/src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts @@ -62,6 +62,7 @@ function createUpdateHarness(): Harness { const oldRuntimeDb = new OrchestrationDb(dbPath) const task = oldRuntimeDb.createTask({ + runId: 'run_legacy_local', spec: 'finish work across an app update', createdByTerminalHandle: COORDINATOR_HANDLE }) diff --git a/src/main/runtime/runtime-legacy-worker-terminal-resume-fence.test.ts b/src/main/runtime/runtime-legacy-worker-terminal-resume-fence.test.ts index 6fe14f2abe7..4d1f81869d5 100644 --- a/src/main/runtime/runtime-legacy-worker-terminal-resume-fence.test.ts +++ b/src/main/runtime/runtime-legacy-worker-terminal-resume-fence.test.ts @@ -60,7 +60,7 @@ describe('settled worker automatic-resume fence persistence', () => { getWorkspaceSessionHostIds: () => [LOCAL_EXECUTION_HOST_ID], flushOrThrow: vi.fn() } as unknown as RuntimeStore - const task = orchestrationDb.createTask({ spec: 'fence me' }) + const task = orchestrationDb.createTask({ runId: 'run_legacy_local', spec: 'fence me' }) const started = orchestrationDb.createStartingWorkerDispatch({ creator: { kind: 'system' }, maxDepth: Number.MAX_SAFE_INTEGER, diff --git a/src/main/runtime/runtime-rpc-request-authorization.test.ts b/src/main/runtime/runtime-rpc-request-authorization.test.ts index d7e7576bc27..5ff19f94563 100644 --- a/src/main/runtime/runtime-rpc-request-authorization.test.ts +++ b/src/main/runtime/runtime-rpc-request-authorization.test.ts @@ -92,9 +92,19 @@ describe('OrcaRuntimeRpcServer', () => { } try { - db.insertMessage({ from: 'worker', to: 'coordinator', subject: 'before reset' }) + db.insertMessage({ + runId: 'run_legacy_local', + from: 'worker', + to: 'coordinator', + subject: 'before reset' + }) const first = await resetMessages('reset-first', firstDevice.token) - db.insertMessage({ from: 'worker', to: 'coordinator', subject: 'after reset' }) + db.insertMessage({ + runId: 'run_legacy_local', + from: 'worker', + to: 'coordinator', + subject: 'after reset' + }) const replay = await resetMessages('reset-replay', firstDevice.token) expect(first).toMatchObject({ @@ -129,6 +139,7 @@ describe('OrcaRuntimeRpcServer', () => { const device = server['deviceRegistry']!.addDevice('existing-cli', 'runtime') const existingFingerprint = createHash('sha256').update(device.token).digest('hex') db.createRemoteDispatchAttachment({ + runId: 'run_home', dispatchId: 'ctx_existing_remote', taskId: 'task_existing_remote', homePeerFingerprint: existingFingerprint, diff --git a/src/shared/orchestration-fleet-projection.test.ts b/src/shared/orchestration-fleet-projection.test.ts index f8cc10de176..5941c696c88 100644 --- a/src/shared/orchestration-fleet-projection.test.ts +++ b/src/shared/orchestration-fleet-projection.test.ts @@ -137,7 +137,7 @@ describe('orchestration fleet projection', () => { host: { kind: 'local' }, liveness: { verdict: 'unverifiable', reason: 'missing_status' }, resource: { state: 'absent', reason: 'unsupervised' }, - nextAction: { kind: 'inspect' } + nextAction: { kind: 'none' } }) }) @@ -229,6 +229,7 @@ describe('orchestration fleet projection', () => { expect(second.workers.at(-1)?.id).toBe('dispatch-109') }) + // Cleanup still earns a command when liveness is unverifiable. it('suggests release only for reclaimable ownership', () => { const result = projectOrchestrationFleet({ workers: [worker('done', { terminalState: 'reclaimable' })], @@ -236,6 +237,7 @@ describe('orchestration fleet projection', () => { now: 1 }) + expect(result.workers[0]?.liveness.verdict).toBe('unverifiable') expect(result.workers[0]?.nextAction).toEqual({ kind: 'release', argv: ['orchestration', 'worker-release', '--dispatch', 'done'] @@ -487,7 +489,8 @@ describe('fleet liveness and attention after a host verdict', () => { verdict: 'unverifiable', reason: 'missing_status' }) - expect(projected.workers[0]!.nextAction.kind).toBe('inspect') + // `recover` is reserved for a proven exit; worker-show would only restate this row. + expect(projected.workers[0]!.nextAction).toEqual({ kind: 'none', argv: [] }) }) it('certifies a process_exited stage whose exit was observed', () => { @@ -519,15 +522,19 @@ describe('fleet liveness and attention after a host verdict', () => { expect(projected.workers[0]!.nextAction).toEqual({ kind: 'none', argv: [] }) }) - it('keeps an unverifiable worker on inspect: absence is never authority to stop', () => { + // worker-show repeats this projection, so inspecting again would loop. + it('asks nothing of an unverifiable worker instead of looping on worker-show', () => { const now = 10 * AGENT_STATUS_STALE_AFTER_MS const projected = projectOrchestrationFleet({ workers: [worker('1')], statuses: [status('1', now - AGENT_STATUS_STALE_AFTER_MS - 60_000)], now }) - expect(projected.workers[0]!.liveness.verdict).toBe('unverifiable') - expect(projected.workers[0]!.nextAction.kind).toBe('inspect') + expect(projected.workers[0]!.liveness).toMatchObject({ + verdict: 'unverifiable', + reason: 'stale_status' + }) + expect(projected.workers[0]!.nextAction).toEqual({ kind: 'none', argv: [] }) }) it('leaves a worker blocked on a question inspectable rather than recoverable', () => { @@ -539,6 +546,22 @@ describe('fleet liveness and attention after a host verdict', () => { expect(projected.workers[0]!.nextAction.kind).toBe('inspect') }) + it.each([{ pendingInput: true }, { pendingApproval: true }])( + 'keeps an unverifiable worker with %o inspectable', + (pending) => { + const projected = projectOrchestrationFleet({ + workers: [worker('1', pending)], + statuses: [], + now: 10_000 + }) + expect(projected.workers[0]!.liveness.verdict).toBe('unverifiable') + expect(projected.workers[0]!.nextAction).toEqual({ + kind: 'inspect', + argv: ['orchestration', 'worker-show', '--dispatch', '1'] + }) + } + ) + // The live worker-list row from a stopped worker: the same receipt proved the exit, // called it absence, and pointed back at the command that reported the settlement. it('never contradicts a proven exit on a stopped worker still owning its terminal', () => { diff --git a/src/shared/orchestration-fleet-worker-projection.ts b/src/shared/orchestration-fleet-worker-projection.ts index a463ca299df..aec9377ea82 100644 --- a/src/shared/orchestration-fleet-worker-projection.ts +++ b/src/shared/orchestration-fleet-worker-projection.ts @@ -176,6 +176,10 @@ export function projectFleetNextAction( ) { return { kind: 'none', argv: [] } } + // worker-show repeats this projection; absence alone cannot earn another command. + if (liveness.verdict === 'unverifiable' && !worker.pendingInput && !worker.pendingApproval) { + return { kind: 'none', argv: [] } + } return { kind: 'inspect', argv: ['orchestration', 'worker-show', '--dispatch', worker.dispatchId]