Repository navigation
fix(native-chat): a paired server admits structured chat by client capability, not its own chat setting - #24203
Conversation
…ity, not its own chat setting A host's experimentalStructuredNativeChat decided whether any paired client could reach agentSession.* at all, and whether session.tabs.* showed it structured tabs. That setting is the host user's own launch preference: whether a new agent opens as a chat or a terminal is decided by whoever launches it. Using it as admission control meant a client whose own preference was "structured chat" was refused on a host whose preference was "terminal", and chats opened while the setting was on were withheld from mobile once it was turned off. The gate now asks one thing: did the client advertise agent-session.structured.v1 (in-process callers negotiate nothing and are always admitted). Tab projection and restore follow the same rule. With the setting no longer gating anything, the separate cleanup gate (close, cancel, unsubscribe, release), which existed only so those kept working after the setting was switched off, is identical to the main gate and is folded into it. The settings listener that republished tabs when the setting changed is removed, since projection no longer depends on it. The host setting still picks the default for launches that start on the host itself (agent.launch from mobile, orchestration worker-start).
…nes and old servers keep terminals Hosts advertise agent-session.structured.client-launch-mode.v1: they admit structured sessions by client capability alone. A remote client that does not advertise it (phones released before agent.launch) asks createSupport to pick the launch mode, so the host keeps answering that with its own setting, exactly as before. Cleanup methods keep their own named gate so a future admission condition cannot make close or cancel refusable.
…chat setting says The structured chat setting decides only what new agents open as. With it off, this machine's structured chats used to be hidden while the host, which no longer reads the setting, still reported them to the workspace activation gate, so a workspace holding only a chat opened empty. The local chat mirror and its startup restore now run whatever the setting says, the continue-after-restart offer follows the chats that exist, and the setting's copy says it applies to new agents.
…ed-chat-capability-admission # Conflicts: # src/main/runtime/rpc/methods/structured-agent-session-admission.test.ts
Round 1 ran the local chat mirror for everyone so existing chats show whatever the setting says. That gave every desktop a permanent session-tabs listener, which turns on the runtime's phone replication paths, plus two full session-tab censuses at startup, and made the browser client mirror its remote host a second time. The runtime now says whether it holds structured chats: its structured host is built only when saved chats were restored at startup or a client created one here, and it announces the moment one is built. The mirror, the startup restore and the continue-after-restart offer run only when the setting launches chats or the host holds some, and never in the browser client. A chat a paired client creates here with the setting off still appears at once. The chat behaviour settings show wherever chats exist, and the setting's copy says it picks what new agents open as. The toggle-off teardown this made dead is removed.
… not once its host is built Session history, resume preparation, terminal resume commands and replay-safe phone launches all build the structured host for users who never had a chat, which turned on the chat mirror and the structured-only settings rows until the next restart. The signal is now derived from the host's records (or a records file still owed its import) and pushed when the first chat is restored or created. A throwing listener no longer fails the install that fired it.
…at on this host starts with A chat on a paired server starts with the server's saved model and options, which the desktop could not read, so its picker showed a guess. createSupport's answer, which the desktop already waits for before a paired launch, now also carries that seed as a new optional field (older clients ignore it). Create and createSupport read it through one resolver so they cannot drift.
…nto its own module Merging main left protocol-version.ts one line over the max-lines limit on this branch. The list of capabilities the desktop advertises to a paired host moves, unchanged, into electron-remote-runtime-client-capabilities.ts, the module the next PR in the stack already uses for it; importers point there.
…ed-chat-capability-admission # Conflicts: # src/main/ipc/desktop-renderer-runtime-capabilities.test.ts
… mode, so retrying an existing chat works with the setting off
…eateSupport answer; a launch-mode client gets supported plus the seed
Review status: #24203 (step 1 of 3)The problem. A paired Orca server used its own owner's chat setting to decide whether anyone could use structured chats on it, and to hide chats that already existed. With the setting off, a desktop or phone asking for a structured chat was refused, and existing chats vanished from phones and from the server's own window. What changes for users. The server now admits a structured chat when the connecting app says it supports one. The chat setting only decides what new agents open as; chats that already exist always show, locally and on paired apps (owner's decision). Released phones keep today's behaviour: "+" still opens a terminal on a server whose setting is off. A machine that has never had a chat does no new work at startup. Fixed during this review (on top of the fixes from the earlier whole-stack review)
Deferred (not in this PR)
Verified
Not verified
CI at the final head |
|
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 🧰 Additional context used📚 Code guidelines (2)📝 WalkthroughWalkthroughThe change adds a client launch-mode capability and updates structured-session admission, launch seed responses, and session-tab projection. The runtime now reports whether it holds structured sessions through IPC. The renderer uses that state with the structured-chat setting to control startup restoration, tab synchronization, and restart offers. Electron remote-runtime capabilities move to a dedicated module. Priority: ➖ Normal Merge Risk: 🟡 Moderate · up to With saved structured chats and the setting off, a startup restore failure can now disable session saving until restart. Contain that optional restore failure before merging. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to The access-policy change does not demonstrate an authorization bypass. The main concern is failure containment: restoring an existing chat can now leave workspace-session saving disabled after a startup error, even when structured-chat launches are turned off. Retained concerns
Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❓ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 40 functions across 50 files. (20 skipped: 5 unsupported, 15 over the file limit.) ✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 4281c2c9-4b6f-4c61-8e25-1290015d9da6
📒 Files selected for processing (74)
src/main/ipc/desktop-renderer-runtime-capabilities.test.tssrc/main/ipc/desktop-renderer-runtime-capabilities.tssrc/main/ipc/runtime-environment-federated-read-routing.test.tssrc/main/ipc/runtime-environment-pairing-verification.tssrc/main/ipc/runtime-environment-request-connections.tssrc/main/ipc/runtime-environment-status-owner.tssrc/main/ipc/runtime-environment-support-routing.tssrc/main/ipc/runtime-environment-transport-routing.tssrc/main/ipc/runtime-environments-call-routing.test.tssrc/main/ipc/runtime-environments-pairing.test.tssrc/main/ipc/runtime-environments-status-diagnostics.test.tssrc/main/ipc/runtime-environments-subscription-lifecycle.test.tssrc/main/ipc/runtime-environments-subscription-routing.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host.tssrc/main/native-chat/agent-session-wire/structured-agent-session-registry.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-registry.tssrc/main/runtime/agent-session-record-store.tssrc/main/runtime/orca-runtime-get-structured-agent-session-create-support.tssrc/main/runtime/orca-runtime-state-fields.tssrc/main/runtime/orca-runtime-structured-agent-session-create-intent.test.tssrc/main/runtime/orca-runtime-structured-native-chat-settings.test.tssrc/main/runtime/orca-runtime-tests/runtime-availability.spec.tssrc/main/runtime/rpc/methods/session-tab-agent-status-projection.test.tssrc/main/runtime/rpc/methods/session-tab-agent-status-projection.tssrc/main/runtime/rpc/methods/session-tab-browser-placement-projection.test.tssrc/main/runtime/rpc/methods/session-tab-close-methods.tssrc/main/runtime/rpc/methods/session-tab-mutation-methods.tssrc/main/runtime/rpc/methods/session-tabs-inventory-census-race.test.tssrc/main/runtime/rpc/methods/session-tabs-inventory.tssrc/main/runtime/rpc/methods/session-tabs-structured-restore.test.tssrc/main/runtime/rpc/methods/session-tabs.tssrc/main/runtime/rpc/methods/structured-agent-session-admission.test.tssrc/main/runtime/rpc/methods/structured-agent-session-gate-classification.test-fixture.tssrc/main/runtime/rpc/methods/structured-agent-session-gate.tssrc/main/runtime/rpc/methods/structured-agent-session-hold.test.tssrc/main/runtime/rpc/methods/structured-agent-session-policy.test.tssrc/main/runtime/rpc/methods/structured-agent-session-policy.tssrc/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.tssrc/main/runtime/rpc/methods/structured-agent-session-send-compatibility.test.tssrc/main/runtime/rpc/methods/structured-agent-session.test.tssrc/main/runtime/rpc/methods/structured-agent-session.tssrc/main/runtime/rpc/methods/structured-session-tab-restore.tssrc/main/runtime/rpc/methods/worktree-rm-background-reply.test.tssrc/main/runtime/rpc/worktree-removal-marker-projection.test.tssrc/main/runtime/structured-agent-session-held-signal.test.tssrc/main/runtime/structured-agent-session-host-refusal.test.tssrc/main/startup/main-process-ipc-bootstrap.tssrc/preload/api/app-api.tssrc/preload/api/app-bridge.tssrc/renderer/src/app-shell/use-app-startup-hydration.tssrc/renderer/src/components/NativeChatResumeOnRestartModal.tsxsrc/renderer/src/components/native-chat-restart-offer-gate.tssrc/renderer/src/components/settings/ExperimentalPane.test.tsxsrc/renderer/src/components/settings/NativeChatExperimentalSetting.tsxsrc/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsxsrc/renderer/src/components/status-bar/NativeChatResumeStatusSegment.tsxsrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/renderer/src/runtime/local-structured-chats.test.tssrc/renderer/src/runtime/local-structured-chats.tssrc/renderer/src/runtime/local-structured-session-tab-retirement.tssrc/renderer/src/runtime/local-structured-session-tabs-sync-hook.test.tssrc/renderer/src/runtime/local-structured-session-tabs-sync.test.tssrc/renderer/src/runtime/local-structured-session-tabs-sync.tssrc/renderer/src/runtime/local-structured-session-tabs-sync/inventory-generation-fence.tssrc/renderer/src/runtime/local-structured-session-tabs-sync/snapshot-apply.tssrc/renderer/src/web/preload-api/web-app-api.tssrc/shared/electron-remote-runtime-client-capabilities.tssrc/shared/protocol-version.tssrc/shared/remote-runtime-client.test.tstests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts
💤 Files with no reviewable changes (4)
- src/main/runtime/orca-runtime-structured-native-chat-settings.test.ts
- src/main/runtime/orca-runtime-state-fields.ts
- src/renderer/src/runtime/local-structured-session-tab-retirement.ts
- src/renderer/src/runtime/local-structured-session-tabs-sync.test.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.
| await restoreLocalStructuredChatsAtStartup(useAppStore.getState().settings, (restore) => | ||
| timeRendererStartupStep('project-structured-session-tabs', restore) | ||
| ) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
git diff 12b8ef8c0be518ccdd71fe5b2d1cccaa744347b8 ccf37e3d04629378268530e96811bc53d0b5f16b -- src/renderer/src/app-shell/use-app-startup-hydration.ts
sed -n '210,360p' src/renderer/src/app-shell/use-app-startup-hydration.ts
sed -n '1,160p' src/renderer/src/runtime/local-structured-session-tabs-sync/inventory-refresh.ts
sed -n '1,100p' src/renderer/src/startup/startup-diagnostics.tsRepository: stablyai/orca
Length of output: 15511
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- helper locations ---'
rg -n --glob '*.ts' --glob '*.tsx' 'restoreLocalStructuredChatsAtStartup|function recoverFromDegradedStartup|recoverFromDegradedStartup|setHydrationSucceeded|setTerminalStartupRestorationReady|session writer|sessionSaving|setSession' src/renderer/src
printf '%s\n' '--- helper definitions and callers ---'
rg -n -C 12 --glob '*.ts' --glob '*.tsx' 'restoreLocalStructuredChatsAtStartup' src/renderer/src
printf '%s\n' '--- startup recovery definitions ---'
rg -n -C 20 --glob '*.ts' --glob '*.tsx' 'function recoverFromDegradedStartup|const recoverFromDegradedStartup|recoverFromDegradedStartup =' src/renderer/src
printf '%s\n' '--- relevant files at head ---'
git diff --name-only 12b8ef8c0be518ccdd71fe5b2d1cccaa744347b8 ccf37e3d04629378268530e96811bc53d0b5f16b -- src/renderer/src
printf '%s\n' '--- base/current helper source ---'
helper=$(rg -l --glob '*.ts' --glob '*.tsx' 'restoreLocalStructuredChatsAtStartup' src/renderer/src | head -1)
printf 'helper=%s\n' "$helper"
if [ -n "$helper" ]; then
sed -n '1,260p' "$helper"
fi
printf '%s\n' '--- current startup tail ---'
sed -n '260,390p' src/renderer/src/app-shell/use-app-startup-hydration.ts
printf '%s\n' '--- base startup corresponding section ---'
git show 12b8ef8c0be518ccdd71fe5b2d1cccaa744347b8:src/renderer/src/app-shell/use-app-startup-hydration.ts | sed -n '240,350p'Repository: stablyai/orca
Length of output: 41626
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- degraded recovery ---'
cat -n src/renderer/src/startup/startup-degraded-recovery.ts
printf '%s\n' '--- persistence gate ---'
cat -n src/renderer/src/lib/workspace-session-persistence-gate.ts
printf '%s\n' '--- gate usages and hydration flags ---'
rg -n -C 10 --glob '*.ts' --glob '*.tsx' 'hydrationSucceeded|terminalStartupRestorationReady|workspace-session-persistence-gate|shouldPersist|session writer' src/renderer/src/store src/renderer/src/lib src/renderer/src/hooks
printf '%s\n' '--- recovery tests around save/readiness behavior ---'
cat -n src/renderer/src/startup/startup-degraded-recovery.test.ts
printf '%s\n' '--- local structured restore tests ---'
cat -n src/renderer/src/runtime/local-structured-chats.test.tsRepository: stablyai/orca
Length of output: 5706
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- gate files ---'
rg --files src/renderer/src | rg 'workspace-session|persistence-gate|session-persistence'
printf '%s\n' '--- hydration writer consumers ---'
rg -n -C 12 --glob '*.ts' --glob '*.tsx' 'hydrationSucceeded|workspaceSessionReady|session writer|persist.*session|session.*persist' src/renderer/srcRepository: stablyai/orca
Length of output: 45661
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- session persistence hook ---'
cat -n src/renderer/src/app-shell/use-app-session-persistence.ts
printf '%s\n' '--- persistence gate test ---'
cat -n src/renderer/src/lib/workspace-session-persistence-gate.test.ts
printf '%s\n' '--- gate implementation references in hook imports ---'
rg -n -C 8 'hydrationSucceeded|workspaceSessionReady|terminalStartupRestorationReady|persist' src/renderer/src/app-shell/use-app-session-persistence.tsRepository: stablyai/orca
Length of output: 22332
Keep structured-chat restore failures out of degraded startup.
When the setting is off but the runtime holds saved structured chats, the new helper runs session.tabs.listAll. A non-OK response throws, and the timing wrapper rethrows it. The outer recovery then leaves hydrationSucceeded false. The session writer stays disabled until restart.
The merge base skipped this call when the setting was off. Catch only this optional restore failure so the normal hydration path can enable session saving.
Suggested fix
- await restoreLocalStructuredChatsAtStartup(useAppStore.getState().settings, (restore) =>
- timeRendererStartupStep('project-structured-session-tabs', restore)
- )
+ try {
+ await restoreLocalStructuredChatsAtStartup(useAppStore.getState().settings, (restore) =>
+ timeRendererStartupStep('project-structured-session-tabs', restore)
+ )
+ } catch (error) {
+ console.warn('Structured chat startup restore failed:', error)
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await restoreLocalStructuredChatsAtStartup(useAppStore.getState().settings, (restore) => | |
| timeRendererStartupStep('project-structured-session-tabs', restore) | |
| ) | |
| try { | |
| await restoreLocalStructuredChatsAtStartup(useAppStore.getState().settings, (restore) => | |
| timeRendererStartupStep('project-structured-session-tabs', restore) | |
| ) | |
| } catch (error) { | |
| console.warn('Structured chat startup restore failed:', error) | |
| } |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Admission by capability.
supportsStructuredAgentSessionsnow ignores the host'sexperimentalStructuredNativeChatand admits a remote caller only when it advertisesagent-session.structured.v1; in-process callers are admitted as before. EveryagentSession.*gate, tab projection and structured restore flows through it. - Legacy
createSupportrule. A remote client that does not advertise the new launch-mode capability still gets the old setting-basedcreateSupportanswer (same error code and reason); every other method ignores the setting. Name and exit comment are explicit. - New
agent-session.structured.client-launch-mode.v1capability. Advertised by the server and by the desktop's own window; deliberately not by mobile or the Electron remote list yet.createSupportnow carries an optionalseedOptionsfrom the same resolvercreateuses. - "Holds chats" signal and renderer mirror. A main-process fact (
records.size || unreadableRecords.size, plus anonFirstRecordpush) drives a new IPC pair; the window's chat mirror, startup restore, restart offer and chat settings now run when the setting is on or the machine holds chats. The toggle-off teardown and its now-dead helpers are removed. - Mechanical extraction.
ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIESmoved to its own module (file-length limit); all imports updated. i18nstructuredCopyupdated in the five catalogs that hold the key.
The core policy change is centralized in one predicate per concern, the legacy path is confined to createSupport, and the tests pin the failure modes that matter (capability-less clients refused, released phones keep the old refusal byte-for-byte, in-process callers admitted, held-signal push-vs-read race, browser client excluded). tc:node, tc:web, oxlint and the targeted suites all pass locally. One pre-existing, environment-dependent failure in runtime-availability.spec.ts (advertises browser screencast…) is untouched by this diff and unrelated.
ℹ️ The documented startup-check follow-up widens its blast radius
The PR notes that a failed startup chat check can surface "Session restore failed / Changes won't be saved until restart", and that this now applies to machines that hold chats with the setting off, not just settings-on users. It is flagged as a known, unreproduced follow-up rather than fixed here — worth confirming a tracking ticket exists before this ships, since it is a data-saving symptom rather than a cosmetic one.
Technical details
# Startup chat restore failure now reaches a wider population
## Affected sites
- `src/renderer/src/app-shell/use-app-startup-hydration.ts` — the restore step now runs via
`restoreLocalStructuredChatsAtStartup` whenever the machine holds chats, not only when the
setting is on.
- `src/renderer/src/runtime/local-structured-chats.ts` — `localStructuredChatsInUse` returns true
on `holds` independently of `experimentalStructuredNativeChat`.
## Required outcome
- A failure of that restore must not leave the session saver in a state where subsequent local
changes are dropped until restart. At minimum, file/track the ticket the PR body references.
## Open questions for the human
- Is there an existing issue for the non-fatal-restore fix, or should one be filed?DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…odule (#24494) #24203 moved ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES out of protocol-version.ts into electron-remote-runtime-client-capabilities.ts. Three files that landed on main while it was in review (SSH access links and managed orcad server work) still import it from protocol-version.ts, and a test #24203 added predates main making the structured host's logger option required, so main's node typecheck fails. Point the three imports at the new module and pass the logger.
…o main passes lint (#24496) The host file reached 302 lines after #24203 and #24311, over the 300-line limit, so the static-analysis job failed on every commit to main. The five chat-tab members now come from createStructuredAgentSessionTabSurface in the existing structured-agent-session-host-tabs module; behaviour is unchanged.
Measured from the post-stage-3 main: 794 commits, 22 conflicting files, only 2 of them in the PTY delivery subsystem. The wall this staged plan existed to isolate is behind us; this stage is identity, usage accounting and renderer surface. Three upstream commits produce most of the conflicts: `9700030689` (headless renderer delivery), `82412dab8b` (profile state in SQLite), `d443320af2` (terminal handoff removal), plus `d2dbe2c385` and `f1463683ee` below. ## The two decisions this merge turned on **OpenCode field meanings: adopt upstream's.** The fork's distinguishing behaviour was counting cache writes in token totals. Upstream `f1463683ee` now does that itself — its `scanner.test` expects 1775, which is the fork's number. What remained was bucketing: upstream has `inputTokens` = uncached+read+write, `cachedInputTokens` = read+write, `cacheWriteInputTokens` for the write subset. Keeping the fork's narrower meanings would have left four cleanly-merged upstream files silently wrong, one of them producing a negative telemetry `input_tokens` that the schema rejects — and the divergence would be invisible to typecheck, since the field names match and only the meanings differ. All five cluster files are therefore byte-identical to `b601b98644`, and every asserted total in `cache-write-tokens.test.ts` is unchanged (1120, 2000, 3125): the feature is preserved, only its composition moved. Fork files adapted: `snapshot-rollups.ts`, `usage-provider-normalization.ts` (restored to upstream — otherwise cache is counted as new input and writes are counted twice), `usage-overview-model.test.ts`, `cache-write-tokens.test.ts`. **`agent-state-rules` URL: keep the fork repo.** Upstream stablyai#24387 downloads rules from `${MAIN_RELEASE_REPO}`, which the fork rebranded to `sj900211/orcinus`. A 404 is non-fatal — the updater warns and serves the bundled rules — so the fork keeps pointing at itself rather than fetching runtime data from a repository it does not control. Upstream tests that pinned `stablyai/orca` had a premise that is false by a ratified decision (`release-channel.ts:27`), so their expectations were corrected, following the precedent of rebrand commit `222baedec6`. Each edited assertion carries a `// Why` marker so a future sync does not revert it. This is correcting a stale premise, not weakening a guard: the tests still pin how the URL is built. ## The Windows CLI launcher was replaced wholesale Upstream `d2dbe2c385` deleted `OrcaCliLauncher.cs` and shipped a Rust crate. The fork's `Orcinus.exe` fix (hidden quest ③) is ported into `native/windows-cli-launcher/src/main.rs` at the four call sites; the `.cs` is removed, which a cross-host test now requires because its MSIL image was vendor-flagged (stablyai#23383). The three `Orca CLI` strings and every `ORCA_*` environment name stay — the CLI is still named `orca`. Without this port the packaged `orca` CLI fails on every Orcinus Windows install, which is the precondition for all orchestration work in this repo. `.github/workflows/orcinus-cd.yml` gains a Rust toolchain step on the Windows leg. Deferred by decision: rebranding the launcher's `build.rs` metadata and icon (cosmetic, and the three pieces must move together). ## Defects found that no conflict marker could show Six files merged cleanly and were still wrong: - `pty/session.ts` family — carried from stage 3's pattern, handled there. - `usage-web-client-fallback.test.ts` lost two slice imports upstream narrowed. - `ui-slice-preference-actions.ts` breached `max-lines` at 302 (fork +10, upstream +12). Resolved without a disable by moving the fork's number-format action to a sibling module; the file is now identical to upstream. - `startup-window-role-reconnect.ts` and `folder-workspace-initial-terminal.ts` are fork-only extractions made for `max-lines`; upstream rewrote the originals, so stablyai#24203 and stablyai#23693 were ported into them by hand. - Upstream's new Reset Terminal bypassed owner-window routing; it now routes. - Upstream's new `terminal-intentional-stop-exit.test.ts` saw zero `pty:exit` sends until given a routed main window. - `satellite-open-interception.ts` would have raised a satellite window on a background CLI open, breaking the "change nothing on screen" guarantee stablyai#24244 added. ## Review An independent critic returned REJECT with **no production defect**: every fork behaviour present, both decisions correctly applied, every gate reproducible. The rejection was for 14 tests in 6 files that the gate's directory scope never ran — all of them new upstream tests pinning upstream identity, the same class the `agent-state-rules` decision had ruled on and which it had been applied to too narrowly. All six are fixed, test-only, each with a `// Why` marker. The critic also corrected a claim in the implementation report: the fork already carries upstream's `agent-state-rules-publish.yml`, so live rule updates need only an `agent-state-rules` environment and a `publish-next` dispatch, not new machinery. `sftp-upload.test.ts` deserves a note: upstream's new cases assert a direct `createWriteStream`, but the fork uploads atomically to `.orcinus-part-<uuid>` and publishes by rename (`sftp-upload.ts:61-63`). The fork's contract stands; the expectations moved to it. One of those cases is `skipIf(win32)` and could not be observed here, but fork CI runs on ubuntu, so it was fixed from the code. ## Gates Cold typecheck (after deleting `config/*.tsbuildinfo`): 0 errors, measured twice by different workers. oxlint: 0 warnings, 0 errors across 29,465 files. verify-orcinus-identity: OK, `upstreamRefs=0`. Unsigned smoke: OK, `icon=orcinus.icns`. Fork contract tests: 24/24. Windows CLI launcher tests: the Rust build actually runs here and passes. Attribution, using the critic's harness across three trees (this merge, main, and `b601b98644`): a 1,236-file fork-adjacent sweep and a 54-file identity sweep went from 14 merge-only failures to 1. That one is the release-tag census entry for the fork-only `verify-orcinus-release-assets.mjs`: the census builds its keys with `join()`, so on win32 they carry backslashes. Six upstream sibling cases fail the same way on the upstream baseline, a direct POSIX evaluation of the key logic passes, and a negative control confirms the entry is detected. The census's upstream key logic was deliberately left alone. Known and not addressed here: `updater-prerelease-feed.ts:156` matches only `stablyai/orca` asset URLs — present on `main`, not merge-caused, and invisible to the identity verifier. The release path has not been exercised with this merge (`build:orcad` moved from bun to node); an `orcinus-cd` dry run is advisable before the next fork release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

ELI5
Orca can show a Claude or Codex agent as a structured chat: Orca runs the agent itself and draws its messages as a chat, instead of drawing a chat over an agent running in a terminal (the older terminal-backed chat). A paired Orca server is another computer running Orca that your phone or desktop is connected to.
Until now, a paired server used its own owner's chat setting to decide whether anyone could use structured chats on it, and whether chats that already existed there were shown at all. After this PR, the server admits a structured chat when the connecting app says it can handle one. The chat setting only decides what new agents open as. Chats that already exist always show.
This is step 1 of a 3-PR plan to make structured chat work for workspaces on a paired Orca server:
This PR is safe to land alone. Older desktops, released phones, the browser client and older servers all keep working (see "Wire compatibility").
What Changed
The problem, as a user saw it
The server owner's setting "Use updated structured native chat" (
experimentalStructuredNativeChat) controlled three things it was never meant to control:structured_agent_session_unsupported). The user's own choice on their own device didn't matter.Why it happened: the server treated its own setting as admission control for every caller and as a filter on what exists. But the setting is the owner's launch preference: how they want new agents to open. Whether a new agent is a chat is the launching app's call, and a chat that exists is a chat that exists.
Scope
agentSession.*) and which tabs it lists (session.tabs.*) for phones and desktops;After this PR, as a user sees it
Mechanism
supportsStructuredAgentSessions(structured-agent-session-policy.ts), now decides everyagentSession.*method, tab projection and restore of structured tabs: did the connecting app advertiseagent-session.structured.v1?agent-session.structured.client-launch-mode.v1.agent-session.structured.v1while still gating on their setting, so that capability alone could not tell an app its chat would be admitted.DESKTOP_RENDERER_RUNTIME_CLIENT_CAPABILITIES) advertise it. With it, retrying a failed existing chat in the server's own window works with the setting off. fix(native-chat): the desktop declares structured chat support to paired Orca servers #24204 adds it for paired desktops.agent.launchexisted askagentSession.createSupport"should this '+' be a chat?". They read any refusal as "open a terminal".requireStructuredCreateSupportAdmissionstill answerscreateSupportwith the server's setting. It returns the same error code and reason as before (clientCapabilityMissing), and an unreadable settings store still reads as off.agent.launch. The code comment names this exit.requireStructuredCleanupHost. It asks only for the capability and a running chat host.app:holdsStructuredAgentSessions(ask), plusapp:structuredAgentSessionsHeldChanged(push when it changes, for example when a paired app creates the first chat here).src/renderer/src/runtime/local-structured-chats.ts). The browser client has no runtime of its own and always answers no.seedOptionson thecreateSupportreply.createuses, so an app's picker shows whatcreatewill actually run.ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIESmoved, byte for byte, fromprotocol-version.tstosrc/shared/electron-remote-runtime-client-capabilities.ts.protocol-version.tsover the file-length limit.Why
The launching app decides how a new agent opens. The server refuses only what it cannot run. Its own preference fills in only the choices an app leaves to it. Alternatives considered:
Differences from the common pattern
agentSession.createSupportfollows the server's setting for apps that do not choose the launch mode. The common pattern never branches on the client.createSupportto choose. It can be removed once the oldest supported phone launches throughagent.launch. A removal ticket is still to be filed.agent.launch, the CLI, orchestration workers, and the legacycreateSupportanswer.clientCapabilityMissingalthough the real cause is the server's setting. The common pattern names the actionable cause.createSupportreply, not a read model.create, and both read one resolver. So the picker shows whatcreatewill run, with no extra round trip.agent-session.structured.v1is already used in both directions.Wire compatibility
Only a new optional capability and a new optional reply field are added. The new IPC is between the main process and the renderer of one build and never crosses the wire.
seedOptions.supportedfromcreateSupport.agent.launchexisted:createSupportgives the same answer as before, so "+" opens a terminal while the server's setting is off.agent.launch, where the server's setting decides. It callscreateSupportonly against old servers.seedOptions: sent only when a chat is supported and the seed is not empty. Phones and older desktops read onlysupported.Linked Issue
None. This is a maintainer PR with no pre-existing issue.
Visual Proof
Live run on a separate Mac (dev build of this branch at
7fdebc8aab3, isolated home and app data, real Claude). Every scenario passed, with no console errors.Setting on: a structured chat answers, with no restart, on a machine that had never held one

