Each `it.each([[true, true]])` ran a single case while advertising a matrix, and
the branches guarded by `host && shell` were dead. Naming the case says what the
test proves, and deleting the constant branches leaves only the lane that runs.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Under --adversarial-content the journey had no native baseline, so it derived
the expected changed-file label from the page it was checking and then asserted
the page contained it. That assertion could never fail. The expected label now
comes from the adversarial repository fixture that created the changed file, and
the journey refuses to run when neither an independent path nor a native
baseline is available.
The workspace-row tap that follows the journey moved next to it, so the step
owns the row it returns to and the runner stays under the .mjs line ceiling.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Session snapshot/feed/activate/close/createBrowser/createTerminal,
quick commands and agent options now run as Desktop mobileWeb.* adapters
behind the generic host-request forwarder instead of shell translators.
Voice, notification, terminal, browser, chat, About and diagnostics
settings render as hosted page routes with native-*/web-* operation
backends. Adds the hosted WebView e2e journeys used to validate them.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Require the first-release hybrid baseline and remove 23 superseded shell operations, fallback adapters, and optional settings paths while preserving native RPC compatibility. Keep large Files, Source Control, and native-chat responses bounded in Desktop adapters, and serialize Terminal preference reads within bridge admission limits. Validate all code gates, rebuild and export, and pass serial iOS and Android adversarial journeys; record remaining domain work and certification gaps.
Identify removable hybrid fallbacks and shell branches while preserving native-client RPCs, SSH compatibility and active domain paths. Document the final hybrid baseline prerequisite and update the implementation scope. Audit only; runtime behavior is unchanged.
Record session/host forwarding, Terminal/About migrations, exact code and platform evidence, and remaining domain work. Defer additional crash-loop and resume enhancements under the requested YAGNI scope.
Record the verified Terminal consumer journey alongside Chat and Browser results. The optional OTA crash-loop fixture is deferred outside the worktree under the requested YAGNI scope.
Exercise text scale, autocorrect and custom shortcut persistence through the real hosted settings and session. Reuse picker and terminal inspection logic and restore original preferences. Full iOS adversarial run passed with ok:true against build eab7fd8f5eb4a37e593526e90dcc5105552691db072da8a982cd1ed30784a2e3; all code gates and export passed.
Move presentation to shared screens and persist terminal scale, autocomplete, shortcut layout and custom keys through paired-host page preferences. Reuse native fallback and external links, and correct native restore-setting response unwrapping. Deliberately update session parity and route census assertions. Full code gates and page export pass; iOS consumer verification is running.
Add negotiated host scope, Desktop-owned session terminal creation and native-chat file actions. Reuse execution-host routing, existing mutation identity and bounded forwarding; preserve legacy fallback before dispatch only. All required code gates and page export pass in the integrated tree; iOS interaction validation is in progress.
Share native presentation and apply paired-host page preferences to terminal link behavior. Preserve inherited native values, handle storage failures, and verify saved values plus Chat route recovery through a real iOS WebView restart.
Negotiate bounded page-owned state while retaining legacy routes. Preserve settings through WebView recovery and cold-resume handle rebinding; explicit native navigation clears page state. Cover the actual init envelope and old-page fallback.
Bind terminal identity before opening its stream and revalidate before clear, rename and display-mode dispatch. Reuse authenticated runtime handlers and preserve pre-dispatch legacy fallback without retrying ambiguous mutations.
Reuse the Chat UI settings screen with paired-host page storage and grant-gated navigation. Verify persistence and theme rendering in the full iOS adversarial journey; retain native recovery presentation.
Resolve host-owned chat resources and reuse terminal.send for authenticated TUI actions. Feature-gate the page consumer on dispatch fencing, share the action deadline across binding and execution, and preserve unknown delivery without fallback after mutation dispatch. Keep native image and persistence capabilities and old shell/host paths. Record green gates and corrected iOS evidence.
Use exact Desktop catalog membership and declared cleanup routes alongside the legacy mobile allowlist. Reproduce the forbidden adapter failure at authenticated dispatch, verify file-result privacy and future fields, and update the legacy unsubscribe census for generic cleanup syntax.
Bound catalog and request lifetime and check page/workspace authority after connection waits. Fence retired physical requests across logical cutover, preserve existing response ambiguity, and advertise dispatch support without changing protocol 2. Record the discovered catalog authorization gap in the tracker.
Add bounded native preference storage and route hosted AsyncStorage through it. Preserve namespace and credential isolation, serialize writes, and reject unavailable or corrupt storage. Remove the inert adapter and reconcile the implementation tracker with completed generic streams and platform evidence.
Retain future transcript fields, validate host-owned bindings for every event, and use per-stream cleanup tokens. Preserve legacy host/shell fallback and cancellation semantics. Required gates and page export pass.
Scope resource handles to the connection, workspace and shell-injected page session. Desktop validates current chat bindings and retains future transcript fields; hosted reads use the generic lane with legacy fallback. All required gates and page export pass.
Wait for the diff body rather than just Review controls, and handle Back through Source Control before opening file previews. iOS confirms terminal links and rendered adversarial diff; the full unattended journey remains tracked. Required gates pass in native-chat-read-gates.
Keep authority on failed or malformed tab snapshots. Exercise headless terminal file links through line-addressed hosted previews, since renderer-backed file tabs require a Desktop renderer. Record the observed native tap and host resolution evidence.
Label standalone toolbar actions, match WebView accessibility values, and fence workspace return checks by pathname. Reuse Android labelled navigation and support adversarial journeys without native screenshot baselines. Record passing iOS file parity and the unresolved terminal file-link gate on both platforms.
Reuse host file methods behind Desktop-owned identity redaction and move list/text presentation into the hosted page. Preserve older shells and Desktop versions through legacy fallback, without changing the generic bridge contract.
Advertise page-safe file reads from Desktop and project their raw results in the hosted page. Preserve legacy handlers and fallback for older shells and oversized replies. Track the remaining long-lived shell implementation and platform evidence.
Add authenticated method catalog queries and a bounded generic unary workspace lane. Preserve opaque workspace bindings across awaits and keep actual host calls counted after page cancellation. Move source-control read presentation to code the hosted page can run, with legacy fallback for older shells, older desktops, and oversized raw responses. Preserve v2 and all legacy handlers. Update dispatch, reauthorization, and response corpus ratchets deliberately. Full requested gates pass; the mobile suite passed after rerunning outside concurrent web export. Broader identifier families, generic subscriptions, and native route migration remain separate work.
Pin version 2 independently of release constants, keep installed APK package admission, and preserve cached version-2 page admission. Add a native shell-init regression using a literal shipped protocol version. All requested TypeScript, lint, quality, mobile and runtime tests, web package build, and Kotlin unit gates pass.
<!-- orca-pr-loc -->
<!-- Programmatic LoC summary. Do not edit by hand; rewritten on every commit. -->
| | Files | Added | Deleted | Net |
| :--- | ---: | ---: | ---: | ---: |
| Test | 6 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$544 | $\color{#cf222e}{\Huge{\mathbf{−}}}$49 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$495 |
| Prod | 36 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$1719 | $\color{#cf222e}{\Huge{\mathbf{−}}}$1703 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$16 |
<!-- /orca-pr-loc -->
## ELI5
Orca ships eight skill guides that agents read before running the CLI. Seven of them (everything except `orchestration`, which #16904 rewrites) were command catalogs that had drifted from the binary. This PR rewrites them so an agent reads the outcome, the done bar, and the safe-failure rule first, loads reference material only at the step that needs it, and never sees a command or flag the installed CLI does not define.
## What changed
- **Seven guides rewritten** to one standard: outcome spine first (Result / Done / Safe failure), conditions instead of case lists, one done bar, one autonomy envelope, references loaded at the point of use via `skills get <topic> --full`, every runnable invocation spelled `ORCA`. `orca-cli` is 424→260 always-loaded lines with three references (browser, automations, publishing); `orca-per-workspace-env` is 794→397 with five (provider-vercel, ssh-host, docker-ssh, windows-scripts, failure-modes).
- **Defects fixed in shipped guides:** `emulator camera` (no such command), iOS `permissions` (backend refuses it), Android pane described as "in development" (shipped in June), `relayGracePeriodSeconds: 0` documented as immediate teardown (it is unbounded), doctor `ok: true` hiding `warn`, an SSH exemplar setting both `jumpHost` and `proxyCommand`, a provisioned-root fetch from `origin`, the Linear unconfirmed-write rule keyed on four verbs when ten emit it. Linear and emulator descriptions dropped embedded commands and angle-bracket placeholders (651→329, 732→404 chars).
- **Generator bundles references.** `skill-guides/<name>/references/*.md` is appended to `--full`; `skills get` help says compact by default, full with references.
- **Stubs single-authored.** The resolver ladder, placeholder rule, and older-binary fallback shared by all eight installable `SKILL.md` files come from one `skill-stubs/_shared/cli-resolution.md` fragment composed by the generator. Projections were byte-identical before the content fixes.
- **Guards:** every `ORCA <cmd>` and flag in every guide and reference resolves against `COMMAND_SPECS` (this found the camera defect); descriptions ≤1024 chars with no angle-bracket tokens; reference routing checked both directions; an always-loaded size ratchet (300 lines) that guides may leave but never join. `orchestration` (440 lines on main) is recorded as an exception until #16904 lands its kernel.
## Relationship to #16904
Split out of #16904 so that PR carries only the orchestration guide. On main, `terminal send` has no `--wait-submit` / `--retry-request` and the orchestration kernel still carries the resolver ladder and worktree-selector rule, so this branch pins `accepted: true` for handoff receipts and leaves the orchestration pins where main has them. The merge in either direction is mechanical: #16904 rebased on this becomes a one-file `orchestration.md` change plus dropping the two exceptions.
## Standard
Compound Engineering's portable skill-authoring guidance (outcome spine, conditions not cases, pinned fragile commands with an ordered hatch, references at point of use). NVIDIA SkillEvaluator Tier 1 (`schema,pii,license,quality,unicode,lint`) was run on every guide; its deterministic checks pass, its template nudges (Instructions/Examples sections, 50–150 char descriptions) do not apply to Orca's stub architecture and were not applied.
## Testing
- `pnpm typecheck:tsc:cli` clean; `check:code-quality:changed` and `check:react-doctor:changed` 0 findings
- `pnpm verify:bundled-skill-guides` and skill-bundle manifest verify clean
- vitest over `config/scripts`, `src/cli/skill-guide-cli-parity.test.ts`, `src/cli/skills.test.ts`, `src/cli/specs/skills.test.ts`, `src/cli/help.test.ts`, `src/main/skills`: 240 files / 2,019 pass
- Live smoke on the built CLI of every `skills get <topic>` and `--full`, every emulator, linear, and vm verb named in the guides, and every projection's resolver, GNOME warning, and bounded fallback (done on the #16904 branch before the split; the guide bodies are identical here except the send-receipt vocabulary noted above)
## Deferred product decisions
Merging `orca-emulator` and `orca-emulator-android` into one skill with a platform branch; collapsing `linear-tickets` to a guide alias; a `skills get --reference <name>` selector so a gate table can load one file; a fresh-agent routing eval before trimming the `orca-cli` (1,015 chars) and `orchestration` descriptions, whose quoted triggers each fixed a routing misroute.
The shell parsed the host's transcript reply against a .strict() page contract.
The host already publishes three things that contract has never carried, all on
the transcript lane the hosted page uses:
- Claude's resolved edit hunks (editPatch), on every Edit tool result since #18765;
- a structured providerFrame on a text block;
- text up to MOBILE_TEXT_BLOCK_CHAR_CAP (64 KiB), where the wire allows 4200.
Each was an unrecognized key or an over-long string, so the parse threw. On read
the page got 'Transcript read failed' and rendered nothing; on subscribe
sanitizeEvent returned null and the ledger cancelled with invalid_message, which
is not retryable, so the live transcript died mid-turn. The native app reads the
same host method directly and showed all three fine, so this was a hosted-only
blank screen rather than a missing feature.
The shell now projects each message onto the contract: text and tool output are
clipped with the truncation marker the host itself uses, the tool-call lifecycle
state is carried, a block whose shape the page cannot render is dropped without
its siblings, and a message whose id would break dedup is dropped rather than
clipped. A projected corpus is asserted to always satisfy the schema, and the
.parse stays behind it as a fence.
editPatch and providerFrame are dropped, not widened: mobile renders edits
through diffFromToolCall and has no provider-frame row, so carrying them would
be wire weight no component reads. Both are additive shell->page fields if a
mobile renderer for them ever lands.
Supersedes sanitizeMobileWebNativeChatMessages, which only reached tool inputs.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(palette): seed Cmd+J filter from sidebar show scope
When opening Cmd+J, the palette's host and project filters now
initialize from the sidebar's current Show scope, so results
match the user's sidebar view. The palette can still be cleared
or changed per open; sidebar never reads back palette filters.
* refactor: pass app state to palette filter builder
Let the builder function extract the sidebar scope it needs instead of
requiring callers to destructure and pass individual properties. This
reduces coupling and simplifies the data flow through the palette
initialization lifecycle.
* Make palette filter repo-granular to preserve sidebar scope
Filter options now list individual repositories instead of grouping
multi-repo projects into single rows. This preserves the exact
repository scope shown in the sidebar when opening Cmd+J, rather than
widening selections to entire projects. Removes per-field selection cap
and stale-value reconciliation, simplifying the filter lifecycle.
* Clarify filter naming and seed from sidebar scope on palette open
- Rename projects→repositories in PaletteFilterModel for semantic accuracy
- Rename rawFilter→filterState for clearer intent
- Initialize filter from sidebar scope in local state, refresh on open
- Remove redundant filter reset from selection lifecycle
* Seed Cmd-J filter from sidebar scope and reset on close
The palette now opens with the sidebar's host and repository scope
applied. Filter changes are temporary: closing discards them, and
reopening reseeds from the sidebar's current state.
- Repository filtering is now granular (individual repos)
- Support shared repository IDs across multiple hosts
- Disambiguate duplicate repository names by path
* Add comment clarifying Projects terminology
Document the naming convention for repository-granular filter choices to help future maintainers understand why "Projects" is used as the user-facing term.
* Remove redundant Escape press from worktree palette filter test
The hosted page renders the same Expo chat components as the native app, so a
field the shell drops is a behaviour difference, not a missing view. Three were
dropped.
`workingMode` never crossed the bridge, so `isMobileNativeChatAgentWorking`
could not see a monitoring agent and the page showed a busy indicator, Stop and
a streaming bubble the app deliberately withholds.
`lastAssistantMessageIsToolOutput` never crossed it either, so the page streamed
raw tool output into the chat bubble.
`model` was already in the contract but the shell never projected it, so the
session-option controller always read a null model.
Also accepts the tool-call lifecycle `state` the structured lane publishes. The
contract refused the key outright, so the first producer to set it on this lane
would have failed the whole message rather than been ignored.
All four are Rule 1 optional shell->page fields. The two closed sets collapse to
absent under the tolerant page parse, and absent is each field's pre-existing
reading: a foreground agent, and a call whose liveness the turn's working flag
decides.
Splits the agent-status projection and the host value bounds out of
mobile-web-session-snapshot.ts, which had 16 lines of max-lines headroom left.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
src/shared/terminal-file-link-matcher.ts was a branch-only copy of the
renderer matcher, frozen at its 2026-07-27 shape. Main has changed the
renderer twice since, including widening LOCAL_PATH_REGEX to Unicode
property classes, so mobile terminal taps missed every non-ASCII path
that desktop already linked.
Moves the renderer's file-link cluster to src/shared and deletes the
copy. Nothing in it was renderer-only: the eight modules import each
other plus file-link-location and terminal-file-url-target, which
already live in shared. Mobile's tap and web link provider now import
the same module, so they also pick up the file-uri and bare-filename
passes the copy never had. matchTerminalFileLinkAtColumn moves in with
them, replacing both the copy's version and the renderer test's local
helper.
The WebView-injected copy has to stay a copy, since that script is
interpolated into HTML and cannot import. Its path regex gets the same
Unicode widening, and the drift that let it diverge in the first place
is closed by two non-ASCII cases in the shared conformance suite, which
all three implementations run. Reverting either widening reddens them.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
main shipped both surfaces with no CSP at all: the rich markdown editor
document had no Content-Security-Policy meta, and MobileHtmlPreview fed raw
agent HTML into a react-native-webview with originWhitelist={['*']} and no
sanitizer. Remote https images therefore rendered on main. The branch added
'img-src data:' to both frames, which silently broke them.
Restores parity by adding https: to img-src in both frame policies, and by
letting the preview sanitizer keep an img src that parses as https:. The
editor's own markdown renderer already admitted https urls, so only its CSP
needed widening. Plaintext http: stays blocked in both: the preview sanitizer
strips it and neither policy lists it. script-src, connect-src and frame-src
are unchanged, as are the img attributes the sanitizer copies.
The mobile-web shell policies are deliberately untouched. Their second policy
is the mermaid frame policy, keyed on mermaid-frame.html, so it never governs
these frames; and the shell blocks every http(s) load below CSP anyway via
blockNetworkLoads on Android and a WKContentRuleList on iOS, so widening it
would loosen isolation for no rendering gain.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Investigated whether the gzip residual left by 6654e83588 is reachable: an old client
whose ceiling was the flat MOBILE_WEB_PACKAGE_MAX_RANGE_BYTES + 64 against a new host
answering a full incompressible 384 KiB range.
It is reachable in principle, and the premise that it is not does not hold. The client of
mobileWeb.package.asset.gzip is NOT the hosted page. MobileWebPackageGzipAssetChunkSchema
has exactly two parsers: the desktop host (mobile-web-package-assets) and the APK's
downloader (mobile-web-package-chunk-decoder, reached from mobile-web-package-chunk-
pipeline). The APK ships from the store and the host ships with the desktop, so they skew
for real. The buildId fence does not close it either: the downloader reads the manifest
first and sends the host's CURRENT buildId, so a stale bundle never gets as far as a
mismatched read -- the rejection is the APK's own compiled-in ceiling, not the build.
Measured, not derived: node zlib's stored-block gzip of 384 KiB of random bytes is 393289
bytes against a 393280 ceiling. Nine bytes over, and the base64 cap misses by 12 chars.
decodeGzipMobileWebPackageChunk returns null, the pipeline raises a non-retryable
invalid_chunk, and the whole package download aborts.
What makes it unreachable today is that nothing here has shipped. mobileWeb.package
appears in no release tag -- src/shared/mobile-web does not exist at v1.4.198 and the
string is absent from mobile-android-v0.0.47 -- so no APK was ever compiled against the
flat ceiling. No host-side split or old-cap fit is warranted for a peer that cannot exist,
and constraining the host to a ceiling 9 bytes too small would add real complexity to
serve nobody.
So the invariant worth writing down is the opposite of 'page and host move together': this
ceiling is a host->client wire constant owned by the APK's decoder and cannot be narrowed
once an APK is compiled against it. Comment records that; the zlib test now asserts the
retired flat ceiling is genuinely below what zlib emits at every level, so a future flat
margin cannot come back. Added the check the neighbouring comment asserted but nothing
covered: the widest chunk the ceiling permits still fits the relay's 1 MiB control-lane
frame after base64 and the E2EE layer's own base64.
No production code changed.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
mobile-native-chat-pending-echo.ts was reported dead. It is not: five of
its exports are live through use-mobile-native-chat-pending-deliveries.
What was dead is one export, appendMobileNativeChatPending, which the
delivery hook had re-inlined instead of calling — and the two copies
disagreed.
The hook picks the image-echo branch when the send's normalized text is
empty, but counted prior image echoes with pending.text.trim() === ''.
A marker-only caption like '[Image #1]' normalizes to empty yet does not
trim to empty, so it took ordinal 1 and was then not counted, handing the
next caption-less photo ordinal 1 as well. Both echoes then reconciled
against the same transcript row. The dead export already shared the
discriminator correctly, so the hook now calls it.
Adds the drafts-level regression test; both it and the existing
pending-echo case yield [1, 1] against the inlined version.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
`init.resumeRoute` is MobileWebResumeRouteSchema.optional(), a discriminated union whose
`kind` is a closed literal set. The shell->page tolerant parse rescued unknown members
inside arrays and unknown values for an optional closed set, but isClosedSet only accepts
enums and literals, so an optional discriminated union of objects got no relaxation: a
page built before a kind existed failed the whole init. That is the worst frame to drop --
init is the page's only grant delivery, so one unrecognized route cost it every capability.
The transform now treats an optional/nullable discriminated union like an optional closed
set, because its discriminant is one. Scoped deliberately to an UNRECOGNIZED discriminant
rather than a blanket .catch on the wrapper: a member the page can name but whose fields
break their bounds is a sender bug, not version skew, and still fails loudly. The existing
'rejects unbounded resume routes' assertion (a 241-character workspaceName on a known
'session' route) therefore keeps failing the parse, and the PII strip on hostPath is
unchanged.
Page->shell stays strict, which is what fences the shell's route memory:
useMobileWebResumeRouteMemory only stores what a strict routeState parse produced, so the
shell can never remember a kind its own build cannot replay. The persisted cold-resume
record (mobile-web-cold-resume-route) stores hostIdentity and hostWorkspaceIdentity with
no kind at all, so it cannot carry one either. The only way the shell holds a route the
current page rejects is a mid-session page downgrade, and that now degrades to the page's
default route on every boot rather than bricking it.
Tests: the transform collapses an unrecognized discriminant and still rejects a malformed
known member and a non-object; a real init carrying kind 'someFutureKind' parses through
both page entry points with resumeRoute absent and both grants intact; the page channel
opens workspaceList and keeps its client; a routeState naming an unknown kind is refused.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The speech ledger was the one shell ledger still hand-rolling its record
map, cancellation and posting because it is push-driven. It maps onto
MobileWebSubscriptionLedger with no base-class change: admit plus a
records.set stands in for open, and enqueue carries the broadcast. The
authority now takes a MobileWebSubscriptionLedgerConfig, which retires
the per-subscription post/closed plumbing, and the broker hands all
three consumers one shared subscriptionPosts() sender.
Speech stays out of replaceClient's closeAll on purpose: its feed is the
shell's device runtime, not the host RPC client, so it survives the swap
and a terminal closure frame would wedge the dictation hook in error
with no resubscribe.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The reported gap does not exist: the host re-reads the file inside the
serialised section and compares sha256 against expectedRevision before
writing, so an external editor between the client read and the write is
already rejected. Nothing pinned that, so a refactor could drop the hash
compare and leave runKeyedSerializedOperation as the only fence, which
only orders Orca's own RPCs. Adds a regression test where a queued save
carries a stale revision while an external writer rewrites the file.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
`inputFloor` and `queryReplyAuthority` on the shell->page terminal stream were both
literals the shell fabricated ('held' / true) on every subscribe. Traced each to what
the desktop host actually publishes.
inputFloor: deleted. The desktop's mobile input floor is claimed lazily at write time
(RuntimeTerminalDriverController.beginMobileInputFloor, from terminal-input-delivery)
and is never published on subscribe or on any stream event. Opcode 17 WriteUnavailable
is a per-write refusal with no regain frame, and the mobile-web shell does not even
advertise writeUnavailable:1 in its subscribe capabilities, so it never receives one.
'read-only' was therefore unreachable and canSendInput reduces to the hostReady flag
the scheduler already tracks. The scheduler's own invisibility reset (setVisible(false))
is the real revocation path and still clears hostReady, so behaviour is unchanged.
queryReplyAuthority: kept, renamed queryReplyNegotiated, and sourced from the host. The
host already echoes capabilities.queryReply:1 on the multiplex 'subscribed' frame (the
Rule 2 handshake for opcode 18), and the shell already reads it into
record.supportsQueryReply. That echo is a negotiation, not the election verdict --
isMobileTerminalQueryReplyAuthority is re-evaluated per frame on the host and never
sent -- so the old name asserted something no host computes. Made optional in the
shell->page schema: absent means a shell that predates the field and cannot prove
negotiation, so the page must not attempt a reply. No new host->client field was needed.
Also fixed the downgrade this exposed: when the host had not echoed the capability, the
shell sent the reply bytes under opcode 0 (Input). Those hosts strip inputKind and take
reply bytes as floor-taking shell input -- the exact hazard
TERMINAL_QUERY_REPLY_INPUT_RUNTIME_CAPABILITY documents and the native path already
drops for. The shell now drops instead, and the page stops sending in the first place.
Lease-only streams publish queryReplyNegotiated:false; they negotiate no output
multiplex and every input request on them already fails not_found.
Metadata events carried the same two fields and are never emitted by the shell today;
both are gone from that event, leaving it the displayMode carrier it is.
Compatibility: host->client is untouched (no new field, no changed frame). shell->page
is branch-local; the new field is optional and the page's tolerant parse reads absence
as not negotiated.
Tests: page reads an omitted queryReplyNegotiated as false; the shell reports false and
drops the reply when a host omits the capability echo, and true when it sends it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The bridge client crossed the max-lines budget when the shell-feature query joined it. The two terminal one-shot requests were the only operations still inlined there; every other capability already has a request client.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
Merges #18741, which opens the structured agent-session lane to Claude on
mobile. Three conflicts, all in mobile/src/session:
- mobile-native-chat-eligibility.ts: import block only. Kept the branch's
MobileWebNativeChatAgentStatus/AgentWorkingMode pair and added main's
isAgentSessionHandleProvider. Main's AgentStatusEntry import is dropped
because the branch no longer references it. Main's generalized
agent-session resolution auto-merged unchanged.
- use-mobile-session-terminal-create-actions.ts: kept the branch's hosted
page-adapter prelude, then main's generalized bare-launch gate whole
(isAgentSessionHandleProvider + createMobileStructuredAgentSession).
- mobile-session-route-parity.test.ts: took the branch's pins and
recomputed from the test's own printed values. Ablated first:
use-mobile-session-terminal-create-actions.ts is the only changed file
in MOBILE_SESSION_ROUTE_SOURCE_FILES, matching main's own ablation.
Runtime strings 476 -> 475 (the dropped 'codex' literal) and the nested
function body re-froze; JSX, style, identity, navigation and capability
digests were all unaffected.
The hybrid page path carries no agent-session surface at all, for Codex or
Claude, so nothing in src/shared/mobile-web or mobile/src/mobile-web needed
a wire change. Left as an open parity item.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
hybrid.tsx crossed the mobile max-lines budget when the shell-features list joined the init message. The envelope is the shell's grant and feature declaration, so it gets its own module.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb