Merge branch 'fix-ergonomics' (early part) into integrate-fixes

This commit is contained in:
Jinwoo-H
2026-09-04 02:23:18 -04:00
27 changed files with 692 additions and 150 deletions
@@ -69,7 +69,7 @@ describe('orchestration kernel', () => {
expect(kernel).toContain('Compatibility operator')
expect(kernel).toContain('Ordinary terminal agent')
expect(kernel).toContain('Model or effort selection does not make a handoff supervised')
expect(kernel).toContain('Never substitute a\nnon-Orca subagent tool')
expect(squash(kernel)).toContain('Never substitute a non-Orca subagent tool')
})
it('makes Dispatch identity, remote uncertainty, folders, and mixed versions a safety floor', () => {
@@ -78,10 +78,10 @@ describe('orchestration kernel', () => {
expect(kernel).toContain('A Dispatch is one authoritative Task attempt')
expect(kernel).toContain('Lifecycle authority comes from the active Dispatch')
expect(kernel).toContain('execution host owns')
expect(kernel).toContain('`live` / `unverifiable` / `exited`')
expect(squash(kernel)).toContain('`live` / `unverifiable` / `exited`')
expect(kernel).toContain('contact loss is not process death')
expect(kernel).toContain('Folder workspaces are valid')
expect(kernel).toContain('Treat unknown optional fields\n as absent')
expect(squash(kernel)).toContain('Treat unknown optional fields as absent')
expect(kernel).toContain('new stream operation requires advertised capability')
expect(kernel).toContain('Never fall back to local execution')
})
@@ -104,37 +104,75 @@ describe('orchestration kernel', () => {
it('teaches worker-start as the only normal-path launch and starts the wave before waiting', () => {
const kernel = readKernel()
const firstStart = kernel.indexOf('worker-start --task <task_a>')
const secondStart = kernel.indexOf('worker-start --task <task_b>')
const firstStart = kernel.indexOf('worker-start --spec "<worker A task>"')
const secondStart = kernel.indexOf('worker-start --spec "<worker B task>"')
const firstWait = kernel.indexOf('check --wait')
expect(firstStart).toBeGreaterThan(kernel.indexOf('run-create'))
expect(secondStart).toBeGreaterThan(firstStart)
expect(firstWait).toBeGreaterThan(secondStart)
expect(kernel).toContain('start the full independent wave before waiting')
expect(squash(kernel)).toContain('start the full independent wave before waiting')
expect(kernel).toContain('`worker-start` is the normal path')
expect(kernel).toContain('operator-created process unsupervised')
expect(kernel).not.toMatch(/^ORCA terminal create/mu)
})
it('makes worker-start --spec the default and keeps task-create for planned fan-out', () => {
const kernel = squash(readKernel())
expect(kernel).toContain('`worker-start --spec` creates the Task and its attempt in one call')
expect(kernel).toContain('Use `task-create` plus `worker-start --task <task_id>`')
})
it('gives the supervised loop an exit condition for a live terminal with a dead agent', () => {
const kernel = squash(readKernel())
expect(kernel).toContain("`worker-list`'s `projection.liveness` is the fleet verdict")
expect(kernel).toContain("`worker-show`'s `observation.status` is PTY liveness only")
expect(kernel).toContain('After three consecutive empty waits')
expect(kernel).toContain('`ORCA orchestration worker-list --json`')
expect(kernel).toContain('`requiresAction`, and literal `nextAction` argv')
expect(kernel).toContain('`attention`')
expect(kernel).toContain('reports `agentWait` null')
expect(kernel).toContain('choose `worker-stop` or `worker-abandon`')
})
it('names --terminal, never --from, as the check caller flag', () => {
const kernel = squash(readKernel())
expect(kernel).toContain('`check` names its caller with `--terminal <handle>`, never `--from`')
expect(kernel).not.toContain('check --from')
})
it('makes a dispatched worker read coordinator follow-ups on a cadence', () => {
const kernel = squash(readKernel())
expect(kernel).toContain('Read coordinator follow-ups at each natural checkpoint')
expect(kernel).toContain('once more immediately before `worker_done`')
expect(kernel).toContain('`ORCA orchestration check --terminal <your_handle> --json`')
})
it('requires full Delivery processing and settled-terminal accounting before ack', () => {
const kernel = readKernel()
expect(kernel).toContain('oldest FIFO Delivery and replays that\nbatch until acknowledged')
expect(kernel).toContain('Process every message')
expect(kernel).toContain("decide each\nsettled terminal's next owner before acknowledging")
expect(squash(kernel)).toContain(
'oldest FIFO Delivery and replays that batch until acknowledged'
)
expect(squash(kernel)).toContain('Process every message')
expect(squash(kernel)).toContain("decide each settled terminal's next owner before the ack")
expect(squash(kernel)).toContain('reused, explicitly retained, or released')
expect(kernel).toContain('worker-release --dispatch <dispatch_id>')
expect(kernel).toContain('check --ack <delivery_id> --wait')
expect(kernel).toContain('Do not follow\nit with `task-update --status completed`')
expect(squash(kernel)).toContain('`worker-list --terminal-state reclaimable --json`')
expect(squash(kernel)).toContain('do not follow it with `task-update --status completed`')
})
it('treats long waits and release uncertainty as safe checkpoints', () => {
const kernel = readKernel()
expect(kernel).toContain('A timeout or empty result is\na checkpoint, not a failure')
expect(kernel).toContain('Do not stop, retry, release, or launch a duplicate\neditor')
expect(kernel).toContain('Never release because of\nidle state, timeout, heartbeat')
expect(squash(kernel)).toContain('A timeout or empty result is a checkpoint, not a failure')
expect(squash(kernel)).toContain('Do not stop, retry, release, or launch a duplicate editor')
expect(squash(kernel)).toContain('Never release because of idle state, timeout, heartbeat')
expect(kernel).toContain('never substitute `terminal close`')
})
@@ -152,7 +190,7 @@ describe('orchestration kernel', () => {
}
expect(kernel).toContain('successful `orchestration send` proves durable enqueue')
expect(kernel).toContain('best-effort attention only')
expect(kernel).toContain('does not prove the recipient read the message')
expect(squash(kernel)).toContain('does not prove the recipient read or accepted it')
})
})
@@ -164,13 +202,18 @@ describe('owned orchestration references', () => {
.filter((name) => name.endsWith('.md'))
.sort()
expect([...new Set(routed)].sort()).toEqual(shipped)
expect(routed).toHaveLength(shipped.length)
expect(kernel).toContain('ORCA skills get orchestration --full')
expect(kernel).toContain(
'returns this exact kernel and every reference from\nthe same CLI build'
const tableRoutes = [...kernel.matchAll(/^\|.*`references\/([^`]+\.md)`.*\|$/gmu)].map(
(match) => match[1]
)
expect(kernel).toContain('If an older CLI rejects\n`--full`')
expect([...new Set(routed)].sort()).toEqual(shipped)
// Why the table and not every mention: prose may cite a reference the gate table already routes.
expect(tableRoutes.sort()).toEqual(shipped)
expect(kernel).toContain('ORCA skills get orchestration --full')
expect(squash(kernel)).toContain(
'returns this exact kernel and every reference from the same CLI build'
)
expect(squash(kernel)).toContain('If an older CLI rejects `--full`')
})
it('owns expanded waves, launch preferences, reuse, and review boundaries', () => {
@@ -182,7 +225,9 @@ describe('owned orchestration references', () => {
expect(reference).toContain('`launch.requested` with `launch.effective`')
expect(reference).toContain('worker-start --task <next_task_id> --terminal')
expect(reference).toContain('A review-only `worker_done` authorizes synthesis')
expect(reference).toContain('post-review fixes and\nPR preparation remain with that owner')
expect(squash(reference)).toContain(
'post-review fixes and PR preparation remain with that owner'
)
})
it('owns worker heartbeat, ask resume, escalation, failure, and idle', () => {
@@ -197,6 +242,13 @@ describe('owned orchestration references', () => {
expect(reference).toContain('Send exactly one terminal report')
expect(reference).toContain('Use `--outcome failed`')
expect(reference).toContain('After `worker_done`, end the dispatched turn and idle')
expect(squash(reference)).toContain(
'ORCA orchestration check --terminal <worker_handle> --json'
)
expect(squash(reference)).toContain('once more immediately before `worker_done`')
expect(squash(reference)).toContain(
'`check` names its caller with `--terminal`, never `--from`'
)
})
it('keeps heartbeat and worker_done recipes bound to the injected capability', () => {
@@ -212,7 +264,7 @@ describe('owned orchestration references', () => {
}
expect(workerDone).not.toContain('--files-modified')
expect(workerDone).not.toContain('--report-path')
expect(reference).toContain('only when applicable, using actual\npaths')
expect(squash(reference)).toContain('only when applicable, using actual paths')
expect(reference).toContain('Do not send documentation placeholders as metadata')
})
@@ -224,18 +276,24 @@ describe('owned orchestration references', () => {
expect(reference).toContain('--worktree new-top-level')
expect(reference).toContain('Folder workspaces are first-class')
expect(reference).toContain('Remote `current` and `new-child` are invalid')
expect(reference).toContain("`--on` selects\nonly the worker's execution server")
expect(reference).toContain('route every follow-up, read,\nstop, and cleanup by Dispatch ID')
expect(squash(reference)).toContain("`--on` selects only the worker's execution server")
expect(squash(reference)).toContain(
'route every follow-up, read, stop, and cleanup by Dispatch ID'
)
expect(reference).toContain('`live`, `unverifiable`, or `exited`')
expect(reference).toContain('unknown stream\nopcodes can be silently dropped')
expect(squash(reference)).toContain('unknown stream opcodes can be silently dropped')
expect(reference).toContain('printed `orca-ide`')
expect(squash(reference)).toContain(
'ORCA project setup-existing-folder --project <project_id> --host <host_id> --path <abs_path> --kind folder --json'
)
expect(squash(reference)).toContain('and rejects a plain directory')
})
it('owns FIFO mail, Dispatch addresses, groups, questions, and gates', () => {
const reference = readReference('messaging-and-gates.md')
expect(reference).toContain('oldest FIFO Delivery')
expect(reference).toContain('Process\nevery row')
expect(squash(reference)).toContain('Process every row')
expect(reference).toContain('send --to dispatch:<dispatch_id>')
for (const group of ['@all', '@grok', '@cursor', '@worktree:<id>']) {
expect(reference).toContain(group)
@@ -244,7 +302,10 @@ describe('owned orchestration references', () => {
expect(reference).toContain('gate-create --task <task_id>')
expect(reference).toContain("Do not create a gate merely to answer a worker's `ask`")
expect(reference).toContain('successful `send` proves durable enqueue')
expect(reference).toContain('Wake and nudge are best-effort\nattention only')
expect(squash(reference)).toContain('Wake and nudge are best-effort attention only')
expect(squash(reference)).toContain(
'`check` names its caller with `--terminal <handle>` and is the only verb that rejects `--from`'
)
})
it('owns positive-evidence retry, unknown outcomes, retain/release, and no terminal close', () => {
@@ -254,12 +315,33 @@ describe('owned orchestration references', () => {
expect(squash(reference)).toContain('| `outcome_unknown` | Inspect')
expect(squash(reference)).toContain('| Remote contact lost | Preserve `unverifiable`')
expect(reference).toContain('--retry-of <dispatch_id>')
expect(reference).toContain('Placement is never\nsilently inherited')
expect(squash(reference)).toContain('Placement is never silently inherited')
expect(reference).toContain('worker-abandon --dispatch')
expect(reference).toContain('worker-retain --dispatch')
expect(reference).toContain('worker-release --dispatch')
expect(reference).toContain('`release_pending`\nor `release_unknown`')
expect(reference).toContain('Never substitute\n`terminal close`')
expect(squash(reference)).toContain('`release_pending` or `release_unknown`')
expect(squash(reference)).toContain('Never substitute `terminal close`')
})
it('owns the lost-response question and the request-show verdicts', () => {
const reference = squash(readReference('recovery-and-cleanup.md'))
expect(reference).toContain('request-show --request <request_id> --json')
expect(reference).toContain('--retry-request <request_id>')
expect(reference).toContain('`completed` means the mutation already took effect')
expect(reference).toContain('`pending` means the original mutation is still running')
expect(reference).toContain('that is not proof nothing happened')
expect(reference).toContain('terminal send --wait-submit <seconds>')
})
it('names worker-list as the enumerating command and the agent-liveness authority', () => {
const reference = squash(readReference('recovery-and-cleanup.md'))
expect(reference).toContain('ORCA orchestration worker-list --json')
expect(reference).toContain("`worker-show`'s `observation.status` is PTY liveness only")
expect(reference).toContain('`attention` categories, `requiresAction`')
expect(reference).toContain('`nextAction` argv')
expect(reference).toContain('the fleet verdict decides')
})
it('owns the custom topology exception without claiming process ownership', () => {
@@ -269,9 +351,9 @@ describe('owned orchestration references', () => {
expect(reference).toContain('terminal create --worktree active')
expect(reference).toContain('dispatch --task <task_id> --to <handle> --inject')
expect(reference).toContain('operator-created process unsupervised')
expect(reference).toContain('creates no supervised worker\nresource row')
expect(squash(reference)).toContain('creates no supervised worker resource row')
expect(reference).toContain('Use `worker-start --terminal <handle>`')
expect(reference).toContain('never\nuse it for an ownership handoff')
expect(squash(reference)).toContain('never use it for an ownership handoff')
})
it('owns legacy labels, read-only degradation, exact recovery, and takeover', () => {
@@ -280,11 +362,11 @@ describe('owned orchestration references', () => {
expect(reference).toContain('[LEGACY COMPATIBILITY]')
expect(reference).toContain('[LEGACY RECOVERY REPLAY — MAY HAVE BEEN SEEN]')
expect(reference).toContain('[LEGACY READ-ONLY]')
expect(reference).toContain(
'degrade to\nread-only inspection and never fall back to local execution'
expect(squash(reference)).toContain(
'degrade to read-only inspection and never fall back to local execution'
)
expect(reference).toContain(
'must not spawn, write, signal, stop, switch, focus, split, or\ninject'
expect(squash(reference)).toContain(
'must not spawn, write, signal, stop, switch, focus, split, or inject'
)
expect(reference).toContain('launcher status `75`')
expect(reference).toContain('run_legacy_local')
+48 -42
View File
@@ -21,11 +21,9 @@ which attempt is authoritative, and when supervised work has settled.
## Outcome
**Result:** every in-scope Task has one explicit outcome and every settled worker
terminal has a next owner or cleanup decision.
**Done:** all expected Dispatches have settled, every delivered message was
processed before acknowledgment, and each settled worker was reused, explicitly
retained, or released.
terminal has a next owner or cleanup decision. **Done:** all expected Dispatches
have settled, every delivered message was processed before acknowledgment, and
each settled worker was reused, explicitly retained, or released.
**Safe failure:** preserve work and authority and report the state as unknown or
`unverifiable`. A timeout, quiet terminal, missing client, or lost remote
@@ -56,12 +54,13 @@ claiming a worker was orchestrated, verify its Task and Dispatch exist.
- After remote start, address the worker by Dispatch ID. The execution host owns
process, filesystem, transcript, stop, and cleanup facts. Preserve the verdicts
`live` / `unverifiable` / `exited`; contact loss is not process death.
- Liveness is layered: `worker-list`'s `projection.liveness` is the fleet verdict
for the agent; `worker-show`'s `observation.status` is PTY liveness only. A live
terminal can still hold a dead or stuck agent.
- Folder workspaces are valid. Do not require Git or assume every workspace is a
worktree.
- When a command requires an exact worktree selector, use the full
`<repo-id>::<path>` value returned by Orca; a bare repo id is not a worktree id.
- For a newly created workspace, pass that returned value as `id:<newFullWorktreeId>`;
do not shorten it to the repository id.
- Worktree selectors need the full `<repo-id>::<path>` value Orca returned,
passed as `id:<fullWorktreeId>`; a bare repo id is not a worktree id.
- Clients and remote servers update independently. Treat unknown optional fields
as absent. A new stream operation requires advertised capability because old
decoders may silently drop unknown opcodes. Never fall back to local execution
@@ -70,11 +69,9 @@ claiming a worker was orchestrated, verify its Task and Dispatch exist.
examples below, replace `ORCA` with it; do not create a shell variable or run
`ORCA` literally. If it fails, report that exact error instead of switching.
- Legacy takeover binds the authenticated invoking terminal; `--from` cannot
nominate another coordinator. It preserves live work and fences the former
coordinator, so never take over while that coordinator is still active.
- A successful `orchestration send` proves durable enqueue. Its wake or nudge is
best-effort attention only; it does not prove the recipient read the message,
started a turn, or accepted steering.
nominate another coordinator. Never take over while that coordinator is active.
- A successful `orchestration send` proves durable enqueue; its wake or nudge is
best-effort attention only and does not prove the recipient read or accepted it.
## Worker obligations
@@ -85,10 +82,13 @@ The injected preamble is authoritative. A dispatched worker must:
answer. Resume the same message ID after an ask timeout.
2. Send heartbeats only at the cadence in the preamble. A heartbeat proves
liveness, not completion.
3. Send `worker_done` exactly once, from the dispatched terminal, with a
3. Read coordinator follow-ups at each natural checkpoint — before starting a
new file, after a test run — and once more immediately before `worker_done`:
`ORCA orchestration check --terminal <your_handle> --json`.
4. Send `worker_done` exactly once, from the dispatched terminal, with a
three-sentence executive summary, both lifecycle IDs, and explicit
`--outcome succeeded` or `--outcome failed`. Never encode failure only in prose.
4. Append `--files-modified` and `--report-path` only with real values when
5. Append `--files-modified` and `--report-path` only with real values when
applicable. After `worker_done`, end the dispatched turn and idle; do not poll
or start new work.
@@ -105,27 +105,27 @@ coordinator-supervised follow-up arrives with a fresh preamble and Task block.
## Canonical supervised loop
Confirm the runtime, create or bind one Run, create all independent Tasks, and
start the full independent wave before waiting:
Confirm the runtime, bind one Run, and start the full independent wave before
waiting. `worker-start --spec` creates the Task and its attempt in one call:
```text
ORCA status --json
ORCA orchestration run-create --objective "<objective>" --json
ORCA orchestration task-create --spec "<worker A task>" --json
ORCA orchestration task-create --spec "<worker B task>" --json
ORCA orchestration worker-start --task <task_a> --worktree current --agent codex --json
ORCA orchestration worker-start --task <task_b> --worktree current --agent claude --json
ORCA orchestration worker-start --spec "<worker A task>" --worktree current --agent codex --json
ORCA orchestration worker-start --spec "<worker B task>" --worktree current --agent claude --json
ORCA orchestration check --wait --types "worker_done,escalation,question" --timeout-ms 900000 --json
```
Use Task dependencies only for real ordering. Prefer parallel waves over chains
deeper than three or four steps. Nested workers obey the configured depth limit;
creating another Run does not reset the caller's depth.
Use `task-create` plus `worker-start --task <task_id>` for planned fan-out with
dependencies or a retry of a known Task. Use dependencies only for real ordering
and prefer parallel waves over chains deeper than three or four steps; nested
workers obey the depth limit, and a new Run does not reset the caller's depth.
A consuming `check` returns the bound Run's oldest FIFO Delivery and replays that
batch until acknowledged. Process every message. Reply to questions, validate
that each `worker_done` belongs to the expected active Dispatch, and decide each
settled terminal's next owner before acknowledging:
A consuming `check` names its caller with `--terminal <handle>`, never `--from`;
omit it inside the coordinator's own Orca terminal. It returns the bound Run's
oldest FIFO Delivery and replays that batch until acknowledged. Process every
message: reply to questions, validate each `worker_done` against the expected
active Dispatch, and decide each settled terminal's next owner before the ack:
```text
ORCA orchestration reply --id <message_id> --body "<answer>" --json
@@ -137,10 +137,16 @@ Keep waiting until every expected Dispatch settles. A timeout or empty result is
a checkpoint, not a failure. Do not stop, retry, release, or launch a duplicate
editor from timeout, idle state, heartbeat, relay loss, or missing client alone.
`worker-start` is the normal path. It composes placement, terminal readiness,
prompt injection, and supervised resource ownership. Low-level
`dispatch --inject` is reserved for an expressiveness gap and leaves an
operator-created process unsupervised.
After three consecutive empty waits, stop waiting blindly and enumerate with
`ORCA orchestration worker-list --json`. Act on each row's `attention`,
`requiresAction`, and literal `nextAction` argv. A worker that is not `live`,
whose `worker-show` reports `agentWait` null, and whose `worker-read` shows no
new progress is stalled, not working: load `references/recovery-and-cleanup.md`
and choose `worker-stop` or `worker-abandon` explicitly.
`worker-start` is the normal path, composing placement, terminal readiness,
prompt injection, and supervised resource ownership. `dispatch --inject` leaves
an operator-created process unsupervised and is only for an expressiveness gap.
## Task-spec contract
@@ -162,22 +168,22 @@ After an accepted success or failure report, immediately do exactly one:
Release is post-settlement cleanup, not cancellation. Never release because of
idle state, timeout, heartbeat, status, question, escalation, or a rejected or
stale completion. If release is uncertain, follow its exact recovery receipt;
never substitute `terminal close`. Released output remains readable through
stale completion. If release is uncertain, follow its exact recovery receipt and
never substitute `terminal close`. Released output stays readable via
`worker-read`.
A valid `worker_done` settles the Task and Dispatch automatically. Do not follow
it with `task-update --status completed`. Do not end the coordinator turn until
all expected Dispatches and settled terminals are accounted for.
A valid `worker_done` settles the Task and Dispatch automatically; do not follow
it with `task-update --status completed`. Enumerate the terminals still owing a
decision with `worker-list --terminal-state reclaimable --json`, and do not end
the coordinator turn until it returns none.
## Conditional references
This compact guide is sufficient for the normal local loop. At an action gate
below, run `ORCA skills get orchestration --full` once and read only the named
bundled reference. `--full` returns this exact kernel and every reference from
the same CLI build in one deterministic document. If an older CLI rejects
`--full`, keep this kernel's safety floor and use that command's `--help`; do not
guess newer flags.
bundled reference: it returns this exact kernel and every reference from the same
CLI build. If an older CLI rejects `--full`, keep this kernel's safety floor, use
that command's `--help`, and never guess newer flags.
| Action gate | Bundled reference |
| ----------------------------------------------------------------------------------- | ----------------------------------------- |
@@ -10,11 +10,16 @@ accepted steering.
## Coordinator delivery loop
```text
ORCA orchestration check --wait --types "worker_done,escalation,question" --timeout-ms 900000 --json
ORCA orchestration check --terminal <handle> --wait --types "worker_done,escalation,question" --timeout-ms 900000 --json
ORCA orchestration reply --id <message_id> --body "<answer>" --json
ORCA orchestration check --ack <delivery_id> --wait --types "worker_done,escalation,question" --timeout-ms 900000 --json
```
`check` names its caller with `--terminal <handle>` and is the only verb that
rejects `--from`. Omit `--terminal` inside an Orca terminal, where Orca resolves
the caller; pass it explicitly from anywhere else, including a dispatched
worker reading coordinator follow-ups.
A consuming coordinator `check` returns the bound Run's oldest FIFO Delivery,
up to 50 messages, and replays that exact batch until acknowledged. Process
every row and required terminal ownership decision before `--ack`. Type filters
@@ -35,7 +40,8 @@ ORCA orchestration send --to dispatch:<dispatch_id> --subject "Follow-up" --body
Do not substitute a remote terminal handle. Omit `--from` for ordinary
coordinator calls; a dispatched worker instead copies the exact `--from` and
capability arguments in its preamble.
capability arguments in its preamble. `check` is the exception: it identifies
its caller with `--terminal`, never `--from`.
Group addresses include `@all`, `@idle`, `@claude`, `@codex`, `@opencode`,
`@gemini`, `@droid`, `@grok`, `@cursor`, and `@worktree:<id>`. Use them only for
@@ -25,6 +25,16 @@ Current and exact existing workspaces create a fresh terminal unless
`--terminal` is explicit. Folder workspaces are first-class; do not invoke Git
or require worktree lineage when the selected workspace is a folder.
Register a folder workspace through project setup. `repo add --path <dir>`
requires a valid Git repository and rejects a plain directory:
```text
ORCA project setup-existing-folder --project <project_id> --host <host_id> --path <abs_path> --kind folder --json
```
Then place work on the returned workspace with an exact selector. `new-child`
and `new-top-level` are worktree creation and do not apply to a folder.
New worktrees use agent-first creation and run setup by default. Preserve the
repository's startup policy: `start-immediately` can report setup as `running`,
while `wait-for-setup` gates prompt delivery on success. Orca lineage, Git base,
@@ -3,21 +3,31 @@
Load this reference only after a failed/stopped/unknown attempt, explicit retry
decision, stop/abandon request, retention request, or uncertain release.
| Proven state | Safe action |
| ---------------------- | ------------------------------------------------------------------ |
| `ready` or active | Keep waiting; optionally read bounded output |
| `failed` or `stopped` | Start a replacement with `--retry-of`; repeat placement explicitly |
| `outcome_unknown` | Inspect, then choose `worker-stop` or explicit `worker-abandon` |
| Accepted `worker_done` | Reuse, retain, or release |
| Remote contact lost | Preserve `unverifiable`; do not stop or retry from absence alone |
| Proven state | Safe action |
| ----------------------- | ------------------------------------------------------------------ |
| `ready` or active | Keep waiting; optionally read bounded output |
| `failed` or `stopped` | Start a replacement with `--retry-of`; repeat placement explicitly |
| `outcome_unknown` | Inspect, then choose `worker-stop` or explicit `worker-abandon` |
| Accepted `worker_done` | Reuse, retain, or release |
| Remote contact lost | Preserve `unverifiable`; do not stop or retry from absence alone |
| Live PTY, stalled agent | Enumerate with `worker-list`; follow its `nextAction` |
## Inspect before acting
```text
ORCA orchestration worker-list --json
ORCA orchestration worker-show --dispatch <dispatch_id> --json
ORCA orchestration worker-read --dispatch <dispatch_id> --limit 50 --json
```
`worker-list` is the enumerating command and the authority on agent liveness:
each row carries `projection.liveness`, `attention` categories, `requiresAction`,
and a literal `nextAction` argv to run. `worker-show`'s `observation.status` is
PTY liveness only, so a `live` terminal whose agent died at a trust prompt still
reads `live` there. When the two disagree, the fleet verdict decides. A worker
that is not `live`, reports `agentWait` null, and shows no new `worker-read`
progress is stalled: stop waiting and choose `worker-stop` or `worker-abandon`.
`worker-read --source auto` uses a proven provider transcript when available and
otherwise returns bounded terminal output with a typed `fallbackReason`.
Continue with its top-level cursor, which is pinned to that source. If Orca
@@ -27,6 +37,28 @@ read `contentComplete`, `clipping`, and `warnings` before assuming omitted older
records are pageable. Never guess a provider session ID, transcript path, or
remote terminal handle.
## Was the mutation applied?
When a mutation's response was lost and named no Dispatch, do not replay blind.
Every orchestration mutation accepts `--retry-request <id>`, which reuses one
operation identity so Orca can replay, join, or recover it instead of starting a
duplicate. Ask what happened first:
```text
ORCA orchestration request-show --request <request_id> --json
```
`completed` means the mutation already took effect; read its recorded receipt
instead of rerunning. `pending` means the original mutation is still running or
Orca restarted before recording its outcome; replay the original command with
`--retry-request <request_id>`. `absent` means this runtime holds no receipt
under your caller identity — that is not proof nothing happened, so inspect the
affected Task, Dispatch, and terminal before deciding whether to retry.
When a worker's terminal accepted input but the submit is unconfirmed, use
`terminal send --wait-submit <seconds>`: it observes the accepted prompt for that
long and, on timeout, returns the input-accepted receipt without resending.
## Retry, stop, and abandon
Retry only a positively proven failed or stopped attempt. Placement is never
@@ -30,6 +30,21 @@ ORCA orchestration ask --from <worker_handle> --dispatch-capability <capability>
A timeout or disconnect leaves the original question pending. Resume its
message ID; do not create a duplicate question.
## Reading coordinator follow-ups
The coordinator steers a running worker with `send --to dispatch:<id>`. That
enqueue is durable but does not interrupt you, so nothing arrives unless you
look:
```text
ORCA orchestration check --terminal <worker_handle> --json
```
Run it at each natural checkpoint — before starting a new file, after a test
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`.
## Escalation
Escalate only before completion and only when the coordinator must intervene:
+20
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import type { CommandSpec } from './args'
import { COMMAND_SPECS } from './specs'
import {
REPEATED_FLAG_SEPARATOR,
findCommandSpec,
@@ -325,6 +326,25 @@ describe('validateCommandAndFlags', () => {
}
})
it('points --from at --terminal on the one verb that renamed the caller flag', () => {
const parsed = parseArgs(['orchestration', 'check', '--from', 'term_a'])
try {
validateCommandAndFlags(COMMAND_SPECS, parsed)
throw new Error('expected validateCommandAndFlags to throw')
} catch (error) {
const data = (error as { data?: { suggestions: string[]; nextSteps: string[] } }).data
expect(data?.suggestions[0]).toBe('terminal')
expect(data?.nextSteps[0]).toContain('--terminal')
}
})
it('leaves --from alone where the command actually accepts it', () => {
const parsed = parseArgs(['orchestration', 'reply', '--from', 'term_a'])
expect(() => validateCommandAndFlags(COMMAND_SPECS, parsed)).not.toThrow()
})
it('attaches did-you-mean suggestions to unknown-command errors', () => {
const suggestSpecs: CommandSpec[] = [
{
File diff suppressed because one or more lines are too long
+11 -1
View File
@@ -105,10 +105,20 @@ export type FlagErrorData = {
nextSteps: string[]
}
// Why: edit distance cannot recover a rename. `orchestration check` is the one verb
// that identifies its caller with `--terminal` while every sibling uses `--from`, so
// the near-miss ranking answered `--json`/`--run` and left the caller stuck (#16904).
// A synonym only fires where the typed flag is rejected and its partner is accepted.
const FLAG_SYNONYMS: Readonly<Record<string, string>> = { from: 'terminal' }
function suggestFlags(flag: string, validFlags: string[]): string[] {
return rankByDistance(
const synonym = FLAG_SYNONYMS[flag]
const ranked = rankByDistance(
validFlags.map((candidate) => ({ label: candidate, distance: levenshtein(flag, candidate) }))
)
return synonym && validFlags.includes(synonym)
? [synonym, ...ranked.filter((name) => name !== synonym)].slice(0, MAX_SUGGESTIONS)
: ranked
}
// Why: include the accepted set so agents can recover without another help call.
+49 -2
View File
@@ -1,8 +1,55 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { formatCliError } from './format'
import { formatCliError, reportCliError } from './format'
import { RuntimeClientError, RuntimeRpcFailureError } from './runtime-client'
function selectorNotFound(): RuntimeRpcFailureError {
return new RuntimeRpcFailureError({
id: 'req_selector',
ok: false,
error: { code: 'selector_not_found', message: 'selector_not_found' },
_meta: { runtimeId: 'runtime_local' }
})
}
describe('worktree selector recovery', () => {
it('names the offending value and the valid forms on a bare repo id', () => {
const output = formatCliError(selectorNotFound(), {
commandPath: ['orchestration', 'worker-start'],
worktreeSelector: 'id:github:stablyai/orca'
})
expect(output).toContain('No Orca workspace matched the worktree selector')
expect(output).toContain('id:github:stablyai/orca')
expect(output).toContain('Did you mean: id:github:stablyai/orca::<absolute-path>')
expect(output).toContain('Valid selector forms:')
expect(output).toContain('a bare repository id is not a worktree id')
})
it('carries the same recovery into the --json failure envelope', () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
reportCliError(selectorNotFound(), true, {
commandPath: ['terminal', 'create'],
worktreeSelector: 'path:/nope'
})
expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toMatchObject({
error: {
code: 'selector_not_found',
data: { selector: 'path:/nope', validSelectorForms: expect.arrayContaining(['current']) }
}
})
log.mockRestore()
})
it('stays silent when no worktree selector was passed', () => {
expect(formatCliError(selectorNotFound(), { commandPath: ['worktree', 'show'] })).toBe(
'selector_not_found'
)
})
})
describe('CLI error recovery', () => {
it('prints did-you-mean next steps for an unknown-command error carrying data', () => {
const error = new RuntimeClientError('invalid_argument', 'Unknown command: worktree remov', {
+35 -1
View File
@@ -5,6 +5,7 @@ import {
stripAutomationOwnerConflictCode
} from '../shared/automation-owner-conflict'
import { automationOwnerConflictRecovery } from './automation-owner-conflict-recovery'
import { worktreeSelectorRecovery } from './worktree-selector-recovery'
import { prepareComputerCliJsonResult } from './computer-format'
import type { RuntimeRpcFailure, RuntimeRpcSuccess } from './runtime-client'
import { RuntimeClientError, RuntimeRpcFailureError } from './runtime/types'
@@ -69,6 +70,21 @@ export {
type CliErrorContext = {
commandPath?: readonly string[]
/** The `--worktree` value this invocation sent; the runtime's error never echoes it. */
worktreeSelector?: string
}
function selectorRecovery(code: string | undefined, context: CliErrorContext) {
return code === 'selector_not_found' && context.worktreeSelector
? worktreeSelectorRecovery(context.worktreeSelector)
: undefined
}
function errorCode(error: unknown): string | undefined {
if (error instanceof RuntimeRpcFailureError) {
return error.response.error.code
}
return error instanceof RuntimeClientError ? error.code : undefined
}
export function printResult<TResult>(
@@ -85,6 +101,10 @@ export function printResult<TResult>(
export function formatCliError(error: unknown, context: CliErrorContext = {}): string {
const message = error instanceof Error ? error.message : String(error)
const selector = selectorRecovery(errorCode(error), context)
if (selector) {
return formatMessageWithNextSteps(message, selector.nextSteps)
}
if (error instanceof RuntimeClientError && error.code === 'runtime_unavailable') {
if (hasOrchestrationRequestId(error.data)) {
return message
@@ -130,9 +150,19 @@ function hasOrchestrationRequestId(data: unknown): boolean {
}
export function reportCliError(error: unknown, json: boolean, context: CliErrorContext = {}): void {
const selector = selectorRecovery(errorCode(error), context)
if (json) {
if (error instanceof RuntimeRpcFailureError) {
console.log(JSON.stringify(withAutomationOwnerConflictRecovery(error.response), null, 2))
const response = withAutomationOwnerConflictRecovery(error.response)
console.log(
JSON.stringify(
selector
? { ...response, error: { ...response.error, data: response.error.data ?? selector } }
: response,
null,
2
)
)
} else {
const response: RuntimeRpcFailure = {
id: 'local',
@@ -201,6 +231,10 @@ function localCliErrorData(error: unknown, context: CliErrorContext): unknown {
if (error instanceof RuntimeClientError && error.data !== undefined) {
return error.data
}
const selector = selectorRecovery(errorCode(error), context)
if (selector) {
return selector
}
const conflict = automationOwnerConflictRecovery(matchAutomationOwnerConflict(error))
if (conflict) {
return conflict
@@ -15,22 +15,37 @@ import { formatWorkerRead, type LegacyWorkerReadResult } from './worker-output'
export const ORCHESTRATION_WORKER_OBSERVATION_HANDLERS: Record<string, CommandHandler> = {
'orchestration worker-show': async ({ flags, client, json }) => {
const result = await client.call<{
dispatch: { id: string; task_id: string; status: string }
worker: { state: string; stage: string; agent_terminal_handle: string | null }
dispatch: { id: string; task_id: string; status: string } | null
worker: { state: string; stage: string; agentTerminalHandle: string | null }
projection?: { liveness: { verdict: string }; nextAction: { argv: string[] } } | null
observation?: { agentWait?: { source: string; reason?: string } | null }
}>('orchestration.workerShow', {
dispatch: getRequiredStringFlag(flags, 'dispatch')
})
printResult(result, json, (value) => {
const base = `${value.dispatch.id} task=${value.dispatch.task_id} [${value.worker.state}] stage=${value.worker.stage}`
const lines = [
`${value.dispatch?.id ?? 'unknown'} task=${value.dispatch?.task_id ?? 'unknown'} [${value.worker.state}] stage=${value.worker.stage}`
]
// Why: PTY status alone read `live` for an agent that died at a trust prompt, so the
// fleet verdict and its next action print beside it rather than in another command.
if (value.projection) {
lines.push(
`Agent liveness: ${value.projection.liveness.verdict}`,
`Next action: ${value.projection.nextAction.argv.join(' ') || 'none'}`
)
}
// Why: absent means unknown on older runtimes, distinct from an evaluated null wait.
if (value.observation === undefined || !('agentWait' in value.observation)) {
return `${base}\nInteractive wait: unknown (not evaluated)`
lines.push('Interactive wait: unknown (not evaluated)')
} else if (value.observation.agentWait) {
const wait = value.observation.agentWait
lines.push(
`Waiting on a human: ${wait.reason ?? 'interactive prompt'} (via ${wait.source})`
)
} else {
lines.push('Interactive wait: none')
}
const wait = value.observation.agentWait
return wait
? `${base}\nWaiting on a human: ${wait.reason ?? 'interactive prompt'} (via ${wait.source})`
: `${base}\nInteractive wait: none`
return lines.join('\n')
})
},
+23
View File
@@ -228,6 +228,29 @@ describe('unknown command surfaces a suggestion', () => {
expect(stderr).toContain('--json')
})
it('names the offending --worktree value and the valid forms on selector_not_found', async () => {
const { RuntimeRpcFailureError } = await import('./runtime/types.js')
callMock.mockRejectedValue(
new RuntimeRpcFailureError({
id: 'req_selector',
ok: false,
error: { code: 'selector_not_found', message: 'selector_not_found' },
_meta: { runtimeId: 'runtime_local' }
})
)
await main(
['orchestration', 'worker-start', '--task', 't1', '--worktree', 'repo-1', '--agent', 'codex'],
'/tmp/repo'
)
expect(process.exitCode).toBe(1)
const stderr = errorSpy.mock.calls.map((call) => String(call[0])).join('\n')
expect(stderr).toContain('No Orca workspace matched the worktree selector "repo-1"')
expect(stderr).toContain('id:repo-1::<absolute-path>')
expect(stderr).toContain('Valid selector forms:')
})
it('reports a pre-command flag that belongs to another command', async () => {
await main(['--workspace', 'worktree', 'list'], '/tmp/repo')
+5 -1
View File
@@ -176,7 +176,11 @@ export async function main(
json
})
} catch (error) {
reportCliError(error, json, { commandPath: parsed.commandPath })
const worktreeSelector = parsed.flags.get('worktree')
reportCliError(error, json, {
commandPath: parsed.commandPath,
...(typeof worktreeSelector === 'string' ? { worktreeSelector } : {})
})
process.exitCode = 1
}
}
+55
View File
@@ -0,0 +1,55 @@
// Why: the runtime answers an unresolvable `--worktree` with a bare
// `selector_not_found` — no offending value and no grammar — so a caller who passed
// a repo id where a worktree id belongs cannot tell what was wrong (#16904). The CLI
// is the only layer that still knows what the caller typed, so it shapes the recovery
// here, in the same validFlags/suggestions/nextSteps shape as an unknown-flag error.
export const WORKTREE_SELECTOR_FORMS = [
'id:<repo-id>::<absolute-path>',
'path:<absolute-path>',
'name:<display-name>',
'branch:<branch>',
'identity:<identity-key>',
'issue:<number>',
'current',
'active'
] as const
export type WorktreeSelectorRecovery = {
selector: string
validSelectorForms: readonly string[]
suggestions: readonly string[]
nextSteps: readonly string[]
}
const PREFIXES = ['id:', 'path:', 'name:', 'branch:', 'identity:', 'issue:']
function suggestForms(selector: string): string[] {
if (selector.startsWith('id:')) {
// A worktree id is `<repo-id>::<path>`; the repo id alone names no checkout.
return selector.includes('::')
? []
: [`id:${selector.slice(3)}::<absolute-path>`, 'path:<absolute-path>']
}
if (PREFIXES.some((prefix) => selector.startsWith(prefix))) {
return []
}
return selector.startsWith('/') || /^[A-Za-z]:[\\/]/.test(selector)
? [`path:${selector}`]
: [`id:${selector}::<absolute-path>`, `name:${selector}`, `branch:${selector}`]
}
export function worktreeSelectorRecovery(selector: string): WorktreeSelectorRecovery {
const suggestions = suggestForms(selector)
return {
selector,
validSelectorForms: WORKTREE_SELECTOR_FORMS,
suggestions,
nextSteps: [
`No Orca workspace matched the worktree selector "${selector}".`,
...(suggestions.length > 0 ? [`Did you mean: ${suggestions.join(', ')}`] : []),
`Valid selector forms: ${WORKTREE_SELECTOR_FORMS.join(', ')}.`,
'List the exact values with `orca worktree list --json`; a bare repository id is not a worktree id.'
]
}
}
@@ -56,8 +56,11 @@ Slack, GitHub comments, or any other channel to reach a human during the run.
# coordinator to do something before you can continue):
orca orchestration send --from term_WORKER --type escalation --subject "Blocked: <reason>" --body "<details>" --task-id task_SNAP --dispatch-id ctx_SNAP
# Check for messages from the coordinator:
orca orchestration check --terminal term_WORKER
# Read coordinator follow-ups. Nothing interrupts you: a durable message only
# arrives when you look, so run this at each natural checkpoint — before you
# start a new file and after a test run — and once more immediately before
# you send worker_done, so a redirect lands before the task settles.
orca orchestration check --terminal term_WORKER --json
=== AFTER YOU SEND worker_done ===
@@ -129,7 +129,18 @@ describe('buildDispatchPreamble', () => {
expect(result).toMatch(/orchestration ask --from term_worker/)
expect(result).toMatch(/orchestration send --from term_worker --type escalation/)
expect(result).toContain('--task-id task_abc123 --dispatch-id ctx_def456')
expect(result).toContain('orchestration check --terminal term_worker')
expect(result).toContain('orchestration check --terminal term_worker --json')
})
it('gives the worker a concrete cadence for reading coordinator follow-ups', () => {
const result = buildDispatchPreamble(baseParams())
const checkLine = result.indexOf('orchestration check --terminal term_worker --json')
const cadence = result.slice(0, checkLine)
// Why: the transport is durable but never interrupts, so "you may check" produced
// workers that never read a single follow-up.
expect(cadence).toContain('before you\n # start a new file and after a test run')
expect(cadence).toContain('immediately before\n # you send worker_done')
})
it('carries the minted Dispatch capability on lifecycle and question commands', () => {
+5 -2
View File
@@ -115,8 +115,11 @@ Slack, GitHub comments, or any other channel to reach a human during the run.
# coordinator to do something before you can continue):
${cli} orchestration send --from ${params.workerHandle}${capabilityFlag} --type escalation --subject "Blocked: <reason>" --body "<details>" --task-id ${params.taskId} --dispatch-id ${params.dispatchId}
# Check for messages from the coordinator:
${cli} orchestration check --terminal ${params.workerHandle}
# Read coordinator follow-ups. Nothing interrupts you: a durable message only
# arrives when you look, so run this at each natural checkpoint — before you
# start a new file and after a test run — and once more immediately before
# you send worker_done, so a redirect lands before the task settles.
${cli} orchestration check --terminal ${params.workerHandle} --json
${postDoneInstructions}`
@@ -187,7 +187,7 @@ describe('orchestration federated setup evidence', () => {
worker: {
state: 'ready',
stage: 'input_accepted',
setup_state: 'failed',
setupState: 'failed',
effects: expect.arrayContaining([
expect.objectContaining({ kind: 'setup', state: 'failed' }),
expect.objectContaining({ kind: 'dispatch_input', state: 'accepted' })
@@ -156,6 +156,7 @@ describe('manual Dispatch observation', () => {
workerState: string
terminalState: string | null
agentTerminalHandle: string | null
projection: { liveness: { verdict: string } }
}[]
}
expect(workerList.workers).toEqual([
@@ -167,12 +168,18 @@ describe('manual Dispatch observation', () => {
})
])
await expect(
call('orchestration.workerShow', { dispatch: dispatch.id })
).resolves.toMatchObject({
worker: { state: 'unsupervised', stage: 'injected', agent_terminal_handle: 'term_worker' },
const workerShow = (await call('orchestration.workerShow', {
dispatch: dispatch.id
})) as { projection: { liveness: { verdict: string } } | null }
expect(workerShow).toMatchObject({
worker: { state: 'unsupervised', stage: 'injected', agentTerminalHandle: 'term_worker' },
observation: { status: 'live', exactWorker: true }
})
// Why: worker-show published only PTY liveness, so it read `live` for a dispatch that
// worker-list called `unverifiable` — and worker-list's nextAction sent you back here.
expect(workerShow.projection?.liveness.verdict).toBe(
workerList.workers[0].projection.liveness.verdict
)
await expect(
call('orchestration.workerRead', { dispatch: dispatch.id, source: 'terminal' })
).resolves.toMatchObject({
@@ -6,9 +6,12 @@ import { defineMethod, type RpcMethod } from '../core'
import { OptionalFiniteNumber, requiredString } from '../schemas'
import {
callFederatedWorkerShow,
exposeDispatchContext,
exposeFederatedWorkerObservation,
exposeObservation,
exposeWorker,
inspectWorkerTerminal,
projectFleetWorker,
resolvePinnedFederatedServer,
showContextOnlyWorker
} from './orchestration-worker-observation'
@@ -118,8 +121,9 @@ export const ORCHESTRATION_WORKER_CONTROL_METHODS: RpcMethod[] = [
)
}
return {
dispatch: db.getDispatchContextById(params.dispatch),
dispatch: exposeDispatchContext(db.getDispatchContextById(params.dispatch) ?? dispatch),
worker: exposeWorker(worker),
projection: projectFleetWorker(runtime, db, params.dispatch),
server: { environmentId: server.environmentId, name: server.name },
remoteRuntimeEpoch:
db.getFederatedDispatch(params.dispatch)?.remote_runtime_epoch ??
@@ -148,19 +152,12 @@ export const ORCHESTRATION_WORKER_CONTROL_METHODS: RpcMethod[] = [
const observation = await inspectWorkerTerminal(runtime, db, params.dispatch)
const resource = db.getWorkerTerminalResourceByOwner(params.dispatch)
return {
dispatch,
dispatch: exposeDispatchContext(dispatch),
worker: exposeWorker(worker),
// Why: the fleet verdict, so worker-show and worker-list cannot disagree.
projection: projectFleetWorker(runtime, db, params.dispatch),
terminal: observation.exact ? observation.terminal : null,
observation: {
status: observation.status,
exactWorker: observation.exact,
// Why: a bare `unverifiable` is not actionable without naming what we lost.
...(observation.reason ? { reason: observation.reason } : {}),
// Why conditional: a present null must mean "looked, nothing waiting". An
// unattached, missing or identity-changed worker was never looked at, and saying
// null there is the false negative this field exists to remove.
...(observation.agentWait !== undefined ? { agentWait: observation.agentWait } : {})
},
observation: exposeObservation(observation),
terminalResource: resource ? exposeWorkerTerminalResource(resource) : null
}
}
@@ -283,7 +283,15 @@ async function projectWorkerListPageWithFilteredSnapshot(
agentTerminalHandle: row.agentTerminalHandle,
terminalState: row.terminalState,
resource: row.resource ? exposeWorkerTerminalResource(row.resource) : null,
projection
// Why: `projection.resource` restated id/ownerDispatchId/releaseState/terminalState
// that the row already carries; only the derived ownership classification is new.
projection: {
...projection,
resource:
projection.resource.state === 'absent'
? projection.resource
: { state: projection.resource.state }
}
}
})
const counts = Object.fromEntries(
@@ -1,7 +1,12 @@
import { describe, expect, it, vi } from 'vitest'
import type { OrcaRuntimeService } from '../../orca-runtime'
import type { OrchestrationDb } from '../../orchestration/db'
import { inspectWorkerTerminal } from './orchestration-worker-observation'
import {
exposeDispatchContext,
exposeWorker,
inspectWorkerTerminal
} from './orchestration-worker-observation'
import type { DispatchContextRow, WorkerDispatchRow } from '../../orchestration/types'
const DISPATCH_ID = 'ctx-worker'
const TERMINAL_HANDLE = 'term-worker'
@@ -63,3 +68,58 @@ describe('inspectWorkerTerminal missing liveness verdict', () => {
})
})
})
describe('worker-show receipt shape', () => {
it('parses the JSON columns once and emits one casing', () => {
const exposed = exposeWorker({
dispatch_id: DISPATCH_ID,
runtime_epoch: 'epoch-1',
state: 'ready',
stage: 'input_accepted',
worktree_id: 'repo::/tmp/wt',
agent_terminal_handle: TERMINAL_HANDLE,
setup_state: 'ran',
effects: '[{"kind":"setup"}]',
residual_resources: '["res-1"]',
start_options: '{"agent":"codex"}',
last_error: null,
created_at: 'now',
updated_at: 'now'
} as WorkerDispatchRow)
expect(exposed).toEqual({
dispatchId: DISPATCH_ID,
runtimeEpoch: 'epoch-1',
state: 'ready',
stage: 'input_accepted',
worktreeId: 'repo::/tmp/wt',
agentTerminalHandle: TERMINAL_HANDLE,
setupState: 'ran',
effects: [{ kind: 'setup' }],
residualResources: ['res-1'],
startOptions: { agent: 'codex' },
lastError: null,
createdAt: 'now',
updatedAt: 'now'
})
})
it('parses host_scope and withholds authority hashes from the dispatch row', () => {
const exposed = exposeDispatchContext({
id: DISPATCH_ID,
run_id: 'run-1',
task_id: 'task-1',
launch_token_hash: 'launch-secret',
capability_hash: 'capability-secret',
host_scope: JSON.stringify({ kind: 'local', hostId: 'local' })
} as DispatchContextRow)
expect(exposed).toMatchObject({
id: DISPATCH_ID,
hostScope: { kind: 'local', hostId: 'local' }
})
expect(exposed).not.toHaveProperty('host_scope')
expect(exposed).not.toHaveProperty('launch_token_hash')
expect(exposed).not.toHaveProperty('capability_hash')
})
})
@@ -3,6 +3,8 @@ import type { OrcaRuntimeService } from '../../orca-runtime'
import type { OrchestrationDb } from '../../orchestration/db'
import { OrchestrationError } from '../../orchestration/orchestration-error'
import { parseWorkerTerminalHostScope } from '../../orchestration/worker-terminal-process-liveness'
import type { OrchestrationFleetWorker } from '../../../../shared/orchestration-fleet-projection'
import { projectWorkerFleet } from './orchestration-worker-list-projection'
import type {
DispatchContextRow,
FederatedDispatchRow,
@@ -83,24 +85,46 @@ export async function inspectWorkerTerminal(
}
}
/** Why conditional: a present `agentWait: null` must mean "looked, nothing waiting"; an
* unattached, missing or identity-changed worker was never looked at, and a bare
* `unverifiable` is not actionable without naming what contact was lost. */
export function exposeObservation(observation: Awaited<ReturnType<typeof inspectWorkerTerminal>>) {
return {
status: observation.status,
exactWorker: observation.exact,
...(observation.reason ? { reason: observation.reason } : {}),
...(observation.agentWait !== undefined ? { agentWait: observation.agentWait } : {})
}
}
export function exposeContextOnlyWorker(dispatch: DispatchContextRow) {
return {
dispatch_id: dispatch.id,
runtime_epoch: null,
dispatchId: dispatch.id,
runtimeEpoch: null,
state: 'unsupervised' as const,
stage: dispatch.capability_hash ? 'injected' : 'context_only',
worktree_id: null,
agent_terminal_handle: dispatch.assignee_handle,
setup_state: 'not_applicable',
effects: [],
residualResources: [],
startOptions: {},
last_error: dispatch.last_failure,
created_at: dispatch.created_at,
updated_at: dispatch.completed_at ?? dispatch.created_at
worktreeId: null,
agentTerminalHandle: dispatch.assignee_handle,
setupState: 'not_applicable',
effects: [] as unknown[],
residualResources: [] as unknown[],
startOptions: {} as unknown,
lastError: dispatch.last_failure,
createdAt: dispatch.created_at,
updatedAt: dispatch.completed_at ?? dispatch.created_at
}
}
// Why: `launch_token_hash` and `capability_hash` are authority material with no receipt
// consumer, and `host_scope` shipped as a JSON string inside JSON. One camelCase shape.
export function exposeDispatchContext(dispatch: DispatchContextRow) {
const exposed: Partial<DispatchContextRow> & { hostScope?: unknown } = { ...dispatch }
delete exposed.launch_token_hash
delete exposed.capability_hash
delete exposed.host_scope
return { ...exposed, hostScope: parseWorkerTerminalHostScope(dispatch.host_scope) }
}
export async function showContextOnlyWorker(
runtime: OrcaRuntimeService,
db: OrchestrationDb,
@@ -108,28 +132,64 @@ export async function showContextOnlyWorker(
) {
const observation = await inspectWorkerTerminal(runtime, db, dispatch.id)
return {
dispatch,
dispatch: exposeDispatchContext(dispatch),
worker: exposeContextOnlyWorker(dispatch),
projection: projectFleetWorker(runtime, db, dispatch.id),
terminal: observation.exact ? observation.terminal : null,
observation: {
status: observation.status,
exactWorker: observation.exact,
...(observation.reason ? { reason: observation.reason } : {}),
...(observation.agentWait !== undefined ? { agentWait: observation.agentWait } : {})
},
observation: exposeObservation(observation),
terminalResource: null
}
}
// Why: the row was spread verbatim beside its parsed copies, so a reader got
// `residual_resources` (a JSON string) next to `residualResources` (an array) and had to
// guess which was authoritative. Parse once, emit camelCase once.
export function exposeWorker(worker: WorkerDispatchRow) {
return {
...worker,
dispatchId: worker.dispatch_id,
runtimeEpoch: worker.runtime_epoch,
state: worker.state,
stage: worker.stage,
worktreeId: worker.worktree_id,
agentTerminalHandle: worker.agent_terminal_handle,
setupState: worker.setup_state,
effects: JSON.parse(worker.effects) as unknown[],
residualResources: JSON.parse(worker.residual_resources) as unknown[],
startOptions: JSON.parse(worker.start_options) as unknown
startOptions: JSON.parse(worker.start_options) as unknown,
lastError: worker.last_error,
createdAt: worker.created_at,
updatedAt: worker.updated_at
}
}
/**
* The same fleet verdict `worker-list` publishes, for one Dispatch.
*
* Why worker-show needs it: `observation.status` is PTY liveness, so an agent that died
* at a trust prompt inside a live pane read `live` here and `unverifiable` from
* `worker-list` — and `worker-list`'s own `nextAction` pointed back at this command.
*/
export function projectFleetWorker(
runtime: OrcaRuntimeService,
db: OrchestrationDb,
dispatchId: string
): OrchestrationFleetWorker | null {
const rows = db.listWorkerTerminalResources({ dispatchIds: [dispatchId], limit: 1 })
if (rows.length === 0) {
return null
}
const now = Date.now()
return (
projectWorkerFleet({
rows,
attentionFacts: db.getWorkerAttentionFactsForDispatches([dispatchId], now),
statuses: runtime.getOrchestrationFleetAgentStatusSnapshot(),
limit: 1,
now
}).workers[0] ?? null
)
}
export function exposeFederatedWorkerObservation(
observation: { status?: string; exactWorker: boolean; reason?: string },
projected: boolean
@@ -290,7 +290,7 @@ describe('orchestration worker recovery', () => {
await expect(
call('orchestration.workerShow', { dispatch: started.dispatch.id })
).resolves.toMatchObject({
worker: { state: 'stopped', stage: 'process_stopped', last_error: null },
worker: { state: 'stopped', stage: 'process_stopped', lastError: null },
observation: { status: 'exited', exactWorker: true }
})
expect(db.getTask(task.id)?.status).toBe('blocked')
@@ -370,7 +370,7 @@ describe('orchestration worker recovery', () => {
})
await expect(show).resolves.toMatchObject({
worker: { stage: 'released', agent_terminal_handle: null },
worker: { stage: 'released', agentTerminalHandle: null },
remoteRuntimeEpoch: 'windows_epoch_new',
terminal: null,
observation: {
+28
View File
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest'
import { describeUnconfirmedAgentStop, describeUnconfirmedStop } from './pty-liveness-verdict'
describe('unconfirmed-stop sentences', () => {
it('terminates a reason that has no terminator', () => {
expect(describeUnconfirmedStop('its SSH provider is no longer registered')).toBe(
'The PTY was not confirmed stopped: its SSH provider is no longer registered.'
)
})
it('does not double the terminator on a reason that is already a sentence', () => {
// A relayed lifecycle_conflict message arrives punctuated and printed `...to failed..`.
expect(
describeUnconfirmedAgentStop({
ptyStopVerdict: 'unverifiable',
ptyStopReason: 'worker w1 cannot transition from stopping to failed.'
})
).toBe(
'The agent terminal was closed but its process could not be confirmed stopped: worker w1 cannot transition from stopping to failed.'
)
})
it('still terminates the live-process wording', () => {
expect(describeUnconfirmedAgentStop({ ptyStopVerdict: 'live' })).toBe(
'The agent terminal was closed but its process could not be confirmed stopped: it is live.'
)
})
})
+8 -2
View File
@@ -16,9 +16,15 @@ export const NO_OBSERVING_PROVIDER_REASON = 'no registered provider can observe
export const SSH_EXIT_UNCONFIRMED_REASON = 'the owning SSH host did not confirm the PTY exit'
export const PTY_LIVE_NOTE = 'The PTY is live.'
// Why: reasons reach these sentences from verdicts, receipts and relayed errors, and
// some already end in a terminator — appending one blindly printed `...to failed..`.
function endSentence(detail: string): string {
return /[.!?]$/u.test(detail.trimEnd()) ? detail.trimEnd() : `${detail.trimEnd()}.`
}
/** The one sentence every surface uses to admit a stop was not confirmed. */
export function describeUnconfirmedStop(reason: string): string {
return `The PTY was not confirmed stopped: ${reason}.`
return `The PTY was not confirmed stopped: ${endSentence(reason)}`
}
/** Words a close whose PTY teardown was never confirmed, for a stop receipt. */
@@ -30,5 +36,5 @@ export function describeUnconfirmedAgentStop(close: {
close.ptyStopVerdict === 'live'
? 'it is live'
: (close.ptyStopReason ?? 'the stop outcome could not be verified')
return `The agent terminal was closed but its process could not be confirmed stopped: ${detail}.`
return `The agent terminal was closed but its process could not be confirmed stopped: ${endSentence(detail)}`
}