Setting off again: the existing chat still shows as a chat

Setting off: a new Claude launch opens the terminal-backed chat

Chat settings stay reachable while the machine holds chats

After a restart with the setting off: the chat is restored, and a follow-up still gets an answer


Retrying a failed chat could not be reached in the UI without breaking the Claude binary; it is covered by
desktop-renderer-runtime-capabilities.test.tsand the admission tests instead.Testing
New and updated tests, and what they prove:
structured-agent-session-admission.test.tsagentSession.*method is served to an app that advertised the capability and refused to one that did not.createworks for a paired app. Close stops the agent and retires its tab, and cancel lands.createSupportanswer, and an unreadable settings store reads as off.seedOptionsmatches whatcreateuses and is absent when the chat is unsupported.structured-agent-session-policy.test.ts,structured-agent-session.test.ts: the single admission rule and the legacy-rule predicate, for phones, desktops and in-process callers.session-tab-agent-status-projection.test.ts,session-tabs-structured-restore.test.ts: tabs and restore follow the app's capability, not the setting. Phones without support get fallback rows, and paired desktops without support get nothing.structured-agent-session-held-signal.test.ts,structured-agent-session-registry.test.tslocal-structured-chats.test.ts,local-structured-session-tabs-sync-hook.test.tsExperimentalPane.test.tsx,NativeChatResumeStatusSegment.test.tsx: the chat settings and the "continue your chats" offer appear when the machine holds chats, whatever the setting says. A paired server's chats do not make this machine ask its own runtime.desktop-renderer-runtime-capabilities.test.ts,runtime-availability.spec.ts: the desktop's own window and the server advertise the launch-mode capability.remote-runtime-client.test.ts: covers the moved capability list.tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts: against the real released build's capability list, a released phone asking a server whose setting is off still gets exactly the old refusal, and a client that chooses its own launch mode is told yes plusseedOptions; the server advertises the new capability.Results:
pnpm tc:nodeandpnpm tc:webpass; the targeted test files (275 tests) and the CI cross-version job (66 tests) pass locally. The last commit only adds cross-version test cases.A separate review proof ran the real dispatcher and projection for 10 kinds of app × setting on/off. Every row came out as expected; that proof is not committed.
All runs were on macOS. The change has no platform-specific code.
I manually tested these changes (live run on a separate Mac; see Visual Proof)
Automated tests added/updated, or explained why not below
AI Disclosure
Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
terminal.create.createignores options sent by the app.Known follow-ups (deferred, not in this PR)
createSupportrule once the oldest supported phone launches throughagent.launch. A ticket is to be filed.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)