Commit Graph
10386 Commits
Author SHA1 Message Date
Merge Sim ed25b3efd7 Merge origin/main into brennanb2025/orchestration-structured-delivery
Carries the durable multi-agent workflow work (#16904) through this branch.
Both PRs touch the same subsystem, so most of this is deciding which of two
independent solutions to the same problem survives. #16904 did not generalize
past the PTY assumption anywhere, so nothing here is redundant: it hardens the
first authority source, this branch adds a second under the same contracts.

Schema. Both sides independently wrote a `current < 31` migration from a shared
base of 30, with different bodies, and main went on to 38. Version 31 therefore
means two different schemas, and the skew checker asserts main's five v31
columns for anything stamped >= 31 — so a database written by this branch failed
the completeness check and replayed the whole chain from v6. That is the checker
working correctly, not a defect, and the replay is row-safe (every DROP in the
chain is a copy-forward rebuild gated on shape, and there is no DELETE or
TRUNCATE anywhere in it). Nothing shipped stamped 31 with this branch's content:
the commit is in no tag and no release branch. So the migration renumbers to 39
in its own file. It no longer creates `structured_pointer_operations` — that is
redundant, because `createTables` runs unconditionally on every open, ahead of
migration. Only the widened archive CHECK needs a migration, since IF NOT EXISTS
cannot widen a constraint on an existing table. The gratuitous rename of
migrate-v13-v30.ts is reverted, which removes a conflict outright.

Batch selection. Both sides changed the same region: this branch extracted it
verbatim into `selectOrchestrationPointerBatch`, main edited it in place and
dropped the JS post-filter. Main is right, and provably so — the unfiltered
waiter returns early, so every surviving waiter contributes a concrete type list
that SQL excludes byte-exactly (`messages.type` is TEXT with no NOCASE
collation), and the trailing slice sits behind an identical SQL LIMIT. Nothing
can reach the post-pass in a filtering state, for either lane, and selection is
synchronous throughout so no waiter can register inside it. The extraction
survives and main's lane now calls it, so the two lanes cannot drift again. The
docblock claimed the post-filter caught a mid-selection waiter; that hazard does
not exist and the claim is gone. Nothing covered this, so it now has tests
against a real database rather than a stub.

Delivery gate. This branch gated only `run:` mailboxes, on the premise that a
delivery row exists only for a `run:` address. Main made that false — deliveries
are keyed by any mailbox handle — and generalized correctly, keying the lookup
on the exact handle being nudged, so a coordinator's run delivery cannot
suppress the nudges it sends its workers. Adopted. It is worth more here than in
the PTY lane: a structured nudge costs a whole provider turn.

Archive kind. Main's new `summarizeWorkerOutputArchive` is a two-way switch that
JSON-parses every non-transcript_pin kind as a terminal tail, so this branch's
`structured_journal` reached `content.lines.every(...)` and threw, at three live
call sites. A structured session's journal reports as `transcript`: it IS the
session's transcript, and a third source value would both leak the
structured/terminal split into a CLI surface this PR keeps uniform and widen a
shape that reaches paired clients.

PTY pointer refusal. Main shipped the same fix independently and narrower, so
this branch's hunk is dropped. Its guard fires only when a session is bound,
which is exactly the set the blanket refusal covered — an unbound pty is always
admitted, so a denial implies a binding.

Also: this branch's release-receipt extraction gives way to main's, with the
structured lease check moved into main's lease module; the worker RPC tree moves
into main's orchestration/{worker,messaging,federation} layout, with this
branch's structured branches replayed into the new homes.

pnpm tc clean. Verified by grepping the MERGED files, not the diffs, that main's
migration registrations, its six durability attach calls, its reset delete, its
fleet-status additions and its dispatch-pointer support all survived — each of
those would have been dropped silently by a keep-ours resolution, with no
conflict marker and a green typecheck.

Both merged files crossed the 300-line cap, which must be split rather than
disabled. Four extractions, each a whole step rather than an arbitrary cut:
assertExplicitWorkerTerminalUsable (the three refusals that must happen before
anything is created), deliverWorkerDispatchPreamble (one preamble, two
transports), tearDownFailedWorkerStart (undo what a failed start created), and
stopStructuredWorkerForRelease (the close half of a release for a worker with no
terminal to close).

One thing this merge restores rather than adds: `canDispatchSubWorkers` on the
local worker's dispatch preamble. The shared ancestor passed it; main's
extraction of startLocalWorker does not, and `buildDispatchPreamble` treats its
absence as false, so a locally started worker on main is no longer taught the
sub-dispatch verbs. With the default nested depth of 1 that section applied to
every first-generation worker, and main still passes the same argument at three
other call sites, so this reads as a line lost while moving ~300 lines rather
than a decision. Restored here for both lanes, since a worker is taught the same
verbs whichever mode it runs in.
2026-09-06 14:57:49 -07:00
Brennan BensonandMerge Sim 6fcd82918d Update mobile 0.0.48 Android download links (#19117)
* Update mobile 0.0.48 Android download links

* Update the mobile docs page APK link to 0.0.48

The docs page the READMEs link to still pointed at 0.0.46, two releases stale.

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 13:29:08 -07:00
Brennan BensonandMerge Sim 3da1c5b2b1 fix(ci): stop Android release notes exceeding the GitHub body limit (#19114)
* fix(ci): stop Android release notes exceeding the GitHub body limit

gh release create --generate-notes let GitHub pick the previous tag. Release
tags live on side branches, so 0.0.46 and 0.0.47 are not ancestors of main and
detection reached back to 0.0.44, generating four releases' worth of notes:
130413 characters against a 125000 limit, which 422'd the publish after a full
Gradle build. The span grows every release.

Pin the comparison to the previous mobile-android release (0.0.47 -> 81862
characters) and cap the body so an unexpected span can never fail the publish.

* fix(ci): fall back when release-notes generation returns an HTTP error

gh writes the JSON error body to stdout on a failed request, so the redirect
left it in the notes file. The non-empty check then treated that blob as valid
notes and skipped the fallback, publishing {"message":...} as the release body.
Gate on exit status instead. Also match the current tag literally when picking
the previous release, so the dots are not regex wildcards.

* fix(ci): reuse the shared character-safe release-body truncation

The byte-based cap could split a multi-byte character at the boundary.
config/scripts/create-draft-release.mjs already exports truncateReleaseBody
with the same 120000 cap and a truncation notice, and the desktop release path
uses it. Import is side-effect free; its main() is guarded.

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 13:28:33 -07:00
Jinwoo Hong 15d0f8aedf skills: rewrite the seven non-orchestration guides to one outcome-first standard (#18724)
<!-- orca-pr-loc -->
<!-- Programmatic LoC summary. Do not edit by hand; rewritten on every commit. -->

| | Files | Added | Deleted | Net |
| :--- | ---: | ---: | ---: | ---: |
| Test | 6 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$​544 | $\color{#cf222e}{\Huge{\mathbf{−}}}$​49 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$​495 |
| Prod | 36 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1719 | $\color{#cf222e}{\Huge{\mathbf{−}}}$​1703 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$​16 |

<!-- /orca-pr-loc -->

## ELI5

Orca ships eight skill guides that agents read before running the CLI. Seven of them (everything except `orchestration`, which #16904 rewrites) were command catalogs that had drifted from the binary. This PR rewrites them so an agent reads the outcome, the done bar, and the safe-failure rule first, loads reference material only at the step that needs it, and never sees a command or flag the installed CLI does not define.

## What changed

- **Seven guides rewritten** to one standard: outcome spine first (Result / Done / Safe failure), conditions instead of case lists, one done bar, one autonomy envelope, references loaded at the point of use via `skills get <topic> --full`, every runnable invocation spelled `ORCA`. `orca-cli` is 424→260 always-loaded lines with three references (browser, automations, publishing); `orca-per-workspace-env` is 794→397 with five (provider-vercel, ssh-host, docker-ssh, windows-scripts, failure-modes).
- **Defects fixed in shipped guides:** `emulator camera` (no such command), iOS `permissions` (backend refuses it), Android pane described as "in development" (shipped in June), `relayGracePeriodSeconds: 0` documented as immediate teardown (it is unbounded), doctor `ok: true` hiding `warn`, an SSH exemplar setting both `jumpHost` and `proxyCommand`, a provisioned-root fetch from `origin`, the Linear unconfirmed-write rule keyed on four verbs when ten emit it. Linear and emulator descriptions dropped embedded commands and angle-bracket placeholders (651→329, 732→404 chars).
- **Generator bundles references.** `skill-guides/<name>/references/*.md` is appended to `--full`; `skills get` help says compact by default, full with references.
- **Stubs single-authored.** The resolver ladder, placeholder rule, and older-binary fallback shared by all eight installable `SKILL.md` files come from one `skill-stubs/_shared/cli-resolution.md` fragment composed by the generator. Projections were byte-identical before the content fixes.
- **Guards:** every `ORCA <cmd>` and flag in every guide and reference resolves against `COMMAND_SPECS` (this found the camera defect); descriptions ≤1024 chars with no angle-bracket tokens; reference routing checked both directions; an always-loaded size ratchet (300 lines) that guides may leave but never join. `orchestration` (440 lines on main) is recorded as an exception until #16904 lands its kernel.

## Relationship to #16904

Split out of #16904 so that PR carries only the orchestration guide. On main, `terminal send` has no `--wait-submit` / `--retry-request` and the orchestration kernel still carries the resolver ladder and worktree-selector rule, so this branch pins `accepted: true` for handoff receipts and leaves the orchestration pins where main has them. The merge in either direction is mechanical: #16904 rebased on this becomes a one-file `orchestration.md` change plus dropping the two exceptions.

## Standard

Compound Engineering's portable skill-authoring guidance (outcome spine, conditions not cases, pinned fragile commands with an ordered hatch, references at point of use). NVIDIA SkillEvaluator Tier 1 (`schema,pii,license,quality,unicode,lint`) was run on every guide; its deterministic checks pass, its template nudges (Instructions/Examples sections, 50–150 char descriptions) do not apply to Orca's stub architecture and were not applied.

## Testing

- `pnpm typecheck:tsc:cli` clean; `check:code-quality:changed` and `check:react-doctor:changed` 0 findings
- `pnpm verify:bundled-skill-guides` and skill-bundle manifest verify clean
- vitest over `config/scripts`, `src/cli/skill-guide-cli-parity.test.ts`, `src/cli/skills.test.ts`, `src/cli/specs/skills.test.ts`, `src/cli/help.test.ts`, `src/main/skills`: 240 files / 2,019 pass
- Live smoke on the built CLI of every `skills get <topic>` and `--full`, every emulator, linear, and vm verb named in the guides, and every projection's resolver, GNOME warning, and bounded fallback (done on the #16904 branch before the split; the guide bodies are identical here except the send-receipt vocabulary noted above)

## Deferred product decisions

Merging `orca-emulator` and `orca-emulator-android` into one skill with a platform branch; collapsing `linear-tickets` to a guide alias; a `skills get --reference <name>` selector so a gate table can load one file; a fresh-agent routing eval before trimming the `orca-cli` (1,015 chars) and `orchestration` descriptions, whose quoted triggers each fixed a routing misroute.
2026-09-06 16:10:16 -04:00
Jinjing 08b96ed1b3 Seed Cmd-J filter from sidebar scope (#19036)
* feat(palette): seed Cmd+J filter from sidebar show scope

When opening Cmd+J, the palette's host and project filters now
initialize from the sidebar's current Show scope, so results
match the user's sidebar view. The palette can still be cleared
or changed per open; sidebar never reads back palette filters.

* refactor: pass app state to palette filter builder

Let the builder function extract the sidebar scope it needs instead of
requiring callers to destructure and pass individual properties. This
reduces coupling and simplifies the data flow through the palette
initialization lifecycle.

* Make palette filter repo-granular to preserve sidebar scope

Filter options now list individual repositories instead of grouping
multi-repo projects into single rows. This preserves the exact
repository scope shown in the sidebar when opening Cmd+J, rather than
widening selections to entire projects. Removes per-field selection cap
and stale-value reconciliation, simplifying the filter lifecycle.

* Clarify filter naming and seed from sidebar scope on palette open

- Rename projects→repositories in PaletteFilterModel for semantic accuracy
- Rename rawFilter→filterState for clearer intent
- Initialize filter from sidebar scope in local state, refresh on open
- Remove redundant filter reset from selection lifecycle

* Seed Cmd-J filter from sidebar scope and reset on close

The palette now opens with the sidebar's host and repository scope
applied. Filter changes are temporary: closing discards them, and
reopening reseeds from the sidebar's current state.

- Repository filtering is now granular (individual repos)
- Support shared repository IDs across multiple hosts
- Disambiguate duplicate repository names by path

* Add comment clarifying Projects terminology

Document the naming convention for repository-granular filter choices to help future maintainers understand why "Projects" is used as the user-facing term.

* Remove redundant Escape press from worktree palette filter test
2026-09-06 13:07:17 -07:00
Brennan BensonandMerge Sim d07c47593d feat(mobile): structured native Claude chat (#18741)
* feat(mobile): structured native Claude chat

Mobile already spoke the structured agent-session protocol for Codex, and the
host already had a Claude capability gate — mobile just never advertised it, so
`projectAgentSessionTabsOut` stripped every Claude tab before it left the
desktop. The structured lane in mobile/ turned out to be agent-agnostic already
(shared reducer, message projection, option catalog, prompt tokens), so this
opens the gate rather than building a second lane:

- advertise `agent-session.structured.claude.v1`
- resolve any structured provider in `resolveMobileNativeChat` via the shared
  `isAgentSessionHandleProvider`, instead of a `'codex'` literal
- widen the `agent-session` route type off `'codex'`
- route bare Claude launches through `agentSession.createSupport` like Codex,
  which still degrades to a terminal when the host refuses (remote, WSL, win32,
  managed-account mismatch, or structured chat switched off)

Deduplicate the create envelope. Renderer and mobile each assembled the
`agentSession.create` params by hand; the fingerprint has to be computed over
the same fields the host recomputes, so both now build it in one shared
`structuredAgentSessionCreateParams`. Mobile's Codex-only launcher becomes
`createMobileStructuredAgentSession(client, worktreeId, agent)` and reuses the
shared display-name map; two copies of a random-UUID fallback collapse into one.

Answer grouped Claude questions. A Claude AskUserQuestion carrying more than one
question — or one multi-select question — is emitted with the real content in
`body.questions` and the flat `options` left EMPTY, so mobile rendered a card
with nothing to tap and the turn stalled with no way out. Codex never emits this
shape. The phone has room for one question at a time, so the group is answered
as steps and submitted once, reusing the shared
`encodeAgentSessionQuestionAnswers` / `isValidAgentSessionQuestionAnswers`
rather than a second encoding.

Prompt responses move into `useMobileStructuredPromptResponses` because grouped
questions carry a multi-step draft the rest of the session does not touch, and
the session hook was at the 300-line cap.

Pin the mobile capability list against the host's parser bounds: it fails closed
to NO capabilities when the array exceeds 64 entries, which would look exactly
like an old client.

Re-pin mobile-session-route-parity: the create-actions edit drops one runtime
string literal and changes one nested function body. Ablated to confirm that
file is the sole cause.

* fix(mobile): derive the grouped-question draft instead of clearing it in an effect

The React Doctor gate flagged the session-change reset as a state adjustment
after a prop change, which renders the stale draft for a frame. Store the
session the answers were collected in alongside them and check it on read, so a
session switch drops the draft during render with no effect at all.

* test(mobile): pin that grouped steps key apart when the questions read identically

Claude can ask the same text twice in one group (once per file, say). The view
keys the question card by its projected content, so identical wording must still
key apart or step 1's checkboxes would be submitted as step 2's answer.

* fix(mobile): harden grouped Claude question answers

* fix(mobile): retry transient structured support probes

* fix(mobile): preserve grouped prompt response compatibility

* fix(mobile): preserve tokenless duplicate choice identity

* fix(mobile): point the launch tests at the generalized create API

The rebase onto #18697 brought its definitive-refusal tests in cleanly, but they
call the pre-rename createMobileStructuredCodexSession, and mobile tsc excludes
test files so nothing caught it. Retarget them and give the agent-copy test a
code that is actually in the definitive allowlist - agent_session_refused now
correctly stays unknown, so it never reached the failure copy it asserted.

* test(mobile): re-pin route parity after the rebase onto main

Main moved its own runtime-string pin to 547; this branch drops the 'codex'
literal from the create-actions gate. Ablated against main's pins to confirm
that file is the sole cause before re-deriving.

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 11:57:21 -07:00
Merge Sim 498756cd87 test(codex): expect the no-identity marker on an unregistered structured child
The refuse-rather-than-guess marker landed after these expectations were
written, and all three assert exact env equality on the unregistered path —
the one branch that now carries ORCA_STRUCTURED_SESSION. One of the two files
was added by this same branch, so this is a self-inflicted drift; the other
predates the branch and was broken by it.

The marker's presence is still pinned positively by
structured-worker-child-identity-env.test.ts and the CLI's
orchestration-structured-session-no-identity.test.ts, so relaxing these three
exact-equality checks loses no coverage of the security property.
2026-09-06 11:52:26 -07:00
Jinwoo Hong 2283f8ba4e docs(orchestration): never pick a worker model the user did not name (#19109)
The sonnet examples were added for a test cohort. Orchestration must not
choose a model on the user's behalf: pass --model only when the user named
one, otherwise inherit the configured agent default.
2026-09-06 14:51:37 -04:00
Brennan BensonandMerge Sim 298571ad9f fix(codex): uncap app-server stdio records (#18590)
Co-authored-by: Merge Sim <sim@local>
2026-09-06 11:49:42 -07:00
Jinjing ebaa01e42c Recover branch compare on visibility change (#19021)
* Recover branch compare on visibility change

Add recovery mode that reuses cached branch comparison data when the window regains focus instead of clearing results and forcing a refresh. This preserves the diff display during operations like rebasing that may cause the window to go to the background.

* Retry failed branch comparison results

Cached branch comparison results with error status are now excluded
from the cache-hit check, ensuring they are retried rather than
silently reused. This fixes missing diffs during rebasing.

* Decouple branch compare recovery from refresh kinds

Recovery is now a dedicated callback invoked independently on visibility changes, rather than a refresh kind. This allows pending recoveries to queue during in-flight requests, improving handling when the window regains focus during rebasing or other operations.
2026-09-06 11:45:46 -07:00
Jinwoo Hong 0c33f58e8a fix(ssh-relay): daemon owns the endpoint credential; a losing start never rotates it (#19052)
<!-- orca-pr-loc -->
<!-- Programmatic LoC summary. Do not edit by hand; rewritten on every commit. -->

| | Files | Added | Deleted | Net |
| :--- | ---: | ---: | ---: | ---: |
| Test | 19 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$​962 | $\color{#cf222e}{\Huge{\mathbf{−}}}$​136 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$​826 |
| Prod | 18 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$​295 | $\color{#cf222e}{\Huge{\mathbf{−}}}$​116 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$​179 |

<!-- /orca-pr-loc -->

## Symptom

Live 2026-09-05 (Orca 1.4.198 client, Ubuntu host): both relay processes `kill -STOP`ped for 20 s, then `-CONT`. The client redeployed while the host was frozen. Its fresh daemon lost the socket bind (`Socket path already in use`) but had **already rewritten** `relay-<id>.sock.credential`. The surviving daemon kept its in-memory credential, so every later `--connect` got `Endpoint credential mismatch; closing socket`, then `Grace started … timeoutMs=0 … ptys=1, clients=0` every ~20 s, forever. Only a manual `kill -TERM` cleared it. Receipts: `review-archive/orchestration-v3-pr16904/smoke-receipts-t012b/E16,E17,E18,E24`.

Three independent defects kept the wedge alive; each is fixed at its own seam.

## Fix

**1. The relay daemon owns credential publication (race-free under two concurrent starters).**
`relay-daemon.ts` binds the socket first, then publishes via the new `src/relay/relay-endpoint-credential-publication.ts`: adopt a valid pre-existing file (older clients still pre-write), else mint 32 random bytes and write temp+rename at 0600. A start that loses the bind exits inside `listen()` and never reaches the file. Why this option and not restore-on-loss or a client-side write: the only process that can *prove* ownership is the one whose `listen()` succeeded, and that proof is atomic with the bind. The client-side pre-write (`ssh-relay-endpoint-credential.ts`) and the launch-command `chmod 600`/`icacls` are removed on POSIX and Windows. The racing test also exposed that macOS reports a mid-bind collision as `EEXIST` rather than `EADDRINUSE`; `relay-socket-ownership.ts` now treats both as "held or stale".

**2. The client distinguishes "no daemon" from "daemon present but not answering", and never rewrites.**
A credential refusal is now typed on the wire: the daemon replies `orca-relay-handshake-credential-mismatch` (same frame type, no new opcode) and the bridge exits **43**; `waitForSentinel` maps it to `RelayCredentialMismatchError`, which the takeover treats as handshake-refusal evidence exactly like exit 42. A relay that holds the endpoint but **never refused** (the stalled-host shape: kernel backlog accepts the probe, handshake gets no answer) is now `RelayEndpointUnresponsiveError`, routed to the relay-lost backoff instead of the terminal Reset Relay path. Silence is not a decision (`docs/reference/ssh-execution-boundary.md`).

**2b. Deploy honours the verdict.** The 40 s live run exposed that the `--connect` catch block in `deployAndLaunchRelay` predates the incumbent probe and swallowed both verdicts as "probe failed, launch fresh", so a fresh daemon was still launched over the live one (it lost the bind by luck, which is exactly the collision in the incident). Held and Unresponsive now propagate; the session backs off on Unresponsive and surfaces Reset Relay on Held. Red-first in `ssh-relay-deploy-incumbent-verdict.test.ts`.

**3. The daemon cannot be wedged by a rotated file, because nothing can rotate it.**
The credential lives in the content-hashed relay dir, and after (1) the only writer is the daemon that owns the socket, so the "file changed under a live daemon" state the incident depended on is no longer reachable in-product. The credential is therefore fixed for the daemon's lifetime, as a plain secret should be. A hand-edited file is refused with the typed reply until restored (tested). Startup adoption of a pre-written file applies an owner-only + same-uid rule (review finding): anything else is replaced by a fresh mint. An earlier revision of this PR also re-read the file on mismatch and adopted it; that was removed as unreachable machinery that turned the credential into a per-handshake file-ownership check.

**3b. Fail closed between bind and publication.** A client that arrives after `listen()` resolves but before the credential is set is refused, not admitted as `unproved`. Nothing can be delivered in that window today; the guard makes the boundary structural instead of an event-loop ordering fact. Red-first in `relay-reconnect-listener-credential-gate.test.ts`.

**Wire compat.** New optional handshake reply only; an old `--connect` hits `Unknown handshake type` and exits 1 pre-sentinel, which it already treated as a generic failure. New daemon adopts an old client's pre-written file; new client still passes `--credential-file` so an old daemon reads it as before. Absence of exit 43 is never used as evidence.

**Also.** `terminal create` on a reconnecting SSH host now says what to do instead of a bare `No PTY provider for connection "<id>"` (prefix preserved; the renderer matches it).

## Tests (red first)

- `src/relay/subprocess.test.ts`: two `--detached` starts race one socket + credential file → exactly one reaches the sentinel, loser exits 1 with `Socket path already in use`, file valid + 0600, a `--connect` reading it reaches `relay.status` and reports the winner's pid. Red before (both starters died: daemon required a pre-existing file), green 6/6 after.
- `src/relay/relay-endpoint-credential-publication.test.ts`: mints after bind; adopts a pre-written 0600 file; replaces a pre-written 0644 file with a fresh mint; refuses a stale credential with exit 43 while still serving the real one, and keeps refusing a rewritten file until it is restored.
- `src/relay/relay-reconnect-listener-credential-gate.test.ts`: a client in the bind-to-publish window is refused and never attached; after publication the right credential is accepted and a wrong one refused; a daemon launched without a credential file is not gated. Red without the guard.
- `ssh-relay-deploy-incumbent-verdict.test.ts`: live-but-silent incumbent → `RelayEndpointUnresponsiveError`, refused → `RelayEndpointHeldError`, and in neither case is `--detached` launched; a failed `test -S` probe still launches fresh. Red 2/3 without the deploy change.
- `ssh-relay-deploy-helpers.test.ts` (exit 43), `ssh-relay-endpoint-takeover.test.ts` (refused → Held even with no `lsof`; silent → Unresponsive, nothing unlinked or signalled), `ssh-relay-session-terminal-error.test.ts` (Unresponsive → `onRelayLost`, not terminal). Deploy/namespace/native-deps tests updated to assert the client writes **no** credential.

## Live proof

New `tests/e2e/ssh-docker-relay-stall-credential.spec.ts` (claimed in `run-ssh-docker-e2e.mjs` and PR source routing), two cases: `kill -STOP` every relay pid in the container, send input during the freeze, hold **20 s** (the incident's duration, which races the mux liveness timeout) or **40 s** (past it for sure), `kill -CONT`; assert status back to `connected`, same pty, same daemon pid, same credential inode and content, relay.log did not shrink (a relaunch truncates it) and has zero `Endpoint credential mismatch` / `Socket path already in use` lines, in-stall input delivered at most once.

Run output (local, fixture image `orca-e2e-ssh-relay:3a864c665ba2cefd`, `ORCA_E2E_SSH_DOCKER=1 SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 … --project electron-headless --workers=1`, head `c2c20fd994`; re-run identically on the final head after the credential-lifetime change, 2 passed (1.7m), same annotations, and the bind-to-publish refusal never fired):

```
✓ keeps the same daemon and credential across a 20s relay freeze (38.3s)
    relay-processes-stopped: 2          relay-processes-continued: 2
    bridge-pids-before-after: 480 -> 480
    socket-clients-accepted-before-after: 1 -> 1
    in-stall-input-delivered: 1
✓ backs off and reattaches, never relaunching, across a 40s relay freeze (57.5s)
    relay-processes-stopped: 2          relay-processes-continued: 4
    bridge-pids-before-after: 480 -> 1202
    socket-clients-accepted-before-after: 1 -> 3
    in-stall-input-delivered: 1
2 passed (1.6m)
```

Client log in the 40 s case shows the new path end to end: `Relay channel lost … reconnect attempt 1/6` → `Socket probe result: "ALIVE"` → `Socket reconnect failed … Relay failed to start within 10s` → `Relay endpoint incumbent: … verdict=live evidence=accepted-connection holders=unenumerable` → `Failed to re-establish relay … A relay still owns … but did not answer the handshake … Orca will retry` → `reconnect attempt 2/6` → `Reconnected to existing relay via socket`. The 20 s case never left the frozen bridge (same bridge pid, one accept), so it exercises the "silence is not death" side of the same race. The 20 s case passed 6/6 across the session; the 40 s case was red on the prior head (`Socket path already in use` + `Startup failed: listen EADDRINUSE` in relay.log from the swallowed verdict) and is green after 2b. Before the fix the same injection produced a fresh daemon that rewrote the credential and a survivor refusing every client.

The `relay-processes-continued` count exceeds `stopped` in the 40 s case because the timed-out client's `--connect` bridge and the loser-side processes are parked behind the frozen listener when `CONT` runs; they exit on their own once it resumes.

## Gates

`pnpm test src/relay src/main/ssh` 332 files / 3884 tests pass · `pnpm typecheck:tsc:node` clean · `check:code-quality:changed` 0 findings · `check:react-doctor:changed` 0 findings · `pr-e2e-gate-contract.test.mjs` 42 pass · no lint disables or max-lines bumps added.

## Noted, not fixed here

- `terminal list` `orphaned:false` / `terminal close` `ptyKilled:true` for a pane whose relay is gone (`orca-runtime-stop-explicitly-closed-tab-ptys.ts`): different seam, `@ts-nocheck` characterization-covered file.
- On a host with no `lsof`, a stalled relay still cannot be enumerated as the holder; it is now retried rather than declared held, but a relay frozen past the backoff budget still ends in the existing "reconnect manually" banner.
2026-09-06 14:39:25 -04:00
Jinwoo Hong 06a607a1d7 feat(orchestration): make multi-agent workflows durable (#16904)
<!-- orca-pr-loc -->
<!-- Programmatic LoC summary. Do not edit by hand; rewritten on every commit. -->

| | Files | Added | Deleted | Net |
| :--- | ---: | ---: | ---: | ---: |
| Test | 225 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$​21666 | $\color{#cf222e}{\Huge{\mathbf{−}}}$​2820 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$​18846 |
| Prod | 348 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$​17107 | $\color{#cf222e}{\Huge{\mathbf{−}}}$​4706 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$​12401 |

<!-- /orca-pr-loc -->

## ELI5

Orca now treats orchestration like a durable control plane instead of inferring success from terminal keystrokes. Agents can tell whether a prompt was accepted or a turn started, replay an ambiguous request without sending twice, and recover coordinator mail after a crash. Completed workers can be inspected, released, or retained, and their panes no longer auto-resume as if the work were still running.

## What changed

- **Run receipts** from `run-create/use/current/show/list` are the row without routing plumbing (`home_database`, `coordinator_pane_key`) and without the duplicate `binding` object.
- **`terminal send` receipts are honest and idempotent.** `input_accepted` and `turn_started` are the only stages; `--wait-submit` observes without resending; `--retry-request <uuid>` replays the exact request against the same process incarnation. A transport timeout keeps the retry ID; only a different runtime answering strips it. Value-less or non-UUID `--retry-request` is rejected on the CLI and the SSH shim.
- **Mailbox delivery is committed before wakeup.** Pointer writes are staged in the DB before any PTY byte, replayed once after restart, and never emit a naked Enter. The watermark that parks concurrent deliveries is released with the DB reservation. Restart rescans pointer-pending and `dispatch:` mailboxes.
- **Lifecycle is a guarded transition graph** (`lifecycle-transition.ts`) with a table-driven test over every caller edge. Task reopen/overturn stays in the public contract. A PTY exit during `worker-stop` is the stop succeeding, not a failure.
- **Worker lifecycle CLI:** `worker-start` (`--spec` creates Task + attempt in one call), `worker-show`, `worker-read` (provider transcript first, bounded terminal fallback with a typed reason, local/WSL/SSH), `worker-stop`, `worker-abandon`, `worker-release`, `worker-retain`, `worker-list` (rowid-fenced pagination, fleet liveness, `attention`, literal `nextAction`).
- **Release is an explicit ownership table** (`decideWorkerTerminalRelease`): only an `owned` resource can be settled, the archive is mandatory where reachable, and an owner whose process is proven exited can always get out of `retained` via `archive_status: unavailable`. User-taken-over, external, and transferred panes stay retained.
- **Settled-worker resume fence** (folds in #17651): a settled dispatch whose pane is still open is fenced at settlement, on stop/abandon/exit, and at startup; lifted on release, retain, takeover, and pane reuse.
- **Liveness is `live` / `unverifiable` / `exited` only**, from execution-host evidence. Fleet projection reads the evidence clock, not the relay delivery clock. A host-certified exit outranks the worker's settled state. `unverifiable` never authorizes stop, abandon, retry, or release, in code or in the guide.
- **Federation:** structured reads negotiate by `method_not_found` so every shipped host keeps transcript-first output; exited remote workers are closed before being reported closed; epoch fencing holds across peer restart, downgrade, and pairing rotation; no per-second forced capability probe.
- **Schema v35:** repairs databases stamped v34 by the pre-fix branch (mailbox_handle default, index predicates), drops the write-only `lifecycle_transition_receipts` ledger and five never-read v31 identity columns.
- **Schema v36:** `dispatch:<id>` mailboxes get a real consumer generation on `dispatch_contexts` and `remote_dispatch_attachments`, bumped and fenced in the same transaction on every re-attach (manual inject, worker-start, federated attach). A stale worker whose Dispatch moved to another process now gets `consumer_fenced` instead of silently acking the new worker's Delivery. Run mailboxes already worked this way.
- **Schema v37:** `dispatch_contexts` records its creator (`creator_handle`, `creator_pane_key`), so a coordinator's context-only self-dispatch is bookkeeping rather than a nesting parent; before this, one self-dispatch made every later `worker-start` from that coordinator fail the depth cap. Pre-v37 rows keep counting (fails closed).
- **Dispatch-mailbox ownership is checked, not inferred.** A `check` from a process whose pane no longer holds the Dispatch, or whose last Attempt was abandoned/failed and moved to another terminal, gets `consumer_fenced` instead of an empty inbox that reads as "no mail yet". `--peek`/`--all` stay readable. A paneless caller still gets `stable_pane_required` with the rebind recovery.
- **Liveness certification is stricter:** a `process_exited` stage whose termination reason is `unknown` (a stop that was issued but never observed) projects `unverifiable`, not `exited`. Federated `worker-show` carries the execution host's verdict and host kind instead of a local guess. A live, ready worker with nothing pending has `nextAction: none` rather than pointing at the `worker-show` that produced it.
- **Wire:** `workerShow` keeps `dispatch.task_id` next to `taskId` for shipped CLIs. `ask --json` uses the standard `{ok, result}` envelope like every sibling verb.
- **Migration start-version detection** treats the two v32 recovery columns as versioned. Before this, every shipped database stamped below 32 resolved to the v6 floor and replayed the whole chain (the v23 backfill synthesized 68 phantom retained workers on a real v30 profile). Verified on a copy of a real 62 MB v30 profile: starts at 30, no row delta, integrity ok, 11 ms.
- **Skill guide** rewritten as a ≤200-line kernel plus seven references, to the outcome-first standard (Result / Done / Safe failure first, conditions not case lists, one done bar, references loaded at the point of use). The canonical loop uses `worker-start --spec`, names `worker-list` for completion accounting, documents `--retry-request` / `request-show` / `--wait-submit`, and requires positive evidence before any stall action. The other seven guides get the same treatment in #18724, split out so this PR stays orchestration-only.
- **`rpc/methods/orchestration-*`** (126 flat files) regrouped into `orchestration/{worker,federation,messaging,runs,gates}/`.

## Why

User reports showed the same boundary failures: false `agent_prompt_stalled` causing duplicate sends (#15180), coordinators unable to trust screen scrapes, cold-parked terminals receiving a pointer without the submit, settled workers accumulating as live tabs and auto-resuming after restart, and no way to tell a stalled worker from a working one.

## Linked issues

Fixes #15180. Fixes #17935 (orchestration skill description is 866 characters; a guard now caps every bundled skill at 1,024). Supersedes #17651 (fence folded in). Advances #16660, #16522, #14907, #13047.

## Review record

This PR was reviewed adversarially after revival: eight independent lenses (lifecycle, mailbox, send, worker, federation, transcript, complexity, live ergonomics), each required to prove findings with a failing test. That produced 16 proven blockers, all fixed with red-then-green regression tests, followed by two re-review rounds and a third fix wave that caught 3 regressions introduced by the fixes and 7 fixes that missed their target; all closed. A final pass (five lenses incl. a live built-runtime smoke, then a re-review of the fix wave) found and fixed seven more, chiefly the stale-worker mailbox steal, the self-dispatch depth wedge, and the unproven-exit certification. Three independent Codex (gpt-6-astra) passes followed: the first found nothing new, the second found and fixed 3 defects (task-status reachability, WSL-local host classification, peer-capability epoch), the third found and fixed 6 (production PTY controller never installed settled writes, ambiguous in-flight pointer failures allowed duplicate replay, SSH/relay deadlines cut off a valid `--wait-submit`, stop-vs-exit race during inspection, and two release-recovery paths for vanished or exited terminals). The full record (findings, proof tests, triage, declines with reasons) is archived outside the repo.

**Rework after the live smoke.** A first live cross-host run on the shipped adhoc build (this Mac, a paired Windows host on the same build, a paired Mac on 1.4.195, and an SSH host) found a P1: a running local worker read `unverifiable`/`missing_status` because the fleet snapshot rows lacked the terminal handle the matcher keyed on. A 59-row failure table over every bug fixed during review showed the same two classes recurring: a fact dropped in transit through optional fields, and two authorities for one fact. Two blind designs (Opus, Codex) converged on the same mechanisms, and the scoped tranches landed here with red-then-green seam tests from the real producer to the real consumer, faults injected only at the transport or hook-ingest boundary:

- **Settlement (data-loss class):** one three-valued `WriteSettlement` (`accepted | refused{reason} | unverifiable{reason, bytesHandedToTransport}`) from the SSH multiplexer through daemon client, providers, controller, to pointer staging. No boolean, no rejection-as-third-state. The two silent degrades that fabricated a handoff are deleted; a provider that cannot settle refuses before any effect. Pointer text and Enter share the contract; a partial flush is `unverifiable`, never `refused`.
- **Evidence identity (false-liveness class):** fleet agent-status evidence is a tagged union (`binding: worker | pane | unresolved{reason}`, `clock: observed | delivery`) minted once at ingest, so a hook row captured on one process incarnation can never bind to a later dispatch on the same pane. The matcher's `!worker.paneKey ||` defaults are gone. One host-scope parser replaces two.
- **Small pre-merge items:** `capability_unsupported` from an old peer is no longer relabelled `host_unavailable`; a producer census test asserts every agent-status consumer path projects a pane-only hook row as `live`.

Two ergonomics defects the second live run surfaced on a real database are fixed here too: a pre-v3 dispatch already marked `completed` projected as `outcome_unknown` / `requiresAction: true` forever (three copies of the outcome ladder disagreed on legacy rows; now one resolver, legacy `completed` reads `succeeded` with nothing to act on, legacy `failed` stays actionable on the failure), and an unscoped `worker-list` enumerated the entire database (now defaults to the Run bound to the calling terminal, `--run` overrides, and the receipt's additive `scope` field says which).

A third live round on the shipped adhoc build of `b082443e1f` (same four hosts) plus an unscripted run in the user's own prompt style (a plain Claude Code shell, `/orchestration`, three workers, zero errors, bound-Run default confirmed) found two more branch defects, fixed with red-then-green tests: a worker freshly started on a paired server projected `unverifiable`/`host_indeterminate` with `requiresAction` for ~3 minutes, including after its own `worker_done`, because the host's federation observation returned `missing_liveness_verdict` for any PTY the liveness register had not yet swept (the host now reads a connected pane it owns locally as `live`; disconnected or SSH-scoped panes stay `unverifiable`); and six pre-v3 completed rows still carried an `input` category because settling through the task-status path or `failDispatch` never closed the Dispatch's pending question threads (both paths close them now, and schema v38 closes threads already pending on settled rows). The guide's `worker-start` examples now show `--model sonnet`, since an omitted model inherits the launcher's default.

A Codex adversarial pass on the tranche diff found one real design hole (identity minted at read time instead of ingest, now closed) and two daemon settlement paths that threw instead of settling (fixed). Two `@ts-nocheck` runtime mixins on these paths were extracted into checked modules; the repo-wide `@ts-nocheck` count is unchanged at 171.

Deletions during review: ~1,900 lines (write-only ledger, unread columns, dead v1 archive path, test harnesses shipped in prod, duplicated liveness and state-machine copies, self-capability checks that were compile-time true).

## Testing

- `pnpm typecheck:tsc:node|cli|web` clean
- `pnpm run check:code-quality:changed` 0 findings; `check:react-doctor:changed` 0
- `pnpm verify:bundled-skill-guides`, `verify:skill-bundle-manifest`
- full `pnpm test` on the integrated head: 72,332 pass / 292 skipped; the only failures were three non-PR files (two zsh live-shell suites hit a node-pty spawn-helper ENOENT while a concurrent native rebuild ran, 44/44 in isolation; `release-checkout.unit.test.ts` is a known 30 s load timeout that passes in isolation on `origin/main` too).
- CI on 70b4811267 (rerun, pre-Codex): the only reds are five SSH e2e specs plus `terminal-send-agent-prompt-submit:198`, each shown failing identically on main (main's E2E workflow is red on its last 40 runs). The terminal-send spec is root-caused and fixed separately in #18707. The Windows hook-service flake (#17721) and the federation load flake did not recur.
- Skills: `pnpm exec vitest run` over the skill gate files plus `src/cli`, `config/scripts`, `src/main/skills` pass; live smoke on the built CLI of `skills get orchestration` and `--full` (7 references).
- live headless runtime (`orca-dev serve`, isolated profile): canonical loop, stop, release, archive read, retry rejection, stale-handle check, SIGKILL-and-replay all verified with receipts
- Live cross-host smoke on the shipped adhoc build of `0d465e7931` (this Mac and a paired Windows host on the build, a paired Mac left on 1.4.195, an SSH host): local, paired-new, paired-old and SSH loops all settle; running workers read `live` on every host and `exited` after release; the old peer reads `capability_unsupported` and refuses release honestly. Injected 10 s relay stall with a send in flight: delivered exactly once after recovery, zero duplicates. Every liveness field across 104 receipts is only `live` / `unverifiable` / `exited`.
- Final live cross-host smoke on the shipped adhoc build of `b082443e1f` (same hosts): every loop settles; 942 of 948 legacy completed rows read settled with `requiresAction: false` before the question-thread fix and all of them after; `worker-list` scope reads `bound` / `flag` / `all` correctly; 122 JSON receipts carry only `live` / `unverifiable` / `exited`. Unscripted prompt-style run: clean.
- Confirmation smoke on the shipped adhoc build of `2da076d4e9` (this Mac and the paired Windows host, both updated): a freshly started Windows worker reads `live` on the first fleet poll and on all 20 that follow, with no `host_indeterminate` at any point, and `exited` after release; all 948 legacy completed rows read `requiresAction: false` with `nextAction: none` after schema v38; every verdict across 60 receipts is `live` / `unverifiable` / `exited`.
- Not physically exercised: WSL hosts, the renderer notification bell (headless has no renderer), same-session fence via a real pane close (renderer-only state), restart mid-delivery on a real app (covered by e2e only).

## Notes

- Remote-wire additions are optional fields or `method_not_found`-negotiated methods; one new Electron-only IPC channel (`agentStatus:legacyWorkerTerminalResumeFence`) never crosses the wire.
- SSH contact loss remains `unverifiable`; the execution host stays authoritative.
- Intentional wire projection change: an SSH host scope with an empty `targetId` now projects host id `ssh` instead of an empty string (remote-wire-compatibility rule 3, old clients decode the same field). A fleet pane key without a terminal handle is now `unidentifiable` rather than matched by pane key alone.
- Found live but pre-existing on main, filed separately: a relay daemon-start collision during transport loss rewrites the endpoint credential and wedges the surviving relay (host needs a manual kill); `terminal create` on a reconnecting SSH host reports an opaque `No PTY provider for connection`; `terminal list` reports `orphaned:false` and `terminal close` reports `ptyKilled:true` for a pane whose relay is gone (orchestration's own projection reads `unverifiable` correctly at the same moment).
- Downgrade after this PR is not a supported path: main opens a v37 database and early-returns (its inserts still work against the v36/v37 defaulted columns), but its one-outstanding-Delivery-per-Run index is a no-op against the branch's mailbox-scoped index of the same name.
- Known follow-ups (not blockers): `worker-list` materializes every dispatch row per call; a positive "agent absent" signal distinct from PTY liveness is a product decision left open (a headless fake agent never reaches `live`, so its `nextAction` stays `inspect`); a context-only self-dispatch still lists as `role: worker` in `worker-list`; `dispatch` task-not-found / task-not-ready / inject-rejected still surface as `runtime_error`; task and inbox receipts still expose raw row columns. Deferred skill product decisions live on #18724.
2026-09-06 14:34:03 -04:00
Brennan BensonandMerge Sim 7ac194a634 Add persistent turn-scoped chat activity indicator (#19044)
* feat(chat): show turn-scoped activity tail

* fix(chat): keep turn activity broad

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 11:33:08 -07:00
Brennan BensonandMerge Sim c36c23df5f fix(chat): stop terminal focus recovery from stealing Cmd+C in the Chat UI (#18751)
* fix(chat): preserve message copy focus

* fix(chat): scope covered-xterm focus guard to the chat leaf

Chat view mode is a tab flag, but only the chat leaf's xterm is covered. In a
split chat tab with a terminal leaf active, the tab-level guard skipped the
terminal's resume focus and the tab-wide deferred focus then landed on the
covered chat xterm. Decide per pane: resume and window-wake read the active
pane's container, and the surface focus query skips leaves hosting the chat
root.

* fix(chat): close covered terminal focus fallbacks

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 11:31:07 -07:00
Neil 3be526c5e6 test: cover SSH reattach replay and enable deterministic Codex CI (#19106)
* test: cover SSH replay replies and run deterministic Codex restore scenarios

* test: register replay probe unit command in reliability gate
2026-09-06 11:25:58 -07:00
Neil 6aa0aaee6b test: isolate source-control generation repositories per scenario (#19105)
* test: isolate source control generation repositories per scenario

* test: explain scenario repository fixture scope
2026-09-06 11:25:52 -07:00
Neil 4cccadcb95 test: keep Activity pane selection in the retained sidebar (#19107) 2026-09-06 11:23:24 -07:00
Merge Sim 3040ee52ac fix(i18n): add the missing worktree-removal agent-session refusal string
The structured-session removal refusal introduced a translate() key with no
en.json entry. Nothing local catches that: typecheck passes, and the full
suite passes, because a missing key falls back to its inline default at
runtime. Only verify:localization-catalog fails on it, which is why CI's
static analysis reddened on a branch that was green everywhere else.

Fallback wording mirrors the sibling unstoppedPtyLive string, since the two
refusals differ only in what is still running and what Force Delete does to it.
2026-09-06 11:21:45 -07:00
Merge Sim 041120bcd8 Merge origin/main into brennanb2025/orchestration-structured-delivery
Carries main's #19040 through this branch's two extractions, which would
otherwise have silently reverted it: both conflicts are the same shape,
where main fixed code this branch had already moved elsewhere.

- resolveStructuredNativeChatSupport no longer refuses a launch that has
  saved session options. Main removed that term from resolveAgentLaunchRoute
  directly; this branch had lifted the whole predicate into the shared
  module, so taking either conflict side kept the refusal alive. The blocker
  member, the unused input field, the orchestration blocker-to-reason entry
  and the case pinning it are removed with it. Main's own routing test failed
  on this merge before the port and passes after, which is what proves the
  port is load-bearing rather than cosmetic.

- The structured create fingerprint now goes through attachFingerprintFields.
  Same shape again: main replaced the inline field list in
  structured-agent-session.ts, but this branch had moved that computation
  into structured-agent-session-create.ts. Options stay out of the digest,
  which is main's point: they are the session's initial state, not its
  identity, so a retry after the user reselects a model replays instead of
  conflicting.
2026-09-06 11:08:58 -07:00
Merge Sim 5b7161e81f docs(orchestration): stop the structured terminal-read refusal recommending a dead end
The refusal told a peer to "page it with `orca orchestration worker-read
--source transcript`", which is wrong three ways and this file said so itself:
its own header explains that this verb exists BECAUSE `worker-read` demands a
dispatch id and coordinator standing "a peer does not have" — and then the
refusal sent that same peer there. The verb it named is also a window index over
the same bounded page, so it is not a paging answer even for a caller who can
reach it; under load it now answers `source_changed` on most polls, which is
better than the silent hole it had before but still not what the sentence
promised.

The refusal now says what actually works — the tail is bounded and newest-last,
so poll it and diff — and names no alternative, because there is none. That is
the honest framing: a durable cursor is not achievable here at all, rather than
blocked on the wire shape. The journal is a reduced, MUTABLE timeline: an item's
projected text changes at its original sequence after later items exist, the
delta coalescer revises repeatedly, settlement can rewrite an item smaller, a
pending approval renders as nothing and then as something, and `sequence` resets
on epoch rollover. No index, numeric or opaque, survives that.

So the docstring's "pagination with a real anchor lives on `worker-read --source
transcript`" is gone too — there is no real anchor there — and the file now
records why no windowed alternative should be built later: a broken cursor fails
UNSAFE, as a silent hole in a poller's output, while diffing a bounded tail fails
safe as a harmless re-read, and a second paging-shaped verb would invite the PTY
assumptions this one cannot honour.

The test asserted the old advice, so it now pins the contract instead: the
refusal explains the working approach and must never name `worker-read`.
`worker-read --source transcript` remains a good bounded snapshot for a
coordinator reading a worker it dispatched; only the "or page it with" clause was
false.
2026-09-06 10:55:52 -07:00
Merge Sim d44e508848 fix(orchestration): stop the structured worker-read cursor misdelivering silently
`worker-read --source transcript` for a structured worker fingerprinted only the
oldest item's id, so `source_changed` fired when the window slid off the front
and could NOT fire when the page's contents changed under a stable oldest item —
which is the normal case, because the journal is a reduced, mutable timeline. A
`running` tool item gains its `[tool result]` at its original sequence once later
items exist, the 60ms delta coalescer revises a message in place, settlement can
rewrite an item smaller, and a pending approval projects to null until it
resolves and then appears in the MIDDLE of the array.

Two silent failures followed, both returning ok. Omission: a caller handed a
coalesced `hel`, resuming past it, never received the revision to `hello world`
— the same defect we refused to ship on the terminal read path, already shipped
here. Duplication: a resolved approval inserted ahead of a saved index, which was
still accepted, so the caller re-read content it already had. The blast radius is
the coordinator polling loop, the verb's primary consumer.

The anchor is now the oldest item PLUS every item whose projected message sits
below the caller's position, by id and revision. `createWorkerOutputSourceIdentity`
already takes an arbitrary string array and the cursor is already opaque
base64url carrying its own position, so neither the wire shape nor the
`source_changed` contract changes.

Prefix-scoped rather than whole-page deliberately: fingerprinting every item on
the page would flip the identity every 60ms with the coalescer window during an
active turn, making the cursor unusable exactly while the worker is working —
that trades a silent bug for a useless verb. Tail growth the caller has not read
cannot invalidate; any change to what it already holds does. Position-dependence
is safe because `p` rides in the same opaque payload as the identity, and the
returned cursor is stamped with the identity of its own end, which is precisely
what the next read recomputes. The frozen archive keeps a constant identity: no
item can be revised under a caller there, so it has no prefix to fingerprint.

Both silent shapes are pinned across a page boundary with the journal mutating
between reads — a static-journal test passes either way. Two ablations at the
real call site: reverting to the oldest-item-only anchor turns both red, and
widening the prefix to the whole page turns the tail-growth case red, which is
what proves the scoping is real in both directions.
2026-09-06 10:52:06 -07:00
Neil b44aaf20c6 test: reuse authoritative SSH connection readiness in localhost fixture (#19102)
* test: reuse authoritative SSH connection readiness in localhost fixture

* test: retain localhost SSH setup diagnostics
2026-09-06 10:37:04 -07:00
Brennan BensonandMerge Sim da48ad2b47 Bump mobile Android versionCode to 16 to match the 0.0.48 release (#19101)
Co-authored-by: Merge Sim <sim@local>
2026-09-06 10:24:38 -07:00
Neil 4d9e963ffd test: enable localhost SSH terminal and hook journey in CI (#19097)
* test: run localhost SSH terminal and hooks in CI

* test: isolate localhost SSH session fixtures across repetitions

* test: route remote agent hook source changes to localhost journey

* test: record localhost SSH reliability evidence and remaining gaps

* test: route the real SSH session hook authority
2026-09-06 10:05:26 -07:00
Neil b459b8f16d test: repair nested SSH fixture after HUB restart (#19098)
* test: restore paired nested SSH fixture after HUB restart

* test: cover failed re-pair selection and background window safety

* test: use required braces in re-pair regression fixture

* test: use current paired runtime identity after re-pairing
2026-09-06 09:51:31 -07:00
Jinwoo Hong 5dab495655 docs(relay): record Roll 2 cell roll (4916ed67 fleet-wide) and tick checklist (#19096)
All 19 general cells on 4916ed67, selector gen 148 -> 186, 0 serving-process
exits across the roll. Three waves used the no-restart mode=rollback resume
(c13 transient trust-probe 409; c26/c21 post-apply runtime-status 503 shed).
Checklist: 2.3, 4.1, 4.3 relay side deployed; status header 2026-09-06.
2026-09-06 12:37:11 -04:00
Neil f5960cec00 test: enable Docker SSH browser network route coverage in CI (#19095)
* test: enable Docker SSH browser network route journeys in CI

* test: register Docker browser job in token permissions contract

* test: declare SSH client dependency and narrow browser fixture routing
2026-09-06 09:29:56 -07:00
Neil 9837adaa07 test: reconnect after replacing same-ID runtime pairing (#19094) 2026-09-06 09:05:08 -07:00
Neil 3631f886a7 test: enable direct and client-hosted SSH browser coverage (#19090)
* test: enable direct and client-hosted SSH browser coverage

* test: record twelve passing SSH browser journey repetitions

* test: distinguish SSH journey evidence from unit runtime budget
2026-09-06 08:25:13 -07:00
Neil 1d2e00819f test: restore SSH bulk-open freeze coverage in headed CI (#19081)
* test: restore SSH bulk-open freeze coverage in headed CI

* test: record ten passing headed SSH freeze repetitions

* test: record ten passing headed SSH freeze repetitions

* test: route changed SSH freeze spec only to its dedicated lane
2026-09-06 07:42:50 -07:00
Neil f952f1ac96 test: run real WSL terminal launch and paste in PR CI (#19072)
* test: continuously exercise real WSL terminal launch and paste

* test: establish live WSL reader before changing default shell

* ci: pin WSL kernel installer and participation selectors

* ci: route deleted WSL paths and record immutable run evidence

* test: require exactly three WSL repetitions in lane contract
2026-09-06 05:24:54 -07:00
Neil adcc30be3b test: canonicalize native Windows paths during repository teardown (#19064) 2026-09-06 03:29:05 -07:00
Merge Sim 45e23ee470 test: pin structured-session close on the folder-workspace removal path
The folder and orphan removal callers now pass closeStructuredSessions so a
live structured session is closed best-effort rather than left bound to a
workspace Orca has forgotten. These three exact-args characterizations describe
that call and had not been updated.
2026-09-06 03:26:00 -07:00
Neil ec64df335e test: reject unsupported app-server in WSL golden stub (#19062) 2026-09-06 03:20:06 -07:00
Merge Sim 37603ec532 fix(orchestration): close the review findings on the structured parity work
Four defects and two follow-ups from the delta review.

The `worktree rm` refusal was a dead end in the desktop UI. Its message matched
no matcher in `classifyWorktreeForceDeleteReason`, and an ordinary desktop delete
already passes `force=true` for the dirty-file skip, so classification returned
null unconditionally: the toast showed raw CLI wording with no Force Delete
button, and a user with a live chat session was stuck unless they knew to reach
for the CLI. That is the #11960 shape `shared/worktree/removal.ts` documents, so
the refusal now has its own prefix, matcher, `WorktreeForceDeleteReason` and
toast copy, classified BEFORE the `force` guard and nulled once the waiver is
spent — exactly how `unstopped-pty` is handled, with matcher and hint kept in
the same file as that contract requires. The copy says Force Delete will close a
running conversation rather than borrowing the "could not confirm" wording,
because Orca watched these sessions stay attached; there is no doubt to waive.

Structured `terminal read` cursors were unsound and are now refused. The PTY
cursor indexes an append-only completed-line buffer with a monotone count; a
session journal is a BOUNDED tail re-projected on every read, so a saved index
addressed different lines as the journal grew — and `truncated` could never fire
to say so, because it tests `cursor < oldestCursor` and `oldestCursor` was always
0. A poller got wrong or duplicated lines under `truncated:false`. Separately, a
streaming turn's lines counted as completed with `partialLine` hardcoded empty,
so a mid-turn cursor consumed a half-written line whose growth was never
redelivered — the `"hel"`/`"hello"` hazard the PTY reader guards against. The
journal does have stable item identity, but `terminal.read`'s cursor is a number
on the wire and cannot carry it, so a cursor read now refuses and names
`worker-read --source transcript`, which already has that contract including
`source_changed`. No cursor space is advertised either: `nextCursor` is null and
the cursor fields are absent, rather than claiming an index the next read cannot
honour. The header claim that all four fields kept their meanings was true of the
shape and false of the invariants; it now says which ones hold.

Two fixes had no test at their real seam, which is the same failure that produced
this whole set — the runtime tested directly, the seam tested by neither. The
group-addressing test hand-composed the recipient list itself, so deleting the
composition at the call site left it green; it now drives `sendGroupMessage` with
no PTY terminals at all. Nothing referenced `isLiveStructuredAgent`, so the
`dispatch --inject` fix had no red-then-green at all; it now has one driving
`RuntimeTerminalAgentPresence.isRunning`. Both were ablated and confirmed red.

Folder-workspace removals sweep and kill PTYs without `requirePhysicalStop`, so
the structured sweep no-opped there and left a live session bound to a workspace
about to be forgotten. They now close best-effort under an explicit
`closeStructuredSessions` flag, kept separate from `requirePhysicalStop` because
the two questions differ: that one asks whether a stop must be PROVEN before
files are touched, and it is what licenses a refusal. These paths do not refuse —
the root is shared so no checkout vanishes under the child, and one of them is a
never-throw forget a refusal would wedge. Reconciliation sweeps set neither and
still close nothing.

Also: the force close is raced against the same sweep deadline every PTY surface
is bounded by, so a wedged provider close reports the timeout instead of hanging
`worktree rm --force` forever; and the refusal now prints a count and the
providers instead of raw session ids, which our own marker rationale treats as
one tab-id hop from a credential.
2026-09-06 03:14:16 -07:00
Neil d19be485d3 test: reject unsupported app-server in golden agent fixture (#19056) 2026-09-06 02:48:34 -07:00
Merge Sim 5b3e4de1cb fix(orchestration): stop two surfaces lying about a worker with no terminal
`orca terminal <verb>` answered `terminal_handle_stale` for a structured
worker's handle. Nothing went stale: the session is live and simply has no
terminal, and it never had one — so callers acted on a false claim and went
hunting for a remint that cannot exist. The refusal now carries its own code and
names the structured equivalents (`orca terminal read`, `worker-read --source
transcript`, `orca orchestration send`), so an agent that lands there learns
what to run rather than what failed. A PTY handle that really did go stale keeps
the old error, and so does a session this runtime no longer owns — that handle
IS dead. `terminal.show` stays non-resolving: synthesising a
ptyId/leafId/paneRuntimeId would hand every public terminal verb something that
looks writable and is not.

`orchestration-worker-specs.ts` promised "the same verbs, the same handle, and
the same worker-read sources", and all three clauses were false for a worker with
no terminal. A spec agents read must not carry a false promise, so it now states
the limitation and the alternative that always works.

Note this had to be reconciled with an invariant this branch already holds: the
worker MODE must stay opaque, or a coordinator starts branching on something no
verb it runs behaves differently for. So the note says "not every worker has a
terminal" and points at `--source auto`/`--source transcript` WITHOUT naming a
kind — the same mode-neutral wording `readStructuredWorkerOutput` already uses
when it refuses `--source terminal`. Both properties are now pinned by tests, so
neither can be restored by breaking the other.
2026-09-06 02:42:51 -07:00
Merge Sim 61577d7a6e fix(orchestration): refuse rather than guess when a chat session has no identity
An ordinary structured chat session — not a dispatched worker — is spawned with
no `ORCA_TERMINAL_HANDLE`, because `structuredWorkerChildIdentityEnv` early-
returns for any session outside the worker registry. `orca orchestration check`
then fell through to `terminal.resolveActive`, which picks the focused tab's
active leaf or the first leaf in the worktree. It returned a valid handle, so
nothing errored — and `check` is destructive by default, so it consumed another
pane's oldest unacknowledged batch and marked it read. The rightful worker never
saw that mail.

`requireUnambiguous` does not fix this, only narrows it: it refuses when MULTIPLE
leaves could be meant, and with exactly one terminal pane in the worktree the
guess still resolves — to a sibling. "One terminal pane plus one chat tab" is a
normal layout, so the common case stayed broken. The pinned test is that case.

So the child now carries `ORCA_STRUCTURED_SESSION`, and every remaining route
that would GUESS an implicit terminal refuses on it with an error naming the flag
to pass. The marker names NOTHING — no handle, no pane key, no session id, no
token — which is the whole reason it is safe: it cannot be replayed, cannot
impersonate, and cannot flow into the hook-attestation, agent-row or
mobile-projection pipelines the way a pane key would. That makes it a different
decision from withholding `ORCA_PANE_KEY`, not a reversal of it. It also grants
no CLI reachability, so packaged builds keep exactly today's exposure.

The comment at `orca-runtime-adopt-terminal-orphans-from-inventory.ts` that
justified the guess — "a structured worker is covered instead by the
`ORCA_TERMINAL_HANDLE` its child is spawned with" — was true only for dispatched
workers and false for every other structured session, a population this branch
creates. It now says which case it covers and which case it does not.
2026-09-06 02:36:46 -07:00
Merge Sim 28ff7f54f2 fix(orchestration): reach structured workers through group addresses
`orca orchestration send --to @all` — and `@idle`, `@claude`, `@codex`,
`@worktree:<id>` — silently skipped every structured worker. Recipients came
from `listTerminals`, which enumerates leaves and PTYs, and a structured session
is on neither. The exclusion happened BEFORE per-recipient resolution, so the
`SendRecipientWarning` machinery never ran: the caller got exit 0 and a receipt
naming the workers that did resolve, and a broadcast "stop work" or "base moved"
reached the PTY workers and nobody else. With every worker structured it
degraded to `terminal_not_found`, which reads as "the group was empty".

Fixed at the group-resolution site rather than inside `listTerminals`. That
result is published to paired mobile and remote clients and to consumers that
assume a summary carries a `ptyId` or is writable, so widening it is its own
change under `docs/reference/remote-wire-compatibility.md`. Group addressing
reads exactly three fields off a recipient, and `RuntimeTerminalSummary` already
satisfies them structurally, so the resolver widens to that smaller shape and
nothing here invents a `worktreePath` or a `branch`. Candidates are liveness-
gated on the same observation the rest of the structured surface uses — mail
addressed to a settled worker would be stored for a lane that will never deliver
it — and once a worker IS a candidate, the existing per-recipient warnings cover
it, so an unresolvable one is reported rather than dropped.

`@idle` needed more than enumeration: `getAgentStatusForHandle` reaches a PTY
probe that throws for a handle with no pane, so a structured worker would have
been enumerated and then silently dropped from the one group address that
selects on status. It now answers from the session's journal — and off the FULL
reduced timeline, never a bounded tail. Settlement tombstones the running turn's
lifecycle item rather than rewriting it, so on any page-sized read a long
tool-calling turn looks identical to an idle session; `@idle` would then
broadcast into a running turn, which Codex answers with `turn already running`
and Claude queues behind. An unreadable session answers null, never idle.

`terminal list` and `worktree ps` still omit structured workers; that is the
wire-visible half and is deliberately not in this change.
2026-09-06 02:31:01 -07:00
Brennan BensonandMerge Sim 6933fd70d7 fix(packaging): ship Claude agent SDK with desktop builds (#19042)
* fix(packaging): include Claude agent SDK at runtime

* test(packaging): cover spaced runtime imports

* fix(packaging): verify every emitted main file for bare runtime imports

The packaged-main verifier read two fixed entry files, but rolldown hoists
modules shared by two entries into out/main/chunks. jsonc-parser is reached
only from a chunk today, so nothing verified it, and the agent-hooks entry
the list names contributes no coverage at all. An import that migrates into
a chunk would silently stop being checked -- the same blindness that let the
missing Claude agent SDK ship.

Scan every out/main/**/*.js entry in the asar instead, keeping the two
required-file assertions as a build-integrity check. Measured against the
shipped 1.4.198 app: 93 entries in 72ms, reporting the absent SDK and
nothing else.

Also tighten the specifier match with a (?<![.\w]) lookbehind. Orca has
three registry methods of its own named require(), two taking a string key,
so a minified registry.require('public-a') otherwise reads as a bare module
specifier and fails packaging with a confusing error -- a risk the wider
file set would have multiplied. The lookbehind drops nothing real: detection
over the shipped bundle is identical with and without it.

* test(packaging): cover the missing packaged main entry assertion

The required-file check had no test, so the refactor that split it out of
the scanning loop could have dropped it silently. Removing the assertion
now fails this case.

* docs(packaging): name the embedded-source-string limit of the main scan

ssh-relay-deploy builds a probe script for the REMOTE host as a string, and
its require("node-pty") / require("@parcel/watcher") survive into
out/main/index.js, where this scan counts them as desktop-main imports. Both
are packaged, so it is benign today, but a remote-only dependency added to
that script would fail desktop packaging with a false message -- and the two
obvious fixes (ship the remote dep, or weaken the guard) are both wrong.
Separating an embedded string from real code needs a parser.

* test(packaging): pin the exact import shape oxc emits for the SDK

The fixture only carried the spaced `import (` variant, so nothing pinned
the form a shipped build actually contains. Use the real emitted shape --
`p??=import(`@anthropic-ai/claude-agent-sdk`)`, no space, backticks, and the
`??=` that precedes it -- and keep the spaced variant on the second entry so
both stay covered.

* fix(packaging): keep the main scan able to see a spread require

The `(?<![.\w])` lookbehind also rejected `[...require("pkg")]`, because the
third dot of a spread satisfies it. That trade is not symmetric: excluding a
member call costs a loud release-build failure if it ever misfires, but
excluding a real specifier is this guard going blind -- the failure mode the
whole verifier exists to prevent. Readmit a dot that ends a spread.

Zero occurrences in the shipped bundle today, so this was latent. The chunk
test's asar mock now also emits directory nodes, because real listPackage does
and extractFile throws on them -- that makes the `.js` anchor's load-bearing
role something the tests can actually catch.

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 02:27:57 -07:00
Brennan BensonandMerge Sim 39cbc68f16 fix(native-chat): honor structured routing with saved options (#19040)
* fix(native-chat): keep saved options on structured route

* fix(native-chat): seed structured session options

* fix(native-chat): preserve create wire compatibility

* fix(native-chat): keep create replayable across an option change

Seeding host-resolved options into the attach fingerprint put a mutable
value into the durable operation identity. A create whose outcome was
unknown, retried under the same operation id after the user reselected a
model, re-resolved different options and hashed to a different
fingerprint — so the ledger refused it as a conflict instead of
replaying. That refusal is not definitive, so no legacy fallback fires
and the launch has no recovery.

Options are the session's initial state, not its identity, and the
reservation still carries them to the record. Excluding them also makes
the digest byte-identical to the pre-change one in every case, not just
when no options resolve.

* refactor(native-chat): name the structured launch option seed

The create-intent resolver narrowed saved options to model/effort with an
inline key literal, inside a file carrying @ts-nocheck — so neither the
key list nor the string narrowing had a typechecked or testable home, and
the repo already expresses this concept as a named shared shape.

Move it to resolveStructuredLaunchSeedOptions beside the persisted
settings it reads, where it is typechecked and unit-tested, and document
why the seed is exactly model and effort: they are the only ids the
picker persists that both providers also accept as strings.

No behavior change. Adds coverage for a non-string persisted effort,
which settings.json can hold and the durable record must not carry.

* test(native-chat): name the structured routing pin for what it asserts

The case drives a saved Codex model and effort through the launch path,
but the structured create intent it asserts on carries no options, so it
pins the route and not the preservation its name claimed. Preservation is
pinned host-side, where the seeding actually happens.

* test(native-chat): pin the empty seed the record cannot carry

valuesByModel is merged over the resolved model, so a stored `model` key
can blank it — the seed then empties out and must resolve to undefined.
Nothing covered that branch, so returning the empty map unguarded stayed
green.

It matters because emitting `{ model: '' }` fails the record's
bounded-string guard, and agent_session_options_invalid is not a wire
refusal code: classifyStoreFailure rethrows it, the client reads the raw
error as an unknown outcome, and the launch strands with no fallback.

Corrects eb33ee6bb3, whose message claimed no behavior change. That
extraction also stopped emitting empty and whitespace-only values, which
is a fix on six input shapes, not a pure move.

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 02:27:44 -07:00
Neil 85c7696427 test: exercise supported ConPTY keyboard protocol reset (#19054) 2026-09-06 02:26:00 -07:00
Merge Sim 3fb592d087 fix(orchestration): arm the takeover signal for structured chat input
`worker-release` closed a structured session a user had taken over, losing work
mid-conversation, while `orchestration-worker-specs.ts:106` promised "Never
closes … user-taken-over terminals".

Every guard was already correct and simply never armed.
`reportWorkerTerminalUserInput` has exactly one call site — the real-user-input
signal on a PTY connection — so structured chat input never reached
`orchestration.workerTerminalUserInput`, `markWorkerTerminalUserOwned` never ran,
ownership stayed `owned` instead of `user_owned`, `retainedReason` never returned
`user_takeover`, and `stopStructuredWorker` proceeded. The durable flag is reused
as-is rather than given a parallel mechanism: it exists precisely so a restart,
an SSH drop or a renderer remount cannot erase a takeover.

Addressed by SESSION, never by pane key. A structured worker's pane key is a
random identity credential — anyone holding it can read and consume that worker's
mailbox, and session ids are embedded in tab ids in plain text — so it stays in
main and the runtime resolves the session to it. Handing it to a renderer to echo
back would make it learnable by anyone who can see a chat pane. The RPC gains an
optional `sessionId` alongside `paneKey`; a host that predates it rejects the
call, and the report is already best-effort with a catch, so that host degrades
to exactly today's behaviour rather than failing a send.

The signal fires from the composer send hook and only past `accepted`: the outbox
dispatcher retries, and orchestration's own pointer nudges never pass through the
composer at all — so neither can be mistaken for a user takeover.
2026-09-06 02:25:51 -07:00
Neil 598a1dc765 test: align Windows shell icons with project runtime ownership (#19053) 2026-09-06 02:14:42 -07:00
Merge Sim 434a829035 fix(orchestration): stop three PTY-only probes answering for structured sessions
Three defects, one shape: a probe that enumerates PTYs or resolves a pane was
standing in for a question that is not about panes at all.

`worktree rm` destroyed a live structured worker. `killAllProcessesForWorktree`
sweeps the renderer graph, the provider session list and the local pty-registry,
and a structured session is registered on none of them — so all three counted
zero, nothing errored, and removal deleted the checkout out from under a running
provider child, which kept running with its `cwd` gone while the dispatch still
reported the worker live and exact. A fourth sweep now asks what the other three
cannot: membership by `location.workspaceId`, which covers a plain chat session
as well as a dispatched worker, and liveness by the same
`live`/`unverifiable`/`exited` observation the rest of the structured surface
uses. It REFUSES a destructive removal rather than auto-closing, on the same
bargain and the same `--force` escape hatch as the unstopped-PTY gate — this is
the verb that deletes a user's work, and a running agent is exactly what they
would want to be told about. Force closes the sessions properly instead of
orphaning a child. Best-effort reconciliation callers are excluded: they repair
state, delete nothing, and must never be failed closed.

Twelve coordinator verbs failed for a structured worker running as itself.
`isLiveTerminalHandle` validated `ORCA_TERMINAL_HANDLE` with `terminal.show`, a
PTY verb whose leaf lookup misses for a session that never had a pane; the pane
remint that would have recovered it needs `ORCA_PANE_KEY`, which a structured
child deliberately does not carry, so every one of them died on
`no_active_sender_terminal` — including the ones the worker's own dispatch
preamble tells it to run. The identity question gets its own probe,
`terminal.resolveIdentity`: a handle and a boolean and nothing writable.
`terminal.show` still refuses a structured handle, because synthesising
ptyId/leafId/paneRuntimeId would hand every public terminal verb something that
looks writable and is not. The PTY half is byte-for-byte today's check,
`getLiveLeafForHandle` included, so its `rendererGraphEpoch` re-check still runs
— that check is the whole reason the sender is validated at all, and a cheaper
probe would have quietly started passing stale post-reload handles. A host that
predates the method answers `method_not_found` and the client falls back to
`terminal.show`, which is correct for that host: one without the identity probe
has no structured workers to miss.

`dispatch --inject` reported `no_agent_detected` for a structured worker, because
`isTerminalRunningAgent` reaches `getLiveLeaf`, throws, and the catch returns
false. A structured session IS the agent; there is no foreground process to
recognise, so it answers before the PTY probes rather than through them.

Also: a Run whose coordinator is structured now gets its `run:` mail. Both lanes
declined and neither logged — the PTY lane because the owner is structured, the
structured lane because the mailbox was not `dispatch:` — so each half believed
the other owned it. The PTY lane's reasoning (a coordinator blocks in
`check --wait`, where a waiter preempts pointer delivery) does not transfer: a
structured coordinator is a chat session whose turn ends. Its `run:` deliveries
take the `hasOutstandingRunDelivery` gate the PTY lane applies for exactly that
mailbox, and only for that mailbox.

The test that would have caught the twelve drives the CLI with
`ORCA_TERMINAL_HANDLE=structworker_…` and no `--from`. Every existing
orchestration CLI test passes `--from` explicitly, so the resolver a real worker
goes through was never exercised — which is why the suite stayed green while the
preamble failed on its first line.

Two files crossed their line ceiling and are split rather than waived:
`worktree-teardown.ts` sheds its two PTY-surface sweeps and the deadline
arithmetic they share, and `orchestration.test.ts` — which sat exactly on 800 —
sheds the two caller-identity suites this change rewrote.
2026-09-06 02:14:15 -07:00
Neil 3d48d3a481 fix(source-control): stack the Create PR notice's settings link below its message (#19046) 2026-09-06 02:03:40 -07:00
Neil 5ae76afda6 test: repair Windows paste fixture setup and newline oracles (#19050) 2026-09-06 01:53:16 -07:00
Neil 0b7837430e test: canonicalize Windows fresh-profile fixture path (#19049) 2026-09-06 01:48:47 -07:00
Merge Sim f02e5bef5c fix(orchestration): deliver direct peer mail to a structured worker, and let a peer read it
Two agent-to-agent verbs had no answer for a worker that IS a structured agent
session, and both failed quietly.

Mail addressed to a worker's own bearer handle — how agents mail each other
outside a dispatch — fell between the lanes. The send stored durably and
reported success, `getLiveTerminalPaneKey` resolved the recipient, and then
neither lane claimed the mailbox: the structured resolver answered only
`dispatch:` addresses, and the PTY lane refuses a structured handle outright.
Nothing errored and nothing logged, so the worker never reacted and the peer
waiting on a reply hung. The resolver now also answers a bare worker handle,
preferring that worker's active dispatch so peer and coordinator nudges share
one operation-ledger budget. A worker BETWEEN dispatches is still nudged, under
a session-scoped key: a dispatch says nothing about whether delivery is safe —
the idle gate and the lease fence do — and its own `check` reads exactly the
direct mailbox the mail is sitting in. The dispatch caller key is left
byte-identical, because the ledger is keyed on (callerKey, operationId) and
reshaping it would re-mint nudges already in flight as second turns.

`terminal read` had no structured branch, so the only peer-accessible read verb
answered `terminal_handle_stale` for a live worker; `worker-read` is closed to a
peer, which holds neither coordinator standing nor a dispatch id. It now serves
the session's journal, projected to LINES and paged by the same reader the PTY
tail uses, so the result stays a plain RuntimeTerminalRead and nothing an agent
reads discloses which kind of worker answered. Bounding and dispatch-capability
redaction are the archive path's, reused rather than rebuilt. A session that is
not attached refuses with the existing not-attached code rather than returning
an empty tail, which would read as "this worker has said nothing".

`terminal.show` still refuses a structured handle. This is read-only on purpose:
synthesising a ptyId/leafId/paneRuntimeId would hand every public terminal verb
something that looks writable and is not.
2026-09-06 01:43:52 -07:00
Neil 337433b39a test: preserve Windows golden command failures (#19047) 2026-09-06 01:30:35 -07:00