Commit Graph
10438 Commits
Author SHA1 Message Date
Jinwoo Hong 0ba7f8dc8d feat(mobile): draw the last known tab strip while a session reconnects (#19258)
* feat(mobile): draw the last known tab strip while a session reconnects

Reopening a workspace the phone has already visited threw away everything
it knew. The route clears its tabs on mount, so until the reconnect lands
and the first snapshot is applied the session screen has an empty header
and a bare spinner, even though the strip it is about to be handed is the
one it drew a minute ago.

Persist the four fields the strip actually draws -- id, type, title, agent
-- per host and workspace, and add a reconnecting-with-cache shape to the
route state so those rows render immediately, disabled, under the ids the
live snapshot will reuse. Live tabs always outrank the cache, so a
mid-session drop keeps its mounted terminals; an exhausted retry loop or a
rejected pairing outranks it the other way, because a strip the user cannot
reach is worse than the existing offline affordance. With nothing cached
the screen behaves exactly as before.

The body stays a placeholder. Replaying stored scrollback into the terminal
WebView would double-render the same rows once the live stream replays them,
so the strip is the cached content and the body waits for the stream.

* fix(mobile): keep shell titles and unpaired hosts out of the cached tab strip

Review of the reconnect strip cache found two ways it leaked.

A terminal's title is whatever the shell last set, which is routinely the
command line: a psql URL with an inline password, a curl with a bearer
token. Both fit well inside the 64-character cap and both were written to
plaintext AsyncStorage verbatim. Browser tabs carried their page title the
same way. Terminals and browsers now collapse to a fixed label, with a
resolved agent naming itself because that lookup is a closed enum. The rule
lives in the storage module rather than its caller, so it holds for entries
an older build already wrote, and a tab type this build cannot draw is
dropped instead of having its title trusted.

The cache also survived forgetting a host. Nothing expired an entry, and
the module-global memory map meant a later save from any surviving host
serialized the forgotten host's rows straight back to disk. Both cleanup
paths now evict by host, dropping the in-memory rows and rewriting storage,
with a pending debounced write cancelled so it cannot restore them.

Also: the storage key digests the workspace id, which ended in a filesystem
path, and cached rows carry the same de-emphasis as the disabled tab-bar
buttons beside them, so an inert row does not pass for a live one.
2026-09-07 04:42:59 -04:00
Jinwoo Hong 23df74d85a perf(mobile): cut the relay reconnect critical path and admit dead sockets faster (#19236)
* perf(mobile): cut the relay reconnect critical path and admit dead sockets faster

Phone medians put E2EE authentication at ~424ms but `connected` at ~630ms,
because the session serialized two RPC round trips behind it: the resume
confirm (`pairing.getEndpoints`) and the capability advisory. Both now ride
the authenticated socket concurrently and off the critical path, so the
session publishes `connected` as soon as E2EE authenticates. Peer identity
is already proven by then — the confirm carries credential/lease bookkeeping
and the cell assignment check, and it still fails the session on a bad answer
or a foreign relayHostId, only later. `persistResumeConfirmation` awaits the
new `whenResumeConfirmed()` instead of assuming the answer is present at
`connected`.

Foreground liveness on a retained relay: `notifyForeground('app-resume')`
now probes past the 10s voluntary minimum on urgent bounds (2s, one miss),
so a socket that died while the process was suspended is admitted in ~2s
instead of ~8s. Focus and network nudges keep the old minimum and bounds.
Relay sessions also gain a 25s idle sweep, gated on foreground so a
backgrounded app spends no probes.

Recovery is no longer blocked by the direct return probe. The probe's 12s
dial is a pure observation on its own socket, so it takes the supervisor's
operation mutex only for the cutover; a relay recovery landing during a
foreground return now starts immediately instead of waiting the budget out.
Requests that do land during the cutover are queued in a new
RelayRecoveryIntentQueue and replayed on release — an owning forced
replacement keeps its intent, everything else replays as a plain recovery.

Tests updated deliberately, for the new ordering:
- 'sends no periodic traffic while an authenticated relay is idle' asserted
  the absence of any relay idle probe, which is exactly the gap D3 closes.
  Replaced by a sweep test plus a backgrounded no-probe test.
- 'rate-limits foreground sequences without suppressing a retry' asserted
  that app-resume was suppressed inside the 10s minimum. An app resume is
  now the one nudge that must never be rate-limited.
- the session helpers waited for the confirm answer before `connected`;
  they now authenticate, read both concurrent frames, and settle them.

* fix(mobile): book backoff when a relay resume confirm fails after the cutover

Review round 1 on 352bfd2300.

P1: publishing `connected` at E2EE authentication made `migrateTo` resolve
before the resume confirm answered, so a confirm that failed afterwards —
a `relayHostId` mismatch from a rehomed desktop is the live case — was still
reported as an `established` dial. registerFailure was skipped, no cooldown
was booked, recordMigration()/setActiveSession() ran for a dying session, and
the queued-recovery replay redialled immediately: a tight loop with a
connected→disconnected blip per pass. The establisher now awaits
whenResumeConfirmed() after the cutover and, if the session is no longer
connected, reports a failed dial (or an aborted one when direct won or the
supervisor went inactive) exactly as a rejected migrateTo used to. The UI
still connects early; only the supervisor's bookkeeping waits.

The state check, rather than getFailure(), is the oracle: a live session can
carry a latched failure without having failed yet, and "is this session still
alive once the confirm settled" is precisely the question migrateTo used to
answer.

P2: the resume probe profile goes to two 2s misses instead of one. The first
frame after a resume rides a cold radio and a possibly distant cell, so one
slow answer is not proof of a dead link; the verdict still lands at 4s rather
than the previous 8s.

Nits: the direct probe's two early returns no longer close the candidate the
finally also closes (the second shape pre-existed); RelayRecoveryIntentQueue
is cleared in the supervisor's stop().

Mutex-hold note: persistResumeConfirmation, and now the establisher's own
await, are bounded by the confirm's request timeout. That would have been the
session's 30s default, so the confirm is pinned to RELAY_CONFIRM_TIMEOUT_MS
(12s) — the same bound migrateTo's waitForAuthenticated applied before.

Test: a supervisor-level case where every dial authenticates then fails the
confirm must book 250/500/1000ms backoff with no immediate redial, and must
never record a migration. It fails on the pre-fix establisher.
2026-09-07 04:40:40 -04:00
Jinwoo Hong f5be177e44 fix(relay): rehome hosts to their preferred region in either direction (#19241)
* fix(relay): rehome hosts to their preferred region in either direction

The regional-rehome worker only moved hosts from a us-central1 cell to an
asia-east2 one, so a host whose desktop later records us-central1 stays where
it was put. Rehoming now compares the fresh preference against the region of
the cell the host is on and moves it to a general cell in the preferred
region either way, through the same drain, migrate, safety, and rate-limit
machinery.

- relay_region_rehome_attempts.preferred_region accepts both regions; existing
  databases are upgraded in place by an idempotent named-constraint swap that
  is safe when several directors start at once.
- A target must carry the drain protocol too: moving a host onto a cell it
  can never be drained off again is the trap this change exists to undo. The
  fleet whose health gates a rehome is now every general drainable cell,
  which is exactly the set of legal sources and targets.
- The trust probe accepts a source cell in any region.

No wire change, and no behaviour change while the durable control is off.

* fix(relay): bound bidirectional rehoming with a per-host cooldown

Moving hosts in both directions removed the property that made the old
one-way worker self-terminating: a desktop whose region probe flips would be
dragged back and forth, one full drain and migrate per flip, because the
preference age never expires while the host keeps reconnecting.

- relay_region_rehome_control gains host_cooldown_ms, an operator input
  plumbed like preference_max_age_ms (workflow, ops script, admin route,
  durable row) and defaulted to seven days. A host with any attempt row
  inside the window, whichever way that move went, is not a candidate; the
  claim re-reads it under lock so an attempt landing between scan and claim
  cannot start a second move. Skips are named host_cooldown, and the lookup
  rides a new index on (user_id, relay_host_id, created_at).
- The candidate scan now also requires the target cell to be enabled, so it
  mirrors the claim-time filter exactly and stops spending batch slots on
  candidates that are certain to be skipped.
- Region CHECK lists are rendered from the shared region list instead of
  being written out four times.
- The operations runbook states that cells without the drain protocol are
  neither sources, targets, nor members of the safety gate.

* fix(relay): keep rehome reads and brakes working across the cooldown rollout

The ops script validated hostCooldownMs on every inspected control, so
against any director image predating the field inspect, pause, disable, and
failed-enable recovery all threw client-side. The workflow always runs from
main while the director image is operator-supplied, so that window opened at
merge and reopened on every rollback: the operator lost read-only visibility
and both emergency brakes while the worker could still be enabled.

The field is now validated only when the director reports it, and every apply
body that echoes an inspected control omits the key when that control lacks
it, so a legacy director never sees an unknown key. The write path stays
fail-closed the other way: enable refuses up front, before any mutation, when
the director does not report a cooldown it could honour.

Also replaces two bare 'us-central1' defaults with RELAY_DEFAULT_REGION.
2026-09-07 04:40:37 -04:00
Jinwoo Hong ecfcc0d833 feat(relay): time successful client accepts and control round trips (#19232)
* feat(relay): time successful client accepts and control round trips

A 6s accept on a cross-region cell was invisible: only the abandoned path
was timed. Record per-stage durations across acceptClient and acceptHostData
(assignment/credential/activity/attach), emit one completed log line per
accept, and aggregate p50/p95/max into the runtime metrics event.

Sample control ping round trips from the pong echo so a host sitting on a
distant cell is visible fleet-wide and per host, rate-limited to one log
line an hour per session.

* fix(relay): review round 1 on accept and control-RTT timing

Omit the accept and RTT percentiles from windows with no samples: accepts
are sparse, so a zero point every 30s would pin the p50 at 0 and collapse
the p95. The *Delta counts still publish, and say when the omission is
expected. Control-renewal output is unchanged.

Add a `basis` stage for the splice lease and connection-basis writes that
run between the host data leg and relay-hello, and start `attach` where the
activity stage ended, so the stages now tile the whole accept and their sum
equals totalMs. Clamp every stage at zero against a backwards clock step.

Carry role/cellId/region on both new log lines, flatten the stage p95 field
names so the log-metric extractors stay top-level, and record that only the
RTT median reads as distance: the desktop echoes the pong on its main
thread, so the p95 and max track desktop stalls.
2026-09-07 04:40:34 -04:00
Jinwoo Hong a3e67365a3 fix(orchestration): recover Codex idle after completion title race (#19243)
* fix(orchestration): recover Codex idle after completion title race

* test(native-chat): enable structured sessions in adoption replay fixture

* test(orchestration): cover deferred pointer recovery after prolonged unknown status

* fix(orchestration): fence completion recovery by process generation
2026-09-07 04:35:56 -04:00
Neil c3a70082c6 Fix MiniMax credential-expiry reporting, region sync, and refresh (#19250)
* Fix MiniMax credential-expiry reporting, region sync, and refresh

Three defects from #14929:

1. The usage endpoint answers an expired cookie or key with HTTP 200 and
   base_resp.status_code 1004, never 401/403 (confirmed against both regional
   hosts). The stale-token branch was therefore unreachable, so expired
   credentials surfaced as 'usage-unavailable' with the raw upstream string,
   and stale policy kept showing old numbers as if the failure were transient.
   Classify 1004 as an expired credential.

2. minimaxEndpoint reached the SettingsUpdate schema and the web store but was
   never projected by RuntimeClientSettingsController.get(), so a paired client
   fell back to 'overseas' regardless of the host's region and rendered the
   wrong console link. Add it to the projection and the store contract.

3. Changing the region persisted without refreshing usage, leaving the previous
   host's snapshot in the status bar until the next poll. Invalidate and refetch
   when the endpoint, group id, or model list changes.

The RPC-level tests mock the controller, so the projection had no real
coverage; the new test fails against the pre-fix projection.

* Localize the MiniMax credential-expiry copy

Classifying 1004 as stale-token made the status bar show the raw English
error verbatim: the new wording matches none of USAGE_AUTH_ERROR_PATTERNS,
whereas the old upstream text ('...log in again') matched and was replaced
with localized copy. That traded a localized-but-misleading message for an
actionable English-only one, which is the wrong trade for the CN users this
work targets.

Tag the error with credentialSource so the renderer can pick the right
localized string per credential kind, and add the three catalog entries.
2026-09-07 01:22:08 -07:00
Neil ffff6eaca2 fix(test): admit the adoption-replay create fixture through the structured gate (#19246)
Semantic conflict between two green PRs. #19176 added this replay test while
`agentSession.*` still admitted a `runtime` client on its negotiated capability
alone; #18700 then made `experimentalStructuredNativeChat` one rule for every
caller. Neither branch saw the other, and main runs no post-merge test gate, so
`agentSession.create` started refusing at the envelope level and the test's
`ok: true` expectation broke.

#18700's rule is the intended behaviour and `create` starts work, so it belongs
behind the gate. The fixture is what is stale: it builds a real
`OrcaRuntimeService` whose client settings are unset. Enable the setting the way
#18700 already did for the sibling pre-commit fixture. The assertions about
durable-identity replay are untouched and now actually run.
2026-09-07 01:01:49 -07:00
Neil 314506003a fix: retain MSYS shell descendants in their terminal job (#19068)
* fix: retain MSYS shell descendants in their terminal job

* test: complete MSYS regression CI registration and teardown contract

* fix(windows): deny job breakaway for the whole Cygwin/MSYS shell family

The per-PTY job probed only msys-2.0.dll, and only for bash.exe/sh.exe.
Cygwin ships the same spawn.cc breakaway logic under cygwin1.dll, and an
MSYS2 zsh escapes exactly like its bash does, so both kept the orphan bug.

Probe the runtime DLL on the shell's own search path instead of matching
shell names: that is the property that decides whether the runtime will
ask for CREATE_BREAKAWAY_FROM_JOB, and it drops the name special-casing.

* chore(patch): restore the conpty.cc index line

The earlier hand-edit dropped it while every sibling section kept one.
Recomputed against the real blobs: applying this patch to 7b286d3d
yields exactly 4b06d185, so git apply -3 has its fallback back.
2026-09-07 00:35:55 -07:00
Neil 374c676f6d fix: repaint hidden output overflow after answered restore deadline (#18904) 2026-09-07 00:25:16 -07:00
Neil 3f4793b6c9 Reorganize MiniMax modules and de-duplicate shared test state (#19197)
* Move MiniMax quota fetch modules into rate-limits/minimax

The five MiniMax fetch/transport modules sat flat among ~110 files covering
eight providers. Nest them so the provider's fetch surface is one directory;
credential stores (main/minimax) and the IPC handler (main/ipc) stay where
their siblings are.

* Build rate-limit and settings test state from shared factories

RateLimitState was hand-copied in 9 places and the full GlobalSettings object
in 2 more, so adding one provider field forced edits in unrelated providers'
files -- which is how MiniMax fields ended up in codex-accounts and the Grok
usage-pane test.

Add createEmptyRateLimitState and createGlobalSettingsFixture and route the
copies through them. Values that deviated from the defaults are passed as
explicit overrides, so the fixtures produce what they produced before.

rate-limit-types.test.ts keeps its literal (it exists to assert the shape) and
service-state.ts keeps its own (InternalRateLimitState is a subset, not the
same type).

* Share the codex-account settings fixture between both harnesses

The two codex-account fixtures still carried the same 30-line override block
verbatim, which is the duplication the shared fixture was meant to remove.
Move it into one createCodexAccountSettings and have both call it.

Also drop the hardcoded POSIX workspaceDir default; callers supply the real
directory and a '/tmp' literal would be a trap on Windows.
2026-09-07 00:12:32 -07:00
Jinwoo Hong a62cfedad8 Resolve push source archive from repository root (#19231) 2026-09-07 03:04:19 -04:00
Jinwoo Hong e4770d712f Restore independent push gateway deployment (#19225)
* Restore isolated push gateway deployment workflow

* Register push deployment in the shared SQL lease census

* Restore push workflow inventory and identity contracts
2026-09-07 02:58:32 -04:00
Neil 2ccf35b135 fix: avoid quadratic trimming during fullscreen terminal redraws (#19214) 2026-09-06 23:41:49 -07:00
Brennan BensonandMerge Sim fa5ef99885 fix(native-chat): settle structured chat turns stranded by a restart (#19122)
* fix: settle structured chat turns after restart

* fix: preserve unconfirmed turn cancellation state

* test: preserve unconfirmed turn lifecycle

* test: narrow unconfirmed cancellation coverage

* fix: keep intentional TUI closes out of recovery

* test: keep branch rename journal mock current

* fix: settle dead TUI handoffs before reacquire

* fix: preserve handoff stage after retry settlement

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 23:34:50 -07:00
Brennan BensonandMerge Sim ba4e79c250 fix(runtime): apply the structured-chat setting to every RPC caller (#18700)
* fix(runtime): apply the structured-chat setting to every RPC caller

supportsStructuredAgentSessions only consulted experimentalStructuredNativeChat
when clientKind === 'mobile', so identical host settings admitted desktop and
in-process callers while refusing a phone. The server branched on client surface.

The setting is now one rule for every caller. The negotiated capability stays a
wire term asked of remote clients only, so a capability-less in-process caller is
still admitted on the setting alone.

Making the projection's structuredNativeChatEnabled argument required surfaced
eight call sites that passed `undefined` for non-mobile clients; they now read the
host setting, so tab projection follows the same single rule.

Announced behaviour change: with the flag off, session.tabs.list/listAll no longer
restore structured tabs for desktop. The desktop renderer already discards them in
that state, and startup record/lease reconciliation is unaffected.

* fix(runtime): keep structured session cleanup available

* test(runtime): enable structured chat in desktop projection fixture

* test(agent-session): settle merged fixtures against the all-clients structured policy

The merge with main left three fixtures written for the old mobile-only rule:
a duplicate getClientSettings key, a create fixture with no host settings at
all, and a projection call whose 'old client' is now the mobile fallback-title
case.

* fix(native-chat): let an admitted caller close a chat after the setting is off

Turning `experimentalStructuredNativeChat` off revoked admission for every
`agentSession.*` method, including `close`. A chat opened while the setting was
on stays mounted, so its owner was left with a live provider child and an X
button that answered `structured_agent_session_unsupported`.

Split the surface by what a method does to work in flight rather than by how it
sounds, and write that rule where the gate lives so the next method lands on the
right side: starting, extending, retaining or reading needs admission; stopping
or retiring work the caller already owns does not. Moves `close` and `cancel`
onto the cleanup gate alongside `unsubscribe` and `release`.

The tightening is unchanged - the cleanup gate still demands the negotiated wire
capability and never creates a host, so an incapable client still cannot see the
surface and no method that starts work is reachable with the setting off.

Extracts the dispatcher harness and the method-to-gate table into fixtures so
the new admission suite can share them without a max-lines disable.

* Drop a duplicate lastActivityAt key carried in from main

The main commit this branch merged (fb322046e8) had two lastActivityAt
properties in the same object literal at both journal stubs, which fails
TS1117 and oxlint. Upstream has since kept only the later value; match it.

Not introduced here, but merged in, so it has to be fixed here.

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 23:32:00 -07:00
c300913f90 fix(mobile): stop double-scaling commit timestamps in history rows (#17731)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
2026-09-06 23:27:13 -07:00
Brennan BensonandMerge Sim 1ae7aa8bb4 feat(native-chat): resume an Agent Session History row into a new structured chat (#19176)
* feat(native-chat): resume an Agent Session History row into a new structured chat

A Claude or Codex row in Agent Session History gains "Resume in New Chat": it
opens a new structured native-chat tab that continues that provider
conversation, with the prior turns already in the journal. Until now those rows
could only be resumed into a PTY terminal; the structured branch could reveal a
chat Orca already owned but could not adopt one it had never held.

Almost all of the machinery existed. Both lanes already resume from the record's
provider handle chain, the journal already has a transcript importer, and the
handle chain already models `adopted` as an origin. The gap was that a create
always minted an empty chain, so the adapters started a fresh conversation. This
seeds that chain.

The client names only the conversation. `agentSession.create` is reachable by
paired mobile clients, so the transcript path and the account home are derived
by the executing host and validated against the account homes it recognises —
a client-supplied path would choose which file the host imports and which
credential directory the provider child launches against.

Failure refuses rather than degrades. A transcript that cannot be found refuses
before anything is created; one that fails or decodes empty *after* the provider
has resumed fails the attach, tearing the child down and publishing no tab,
because an empty journal beside a context-carrying agent claims a continuity the
provider never gave.

Codex can resume into any workspace since it is handed the rollout path; Claude
resolves transcripts under a project key derived from the launch cwd, so it is
offered only for the workspace the conversation was recorded in.

* fix(native-chat): widen adopted-home discovery and keep ordinary launches untouched

Three corrections from review of the first commit.

The adoption's account-home candidates now include the extra Codex homes session
discovery already scans. A row this host listed could otherwise refuse to
resume, which reads as the feature being broken rather than as a scope.

Ordinary launches call `createStructuredAgentSessionLaunchIntent` with two
arguments again. Passing the resume source unconditionally appended a trailing
`undefined` that four existing call-site assertions had to absorb; the churn was
the caller's fault, not the tests'.

The transactional adoption guard's comment claimed the self-exemption is what
lets a committed create replay. It is not: replay is settled earlier by the
operation ledger, and an adoption always arrives with a null expected fence, so
a request naming an existing session id is refused a few lines below either way.
The exemption is part of what "another record" means, and the comment now says
that instead.

* fix: preserve history adoption through create and retries

* fix: replay committed history adoption from durable identity

* fix: validate history before claiming adopted sessions

* fix: extract AI vault resume domains

* fix: recognize typed history resume refusals

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 23:24:21 -07:00
bixandm4air bd242a0158 fix(editor): support Shift+wheel scrolling in combined diffs (#11756)
* fix(editor): support Shift+wheel in combined diffs

* add active modified-pane test

* fix(editor): skip shift-wheel capture when a diff pane cannot scroll sideways

Word-wrapped panes never overflow horizontally, so consuming the gesture
left it dead instead of reaching the outer combined-diff list.

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-09-06 23:02:12 -07:00
Jinwoo Hong d53cbed43f revert: hold mobile push feature for user testing (#19203)
Reverts 3160b54c69. Restore through a separate draft PR after user validation.
2026-09-07 00:30:21 -04:00
Brennan BensonandMerge Sim f1d8545024 feat(chat): support structured /clear and /compact commands (#19164)
* feat(chat): support structured clear and compact commands

* fix(chat): authorize mobile commands and bound clear-chain projection

* fix(chat): localize conversation command send errors

* fix(chat): retain clear pane identity with reopened history

* test: account for combined structured session RPC additions

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 21:17:00 -07:00
Jinwoo Hong fb322046e8 skills: rewrite and trim the seven non-orchestration guides (#19128)
* skills: rewrite the seven non-orchestration guides to one outcome-first standard

Every guide leads with Result / Done / Safe failure, states conditions instead of case lists, keeps one done bar and one autonomy envelope, and loads references at the point of use via `skills get <topic> --full`. orca-cli drops from 424 to 260 always-loaded lines with three references; orca-per-workspace-env from 794 to 397 with five.

Defects fixed in shipped guides: `emulator camera` (no such command), iOS `permissions` (backend refuses it), Android pane described as in development, `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`, and the Linear unconfirmed-write rule keyed on four verbs when ten emit it.

The resolver ladder, placeholder rule, and older-binary fallback shared by every installable SKILL.md now come from one skill-stubs/_shared/cli-resolution.md fragment composed by the generator, which also bundles per-guide references into --full. New guards: every ORCA invocation and flag resolves against COMMAND_SPECS, descriptions carry no angle-bracket tokens, reference routing is checked both ways, and an always-loaded size ratchet (300 lines) that guides may leave but never join.

* skills: address review on the SSH recipe and the parity guard

- ssh-host create script: route the bootstrap ssh through the chosen jump host or proxy command, refuse both at once, use StrictHostKeyChecking=accept-new instead of a blind ssh-keyscan append, and pass gh_token/project_root/repo_url/repo_ref to the remote bash via printf %q so a quote in a value cannot break out of the command.
- per-workspace-env envelope: the step-10 workspace test the user asked for is no longer forbidden by the same paragraph.
- linear guides: name the full verb, ORCA linear list-issues.
- parity guard: a prefix reference such as ORCA linear --help or ORCA emulator --webcam now has its flags checked against every command under that prefix; only an exact path or an explicit ... was checked before.

* skills: tighten prose in the seven rewritten guides

Shorter outcome spines, one idea per sentence, no restated rationale after a rule. No rule, command, or pinned phrase changes; 47 net lines fewer across the guides and references.

* skills: route orca-cli and per-workspace-env gates through --reference

Both guides told agents to load --full at a gate because the per-reference
selector did not exist when they were written. Now that main serves
`skills get <topic> --reference references/<file>.md`, load only the
named file and keep --full as the fallback for an older CLI, matching the
orchestration kernel.

* skills: drop outcome-spine boilerplate from the CLI-wrapper guides

The Result/Done/Safe-failure preambles and Next Action closers restated
rules the body already carries. Agents stop fine without them, and for
a CLI wrapper the command surface is the guide. Keeps the one substantive
rule computer-use's Done block added (never report unverified as success)
inside Action Rules. orchestration and per-workspace-env keep theirs:
those are multi-step workflows where the done bar is load-bearing.

(cherry picked from commit 44a74baf73)

* skills: trim the guides and stubs to what agents actually need

- Drop the Result/Done/Safe-failure preambles and Next Action closers from
  the six CLI-wrapper guides; the one substantive rule (never report an
  unverified computer-use action as success) moves into Action Rules.
- Drop the 'guide may be stale, trust --help' lines: the guide is served by
  the binary that runs the commands, so it cannot be stale relative to it.
- Drop the status --json / open --json preflight from every guide; the stub
  no-guessing paragraph now says to start Orca only when a command reports
  it is not running.
- Cut the ORCA placeholder paragraph in each guide to one line that points
  back at the stub's resolution.
- Trim the orchestration, orca-cli, and computer-use descriptions to trigger
  phrases plus one line of scope.
- Remove the older-binary fallback section from every stub (and its two
  shared blocks); a binary without skills get gets one sentence.
- Remove the guide size ratchet test.

* skills: apply independent review cleanup

* skills: clarify guide loading and Linear command discovery

* skills: harden environment recipe examples

* test: complete branch rename journal doubles

* skills: clarify custom Codex launch and refresh model example

* test: deduplicate journal fix now present on main
2026-09-07 00:03:48 -04:00
Brennan BensonandMerge Sim bf4e270504 fix(native-chat): list the slash commands and skills a structured Claude session actually loaded (#19127)
* fix(native-chat): list the slash commands and skills a structured Claude session actually loaded

The chat composer's `/` menu was built from a curated five-command catalog plus a
host disk scan of skill roots. Neither is what the running session can do: the
session reports its own `/` surface, which carries this repo's `.claude/commands`,
the skills that only reach it through plugin roots, and a hide-list of commands
that mean nothing outside a terminal UI. On one local session the menu offered 6
commands and 17 skills where the session reported 62 commands and 33 skills.

Read that surface per session and let it drive the picker:

- A per-session catalog seeded from the frame that proves the session and kept
  current by every later report, exposed over a new `agentSession.commands` read.
- The report is the authority on WHICH skills exist; the disk scan stays the
  source of scope and description for the names both know about, so a skill the
  session never loaded is no longer offered and one it loaded from a root the
  scan cannot see now is.
- A host that predates the read answers `method_not_found` and the composer keeps
  its curated catalog, so mixed versions and the PTY lane are unchanged.

* test: register agentSession.commands on the three surface ratchets

The structured method count, the mobile allowlist, and the cross-version call
table each enumerate the agentSession surface on purpose, so an additive method
has to be declared in all three rather than counted around.

* fix: preserve session catalog authority and publish live updates

* fix(native-chat): publish authoritative command catalogs on session updates

* fix: seed Claude slash catalog before the first prompt

* test: verify unclassified catalogs survive session publication

* test: complete structured rename journal fixtures

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 20:33:14 -07:00
Brennan BensonandMerge Sim 546fd9b21f fix(native-chat): remember structured chat model and effort picks (#19147)
* fix(native-chat): remember structured chat model and effort picks

Structured Claude and Codex sessions already read the saved launch
options at create, but nothing ever wrote them back. The only writer of
`nativeChatSessionOptions` was the PTY picker, and the composer swaps in
the structured surface for structured panes, so a structured pick went
nowhere: it was forgotten when the session ended and every new session
started at the CLI default.

Persist a settled pick from both the desktop and mobile structured
surfaces. Model and effort are stored as a pair, because a launch
resolves a stored effort only under a stored model — so an effort-only
pick adopts the model it was chosen against, otherwise the remembered
effort never reaches a launch at all.

Two things the persist path deliberately avoids: it writes what the
provider committed rather than what was requested, since Codex
reconciles an effort the newly selected model cannot run; and it never
writes the provider readback, which is the CLI's own default and would
pin a `-m` the user never chose.

* fix(native-chat): persist session option picks atomically

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 20:28:17 -07:00
Brennan BensonandMerge Sim 68dd3909c7 feat(orchestration): orchestrate native-born structured chat sessions (#18827)
* feat(orchestration): orchestrate native-born structured chat sessions

Orchestration resolves every worker through a terminal handle and a pane key
backed by a live PTY. A session created directly as structured has neither, so
it was not refused by orchestration — it was invisible. A coordinator could not
start one, address one, or receive `worker_done` from one.

Add a second authority source rather than a parameter channel. A registry maps a
session id to the same three facts the PTY path supplies — a bearer handle, a
pane key and a host scope — and the four runtime getters consult it before
giving up on `ptysById`. `orchestration.send` and `verifyDispatchCapability` are
untouched: authority stays host-derived and the CLI still cannot assert who it
is. PTY handles short-circuit on the handle prefix, so the terminal path is
unchanged.

Mail travels as a session turn instead of as bytes, on a sibling lane that keeps
the PTY lane's outstanding-run, waiter, reserved-type and batch rules.
Orchestration's database stays the source of truth; the send is best-effort,
exactly as the byte write is, and mail is consumed only on a proven-accepted
dispatch. Delivery waits for the session to be between turns, because one
provider refuses a mid-turn start outright and the other cannot acknowledge one
inside the ack window.

Security properties, each pinned by test: the pane key's leaf is random and
persisted rather than derived, since `check` is identity-gated and accepts a
caller-supplied pane key; the handle is a random bearer token; the child env
carries no pane key, which would otherwise flow into hook pipelines that assume
a PTY leaf; hook attestation stays closed for structured handles; and process
continuity comes from record lineage, never the runtime fence, which the host
bumps during its own crash recovery.

Also remove the "Orchestration paused" notice, which gated only on dispatch
status and rendered over bridge chat where orchestration always worked; refuse
the implicit-sender fallback when a worktree has more than one candidate leaf
instead of guessing; and collapse the archive kinds to one named type with a
compile-time assertion that the capture set cannot drift ahead of the storable
set.

* fix(orchestration): answer the structured idle gate from the reduced timeline

The structured pointer gate read a bounded 40-item tail page. A settled turn is
tombstoned rather than rewritten, so an idle worker with any real history carries
no turnLifecycle item at all and the "full page, no lifecycle item" guard read it
as busy forever: every nudge after the worker's first substantial turn parked on a
settle edge that had already passed, and the preamble tells workers not to poll.
The attention gate had the mirror bug — a prompt older than the tail window was
missed and the nudge was delivered into a session blocked on a human.

Both facts now come from `journal.snapshot()`, the fully reduced timeline, via a
new narrow `readGateFacts` host read; the policy module stays pure and still
projects through the shared helpers the chat view reads.

Also:
- Park `session-not-attached` on the journal edge, so mail that arrives during a
  transient detach is redriven by the re-attach reset instead of sitting unread.
- Resolve a structured worker's provider from the durable agent-session record
  when the registry entry was rehydrated, so a restarted Codex worker is no longer
  reported and archived as Claude.
- Clear `structured_pointer_operations` in every `orchestration reset` scope.
- Drop the per-chat-pane dispatch-status store subscription left behind by the
  removed paused notice, and re-pin the two terminal-pane ratchets it moves.
- Hoist the identical pointer batch selection out of both delivery lanes into
  `selectOrchestrationPointerBatch`.
- Refuse the pre-graph-ready focus-based guess for `requireUnambiguous` callers,
  matching the ready path.
- Move the host teardown phase list into the teardown module it belongs to, which
  is what keeps the host inside its max-lines budget.

* fix(orchestration): discard a structured worker session whose create settled unknown

`commitStructuredAgentSessionCreate` answers `agent_session_operation_unknown` when
`attach` SUCCEEDED and only the tab publish failed, so `created.ok === false` is not
proof that nothing exists. The worker start read it that way and skipped
`discardCreatedSession`, leaving a live provider child that took no hold, has no
`bindingsByDispatchId` entry and no published tab — the outer
`releaseStructuredWorkerSession` no-ops without a binding, and a session that never had
a holder never starts the eviction clock, so nothing in the runtime ever retires it. A
throw out of the commit half is past `attach` for the same reason; the pre-commit half
refuses rather than throwing. Cleanup now asks whether the create MAY have committed,
via the existing `isDefinitiveAgentSessionCreateRefusal` predicate.

Also:
- Strengthen the pre-ready `requireUnambiguous` test so it actually pins the guard: the
  snapshot now carries a focused terminal, so deleting the `? [] :` ternary turns the
  test red instead of leaving the refusal to the ambiguous `listTerminals` fallback.
- Correct the guard's justification comment, which cited `orchestration check` as
  covered. `check` resolves through the `--terminal` scope and still guesses; the
  guard covers the implicit `--from` sender, and a structured worker is covered by the
  `ORCA_TERMINAL_HANDLE` baked into its child.

* docs(orchestration): stop two structured-worker comments claiming guarantees the code does not give

The send-time owner re-check reads `target.refusal`, the snapshot the resolver
already admitted, so `decideStructuredPointerDelivery` can only agree with the
resolve-time answer and `owner-not-settled-native` is unreachable from that call
site. What actually fences an owner that moved is `expectedRuntimeFence`, which a
handoff bumps. Say that, so nobody later drops the fence trusting a re-check that
is structurally a tautology.

`discardCreatedSession` was credited with retiring "a published background tab
that no dispatch owns". It hides the DURABLE tab reference and closes the
session; the live tab snapshot keeps the row, so the background tab this start
published stays on screen until the app restarts. Same for stop and release. The
comment now describes what the two calls do — including that both are no-ops on a
session that was never attached, which is what makes the non-definitive-refusal
path safe to reach unconditionally.

* fix(orchestration): retire a structured worker's chat tab when the worker settles

Starting a structured worker always publishes a real `agent-session:<id>` tab, but
every settlement path only called `setSessionTabVisibility(sessionId, false)` plus
`host.close(sessionId)`. That clears the DURABLE restore index and leaves the LIVE
snapshot untouched, so stop, release and the half-started discard all left a dead
"Claude Chat" / "Codex Chat" tab in the worktree's tab bar for the rest of the app
session — five dispatches, five dead tabs — and opening one re-attached the released
session, respawning a provider child outside orchestration's hold accounting.

The snapshot-pruning half of `closeStructuredAgentSessionTab` is extracted into
`structured-agent-session-tab-retirement.ts` and exposed on the runtime as
`retireStructuredAgentSessionTabFromSnapshot`, so the user-initiated tab close and
the three settlements share one implementation instead of a second copy.

The settlement side is best-effort BY CONSTRUCTION: it runs only after the close is
already proven, calls the runtime method optionally, and swallows any throw. It
talks to no renderer, so the startup release reconciler can call it too. Nothing
here can turn a proven stop into `release_unknown`.

* fix(orchestration): stop a structured worker's nudges, archive and liveness from lying

Five defects in the structured-worker lanes, each with the same shape: a check
that answered from something other than what it claimed to measure.

- The pointer lane gated a WORKER's `dispatch:` mailbox on its RUN's outstanding
  delivery. Delivery rows exist only for a `run:` address, so that row belongs to
  the coordinator — and a coordinator holds one for exactly as long as it is
  acting on received mail, which is when it replies to its workers. The gate is
  gone; there is no coordinator mailbox in this lane to protect.
- `dispatch-rejected` now parks on the journal edge. A rejection consumes no mail
  and nothing else redrives the mailbox, so an unparked pointer left the worker
  idle on durable mail until unrelated mail happened to arrive.
- The released journal archive bounded forward — keeping the HEAD — before
  capping newest-first, so a long worker's archive ended at its early exploration
  and dropped the answer it was released for, under a warning that said the
  oldest messages had gone. One newest-first pass now, and the warning is true.
- The durable pointer operation id was reused on a matching BODY fingerprint, and
  the body names only the unread count. Two unrelated same-size batches collided,
  the host replayed its ledger answer as `accepted` with no turn sent, and the
  lane marked the new mail delivered. Reuse is keyed on the batch's message ids.
- `worker-read` on a structured worker hardcoded `terminal: 'running'` and
  emitted no `liveness`, so a runtime that could not see the session reported the
  worker as alive. It now carries the observed verdict, as the PTY branch does.

Also: the live journal cursor is an index into a re-derived tail window, so the
page's oldest item joins its source identity — a slid window now answers
`source_changed` instead of silently resuming past the items it skipped. And a
stop that reached no host reports `processAction: 'none'`, after installing the
host the way release already does.

* fix(orchestration): stop a released structured archive claiming a close that never landed

`worker-read` on a released structured worker hardcoded `liveness: 'exited'`. The
archive is frozen BEFORE the close, so it proves nothing about the provider child,
and the read is served for `release_state` in `releasing` / `unknown` too — the two
states that exist precisely to record a close that did NOT land. A coordinator that
read `exited` from a `release_unknown` worker would start a replacement over the same
worktree while the original child was still attached, which is the outcome
docs/reference/ssh-execution-boundary.md rule 2 exists to prevent, and it contradicts
the release receipt's own "the structured session close was not proven" text.

The verdict now comes from the resource row the read already holds: only a settled
`released` row is `exited`, everything else is `unverifiable` — which the existing
mapping renders as `terminal: 'unknown'`, the same way the live branch does.

* fix(orchestration): stop a structured worker-start reporting a preamble it never delivered

Two ways a structured `worker-start` handed the coordinator a receipt that did not
describe the worker it got.

`sendStructuredWorkerPreamble` threw only on a refusal and on `rejected`, so a
submission that settled `unknown` fell through as success: the start pushed
`dispatch_input: accepted` and marked the dispatch ready. `unknown` is not rare —
`dispatchSafely` converts ANY thrown adapter call (provider child gone, transport
dropped, ack window missed) into it, and `performSend` still returns ok. The worker
then has no task spec while its coordinator blocks in `check --wait --types
worker_done` until timeout. This PR's own mail lane already states the rule —
"`pending` is not yet an acknowledgement; only `accepted` may consume mail" — so the
preamble now applies it too, and raises `operation_unknown` for the states that
prove neither delivery nor failure, which is the code `failWorkerStartWithReceipt`
turns into the `outcome_unknown` receipt whose nextCommands send the coordinator to
look. `rejected` stays a proven failure.

`--structured` also accepted `--model` / `--effort` and dropped them: structured
session creation takes no launch preferences, while `launch.receipt.effective`
echoes whatever was requested either way, so `--model opus` ran on the workspace
default and the receipt still said `opus`. Refused now, for the same reason
`--terminal` refuses them, and the spec note records that refusal along with the
new-child/new-top-level one it never mentioned.

Tests: the refusal guard had no coverage at all, and `structured-mailbox-pointer-host`
— where the full-timeline gate read lives — had none either; reinstating the bounded
tail there left the whole repo green. Both are covered now, and the vacuous
"never selects an exact provider session" case is re-pointed at the absent
`ORCA_PANE_KEY` that actually keeps that selector shut.

* fix(orchestration): let a structured worker actually reach the Orca CLI, and stop four settlements lying

A structured worker's provider child runs `orca orchestration ...` exactly like a PTY worker's
agent does, but it was handed the ambient PATH. On packaged Linux the CLI installs as `orca-ide`
so it never claims GNOME Orca's /usr/bin/orca (#7904), so bare `orca` execs the screen reader and
the worker can never read mail, reply or send worker_done; on packaged macOS/Windows the bundled
launcher is only reachable from the app's own resources dir. The PTY lane already solves this
inside `buildPtyHostEnv`; that block is now its own module and both lanes call it.

Also:
- a worker start that fails AFTER its session exists now discards the session, so a failed start
  stops stranding a dead chat tab that the durable restore index republishes on every launch;
- a structured worker's resource reconciles to `released` after settlement forgot its identity,
  instead of answering `unverifiable` for the life of the DB;
- `closeAttempted` is set only once a close is issued, so a tab-visibility failure can no longer
  report `closed_agent_terminal` for a running child;
- `forgetSession` prunes only what the settled worker parked, not every sibling whose target
  momentarily fails to resolve;
- release settles with an explicitly empty, warned archive when the journal is unreadable AND the
  session is proven exited — closing the chat tab is routine, and `archive_failed` there wedged
  release on evidence that could never arrive;
- the new migration test uses mkdtemp and cleans up, so it stops failing Windows CI and leaking.

* fix(orchestration): merge the duplicated release-receipts import

The release-completion module imported ./orchestration-worker-release-receipts
twice, which trips import/no-duplicates in audit:code-quality:native. The
changed-file gate does not load that config, so only whole-tree CI saw it.

* docs(runtime): note that a background structured tab re-publish is a no-op

The activate:false branch for an already-published session returns without
writing the snapshot or emitting, so it cannot re-surface a client whose
mirror lost the tab. Orchestration is safe from this only incidentally.

* feat(orchestration): make the worker mode the user's own default, not a flag

`worker-start --structured` was an explicit opt-in that REFUSED --on, --terminal,
--model/--effort and worktree-creating placements. The flag, its spec entry and the
`structured` RPC param are gone: the mode now follows the user's setting for new agent
tabs, so a local claude/codex worker is a structured chat session whenever the user's
own default says agent tabs open as one.

A setting is a preference, not a demand, so none of those combinations refuses any more.
A dispatch that cannot be structured starts an ordinary PTY terminal worker and the
receipt names the mode that ran and why, so the fallback is never silent:

- a remote --on, an existing --terminal, a new-child/new-top-level worktree and
  --model/--effort are decided from the request;
- the agent, TUI launch customization, Codex-on-Windows and the runtime capability are
  decided by the shared launch route;
- WSL, remoteness and the Windows start-time gate are settled by the executing host's own
  agentSession.createSupport, asked once the worktree resolves and before anything is
  created, so a refusal is a terminal worker rather than a failed start.

The decision is the renderer's, lifted rather than copied: `resolveAgentLaunchRoute`'s
structured half and the settings predicate now live in
shared/structured-native-chat-launch-route, which both surfaces call, and the TUI launch
customization test moves to shared beside it. `getClientSettings` gains the two native-chat
default booleans it was missing.

No security invariant moves: the structured worker registry, bearer handle, persisted pane
key, the absence of ORCA_PANE_KEY from the child env, hook attestation and lineage-derived
process incarnation are untouched.

* fix(orchestration): stop the worker mode leaking into the agent contract

The mode a worker runs in is a runtime implementation detail. An agent should be
taught the same verbs, run the same commands and read the same receipts whether it
is a structured chat session or a PTY terminal — otherwise a settings-driven
fallback silently changes what the agent can do.

The real leak was `canDispatchSubWorkers`, which was forced false for a structured
worker. That was not a wording choice: `worker-start` resolved `--from` through
`showTerminal`, which needs a live PTY or renderer leaf, so a `structworker_`
coordinator genuinely could not dispatch. Rather than withhold the capability, the
one fact the command needs from `--from` — its worktree id — now comes from
`getOrchestrationDispatchAuthority`, the same authority the pane-key and
process-incarnation getters already answer structured handles from. Sub-dispatch is
gated on depth alone, identically for both modes.

`showTerminal` itself is deliberately NOT taught structured handles: it returns a
ptyId, a leaf id and a pane runtime id, and synthesising those for a session with no
PTY would hand every caller of a public terminal verb something that looks writable
and is not. `inspectWorkerTerminal` already returns `terminal: null` for exactly
that reason.

Also neutralised three agent-visible refusals that named the worker's kind: a
`worker-read --source terminal` on a worker with no terminal now names the sources
that do work, and both archive refusals say "transcript output" rather than
"structured chat output" (the PTY `transcript_pin` branch said "structured" too).

New tests pin both properties: the two preambles are byte-identical once the handle
and per-dispatch ids are normalised, and a structured coordinator starts a worker
with `showTerminal` rejecting.

* fix(orchestration): stop claiming a structured worker was checked for a prompt

worker-show reported observation.agentWait: null for every structured worker. The
field's own contract says null means Orca looked and found no wait, and absent means
it never looked — and nothing looks here: a structured worker parks on a journal
question item, which no terminal prompt scan can see.

So null was a false negative on the one field a coordinator is explicitly told to
read, and it was mode-dependent: the same worker as a PTY would have reported the
wait. Absent is both the honest value and a state a PTY worker already reaches (an
older host, an unreadable pane, a probe that did not answer), so it discloses
nothing about which mode ran.

* docs(cli): stop the worker-start spec pointing a caller at the worker kind

The note said "the receipt mode field names the mode used and why", which is an
instruction to read a field no verb behaves differently for — the one thing the
mode was not supposed to become. It now says what a caller actually needs: the
dispatch always starts, the options passed are the ones honoured, and every worker
is driven the same way. The receipt still carries the mode for operators and
telemetry; nothing tells an agent to look at it.

* perf(orchestration): coalesce the structured redrive edge

Every journal batch is a redrive candidate, because a settled turn is tombstoned
rather than rewritten — there is no completed row to watch for. That is free while
nothing is parked on the session, but once mail IS parked each batch re-resolved the
dispatch, queried unread mail and read the host's gate facts, only to re-park because
the turn was still running. A turn streaming tool calls paid that per batch.

The edge now coalesces on a 300ms quiet window with a 2s starvation cap, so a
streaming turn costs a handful of evaluations instead of one per batch and a settled
turn still nudges promptly. Delivery semantics are untouched: the gate, the
accepted/rejected/unknown handling and the retain rules all still run exactly as
before, just fewer times. Nor is this the path fresh mail takes to an idle worker —
that is `deliverForHandle` at enqueue time, which this does not touch — so the
common case gains no latency.

The mechanism is the session.tabs notify coalescer, generalised into
`keyed-trailing-edge-coalescer` and called by both rather than duplicated; the
session.tabs windows stay where they were, since 50ms is right for a spinner title
and far too tight for a journal stream. Disposal drops the pending timer rather than
flushing it, on the existing subscription disposer that every settlement already
reaches, so a redrive can never fire for a session no dispatch owns.

* 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.

* 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.

* 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.

* 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.

* 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.

* 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.

* 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.

* 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.

* 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.

* 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.

* 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.

* 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.

* fix(orchestration): require exit evidence before settling structured close

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 20:24:21 -07:00
Neil 2dd3958339 test: distinguish external retention from owned worker recovery (#19190) 2026-09-06 20:16:38 -07:00
Jinwoo Hong 3160b54c69 feat: real background push notifications for the mobile app (#8129) (#18554)
* feat(cloud): add the mobile push gateway and its contract package (#8129)

A small open-source service that holds the APNs key and FCM credentials and
sends background push to paired phones on the desktop's behalf. Hosts
authenticate with a box challenge and HMAC proof on their pairing key, the
same shape the relay uses, so signed-in and accountless desktops share one
path. Tokens are stored; alert text is held only for the coalescing window.

The contract doc in docs/reference is the source of truth for every wire
shape. The interop test runs the real desktop answerer against a real
gateway-issued challenge so transcript drift fails in CI.

* feat(push): register phones and send background push from the desktop (#8129)

Adds the notifications.remote-push.v1 capability, the registerPush and
unregisterPush RPCs on the mobile allowlist, a gateway client with a cached
session and 401 re-auth, a durable unregister outbox, and a dispatcher that
offers every mobile notification to the gateway after the socket fan-out.
The dispatcher is fire-and-forget with one retry and drops registrations the
gateway reports dead.

Puts agentState on the mobile frame and fixes the #4375 wording so a working
agent is never announced as finished. The relay host-proof code moves onto a
shared envelope module with no behaviour change.

* feat(mobile): background push registration, receive, and settings (#8129)

Fetches the native APNs or FCM token, registers it with every paired host
that advertises the capability, and re-registers on token change. Foreground
pushes are suppressed inside handleNotification against the same seen set
the socket path uses, so nothing shows twice. Taps route by host fingerprint.
One Background notifications switch, off by default, with the disclaimer and
needs-input / finished sub-switches; hidden until a paired desktop is new
enough. Adds google-services.json and the expo-notifications plugin.

* chore(cloud): Terraform and deploy workflow for the push gateway (#8129)

Declares the Cloud Run service, runtime account, secrets, and orca_push
database behind push_gateway_enabled, true only in production. The deploy
workflow is gated like the relay's, deploys with no traffic, probes /ready
and a validate-only FCM send, then shifts traffic. It runs as the shared
production deploy account because the Cloud SQL rollout lease grant is
foundation-owned; its extra authority is three bindings on the push service.
docs/push-gateway.md carries the import commands for the resources created
by hand and the APNs key rotation procedure.

* docs: describe background notifications on the phone (#8129)

* docs: check in the mobile push contract (#8129)

Seven committed files cite it as the source of truth for every wire shape;
docs/reference is allowlisted per file, so add the entry.

* test(push): replay one checked-in host-proof vector on both sides (#8129)

Cloud Verify installs only the cloud workspace, so the gateway suite cannot
import the desktop answerer. Replace the cross-workspace import with a fixed
challenge vector generated from the contract package; the gateway fixture and
the desktop answerer each replay it and must produce the same HMAC. A
transcript drift on either side now fails in that side's own suite.

* fix(cloud): open the push gateway with invoker_iam_disabled, not an allUsers binding (#8129)

The production domain-restricted-sharing policy rejects an allUsers
run.invoker member, which the runbook anticipated. Opt the service out of
invoker IAM the way the relay director already does; the host proof is the
authentication either way.

* docs(cloud): the push.onorca.dev record exists and is hand-managed (#8129)

* fix(push): close review findings in the gateway (#8129)

- Quota reservation takes a per-host advisory lock; READ COMMITTED admitted
  a whole burst past the cap (80/80 without, 60/80 with, against Postgres 16).
- Challenge issuance no longer writes push_hosts; the row lands on proof
  verification. Stale hosts prune after 30 days. Per-IP token bucket on the
  two unauthenticated routes.
- Streaming body limit via hono bodyLimit; a chunked body bypassed the
  Content-Length check.
- registrationIds deduped in the schema; per-host device cap of 64; list
  bounded to its schema.
- Gateway-side challenge TTL is the specified 10 s, not 40 s.
- APNs stream settles on close as well as end/error.

* fix(push): close review findings in the desktop client (#8129)

- A gateway registration the registry cannot persist is enqueued for delete
  instead of leaking a live token.
- Unregister outbox re-reads pending per pass, honours enqueues during a
  drain, and retries with backoff instead of waiting for the next launch.
- Dispatcher batches registrations by 20 rather than starving the rest.
- 401 compare-and-clear; a 401 after re-auth is unreachable; refused
  handshakes and 429s are cached briefly instead of re-handshaking per event.
- Service is stopped on quit.

* fix(mobile): close review findings in push registration and receive (#8129)

- Consent generation guards a register that finishes after the switch went
  off; the host is re-queued for unregister instead of recorded live.
- Foreground pushes seed the watermark before adopting the epoch, so a push
  on a never-connected session cannot wipe a valid watermark.
- aps-environment follows the build via app.config.js; the iOS release
  workflow sets it to production. A bare plugin entry wrote development.
- Pushes the OS showed while closed are marked seen before catch-up replay.
- Token null result is not cached; failed capability probes are retried and
  never block an unregister; coalesced summaries are shown but not marked.
- Unresolvable fingerprint routes nowhere and is suppressed in foreground.
- Android channel ensured at boot; capability hook diffs clients by identity.

* fix(cloud): harden the push deploy workflow and size the gateway to the budget (#8129)

- Roll traffic back on a failed post-shift check; delete a candidate that
  never took traffic; retry the origin probe and the FCM probe.
- Assert Terraform-owned scaling instead of mutating it from the workflow.
- Build before taking the Cloud SQL rollout lease.
- Declare the database pool in Terraform (2 per instance, max 2 instances)
  and add the gateway to the connection budget; the previous default put the
  shared instance 65 connections over its ceiling.
- State plainly that the shared deploy identity's relay authority is inherited.

* fix(push): read the runtime from shared state at push startup (#8129)

Threading the runtime through launchDesktopMode put the launch module one
line over the 300-line lint budget after the rebase.

* fix(push): key the unauthenticated rate limit on the hop Cloud Run wrote (#8129)

Cloud Run appends the connecting peer to x-forwarded-for; the limiter read
the left-most value, which the caller controls, so a forged first hop earned
a fresh bucket per request.

* fix(push): close the final security review findings in the gateway and infra (#8129)

- app.onError logs only the error name and answers a bare 500; hono's default
  handler printed the whole error, and a pg error carries the row in detail
- a second per-IP bucket (240/min) runs ahead of the bearer lookup on every
  authenticated route, so forged bearers cannot spend the two-connection pool
- one live session per host: minting deletes the host's earlier row
- device-less hosts are pruned after 1 h, not 30 d; any keypair mints one free
- notificationId is printable ASCII, since it becomes the APNs collapse header
- the impersonated FCM probe token is masked in the workflow log
- prevent_destroy on the Apple secrets and the orca_push database

* fix(push): close the final security review findings in the desktop client (#8129)

- fetch never follows a redirect: a 307 would replay the host proof and the
  phone's token to whatever origin the redirect named
- registerPush params are strict and the paired identity is spread last
- a per-device bucket (10/min) bounds a phone looping registerPush, which
  costs a gateway write and a synchronous registry write each time

* fix(mobile): close the final security review findings in push receive (#8129)

- a push with no epoch can no longer claim a seq-derived dedup key, in the
  foreground or from the tray; a forged seq:N could otherwise swallow the
  real bell at that seq
- a provider-delivered push with no host catalog, or no fingerprint at all,
  stays unrouted instead of falling back to the hostId its raw data carries

* docs(push): record the ip buckets, session and host retention, and the token-ownership limit (#8129)

* fix(push): apply the schema on an untimed pool and retry statement-timeout aborts (#8129)

Ports the relay's #18722 pattern to the gateway: DDL runs on a one-connection
pool with statement_timeout 0 that is closed before the serving pool opens, and
SQLSTATE 57014 joins the bounded transaction retry path.

* fix: harden mobile push delivery and deployment recovery

* feat: align mobile notification preferences with desktop delivery

* fix: accept variable-length APNs device tokens

* fix: deduplicate native APNs and background socket notifications
2026-09-06 23:16:29 -04:00
Neil 79eb66608a test: retain paired browser value from successful poll (#19189) 2026-09-06 20:09:39 -07:00
Neil 6c8ce54ad8 test: publish restored snapshot before draining its held FIFO (#19186) 2026-09-06 20:00:08 -07:00
Neil 1848855515 test: enable software WebGL for Linux CI headful specs (#19001)
* test: enable CI WebGL and route GPU-dependent regressions

* test: retain headful atlas cases in terminal rendering goldens

* test: reuse golden command in project coverage assertions
2026-09-06 19:27:12 -07:00
TimothyVangandNeil 4934920f06 fix(rate-limits): stop reporting Grok usage as 0% when the API omits the percent (#17936)
mapWeeklyCredits treated an absent creditUsagePercent as a confirmed protobuf
zero whenever the weekly period matched billing bounds, so unified-billing
accounts whose credits view never reports the percent showed a confident 0%
and short-circuited the monthly fallback (#15740). Those payloads emit
onDemandUsed/prepaidBalance zeros, which disproves the "encoder drops zeros"
premise.

Resolution order is now: reported percent → monthly used/monthlyLimit pair as
a monthly window → synthetic 0 only when the payload emits no usage scalars at
all and the weekly period is confirmed → unavailable with an explicit reason
the Accounts pane surfaces.

Rebased onto current main from nwparker/grok-usage-percent-fallback (#15878).

Fixes #15740

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-09-06 19:25:57 -07:00
weekbinandNeil e48d83a5e1 Fix MiniMax China usage routing and credential handling (#14929)
* feat(minimax): endpoint selector, API key auth, weekly usage window (#14264)

The MiniMax (MiniMax) Coding Plan usage fetch was hardcoded to the
overseas platform (platform.minimax.io) and a single 5h session
window, so users on the CN endpoint (www.minimaxi.com) got nothing.

Three changes:

- Add `minimaxEndpoint` (`overseas`|`cn`) and
  `minimaxApiKeyConfigured` settings fields with sensible defaults
  that preserve current behavior. The CN endpoint also accepts an
  API key (safeStorage-encrypted via a new
  `minimax-api-key-store.ts` + IPC pair) for users without a
  browser session cookie. Status-bar visibility now OR's both
  credential flags.
- Cookie-jar origin now tracks the active endpoint. Previously
  cookies were stored under the overseas origin and silently
  dropped when the user picked CN — fixed by threading
  `endpointMode` through the request context, the manual cookie
  header path, and the cookie-jar clear.
- Parse the weekly window in addition to the 5h session and
  surface both as per-window chips (`5h [bar] 10%   wk [bar] 20%`).
  The status bar's compact section prefers the session window; the
  popover keeps the existing `Session` / `Weekly` labels. The
  MiniMax fetcher is split into three files (data / parse / main)
  to stay under the 300-line cap.

i18n is scoped to the Settings-page text (en + zh only); the 5H/7D
duration shorthands stay English across locales by project convention.

Tests: 9 new/updated files; cookies + API key exercised end-to-end
via the rate-limit service with the upstream-refactored test files
(`service-minimax-usage.test.ts`,
`web-preload-api-settings.test.ts`,
`web-preload-api-agent-providers.test.ts`,
`service-test-harness.ts`, and the runtime-home / reset-credit
fixtures).

Refs #14264

* Keep merge formatting scoped to MiniMax

* Keep MiniMax credential status in rate-limit test fixtures

* Use the China console origin for MiniMax request referer

---------

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-09-06 19:20:50 -07:00
Neil e9af947035 test: confirm running-command prompts when closing tabs (#18965)
* test: wait for rendered tabs and handle busy close confirmation

* test: wait for create-menu item click actionability

* test: settle initial terminal focus before create-menu actions

* test: capture menu focus events for Linux CI diagnosis

* test: remove menu diagnostics after identifying deferred layout focus

* test: check Markdown menu dismissal after editor readiness
2026-09-06 19:15:57 -07:00
Neil b3acef218a test: verify imported projects through the virtualized sidebar (#19003) 2026-09-06 19:14:42 -07:00
Neil 4be1c01c42 test: await rendered remote agent placement before checking mirrors (#18983) 2026-09-06 19:14:39 -07:00
Neil 357a4d4920 test(e2e): scope paired preview link checks to confirmation (#18924) 2026-09-06 19:14:37 -07:00
Neil 1e301ab1df test: cover native Wayland Hangul in isolated CI (#19174)
* test: exercise native Wayland Hangul in isolated CI session

* test: wait for nested compositor socket before selecting IBus

* test: align Wayland IBus discovery with GNOME environment filtering

* test: assert Wayland launch and register native Hangul evidence
2026-09-06 19:02:54 -07:00
Neil af5918a254 test: use host-qualified paired palette row identities (#19175) 2026-09-06 18:55:26 -07:00
Neil e7563c63f1 fix: fence browser recovery to attach inventory placements (#18910)
* fix: fence browser recovery to attach inventory placements

* refactor: name the attach-inventory fence and make its test deterministic

Extract the placement check into isPlacedAsObservedAtAttach so the recovery
filter stays a flat list of named predicates, and document that omitting
pagePlacementsAtAttach recovers against unfenced live state.

Replace the 30-microtask drain in the post-attach regression with the handler's
own completion: attach only settles after recovery returns, so awaiting the
dispatch orders the assertions instead of guessing at a microtask count.

Verified by forcing the fence open: both regressions fail (the post-attach one
in 60ms on a retired placement) and the other 27 still pass.

* test: settle the attach handler even when the regression fails early

The barrier ran inline, so a waitFor timeout or the placement guard left the
attach handler parked on a promise nothing awaited. Hoist it into settleAttach
and call it from a finally as well; cleanup is guarded and the dispatch promise
is already settled, so the second call is a no-op.
2026-09-06 18:50:15 -07:00
Neil fc37958b45 fix: release floating terminal WebGL contexts while closed (#19000)
* fix: release floating terminal WebGL contexts while closed

* test: pin retention polarity through a real PaneManager

Replace the prototype-surgery fake with a constructed PaneManager so the
suspend path exercises real constructor state, and add the retain-branch
case so an inverted default cannot pass silently.

De-shadow `window` in the system-resume e2e main-process callback.
2026-09-06 18:48:40 -07:00
Andrey 14e0d40e06 fix: recognize the kimi-code process as the kimi agent (#18634) 2026-09-06 18:43:39 -07:00
Neil 463cab2f71 fix(cmd-j): remove duplicate browser ownership inputs (#19172) 2026-09-06 18:40:43 -07:00
Neil 373514ef26 perf(worktrees): stop worktree teardown replacing arrays and maps it never touched (#19145)
* perf(worktrees): stop worktree teardown replacing arrays and maps it never touched

Removing a worktree fires three store writes through
removed-worktree-renderer-teardown.ts, and each handed back a fresh reference
even when it removed nothing:

- remove-worktree-store-cleanup filtered openFiles unconditionally. #19058 gave
  the ~50 record maps in this file identity preservation and missed the one plain
  array; the sibling purge path already had the guard this copies.
  openFiles is selected whole by the editor panel, file explorer and git-status
  polling.
- shutdownWorktreeBrowsers spread-then-deleted browserTabsByWorktree and
  activeBrowserTabIdByWorktree; both now go through omitRecordKeys.
- markShutdownPending rebuilt suppressedPtyExitIds and pendingPtyShutdownIds even
  with no guard ids at all, which is the normal case when the panes already
  exited. It now returns early, and skips the suppressed map when every id is
  already true.

Same contents, same keys removed; only the reference is reused when nothing
changed.

* fix(test): use AppState['openFiles'][number] instead of a nonexistent module

The test imported OpenFile from shared/editor-types, which does not exist. Vitest
passed because a type-only import is erased at runtime; CI typecheck caught it.
I had run tsc before adding this file and never re-ran it.

* refactor(terminals): reuse copyOnWriteRecord in markShutdownPending and pin its identity contract
2026-09-06 18:33:55 -07:00
Neil be10e5455e perf(store): keep recentlyRetiredAgentStatusPaneKeys identity on no-op retirement (#19142)
boundRecentlyRetiredAgentStatusPaneKeys always rebuilt the record, replacing
its reference even when nothing changed; a probe counted 1,099 such writes
across the store suite. Return the existing record when no key would be
evicted and the additions are already its tail in the same relative order.
Key-set equality is deliberately NOT enough: re-adding a key must move it to
the tail because that LRU order decides which key the cap evicts next.

Share the LRU bound with boundRecentlyClosedAgentStatusTabIds, which had the
same always-rebuild shape.
2026-09-06 18:33:44 -07:00
Neil 0cba706b01 fix(ports): coalesce advertised URL refresh bursts (#19150) 2026-09-06 18:33:19 -07:00
Neil deb0be1c52 fix: recognize working WSL1 without a WSL2 kernel (#19061)
* fix: recognize working WSL1 without a WSL2 kernel

* fix: recognize unsigned Windows missing-kernel status

* fix(wsl): fold the missing-kernel guest probe into wsl-availability

The separate wsl-missing-kernel-probe module failed three CI gates: it was
not in the web typecheck project (TS6307), it added a new direct wsl.exe
spawn outside wsl-runner, and its `catch { return false }` tripped the
probe-failure-semantics ratchet.

wsl-availability.ts already owns the answer and is already on the invocation
allowlist, so the probe lives there now. A guest probe that cannot spawn
keeps the real --status failure instead of minting a fresh negative, which
is what the ratchet exists to prevent -- and is the more correct semantics.
2026-09-06 18:33:12 -07:00
Neil e28b15928a fix: avoid credit deadlock during large SSH PTY recovery (#19026)
* fix: avoid credit deadlock during large SSH PTY recovery

* test: restore bounded SSH flood recovery coverage

* test(relay): pin the recovery fence to the accepted checkpoint

The oversized-tail cases asserted that the drain completes, but not that
recoveryEndSu lands on the checkpoint, so passing the pre-rotation snapshot
(which carries the old client's window and a stale creditedEndSu) fenced
below the checkpoint and still passed. Assert the fence value, narrow
boundedPtyRecoveryEnd to the three fields it reads, and cover the exact
one-window boundary that separates a live drain from an ordinary fence.
2026-09-06 18:29:31 -07:00
Neil f88cbb4fc9 fix: keep paired tab updates live after runtime terminal fallback (#19022)
* fix: preserve session publication during runtime terminal fallback

* refactor(runtime): align fallback epoch comment and test preamble

Match the file's `// Why:` comment convention on the inherited
publication epoch, and drop a redundant duplicate mocks import in the
lineage regression test while keeping the required side-effect order.

No behavior change.
2026-09-06 18:29:28 -07:00
Neil 0d973c1505 fix: honor remote terminal insertion in the calling client (#18995)
* fix: settle remote terminal insertion in the calling client

* refactor: share one anchor insertion path for local and remote terminals

Extract the created-tab-after-anchor reorder that the local terminal IPC
bridge already carried into insertUnifiedTabAfterAnchor, and settle the
remote placement through it instead of a second copy.

Also repairs two anchor-resolution gaps in the settlement:
- keep an exact unified tab id (legacy leaf-keyed anchors, browser and
  editor tabs) instead of collapsing every anchor to a terminal parent,
  which could mint a `web-terminal-<browser tab>` id that matches nothing
- fall back to the anchor's own group when the requested group was closed
  while the mirrored tab was still in flight
2026-09-06 18:29:25 -07:00
Neil b497f15b53 fix: preserve renderer browser publication during client-hosted page updates (#18961)
* fix: preserve renderer browser publication during client-hosted page updates

* refactor: drop the now-dead publicationEpoch selection argument

applyBrowserSessionTabSelection took a publicationEpoch and wrote it over
the epoch the spread snapshot already carried. Its only production caller
now passes snapshot.publicationEpoch, so the parameter is a no-op whose
only remaining power is to reintroduce the epoch rotation this PR fixes.

Remove it, and collapse the repeated prototype-cast boilerplate in the new
reconciliation test into one helper.

No behavior change.

* fix: keep reconcile from publishing a browser row twice

The retention filter partitioned existing rows by placement kind, so its
disjointness from the live build relied on a non-local invariant: that the
page registry only ever stores client placements and that server tabs are
empty while no offscreen backend exists. Drop ids the live build already
published instead, so a duplicate row is impossible by construction rather
than by coincidence.

* fix: stop the browser reconcile republishing on a pure reordering

headlessBrowserTabsUnchanged compares by array index, so rebuilding the live
list renderer-first read an interleaved snapshot as changed and republished
with a bumped version and rebuilt tab groups for no semantic change - the
same churn this branch exists to remove.

Key the live set by id and emit it in the order the snapshot already had.
Keying also makes uniqueness unconditional rather than resting on the page
registry only ever storing client placements.
2026-09-06 18:29:22 -07:00
Neil 225a47533d fix: preserve paired host sessions during startup residue cleanup (#18922)
* fix: preserve paired host sessions during startup residue cleanup

* refactor(persistence): tighten the paired-host retention pass

Dedupe the owner-key -> repo-id extraction the retention and seeding
passes both needed, and name the `runtime:*` check instead of repeating
the parse three times.

Reach the session walker directly by exporting
`addWorkspaceSessionWorktreeOwners` rather than fabricating a
`{ workspaceSession }` state slice to get at it.

Correct the docstrings: `runtime:*` also covers a serving host's own
partition, and the "authoritative removal" they promised has no product
caller on a paired client today, so say what the exemption actually
costs.

Add a survived-load assertion to the explicit-removal test, which
otherwise passed against the pre-fix sweep -- the partition was already
empty before the removal ran.

No behavior change beyond the docs and the test assertion.
2026-09-06 18:29:19 -07:00