Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2f5930b
fix(orchestration): refuse run-use by a worker on the Run it works for
brennanb2025 Oct 15, 2026
110fb8f
fix(orchestration): tell a worker naming its own Run where its mail is
brennanb2025 Oct 15, 2026
f32d591
feat(orchestration): refuse run-use over a live coordinator without -…
brennanb2025 Oct 15, 2026
0d918d4
fix(orchestration): stop pointing workers at run-use
brennanb2025 Oct 15, 2026
51899d8
fix(orchestration): stop routing mail to addresses that grabbed a Run…
brennanb2025 Oct 15, 2026
7e73137
fix(cli): keep the orchestration spec file within its line limit
brennanb2025 Oct 15, 2026
e00c287
fix(orchestration): read remembered coordinator Run ids without a typ…
brennanb2025 Oct 15, 2026
a9a9dbb
fix(orchestration): compare seat-grab times at whole seconds
brennanb2025 Oct 15, 2026
0ee076e
fix(orchestration): a worker coordinates only Runs it created
brennanb2025 Oct 15, 2026
da2e196
fix(orchestration): say what happens to mail on takeover and stop nam…
brennanb2025 Oct 15, 2026
2a68fbb
refactor(orchestration): throw one non-null refusal from the live gate
brennanb2025 Oct 15, 2026
2b3b672
refactor(runtime): host the coordinator presence probe beside the hoo…
brennanb2025 Oct 15, 2026
dc1730a
fix(orchestration): use findLast for the worktree name
brennanb2025 Oct 15, 2026
cf73598
refactor(runtime): move the coordinator presence probe beside the pty…
brennanb2025 Oct 15, 2026
6c1eb61
fix(orchestration): keep workers out of their own Run seat
brennanb2025 Oct 15, 2026
a118df0
fix(orchestration): clarify worker Run handoffs in skill
brennanb2025 Oct 15, 2026
69818e2
test: align explicit Run gate refusal wording
brennanb2025 Oct 16, 2026
5bab97a
Merge remote-tracking branch 'origin/main' into brennanb2025/worker-c…
brennanb2025 Oct 16, 2026
7cef60c
Move coordinator presence beside terminal presence checks
brennanb2025 Oct 16, 2026
e066e0d
Merge remote-tracking branch 'origin/main' into brennanb2025/worker-c…
brennanb2025 Oct 16, 2026
61ffe2d
Walk complete worker creator lineage when binding Runs
brennanb2025 Oct 16, 2026
5688a64
Merge origin/main into brennanb2025/worker-cannot-bind-parent-run
brennanb2025 Oct 22, 2026
ff924f6
Restore main documentation after merge hook formatting
brennanb2025 Oct 22, 2026
0a85815
Merge origin/main into worker run binding fix
brennanb2025 Oct 23, 2026
e2e2ddf
perf(orchestration): check run bindings without terminal snapshots
brennanb2025 Oct 23, 2026
3787d57
test(orchestration): narrow SQLite instrumentation before access
brennanb2025 Oct 23, 2026
f5eccb8
test(orchestration): import the explicit RPC methods module
brennanb2025 Oct 23, 2026
ab1c437
test: cover orchestration restart and continued Run handoff
brennanb2025 Oct 23, 2026
dbadf5e
test: place orchestration CLI integration in CLI project
brennanb2025 Oct 23, 2026
d3c6c99
test(cli): use explicit module extensions in restart handoff test
brennanb2025 Oct 23, 2026
a865bf9
test(orchestration): isolate source CLI integration in subprocesses
brennanb2025 Oct 23, 2026
b2fbefe
refactor(orchestration): leave the live-coordinator takeover rule to …
brennanb2025 Oct 25, 2026
b9662bc
test(orchestration): cover delegated worker legacy takeover refusal
brennanb2025 Oct 26, 2026
6754bd2
Merge origin/main into orchestration worker binding fix
brennanb2025 Oct 26, 2026
fc7b415
test(orchestration): explain source CLI integration timeout
brennanb2025 Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/vitest.performance.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const contracts = [
'src/main/runtime/rpc/methods/repo.test.ts',
'src/renderer/src/components/tab-bar/use-new-tab-agent-menu-host-preferences.test.tsx',
'src/main/sqlite/sync-database.test.ts',
'src/main/runtime/orchestration/run-binding-performance.test.ts',
'src/main/runtime/orchestration/db/row-column-lists.test.ts',
'src/shared/native-chat/tools/native-chat-tool-attribution-allocation.test.ts',
'src/shared/native-chat/tools/native-chat-tool-pairing.performance.test.ts',
Expand Down
6 changes: 6 additions & 0 deletions skill-guides/orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ ORCA orchestration worker-start --spec "<worker B task>" --worktree current --ag
ORCA orchestration check --wait --types "worker_done,escalation,question" --timeout-ms 900000 --json
```

After a restart, resume your own Run with `run-use --id <run_id>`. A worker
cannot bind the Run it works for or an ancestor, and cannot use
`--takeover-legacy`; an already-bound no-op remains allowed. When its
coordinator directs a handoff, a replacement worker may resume a separate Run
with plain `run-use`.

If `worker-start` exits non-zero, do not relaunch. Read the receipt's
`failedStage` and `residualResources`, then load
`references/recovery-and-cleanup.md`.
Expand Down
7 changes: 6 additions & 1 deletion skill-guides/orchestration/references/worker-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ run — and once more immediately before `worker_done`, so a redirect or a
cancellation lands before the Task settles. `check` names its caller with
`--terminal`, never `--from`. Stop checking after `worker_done`.

If `check` returns `consumer_fenced`, this process no longer owns its Dispatch:
Do not use `run-use` to check your Dispatch's mail or bind the Run you work for.
You may resume a separate Run when your coordinator directs a handoff, but you
cannot use `--takeover-legacy`. Plain `check` returns your Dispatch's mail
unless you are coordinating a Run; report to your coordinator with `send`.

If a plain `check` (no `--run`) returns `consumer_fenced`, this process no longer owns its Dispatch:
the Attempt was re-attached to another worker or settled without you. Stop, do
not send `worker_done`, and do not retry the check. An empty `check` never means
you were replaced; `consumer_fenced` is the only way you learn that.
Expand Down
6 changes: 3 additions & 3 deletions src/cli/bundled-skill-guides.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/cli/root-help-text-primary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const ROOT_HELP_TEXT_PRIMARY = [
'',
'Orchestration:',
' orchestration run-create Create and bind a lightweight orchestration Run',
' orchestration run-use Bind this coordinator to an existing Run',
' orchestration run-use Resume coordinating a Run (workers never use this)',
" orchestration run-current Show this coordinator's bound Run",
' orchestration run-list List lightweight orchestration Runs',
' orchestration run-show Show one lightweight orchestration Run',
Expand Down
4 changes: 2 additions & 2 deletions src/cli/specs/orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
},
{
path: ['orchestration', 'run-use'],
summary: 'Bind this coordinator to an existing Run',
summary: 'Resume coordinating a Run',
usage:
'orca orchestration run-use --id <run_id> [--from <handle>] [--takeover-legacy] [--retry-request <id>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'id', 'from', 'takeover-legacy', 'retry-request'],
identityFlagRoles: { from: 'caller' },
notes: [
'--takeover-legacy must run in the live coordinator agent terminal it binds; it preserves existing worker assignments.'
'A worker cannot bind its own or ancestor Run or use --takeover-legacy. --takeover-legacy must run in the live coordinator agent terminal it binds; it preserves existing worker assignments.'
]
},
{
Expand Down
279 changes: 279 additions & 0 deletions src/main/runtime/orchestration-cli-restart-handoff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import { spawn } from 'node:child_process'
import { mkdtempSync, rmSync } from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, expect, it, vi } from 'vitest'
import { z } from 'zod'
import { OrchestrationDb } from './orchestration/db'

const cliPath = join(process.cwd(), 'src/cli/index.ts')
const tsxLoader = pathToFileURL(createRequire(import.meta.url).resolve('tsx')).href

const entity = z.object({ id: z.string() })
const check = z.object({
result: z.object({
runId: z.string(),
deliveryId: z.string(),
messages: z.array(z.object({ id: z.string() }).passthrough())
})
})

function idOf(response: unknown, key: string): string {
const { result } = z.object({ result: z.record(z.string(), z.unknown()) }).parse(response)
return entity.parse(result[key]).id
}

afterEach(() => {
vi.unstubAllEnvs()
vi.restoreAllMocks()
})

// Sequential source-CLI subprocesses and a disk-backed runtime restart exceed the default test budget.
it("retains CLI work and mail across restart, refuses a worker's own Run, and completes a separate Run handoff", async () => {
const home = mkdtempSync(join(tmpdir(), 'orca-cli-restart-handoff-'))
for (const name of [
'HOME',
'USERPROFILE',
'APPDATA',
'LOCALAPPDATA',
'XDG_CONFIG_HOME',
'XDG_CACHE_HOME',
'XDG_DATA_HOME',
'XDG_STATE_HOME',
'XDG_RUNTIME_DIR',
'CLAUDE_CONFIG_DIR',
'CODEX_HOME',
'ORCA_E2E_HOME_DIR',
'ORCA_E2E_USER_DATA_DIR',
'ORCA_USER_DATA_PATH'
]) {
vi.stubEnv(name, home)
}
vi.stubEnv('ORCA_BACKGROUND_LAUNCH', '1')
vi.stubEnv('ORCA_DISABLE_MACOS_LOGIN_SHELL', '1')
for (const name of [
'ORCA_ENVIRONMENT',
'ORCA_PAIRING_CODE',
'ORCA_REMOTE_PAIRING',
'ORCA_AGENT_SESSION_ID',
'ORCA_RUNTIME_SOURCE_ID',
'ORCA_RUNTIME_SOURCE_INCARNATION',
'ORCA_RUNTIME_SOURCE_PROFILE_PATH'
]) {
vi.stubEnv(name, '')
}
const { OrcaRuntimeRpcServer } = await import('./runtime-rpc')
const fixture = await import('./orchestration-mailbox-notification-test-harness')
const { registerBoundPty } = await import('./bind-first-pane.test-fixture')
const coord = fixture.TERMINAL_HANDLE
const worker = fixture.SECOND_TERMINAL_HANDLE
const predecessor = 'term_predecessor'
const predecessorTab = '55555555-5555-4555-8555-555555555555'
const predecessorLeaf = '66666666-6666-4666-8666-666666666666'
let db = new OrchestrationDb(join(home, 'orchestration.db'))

function runtimeForDatabase() {
const { runtime } = fixture.createRuntime(db)
fixture.registerSecondPane(runtime)
registerBoundPty(runtime, 'pty-predecessor', fixture.WORKTREE_ID, null, {
tabId: predecessorTab,
leafId: predecessorLeaf,
incarnationId: 'predecessor-incarnation'
})
runtime.registerPreAllocatedHandleForPty('pty-predecessor', predecessor)
vi.spyOn(runtime, 'getNestedWorkerMaxDepth').mockReturnValue(2)
return runtime
}
let runtime = runtimeForDatabase()
let server = new OrcaRuntimeRpcServer({ runtime, userDataPath: home })
async function cli(handle: string, args: string[], exitCode = 0): Promise<unknown> {
const child = spawn(
process.execPath,
[
'--import',
tsxLoader,
'--conditions=orca-source',
cliPath,
'orchestration',
...args,
'--json'
],
{
cwd: home,
env: {
...process.env,
ORCA_TERMINAL_HANDLE: handle,
ORCA_PANE_KEY: runtime.getTerminalPaneKey(handle) ?? '',
ORCA_AGENT_LAUNCH_TOKEN:
handle === coord
? fixture.LAUNCH_TOKEN
: handle === worker
? fixture.SECOND_LAUNCH_TOKEN
: ''
},
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
timeout: 15_000
}
)
let stdout = ''
let stderr = ''
child.stdout.setEncoding('utf8').on('data', (chunk: string) => {
stdout += chunk
})
child.stderr.setEncoding('utf8').on('data', (chunk: string) => {
stderr += chunk
})
const code = await new Promise<number | null>((resolveExit, rejectExit) => {
child.once('close', resolveExit)
child.once('error', rejectExit)
})
expect(code, JSON.stringify({ args, stdout, stderr })).toBe(exitCode)
const response: unknown = JSON.parse(stdout)
expect(response).toMatchObject({ ok: exitCode === 0 })
return response
}

try {
await server.start()
const root = idOf(
await cli(coord, ['run-create', '--objective', 'Retained parent work']),
'run'
)
const task = idOf(await cli(coord, ['task-create', '--spec', 'Finish after restart']), 'task')
const dispatch = idOf(
await cli(coord, ['dispatch', '--task', task, '--to', worker]),
'dispatch'
)
const separate = idOf(
await cli(predecessor, ['run-create', '--objective', 'Predecessor work']),
'run'
)
const continuation = idOf(
await cli(predecessor, ['task-create', '--spec', 'Continue predecessor work']),
'task'
)
const retainedMessage = idOf(
await cli(worker, ['send', '--subject', 'Retain this report']),
'message'
)
const retainedBatch = check.parse(await cli(coord, ['check'])).result
expect(retainedBatch).toMatchObject({ runId: root, messages: [{ id: retainedMessage }] })

await server.stop()
db.close()
db = new OrchestrationDb(join(home, 'orchestration.db'))
runtime = runtimeForDatabase()
server = new OrcaRuntimeRpcServer({ runtime, userDataPath: home })
await server.start()
expect(await cli(coord, ['run-current'])).toMatchObject({ result: { run: { id: root } } })
expect(await cli(coord, ['task-list'])).toMatchObject({
result: { tasks: [{ id: task, status: 'dispatched', dispatch_id: dispatch }] }
})
expect(check.parse(await cli(coord, ['check'])).result).toEqual(retainedBatch)

const bindingBefore = db.getRunRaw(root)
expect(await cli(worker, ['run-use', '--id', root], 1)).toMatchObject({
error: { code: 'worker_cannot_coordinate_own_run', data: { effectsApplied: false } }
})
expect(db.getRunRaw(root)).toEqual(bindingBefore)
expect(check.parse(await cli(coord, ['check'])).result).toEqual(retainedBatch)
expect(await cli(coord, ['check', '--ack', retainedBatch.deliveryId])).toMatchObject({
result: { acknowledged: retainedBatch.deliveryId, count: 0 }
})
expect(await cli(worker, ['run-use', '--id', separate])).toMatchObject({
result: { run: { id: separate } }
})

const question = idOf(
await cli(worker, [
'send',
'--to',
`run:${root}`,
'--type',
'question',
'--subject',
'Proceed with predecessor work?'
]),
'message'
)
const questionBatch = check.parse(await cli(coord, ['check'])).result
expect(questionBatch).toMatchObject({
runId: root,
messages: [{ id: question, type: 'question' }]
})
const reply = idOf(
await cli(coord, ['reply', '--id', question, '--body', 'Proceed with the retained task']),
'message'
)
const replyBatch = check.parse(await cli(worker, ['check'])).result
expect(replyBatch).toMatchObject({
runId: separate,
messages: [
{
id: reply,
thread_id: question,
to_handle: `run:${separate}`,
run_id: separate,
body: 'Proceed with the retained task'
}
]
})
expect(db.getMessageById(reply)?.read).toBe(0)
expect(await cli(worker, ['check', '--ack', replyBatch.deliveryId])).toMatchObject({
result: { runId: separate, acknowledged: replyBatch.deliveryId, count: 0 }
})
expect(db.getMessageById(reply)?.read).toBe(1)
await cli(coord, ['check', '--ack', questionBatch.deliveryId])

const continuedDispatch = idOf(
await cli(worker, ['dispatch', '--task', continuation, '--to', predecessor]),
'dispatch'
)
expect(
await cli(predecessor, [
'send',
'--type',
'worker_done',
'--subject',
'Predecessor work complete',
'--payload',
JSON.stringify({
taskId: continuation,
dispatchId: continuedDispatch,
outcome: 'succeeded'
})
])
).toMatchObject({ result: { lifecycle: { action: 'completed' } } })
expect(await cli(worker, ['task-list'])).toMatchObject({
result: { tasks: [{ id: continuation, status: 'completed' }] }
})
expect(await cli(worker, ['dispatch-show', '--task', continuation])).toMatchObject({
result: { dispatch: { id: continuedDispatch, status: 'completed' } }
})

expect(
await cli(worker, [
'send',
'--type',
'worker_done',
'--subject',
'Parent work complete',
'--payload',
JSON.stringify({ taskId: task, dispatchId: dispatch, outcome: 'succeeded' })
])
).toMatchObject({ result: { lifecycle: { action: 'completed' } } })
expect(await cli(coord, ['task-list'])).toMatchObject({
result: { tasks: [{ id: task, status: 'completed' }] }
})
expect(await cli(coord, ['dispatch-show', '--task', task])).toMatchObject({
result: { dispatch: { id: dispatch, status: 'completed' } }
})
} finally {
await server.stop()
db.close()
rmSync(home, { recursive: true, force: true })
}
}, 120_000)
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,8 @@ describe('orchestration notification mailbox consistency', () => {
.map(([sql]) => sql)
.find((sql) => sql.includes('FROM run_coordinator_handles AS coordinator'))
expect(routingSql).toContain('INDEXED BY idx_messages_undelivered_direct_run')
expect(routingSql).not.toContain('NOT EXISTS')
// An anti-join per coordinator row is fine; one per candidate message would scan the backlog.
expect(routingSql).not.toMatch(/NOT EXISTS \([^)]*candidate\./)
expect(db.getMessageById(owned.id)?.to_handle).toBe(`run:${ownedRun.id}`)
expect(db.getUnreadDirectMessageTypes(detachedHandle)).toEqual(['status'])
db.close()
Expand Down
2 changes: 1 addition & 1 deletion src/main/runtime/orchestration/db/dispatch-depth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function recordedCreatorIdentity(creator: DispatchCreator): {
}

/** Self-dispatch is bookkeeping; pre-v37 rows lack creator identity and conservatively count as parents. */
function isSelfCreatedDispatch(row: DispatchContextRow): boolean {
export function isSelfCreatedDispatch(row: DispatchContextRow): boolean {
if (row.creator_pane_key && row.assignee_pane_key) {
return isEquivalentPaneKey(row.creator_pane_key, row.assignee_pane_key)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { DispatchContextRow, MessageType } from '../../types'
import { DISPATCH_PANE_KEY_MATCH_SUFFIX_SQL, paneKeyMatchSuffix } from '../pane-key-match'
import type { OrchestrationDb } from '../orchestration-db'
import { DISPATCH_CONTEXT_COLUMN_LIST } from '../row-column-lists'
import { workerSeatGrabSql } from '../runs/run-coordinator-mail-routing'
import {
ORCHESTRATION_DELIVERY_BATCH_LIMIT,
type ForeignDirectMailboxRoutingPage
Expand Down Expand Up @@ -58,6 +59,7 @@ export function routeForeignDirectMessagesToOwnedMailboxes(
ON candidate.run_id = coordinator.run_id
AND candidate.to_handle = coordinator.terminal_handle
WHERE coordinator.terminal_handle = ?${runExclusion}
AND NOT ${workerSeatGrabSql('coordinator')}
AND candidate.read = 0 AND candidate.delivered_at IS NULL
AND candidate.delivery_contract = 'current_delivery'`,
`SELECT candidate.id, candidate.run_id, candidate.type, candidate.sequence
Expand Down
Loading
Loading