Commit Graph
51 Commits
Author SHA1 Message Date
e8310d5a4f fix(opencode): submit admitted native startup briefs without overwriting input (#24762)
* fix: wait for OpenCode worker composer before first dispatch

Reuse captured composer readiness on local and paired execution hosts and revoke launching-shell paste anchors.

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>

* feat(opencode): probe execution-host CLI capabilities

* fix(opencode): select plugin default for execution host loader

* fix(opencode): limit prompt prefill capability to verified release

* feat(opencode): probe launch capabilities on the execution host

* fix(opencode): select plugin loader for the launched host binary

* fix(opencode): match WSL probe cwd and declared guest environment

* fix(opencode): preserve launch environment deletion boundaries

* wip(opencode): authorize native startup prompt intent at execution owner

* fix(opencode): atomically replace status plugin entrypoints

* fix(opencode): retain plugin permissions across restrictive umasks

* test(opencode): resolve permission fixture from primary cwd

* feat(opencode): install startup prompt plugin independently of status hooks

* fix(opencode): wait for admitted startup intent and preserve failed-launch briefs

* fix(opencode): unsubscribe hook settings during async host shutdown

* STRICT launch CI contract correction

* CAPS launch CI contract correction

* INTENT launch CI contract correction

* test: initialize Claude prompt state in output retention fixture

* Wait for OpenCode location hydration in intent startup

* Bind OpenCode startup readiness to the current location in intent startup

* Retry interrupted OpenCode startup prompt claims

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>
Co-authored-by: Orca startup hydration review <agents@stably.ai>
Co-authored-by: Orca <dev@stably.ai>
2026-10-04 01:41:01 -07:00
+34 c2dba12072 Add host-owned OpenCode and Devin managed account profiles (#24636)
* Add host-owned OpenCode and Devin account profiles

* Manage OpenCode and Devin profiles in account Settings

* Expose registered account roots to host transcript readers

* Clarify managed profile provider flags

* Retain isolated Electron home in browser sidecars

* Restore inherited account environment and preserve cleanup retries

* Check relay environment values before merging

* Consolidate managed account type imports

* fix(accounts): use existing localized provider names

Align the new Japanese account copy with the existing catalog repair policy.

* Keep managed account baselines private to the execution host

* Align account enrollment help with accepted providers and flags

* Show the active System account in managed profile lists

* Document the validated Linux managed account scope

* Fix managed account removal, credential audits, and runtime bundling

* Quarantine removed accounts and audit captured credential snapshots

* Continue Antigravity IDE and 2.0 history in new CLI conversations (#24692)

* feat(antigravity): bridge IDE history into new CLI conversations

* fix(antigravity): preserve fresh-launch model and environment for IDE references

* fix(antigravity): forward IDE history opt-in through desktop IPC

* fix(antigravity): rebuild remote IDE reference startup on its host

* fix(antigravity): register IDE continuation action labels

* fix(antigravity): confine IDE references and bound metadata reads

* fix(antigravity): localize IDE continuation badges

* Preserve scanner service cache assertions and refresh Antigravity opening metadata

* Preserve Antigravity opening joins and target folder runtime authority

* fix(opencode): retry timed-out SSH plugin updates (#24666)

Preserve bounded retry behavior and the current-main status-envelope fields.

Original-PR: #24124
Reviewed-source: 103144f9c5

Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>

* fix(opencode): keep Go credentials private and resolve backend keys (#24615)

Preserve the complete credential storage, migration, IPC, Settings and rate-limit refresh change alongside standalone GLM plans, current-main database diagnostics and the reviewed unknown-backend environment correction. Keep native discovery cancellation third and selected environment fourth.

Original-topic-commit: 7903f1cddb
Original-topic-commit: 588117b5cf
Original-topic-commit: dedd4f8c86
Original-topic-commit: 6645dae104
Original-topic-commit: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-from: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-onto: b032867021

Co-authored-by: kespineira <kespineira@users.noreply.github.com>
Co-authored-by: kevimux <kevimux@users.noreply.github.com>
Reported-by: pullfrog
Reviewed-full-source: 849fe093073f4c1606bd65d79a0c725d955d0d1f
Native-helper-source: 80dbe23237


Reviewed-full-current-source: 36acb57d44adb3d378c0289c8c15f7da0fda214c

* Skip store notifications when refreshed work items are unchanged (#24530)

An identical forced refresh previously returned an empty Zustand patch, creating a new root state and notifying every subscriber. It now returns the current state after renewing the existing freshness timestamp. Real row or metadata changes still publish once.

* Read project activity once while sorting the sidebar (#24549)

Reuse the existing pure recent rank once per actual entry tuple within each synchronous sort; discard the lookup after sorting.

* Skip impossible link and tag matches in docs search results (#24646)

* Skip impossible HTML matches when formatting docs search results

After the existing link-removal phase, skip the unchanged HTML regex only when the current string has no closing delimiter.

* Skip impossible link and tag matches in docs search results

Skip the five unchanged link regexes when their protected-code input lacks ]; skip the unchanged HTML regex when its post-link input lacks >.

* Decode complete transcript lines without copying their bytes (#24546)

Decode each single owned Buffer synchronously; preserve concatenation for multipart records and remove a redundant tail array copy.

* Clear unused speech worker timers after errors and exit (#24924)

Call the existing idle timer cleanup when private current-worker state is cleared; keep current-worker guards, transcript/error order, deliberate warmth and stop deadline unchanged.

* Reuse documentation search excerpts during result navigation (#24574)

Memoize the existing pure excerpt renderer by complete raw text for the current result array, retaining original rendering as a miss fallback.

* Append subagent handoffs without recopying their message list (#24672)

Append each message reference to its private call-local scope list; retain the final merge and existing ordering.

* Cancel plugin retry timers when their fetch is replaced (#24700)

Reuse the existing timer clear block immediately after the fetch generation advances; preserve live retries and all state publications.

* Avoid rebuilding visible file paths for single-row selection (#24743)

Skip the visible path array only for keyboard replacement selection; the existing replacement branch never reads that order.

* Reuse prepared reads while searching saved agent conversations (#24535)

Use schema-complete session columns to enable the existing bounded statement cache without caching result rows.

* Reuse discovered mobile chunks during static traversal (#24774)

Iterate the existing live visited Set in FIFO discovery order instead of maintaining a second shifted queue; both actual callers own untouched esbuild JSON metadata.

* Skip unused image-size calculations in mobile web browser requests (#24941)

Use the existing mobile density budget only for mobile view; pass the existing constant to the existing assembler for web/default mode, where that argument is discarded. No cache, policy, request or native path changes.

* Skip diff analysis after a result has been discarded (#24711)

Move the unchanged pure render-limit calculation after both existing generation and section-token rejection fences.

* Skip late browser grab toasts after their surface closes (#24727)

Reuse the existing mounted-owner ref at the toast presentation entry while preserving all admitted extraction/screenshot and clipboard completion.

* Classify native chat waiting messages once (#24771)

Use the existing native filter traversal to populate a fresh waiting array, keeping the original predicate, policy, projection and output ordering while classifying each actual private slot once.

* Skip discarded Kanban pruning for an empty selection (#24782)

Guard only the existing open pruning branch when both its private selected Set and nullable anchor are empty; retain all original pre-guard projections and nonempty/anchor-only/public-helper behavior.

* Index discovered test files once during shard selection validation (#24532)

Verified selection plans previously validated each selected filename with a scan of all discovered files. One per-call Set now handles membership checks. Exact source/discovery checks, nonempty selection, fallback to all tests, shard balancing and manifests remain intact.

* Clear the watchdog benchmark heartbeat when CPU sampling fails (#24802)

Move the existing initial CPU observation and histogram enable inside the existing try/finally so their failures clear the sampler's owned heartbeat, preserving successful operation order and original errors.

* Reuse the parsed notification when leasing a push delivery (#24645)

* Reuse the parsed notification when leasing a push delivery

Pass the notification already parsed for the dismissal check into the existing private delivery builder.

* Reuse the parsed notification when leasing a push delivery

Pass the notification already parsed for the dismissal check into the existing private delivery builder.

* fix(accounts): canonicalize account removal to rm

Use account rm as the canonical removal command and keep account remove as an alias. Preserve the existing accounts.removeData RPC and the full original 63-path account component.

Original-Account-Source: cf71ae4cb6
Frozen-Account-Base: 08ee7ba9ef
Frozen-Integrated-Main: 53f9ea7839
Private validation source only; no ref or publication.

* Update README downloads badge

* Let retired-cache GC observations settle across the existing six-turn budget (#24967)

* Collect test-selection evidence when full unit tests fail (#24955)

* Collect advisory unit-selection evidence from failed full runs

* Trigger checks after retargeting the evidence fix to main

* Check each project repository once while filtering mobile cards (#24540)

Reuse exact raw source/slug matching decisions within one project filter call, preserving the matcher, membership, negative matches, output order and identity.

* Skip renderer callbacks after their request has ended (#24631)

Reuse the existing pending-request identity check before starting its deferred renderer callback.

* Decode single-piece saved-session tails without copying them (#24632)

Decode the existing owned Buffer directly when the EOF tail has one piece; preserve the existing concatenation for multiple pieces.

* Avoid irrelevant scroll-action searches in Mac snapshots (#24731)

Check the current pure action name before searching for vertical scroll actions in the same immutable array.

* Stop copying every retained browser page before attachment (#24553)

Find the first page/generation match directly in the existing insertion-ordered Map instead of copying all page references into an array.

* Reuse event byte sizes when relay watchers notify several clients (#24558)

Store the existing grouped numeric byte-size array on the already call-local watcher batch sizing object, for reuse by later chunking clients.

* Stop building unused import candidates in the test planner (#24611)

Replace the existing ordered extension map/find with a loop that forms each candidate only when its existing lookup is reached.

* docs(terminal): explain local macOS/Linux shell startup files

Document the actual login/interactive shell startup-file order and existing shell setup behavior.

Co-authored-by: brynnclaw <261708852+brynnclaw@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>

* fix(editor): recognize Ruby task and configuration files

Add Ruby task/configuration filenames to the existing generated language associations.

Co-authored-by: ggbdpq <ggbdpq@gmail.com>
Co-authored-by: Neil <neil@stably.ai>

* Speed up large Markdown Find and render oversized tables (#24948)

* Speed up large Markdown Find and render oversized tables

* Poll table preview geometry outside hidden renderers

* Preserve Markdown navigation through refresh and tab restoration

* Confirm Markdown restoration when refreshed content is ready

* Trace table refresh positions and update Unicode search reference

* Recognize queued measurement scrolls before restoring Markdown anchors

* Rebuild Markdown Find ranges after renderer components change

* fix(source-control): generate clean OpenCode messages locally and over SSH (#24613)

* fix(source-control): generate clean OpenCode answers on local and SSH hosts

Restack the original focused change onto current main, preserving every owned source and test blob and the merged CI contract and journal cleanup fixes.

Original-commit: 64bb15e3f2
fix(source-control): generate clean OpenCode answers on local and SSH hosts

Use configured models and JSON answer/error events, preserve run-first arguments, and handle the precise v2 variant rejection. Hydrate SSH execution-host PATH through the existing bounded login environment resolver before direct spawning.

Credits: andy-murr (PR #5197 SSH environment intent) and coelho-doti (PR #13065 argument-order intent).

Original-commit: 1b60ec5d11
fix(source-control): retry inline OpenCode model and variant options

Original-commit: 98fdecc7a3
Preserve OpenCode named errors without a data message

Restacked-from: 98fdecc7a3
Restacked-onto: f7b1f9d8be

* fix(ci): prevent concurrent pnpm refresh during mobile typechecks (#24776)

* fix(ci): run mobile typechecks without concurrent dependency refresh

* test(ci): check effective Linux E2E package list

* test(ci): preserve the mobile production compiler barrier

---------

Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>

* test(terminal): restore the live fish fixture prerequisites (#24947)

A restored pane waits for the initial status replay before subscribing to
PTY output. This fixture never settled that replay, so fish printed its
mode-2031 arm before the renderer connected. Its PTY API also omitted the
reset-input listener required by the serializer, aborting attachment.

Settle and dispose the existing startup-snapshot registration and provide
the same reset-listener mock used by the other PTY tests. The real fish
child-stdin assertions and timeouts remain unchanged. No production change.

* fix(shortcuts): defer TUI editing chords in terminal-first mode (#24640)

Restack the original focused change onto current main, preserving every owned source and test blob and the merged CI contract and journal cleanup fixes.

Original-commit: f707cde14a
fix(shortcuts): defer TUI editing chords in terminal-first mode

Original-commit: 0c6348e49e
docs(shortcuts): describe deferred preview terminal chords

Original-commit: be62c1b6c6
Align worktree history shortcut metadata with terminal conflict policy

Restacked-from: be62c1b6c6
Restacked-onto: f7b1f9d8be

* Register supervised Qoder China and Qwen Code (#24616)

* Add Qoder session history and search with real CLI coverage

* Allow the real Qoder marker file to end with a newline

* Keep Qoder tool output out of history previews and search

* Keep Qoder search pages readable by older clients

* Verify persisted Qoder history after a real generated and resumed task

* Negotiate Qoder filters before searching an older execution host

* Combine search client imports for the CI plugin gate

* Keep the relay search oracle aligned with legacy agent filtering

* Register supervised Qoder China and Qwen lifecycle integration

* Cover Qoder China mobile assets and mixed-host resume gates

* Verify Qoder provider tags against the older released wire parser

* Verify China and Qwen keep independent Windows hook scripts

* Verify Qoder registrations against the installed older Windows release

* test(qoder): align search capability contracts and pin old-host fencing

* fix(qoder): rank exact picker identities and command aliases first

* test(qoder): preserve the regional CLI shared icon expectation

Keep the full bundled-asset and no-remote-image checks, with an explicit
shared-logo basename for Qoder China. The map also works with older
catalog type unions.

* fix(qoder): align China catalog entry with fallback order

---------

Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>

* Use the measured pnpm lookup policy automatically in hosted root CI (#24951)

* Select lookup automatically for the measured hosted root-install profile

* Record hosted automatic-mode cold cache publication proof

* fix: bound remote generation setup and honor OpenCode option terminators

Count execution-host profile resolution inside the existing request deadline, cancel its waiter promptly, and pass only the remaining time to the child. Shared bounded profile probes keep their existing cache lifetime; the SSH transport margin is unchanged.

Read OpenCode output format from active final-argv options before -- so literal prompt arguments cannot select the JSON finalizer.

Fresh exact-source controls reproduce nine failures before; 139 related checks pass after, including primary/fallback delays, deadline boundaries, cancellation, parser metadata, and SSH lanes. Node typecheck and strict changed-file lint pass.

* Continue Antigravity IDE and 2.0 history in new CLI conversations (#24692)

* feat(antigravity): bridge IDE history into new CLI conversations

* fix(antigravity): preserve fresh-launch model and environment for IDE references

* fix(antigravity): forward IDE history opt-in through desktop IPC

* fix(antigravity): rebuild remote IDE reference startup on its host

* fix(antigravity): register IDE continuation action labels

* fix(antigravity): confine IDE references and bound metadata reads

* fix(antigravity): localize IDE continuation badges

* Preserve scanner service cache assertions and refresh Antigravity opening metadata

* Preserve Antigravity opening joins and target folder runtime authority

* fix(opencode): retry timed-out SSH plugin updates (#24666)

Preserve bounded retry behavior and the current-main status-envelope fields.

Original-PR: #24124
Reviewed-source: 103144f9c5

Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>

* fix(opencode): keep Go credentials private and resolve backend keys (#24615)

Preserve the complete credential storage, migration, IPC, Settings and rate-limit refresh change alongside standalone GLM plans, current-main database diagnostics and the reviewed unknown-backend environment correction. Keep native discovery cancellation third and selected environment fourth.

Original-topic-commit: 7903f1cddb
Original-topic-commit: 588117b5cf
Original-topic-commit: dedd4f8c86
Original-topic-commit: 6645dae104
Original-topic-commit: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-from: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-onto: b032867021

Co-authored-by: kespineira <kespineira@users.noreply.github.com>
Co-authored-by: kevimux <kevimux@users.noreply.github.com>
Reported-by: pullfrog
Reviewed-full-source: 849fe093073f4c1606bd65d79a0c725d955d0d1f
Native-helper-source: 80dbe23237


Reviewed-full-current-source: 36acb57d44adb3d378c0289c8c15f7da0fda214c

* fix(opencode): enforce deadline through executable startup

Keep synchronous Windows PATH resolution and child startup within the existing request budget.

---------

Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>
Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>

* fix(accounts): recover interrupted profile removal on startup

Scan private removal backups before quarantined cleanup, reuse the existing state schema to validate the exact UUID, and preserve registered or unmarked profiles. Mark account rm as destructive using the existing typo recovery policy. Retain the full original account component and public author ancestry.

Account-PR: 24636
Original-Account-Source: cf71ae4cb6
Reviewed-Public-Parent: 6abaf736e3
Frozen-Integrated-Main: 53f9ea7839
Private source only; no ref or publication.

* fix(persistence): preserve projectGroupOrder on restart for flat folder-scan groups

Retain saved project ranks when the project remains in its folder-scan group.

Co-authored-by: lurunzi <lurunzi@gmail.com>
Co-authored-by: Neil <neil@stably.ai>

* fix(runtime): reclaim temp files orphaned by interrupted mobile store writes

Reuse the existing stale temporary-file cleanup policy when each mobile store opens.

Co-authored-by: LDH1103 <ldh517525@gmail.com>
Co-authored-by: Neil <neil@stably.ai>

* fix(terminal): show actionable copy for missing folder workspace paths

Show the existing folder-path failure with concrete recovery instructions.

Co-authored-by: Wayn_Liu <wayntingliu@gmail.com>
Co-authored-by: Neil <neil@stably.ai>

* docs: reference local orca.yaml and .worktreeinclude

Document the existing workspace configuration, include/copy rules and sharing behavior.

Co-authored-by: Neil <neil@stably.ai>

* fix(orchestration): allow model selection for OMP workers

Pass an explicitly requested model through the existing OMP worker launch catalog.

Co-authored-by: Neil <neil@stably.ai>

* fix(cli): preserve primitive success results in JSON output

Check for an object before inspecting screenshot fields so primitive success results remain printable.

Related: https://github.com/stablyai/orca/pull/14735

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: VXNCXNX <VXNCXNX@users.noreply.github.com>

* Show loaded file changes before deleting a workspace

Expose up to ten already loaded changed paths without altering deletion counts, hydration or authorization.

Related: https://github.com/stablyai/orca/pull/22778

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Michiel de Gooijer <mdgooijer@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(keybindings): record and match Option+digit shortcuts on macOS

Use the physical digit key for explicitly assigned Mac Option+digit shortcuts while retaining modifier checks.

Co-authored-by: marcuslannister <marcus@lannister.cc>
Co-authored-by: Neil <neil@stably.ai>

* fix(editor): don't throw closing a stale active file

Recover stale active-file selection within the owning workspace before choosing a surviving editor.

Related: https://github.com/stablyai/orca/pull/24715

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: Neil <neil@stably.ai>

* Fix Project Settings targeting for multiple local checkouts

Carry the selected checkout identity into the existing project Settings action.

Co-authored-by: fsmeier <1506919+fsmeier@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>

* feat(documents): open CSV and TSV files from the OS

Extend existing OS document associations and delivery to CSV/TSV, preserving restoration and authorization.

Co-authored-by: Neil <neil@stably.ai>

* feat(cli): link GitHub and GitLab items on worktree create and set

Expose and validate the existing workspace link fields, preserving omitted values and provider identity checks.

Related: https://github.com/stablyai/orca/pull/22609

Co-authored-by: marco song <marco.song@mvlchain.io>
Co-authored-by: SongMarco <20613630+SongMarco@users.noreply.github.com>
Co-authored-by: Luca Critelli <lucacri@gmail.com>
Co-authored-by: Neil <neil@stably.ai>

* feat(sidebar): include folder workspaces in keyboard navigation

Use the rendered sidebar row order and host identity when cycling through folder and Git workspaces.

Related: https://github.com/stablyai/orca/pull/10555

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: JeongUk Park <jeongph.dev@gmail.com>

* fix(build): turn off MSBuild file tracking for Windows native rebuilds

Default Windows native rebuilds to TrackFileAccess=false while preserving explicit caller preferences.

Co-authored-by: B1nh M1nh <43268322+b1nhm1nh@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>

* Fix Ctrl+M in Linux and Windows terminals

Keep the Minimize menu item but disable its accelerator registration on Linux and Windows.

Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>

Related contribution: https://github.com/stablyai/orca/pull/24143

* fix(startup): read a nushell login PATH from $env.PATH

Use Nushell login command syntax and preserve the actual login PATH value.

Related: https://github.com/stablyai/orca/pull/22677

Co-authored-by: Kh05ifr4nD <meandSSH0219@gmail.com>
Co-authored-by: Neil <neil@stably.ai>

* fix(cursor): run local hooks through sh for non-POSIX login shells

Keep POSIX hook syntax inside one quoted sh command so the login shell can invoke it safely.

Related: https://github.com/stablyai/orca/pull/22663

Co-authored-by: Kh05ifr4nD <meandSSH0219@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Neil <neil@stably.ai>

* Fix word wrap for both panes in side-by-side diffs

Forward wrapping to both diff panes through the existing editor option path and clean up listeners.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Neil <neil@stably.ai>

* fix(monaco): highlight Svelte block closers inside markup

Return Svelte block closers to the existing markup tokenizer state.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Neil <neil@stably.ai>

* fix(repos): keep active clone dialog open on outside clicks

Ignore accidental outside dismissal only while cloning; Escape, Close and Back still cancel.

Related: https://github.com/stablyai/orca/pull/24581, https://github.com/stablyai/orca/pull/23430

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Nawapat Buakoet <nawapat.b@covest.finance>

* fix(editor): keep chat visible after closing Markdown tabs

Count remaining unified chat tabs before clearing workspace selection during editor close.

Related: https://github.com/stablyai/orca/pull/24273, https://github.com/stablyai/orca/pull/23760

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Honor typed starting numbers in chat ordered lists

Forward the existing parsed ordered-list start attribute to both chat Markdown renderers.

Related: https://github.com/stablyai/orca/pull/19765

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Frederic Barthelemy <git@fbartho.com>

* Normalize base source once while checking changed-code diagnostics (#24557)

Reuse the exact existing moved-code matching algorithm with run-local lazy normalization of immutable base source blocks across diagnostics.

* Support real OpenCode sessions in native Chat (#24647)

* Use bounded OpenCode context for vault session continuation

OpenCode database and synthetic row paths are not text transcripts. Use the
vault preview or captured pane context, preserving actual transcript paths
containing a hash and supporting both OpenCode lanes and Windows paths.

Adapted the intent of #11859 and extended it to actual installed v2 vault rows.

Co-authored-by: mrcha033 <mrcha033@users.noreply.github.com>

* Read real OpenCode sessions in terminal-backed native Chat

Reuse the bounded AI Vault SQLite worker for v1 and v2 session pages and live updates. Keep terminal input as the real execution path and pace OpenCode Stop through its two-Escape interrupt.

Co-authored-by: xodmd45-ctrl <xodmd45-ctrl@users.noreply.github.com>

* fix(opencode): publish approval cards for permission requests

* Send OpenCode native approval through its Enter selector

* Resolve mobile Chat readability for folder workspaces

* Bound OpenCode part batches and preserve v2 image attachments

* Prefer live migrated OpenCode sessions over legacy copies

* Consolidate mobile Chat eligibility test imports

* Consolidate OpenCode SQLite protocol type imports

* Update native chat settings contract for both OpenCode agents

* fix(native-chat): reconcile bounded OpenCode transcript reads

* fix(native-chat): dispatch OpenCode questions safely

* fix(native-chat): keep native discovery and transcript windows current

* feat(accounts): link standalone GLM Coding Plans (#24618)

* feat(accounts): link standalone GLM Coding Plans

Adapt the reviewed GLM accounts contribution to current main, retain Antigravity behavior, guard late credential results, expose storage protection, and redact quota errors.

Co-authored-by: Luchong <lu740528977@gmail.com>

* fix(accounts): retain GLM credential results during quota refresh

* fix(accounts): make GLM credential editing desktop-only

* fix(accounts): mirror the host GLM site in paired clients

* fix(accounts): report unknown GLM host details and split web settings tests

Apply the independently reviewed Accounts correction from697284a without the v2 adapter commits. Preserve the saved-key store and serialized write behavior.

* fix(zcode): ship required GLM account translation entries

* chore: record GLM reconciliation hook validation

* chore: validate installed GLM commit hooks

* test: complete GLM account fixtures and web API inventory

---------

Co-authored-by: Luchong <lu740528977@gmail.com>

* fix(native-chat): route transcript requests through shared SQLite worker

* fix(ci): prevent concurrent pnpm refresh during mobile typechecks (#24776)

* fix(ci): run mobile typechecks without concurrent dependency refresh

* test(ci): check effective Linux E2E package list

* test(ci): preserve the mobile production compiler barrier

---------

Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>

* test(terminal): restore the live fish fixture prerequisites (#24947)

A restored pane waits for the initial status replay before subscribing to
PTY output. This fixture never settled that replay, so fish printed its
mode-2031 arm before the renderer connected. Its PTY API also omitted the
reset-input listener required by the serializer, aborting attachment.

Settle and dispose the existing startup-snapshot registration and provide
the same reset-listener mock used by the other PTY tests. The real fish
child-stdin assertions and timeouts remain unchanged. No production change.

* fix(shortcuts): defer TUI editing chords in terminal-first mode (#24640)

Restack the original focused change onto current main, preserving every owned source and test blob and the merged CI contract and journal cleanup fixes.

Original-commit: f707cde14a
fix(shortcuts): defer TUI editing chords in terminal-first mode

Original-commit: 0c6348e49e
docs(shortcuts): describe deferred preview terminal chords

Original-commit: be62c1b6c6
Align worktree history shortcut metadata with terminal conflict policy

Restacked-from: be62c1b6c6
Restacked-onto: f7b1f9d8be

* Register supervised Qoder China and Qwen Code (#24616)

* Add Qoder session history and search with real CLI coverage

* Allow the real Qoder marker file to end with a newline

* Keep Qoder tool output out of history previews and search

* Keep Qoder search pages readable by older clients

* Verify persisted Qoder history after a real generated and resumed task

* Negotiate Qoder filters before searching an older execution host

* Combine search client imports for the CI plugin gate

* Keep the relay search oracle aligned with legacy agent filtering

* Register supervised Qoder China and Qwen lifecycle integration

* Cover Qoder China mobile assets and mixed-host resume gates

* Verify Qoder provider tags against the older released wire parser

* Verify China and Qwen keep independent Windows hook scripts

* Verify Qoder registrations against the installed older Windows release

* test(qoder): align search capability contracts and pin old-host fencing

* fix(qoder): rank exact picker identities and command aliases first

* test(qoder): preserve the regional CLI shared icon expectation

Keep the full bundled-asset and no-remote-image checks, with an explicit
shared-logo basename for Qoder China. The map also works with older
catalog type unions.

* fix(qoder): align China catalog entry with fallback order

---------

Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>

* Use the measured pnpm lookup policy automatically in hosted root CI (#24951)

* Select lookup automatically for the measured hosted root-install profile

* Record hosted automatic-mode cold cache publication proof

* fix(native-chat): keep OpenCode history usable at read limits

Continue past failed database probes while preserving discovery cancellation.
Verify rows displaced by a capped tail before deciding whether to replace history.
Represent oversized v1/v2 rows with the existing omission text and stable cursors.

* Continue Antigravity IDE and 2.0 history in new CLI conversations (#24692)

* feat(antigravity): bridge IDE history into new CLI conversations

* fix(antigravity): preserve fresh-launch model and environment for IDE references

* fix(antigravity): forward IDE history opt-in through desktop IPC

* fix(antigravity): rebuild remote IDE reference startup on its host

* fix(antigravity): register IDE continuation action labels

* fix(antigravity): confine IDE references and bound metadata reads

* fix(antigravity): localize IDE continuation badges

* Preserve scanner service cache assertions and refresh Antigravity opening metadata

* Preserve Antigravity opening joins and target folder runtime authority

* fix(opencode): retry timed-out SSH plugin updates (#24666)

Preserve bounded retry behavior and the current-main status-envelope fields.

Original-PR: #24124
Reviewed-source: 103144f9c5

Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>

* fix(opencode): keep Go credentials private and resolve backend keys (#24615)

Preserve the complete credential storage, migration, IPC, Settings and rate-limit refresh change alongside standalone GLM plans, current-main database diagnostics and the reviewed unknown-backend environment correction. Keep native discovery cancellation third and selected environment fourth.

Original-topic-commit: 7903f1cddb
Original-topic-commit: 588117b5cf
Original-topic-commit: dedd4f8c86
Original-topic-commit: 6645dae104
Original-topic-commit: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-from: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-onto: b032867021

Co-authored-by: kespineira <kespineira@users.noreply.github.com>
Co-authored-by: kevimux <kevimux@users.noreply.github.com>
Reported-by: pullfrog
Reviewed-full-source: 849fe093073f4c1606bd65d79a0c725d955d0d1f
Native-helper-source: 80dbe23237


Reviewed-full-current-source: 36acb57d44adb3d378c0289c8c15f7da0fda214c

---------

Co-authored-by: mrcha033 <mrcha033@users.noreply.github.com>
Co-authored-by: xodmd45-ctrl <xodmd45-ctrl@users.noreply.github.com>
Co-authored-by: Luchong <lu740528977@gmail.com>
Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>
Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>

* fix(accounts): protect registered UUID case variants during removal recovery

Compare validated account UUID identities without changing stored IDs or filesystem paths. Protect registered originals and quarantines, including explicit retry variants, and accept equivalent UUID spelling in a valid removal backup. Retain the complete account component and published contributor ancestry.

Private source only; no index, ref, or public mutation.

* Drain removal fixture jobs before resetting and deleting their records (#24977)

* Reuse measured Electron preparation for current Terminal Perf refs (#24968)

* feat(jcode): add Jcode as a supported TUI agent with managed hooks

Ports PR #10521 onto current main: agent catalog, managed hook service,
agent-status listener, session resume, AI Vault parser, per-pane daemon
isolation, and Source Control AI support.

Co-authored-by: Neil <neil@stably.ai>

* feat(jcode): evidence-backed status pipeline (turn_start, live pre_tool, questions)

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): register vault fixture, dynamic model discovery, script refresher

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): keep finished-turn detail so completion notifications fire

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): show the prompt in agent rows instead of jcode's repainting title

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): pre-warm the per-pane daemon so a cold runtime dir cannot time out

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): stop the OSC color skip from crashing every pane connect

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* refactor(jcode): fold three reverse-scan copies into one, reuse the shared hook POST

The jcode journal reader, the Claude transcript reader and the Command Code
transcript reader each carried their own copy of the same reverse chunked line
scan; they now share one tested helper. jcode's managed hook script drops its
hand-rolled curl for buildPosixAgentHookPostCommand, which also gains it the
raw-JSON transport and the --noproxy guard the bespoke copy was missing.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): narrow dynamic reads with predicates, pin the Windows hook shape

CI's anti-slop audit rejects Reflect.get: parse dynamic input into a named type
instead. Adds Windows script-shape tests too, since Windows is the platform this
change could not be exercised on directly.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* test(jcode): measure the gate against the synchronous path, not the clock

The absolute 1s bound was the flake CI shard 3/8 hit: it is tight enough to catch
a synchronous gate on an idle laptop and too tight on a loaded runner. Measuring
the same POST both ways on the same machine makes the claim a ratio, which is what
the test is actually about. Mutation-checked: a synchronous gate reads 6120ms
against a 1517ms observer.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* test(jcode): drop the wall-clock gate assertion

The bound was machine-speed sensitive and flaked on CI shard 3/8 at 1525ms. The
structural assertions (detached gate branch, foreground observer branch) and the
no-hang stdin drain cover the same contract without a timer.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): address CodeRabbit review on #22539

- Windows posted no payload at all: the shared builder reads `payload@-` from
  stdin, which the gate has already drained and observer hooks never receive, so
  every Windows event was dropped. Write the env var to a temp file and pipe it.
- The daemon pre-warm never fired for daemon-host spawns, which is the default
  local path; it now runs there too, and from the final env so the daemon gets the
  hook port and token.
- A failed runtime-dir mkdir took down every local terminal, jcode or not.
- removeJcodeManagedHooks matched the raw line, so a user hook whose comment
  mentioned the managed script was deleted; matching on Windows never worked.
- A managed entry left by a copied home or a platform switch is now repointed
  instead of being reported as user-owned forever.
- The OSC colour skip only checked launchAgent, so a command- or telemetry-named
  jcode pane still leaked the reply into its composer.
- `['hooks']` and a commented scalar are recognised, instead of appending a
  second [hooks] table that makes jcode reject the whole config.
- A failed tool's error is marked as tool output rather than agent prose.
- The vault keeps a session's stored name, counts its tokens, and skips
  background_task and [Scheduled task] turns.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): read the journal once per turn, key prompts by byte offset

The jcode prompt reader ran a synchronous bounded file scan plus a JSON
parse on every hook event. jcode blocks on pre_tool, so a turn that ran
four tools charged the user eight scans of latency it did not need — the
prompt cannot change inside a turn.

- Cache the journal read per pane, refreshed on the turn boundary that
  can change it. The cache holds the whole evidence record, since
  hasExplicitUserPrompt needs the transcript-evidence flag and not just
  the text, and it joins the existing pane-scoped lifecycle (close,
  rename, reset) rather than living in a module singleton.
- Key a journal prompt by its absolute byte offset instead of its
  region-local line index. The backward scan windows the file from EOF,
  so appending shifted every boundary and reminted the key for a prompt
  that never moved; a repeated turn_end then slipped past the same-hash
  dedupe as a second done event with duplicate telemetry.
- Pin the platform in the daemon pre-warm tests. The pre-warm is a no-op
  off POSIX, so the dedupe and retry cases would have passed vacuously
  on a Windows runner.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): quote the managed hook path, drop tools from patch prompts

Three fixes, all on paths this PR could not exercise locally.

The managed hook command was stored as a bare path. jcode tokenizes that
string shell-style before exec'ing it directly (parse_hook_command,
crates/jcode-terminal-launch/src/lib.rs): unquoted whitespace splits, and
every unquoted backslash is consumed as an escape. So on Windows
`C:\Users\me\.orca\agent-hooks\jcode-hook.cmd` reached exec as
`C:Usersme.orcaagent-hooksjcode-hook.cmd` and no hook fired at all, and a
POSIX home with a space split into two arguments. Store the path
single-quoted (verbatim, backslashes included), falling back to double
quotes for a path containing a single quote. Existing bare entries are
already repointed by the stale-key path, and getStatus accepts both forms
so the repair is not reported as a user-owned hook. The quoting helper was
previously dead code that only tests called; the three production sites
now use it. isJcodeManagedCommand also normalizes separators, since a
`/`-only needle never matched a Windows entry.

Commit-message generation feeds a staged patch to `jcode run` as the
prompt — attacker-influenced text — while jcode's default profile exposes
shell, read, write, and MCP. Pass `--tool-profile none`, which resolves to
an empty allowed-tool set in jcode's config (base_allowed_tools), matching
the read-only posture claude (plan) and codex (read-only) already take.

docs/reference/jcode-hook-events.md was never actually in this PR: the
repo ignores docs/** and tracks reference docs by allow-list only, so the
captured-payload evidence four source comments point at was silently
dropped. Allow-list it.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* test(jcode): pin the tool-profile flag in the generation plan too

The argv assertion lives in two places; --tool-profile none only landed in
one, so the plan test still expected the unrestricted argv.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(text-generation): refuse an oversized argv prompt on Linux too

The pre-spawn size guard only ran on Windows. Linux caps a single argv
entry at MAX_ARG_STRLEN (32 pages, 128 KiB on a 4-KiB-page host) and
execve fails with E2BIG past it, so an agent that delivers the whole
prompt as one argument — jcode, and the other argv-delivery agents —
failed on a large staged diff with an error the user could not act on.

The cap is per-argument and in bytes, which is why it is not the Windows
line budget: 40k chars trips Windows and is nowhere near the Linux limit,
so folding them together would have refused prompts Linux runs fine.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): register in main's remote-installer guard, drop our duplicate

Rebasing onto 853 commits of main surfaced two things the earlier branch
had hidden.

main already owns a guard for the issue-#7253 bug class
(`remote-hook-service-registry-coverage.test.ts`). This branch had added a
second, near-identical one — a parallel implementation of a test that
already existed, which is what AGENTS.md's reuse rule is about. Deleted
ours and registered jcode in main's, which is the one that has kept pace
with every agent added since.

Also fixes a missing separator in the mobile icon map. `pnpm tc` does not
cover `mobile/`, so only the session-route closure suite caught it.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): decompose the four files jcode pushed over max-lines

Adding an agent tipped four modules past their line budget. AGENTS.md
forbids a `max-lines` disable or a per-file bump, so each is split on a
real seam rather than silenced:

- agent-catalog.tsx keeps `AgentIcon`, which 70+ files import, and the
  rows move out. The rows alone exceed the 300-line budget a `.ts` file
  gets, so they follow the primary/secondary split this repo already uses
  for commit-message agent specs.
- getAgentResumeArgv -> agent-resume-argv.ts, re-exported so the 18 call
  sites keep one import path.
- isDiscoverableSessionFile/pathSegments -> session-file-discovery.ts.
- remoteCodexSources -> remote-session-scanner-codex-sources.ts; Codex is
  the one remote agent with two CODEX_HOME roots.

Also:
- Records the readiness-census baseline jcode now needs. main added that
  gate while this branch was out; the fixture is the recorded 74-case
  matrix, not a hand-written one.
- Restores two entries a rebase resolution silently dropped from
  config/tsconfig.cli.json (gitlab/project-ref-parser,
  startup/shell-path-probe). Nothing to do with jcode; losing them was a
  conflict-resolution mistake on this branch.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* feat(native-chat): open structured chats on the paired Orca server that owns the workspace (#24205)

* fix(native-chat): a host admits structured sessions by client capability, 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).

* fix(native-chat): the desktop declares structured chat support to paired hosts

The desktop renderer advertised agent-session.structured.v1 (and the Claude, turn-item and
background-task capabilities that go with it) to its own main process but not to a paired Orca
server. The server therefore refused every agentSession.* call from the desktop and stripped
structured chat tabs out of the tab list it published to it, so a structured chat running on a
paired server never appeared on the desktop, even though the renderer already mirrors a host's
agent-session tabs and drives each one against the server that owns its workspace.

The same renderer reads structured chats on either host, so the remote Electron list now carries
the same structured-session capabilities as the local one, and the capability test pins that
nothing is advertised only locally.

* feat(native-chat): open structured chats on the paired server that owns the workspace

With the structured-chat default on, an agent launched in a workspace that lives on a paired Orca
server always opened as a terminal (or the terminal-backed chat view). Three things kept it off
the structured path: the launch check refused every host but this machine, a remote workspace was
handed to the host-published terminal path before the structured route was even considered, and
the structured launch pipeline sent create and every follow-up call to this machine's runtime.

A workspace's owning runtime is fixed, so the pipeline now derives it from the workspace instead
of assuming this machine (structured-agent-session-owner.ts, the same derivation the chat pane
already uses to read a session). The launch intent carries that target; the pre-create support
check, create, the publication check and fence read, the launch prompt send, held option picks,
the "focus this chat" marker, the placeholder tab's host, and tab close/purge all use it.

The launch check now accepts a paired server and asks that server's own capabilities (read from
the status the client already cached for it) rather than this machine's. An SSH workspace stays
terminal-backed: no Orca runtime runs there. The host still answers createSupport before
anything is created, so an older server that refuses shows the failure in the chat tab.

A chat the user closed before its create landed is now also retired on the paired server when it
publishes, as the local sync already does. Orchestration workers placed on another runtime are
unchanged: federation creates terminal agents only.

* fix(native-chat): negotiate client-chosen launch mode so released phones 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.

* refactor(runtime): keep the Electron client capability list in its own module

protocol-version.ts is at its line budget; the list is what the desktop
advertises to paired hosts, not the host's own contract.

* fix(native-chat): the desktop declares it picks each launch mode itself

Paired hosts and the desktop's own main process then answer createSupport
by the workspace rather than by their own chat setting.

* fix(native-chat): pin each structured chat to the host it was launched on

- Route: a paired server opens a chat only when it advertises the
  client-chosen launch mode; an older server keeps its terminal. Its
  capabilities come from the store's host status, not the compatibility
  cache that is empty after boot or reconnect.
- A launch command override is this machine's: the route applies it only
  locally, and a host's createSupport refuses on its own override.
- The owning host is resolved once, from the same value the route used,
  and carried on the launch intent, its persisted record (legacy records
  load as local), the provisional tab and every mirrored chat tab. Close,
  purge, retry and reload read it instead of re-deriving it from a
  worktree id two hosts can share; an owner that cannot be named refuses.
- Cancellation tombstones record their host: only that host's
  authoritative inventory retires one, restored cleanup closes it there,
  and a paired host's tombstone expires after 30 days if it never answers.
- A paired server's frame settles launches it published, as the local
  inventory already does for this machine.

* fix(native-chat): a paired server that declines a chat opens its terminal instead

createSupport only reads, so both of its non-answers are settled before
anything is created:
- A paired server that answers it cannot run the chat (a WSL repo, a
  Claude account mismatch, its own launch command override) closes the
  chat tab and opens the terminal the route would have chosen, with a
  notice saying why. This machine's own decline stays a failed chat.
- A host that could not be asked closes the chat tab and leaves one
  failure toast, instead of a lingering "could not confirm" chat.

* test(native-chat): a provisional chat carries its launch's host and hands pre-create failures on

* chore(native-chat): justify the two type assertions this change's lines touch

* test(native-chat): state why each staged test fixture is cast

* fix(native-chat): chats that already exist keep showing whatever the 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.

* fix(native-chat): the browser client keeps its host terminal on paired servers

A browser client whose own preferences turn structured chat on took the
structured route for every paired-server workspace, but its handshake
never says it reads structured sessions, so the server refused the chat
and the user got a failed chat tab where a host terminal used to open.
The route for a paired host now also asks what this client advertises to
it: the desktop's list does, the browser client's does not. Its handshake
list is now a named constant the route reads, so the two cannot drift.

The chat setting's copy now says it runs on paired Orca servers too;
WSL and SSH hosts still use terminal chat.

* fix(native-chat): a retried launch a paired server declines opens its terminal too

A launch restored after a reload settles only through its Retry, so a
declining paired server left a failed chat there while a first launch got
the server's terminal and a notice. The chat's Retry now hands the same
pre-create failures to the same replacement, carrying the prompt the
launch had staged.

* refactor(native-chat): a paired host's cancelled-chat record ends on its 30-day TTL

The paired census re-read a host's whole inventory after every
authoritative frame to retire tombstones, and a tombstone restored after
a reload needed a second such frame, so in practice it retired nothing.
A tombstone guards a random session id and is inert once stale; the chat
is already closed on its host whenever a frame shows it. The census, its
trigger in the mirror layer and its cleanup are removed; the owner-scoped
tombstones, close-on-sight, the TTL and publication marking from frames
stay.

* fix(native-chat): a chat's pane and status read from the host recorded on its tab

The chat pane and its sidebar status still derived the host from the
workspace id, which two hosts can share; a paired chat in a non-active
same-id workspace was read from this machine. Both now read the owner
stamped on the tab, as close, purge and publication already do.

* fix(native-chat): "Resume in chat" follows the terminal resume's host rule

Agent Session History offered "Resume in chat" for a conversation
recorded on this machine into a paired server's workspace, where its
transcript does not exist. A chat now resumes a conversation only on the
host that recorded it, as the terminal resume does, and that host is the
one asked whether it can resume history.

* fix(native-chat): the chat setting says older paired servers keep terminal chat

* test(native-chat): pin that a host advertises the client-chosen launch mode

* fix(native-chat): mirror this machine's chats only where it holds them

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.

* test(native-chat): route a paired-server launch over the capability lists both sides really advertise

* test(native-chat): record install listeners without a cast

* fix(native-chat): a paired server admits a chat before any of it exists here

The desktop opened a paired server's chat tab, launch record, queued
prompt and focus intent before asking the server, so a "no" needed a
replacement that undid and redid all of it, and every piece it missed
was a bug: the workspace deselected, the caller told "failed" while a
terminal ran its prompt, the caller's arguments and other queued prompts
lost, and a create whose reply was lost treated as never sent.

A paired launch now asks the server first and commits nothing until it
answers. Admitted opens the chat as before. Declined runs the caller's
own launch as the server's terminal, with the existing notice (a resume
fails instead, having no terminal equivalent). Unreachable opens nothing
and names the server in one toast. The new-tab launcher reports the
host's surface for paired workspaces, as it did before paired chats,
with the prompt delivery of whichever surface got the prompt. The
replacement and its error classes are gone, and the probe inside a
launch is back to its old meaning: a "no" is a failed chat with Retry,
and no answer leaves "Could not confirm" with Retry and the prompt kept,
here as on this machine.

* fix(native-chat): mirror this machine's chats only once it holds one, 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.

* fix(native-chat): a fork's reveal never seeds a terminal beside the surface the launcher opens

Forking into a paired-server workspace revealed it as if nothing would open there, so the reveal
created a blank host terminal beside the forked chat (and beside a forked agent terminal on main).
The launcher always opens the fork's surface itself, so the reveal now says so for every surface,
as the fix-checks launch already does.

* fix(native-chat): a declined direct launch keeps the caller's CLI args; an unreachable resume toasts once

When a paired server declines a "Fix checks" chat in a new workspace, the terminal that opens
instead now carries the recipe's saved CLI arguments, launch platform and launch source, as the
terminal route did. "Resume in chat" to a server that cannot be reached showed the admission's
"Could not reach" toast and the vault's generic one; the admission marks its failure notified and
the vault adds nothing.

* fix(native-chat): a declined background create opens its terminal without switching workspaces

Since #23974 a worktree create the user moved away from must not pull them onto the new
workspace. When a paired server declined that create's chat, the fallback terminal opened as a new
agent tab, whose host create selects the workspace. The create now opens its own agent terminal the
way main's background branch does: in place from the request's startup plan (so its CLI args carry),
without selecting the workspace. A create the user is still watching keeps the new-tab fallback.

* test(native-chat): name the launch's host in main's new outbox fence test

Main's new staging-failure test calls settleStructuredAgentLaunchPrompt without the target this PR
made required; it is a local launch, as in the sibling tests.

* fix(native-chat): a paired server's new chat shows no model until the server reports the one it started

A chat on a paired server starts with the server's saved model and options, but the picker showed
this desktop's saved selection (or the catalog default) until the server reported a model, and a
pick made in that window was remembered on the server under that guessed model. A paired launch
now carries no desktop seed, and until the server reports its model the picker names no model and
takes no picks. Local chats are unchanged.

* test(native-chat): seed the paired repo without a cast

The repo literal already satisfies Repo, so the changed-lines cast gate has nothing to excuse.

* feat(native-chat): createSupport reports the saved selection a new chat 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.

* fix(native-chat): a paired server's new chat shows the selection the server will start it with

The paired server now names its saved model and options in the admission answer the desktop
already waits for. That seed goes into the launch intent and its persisted record, so the picker
shows the server's model at once, stays pickable like a local chat, and remembers picks on the
server under that model; a reload shows the same. The locked picker remains only for a server too
old to name a seed.

Also moves host admission and launch-outcome tracking into their own modules: the latest main
merge left structured-agent-session-launch.ts over the max-lines limit.

* test(native-chat): expect the launch intent's new seed argument in exact-call assertions

* refactor(protocol): move the Electron remote client capability list into 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.

* fix(native-chat): a paired chat with no saved server model is pickable; Retry shows the server's current seed

A server whose user never saved a chat model sends no seed, and the desktop showed a locked,
model-only picker for it, although that is the common case: no server that can admit a paired chat
predates the seed field. Such a chat now behaves like a local chat with no saved model: the CLI
default, pickable. The lock and its snapshot helper are gone.

Retry kept the first admission's seed while the create probe, which already runs on every attempt,
reported the server's current one and dropped it. The probe's seed now replaces a paired launch's
seed and the picker's, so a retried chat shows what its create will run.

* test(cross-version): stub the launch seed resolver createSupport now reads

* test(protocol): pin the desktop capability divergence against what a paired server receives

Every paired transport sends the shared remote base plus the Electron list, so the
divergence test now compares that union with the renderer's local list instead of
the declared Electron list. A capability added only to the shared base can no
longer slip past it. The two base-only capabilities it surfaced are recorded:
skills.install-result.v2 has no local caller; the authoritative-inventory label is
read by the local tabs sync but dropped by main, and is marked unsettled.

The turn-item and both background-task-stop capabilities were already sent through
the shared base, so the Electron list no longer repeats them. The wire set is
unchanged; this PR's real change on the wire is structured.v1, the Claude
structured capability and the client launch-mode capability.

* fix(native-chat): the desktop tells its own host it picks each launch mode, so retrying an existing chat works with the setting off

* docs(native-chat): name the real exit for the released-phone createSupport rule

* fix(native-chat): the route reads the capabilities a paired host actually receives

The renderer decided whether a paired host would admit a chat from the desktop's Electron list, but
every desktop transport sends that list plus the shared remote base. They agreed only because the
route's checks happened to sit in both. The route input is now built with the same
remoteRuntimeClientCapabilities the transports use (the browser client already sends its list as is),
and a test pins each against the real handshake.

* test(cross-version): a released client still gets the host-setting createSupport answer; a launch-mode client gets supported plus the seed

* test(native-chat): let main's child-records test resolve each chat's owner

Main's new test mocks worktree-runtime-owner with only the runtime environment id, but the status
projection in this PR also resolves each structured chat's owner from the worktree. The mock keeps
the module's real exports and overrides only what the test pins.

* fix(deps): take #24204's lockfile that the merge reverted

* test(native-chat): let the Codex child-approval e2e unit test resolve each chat's owner

Its worktree-runtime-owner mock exported only the runtime environment id, but this PR's status
projection also resolves each structured chat's owner from the worktree. The mock now keeps the
module's real exports and overrides only that id, as structured-child-records-switch does.

* test(native-chat): move the close-race launch cases into their own file

Merging main added launch tests on both sides and took structured-agent-session-launch.test.ts past
the 800-line limit. The three cases where a tab close races a launch move to
structured-agent-session-launch-close-race.test.ts, with the same setup the other split launch
suites copy.

* feat(cli): add --unread/--read to orca worktree set

Pass validated read/unread flags through the existing workspace metadata update.

Contributor attribution correction: the earlier Ctrl+M squash message placed its related link after the co-author footer, so GitHub did not recognize all contributors. Preserve that credit here for https://github.com/stablyai/orca/pull/24100 and its related contribution https://github.com/stablyai/orca/pull/24143 .

Related CLI link validation retained from https://github.com/stablyai/orca/pull/20036 .

Co-authored-by: Raz Shlomo <12373339+razshlomo@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>

* feat(cli): configure external worktree visibility per repo

Pass show/hide/inherit to the existing repository visibility update.

Related: https://github.com/stablyai/orca/pull/23025

Preserves CLI link validation from https://github.com/stablyai/orca/pull/20036 and read controls from https://github.com/stablyai/orca/pull/22714 .

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: KAPUIST <thsxornjs12@gmail.com>

* perf(git): reuse queries and stop canceled catalog scans (#24923)

* perf(git): reuse queries and stop canceled catalog scans

* refactor(git): check relay filesystem error codes

* Allow cold node-pty setup on Windows ARM CI

Keep a ten-minute bound only for node-pty rebuilt on Windows ARM CI hosts; all other node-gyp calls retain five minutes. Cold setup in two completed ARM jobs left compilation less than a minute.

* test(e2e): require exact Linux package tokens (#24995)

* Allow cold node-pty setup on Windows ARM CI (#24997)

Keep a ten-minute bound only for node-pty rebuilt on Windows ARM CI hosts; all other node-gyp calls retain five minutes. Cold setup in two completed ARM jobs left compilation less than a minute.

* fix(jcode): harden Windows hooks and negotiate remote history (#24998)

Redirect the managed Windows payload file into curl instead of starting
pipeline shells, register native Windows delivery coverage, and document
Jcode v0.89.0+ as the upstream launcher requirement for invisible hooks.

Negotiate Jcode history in both directions with mixed-version Orca hosts,
preserving supported search filters and old-client response compatibility.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>
Co-authored-by: JianJia2018 <39438074+JianJia2018@users.noreply.github.com>

* fix(git): preserve SSH review context and worktree ownership (#24945)

* fix(git): preserve SSH arguments and guard background writes

* test(terminal): settle fish fixture startup readiness

* fix(git): preserve bare UNC SSH paths

* fix(git): unescape shell operators in Windows SSH paths

* test(runtime): settle removal writes before fixture cleanup

* test(git): skip optional OpenSSH probe when unavailable

* perf(git): skip equal-tip reads and bound relay discovery

* test(processes): ratchet the removed relay Git spawn

* test(shells): wait for initial zsh output before sending input

* Fix SSH review context and recover Git maintenance cleanup safely

* Keep relay Git compatibility fixtures outside shared client projects

* Fence superseded maintenance and preserve mixed-version session search

* test: model Git child termination and search catalogs

* test: retain catalog authority over history flags

* fix(opencode): keep shared-session TUI activity with its owning pane (#24962)

* fix(opencode): bind legacy attach status to its TUI pane

Adapt the official v1 TUI API to the existing pane reporter and learn structural session identity only after execution-host admission. Preserve known creators, gate new reports to capable hosts, and register the legacy TUI entry without changing user settings or overlay targets.

Credit werlang for the same-directory investigation in #22838 and #22847; no source from that PR is copied.

(cherry picked from commit d9eeb028a23a66b78b08538a5ebf6052499ec24f)

* fix(opencode): fence legacy TUI viewers before creator attribution

Use the execution host's existing admission policy on the physical TUI
pane and launch token before rewriting to a session creator or mutating
the launch-token cache. Preserve live viewers, frozen server stamps and
OpenCode 2 behavior.

Add HTTP regressions for retired panes, closed tabs, replaced tokens and
relay retirement, plus current-launch, alias and compatibility controls.

Follow-up to #22838 and @werlang's original #22847; preserves the original
intentional creator attribution without copying that PR's source.

* fix(opencode): preserve current plugin exports in legacy TUI wrapper

Keep the current server, setup, id and other default export entries when
adding the legacy TUI entry. Cover their preservation and seed the ACL
fixtures with the separate installed TUI/config bytes.

This completes the current-main port of the legacy identity replacement
for issue #22838, carrying @werlang's original PR #22847 contribution.

* fix(opencode): retry timed-out SSH plugin updates (#24666)

Preserve bounded retry behavior and the current-main status-envelope fields.

Original-PR: #24124
Reviewed-source: 103144f9c5

Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>

* fix(opencode): keep Go credentials private and resolve backend keys (#24615)

Preserve the complete credential storage, migration, IPC, Settings and rate-limit refresh change alongside standalone GLM plans, current-main database diagnostics and the reviewed unknown-backend environment correction. Keep native discovery cancellation third and selected environment fourth.

Original-topic-commit: 7903f1cddb
Original-topic-commit: 588117b5cf
Original-topic-commit: dedd4f8c86
Original-topic-commit: 6645dae104
Original-topic-commit: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-from: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-onto: b032867021

Co-authored-by: kespineira <kespineira@users.noreply.github.com>
Co-authored-by: kevimux <kevimux@users.noreply.github.com>
Reported-by: pullfrog
Reviewed-full-source: 849fe093073f4c1606bd65d79a0c725d955d0d1f
Native-helper-source: 80dbe23237


Reviewed-full-current-source: 36acb57d44adb3d378c0289c8c15f7da0fda214c

* Skip store notifications when refreshed work items are unchanged (#24530)

An identical forced refresh previously returned an empty Zustand patch, creating a new root state and notifying every subscriber. It now returns the current state after renewing the existing freshness timestamp. Real row or metadata changes still publish once.

* Read project activity once while sorting the sidebar (#24549)

Reuse the existing pure recent rank once per actual entry tuple within each synchronous sort; discard the lookup after sorting.

* Skip impossible link and tag matches in docs search results (#24646)

* Skip impossible HTML matches when formatting docs search results

After the existing link-removal phase, skip the unchanged HTML regex only when the current string has no closing delimiter.

* Skip impossible link and tag matches in docs search results

Skip the five unchanged link regexes when their protected-code input lacks ]; skip the unchanged HTML regex when its post-link input lacks >.

* Decode complete transcript lines without copying their bytes (#24546)

Decode each single owned Buffer synchronously; preserve concatenation for multipart records and remove a redundant tail array copy.

* Clear unused speech worker timers after errors and exit (#24924)

Call the existing idle timer cleanup when private current-worker state is cleared; keep current-worker guards, transcript/error order, deliberate warmth and stop deadline unchanged.

* Reuse documentation search excerpts during result navigation (#24574)

Memoize the existing pure excerpt renderer by complete raw text for the current result array, retaining original rendering as a miss fallback.

* Append subagent handoffs without recopying their message list (#24672)

Append each message reference to its private call-local scope list; retain the final merge and existing ordering.

* Cancel plugin retry timers when their fetch is replaced (#24700)

Reuse the existing timer clear block immediately after the fetch generation advances; preserve live retries and all state publications.

* Avoid rebuilding visible file paths for single-row selection (#24743)

Skip the visible path array only for keyboard replacement selection; the existing replacement branch never reads that order.

* Reuse prepared reads while searching saved agent conversations (#24535)

Use schema-complete session columns to enable the existing bounded statement cache without caching result rows.

* Reuse discovered mobile chunks during static traversal (#24774)

Iterate the existing live visited Set in FIFO discovery order instead of maintaining a second shifted queue; both actual callers own untouched esbuild JSON metadata.

* Skip unused image-size calculations in mobile web browser requests (#24941)

Use the existing mobile density budget only for mobile view; pass the existing constant to the existing assembler for web/default mode, where that argument is discarded. No cache, policy, request or native path changes.

* Skip diff analysis after a result has been discarded (#24711)

Move the unchanged pure render-limit calculation after both existing generation and section-token rejection fences.

* Skip late browser grab toasts after their surface closes (#24727)

Reuse the existing mounted-owner ref at the toast presentation entry while preserving all admitted extraction/screenshot and clipboard completion.

* Classify native chat waiting messages once (#24771)

Use the existing native filter traversal to populate a fresh waiting array, keeping the original predicate, policy, projection and output ordering while classifying each actual private slot once.

* Skip discarded Kanban pruning for an empty selection (#24782)

Guard only the existing open pruning branch when both its private selected Set and nullable anchor are empty; retain all original pre-guard projections and nonempty/anchor-only/public-helper behavior.

* Index discovered test files once during shard selection validation (#24532)

Verified selection plans previously validated each selected filename with a scan of all discovered files. One per-call Set now handles membership checks. Exact source/discovery checks, nonempty selection, fallback to all tests, shard balancing and manifests remain intact.

* Clear the watchdog benchmark heartbeat when CPU sampling fails (#24802)

Move the existing initial CPU observation and histogram enable inside the existing try/finally so their failures clear the sampler's owned heartbeat, preserving successful operation order and original errors.

* Reuse the parsed notification when leasing a push delivery (#24645)

* Reuse the parsed notification when leasing a push delivery

Pass the notification already parsed for the dismissal check into the existing private delivery builder.

* Reuse the parsed notification when leasing a push delivery

Pass the notification already parsed for the dismissal check into the existing private delivery builder.

* Update README downloads badge

* Let retired-cache GC observations settle across the existing six-turn budget (#24967)

* Collect test-selection evidence when full unit tests fail (#24955)

* Collect advisory unit-selection evidence from failed full runs

* Trigger checks after retargeting the evidence fix to main

* Check each project repository once while filtering mobile cards (#24540)

Reuse exact raw source/slug matching decisions within one project filter call, preserving the matcher, membership, negative matches, output order and identity.

* Skip renderer callbacks after their request has ended (#24631)

Reuse the existing pending-request identity check before starting its deferred renderer callback.

* Decode single-piece saved-session tails without copying them (#24632)

Decode the existing owned Buffer directly when the EOF tail has one piece; preserve the existing concatenation for multiple pieces.

* Avoid irrelevant scroll-action searches in Mac snapshots (#24731)

Check the current pure action name before searching for vertical scroll actions in the same immutable array.

* Stop copying every retained browser page before attachment (#24553)

Find the first page/generation match directly in the existing insertion-ordered Map instead of copying all page references into an array.

* Reuse event byte sizes when relay watchers notify several clients (#24558)

Store the existing grouped numeric byte-size array on the already call-local watcher batch sizing object, for reuse by later chunking clients.

* Stop building unused import candidates in the test planner (#24611)

Replace the existing ordered extension map/find with a loop that forms each candidate only when its existing lookup is reached.

* docs(terminal): explain local macOS/Linux shell startup files

Document the actual login/interactive shell startup-file order and existing shell setup behavior.

Co-authored-by: brynnclaw <261708852+brynnclaw@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>

* fix(editor): recognize Ruby task and configuration files

Add Ruby task/configuration filenames to the existing generated language associations.

Co-authored-by: ggbdpq <ggbdpq@gmail.com>
Co-authored-by: Neil <neil@stably.ai>

* Speed up large Markdown Find and render oversized tables (#24948)

* Speed up large Markdown Find and render oversized tables

* Poll table preview geometry outside hidden renderers

* Preserve Markdown navigation through refresh and tab restoration

* Confirm Markdown restoration when refreshed content is ready

* Trace table refresh positions and update Unicode search reference

* Recognize queued measurement scrolls before restoring Markdown anchors

* Rebuild Markdown Find ranges after renderer components change

* fix(source-control): generate clean OpenCode messages locally and over SSH (#24613)

* fix(source-control): generate clean OpenCode answers on local and SSH hosts

Restack the original focused change onto current main, preserving every owned source and test blob and the merged CI contract and journal cleanup fixes.

Original-commit: 64bb15e3f2
fix(source-control): generate clean OpenCode answers on local and SSH hosts

Use configured models and JSON answer/error events, preserve run-first arguments, and handle the precise v2 variant rejection. Hydrate SSH execution-host PATH through the existing bounded login environment resolver before direct spawning.

Credits: andy-murr (PR #5197 SSH environment intent) and coelho-doti (PR #13065 argument-order intent).

Original-commit: 1b60ec5d11
fix(source-control): retry inline OpenCode model and variant options

Original-commit: 98fdecc7a3
Preserve OpenCode named errors without a data message

Restacked-from: 98fdecc7a3
Restacked-onto: f7b1f9d8be

* fix(ci): prevent concurrent pnpm refresh during mobile typechecks (#24776)

* fix(ci): run mobile typechecks without concurrent dependency refresh

* test(ci): check effective Linux E2E package list

* test(ci): preserve the mobile production compiler barrier

---------

Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>

* test(terminal): restore the live fish fixture prerequisites (#24947)

A restored pane waits for the initial status replay before subscribing to
PTY output. This fixture never settled that replay, so fish printed its
mode-2031 arm before the renderer connected. Its PTY API also omitted the
reset-input listener required by the serializer, aborting attachment.

Settle and dispose the existing startup-snapshot registration and provide
the same reset-listener mock used by the other PTY tests. The real fish
child-stdin assertions and timeouts remain unchanged. No production change.

* fix(shortcuts): defer TUI editing chords in terminal-first mode (#24640)

Restack the original focused change onto current main, preserving every owned source and test blob and the merged CI contract and journal cleanup fixes.

Original-commit: f707cde14a
fix(shortcuts): defer TUI editing chords in terminal-first mode

Original-commit: 0c6348e49e
docs(shortcuts): describe deferred preview terminal chords

Original-commit: be62c1b6c6
Align worktree history shortcut metadata with terminal conflict policy

Restacked-from: be62c1b6c6
Restacked-onto: f7b1f9d8be

* Register supervised Qoder China and Qwen Code (#24616)

* Add Qoder session history and search with real CLI coverage

* Allow the real Qoder marker file to end with a newline

* Keep Qoder tool output out of history previews and search

* Keep Qoder search pages readable by older clients

* Verify persisted Qoder history after a real generated and resumed task

* Negotiate Qoder filters before searching an older execution host

* Combine search client imports for the CI plugin gate

* Keep the relay search oracle aligned with legacy agent filtering

* Register supervised Qoder China and Qwen lifecycle integration

* Cover Qoder China mobile assets and mixed-host resume gates

* Verify Qoder provider tags against the older released wire parser

* Verify China and Qwen keep independent Windows hook scripts

* Verify Qoder registrations against the installed older Windows release

* test(qoder): align search capability contracts and pin old-host fencing

* fix(qoder): rank exact picker identities and command aliases first

* test(qoder): preserve the regional CLI shared icon expectation

Keep the full bundled-asset and no-remote-image checks, with an explicit
shared-logo basename for Qoder China. The map also works with older
catalog type unions.

* fix(qoder): align China catalog entry with fallback order

---------

Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>

* Use the measured pnpm lookup policy automatically in hosted root CI (#24951)

* Select lookup automatically for the measured hosted root-install profile

* Record hosted automatic-mode cold cache publication proof

* fix: bound remote generation setup and honor OpenCode option terminators

Count execution-host profile resolution inside the existing request deadline, cancel its waiter promptly, and pass only the remaining time to the child. Shared bounded profile probes keep their existing cache lifetime; the SSH transport margin is unchanged.

Read OpenCode output format from active final-argv options before -- so literal prompt arguments cannot select the JSON finalizer.

Fresh exact-source controls reproduce nine failures before; 139 related checks pass after, including primary/fallback delays, deadline boundaries, cancellation, parser metadata, and SSH lanes. Node typecheck and strict changed-file lint pass.

* Continue Antigravity IDE and 2.0 history in new CLI conversations (#24692)

* feat(antigravity): bridge IDE history into new CLI conversations

* fix(antigravity): preserve fresh-launch model and environment for IDE references

* fix(antigravity): forward IDE history opt-in through desktop IPC

* fix(antigravity): rebuild remote IDE reference startup on its host

* fix(antigravity): register IDE continuation action labels

* fix(antigravity): confine IDE references and bound metadata reads

* fix(antigravity): localize IDE continuation badges

* Preserve scanner service cache assertions and refresh Antigravity opening metadata

* Preserve Antigravity opening joins and target folder runtime authority

* fix(opencode): retry timed-out SSH plugin updates (#24666)

Preserve bounded retry behavior and the current-main status-envelope fields.

Original-PR: #24124
Reviewed-source: 103144f9c5

Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>

* fix(opencode): keep Go credentials private and resolve backend keys (#24615)

Preserve the complete credential storage, migration, IPC, Settings and rate-limit refresh change alongside standalone GLM plans, current-main database diagnostics and the reviewed unknown-backend environment correction. Keep native discovery cancellation third and selected environment fourth.

Original-topic-commit: 7903f1cddb
Original-topic-commit: 588117b5cf
Original-topic-commit: dedd4f8c86
Original-topic-commit: 6645dae104
Original-topic-commit: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-from: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-onto: b032867021

Co-authored-by: kespineira <kespineira@users.noreply.github.com>
Co-authored-by: kevimux <kevimux@users.noreply.github.com>
Reported-by: pullfrog
Reviewed-full-source: 849fe093073f4c1606bd65d79a0c725d955d0d1f
Native-helper-source: 80dbe23237


Reviewed-full-current-source: 36acb57d44adb3d378c0289c8c15f7da0fda214c

* fix(opencode): enforce deadline through executable startup

Keep synchronous Windows PATH resolution and child startup within the existing request budget.

---------

Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>
Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>

* fix(persistence): preserve projectGroupOrder on restart for flat folder-scan groups

Retain saved project ranks when the project remains in its folder-scan group.

Co-authored-by: lurunzi <lurunzi@gmail.com>
Co-authored-by: Neil <neil@stably.ai>

* fix(runtime): reclaim temp files orphaned by interrupted mobile store writes

Reuse the existing stale temporary-file cleanup policy when each mobile store opens.

Co-authored-by: LDH1103 <ldh517525@gmail.com>
Co-authored-by: Neil <neil@stably.ai>

* fix(terminal): show actionable copy for missing folder workspace paths

Show the existing folder-path failure with concrete recovery instructions.

Co-authored-by: Wayn_Liu <wayntingliu@gmail.com>
Co-authored-by: Neil <neil@stably.ai>

* docs: reference local orca.yaml and .worktreeinclude

Document the existing workspace configuration, include/copy rules and sharing behavior.

Co-authored-by: Neil <neil@stably.ai>

* fix(orchestration): allow model selection for OMP workers

Pass an explicitly requested model through the existing OMP worker launch catalog.

Co-authored-by: Neil <neil@stably.ai>

* fix(cli): preserve primitive success results in JSON output

Check for an object before inspecting screenshot fields so primitive success results remain printable.

Related: https://github.com/stablyai/orca/pull/14735

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: VXNCXNX <VXNCXNX@users.noreply.github.com>

* Show loaded file changes before deleting a workspace

Expose up to ten already loaded changed paths without altering deletion counts, hydration or authorization.

Related: https://github.com/stablyai/orca/pull/22778

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Michiel de Gooijer <mdgooijer@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(keybindings): record and match Option+digit shortcuts on macOS

Use the physical digit key for explicitly assigned Mac Option+digit shortcuts while retaining modifier checks.

Co-authored-by: marcuslannister <marcus@lannister.cc>
Co-authored-by: Neil <neil@stably.ai>

* fix(editor): don't throw closing a stale active file

Recover stale active-file selection within the owning workspace before choosing a surviving editor.

Related: https://github.com/stablyai/orca/pull/24715

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: Neil <neil@stably.ai>

* Fix Project Settings targeting for multiple local checkouts

Carry the selected checkout identity into the existing project Settings action.

Co-authored-by: fsmeier <1506919+fsmeier@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>

* feat(documents): open CSV and TSV files from the OS

Extend existing OS document associations and delivery to CSV/TSV, preserving restoration and authorization.

Co-authored-by: Neil <neil@stably.ai>

* feat(cli): link GitHub and GitLab items on worktree create and set

Expose and validate the existing workspace link fields, preserving omitted values and provider identity checks.

Related: https://github.com/stablyai/orca/pull/22609

Co-authored-by: marco song <marco.song@mvlchain.io>
Co-authored-by: SongMarco <20613630+SongMarco@users.noreply.github.com>
Co-authored-by: Luca Critelli <lucacri@gmail.com>
Co-authored-by: Neil <neil@stably.ai>

* feat(sidebar): include folder workspaces in keyboard navigation

Use the rendered sidebar row order and host identity when cycling through folder and Git workspaces.

Related: https://github.com/stablyai/orca/pull/10555

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: JeongUk Park <jeongph.dev@gmail.com>

* fix(build): turn off MSBuild file tracking for Windows native rebuilds

Default Windows native rebuilds to TrackFileAccess=false while preserving explicit caller preferences.

Co-authored-by: B1nh M1nh <43268322+b1nhm1nh@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>

* Fix Ctrl+M in Linux and Windows terminals

Keep the Minimize menu item but disable its accelerator registration on Linux and Windows.

Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>

Related contribution: https://github.com/stablyai/orca/pull/24143

* fix(startup): read a nushell login PATH from $env.PATH

Use Nushell login command syntax and preserve the actual login PATH value.

Related: https://github.com/stablyai/orca/pull/22677

Co-authored-by: Kh05ifr4nD <meandSSH0219@gmail.com>
Co-authored-by: Neil <neil@stably.ai>

* fix(cursor): run local hooks through sh for non-POSIX login shells

Keep POSIX hook syntax inside one quoted sh command so the login shell can invoke it safely.

Related: https://github.com/stablyai/orca/pull/22663

Co-authored-by: Kh05ifr4nD <meandSSH0219@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Neil <neil@stably.ai>

* Fix word wrap for both panes in side-by-side diffs

Forward wrapping to both diff panes through the existing editor option path and clean up listeners.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Neil <neil@stably.ai>

* fix(monaco): highlight Svelte block closers inside markup

Return Svelte block closers to the existing markup tokenizer state.

Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Neil <neil@stably.ai>

* fix(repos): keep active clone dialog open on outside clicks

Ignore accidental outside dismissal only while cloning; Escape, Close and Back still cancel.

Related: https://github.com/stablyai/orca/pull/24581, https://github.com/stablyai/orca/pull/23430

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Nawapat Buakoet <nawapat.b@covest.finance>

* fix(editor): keep chat visible after closing Markdown tabs

Count remaining unified chat tabs before clearing workspace selection during editor close.

Related: https://github.com/stablyai/orca/pull/24273, https://github.com/stablyai/orca/pull/23760

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Honor typed starting numbers in chat ordered lists

Forward the existing parsed ordered-list start attribute to both chat Markdown renderers.

Related: https://github.com/stablyai/orca/pull/19765

Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Frederic Barthelemy <git@fbartho.com>

* Normalize base source once while checking changed-code diagnostics (#24557)

Reuse the exact existing moved-code matching algorithm with run-local lazy normalization of immutable base source blocks across diagnostics.

* Support real OpenCode sessions in native Chat (#24647)

* Use bounded OpenCode context for vault session continuation

OpenCode database and synthetic row paths are not text transcripts. Use the
vault preview or captured pane context, preserving actual transcript paths
containing a hash and supporting both OpenCode lanes and Windows paths.

Adapted the intent of #11859 and extended it to actual installed v2 vault rows.

Co-authored-by: mrcha033 <mrcha033@users.noreply.github.com>

* Read real OpenCode sessions in terminal-backed native Chat

Reuse the bounded AI Vault SQLite worker for v1 and v2 session pages and live updates. Keep terminal input as the real execution path and pace OpenCode Stop through its two-Escape interrupt.

Co-authored-by: xodmd45-ctrl <xodmd45-ctrl@users.noreply.github.com>

* fix(opencode): publish approval cards for permission requests

* Send OpenCode native approval through its Enter selector

* Resolve mobile Chat readability for folder workspaces

* Bound OpenCode part batches and preserve v2 image attachments

* Prefer live migrated OpenCode sessions over legacy copies

* Consolidate mobile Chat eligibility test imports

* Consolidate OpenCode SQLite protocol type imports

* Update native chat settings contract for both OpenCode agents

* fix(native-chat): reconcile bounded OpenCode transcript reads

* fix(native-chat): dispatch OpenCode questions safely

* fix(native-chat): keep native discovery and transcript windows current

* feat(accounts): link standalone GLM Coding Plans (#24618)

* feat(accounts): link standalone GLM Coding Plans

Adapt the reviewed GLM accounts contribution to current main, retain Antigravity behavior, guard late credential results, expose storage protection, and redact quota errors.

Co-authored-by: Luchong <lu740528977@gmail.com>

* fix(accounts): retain GLM credential results during quota refresh

* fix(accounts): make GLM credential editing desktop-only

* fix(accounts): mirror the host GLM site in paired clients

* fix(accounts): report unknown GLM host details and split web settings tests

Apply the independently reviewed Accounts correction from697284a without the v2 adapter commits. Preserve the saved-key store and serialized write behavior.

* fix(zcode): ship required GLM account translation entries

* chore: record GLM reconciliation hook validation

* chore: validate installed GLM commit hooks

* test: complete GLM account fixtures and web API inventory

---------

Co-authored-by: Luchong <lu740528977@gmail.com>

* fix(native-chat): route transcript requests through shared SQLite worker

* fix(ci): prevent concurrent pnpm refresh during mobile typechecks (#24776)

* fix(ci): run mobile typechecks without concurrent dependency refresh

* test(ci): check effective Linux E2E package list

* test(ci): preserve the mobile production compiler barrier

---------

Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>

* test(terminal): restore the live fish fixture prerequisites (#24947)

A restored pane waits for the initial status replay before subscribing to
PTY output. This fixture never settled that replay, so fish printed its
mode-2031 arm before the renderer connected. Its PTY API also omitted the
reset-input listener required by the serializer, aborting attachment.

Settle and dispose the existing startup-snapshot registration and provide
the same reset-listener mock used by the other PTY tests. The real fish
child-stdin assertions and timeouts remain unchanged. No production change.

* fix(shortcuts): defer TUI editing chords in terminal-first mode (#24640)

Restack the original focused change onto current main, preserving every owned source and test blob and the merged CI contract and journal cleanup fixes.

Original-commit: f707cde14a
fix(shortcuts): defer TUI editing chords in terminal-first mode

Original-commit: 0c6348e49e
docs(shortcuts): describe deferred preview terminal chords

Original-commit: be62c1b6c6
Align worktree history shortcut metadata with terminal conflict policy

Restacked-from: be62c1b6c6
Restacked-onto: f7b1f9d8be

* Register supervised Qoder China and Qwen Code (#24616)

* Add Qoder session history and search with real CLI coverage

* Allow the real Qoder marker file to end with a newline

* Keep Qoder tool output out of history previews and search

* Keep Qoder search pages readable by older clients

* Verify persisted Qoder history after a real generated and resumed task

* Negotiate Qoder filters before searching an older execution host

* Combine search client imports for the CI plugin gate

* Keep the relay search oracle aligned with legacy agent filtering

* Register supervised Qoder China and Qwen lifecycle integration

* Cover Qoder China mobile assets and mixed-host resume gates

* Verify Qoder provider tags against the older released wire parser

* Verify China and Qwen keep independent Windows hook scripts

* Verify Qoder registrations against the installed older Windows release

* test(qoder): align search capability contracts and pin old-host fencing

* fix(qoder): rank exact picker identities and command aliases first

* test(qoder): preserve the regional CLI shared icon expectation

Keep the full bundled-asset and no-remote-image checks, with an explicit
shared-logo basename for Qoder China. The map also works with older
catalog type unions.

* fix(qoder): align China catalog entry with fallback order

---------

Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>

* Use the measured pnpm lookup policy automatically in hosted root CI (#24951)

* Select lookup automatically for the measured hosted root-install profile

* Record hosted automatic-mode cold cache publication proof

* fix(native-chat): keep OpenCode history usable at read limits

Continue past failed database probes while preserving discovery cancellation.
Verify rows displaced by a capped tail before deciding whether to replace history.
Represent oversized v1/v2 rows with the existing omission text and stable cursors.

* Continue Antigravity IDE and 2.0 history in new CLI conversations (#24692)

* feat(antigravity): bridge IDE history into new CLI conversations

* fix(antigravity): preserve fresh-launch model and environment for IDE references

* fix(antigravity): forward IDE history opt-in through desktop IPC

* fix(antigravity): rebuild remote IDE reference startup on its host

* fix(antigravity): register IDE continuation action labels

* fix(antigravity): confine IDE references and bound metadata reads

* fix(antigravity): localize IDE continuation badges

* Preserve scanner service cache assertions and refresh Antigravity opening metadata

* Preserve Antigravity opening joins and target folder runtime authority

* fix(opencode): retry timed-out SSH plugin updates (#24666)

Preserve bounded retry behavior and the current-main status-envelope fields.

Original-PR: #24124
Reviewed-source: 103144f9c5

Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>

* fix(opencode): keep Go credentials private and resolve backend keys (#24615)

Preserve the complete credential storage, migration, IPC, Settings and rate-limit refresh change alongside standalone GLM plans, current-main database diagnostics and the reviewed unknown-backend environment correction. Keep native discovery cancellation third and selected environment fourth.

Original-topic-commit: 7903f1cddb
Original-topic-commit: 588117b5cf
Original-topic-commit: dedd4f8c86
Original-topic-commit: 6645dae104
Original-topic-commit: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-from: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-onto: b032867021

Co-authored-by: kespineira <kespineira@users.noreply.github.com>
Co-authored-by: kevimux <kevimux@users.noreply.github.com>
Reported-by: pullfrog
Reviewed-full-source: 849fe093073f4c1606bd65d79a0c725d955d0d1f
Native-helper-source: 80dbe23237


Reviewed-full-current-source: 36acb57d44adb3d378c0289c8c15f7da0fda214c

---------

Co-authored-by: mrcha033 <mrcha033@users.noreply.github.com>
Co-authored-by: xodmd45-ctrl <xodmd45-ctrl@users.noreply.github.com>
Co-authored-by: Luchong <lu740528977@gmail.com>
Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>
Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>

* Drain removal fixture jobs before resetting and deleting their records (#24977)

* Reuse measured Electron preparation for current Terminal Perf refs (#24968)

* feat(jcode): add Jcode as a supported TUI agent with managed hooks

Ports PR #10521 onto current main: agent catalog, managed hook service,
agent-status listener, session resume, AI Vault parser, per-pane daemon
isolation, and Source Control AI support.

Co-authored-by: Neil <neil@stably.ai>

* feat(jcode): evidence-backed status pipeline (turn_start, live pre_tool, questions)

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): register vault fixture, dynamic model discovery, script refresher

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): keep finished-turn detail so completion notifications fire

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): show the prompt in agent rows instead of jcode's repainting title

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): pre-warm the per-pane daemon so a cold runtime dir cannot time out

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): stop the OSC color skip from crashing every pane connect

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* refactor(jcode): fold three reverse-scan copies into one, reuse the shared hook POST

The jcode journal reader, the Claude transcript reader and the Command Code
transcript reader each carried their own copy of the same reverse chunked line
scan; they now share one tested helper. jcode's managed hook script drops its
hand-rolled curl for buildPosixAgentHookPostCommand, which also gains it the
raw-JSON transport and the --noproxy guard the bespoke copy was missing.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): narrow dynamic reads with predicates, pin the Windows hook shape

CI's anti-slop audit rejects Reflect.get: parse dynamic input into a named type
instead. Adds Windows script-shape tests too, since Windows is the platform this
change could not be exercised on directly.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* test(jcode): measure the gate against the synchronous path, not the clock

The absolute 1s bound was the flake CI shard 3/8 hit: it is tight enough to catch
a synchronous gate on an idle laptop and too tight on a loaded runner. Measuring
the same POST both ways on the same machine makes the claim a ratio, which is what
the test is actually about. Mutation-checked: a synchronous gate reads 6120ms
against a 1517ms observer.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* test(jcode): drop the wall-clock gate assertion

The bound was machine-speed sensitive and flaked on CI shard 3/8 at 1525ms. The
structural assertions (detached gate branch, foreground observer branch) and the
no-hang stdin drain cover the same contract without a timer.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): address CodeRabbit review on #22539

- Windows posted no payload at all: the shared builder reads `payload@-` from
  stdin, which the gate has already drained and observer hooks never receive, so
  every Windows event was dropped. Write the env var to a temp file and pipe it.
- The daemon pre-warm never fired for daemon-host spawns, which is the default
  local path; it now runs there too, and from the final env so the daemon gets the
  hook port and token.
- A failed runtime-dir mkdir took down every local terminal, jcode or not.
- removeJcodeManagedHooks matched the raw line, so a user hook whose comment
  mentioned the managed script was deleted; matching on Windows never worked.
- A managed entry left by a copied home or a platform switch is now repointed
  instead of being reported as user-owned forever.
- The OSC colour skip only checked launchAgent, so a command- or telemetry-named
  jcode pane still leaked the reply into its composer.
- `['hooks']` and a commented scalar are recognised, instead of appending a
  second [hooks] table that makes jcode reject the whole config.
- A failed tool's error is marked as tool output rather than agent prose.
- The vault keeps a session's stored name, counts its tokens, and skips
  background_task and [Scheduled task] turns.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): read the journal once per turn, key prompts by byte offset

The jcode prompt reader ran a synchronous bounded file scan plus a JSON
parse on every hook event. jcode blocks on pre_tool, so a turn that ran
four tools charged the user eight scans of latency it did not need — the
prompt cannot change inside a turn.

- Cache the journal read per pane, refreshed on the turn boundary that
  can change it. The cache holds the whole evidence record, since
  hasExplicitUserPrompt needs the transcript-evidence flag and not just
  the text, and it joins the existing pane-scoped lifecycle (close,
  rename, reset) rather than living in a module singleton.
- Key a journal prompt by its absolute byte offset instead of its
  region-local line index. The backward scan windows the file from EOF,
  so appending shifted every boundary and reminted the key for a prompt
  that never moved; a repeated turn_end then slipped past the same-hash
  dedupe as a second done event with duplicate telemetry.
- Pin the platform in the daemon pre-warm tests. The pre-warm is a no-op
  off POSIX, so the dedupe and retry cases would have passed vacuously
  on a Windows runner.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): quote the managed hook path, drop tools from patch prompts

Three fixes, all on paths this PR could not exercise locally.

The managed hook command was stored as a bare path. jcode tokenizes that
string shell-style before exec'ing it directly (parse_hook_command,
crates/jcode-terminal-launch/src/lib.rs): unquoted whitespace splits, and
every unquoted backslash is consumed as an escape. So on Windows
`C:\Users\me\.orca\agent-hooks\jcode-hook.cmd` reached exec as
`C:Usersme.orcaagent-hooksjcode-hook.cmd` and no hook fired at all, and a
POSIX home with a space split into two arguments. Store the path
single-quoted (verbatim, backslashes included), falling back to double
quotes for a path containing a single quote. Existing bare entries are
already repointed by the stale-key path, and getStatus accepts both forms
so the repair is not reported as a user-owned hook. The quoting helper was
previously dead code that only tests called; the three production sites
now use it. isJcodeManagedCommand also normalizes separators, since a
`/`-only needle never matched a Windows entry.

Commit-message generation feeds a staged patch to `jcode run` as the
prompt — attacker-influenced text — while jcode's default profile exposes
shell, read, write, and MCP. Pass `--tool-profile none`, which resolves to
an empty allowed-tool set in jcode's config (base_allowed_tools), matching
the read-only posture claude (plan) and codex (read-only) already take.

docs/reference/jcode-hook-events.md was never actually in this PR: the
repo ignores docs/** and tracks reference docs by allow-list only, so the
captured-payload evidence four source comments point at was silently
dropped. Allow-list it.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* test(jcode): pin the tool-profile flag in the generation plan too

The argv assertion lives in two places; --tool-profile none only landed in
one, so the plan test still expected the unrestricted argv.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(text-generation): refuse an oversized argv prompt on Linux too

The pre-spawn size guard only ran on Windows. Linux caps a single argv
entry at MAX_ARG_STRLEN (32 pages, 128 KiB on a 4-KiB-page host) and
execve fails with E2BIG past it, so an agent that delivers the whole
prompt as one argument — jcode, and the other argv-delivery agents —
failed on a large staged diff with an error the user could not act on.

The cap is per-argument and in bytes, which is why it is not the Windows
line budget: 40k chars trips Windows and is nowhere near the Linux limit,
so folding them together would have refused prompts Linux runs fine.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): register in main's remote-installer guard, drop our duplicate

Rebasing onto 853 commits of main surfaced two things the earlier branch
had hidden.

main already owns a guard for the issue-#7253 bug class
(`remote-hook-service-registry-coverage.test.ts`). This branch had added a
second, near-identical one — a parallel implementation of a test that
already existed, which is what AGENTS.md's reuse rule is about. Deleted
ours and registered jcode in main's, which is the one that has kept pace
with every agent added since.

Also fixes a missing separator in the mobile icon map. `pnpm tc` does not
cover `mobile/`, so only the session-route closure suite caught it.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* fix(jcode): decompose the four files jcode pushed over max-lines

Adding an agent tipped four modules past their line budget. AGENTS.md
forbids a `max-lines` disable or a per-file bump, so each is split on a
real seam rather than silenced:

- agent-catalog.tsx keeps `AgentIcon`, which 70+ files import, and the
  rows move out. The rows alone exceed the 300-line budget a `.ts` file
  gets, so they follow the primary/secondary split this repo already uses
  for commit-message agent specs.
- getAgentResumeArgv -> agent-resume-argv.ts, re-exported so the 18 call
  sites keep one import path.
- isDiscoverableSessionFile/pathSegments -> session-file-discovery.ts.
- remoteCodexSources -> remote-session-scanner-codex-sources.ts; Codex is
  the one remote agent with two CODEX_HOME roots.

Also:
- Records the readiness-census baseline jcode now needs. main added that
  gate while this branch was out; the fixture is the recorded 74-case
  matrix, not a hand-written one.
- Restores two entries a rebase resolution silently dropped from
  config/tsconfig.cli.json (gitlab/project-ref-parser,
  startup/shell-path-probe). Nothing to do with jcode; losing them was a
  conflict-resolution mistake on this branch.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>

* feat(native-chat): open structured chats on the paired Orca server that owns the workspace (#24205)

* fix(native-chat): a host admits structured sessions by client capability, 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).

* fix(native-chat): the desktop declares structured chat support to paired hosts

The desktop renderer advertised agent-session.structured.v1 (and the Claude, turn-item and
background-task capabilities that go with it) to its own main process but not to a paired Orca
server. The server therefore refused every agentSession.* call from the desktop and stripped
structured chat tabs out of the tab list it published to it, so a structured chat running on a
paired server never appeared on the desktop, even though the renderer already mirrors a host's
agent-session tabs and drives each one against the server that owns its workspace.

The same renderer reads structured chats on either host, so the remote Electron list now carries
the same structured-session capabilities as the local one, and the capability test pins that
nothing is advertised only locally.

* feat(native-chat): open structured chats on the paired server that owns the workspace

With the structured-chat default on, an agent launched in a workspace that lives on a paired Orca
server always opened as a terminal (or the terminal-backed chat view). Three things kept it off
the structured path: the launch check refused every host but this machine, a remote workspace was
handed to the host-published terminal path before the structured route was even considered, and
the structured launch pipeline sent create and every follow-up call to this machine's runtime.

A workspace's owning runtime is fixed, so the pipeline now derives it from the workspace instead
of assuming this machine (structured-agent-session-owner.ts, the same derivation the chat pane
already uses to read a session). The launch intent carries that target; the pre-create support
check, create, the publication check and fence read, the launch prompt send, held option picks,
the "focus this chat" marker, the placeholder tab's host, and tab close/purge all use it.

The launch check now accepts a paired server and asks that server's own capabilities (read from
the status the client already cached for it) rather than this machine's. An SSH workspace stays
terminal-backed: no Orca runtime runs there. The host still answers createSupport before
anything is created, so an older server that refuses shows the failure in the chat tab.

A chat the user closed before its create landed is now also retired on the paired server when it
publishes, as the local sync already does. Orchestration workers placed on another runtime are
unchanged: federation creates terminal agents only.

* fix(native-chat): negotiate client-chosen launch mode so released phones 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.

* refactor(runtime): keep the Electron client capability list in its own module

protocol-version.ts is at its line budget; the list is what the desktop
advertises to paired hosts, not the host's own contract.

* fix(native-chat): the desktop declares it picks each launch mode itself

Paired hosts and the desktop's own main process then answer createSupport
by the workspace rather than by their own chat setting.

* fix(native-chat): pin each structured chat to the host it was launched on

- Route: a paired server opens a chat only when it advertises the
  client-chosen launch mode; an older server keeps its terminal. Its
  capabilities come from the store's host status, not the compatibility
  cache that is empty after boot or reconnect.
- A launch command override is this machine's: the route applies it only
  locally, and a host's createSupport refuses on its own override.
- The owning host is resolved once, from the same value the route used,
  and carried on the launch intent, its persisted record (legacy records
  load as local), the provisional tab and every mirrored chat tab. Close,
  purge, retry and reload read it instead of re-deriving it from a
  worktree id two hosts can share; an owner that cannot be named refuses.
- Cancellation tombstones record their host: only that host's
  authoritative inventory retires one, restored cleanup closes it there,
  and a paired host's tombstone expires after 30 days if it never answers.
- A paired server's frame settles launches it published, as the local
  inventory already does for this machine.

* fix(native-chat): a paired server that declines a chat opens its terminal instead

createSupport only reads, so both of its non-answers are settled before
anything is created:
- A paired server that answers it cannot run the chat (a WSL repo, a
  Claude account mismatch, its own launch command override) closes the
  chat tab and opens the terminal the route would have chosen, with a
  notice saying why. This machine's own decline stays a failed chat.
- A host that could not be asked closes the chat tab and leaves one
  failure toast, instead of a lingering "could not confirm" chat.

* test(native-chat): a provisional chat carries its launch's host and hands pre-create failures on

* chore(native-chat): justify the two type assertions this change's lines touch

* test(native-chat): state why each staged test fixture is cast

* fix(native-chat): chats that already exist keep showing whatever the 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.

* fix(native-chat): the browser client keeps its host terminal on paired servers

A browser client whose own preferences turn structured chat on took the
structured route for every paired-server workspace, but its handshake
never says it reads structured sessions, so the server refused the chat
and the user got a failed chat tab where a host terminal used to open.
The route for a paired host now also asks what this client advertises to
it: the desktop's list does, the browser client's does not. Its handshake
list is now a named constant the route reads, so the two cannot drift.

The chat setting's copy now says it runs on paired Orca servers too;
WSL and SSH hosts still use terminal chat.

* fix(native-chat): a retried launch a paired server declines opens its terminal too

A launch restored after a reload settles only through its Retry, so a
declining paired server left a failed chat there while a first launch got
the server's terminal and a notice. The chat's Retry now hands the same
pre-create failures to the same replacement, carrying the prompt the
launch had staged.

* refactor(native-chat): a paired host's cancelled-chat record ends on its 30-day TTL

The paired census re-read a host's whole inventory after every
authoritative frame to retire tombstones, and a tombstone restored after
a reload needed a second such frame, so in practice it retired nothing.
A tombstone guards a random session id and is inert once stale; the chat
is already closed on its host whenever a frame shows it. The census, its
trigger in the mirror layer and its cleanup are removed; the owner-scoped
tombstones, close-on-sight, the TTL and publication marking from frames
stay.

* fix(native-chat): a chat's pane and status read from the host recorded on its tab

The chat pane and its sidebar status still derived the host from the
workspace id, which two hosts can share; a paired chat in a non-active
same-id workspace was read from this machine. Both now read the owner
stamped on the tab, as close, purge and publication already do.

* fix(native-chat): "Resume in chat" follows the terminal resume's host rule

Agent Session History offered "Resume in chat" for a conversation
recorded on this machine into a paired server's workspace, where its
transcript does not exist. A chat now resumes a conversation only on the
host that recorded it, as the terminal resume does, and that host is the
one asked whether it can resume history.

* fix(native-chat): the chat setting says older paired servers keep terminal chat

* test(native-chat): pin that a host advertises the client-chosen launch mode

* fix(native-chat): mirror this machine's chats only where it holds them

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.

* test(native-chat): route a paired-server launch over the capability lists both sides really advertise

* test(native-chat): record install listeners without a cast

* fix(native-chat): a paired server admits a chat before any of it exists here

The desktop opened a paired server's chat tab, launch record, queued
prompt and focus intent before asking the server, so a "no" needed a
replacement that undid and redid all of it, and every piece it missed
was a bug: the workspace deselected, the caller told "failed" while a
terminal ran its prompt, the caller's arguments and other queued prompts
lost, and a create whose reply was lost treated as never sent.

A paired launch now asks the server first and commits nothing until it
answers. Admitted opens the chat as before. Declined runs the caller's
own launch as the server's terminal, with the existing notice (a resume
fails instead, having no terminal equivalent). Unreachable opens nothing
and names the server in one toast. The new-tab launcher reports the
host's surface for paired workspaces, as it did before paired chats,
with the prompt delivery of whichever surface got the prompt. The
replacement and its error classes are gone, and the probe inside a
launch is back to its old meaning: a "no" is a failed chat with Retry,
and no answer leaves "Could not confirm" with Retry and the prompt kept,
here as on this machine.

* fix(native-chat): mirror this machine's chats only once it holds one, 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.

* fix(native-chat): a fork's reveal never seeds a terminal beside the surface the launcher opens

Forking into a paired-server workspace revealed it as if nothing would open there, so the reveal
created a blank host terminal beside the forked chat (and beside a forked agent terminal on main).
The launcher always opens the fork's surface itself, so the reveal now says so for every surface,
as the fix-checks launch already does.

* fix(native-chat): a declined direct launch keeps the caller's CLI args; an unreachable resume toasts once

When a paired server declines a "Fix checks" chat in a new workspace, the terminal that opens
instead now carries the recipe's saved CLI arguments, launch platform and launch source, as the
terminal route did. "Resume in chat" to a server that cannot be reached showed the admission's
"Could not reach" toast and the vault's generic one; the admission marks its failure notified and
the vault adds nothing.

* fix(native-chat): a declined background create opens its terminal without switching workspaces

Since #23974 a worktree create the user moved away from must not pull them onto the new
workspace. When a paired server declined that create's chat, the fallback terminal opened as a new
agent tab, whose host create selects the workspace. The create now opens its own agent terminal the
way main's background branch does: in place from the request's startup plan (so its CLI args carry),
without selecting the workspace. A create the user is still watching keeps the new-tab fallback.

* test(native-chat): name the launch's host in main's new outbox fence test

Main's new staging-failure test calls settleStructuredAgentLaunchPrompt without the target this PR
made required; it is a local launch, as in the sibling tests.

* fix(native-chat): a paired server's new chat shows no model until the server reports the one it started

A chat on a paired server starts with the server's saved model and options, but the picker showed
this desktop's saved selection (or the catalog default) until the server reported a model, and a
pick made in that window was remembered on the server under that guessed model. A paired launch
now carries no desktop seed, and until the server reports its model the picker names no model and
takes no picks. Local chats are unchanged.

* test(native-chat): seed the paired repo without a cast

The repo literal already satisfies Repo, so the changed-lines cast gate has nothing to excuse.

* feat(native-chat): createSupport reports the saved selection a new chat 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.

* fix(native-chat): a paired server's new chat shows the selection the server will start it with

The paired server now names its saved model and options in the admission answer the desktop
already waits for. That seed goes into the launch intent and its persisted record, so the picker
shows the server's model at once, stays pickable like a local chat, and remembers picks on the
server under that model; a reload shows the same. The locked picker remains only for a server too
old to name a seed.

Also moves host admission and launch-outcome tracking into their own modules: the latest main
merge left structured-agent-session-launch.ts over the max-lines limit.

* test(native-chat): expect the launch intent's new seed argument in exact-call assertions

* refactor(protocol): move the Electron remote client capability list into 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.

* fix(native-chat): a paired chat with no saved server model is pickable; Retry shows the server's current seed

A server whose user never saved a chat model sends no seed, and the desktop showed a locked,
model-only picker for it, although that is the common case: no server that can admit a paired chat
predates the seed field. Such a chat now behaves like a local chat with no saved model: the CLI
default, pickable. The lock and its snapshot helper are gone.

Retry kept the first admission's seed while the create probe, which already runs on every attempt,
reported the server's current one and dropped it. The probe's seed now replaces a paired launch's
seed and the picker's, so a retried chat shows what its create will run.

* test(cross-version): stub the launch seed resolver createSupport now reads

* test(protocol): pin the desktop capability divergence against what a paired server receives

Every paired transport sends the shared remote base plus the Electron list, so the
divergence test now compares that union with the renderer's local list instead of
the declared Electron list. A capability added only to the shared base can no
longer slip past it. The two base-only capabilities it surfaced are recorded:
skills.install-result.v2 has no local caller; the authoritative-inventory label is
read by the local tabs sync but dropped by main, and is marked unsettled.

The turn-item and both background-task-stop capabilities were already sent through
the shared base, so the Electron list no longer repeats them. The wire set is
unchanged; this PR's real change on the wire is structured.v1, the Claude
structured capability and the client launch-mode capability.

* fix(native-chat): the desktop tells its own host it picks each launch mode, so retrying an existing chat works with the setting off

* docs(native-chat): name the real exit for the released-phone createSupport rule

* fix(native-chat): the route reads the capabilities a paired host actually receives

The renderer decided whether a paired host would admit a chat from the desktop's Electron list, but
every desktop transport sends that list plus the shared remote base. They agreed only because the
route's checks happened to sit in both. The route input is now built with the same
remoteRuntimeClientCapabilities the transports use (the browser client already sends its list as is),
and a test pins each against the real handshake.

* test(cross-version): a released client still gets the host-setting createSupport answer; a launch-mode client gets supported plus the seed

* test(native-chat): let main's child-records test resolve each chat's owner

Main's new test mocks worktree-runtime-owner with only the runtime environment id, but the status
projection in this PR also resolves each structured chat's owner from the worktree. The mock keeps
the module's real exports and overrides only what the test pins.

* fix(deps): take #24204's lockfile that the merge reverted

* test(native-chat): let the Codex child-approval e2e unit test resolve each chat's owner

Its worktree-runtime-owner mock exported only the runtime environment id, but this PR's status
projection also resolves each structured chat's owner from the worktree. The mock now keeps the
module's real exports and overrides only that id, as structured-child-records-switch does.

* test(native-chat): move the close-race launch cases into their own file

Merging main added launch tests on both sides and took structured-agent-session-launch.test.ts past
the 800-line limit. The three cases where a tab close races a launch move to
structured-agent-session-launch-close-race.test.ts, with the same setup the other split launch
suites copy.

* feat(cli): add --unread/--read to orca worktree set

Pass validated read/unread flags through the existing workspace metadata update.

Contributor attribution correction: the earlier Ctrl+M squash message placed its related link after the co-author footer, so GitHub did not recognize all contributors. Preserve that credit here for https://github.com/stablyai/orca/pull/24100 and its related contribution https://github.com/stablyai/orca/pull/24143 .

Related CLI link validation retained from https://github.com/stablyai/orca/pull/20036 .

Co-authored-by: Raz Shlomo <1237333…

* fix(ci): wait for the fragment navigation policy report (#25017)

---------

Co-authored-by: Justas Brazauskas <brazauskasjustas@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Brynn Bendixen <brynnbendixen@gmail.com>
Co-authored-by: brynnclaw <261708852+brynnclaw@users.noreply.github.com>
Co-authored-by: 小七 <ggbdpq@gmail.com>
Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>
Co-authored-by: lurunzi <lurunzi@gmail.com>
Co-authored-by: Dongho Lee <126236161+LDH1103@users.noreply.github.com>
Co-authored-by: LDH1103 <ldh517525@gmail.com>
Co-authored-by: Wayn_Liu <115852642+Waynting@users.noreply.github.com>
Co-authored-by: Wayn_Liu <wayntingliu@gmail.com>
Co-authored-by: VXNCXNX <VXNCXNX@users.noreply.github.com>
Co-authored-by: Michiel de Gooijer <mdgooijer@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Marcus <70784566+marcuslannister@users.noreply.github.com>
Co-authored-by: marcuslannister <marcus@lannister.cc>
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: Florian Schwarzmeier <1506919+fsmeier@users.noreply.github.com>
Co-authored-by: SongMarco <snubflow@gmail.com>
Co-authored-by: marco song <marco.song@mvlchain.io>
Co-authored-by: SongMarco <20613630+SongMarco@users.noreply.github.com>
Co-authored-by: Luca Critelli <lucacri@gmail.com>
Co-authored-by: JeongUk Park <jeongph.dev@gmail.com>
Co-authored-by: B1nh M1nh <43268322+b1nhm1nh@users.noreply.github.com>
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
Co-authored-by: Kh05ifr4nD <meandSSH0219@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Wooseong Kim <2222333+innocarpe@users.noreply.github.com>
Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: Nawapat Buakoet <nawapat.b@covest.finance>
Co-authored-by: Frederic Barthelemy <git@fbartho.com>
Co-authored-by: mrcha033 <mrcha033@users.noreply.github.com>
Co-authored-by: xodmd45-ctrl <xodmd45-ctrl@users.noreply.github.com>
Co-authored-by: Luchong <lu740528977@gmail.com>
Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: razshlomo <razshlomo@users.noreply.github.com>
Co-authored-by: Raz Shlomo <12373339+razshlomo@users.noreply.github.com>
Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>
Co-authored-by: KAPUIST <thsxornjs12@gmail.com>
Co-authored-by: JianJia2018 <39438074+JianJia2018@users.noreply.github.com>
2026-10-03 08:44:32 -07:00
Jinwoo Hong b06f40f3ba fix(opencode): read the binder's session store off the main thread; ship the reader worker in orcad (#24638)
* fix(opencode): read the binder's session store on the foreign SQLite reader worker (STA-9122)

Before: the OpenCode session binder listed new sessions from opencode.db with
node:sqlite on the main thread every 60 s (and on SessionStart kicks), so a
large or contended store could stall the app the same way Cursor's did.

After: the read is a pure openCodeBinderSessions reader in
foreign-sqlite-readers/readers/, run only on the worker. The binder's
correlation, pane snapshot and process sweep stay where they were.

- The binder round awaits listSessions and re-checks its generation right
  after, so a stop() during the read discards the round before it touches the
  unbound map or the watermark.
- The client's in-flight dedupe key now includes the cursor, so a stale round
  from before a restart cannot hand its rows to the restarted round.
- Idle teardown is per reader. The binder lane keeps its thread for 120 s,
  longer than its 60 s poll, so the thread is not respawned every round.
- A timeout, crash, malformed reply or unstartable worker resolves to [] (no
  sessions), the value the old read already returned on failure.
- An absent store still reads as [] without a log line, and a permission or
  corrupt-file failure still logs (kept from #24577, now in the reader: it
  stats the path and throws anything but ENOENT/ENOTDIR to the client's log).
- The binder lane inherits #24572's limits from the shared lane: no respawn
  until a timed-out worker has exited, 2 consecutive deaths, a queue cap of
  8. Its timeout stays 60 s, matching its poll.
- dispatch switches on the destructured kind, so a new kind without a case
  still fails to compile.

orcad: the hook server runs there too, so orcad now ships
foreign-sqlite-reader-entry.js beside orcad.js (ORCAD_ARTIFACTS, built as an
orcad child). build-orcad runs a smoke check that starts the built worker
under the build's Node and under the pinned runtime, and does a real binder
read on a fixture DB, a Cursor read of a missing file and an OpenCode history
list. The OpenCode history scanner uses the same entry and was bundled into
orcad without it, so on orcad it always failed closed; it can now run.

Tests: reader (cursor, same-ms ids, OpenCode 2 rows, missing then created,
corrupt, inaccessible directory), retirement gate for the binder lane, dispatch
routing, client lane (rows, failure -> [], dedupe per cursor, own thread, idle
teardown default and override), binder loop with an async listSessions
(failure -> [], stop during the read), orcad path resolution through orcad's
host adapters, artifact list, and the smoke check against good, missing and
non-reading entries.

* test(opencode): cover the binder read deadline with fake timers and name the failure test accurately (STA-9122)
2026-10-02 19:58:29 -04:00
Jinwoo Hong 564f4d021a feat: live updates for agent state rules (#24387)
Orca downloads a newer agent-state-rules.json from a fixed GitHub release (stable or next channel), validates it like the bundled rules, and applies it without a restart; a local override wins over the download, which wins over the bundled rules. A hand-started workflow from main is the only publisher; merging publishes nothing.
2026-10-02 14:59:24 -04:00
OrcaWinandm4air 5f308bfa9c revert: take the 26 Phase 3 (#16741 port) PRs back out of main (#24559)
* Revert "feat(orcad): source-side dormant export of a relay-hosted SSH target (#16741 T6-8) (#24519)"

This reverts commit 783101b304.

* Revert "feat(ssh): update, roll back, recover and stop a managed orcad server (#16741 T6-5 follow-up) (#24463)"

This reverts commit 38c2d1dcb9.

* Revert "feat(ssh): deploy and pair an empty managed orcad server over SSH (#16741 T6-5) (#24453)"

This reverts commit 8b76683b40.

* Revert "fix(ssh): orcad GC honors the activation journal; readiness requires proven daemon coverage (#16741 T6 follow-up) (#24451)"

This reverts commit d3f8c5063b.

* Revert "feat(ssh): remote orcad stop by request file and journaled decommission (#16741 T6-4) (#24449)"

This reverts commit 43d9b43d3f.

* Revert "feat(orcad): supervisable server: stop requests, managed stop receipts and a lifetime that keeps its lock on failed teardown (#16741 T6-3) (#24433)"

This reverts commit b093d3ab20.

* Revert "feat(ssh): crash-safe orcad activation, rollback and recovery (#16741 T6-2) (#24423)"

This reverts commit 1a9ac0e955.

* Revert "feat(runtime): SSH access links for paired servers in a downgrade-safe sidecar (#16741 T5-1+T5-2) (#24420)"

This reverts commit 99db2bfae4.

* Revert "feat(relay): capability-gated owner reset with a durable preparation journal (#16741 T3 R1) (#24418)"

This reverts commit 34a582bd39.

* Revert "feat(ssh): track connection-manager drains, test probes and provider continuations (#16741 T2 P3+P8a) (#24407)"

This reverts commit d53063d2b1.

* Revert "feat(daemon): idle retirement, session census and recovery-only provider (#16741 T2 P4b) (#24409)"

This reverts commit ff212dbbef.

* Revert "feat(ssh): add pty.resumeClient and split SSH PTY process listing (#16741 T2 P5+P6) (#24414)"

This reverts commit 92cb71765e.

* Revert "feat(relay): await owned watcher and agent children on shutdown (#16741 T2 P1) (#24400)"

This reverts commit 6b36e4f85b.

* Revert "feat(session): retry failed renderer session writes and verify local folder PTYs (#16741 T2 P9) (#24406)"

This reverts commit d23ecef301.

* Revert "feat(ssh): remote orcad primitives on the pinned Node runtime (#16741 T6-1) (#24419)"

This reverts commit dd87ae578d.

* Revert "fix(runtime): fence runtime-environment subscriptions and status probes by identity (#16741 T5-3) (#24421)"

This reverts commit ece9e4d2e3.

* Revert "feat(orcad): migration manifest and dormant-state contracts (#16741 T6-7) (#24422)"

This reverts commit 3fbdaba262.

* Revert "feat(ssh): wire SshConnection through the work and transport close ledgers (#16741 T2 P2) (#24401)"

This reverts commit 4e8edc8872.

* Revert "feat(profiles): carry markdown frontmatter visibility in project transfers (#16741 T2 P7) (#24405)"

This reverts commit 60c93263cc.

* Revert "fix(runtime): project the PTY incarnation onto mobile session tabs (#24413)"

This reverts commit 99e0303572.

* Revert "feat(daemon): tag daemon stream data with the PTY incarnation id (#16741 T2 P4a) (#24402)"

This reverts commit 817af768b0.

* Revert "feat(ssh): port the SSH connection work ledger and transport close ledger (#16741 T2) (#24210)"

This reverts commit c9918931c8.

* Revert "feat(relay): fence and drain file and git response streams on shutdown (#24185)"

This reverts commit dc08ffeba9.

* Revert "refactor(runtime-rpc): extract the Node WebSocket lifecycle; opt-in pinned port (#24186)"

This reverts commit a789233bbb.

* Revert "feat(relay): route relay handlers through work admission; producer publication drain (#24181)"

This reverts commit 0b812bd698.

* Revert "feat(relay): land the #16741 T1 seam (work drain, publication drain, release gate) (#24156)"

This reverts commit 3aa2d3af7c.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-02 00:52:32 -07:00
OrcaWinandm4air 783101b304 feat(orcad): source-side dormant export of a relay-hosted SSH target (#16741 T6-8) (#24519)
Read-only export of a direct-SSH target's catalog and dormant state into the signed T6-7 manifest: repositories, folder workspaces and their project groups, worktree metadata and lineage, sparse presets, retired worktree names, the workspace session with bounded scrollback snapshots, automations, and client routing. Reads go through the profile-state Store via a read-only OrcadSourceExportPersistence domain; nothing retires the source. Adds the export-aware migration preflight on top of T6-5's dependents census, a resumable snapshot transfer driver with injected destination operations, and destination-side chunk staging keyed to a caller-supplied staged manifest. Lands the P7/P9 holds: session-owner projection hooks, syncDirectoryDurablySync and the durable-write mode, scrollback path and stored-bytes exports, retained refs, and dormant-tab buffer preservation. Inert until T6-10.

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-01 23:03:13 -07:00
38c2d1dcb9 feat(ssh): update, roll back, recover and stop a managed orcad server (#16741 T6-5 follow-up) (#24463)
* feat(ssh): update, roll back, recover and stop a managed orcad server (#16741 T6-5 follow-up)

Builds managed-server maintenance on T6-2's deploy, rollback and recovery and
T6-4's journaled decommission. Each step reads a terminal census through the
server's tunnel; orcad answers it through a new capability-gated
orcad.terminalCensus RPC, and an older host or lost answer is unverifiable.
Update defers over live or uncounted terminals and status reports the last
deferral. A stop unlinks the server (deployment record, tunnel, SSH claim)
only after a proven exit. Inert until the T6-6 settings UI.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(rpc): load the orcad terminal census lazily so the dispatcher does not pull in the xterm window polyfill

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 15:53:24 -07:00
OrcaWinandm4air d3f8c5063b fix(ssh): orcad GC honors the activation journal; readiness requires proven daemon coverage (#16741 T6 follow-up) (#24451)
GC pins every slot an in-flight activation journal names and skips the pass entirely when a journal is unreadable or a fence is held without one. orcad's self-test now reports the coverage the daemon says its probe achieved, and remote readiness probes accept a slot only on pty-spawn coverage, or handshake on win32; builds without the field keep the identity gate.

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-10-01 13:32:33 -07:00
OrcaWinandm4air 43d9b43d3f feat(ssh): remote orcad stop by request file and journaled decommission (#16741 T6-4) (#24449)
Clients stop an orcad that advertises health.stopRequests through its slot-local request file and keep SIGTERM for older builds. Decommission runs through the activation journal and fence: it refuses while the terminal census is live or uncounted, stops the instance with an instance-bound managed request, cancels a stop orcad never acted on, and deactivates the record only on proven exit. orcad gains --cancel-managed-stop and an exclusive per-transaction decision file so a cancel can never race a dispatched stop. POSIX-only and inert: no production caller.

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-10-01 13:32:25 -07:00
OrcaWinandm4air b093d3ab20 feat(orcad): supervisable server: stop requests, managed stop receipts and a lifetime that keeps its lock on failed teardown (#16741 T6-3) (#24433)
orcad stops through slot-local and instance-bound request files, so a reused PID is never signalled. A managed stop is proven by its completion command and recorded as a receipt. Optional daemon retirement is best effort: an idle daemon retires, while a busy or unverifiable one stays up with its admission fence released. Runtime teardown runs in reverse order and keeps the instance lock and profile admission when any writer fails to stop. Browser discovery no longer delays readiness. Legacy worker recovery and watcher children are drained before the final flush. Headless terminal close no longer waits on a renderer tab that does not exist. No production deployment.

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-10-01 12:45:34 -07:00
Jinwoo Hong 56c7642aae test(orcad): skip the live-terminal runtime hand-over across a protocol bump (#24429)
* test(orcad): skip the Bun-to-Node live-terminal hand-over across a protocol bump

The last Bun orcad's daemon reports protocol 38 forever, so asserting the
adopted daemon matches this checkout's PROTOCOL_VERSION failed every bump.
Ask the Bun slot's daemon for its protocol once, run the hand-over when it
matches, and skip with the two versions named when it does not: a daemon
at another protocol is never adopted across an update.

* test(orcad): clean up the Bun protocol probe even when its launch fails

The probe's cleanup ran only after a successful launch, so a launch that
timed out or threw left its orcad and daemon running. One finally now
stops the orcad, kills what it launched, and kills any daemon named by a
pid file in the probe's data root.
2026-10-01 14:28:05 -04:00
Brennan Benson c6cfcc034e refactor(native-chat): structured chat failures always reach the diagnostics log (#24312)
* refactor(native-chat): give the structured chat host one required logger

The structured chat runtime took an optional onError callback that the
desktop never passed, so a late dispatch settlement, an unanswered-dispatch
release, a journal event-sink write and a provider lifecycle delivery that
failed were dropped with no trace. Other host failures went to scattered
console.warn calls, which reach nothing in a packaged desktop build.

The runtime and host now take one required logger (warn/error with a scope
and fields). The production logger writes each entry as a failed span to
<userData>/logs/main.trace.ndjson, which the diagnostic bundle collects, and
to the console (stderr under a supervised headless host). The runtime and the
host wrap it so a logger that throws never fails what it reports, and the
install refuses without one. Sites that deliberately kept a recovery-capsule
error out of the log still log no error object.

* refactor(native-chat): hand the chat host's collaborators the logger, and give orcad its trace file

The delivery loop, idle sweep, queued-message drain, lease renewer, event
sink, conversation map and provider start/exit settlement each took an
internal error callback that the host mapped onto the logger. They now take
the logger itself and log under their own scope. The event sink keeps one
onFailed hook, which decides whether to stop the provider, not whether to
report. The dead-generation settlement returns its failure so each caller
logs it under its own scope.

orcad now installs the desktop's local trace sink under its own data root, so
a headless host's chat failures reach <data-root>/logs/main.trace.ndjson as
well as stderr.

Also passes the logger in the test fixtures the first commit missed, which
tc:node caught.

* fix(native-chat): keep repeated chat failures from flooding the trace file, and record their causes

- The production structured-chat logger writes a repeated failure (same level, scope, session,
  message and error text) once per 5 minutes, carrying how many repeats it swallowed; the
  tracked set is capped at 256.
- Trace entries now carry the error's code (and SQLite errcode) and up to three causes by name and
  message.
- A chat read whose conversation will not open is logged through the host's logger
  (open-for-read), and so are the runtime's chat-tab bookkeeping failures that already hold the
  host.
- orcad writes its own orcad.trace.ndjson, closes it after every quit handler, and flushes it on
  process exit; a trace file that cannot be opened leaves tracing off instead of stopping the app
  or orcad.
- Tests: the desktop wiring test proves the logger reaches the trace sink, and the privacy tests
  read every level the logger received.

* fix(native-chat): log a created chat's tab-publication and launch-prompt failures through the host's logger

* fix(native-chat): key a repeated chat failure on everything its entry writes

The repeat suppression keyed on the message and the error's text, so two refusals with the same
code but different causes, a plain error and a refusal of one code, or two object-valued errors
shared a key and the second was swallowed for five minutes. The key is now the entry's whole
written content (fields, code, errcode, refusal reason, cause chain, a stable rendering of a
non-error value) plus the error's name and message; a refusal's reason is also written.

* test(native-chat): pin that an error's name keeps two repeated failures apart

* test(native-chat): build the refusal in the repeat-key test as the wire does

* fix(native-chat): read an error's code and a refusal's reason by narrowing, not Reflect.get
2026-10-01 11:21:50 -07:00
8afa1db50c feat(ssh): rung B glibc 2.17 compat runtime; gate remote vault on host node:sqlite (#24148)
* feat(ssh): wire rung B to the glibc 2.17 compat runtime; gate rung C vault on full node:sqlite

- COMPAT_RELAY_RUNTIMES lists linux-x64-glibc217; rung B plans the compat slot and compat
  pinned Node when glibc is below 2.28 or rung A refused with libc_floor/missing_lib.
- The relay version folds the compat runtime's executable hash; refusals are cached per runtime.
- The orcad template stages an optional linux-x64-glibc217 target (base package + compat
  node-pty slot + compat runtime marker); the verifier and materializer accept it.
- node-pty slot loader falls back to the compat slot when the default slot is missing or
  needs a newer glibc.
- Runtime store GC keeps the compat pin beside the default one on every relay connect.
- hasNodeSqliteReaderApi (DatabaseSync + backup) gates relay session search and the relay
  OpenCode reader, which now names the host Node version in its unavailable reason; the SSH
  vault reader installs the compat Node on old-glibc hosts and uploads nothing when no
  pinned Node can run.
- Rung D: a remembered noexec reports home_noexec and never advises installing Node.

* fix(ssh): re-prove a replayed noexec after rung D so allowing exec recovers the host

* fix(ssh): keep the rung B compat runtime pinned in the relay-connect store GC

* test(ssh): mock deployment-target facts in the Windows OpenCode runtime tests

* ci(ssh): build the glibc 2.17 compat slot for the hostile-host matrix; CentOS 7 lands on rung B

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-01 05:32:05 -07:00
OrcaWinandm4air ddd4927a0b build(orcad): server node-pty slots at glibc 2.28, plus a glibc 2.17 compat slot (#24134)
* build(orcad): build server glibc slots on glibc 2.28 and add the glibc 2.17 compat slot

Design D6: the default linux-{x64,arm64}-glibc node-pty slots now build in
manylinux_2_28 (digest-pinned) and are gated at glibc 2.28 / GLIBCXX_3.4.25
through a floor profile on verify-linux-glibc-floor.cjs; the desktop keeps
its Ubuntu 20.04 (2.31) default.

Adds the opt-in linux-x64-glibc217 compat target: NODE_RUNTIME_COMPAT_ASSETS
pins the unofficial glibc-217 Node (update/check pin scripts cover it, outside
SERVER_TARGETS), and a new CI lane builds the compat slot in manylinux2014
with static libstdc++, gates it at glibc 2.17 with no shared C++ runtime in
DT_NEEDED, and smokes it under the glibc-217 Node.

* refactor(node-runtime-pin): route compat lookups through isCompatServerTarget; keep the glibc doc's slot-name paragraph intact

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-10-01 01:10:57 -07:00
OrcaWinandm4air 8c2cd7d331 feat(ai-vault): read remote OpenCode history with the pinned Node; remove Bun (#24128)
* feat(ai-vault): read OpenCode history with the pinned Node instead of Bun

SSH hosts whose Node lacks node:sqlite (or its backup(), which 22.13-22.15
omit) now get the pinned Node in the shared ~/.orca-remote/runtimes/node-<sha>
store orcad uses: POSIX hosts receive the official archive and extract and
hash-verify it on the host; Windows hosts receive the verified node.exe the
client extracted, promoted by host Node with the same hash check. WSL distros
use the same layout and checks under ~/.cache/orca/runtimes/.

The Bun release pin table and its materializer are deleted. Old relays keep
reading their vault-sqlite/<sha>/bun references; nothing deletes those files.
An unconfirmed runtime upload now keeps its stage instead of removing it.

* refactor(sqlite): drop the Bun SQLite adapter; node:sqlite is the only backend

Nothing outside Electron runs on Bun any more (design D4), so SyncDatabase
loses its Bun branch, and bun-sqlite-database, bun-sqlite-statement and
bun-readonly-wal go, with the relay's bun:sqlite external. The profile-state
backup worker admits Electron or an entry that exists, and startup errors
name the pinned Node. The D7 cross-runtime gate still runs Bun 1.4.2, now
reaching Bun's SQLite through its node:sqlite.

* test(native-chat): drop the Bun SQLite driver case now that node:sqlite is the only backend

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-10-01 01:10:31 -07:00
OrcaWinandm4air 6593d7d194 feat(orcad): run orcad on the pinned Node instead of Bun (#24110)
* ci(daemon): gate PRs on daemon protocol crossing from the newest release

Lands daemon-protocol-facts.mjs from the Windows update diagnostic branch with a
stricter parser, and adds check-daemon-protocol-crossing.mjs (rule R1): the working
tree must attach the newest release tag's daemon. Rollback crossing is reported only.
Runs in the cross-version-wire job, which already has full tags; tag selection moves
to config/scripts/stable-release-tags.mjs so both use one rule.

* feat(persistence): run profile backups in the worker whenever its entry is bundled

* refactor(orcad): make profile and native preflight runtime-neutral

The profile preflight parser now takes the expected runtime identity from the
caller (shipped callers pass the pinned Bun identity), and the native
preflight is renamed to orcad-runtime-native-preflight with neutral wording.

* feat(runtime): pin the Node 24.21.0 server runtime with an offline CI check

Add src/shared/node-runtime-pin.ts (NODE_RUNTIME_PIN, SERVER_TARGETS,
NODE_RUNTIME_ASSETS for all 8 server targets plus the headers tarball),
generated by config/scripts/update-node-runtime-pin.mjs from the nodejs.org
and unofficial-builds SHASUMS. check-node-runtime-pin.mjs verifies, with no
network, that the pin tracks the locked Electron, matches engines.node's
major, and covers exactly SERVER_TARGETS; it runs in the static analysis job.

ORCAD_BUN_TARGETS consumers now read SERVER_TARGETS so there is one target
list; orcad's Bun runtime and build output are unchanged.

* test(persistence): skip plain-Node backup selection tests in the Bun profile suite

* fix(runtime): reject a pinned archive that belongs to another target

* ci(daemon): fail PRs that swap a runtime launcher and bump the daemon protocol

D7.1 R3: hosting orcad or the daemon on another runtime is not a protocol change,
so one PR must not do both. The launcher file list lives in the check script; the
allow-runtime-launcher-protocol-bump label overrides it.

* feat(orcad): select pinned-Node slots by a .runtime-node marker

D7.1 R5: a Node slot names its shared runtimes/node-<sha256>/node through
.runtime-node instead of .build-target, so Bun-era clients read it as a legacy
slot rather than exiting 78 on a missing bun-runtime. Nothing builds the marker yet.

* fix(runtime): load the Node pin without the typeless-module warning

check-node-runtime-pin.mjs now requires the pin and takes nodeDistArchiveName from
its own module, so it no longer loads the update script's build graph.

* fix(orcad): resolve Node slots to the design's runtimes/node-<sha>/bin/node layout

* feat(orcad): 8-slot node-pty prebuilds against the pinned Node headers at N-API 8

- build-orcad-prebuilds.mjs adds win32-x64/arm64 (conpty.node, the vendored
  conpty.dll/OpenConsole.exe, upstream's N-API conpty_console_list.node), compiles
  in a scratch copy against the hash-verified pinned headers (node.lib pinned per
  Windows arch) with NAPI_VERSION=8, rejects post-8 node_api_* imports, and writes
  a schema 2 manifest with per-file sha256, N-API level and the glibc need.
- --require-slots [slots] verifies files against hashes; --smoke loads the slot
  under the pinned Node and spawns a PTY; --print-slot names the host slot.
- The slot installer gates on N-API, libc, arch, glibc and file hashes instead of
  the exact NODE_MODULE_VERSION, and installs nested files (conpty/).
- bun-profile-tests.yml builds, verifies and smokes each runner's slot.

* fix(orcad): scope node-pty's glibc .symver pins to glibc on musl prebuild slots

musl's unversioned libc cannot satisfy openpty@GLIBC_* references at link
time, so the Alpine slot compile would fail. Pin the staged pty.cc guard to
__GLIBC__ and assert both musl transforms against the installed patch.

* feat(orcad): run orcad on the pinned Node instead of Bun

A packaged orcad slot now references the pinned Node 24.21.0 by its
executableSha256 (`.runtime-node`, `.server-target`) instead of carrying
bun-runtime, and ships node-pty from the slot's prebuild, only its own
ripgrep, and no Windows Bun PTY gate. The runtime lives beside the slots
at runtimes/node-<sha>/bin/node (node.exe on Windows, upstream name).

- build:orcad (build-orcad-node.mjs) builds the host slot's prebuild when
  missing and places the pinned runtime; the template is schema 3 with
  per-target files.
- handoffToBundledOrcad() resolves the slot's runtime reference and checks
  process.versions.node against the pin; a host Node >= 18 still hands off.
  Startup preflight keys on running as that runtime; callers expect 'node'.
- orcad and its daemon use node-pty (ConPTY + windows-pty-job on Windows);
  the Bun PTY sources, gate entry and canUseBunPty branches are removed.
- SSH deploy uploads the official archive once per pin, extracts and
  hash-checks it on the host, and self-tests it before publishing. Bun
  slots stay launchable for rollback; Node slots never use host Node.
- The runtime materializer is generic over pinned assets; the Bun wrapper
  remains only for the OpenCode vault reader (design Phase 2).
- Cross-runtime test: a profile DB written by Bun 1.4.2 (WAL left by
  SIGKILL) opens and backs up under the pinned Node, and the reverse.

No daemon PROTOCOL_VERSION change (design D7.1 R3).

* docs(ci): name the headless lanes after the pinned Node, drop Bun shard timings

Design D10: ci-demand-rollout.md and ci-runner-efficiency.md follow the
bun-profile-tests.yml -> node-server-tests.yml rename; shard timings drop the
deleted Bun PTY tests and follow the renamed ones.

* chore(ci): count the runtime archive download as a runtime launcher path

* fix(orcad): pin the macOS C++ standard for node-pty prebuilds

The official Node headers' config.gypi sets clang: 0, so common.gypi skips its
gnu++20 xcode_settings and Apple clang 15 (macos-14 runners) compiles
node-addon-api as C++98.

* fix(orcad): resolve the preflight's slot through realpath, as the handoff does

A symlinked orcad.js handed off to its real slot's pinned Node, but the
startup and profile preflights read the symlink's directory, found no
runtime marker there, and silently skipped the readiness check.

* refactor(ssh): drop materializeCachedNodeRuntime, which nothing calls

Deploys upload the verified official archive (design D5); no client path
needs an extracted Node executable cached by digest.

* test(orcad): gate the Bun-to-Node upgrade and Node-to-Bun rollback with live terminals

Design D7.1 R1/R3/R4 and D7.2. The last Bun orcad and this checkout's Node
slot are installed side by side under ~/.orca-remote, launched and stopped
with the client's own deploy commands, and share one data root. Each
direction proves the incoming orcad adopts the outgoing runtime's daemon
(same PID, same shell, output continues), opens its profile database and
backs it up with its own shipped worker, and that GC keeps the slot the
live daemon was forked from.

The node-server Linux lanes provide Bun 1.4.2 and build that Bun orcad
from main, and run with --cross-runtime. --artifact and --cross-runtime
now make their tests fail on a missing input instead of skipping.

* ci(node-server): pin node:24.21.0-alpine by its multi-arch index digest

* test(ssh): name the runtime archive fixture after its role

* test(node-server): load node-pty from the packaged slot in artifact runs

The node-server lane installs dependencies without building node-pty, and
Linux has no upstream prebuild, so the real-PTY failed-I/O teardown test
(picked up by the pty-subprocess selector) could not load pty.node. In
--artifact runs, alias node-pty to out/orcad's shipped slot so the test
exercises the addon orcad actually runs under the pinned Node.

* fix(orcad): let the Windows profile preflight exit after its PTY probe

On Windows, node-pty keeps the conout worker thread and pseudoconsole alive
until kill(), even after the shell exits. The PTY health probe never killed a
cleanly exited probe, so the packaged preflight printed its readiness line
and then hung until the build's 30s timeout, reported with an empty stderr.

- The probe kills its PTY on Windows after exit and uses the bundled ConPTY
  the daemon spawns with.
- The preflight exits once stdout is flushed; its owner reads to EOF.
- Preflight failures now report code, signal, timeout, stdout and stderr.

* test(node-server): load the slot's node-pty in the real-PTY test, not by alias

A vite alias redirected only ESM imports of node-pty; windows-pty-job and
local-pty-utils resolve it through require, so Windows loaded two conpty.node
copies and the Git Bash job-membership proof read an empty job. The failed-I/O
teardown test now loads node-pty through a fixture that picks the packaged slot
in artifact lanes.

The pty-subprocess selector was a prefix that also pulled in its POSIX-host
sibling unit tests, which pr.yml runs and which were never qualified on
Windows. Select the directory plus the two sibling files that belong here.

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-10-01 00:39:00 -07:00
OrcaWinandm4air 49a83deaef refactor(orcad): make profile backup and preflight runtime-neutral (#24088)
* feat(persistence): run profile backups in the worker whenever its entry is bundled

* refactor(orcad): make profile and native preflight runtime-neutral

The profile preflight parser now takes the expected runtime identity from the
caller (shipped callers pass the pinned Bun identity), and the native
preflight is renamed to orcad-runtime-native-preflight with neutral wording.

* test(persistence): skip plain-Node backup selection tests in the Bun profile suite

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-09-30 23:23:23 -07:00
OrcaWinandm4air 3135fbbf49 feat(runtime): pin the Node 24.21.0 server runtime with an offline CI check (#24087)
* feat(runtime): pin the Node 24.21.0 server runtime with an offline CI check

Add src/shared/node-runtime-pin.ts (NODE_RUNTIME_PIN, SERVER_TARGETS,
NODE_RUNTIME_ASSETS for all 8 server targets plus the headers tarball),
generated by config/scripts/update-node-runtime-pin.mjs from the nodejs.org
and unofficial-builds SHASUMS. check-node-runtime-pin.mjs verifies, with no
network, that the pin tracks the locked Electron, matches engines.node's
major, and covers exactly SERVER_TARGETS; it runs in the static analysis job.

ORCAD_BUN_TARGETS consumers now read SERVER_TARGETS so there is one target
list; orcad's Bun runtime and build output are unchanged.

* fix(runtime): reject a pinned archive that belongs to another target

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-09-30 22:57:10 -07:00
Brennan Benson 0b79720c2e feat(native-chat): the chat strip and the sidebar read the host's child records (#22614)
* feat(native-chat): publish the host's child records to the status summary and the chat strip

The status summary and the background-task channel now read a session's child
records from the host's canonical store, through the status sink its row
landed in, and derive the legacy task and subagent shapes from the same views.
The parent row folds its child-work liveness from those records at ingest,
not from the summary's task list. The adapters no longer push their task DTO
to clients: the onBackgroundTasksChanged path is gone, and a child-work ingest
is what republishes both the summary and the strip. Finished children stay
listed until the session's own next turn starts. A reader that predates child
views never receives a roster whose rows are all settled.

* feat(native-chat): the chat strip reads the host's child records with its parent's verdict

The strip's roster now renders from the child views its channel carries, and
passes the verdict the session's own status row gives its children, built the
way the sidebar builds it (the row's freshness and the status feed's
observation). So one child reads the same in the strip and the sidebar, live,
after the transport drops, and once the row goes stale. A roster of finished
children stays shown until the next turn but no longer animates the monitoring
indicator or blocks conversation commands.

Tests: an end-to-end run on a host with no renderer (a real hook server as the
status sink) shows the summary and the strip channel carrying the same records
at every step, the parent row folded from them, retention, and an older
client's task list holding live work only; a wired renderer test shows both
surfaces agree when live, lost and stale.

* test(native-chat): a newer host's view degrades, an older reader keeps its live roster, a finished roster holds nothing open

- The view decoder ignores unknown keys, degrades unknown kinds, states,
  outcomes and memberships, and drops only rows it cannot identify.
- At the RPC boundary a reader that predates child views gets no strip for a
  roster of finished children and never the views themselves; a stop-only
  reader keeps rows whose host offers no targeted stop.
- The strip shows finished children without reading them as live work.
- The row keeps its child list's identity when a summary repeats it.

* refactor(native-chat): the summary's task list is the legacy projection's live rows, unfiltered

* test(native-chat): type the switch tests' mocks instead of asserting them

* test: remote clients advertise reading child views

* docs(agent-status): the structured row folds the store's child records

* fix(agent-status): keep the view reader's header from reading as a value import to the renderer boundary

The renderer node-builtin boundary test scans raw text, so a header comment
that said "imports" ahead of the import block turned the type-only import of
agent-status-child-work into a value edge that reaches node:crypto.

* refactor(native-chat): the status summary's broadcast equality gets its own module

The status feed crossed the file-size limit once the summary gained the main agent's turn
outcome beside the child views. Which summary changes reach every session list now lives in
structured-agent-session-status-summary-equality.ts.

* fix(native-chat): command admission reads the strip's child records

A conversation command was refused on the provider tracker's own roster
while the strip read the host's child records, so a drift between the two
rule sets could refuse /clear with a stop instruction the strip had no
button for. Admission now reads the same records through the same read as
the strip, uses the strip's liveness fold, and asks for a stop only when
the strip renders one. The adapter contract no longer exposes the tracker
roster, so no host decision can read it.

Also records when the legacy child shapes die, every earlier death of a
settled child, and the display-precision invariant behind the summary's
clock tolerance.

* refactor(native-chat): command admission takes only what it reads of a turn

* fix(native-chat): the session list drops a session's children when the store does

A session's end no longer removes its child records: a child still running
settles with an outcome nobody reported, and a finished one stays listed.
Records now leave only at the session's own next turn, at the cap on settled
records, or when the host lets go of the session and its row leaves the store.

The summary kept after the host lets go used to strip its children on close,
a rule of its own. It now re-reads them from the store when the row leaves,
through the same read every live summary uses, so the session list and the
chat strip list the same children at each step, including a forget with no
close. Closing only revokes ownership, as before the child records existed.

* test(native-chat): write the Codex frame script's parent row out step by step

Once every surface reads the child records, the provider tracker's roster is
no oracle: it and the records read the same child executions, so agreeing
with it cannot catch a defect in either. Each frame now states the child
liveness and the parent row it must fold to.

* fix(native-chat): the idle sweep and the restart snapshot read the host's child records

The idle sweep (keep an agent running while its subagents or commands run) and
the restart-resume snapshot (what a chat was doing when Orca stopped it) both
read the provider tracker's roster through the adapter interface, which no
longer carries it. Both now take the host's one child-record read, the same one
the status summary, the chat strip and command admission use.

The snapshot's working test also folded that roster through the shared fold's
old `backgroundTasks` input, which the fold no longer reads, so a settled lead
whose subagent was still running would have been offered nothing. It now hands
the fold the records.

* test(native-chat): the child-record tests follow the merged command lifecycle

A command is a live child record from its start and is removed, not settled,
when it stops, whatever Codex tagged it. The end-to-end switch now shows the
child's `npm test` as a live row beside its dev server, and both are gone once
they exit; only the finished subagent stays listed until the next turn. Letting
go of the session is its tab closing, since a closed conversation whose tab
remains keeps its row.

Command admission's finished row is a subagent, the one kind that settles, and
the failed-verdict row test admits its live subagent as a host record, the only
thing the row folds.

* refactor(native-chat): the status feed's journal projection cache gets its own module

The status feed crossed the file-size limit once the child records joined the
agent-start signal and the completion feed's status read. The per-journal
projection, cached per commit, now lives in
structured-agent-session-status-journal-projection.ts.

* test(native-chat): the admission test's compaction resolves with a real outcome

Main's compaction result is a tagged outcome; the host-level admission test
resolved its mock compaction with an empty object.

* feat(native-chat): the sidebar lists running subagents; the strip, running then the newest finished

The host keeps every child record; what each surface lists is picked from them on every read, so
nothing is stored twice. The status summary, which every session list reads, now carries only
running children (and a finished one whose shell still runs, which reads monitoring): a finished
or failed subagent leaves the sidebar and stays in the chat's strip. The strip lists every running
child, then the newest finished ones, 100 rows in all; more than 100 running all show.

This matches common practice: sidebars show live subagents, and finished ones stay in the chat's
panel, newest first. No wire field is added. An older client reads fewer rows: its legacy task
lists were already live-only in the summary, and the strip's settled tasks come from the same
bounded roster.

* fix(sidebar): one rule for what the worktree sidebar lists: running children, from every source

`worktreeSidebarListsChild` is the one definition: a child that runs, counting a finished one
whose own shell still runs (it reads monitoring). The sidebar's row builder applies it to every
child source it reads, a terminal agent's hook roster and a chat session's records alike, and the
host's status summary applies the same predicate, so the sidebar's payload stays small. The chat's
strip keeps finished children, newest first.

A terminal agent's hook roster already drops a child on its own stop, so nothing changes there:
a teammate between turns and a child gone quiet still run, and still show. The selection module
moves to `agent-child-work-listing.ts`, since it now covers every source, not only chat sessions.

* test(native-chat): the switch test passes the startup child key main's status bar takes

* fix(native-chat): a finished child stays until the user's next send, not a turn Claude opens on its own

Claude wakes the agent on its own when a background task ends, and that wake is a
new root turn. Keying retention on the newest root turn retired every finished
child about two seconds after a background agent or shell finished, so its
outcome never showed in the strip.

Retention now keys on the user's newest send the provider accepted (a message, a
steer or a command), with the journal epoch so a rewind still retires. A turn the
provider opens itself and a subagent's turn carry no send. Replayed captured wake
orders through the real adapter, hook server and status feed.

* fix(native-chat): a background Stop reaches the tasks the child records show

The strip draws a row's Stop, and /clear, /compact and rewind wait for background
work, from the host's child records, but the Claude adapter still resolved which
tasks a Stop reached from its own tracker's roster, and refused to stop at all
once that roster was empty. A task the records kept live after the roster dropped
it showed a Stop that sent nothing and blocked those commands until the chat tab
closed.

The host now resolves the provider ids a Stop sends from the records (the same
per-row rule the strip and admission use; every such row for stop-all), and the
adapter stops exactly those, with no tracker guard. An acknowledged stop ends the
record: a running task sends its own stopped frame first, and the CLI answers
success with no frame for a task it no longer knows. A refused stop leaves the
record live. No production code reads the tracker's roster any more.

* fix(native-chat): one rule for a finished child that still owns live work, at any depth

The listing kept a finished child whose work ran through any depth of ownership,
but retention at the user's next turn protected only the direct owner, so a
finished agent whose finished subagent still ran a shell was removed and that
subagent jumped to the top level. Both now read settledOwnersOfLiveWork.

* fix(native-chat): an older client sees a Codex child's shell as it did before views

Clients that predate child views read a flat task roster derived from the views.
It listed a Codex child agent's shell as an extra row beside the running agent,
then as a bare command once the agent finished. The derivation now hides a
running agent's commands and names a finished agent's as "<agent> — <command>",
as the Codex tracker did; the label rule moves to a shared module both use.

* fix(native-chat): the chat decodes a roster's child rows once, as the frame arrives

The client reducer compared raw wire rows, so a row shaped by a newer host could
throw there, and the strip decoded a new array on every render, which defeated its
grouped-rows memo while a turn streamed. Rows are now decoded where the frame
enters the reducer, an unchanged roster keeps its identity, and the strip's parent
context is rebuilt only when one of its values changes.

* fix(native-chat): the strip channel forgets a closed conversation's roster

It kept the last roster fingerprint of every conversation for the host's lifetime.
The conversation map now tells observers when one leaves it. Also corrects the
summary's children comment: it carries running children only.

* docs(native-chat): rewrap the retention comment

* fix(native-chat): a task's own ending replaces a Stop's, and a child finished after the user wrote stays

Two lifecycle gaps from the round-1 fixes.

A Stop acknowledged ahead of the task's own ending relabelled it. The SDK hands
Orca a control answer as soon as it reads it and queues other frames, so when a
task finished just as the user pressed Stop, the acknowledgement arrived before
the task's completion the CLI wrote first, and the task read "Stopped" with its
result lost. An acknowledged Stop now ends a record provisionally (outcome basis
`stop-acknowledged`); the task's own terminal frame replaces it, and nothing
replaces an ending the task reported itself.

A finished child still vanished with no new action from the user when the send
the provider took was written before the child finished: a steer Claude takes at
its next boundary, or a queued draft handed over at the end of the turn. The
user's next turn now carries when they acted (the send's written time, or the
draft's queued time, both on the host clock), and only children that finished at
or before it retire; a later one stays until the user's next send. A rewind
still retires every finished child.

Also: a Stop-all keeps stopping the remaining tasks after one request fails, then
reports the failure.

* fix(native-chat): the strip keeps one empty list for a roster that omits one

A roster with only running or only finished rows made a new empty array on every
render, so the strip regrouped its rows each time. One shared empty list keeps
its memo.

* fix(native-chat): a strip row whose owner the 100-row budget cut renders under the main agent

The budget can keep a finished child and cut its finished owner. The child still
named that owner, so it rendered nowhere. The selection now clears an owner it
did not keep, as the view contract says for an owner outside the projection.

* fix(native-chat): a stop-all that times out stops asking, and "no children" is sent once

A stop-all kept asking after a request timed out, so a Claude CLI that stopped
answering control requests cost one full deadline per task, while the chat's
sends, Stop and /compact waited behind it. A timeout now ends the loop; other
request failures still let the remaining tasks be stopped.

The chat strip channel never remembered that it had sent "no children", so
every change in a chat with none re-sent that frame to each subscriber. It now
remembers it, and forgets only when the conversation closes.

Also renames agent-child-work-stop.ts to agent-child-work-stop-targets.ts, which
says what it answers: the provider ids a background Stop reaches.

* fix(native-chat): the status feed reads a journal snapshot with no submissions, and e2e tests use the child-work reader

CI on dbd2439cd4 was red in three places:
- first-work-branch-rename and the agentSession.subscribeStatus RPC test feed the status feed a
  journal whose snapshot lists items only. The projection reads the user's newest accepted send
  from `snapshot.submissions`; it now tolerates their absence, as the status projection beside
  it already did.
- the cross-version downgrade test still passed `backgroundTasks` to the teardown's working
  marker, which now takes `childWork`.
- an e2e unit test still gave the status feed the removed `readBackgroundTasks` dependency
  (harmless at run time, a type error in the tests/ project).

* fix(native-chat): the chat strip lists running children only, by the sidebar's rule, and hides when none runs

A finished subagent's result is already in the transcript ("Ran N subagents ·
completed"), so the strip is for work that runs. It now lists exactly what the
sidebar lists, by one predicate (a running child, or a finished one whose own
shell still runs, which reads monitoring), and the host sends no roster once none
runs, so the strip hides.

Gone with it: the 100-row budget and the running-then-newest-finished
selection, the re-homing of a child whose owner the budget cut, and the RPC
gate's rule for a roster of finished rows only (no such roster exists now).
Older clients still get their derived task list, running work only.

Finished records still stay in the host's store until the user's next accepted
message: they refuse a late frame of their run, let a task's own ending replace
an acknowledged Stop's, and keep a running shell's owner. Dating that retention
by when the user wrote the message only kept finished rows visible longer, so it
is removed.

* fix(native-chat): the strip shows running work only from any host, and hides after a released session's last child

- A new app paired with an older host no longer shows that host's finished task
  rows: the strip lists running work only, whatever host sent it, and hides when
  an older host's roster has only finished rows left.
- A test for the path that hides the strip when a session's last running child
  settles after the provider let go of the session (Claude's release path): the
  channel sends `null` though no provider answers for the session any more.
- A test comment still described the strip keeping finished children.
2026-09-30 18:23:24 -07:00
Brennan Benson 24540300f0 fix(agent-status): preserve hook presence when process checks cannot answer (step 1 of 3) (#23947)
* fix(agent-status): admit hook process presence on the execution host

* fix(agent-status): restrict process checks to real hook ingress

* fix(agent-status): keep presence checks from causing false exits or losing real ones

- Pin the macOS process start time to UTC on both the hook and the host so a
  shell TZ or a time-zone change cannot turn a live Claude into an exit.
- An unanswered process check falls back to the foreground confirmation, so
  Codex, SSH and Windows panes still leave the agent state on a real exit and
  the Codex late-completion recovery still runs.
- A nested agent that inherits the pane key cannot take over the pane's
  presence; a retired session is replaced by the next session even when its
  SessionStart was lost.
- Drop the unused Windows process read (no Windows hook captures an identity
  yet) so shared code no longer imports main-process modules.
- Skip the capture outside Orca panes, gate relay re-checks to real title
  changes, and list the capture module in the CLI project.

* refactor(agent-status): own pane presence by the agent's process, not its session

Presence now exists only when a hook carries the agent's process identity,
and only that process's evidence changes it: its SessionEnd ends the pane,
its /clear and /resume keep it, and hooks from any other process (a nested
agent inheriting the pane key) update status without taking ownership.
Hooks without an identity (Windows, sessions started before the capture)
behave exactly as before, so a nested agent can no longer end a pane it
does not own. An ended owner stops answering 'exited', and the host probes
the owner only when another process reports in the pane.

* fix(agent-status): close round-3 review gaps in presence handling

- Relay retries and transcript polls schedule against the row the relay
  cached, and identity-less events pass through the transition unchanged,
  so SSH Grok replies and Codex transcript polls deliver again.
- A live process check restores the runtime's agent status and releases
  queued orchestration mail, like a foreground read that finds the agent.
- An answered foreground read naming a non-agent (wsl.exe, tmux) is still
  an exit; only silence is not.
- A suspended (Ctrl-Z) agent is unverifiable, not live, so the foreground
  read decides as before.
- An agent of another type started mid-turn cannot own or end the pane.
- Replayed spool hooks check each pane once, and SessionEnd ends presence
  only for reasons that end the process.

* refactor(agent-status): record the pane's owning agent even without a process id

Ownership is now decided only by comparing the recorded owner with the
sender, never from the row's agent type or turn state (which identity
resolution rewrites). The first agent hook in an empty pane claims it; a
live owner keeps it against any other agent (a nested claude -p, a Codex
started inside Claude or the reverse); only the owner's own proven process
can end it. An owner no hook identified never ends from a hook and is not
probed, so those panes behave as before.

* test(agent-status): read the optional process id in the relay presence test

* fix(agent-status): other agents' SessionEnd hooks settle their status again

Only an admitted exit (Claude's process-ending SessionEnd, or a host-proved
exit) is marked ended on the event, and ownership keys on that marker, not
the hook name. Devin, Qoder, CodeBuddy and Copilot SessionEnd hooks are
ordinary status updates again, locally and through the relay.

* test(agent-status): cover the owner's own SessionEnd through the relay

* fix(agent-status): rows without a process identity keep today's command-finished cleanup

The renderer's command-finished cleanup kept every row when its shell check
could not answer, which left Codex, hookless-agent and old-relay rows over
SSH showing done after a real exit. It now asks the host whether the pane's
agent process can be checked: only a pane with an identified, running owner
keeps its row on an unanswered check; every other pane drops it exactly as
before. The drop stays armed while the host answers, so a new command still
cancels it, and a missing or failing answer (web client, older host) keeps
today's behaviour.

* fix(agent-status): panes without an identified owner keep today's exit confirmation

The title-driven exit confirmation applied 'silence is never an exit' to
every pane. It now applies only when the pane has an identified owner whose
process cannot be checked right now; a pane with no process identity
(Codex, hookless agents, Windows, old relays, sessions started before the
update, or an owner that already ended) confirms exits exactly as before.
2026-09-30 18:21:46 -07:00
Neil 17690e6b9a style: settle oxfmt 0.70 drift and stop formatting vendored licences (#23377)
The oxfmt 0.65 -> 0.70 bump landed without a repo-wide reformat, so 36 files
already in the tree no longer matched what the new version emits. Anyone running
`pnpm format` picked all of them up alongside their own change.

Also excludes `resources/licenses/**`: `oxfmt --write .` was rewriting the
vendored PCRE2 licence, turning its `*` redistribution bullets into `-`. Third
party licence text has to be reproduced verbatim, so formatting must not touch it.
2026-09-27 01:14:53 -07:00
OrcaWinandm4air 10cfe9218f test: target writer timeouts and diagnose Bun startup stalls (#23267)
* test(persistence): start the fault timer after the real worker is ready

* test(orcad): preserve launcher startup phase evidence on timeout

* test: isolate historical hourly build version inputs

* test: align historical package input with shared CI repair

* test: inject hourly package version without module mocking

* test: share the hourly package input contract across CI repairs

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-09-26 15:05:44 -07:00
OrcaWinandm4air 8416e8de10 refactor(persistence): retire ordinary JSON profile writes (#23202)
* refactor(persistence): retire ordinary JSON profile writes

Require SQLite for writable profiles and keep import, compatibility export, and recovery in a documented legacy-json boundary.

* fix(cli): preserve dynamic profile imports in release output

* test(persistence): exercise SQL races and verify packaged CLI imports

* test(persistence): consolidate shared fixture imports

* test(persistence): close SQLite fixtures before cleanup and await launcher output

* test(automations): use SQLite fixtures for dispatch fencing and skip coalescing

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-09-26 12:56:32 -07:00
OrcaWinandm4air 9700030689 refactor(pty): make renderer delivery optional for headless runtimes (#23117)
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-09-26 02:25:16 -07:00
OrcaWin 6fc3cdcad6 Bundle Bun for headless Orca and profile persistence (#22635)
Bundle a pinned, verified Bun runtime for headless Orca so existing Node launch commands can hand off before opening a profile. Keep desktop execution on Electron.

Add the Bun SQLite adapter and terminal backend, bounded shutdown, process inspection and cross-platform artifact qualification. Keep future managed SSH deployment separate from current production launch paths.
2026-09-25 22:49:06 -07:00
OrcaWin 82412dab8b Persist profile state in SQLite with background writes (#22612)
Migrate profile state to SQLite and move writes and backups into a background worker. Acknowledge terminal, SSH and automation changes only after durable saves. Preserve JSON import, recovery, rollback and compatibility exports.

Validate migration, worker failures, maintenance, cross-profile moves and terminal lifetime races with unit, integration and end-to-end coverage.
2026-09-25 22:47:33 -07:00
Brennan Benson acf8e679ea feat(native-chat): Claude sessions write their subagents into the host status store (#22536)
* refactor(native-chat): the host hands out client delivery's status subscriptions as they are

subscribeStatus and subscribeTurnCompletions wrapped client delivery's bound
methods in forwarding lambdas; they are now the same members, the way
waitForSendSettlement already is. The host is at its size limit, and the
next channel it hands out needs the line.

* feat(native-chat): Claude sessions write their subagents into the host status store

The Claude background-task tracker queues child-work evidence at each decision it
already makes (start, update, progress, terminal frame, roster replacement, turn
end, session end), plus the two facts its legacy row ignores: a foreground child's
progress and a foreground spawn call's result. The adapter drains that evidence
after the journal handled the frame and the parent row was republished, and the
host folds it into one record per child in its canonical store.

Nothing reads the records yet; the strip and sidebar keep their current sources.

* test(native-chat): pin the Claude child-work evidence and the host reduction of it

* test(native-chat): prove every hop from a Claude frame to the host's child record

The adapter delivers evidence after the frame's journal rows and the parent's
republished row; the frame script keeps the parent state today reads while the
records add outcome and activity; the runtime hands the evidence to the status sink
under the session's own address; both entry points wire the sink to the ingest.

* test(native-chat): read an optional task list as optional in the producer script

* test(native-chat): an address whose publish threw carries no child work

* test(agent-status): a foreign record differs from ours by producer alone

* feat(native-chat): a foreground Claude child's own tool call is what its record says it is doing

A child's tool traffic reaches the parent stream only for a foreground child. Read
after the journal handled the frame, the child's newest call still awaiting a result
becomes its open operation, previewed the way a hook-reported row previews its own
tool; the result closes it. The open call is derived from the journal's own
bookkeeping, not held a second time.

* fix(native-chat): a Claude child restarted under a new spawn call keeps reporting to its record

A task that ended and starts again stays hidden from the legacy row until a roster
lists it, so the tracker held no run for it: the new run's progress reached nothing
and a foreground re-run's own spawn result settled nothing. The run is now held
beside the live map, where the legacy row never reads it, until a roster hands it
back or it ends. A parity test pins the record's run count to the journal roster's
attempt on a new spawn call, the one event both count.

* refactor(native-chat): the Claude child-tool queries and translator contract get their own homes

The translator's child-tool queries move into claude-child-tool-queries.ts and its contract
type into claude-journal-translator-contract.ts. Brings the translator back under the size
limit.

* refactor(native-chat): Claude child evidence carries only its own edge's facts

Admission now keeps what a child's record already knows: labels, model,
owner, residency, the last message within an invocation, and a token count
that never shrinks. The evidence side copied all of those forward itself, a
second owner of the same rule. It now sends only what this edge observed,
and a task's token count comes from the frame that reported it.

* refactor(native-chat): Claude child evidence hands admission its raw labels

Admission now folds provider text to one line and drops a malformed fact
instead of refusing the record, so the evidence side no longer folds labels
itself. The description keeps admission's longer bound.

* fix(agent-status): admission alone decides a settled child's second ending

The reconciliation returned before admission whenever a record had already
settled with a definite outcome. That dropped the evidence an `unknown` ending
carries (its last message and tokens), which admission's refine-only rule keeps,
so that rule never ran for the structured producers.

The latch goes. Admission keeps the definite outcome, lands the late evidence,
and refuses a conflicting definite ending as `stale-invocation`, which the host
ingest already counts as the fence doing its job, not a fault.

Pinned through the real Claude producer and the host's own ingest.

* perf(agent-status): keep child records off the status hot paths

Child records made every store write and every status notification scale with the
whole store. Each Claude child progress frame cost about 2 ms with 5 chats holding
~200 child records (about 14 ms at ~1,400), and every status change on any lane
re-parsed every child record just to list parent rows.

- The store derives each frozen record's key once instead of re-parsing it on every
  mutation's validation and every alias lookup.
- Settled history is trimmed only when a batch settles something.
- Parent listing and the structured row's revision stamp read the parents and the
  revision directly instead of building a full snapshot.

A progress frame now costs about 0.3 ms at the same size, and listing parent rows no
longer depends on how many child records the store holds.

* fix(native-chat): an errored Claude spawn result no longer decides how the child ended

Interrupting a foreground Claude agent while it runs a tool delivers the spawn call's errored
result before the child's own killed/stopped frames. The spawn result settled the record
`failed` first, and admission then refused the later `cancelled` as a conflicting ending, so an
interrupted child read as a failure.

An errored spawn result now settles the child `unknown`; the child's own terminal frame refines
it to `cancelled` or `failed`. A successful spawn result still settles `succeeded`. The test
replays both frame orders the real CLI produced when interrupted.

* test(native-chat): pin a Claude foreground child's real finishing order

The real CLI ends a foreground agent with its own completed update, then a notification
carrying the final summary and usage, and only then the spawn call's result. Existing tests
modeled the spawn result arriving first, so nothing checked that the notification's summary
and tokens still land on a record the update already settled.

* perf(agent-status): a store write costs what it touches, not the whole store

With child records on the host, every mutation copied all five store maps and re-validated
every record, and reads scanned every child and alias. A parent status publish cost about
10 ms with 4,000 child records in the store, and a child update about 13 ms.

- A mutation writes into drafts over the committed maps and lands in place; a refused one
  is dropped with nothing to undo. The drafts keep the exact map order a copy would have.
- Only what a mutation touched is re-validated: touched parents, children, aliases, facts
  and tombstones, plus every alias of a touched child and whatever a removed parent owned.
  The full validation stays for snapshot restore.
- The snapshot byte budget is a running total instead of a re-measure.
- Children by parent, facts by parent, aliases by child, aliases by identity and retired
  aliases are indexed, so reads return stored records without scanning or re-parsing.
- The memoized alias identity and tombstone-key checks are gone: indexes derive them once.

A parent publish now costs about 0.015 ms and a child update about 0.06 ms at 40, 1,000 and
4,000 children alike. A seeded fuzz holds the store to the copy-and-validate-everything
path decision for decision, snapshot for snapshot and read for read, and a replica fed the
envelopes ends identical.

* fix(native-chat): a Claude child ends only on its own terminal frame

The child records were fed from the legacy background-task tracker's display decisions, so
they inherited rules that are not truth: a turn ending swept foreground children, a roster
omitting a background child settled it, a foreground spawn call's result ended the child,
and a new background start after any roster produced no record. Captured from the real CLI,
an agent moved to the background keeps its own shell running for 40 s after the parent's
turn ends, and that shell was settled `unknown` at the parent's `result`. Replayed with the
spawn result ahead of the roster, the same agent settled as a false success and was then
revived as a spurious second run.

A new decoder reads the task frames directly. `task_started` opens a child (a start for an
ended task id is a restart, the way messaging a finished agent resumes it), progress and a
live `task_updated` update it, and a terminal `task_updated` or `task_notification` ends it.
Rosters, turn ends and spawn results say nothing about a child. Every child in every capture
gets its own terminal frame, so no evidenced ending is lost. The notification's `tool_use_id`
names the run that ended (captured on a resumed agent's second run), so an ending from a run
that is already over no longer ends the current one; a run id the record never saw still
ends it, so nothing strands.

The tracker, its settled-task retention and the frame readers are back to exactly what main
has: the aggregate-roster split and the restart holding map are deleted, and the legacy row is
unchanged by construction.

* fix(agent-status): a session's end settles its live children instead of erasing them

When a structured session ended, the reducer removed every child record it held, finished
or not, so a reader could no longer tell how the session's work had ended. Now a child still
live when its session ends settles `unknown` (nothing reported how it ended), and a child that
had already ended keeps its outcome. The records still die with their parent: closing or
releasing the session drops the parent row, and the store drops its children with it. A
child's own outcome arriving after the session ended still refines the `unknown`.

The `inventory` and `turn-ended` edges, and the rules that settled children on a roster
omission or at a turn boundary, are deleted: no producer sends them any more. A restart is
now its own flag on a live edge, which is what a producer reports when a finished child
starts again under the same run handle.

* test(native-chat): replay the real Claude CLI's frame orders into a real host

Scrubbed cuts of five Claude CLI 2.1.280 stream-json captures (ids, paths and prompts replaced,
frame order and relative clock kept), replayed through the adapter into a hook server:

- an agent moved to the background keeps its own shell live past the parent's turn, and the
  shell settles at its own notification's time;
- the same capture with the spawn result ahead of the move ends the agent once, from its own
  notification, with no second run;
- a roster that omits a background child without its own ending leaves it live;
- a session that ends settles what still runs `unknown` and keeps every record;
- messaging a finished background agent opens its second run, which ends from its own frame;
- interrupts in both captured orders end `cancelled`, and a finished foreground agent keeps
  its summary and usage.

* test(agent-status): hold the store's running indexes and byte total to a rebuild

The copying-store fuzz never reaches the snapshot byte budget, so a drift in
the running byte total (or any index the public reads do not surface) passed
it. After every fuzzed step, including refusals, compare every index with one
rebuilt from the committed maps.
2026-09-25 12:50:50 -07:00
88f2f01061 fix(daemon): escape the terminal daemon into its own systemd scope so a service restart no longer kills every live PTY (#19430)
* fix(daemon): escape the terminal daemon into its own systemd scope so a service restart no longer kills every live PTY

Root cause: daemon-launched-child.ts forks the detached terminal daemon with
detached: true, which escapes the POSIX process group (setsid) but never the
systemd cgroup. Every PTY the daemon owns is itself an undetached direct
child of the daemon (native-pty-spawn.ts). Under a combined systemd unit
(Type=simple, KillMode=mixed, per docs/reference/headless-linux-server.md),
a systemctl restart/stop SIGKILLs every process still in the cgroup at the
stop timeout -- the daemon and every live terminal -- even though the
codebase already has a fully-built adoption/reattachment path for a
surviving daemon (orcad-entry.ts's refreshRestoredOrchestrationAuthority +
reconcileLegacyWorkerTerminals, gated on daemonOwnsFreshPersistentPtys()).
That path never fires today because the daemon never survives long enough.

Fix: when systemd is actually supervising the process and the OS user has a
reachable systemd --user manager (isDurableDaemonScopeSupported(), Linux
only), launch the daemon via systemd-run --user --scope so it lands in a
cgroup that is a sibling of the service unit's cgroup, not a descendant of
it. A systemctl restart of the combined unit then never reaches it. Any
failure of the scoped launch (no reachable bus, D-Bus policy rejection,
etc.) falls back transparently to the existing plain fork() launch, so
every platform/environment without this capability is unaffected.

The daemon self-detects its own resulting cgroup scope via /proc/self/cgroup
(detectOwnCgroupScopeUnit()) rather than trusting the launcher's intent, and
publishes it as cgroupUnit in its pid record and orcad's health/readiness
payload (health.terminalDaemon.cgroupUnit), so a running deployment can be
observed to confirm the fix actually engaged.

No new session registry is added: the existing daemon pid-record + adoption
protocol (publishDaemonPidFile, daemon-pid-record-quarantine.ts's
dead-record reclaim, refreshRestoredOrchestrationAuthority) already
implements durable, crash-safe reattachment for a surviving daemon -- it
was simply never exercised against a full unit restart before now.

Proven via a systemd-in-Docker recovery test: a live PTY session's shell
process, its daemon, and the daemon's cgroup scope were all confirmed
unchanged across a real systemctl restart of a Type=simple/KillMode=mixed
unit, while the main process pid changed (confirming the unit actually
restarted) and the new process's health payload recognized the surviving
daemon as adopted and live. A fresh write into the same PTY post-restart
reached the same running shell. Ordinary terminal create/work/release and
the #18789/#18790 worker-release reap-fix regression tests are unaffected.

Fixes stablyai/orca#19408

* fix(daemon): probe the real per-UID XDG_RUNTIME_DIR before trusting the process's own env

isDurableDaemonScopeSupported()/buildDurableDaemonScopeCommand() trusted the current
process's own XDG_RUNTIME_DIR env var first, falling back to /run/user/<uid> only when
that var was unset entirely. On mtl-02, orca-serve@factory.service's RuntimeDirectory=
hardening directive makes systemd export XDG_RUNTIME_DIR=/run/orca_serve/factory into the
unit's process -- a private scratch dir that shares the env var's name but has nothing to
do with the user session bus. /proc/<pid>/environ on that host confirmed exactly that path
plus DBUS_SESSION_BUS_ADDRESS=disabled:, while the real bus was reachable the whole time at
/run/user/985 (confirmed via systemctl --user is-system-running with that dir exported by
hand). The probe treated the hardened override as authoritative, found no bus socket there,
and reported unsupported on every launch -- so the cgroup-escape fix from #19408/#19430
never actually engaged on real hardware, even though tonight's factory deployment picked it
up.

Fix: resolveUserRuntimeDir() now always tries the conventional /run/user/<uid> path first
(computed independently via getuid(), never trusted from env), checking for a genuinely
connectable bus socket via statSync(...).isSocket() rather than a bare existsSync. It falls
back to the process's own XDG_RUNTIME_DIR only when that canonical path has no reachable
bus -- covering hosts that legitimately have no /run/user/<uid> at all but do have a
working bus wherever their own environment points. buildDurableDaemonScopeCommand() now
explicitly sets XDG_RUNTIME_DIR to whichever path this resolution picked, rather than
inheriting the spread env's (possibly hardened-wrong) value.

Both isDurableDaemonScopeSupported() and buildDurableDaemonScopeCommand() gained an
injectable canonicalRuntimeDir parameter (defaulting to the real computed path) so tests
can exercise the hardened-override scenario deterministically with a real, connectable
AF_UNIX socket fixture instead of the live host's actual runtime directory.

Docker's stock jrei/systemd-ubuntu test container never had this hardening directive, so
this gap was structurally invisible to the container-based verification in #19430 -- only
caught against real mtl-02 hardware.

* fix(daemon): report the daemon's own pid over the ready handshake, not systemd-run's

The launcher used to infer the daemon's identity pid from the immediate
spawned child (`child.pid`). On the durable-scope path that child is
`systemd-run --user --scope`, not the daemon, so the launcher was asserting
an identity it had no authority over.

`DaemonReadyIdentity` now carries a required `pid` populated from
`process.pid` inside the daemon itself, and `daemon-launched-child.ts` takes
`launchedIdentity.pid` from that self-report. Both sides of the
`holdDaemonAdoptionLease` pid comparison therefore originate inside the
daemon process, which is the idiom this branch already uses for cgroup
membership (`detectOwnCgroupScopeUnit` reads `/proc/self/cgroup` rather than
trusting what the launcher intended).

Note on the reported consequence: `systemd-run --scope` registers its *own*
pid on the transient scope unit and then `execvpe()`s the target command --
same pid, no intermediate process -- so adoption did not in fact fail on
systemd >= 206 (verified against systemd 255.4-1ubuntu8.17 and current main,
`src/run/run.c` `start_transient_scope()`). The fix stands on its own merits:
it removes a silent dependency on that exec-vs-fork implementation detail,
which a `systemd-run` shim earlier in PATH or any future systemd change would
have broken with no diagnostic.

`terminateLaunchedDaemonChild` was audited and deliberately left on
`child.pid`: for the same execve-preserves-pid reason that pid is either
still systemd-run mid-scope-setup (killing it correctly aborts the launch) or
already the daemon, so it targets the right process either way.

Regression coverage: `daemon-launched-child-identity.test.ts` pins the
identity source, and `daemon-ready-identity.test.ts` gains pid-validation
cases. Ready-message fixtures across the `daemon-init-*` suites were updated
for the now-mandatory field.

Addresses:
https://github.com/stablyai/orca/pull/19430#discussion_r3953722704
https://github.com/stablyai/orca/pull/19430#discussion_r3954346518

* test(daemon): assert cgroupUnit in the pid-file parse contract

`parseDaemonPidFile` returns `cgroupUnit` on every branch as of the
durable-scope commit on this branch, but five exhaustive `toEqual`
assertions in daemon-health.test.ts still described the pre-scope shape, so
they failed on the branch independently of any later change.

Adds the field to those expectations. Deliberately not relaxed to
`toMatchObject`: asserting the full parsed shape is what makes these tests
catch a field silently dropped from the pid-file contract.

* refactor(daemon): resolve the canonical user runtime dir at one point

The per-UID path cannot change for a live process, so compute it once into a module
const instead of threading the same default call through three signatures, and drop
the try/catch around a getuid() that cannot throw once it exists. Trims the module
prose to the non-obvious facts and corrects the pid-file record comment: an unscoped
daemon writes null; only records no daemon wrote are absent.

* test(daemon): clean up the cgroup-scope fixtures and assert a verdict

The cgroup fixture tracked only the file it wrote, leaking one temp dir per case.
Drains both fixture lists with splice so the pop-may-be-undefined guards go away,
and replaces a not-throw/typeof-boolean pair with the verdict it was circling:
no resolvable runtime dir means unsupported.

* refactor(daemon): share the detached child options across both launch paths

cwd, detached and stdio were repeated in the fork and systemd-run branches, which
left the two comments explaining them hovering over the env block instead. Names
them once so each branch carries only its own delta.

* refactor(daemon): validate the ready pid like every other field

typeof-first narrows the value, so the two 'as number' casts the isSafeInteger check
needed disappear and the pid guard reads like the startedAtMs guard below it.

* fix(daemon): don't retry the launch unscoped after losing the endpoint race

A scoped attempt that lost the endpoint to another daemon was retried unscoped: a
second doomed fork, a misleading 'cgroup-scope launch failed' warning, and the same
DaemonEndpointUnavailableError the caller was already going to adopt on. Rethrows it
instead, since no launch mode can win a race that is already lost.

Also drops a private alias for DaemonChildSpawnOptions and the two 'as number' casts
on child.pid in the startup-failure cleanup.

* fix(daemon): unlink the pid record by the pid the daemon published

The record holds the daemon's self-reported pid, so match on that rather than on the
immediate child's, which is the systemd-run wrapper's until it execs.

* fix(daemon): route the scope launch through the child-process chokepoint

The two files this PR added imported `node:child_process` directly, which
`child-process-import-boundary.test.ts` fails on deterministically: the
offender count went 155 -> 157 against a pin of exactly 155. Raising the pin
or listing the files is what that test explicitly forbids, and the allowlist's
own note says a split "moved the import, it did not add one" -- so the fix is
to get both new files off the module and put the count back at 155.

- `daemon-cgroup-scope.ts`: the `systemd-run --version` probe now uses
  `runProcessSync` instead of `execFileSync`, so it gets the shared spawn
  decisions. Kept synchronous deliberately: `launchDaemonChild` attaches the
  readiness listener in the same tick it is called, and an await before the
  spawn moves the child past that tick. A non-zero exit is data rather than a
  throw here, so the verdict now checks `code === 0 && !timedOut`.
- `daemon-launched-child-spawn.ts`: the scoped launch uses `spawnProcess`, and
  the long-standing unscoped launch keeps `fork` semantics through a new
  `forkProcess`.
- `src/shared/child-process/fork-process.ts`: the fork arm of the chokepoint.
  `spawnProcess` cannot express a Node child with an IPC channel started from
  a module path under an overridden `execPath`, and the existing launch tests
  are written against `fork`'s contract, so a spawn rewrite would have changed
  module resolution, `execPath` and `execArgv` at once. It passes
  `windowsHide: true` -- the flag every other call site in that directory
  sets, reachable via an assertion because `ForkOptions` omits it -- which
  keeps `windows-console-visibility.test.ts` at its pin of 65 too.

Both ratchets pass with both pins and both allowlists untouched.

Docs: `orcad-operations.md` and `headless-linux-server.md` still described the
limitation this PR removes as permanent. Both now describe the durable-scope
survival path and its preconditions (systemd as PID 1, a reachable user bus /
`loginctl enable-linger`, `systemd-run` on PATH), and scope the old text to
the unscoped-fallback case, pointing at `health.terminalDaemon.cgroupUnit` as
the way to tell the two apart on a running host.

* fix(daemon): seal the cgroup capability probe from the host and correct KillMode=mixed docs

The capability probe consulted the host's own /run/systemd/system marker and
spawned the real systemd-run binary, so the hermetic unit tests could only pass
on a systemd host (and fail closed otherwise, even with faked bus sockets).

- Thread systemdBootPath and runVersionProbe as test seams through
  isDurableDaemonScopeSupported, defaulting to the real boot marker and
  systemd-run --version probe in production.
- Narrow the injected probe to the ProcessResult slice it consumes.
- Cover: no-systemd-boot, non-zero probe exit, and probe-timeout cases.
- Correct KillMode=mixed semantics in the docs: the cgroup-wide SIGKILL fires
  the instant the main process exits, not after TimeoutStopSec; document the
  Docker-container caveat and add KillMode=mixed to the multi-service template.

* fix(daemon): satisfy assertion checks in scoped launch

* fix(daemon): satisfy anti-slop and console guards

* test(serve): update shutdown docs assertions for daemon scope

* fix(daemon): migrate adopted legacy scopes

* docs: qualify restart safety by daemon scope

* docs(daemon): qualify Upgrade restart prose with durable scope caveat

Align the Upgrade section in docs/reference/headless-linux-server.md with
the earlier preservation section and docs/reference/orcad-operations.md:
a service restart terminates live processes only when running under the
unscoped fallback, and stops should be treated as destructive unless
health.terminalDaemon.cgroupUnit names an orca-daemon-*.scope.

Update the shutdown workflow test assertion in
config/scripts/headless-serve-shutdown-workflow.test.mjs to match.

* fix(daemon): harden legacy scope migration

---------

Co-authored-by: Lesley Murfin <260182349+LesleyMurfin@users.noreply.github.com>
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-09-21 17:23:30 -07:00
Jinwoo Hong a01027697c feat(session-search): enable indexing on paired servers from a client (#20886)
* feat(session-search): add ranked history panel search and consent

* test: wait for initial session indexing before refreshing results

* feat(session-history): add local search settings and index controls

* Use shared local host identifier for session index status

* feat(session-search): merge all-computers search across hosts

The `all` scope on `aiVault:searchSessions` now fans out from the desktop
to every host the session list enumerates and merges the pages into one.
Legs run in parallel: the local index through the search service, SSH and
runtime hosts through the existing remote search client.

Two fixed orders, because relevance scores from independent indexes are
not comparable. `newest` asks every leg for recency and k-way merges on
`updatedAt`, nulls last, ties broken on execution host id. `relevance`
rotates hosts in host-id order by their own rank.

The merged cursor is an opaque base64url payload holding each host's
cursor, how many of its current page were already emitted, and the
generation that offset counts into, plus the page size and sort the
cursor belongs to. A host whose index moved is fenced to `stale` and
stops contributing; the rest keep paging. Per-host outcomes ride back on
one new optional `hosts` field on the results response.

`aiVault:searchStatus` with `all` stays refused, and neither the runtime
RPC nor the CLI gains the scope, so a fan-out is never two hops.

* fix(preload): let the search bridge address the all-computers scope

* feat(settings): live index status, enable confirm, advanced delete

* feat(session-search): search every computer from the history panel

The panel's "All computers" scope produced no request: the hook parsed the
scope into a single host id and stopped when that was null, so the panel
answered "Choose one computer to search its sessions." The desktop already
merges every enumerated host behind `aiVault:searchSessions`, so pass the
scope straight through and stamp each hit with the host it came back on.

Hosts the merge could not search are named under the results header with a
short reason, since a silent partial answer reads as "no such session".

(cherry picked from commit c6b9179316)

* feat(session-search): enable indexing on paired servers from a client

Adds `aiVault.setSearchEnabled` so a desktop can turn a paired Orca server's
transcript index on or off and have the server apply it without a restart.

The runtime method refuses any caller without a `pairedDeviceId` with a
`forbidden`-class error, writes the whole resolved policy through the runtime
store so retention rides along untouched, then reaches the index through a
host-supplied hook: `applySessionSearchSettingsChange` on the desktop, the
in-process instance's new `apply` on orcad. The relay is unchanged.

Wire compatibility is Rule 1 shaped: a new optional method. A server that
predates it answers method-not-found, which the desktop IPC handler maps to an
error whose message is exactly `host-too-old`. Old clients never call it. The
method is deliberately absent from the mobile allowlist, and `aiVaultSearch`
stays out of the paired settings projection.

(cherry picked from commit 640c715fbd)

* fix(session-search): report a paired server without session search as host-too-old on status reads

(cherry picked from commit 1463e8a4bc)

* fix(settings): let Button and Collapsible own their spacing and type
2026-09-16 12:50:59 -04:00
Brennan Benson 291b4ddd6f feat(agent-status): route structured sessions through canonical ownership (#20718)
* feat(agent-status): route structured status through canonical ownership and fence child lifetimes

Restacked onto the canonical store and child-work contract. Completing that
restack drops the `reopenStructuredParent` mutation flag this change had
carried, along with its contract field, its codec branch, and its single
call site in structured ingest, which passed a hardcoded `true`.

The flag was a narrow escape hatch from the absolute `tombstones.has(...)`
rule that governed parent upserts in this branch's original base. The
canonical store replaces that rule with a revision envelope, because a
bounded store compacts tombstones away and a presence-based guard silently
stops fencing once one is evicted. With the envelope deciding the outcome,
the escape hatch has nothing left to escape from, so removing it changes no
production behaviour.

`agent-status-store-reopen.test.ts` is rewritten against the envelope: the
reopen case now pins that an unflagged republication succeeds while replay
from before the reopen stays fenced even after the parent tombstone is
compacted away, and the second case pins where the guard genuinely bites —
a republication inside the removing mutation itself, for every subject kind.

* fix(agent-status): re-admit unchanged structured owners after teardown

* fix(agent-status): clear anti-slop object-param and Reflect.apply findings

- agent-status-store-byte-budget.ts: type the byte-budget helper's
  record parameter as the union of what its call sites actually pass
  (the snapshot header plus each store entity record) instead of the
  broad `object`.
- server-structured-canonical-status.test.ts: replace `Reflect.apply`
  with a typed, explicitly-bound call that models a caller at an
  untyped boundary omitting the trusted owner subject.

* docs(agent-status): drop the 2A progress doc from docs/reference

docs/reference/ holds implementation detail, not rollout progress. The
canonical-boundary notes move to the effort's working directory; the
agent-status-store status section keeps the boundary statement and loses
the now-dangling link.

* fix(agent-status): mint the canonical epoch on first use, not at construction

The hook server's canonical store was built in an instance-member initializer, so
constructing AgentHookServer — which happens at import time for the module
singleton — demanded a live randomUUID. Any importer that stubs node:crypto threw
'Invalid agent status store epoch' before a single test ran.

The store is now created on first canonical access and reset by dropping it, so
construction owes nothing to a crypto implementation and the epoch still rotates
per authority incarnation.

* fix(agent-status): drop the orphaned snapshot budget and a duplicated pane guard

Two leftovers from the canonical-store routing change:

agent-status-store-snapshot-budget.ts lost its only caller when the store state
switched to agentStatusStoreFitsByteBudget. Nothing in the repo imports it now,
so the module goes with the caller it existed for. The replacement is not a
straight copy: it only memoises a record's measured size once the record is
frozen, so a still-mutable record can no longer return a stale byte count.

persistedStructuredWorkerPaneKeyIsValid repeated its public-pane-key rejection
verbatim three lines below the first one. The tests covering that rejection pass
on the first occurrence alone, so the second decided nothing and only obscured
which predicate was load-bearing.

* fix(agent-status): stop a failed structured publish from latching as owned

Three defects found reviewing the structured routing path.

combinedStatusEntries defaulted a missing listing order to 0, but the counter it
compares against starts at 1, so any unordered row sorted above every ordered
one. Unknown order now sorts last.

The owner map recorded a session as owned before the sink ran. A publish that
threw therefore left matchesLocation reporting an owned location for a row that
was never written, and the unchanged-projection path — the only thing that would
re-offer it — stopped. The address still has to survive a throw so teardown can
forget a row that did land, so the two facts are now separate: the address is
recorded up front, and only a publish that returned marks the row as landed.

The reopen test claimed the revision envelope rather than the tombstone fences a
stale replay. It cannot tell: transport consecutiveness, the parent-revision
validator and the tombstone guard each refuse that replay alone, and ablating any
two leaves the test green. It now asserts the outcome and says so.
2026-09-16 01:20:58 -07:00
Jinwoo Hong eba56f2f69 feat(ai-vault-search): construct the session search indexer in the scanner service behind a setting (#20516)
* feat(ai-vault-search): persist agent-session search consent and retention

Two booleans and nothing else: `enabled` and `historyDays`, off by default
because building the index reads every transcript on the machine. No `paused` --
the PR 3 indexer is immutable, so every change is close-and-construct.

The settings IPC normalizes a write like every other field and hands the change
to the index; there is no UI for it until PR 8.

* feat(ai-vault-search): hold one indexer and engine pair per host

The object that owns a host's live index and the three recipes that change it.
The indexer is immutable, so a settings change is close-and-construct, disabling
is close with no replacement, and clearing is close, remove the database,
construct. The new instance's first sweep purges a narrowed window and admits a
widened one, so neither needs a code path.

The database sits beside the scanner's parse cache, one file per host. A runtime
with no node:sqlite can hold no index at all, which the Node 18 floor on orcad
and the relay makes a real case rather than a hypothetical one.

* feat(ai-vault): let the scanner child own the session search index

The transcript reader runs in that child, so the index consumer has to as well:
one read serves both the session list and the index. Three request operations
(search, status, reconcile) and one fire-and-forget settings message carry
everything a parent needs; main never opens the database file.

The init frame becomes a factory because it is read at every spawn, so a
respawned child sees current consent rather than the first frame's. A child
holding a running index is never idle from the parent's side, so idle retirement
is suppressed while the index is on -- retiring it would stop the reconcile loop
until some later scan happened to respawn one.

Both files this lands in were already at the max-lines ceiling, so three
collaborators move to where they belong rather than being disabled around: the
invalidation deadline into the class that owns invalidations, call cancellation
and the start requeue into the call-state module, and orcad's flag parsing into
its own file.

* feat(ai-vault-search): register a search service on every host that answers

Without a registered service a host answers no-service, which means "this host
does not have the feature" rather than "the index is off". All three hosts now
answer the second thing.

The desktop forwards to the scanner child. orcad and the SSH relay daemon have
no such child -- orcad ships only the watcher and daemon entries, and the relay's
AI Vault sidecar runs the remote scanner, which publishes nothing to the
transcript channel -- so on those two the index lives in the process that would
drive its reads, gated on a runtime that has node:sqlite at all.

The relay registers with consent off and no way to turn it on: nothing carries a
setting to a remote host yet. That is the honest state, and it is still worth
registering, because it is what tells a client the difference between off and
too old.

* test(ai-vault-search): price a warm pass over five thousand transcripts

The number the reconcile interval will be revisited against, measured rather
than argued: a warm sweep stats every file under every root, a warm cycle stats
the newest N per agent, and neither reads what the index already holds. It does
not tune the interval.

* fix(ai-vault-search): answer the casting gate without assertions

main's new type-assertion rule reaches every file this branch touches. All nine
sites drop the cast rather than carry a SAFETY: rationale: the operation guard
narrows with `in`, the sqlite probe narrows the builtin it loads, the child test
keeps the discriminated reply instead of widening it, and the settings resolver
takes `unknown` -- which is what it really reads, since a persisted profile can
hold a value no version of this code wrote.

* fix(ai-vault-search): let a refreshed scan root reach the live index

The parent re-resolves scan roots before every policy push, precisely so a
WSL distro or extra Codex home that appeared since the child spawned enters
the window. The child forwarded only the settings to a live instance and used
the roots solely in its `??=` initializer, so those roots were dropped for the
child's lifetime.

The indexer stays immutable: a structurally different root set closes the pair
and constructs a new one, the same way a changed databasePath already does.
Compare via `sameSessionSearchRoots` rather than a plain JSON compare, because
nothing fixes the key order two producers write; lists are sorted too, since
the indexer walks every root and a re-enumeration that reorders is not a
change. An unchanged set still never restarts a running index.

The orcad and relay in-process hosts resolve roots once at install and never
re-apply, so they have no such seam.

* fix(ai-vault): restart the scanner child the index is holding

Three review items.

The hold keeps a child alive for the index, but only a queued call ever
started one: `pump()` skipped a hold with an empty queue, so an idle indexing
child that crashed, or an `ensureChild()` that failed at start, left indexing
stopped until an unrelated request happened to arrive. `pump()` now starts the
child the hold requires, which is also the restart callback the fault policy
already schedules, so the existing delay and circuit bound the retry exactly as
they bound a queued call's start. `updateSessionSearch` goes through the same
seam instead of its own `ensureChild` call.

A search registers no AbortController, so a cancel sent for a search id was
added to the `cancelled` set and never consumed. Nothing can reach that today
-- no caller passes a signal and the child answers in milliseconds -- so this
is only a leak of ids: consume it when the search settles.

The orcad argument doc claimed a `--`-prefixed value stays a flag. The parser
takes the next token regardless, and orcad-launch-contract.test.ts pins that,
so the doc is what was wrong. Behaviour is unchanged.

* fix(ai-vault): recover search indexing and refresh scan roots

* fix(ai-vault): defer search refresh policy reads

* fix(session-search): stabilize paging and host enablement

* fix(session-search): refresh host roots within full sweeps

* docs(session-search): clarify initial root fallback
2026-09-14 13:38:37 -04:00
OrcaWinandOrca Worker e75b46904f perf: avoid rescanning partial browser sidecar responses (#20242)
* perf: avoid rescanning partial browser sidecar responses

* test: guard sidecar response framing scan cost

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
2026-09-12 18:04:24 -07:00
Brennan BensonandMerge Sim da5d555259 refactor(agent-status): delete the runtime's retained row store (PR 1b) (#19785)
* docs(agent-status): plan PR 1b at file level

Names the five RuntimeAgentRowStore call sites and what each becomes, why
terminalHandle has to be stamped before the store can go, and the one
intended behavior change.

* feat(agent-status): stamp the pane terminal handle on hook-server rows

The runtime's retained row store carried the pty binding two readers need. Put
that fact on the row that already owns the pane instead, resolved through the
same lookup the renderer-facing IPC boundary runs, so the two surfaces cannot
disagree about which terminal a pane is.

Carried forward when a later write resolves no handle (only main's OSC parse
can), and never persisted: a handle belongs to the runtime that issued it.

* refactor(agent-status): route the session-tabs republish off the store

`retain()` was not only a duplicate store: its boolean return was the signal
that republished `session.tabs` for a status-only transition, which no title
change covers (#7970). `hook-status-session-tabs-invalidation.ts` already
mirrors that change set plus hook restore provenance, so route the signal off
the store rather than keep a second comparator.

Adds the status-drop arm a user dismissal emits, which the pane-clear fan-out
deliberately skips — now load-bearing, because a dismissed row leaves the
listing at once.

Installed on both hosts. orcad had neither the OSC producer nor this signal, so
its runtime observed agent status and published it nowhere; deleting the
retained copy without wiring it would list no PTY agents there at all.

* refactor(agent-status): delete the runtime's duplicate retained row store

`RuntimeAgentRowStore` held the same payload the hook server already holds, so
the same pane could legitimately read differently in the sidebar, in
`worktree ps`, and on the phone. Both of its readers move onto the store's
snapshot in `runtime-hook-agent-row-selection.ts`, and
`collectRuntimeWorktreePtyAgentSources` loses the retained-versus-hook
reconciliation that only existed because two stores could disagree.

`ConnectedPtyEvidence` trades its flat pty-id set for `ptyIdByTerminalHandle`,
which is how a row still resolves the connected PTY behind it — the
working-terminal rollup's match key, and the last rescue for a row whose pane
binding a controller incarnation nulled under it.

The one intended behavior change: a row the user dismisses on the desktop
leaves `worktree ps` and mobile at once instead of lingering until the pty
exits. One store means one dismissal.

The suites written against the retained store are rewired to a real
AgentHookServer rather than deleted, so each still asserts the listing
behavior it named.

* docs(agent-status): record what PR 1b landed

Past tense, plus two corrections to the plan: `terminalHandle` is not the pty
id (they are different identifiers, and the explicit-status reader was already
comparing against a real handle), and the legacy numeric pane key is a
consequence the plan did not name.

* fix(agent-status): harden single-store lifecycle

* fix(agent-status): preserve mobile terminal rejoin

* fix(agent-status): preserve unverifiable remote rows

* fix(agent-status): own PTY row lifecycle in hook server

* fix(agent-status): preserve state and renew freshness

* fix(agent-status): ignore freshness for dismissed identity rows

* fix(agent-status): fence orcad observed identities

* fix(orcad): always release daemon adapter on cleanup

* fix(agent-status): cover remint and headless lifecycle edges

* fix agent status identity recovery gaps

* fix(agent-status): suppress duplicate child-only row mutation

* test(runtime): preserve hook store wiring in transcript harness

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-11 15:28:16 -07:00
Jinwoo Hong 74cc9b5039 feat(desktop): native mobile push integration (2/3) (#19935)
* feat(desktop): integrate native mobile push delivery and lifecycle

* fix(desktop): preserve notification replay policy and review invariants

* fix(desktop): correct notification locale namespace and auto-ack tests
2026-09-10 20:16:56 -04:00
Brennan BensonandMerge Sim ebb1acfa37 refactor(agent-status): publish structured sessions into the hook server store (#19683)
* refactor(agent-status): publish structured sessions into the hook server store

Structured (native chat) sessions have no PTY and no hook script, so their
status never reached the hook server's store; #19217 gave `worktree ps` its
own adapter over the structured feed instead. The feed now writes every
projection into that store through a status sink the runtime wires, drops
the row when the host closes the session, and `worktree ps` reads the one
snapshot like every other agent.

Rows carry a `structuredHost` marker and the journal clock; they are never
persisted to last-status.json, and the main process does not forward them
to the renderer yet, whose feed bridge still owns them until it is retired.

Design and the two follow-ups: docs/reference/agent-status-store.md.

* chore: drop stray @pnpm/exe lockfile entry

An unrelated local pnpm run added @pnpm/exe as a packageManagerDependency
with no package.json change, so CI's --frozen-lockfile install failed
before any job ran.

* docs(agent-status): describe the step that actually landed

The design record claimed PR 1 deletes RuntimeAgentRowStore, drops the
retained-versus-hook reconciliation, stamps terminalHandle on OSC rows, and
tags rows with a source field of 'structured-host'. None of that is true of
the shipped code: the retained store and its reconciliation are still in
place, and the row field is structuredHost: 'held' | 'owned'.

AGENTS.md points every future contributor here before they touch agent
status, so split the roadmap into the 1a that landed and the 1b that has not,
and name the fields the code actually writes.

* fix(agent-status): pair session removal with the status-row forget

A session dropped from the host's map without an explicit forget left its row
in the store forever: `structuredHostOwned` bypasses the staleness check, so a
failed re-attach (the Claude rewind path reaches one) stranded a permanently
working agent in `worktree ps` and on mobile with no UI able to clear it.
Deletion and forget are now one operation both callers route through.

* fix(agent-status): give orcad the store worktree ps reads from

`orcad` constructed its runtime with neither `getAgentStatusSnapshot` nor
`structuredAgentStatusSink`, so once `worktree ps` sourced rows only from that
snapshot the headless host published nowhere and listed nothing. The hook
server's store is a module singleton whose import tree never reaches Electron,
and its file paths come from `start()`, which orcad never calls.

* fix(agent-status): drop a structured row without a renderer clear

`dropStructuredStatus` went through `clearPaneState`, which fans a pane clear
out to the renderer for a pane key the renderer's own feed bridge still writes
- so 'exactly one writer per pane key' held for writes and not for deletes.
`dropStatusEntry` routes through the status-drop tap instead, and skips the
resume-identity remnant: a structured session has no pane to resume into, and
every null-status publish would otherwise re-mint one.

* test(agent-status): pin both half-migration structured-row filters

Neither the `agentStatus:getSnapshot` filter nor the main-window listener's had
a single assertion, so deleting either — the first step of PR 2 — was green
everywhere. Also covers the perf skip and the drop's lack of a renderer clear.

* docs(agent-status): correct three statements this PR made false

The sink JSDoc claimed only tests construct a host without one; `orcad` did.
The doc argued a structured row needs no tab mirror 'because headless serve has
no renderer', reasoning about exactly the topology the wiring had not reached.
The deleted runtime adapter's warning that the pane key must be the DERIVED one
- never a bearer handle or minted worker key - was lost with it.

* test(agent-status): declare orcad in the hook-row producer census

Wiring the hook store into the orcad runtime added a production site that
hands hook rows to a consumer, which the census ratchet pins deliberately.

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-10 11:27:27 -07:00
Neil 34999e328e fix(orcad): stop demanding a spawn-helper only macOS builds (#18122)
node-pty declares the spawn-helper target inside binding.gyp's OS=="mac"
block and pty.cc execs it only under __APPLE__. Asserting it on
`!== 'win32'` made every Linux orcad boot degraded with
spawn_helper_missing while its terminals worked fine.

Route all four sites through one shared `usesNodePtySpawnHelper`
predicate: the precondition verdict, the prebuilt slot install, the
+x repair, and the prebuilds build script (which threw outright on a
Linux slot build).

Fixes #17844
2026-09-02 02:49:09 -07:00
Neil b552bcb91f fix(relay): diagnose why node-pty will not load instead of hedging (#17891)
The relay could only say "terminals are unavailable" and then list three
remedies for four different faults, none of which the user could verify
(#17830). Two things were destroying the evidence:

- `loadPtyUncached` caught the load error into bare `catch {}` blocks
  (pty-handler.ts:539, :551) and returned null. The only cause anyone had
  was discarded on the spot.
- node-pty's own loader walks three directories and rethrows only the LAST
  failure, so even an uncaught error arrives as `Cannot find module
  '../prebuilds/...'` — the GLIBC/ABI/arch sentence is already gone.

The relay now keeps the load error, recovers the real dlopen message with an
out-of-process load of the file node-pty would have opened, reads what
node-gyp configured the binding for (`build/config.gypi`), captures the
host's Node ABI, arch and glibc, and probes the toolchain only when nothing
was compiled. Each fault gets its own message naming values the user can
check: toolchain_missing, dependency_missing, abi_mismatch, arch_mismatch,
libc_floor, shared_library_missing, load_crashed, and load_failed which
quotes the loader verbatim. A probe that did not answer stays `unverifiable`
and prescribes nothing.

The classification is now also structured data on the error, so a client can
repair the host instead of printing a paragraph: an additive, schema-validated
`data` field on an existing JSON-RPC error, with `repairable` true only for a
proved fault that recompiling on the host actually fixes.

Reuses orcad's loader-message parsers and out-of-process probe rather than
adding a second copy; `classifyLoaderMessage` moves to a shared module and
gains architecture and missing-shared-library cases, which the orcad boot
precondition picks up too.
2026-09-01 22:38:07 -07:00
Brennan BensonandMerge Sim b8d5b0486e Fix opening HTTPS URLs from headless runtimes (#17467)
* fix(runtime): open headless browser URLs on paired client

* fix(pty): tolerate runtimes without browser relay probe

* fix(browser): require automation-capable client host

* fix(browser): map client URL opener in sidecar

---------

Co-authored-by: Merge Sim <sim@local>
2026-08-30 16:48:43 -07:00
NeilandOrcaWin 3457acb647 fix(memory): hydrate retained PTYs before diagnostics (#17308)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-29 19:42:56 -07:00
Jinjing c4b39295c1 style: format codebase (#16935)
* style: format codebase

* style: format codebase

* refactor: extract skill install dialog footer and content

Extract footer and content sections from SkillInstallDialog and
SkillInstallManagementDialog into separate components for improved
maintainability and clarity of component responsibilities.
2026-08-28 00:59:21 -07:00
Neil 5631aa00dd feat(orcad): items 2–7 — degradation, natives, daemon, ops, deploy (#16398)
* fix(ports): stop joining an undefined resourcesPath on a non-Electron host

`resolveWorkerEntryPath` branched on `isPackaged` alone and joined
`process.resourcesPath`. orcad reports `isPackaged` true — correctly, it is a
production build, and ~15 consumers read it that way to gate HTTPS-only skill
downloads and the real CLI name — but `process.resourcesPath` is Electron-only
and `undefined` under plain Node.

So the packaged branch threw
`TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string`
where a clean "worker unavailable" was the honest outcome. The type said
`resourcesPath: string`, which is how it went unnoticed; it is now
`string | undefined`, so the compiler carries the fact.

A host with no Electron resources tree has no asar to look in, so it falls back
to the module directory and lets the caller report a missing worker.

Found by the item 1 agent while auditing the same `isPackaged` defect class in
the watcher. Verified in both directions: reverting the guard reproduces the
TypeError.

* feat(orcad): prove node-pty loads before anything requires it

Of the two ways node-pty fails, only one is catchable. A missing module throws
MODULE_NOT_FOUND. A module built against the wrong libc or Node ABI is refused by
the dynamic loader, and in the worst case takes the process down before any handler
exists — that is #9902, which crashed the desktop app on Ubuntu 20.04 before a
window appeared. There was no libc or ABI precondition anywhere in the tree.

So orcad now proves the load in a CHILD process, from main.ts, before anything
requires node-pty. Whatever the child does — throw, abort, die on a signal — is data
rather than our own death, and the operator gets a sentence naming the host's libc,
Node ABI and prebuild slot plus the command to run. Proven-unloadable exits 78
(EX_CONFIG), so a supervisor does not restart an unequippable host forever. A probe
that never answered is unverifiable, not blocked: refusing to boot on an inconclusive
signal would take down hosts that work.

The child dlopens the file node-pty would have chosen, before requiring the package.
node-pty's loader walks several directories and rethrows only the LAST error, so a
refused binary reads as "Cannot find module ./prebuilds/..." — which sends the
operator to install a module that is already there. It also reports through stdout:
node echoes the whole -e source above a stack trace, and matching tokens against
stderr made the probe's own source text answer for the verdict.

Verdicts reach clients as a terminal_unavailable degradation alongside the existing
browser_unavailable one, through the same cause-registry shape. degradations[].code
is now an open vocabulary; clients already render only `message`.

Prebuilds are compiled from PATCHED sources — the patch IS the glibc-floor fix, so an
upstream tarball reproduces #9902 — into linux-{x64,arm64}-{glibc,musl} and
darwin-{x64,arm64} slots. libc is in the slot name because node-pty's loader falls
back to prebuilds/<platform>-<arch> and cannot tell glibc from musl. orcad installs
the matching slot at boot, so a host with no compiler serves terminals.

The relay's five pure toolchain-diagnosis functions moved to a transport-free module
so the Node bundle can reuse them without dragging ssh2 in behind them; the relay
keeps its API by re-export. macOS gets `xcode-select --install` rather than the
cross-distro apt/dnf/pacman/apk menu, every line of which is wrong there.

* test(orcad): pin the node-pty precondition to ground truth, not a prepared host

CI's test shard runs `vitest` directly, so `ensure-native-runtime --runtime=node`
never prepares node-pty for the Node ABI — `degraded` is the correct verdict
there, and asserting 'ok' encoded an environment the shard does not have.

Asserting whatever it returned would be vacuous, so the expectation is now
derived from an independent require() of node-pty. Verified it still bites:
forcing the precondition to always report 'ok' fails the suite.

* feat(orcad): run the terminal daemon, and the ops contract around it

orcad declared `canRecoverPersistentLocalPtys: () => false` because it did not
run the terminal daemon, so every restart, update and rollback SIGKILLed every
running terminal — on the host whose selling point is that work survives the
client going away. That is the one property `ssh-execution-boundary.md`
recommends the peer model for.

Item 4 — the daemon:

- Port the launch path off electron: `daemon-init.ts`,
  `daemon-host-relocation.ts` and `observability/logs-directory.ts` now read
  the `AppEnvironment` port. Relocation additionally asks whether the app root
  is an asar archive rather than whether the build is packaged, so a Node host
  answering `isPackaged() === true` no longer walks into an Electron-only
  NSIS-escape path (same precedent as `parcel-watcher-entry-path.ts`).
- `build-orcad.mjs` emits `daemon-entry.js` beside `orcad.js`, scans the
  forked children's metafiles for electron/node:sqlite, and load-checks the
  child under plain Node.
- orcad spawns and adopts the daemon; shutdown disconnects and never kills it.
  `canRecoverPersistentLocalPtys` now reads the live provider and is false
  under degraded routing, where fresh terminals would die with the process.

Item 3 — the ops contract (docs/reference/orcad-operations.md):

- Bind policy: `--bind`, default loopback, pinned so neither `orca serve`'s
  wide default nor the connected-device widen can override it, and so a paired
  client cannot rebind the listener from outside.
- Instance lock on the data root before profile load, scoped to the runtime
  role so it never refuses a restart that a live daemon makes worthwhile.
- Supervision: exit codes a supervisor can act on (78 = do not retry),
  second-signal escalation, a shutdown deadline, and crash-loop containment on
  daemon respawn.
- Health in the readiness payload: build hash, Node ABI, and a PTY self-test
  that spans both processes — the daemon spawns a real PTY in its own process
  and the verdict crosses its socket.

Both bundle load-checks now assert on exit codes: these bundles are minified
onto one line, so Node's uncaught-exception report echoes every string literal
in the bundle and the previous message match passed against a bundle that
never loaded.

* feat(orcad): deploy, activate and roll back a versioned orcad install

Plan items 6 and 7 from docs/design/shipping-orcad.html.

Install reuses the relay's transaction verbatim — per-version lock, staged
SFTP write, .install-complete sentinel, stale-lock recovery — under a
parameterized namespace, so orcad-<v>/ sits beside relay-<v>/ permanently
(§06). Parameterizing GC is the trap that creates: each model now collects
only its own directories, enforced twice (prefix-scoped remote listing plus
a local ownership re-check), and a client picks its model from how the host
is registered, never from what it finds on disk.

Activation is separate from installation, because a versioned directory
selects nothing. A candidate is launched, publishes orca_server_ready, and
only becomes active if its cross-process health payload passes: right build
hash, listening, daemon live, PTY self-test green. A rejected candidate is
stopped and the incumbent restarted, so a careful deploy cannot cause the
outage it was being careful about.

Update and rollback are shaped by the daemon. An update restarts orcad, the
daemon outlives it, and the surviving daemon was forked from the outgoing
bundle — so live terminals defer the update rather than proceed, and GC pins
the active version, the rollback target and the live daemon's bundle. Orca's
persisted state carries no schema version, so rollback restores a
pre-activation snapshot rather than trusting backward-readability; the point
past which it is unsafe is the first terminal created after activation,
which the snapshot cannot describe and the surviving daemon still owns.

Running the generated shell for real found two bugs the text assertions
missed: tar members re-quoted inside a shell variable captured nothing, and
kill -0 reports a zombie as alive.

* test(orcad): assert the precondition is self-consistent, not environment-shaped

The real-host case cannot predict a status: CI's shard runs vitest directly, so
node-pty is never built for the Node ABI and 'degraded' is correct there, while a
prepared checkout gives 'ok'.

The previous attempt used require('node-pty') as ground truth, which resolves the
JS wrapper while the native binding loads lazily — it proved strictly less than
the precondition checks, and failed CI for exactly that reason.

What is invariant on a host with node-pty installed: never 'blocked', and never a
degraded verdict carrying an unestablished reason. The injected-input tests keep
the logic coverage.

* fix(orcad): drop an eslint-disable the rule no longer needs

* test(orcad): separate slot placement from the load verdict

Both remaining CI failures were the same shape: tests reaching into node_modules
for a pty.node that only exists after `ensure-native-runtime --runtime=node`,
which CI's shard never runs because it invokes vitest directly.

Slot *placement* is the logic worth checking on every host, so it now uses a
synthetic payload and asserts the verdict stays honest about not loading. The
three assertions that genuinely need a Node-ABI binding are gated on it existing.

Verified: breaking slot installation fails both placement tests; with the real
pty.node hidden the file is 17 passed / 3 skipped instead of ENOENT.

* test(orcad): gate the load-dependent cases on a real load, not on the file existing

CI ships a pty.node built for Electron's ABI, so existsSync was true while require
still failed — the gate ran exactly the tests that host can never satisfy. It now
probes the binding in a child process, so a bad one cannot take the runner down.

The self-consistency assertion also allowed too little: 'blocked' is the honest
verdict for a corrupt binding, alongside 'ok' on a prepared host and 'degraded' on
an unprepared one. What stays invariant is that anything other than 'ok' names an
established cause, so a terminal is never declined for a reason nobody worked out.

Verified against all three host states: prepared (19 passed), unprepared, and a
corrupt binding (17 passed / 3 skipped, no failures).

* test(orcad): gate on the whole premise — binding AND spawn-helper

CI has a loadable pty.node but no spawn-helper, and a slot without the helper is
legitimately 'degraded'. So the previous gate let a test run whose premise ('a
complete slot yields ok') that host cannot satisfy.

Verified in both states: with the helper present 19 pass; with it removed the
load-dependent cases skip (17 passed / 3 skipped) instead of failing.

* fix(orcad): preserve degradation types after rebase
2026-08-27 00:18:51 -07:00
Neil 88ecc739e3 fix(browser): bound the agent-browser daemon's life instead of hoping teardown runs (#16367) (#16588)
* fix(browser): bound the agent-browser daemon lifetime (#16367)

`agent-browser` is a client/daemon CLI. Orca only ever spawns the short-lived
client; that client forks a daemon Orca holds no handle on, which reparents to
pid 1 immediately. Nothing in Orca reclaimed it, so a crashed or SIGKILL'd run
left one daemon per browser tab alive forever — two of them at ~25.6 GiB and
~7.2 GiB RSS saturated a 64 GiB cgroup under headless `orca serve`.

Three fixes, in order of how much they cover:

1. Set `AGENT_BROWSER_IDLE_TIMEOUT_MS` on both spawn paths (the bundled-binary
   bridge and the orcad external-Chromium provider). This is the only bound
   that survives every way Orca can die, including SIGKILL, where no teardown
   code ever runs. 10 minutes: >6x the bridge's 90s `EXEC_TIMEOUT_MS`, so it
   can never cut a command, a retry chain, or an ordinary gap between two user
   commands, while capping an abandoned daemon at minutes instead of days.
   Verified against agent-browser 0.27.0: an idle daemon exits and takes its
   Chromium tree and socket sidecar files with it, and a daemon attached over
   `--cdp` (the bridge's case) leaves the attached browser running, so an Orca
   tab is never closed by its daemon idling out.

2. Await `destroyAllSessions()` in the will-quit teardown barrier. It was
   fire-and-forget and the only browser member missing from
   `settleTeardownWithinDeadline`; each session's close is its own
   agent-browser child taking hundreds of ms, so `app.quit()` won.

3. Sweep daemons a previous run left behind, using agent-browser's own
   `session list` / `close` rather than a pid walk (see `windows-pty-job.ts`
   for why walking your own orphans is guesswork). `closeStaleAgentBrowserSession`
   only ever reset the one name a new tab was about to reuse. The sweep runs
   only when `AGENT_BROWSER_SOCKET_DIR` is set, because that private
   per-profile directory is what proves the enumeration can only see this
   Orca profile's daemons; it is never set on Windows, so Windows gets no
   enumeration rather than a machine-wide sweep that could close a daemon Orca
   does not own. Windows stays bounded by the idle timeout, which needs no
   ownership proof. orcad's session name is stable across runs, so it closes
   that one name at start instead — a killed orcad's daemon would otherwise be
   reused while still holding the previous run's Chromium on a dead serve port.

Where the 25 GiB went is inference from code, not a measurement: `captureStart`
sets `activeCapture` and only an explicit `captureStop` ends it, so a HAR
capture in a daemon living for days is unbounded. Not claimed as proven; the
idle bound caps it either way.

Not re-landed: the queue bounds from #10179 (reverted by #10255) bound Orca's
own main-process heap, not the daemon's RSS, so they do not address this report.

Also true but left alone: the 3-strike breaker's `destroySession` is an
unawaited call whose `close` is `catch {}`-swallowed, and it only fires while a
command is in flight — an idle-but-bloated daemon is never noticed. The idle
timeout now bounds that case. `getOffscreenBrowserBackend()?.destroyAll?.()` is
declared `void` and fully synchronous, so unlike `destroyAllSessions` it has no
promise to lose and needs no barrier entry.

* fix(browser): scope the daemon idle bound and close every daemon Orca owns

Review follow-ups on the agent-browser orphan fix.

- Never idle-bound the orcad external-Chromium daemon: it owns the user's
  remote browser, so the 10-minute bound closed a live session and every tab
  in it. Per-tab helper daemons keep the bound; the stable session name plus
  the `close` in start() is what reclaims a killed orcad's Chromium tree.
- Retire a page's daemon from the headless offscreen backend, which is the
  only place `orca serve` closes a page and never reached the bridge. Credit
  to @Jinwoo-H (#16564) for identifying this owner-boundary gap.
- Bound the teardown close at 5s so the will-quit barrier member cannot
  inherit the 90s exec timeout, and close sessions still being created.
- Gate the startup sweep on a socket directory Orca derived itself; an
  inherited AGENT_BROWSER_SOCKET_DIR is no proof of per-profile ownership.
- Replay a session's network routes when the daemon idled out between two
  commands, instead of silently serving unstubbed requests.

* fix(browser): give the orphan sweep a kill switch

Of the three behaviours this PR adds, two are already recoverable in the field
without a build: the idle bound is an env passthrough an operator can raise, and
the quit close is bounded by its own timeout inside the teardown deadline. The
startup sweep was the exception — it fires unconditionally, and if it closes a
daemon it should not, or spawns one process per stale name on a profile holding
hundreds, the only remedy was a revert.

ORCA_DISABLE_AGENT_BROWSER_SWEEP=1 turns it off, matching the existing
ORCA_DISABLE_CODEX_TRUST_RPC / ORCA_DISABLE_HTTP2 convention.

Note for anyone reaching for it on macOS: a Finder-launched Orca does not see
shell env, so it needs launchctl setenv or a terminal launch.

* fix(orcad): reuse a surviving browser session instead of closing the user's

start() closed the daemon before every open, killing the Chromium tree with it.
That runs on every provider start, not just after a crash — so an `orca serve`
restart took the remote user's browser and every tab in it.

The justification was borrowed from the pane bridge, which passes --cdp and so
really does hold a port that dies with its Orca. This session passes only
--session and --profile: nothing binds it to the old process, and the daemon
owns its Chromium independently. A survivor is reusable as-is.

start() now probes for an active tab first and returns it untouched. Only a name
that answers nothing gets closed and reopened — which is still the killed-orcad
case the stable session name exists to recover.

This matters more as orcad becomes the backend the remote host runs on: the
browser it manages belongs to a user, not to the process that happens to be
driving it this minute. It is also the same principle that already exempts this
path from AGENT_BROWSER_IDLE_TIMEOUT_MS.
2026-08-26 16:49:31 -07:00
Jinjing cda2280d63 Show all automations (#16532)
* Add all-host automations with scoped ownership and multi-authority suppo

Enable automations to run on multiple hosts (SSH targets and local) with
owner-fenced mutations, scoped list queries per host, and conflict
resolution. Introduces desktop and runtime authorities as distinct
automation storage owners, with per-host caching, invalidation, and
retry scheduling on the renderer. Captures registration generations for
SSH hosts to survive re-adoption. Adds CLI support for destination
selection and conflict recovery.

* Filter automation create projects by destination host

Only offer projects available on the selected destination, preventing
the mismatches that would fail at submit time. Auto-adjust the project
selection if it becomes unavailable when the destination changes.

* Add runtime storage authority support for automations

- Support both runtime and desktop as automation storage authorities
- Make owner preconditions optional for legacy-client compatibility
- Cache automation list projections to improve performance
- Add per-row repo/worktree resolution for cross-authority collisions
- Extend automation.list RPC to always include owner metadata

* Replace child_process.execFile with runProcess for external automations

- Migrate external-manager to use cross-platform runProcess wrapper per child-process safety policy
- Abstract electron app/ipcMain APIs in orca-runtime via environment accessors
- Install fake app environment in automation tests for consistent setup
- Reorganize imports to use specific module paths (ssh-target-registry, agent-detection, browser-error)
- Remove external-manager from child-process import allowlists (no longer violates direct import)

* Unify desktop automation CRUD onto the local runtime RPC surface

The desktop authority now speaks the same automation.* RPC contract as
remote runtimes, via callRuntimeRpc({kind:'local'}) -> runtime:call ->
the shared RpcDispatcher. The automations:list/listRuns/create/update/
delete/runNow IPC arms, their preload members, and every renderer
desktop-vs-runtime transport fork are retired; the runtime methods are
the single implementation of scoped lists, owner fencing, and change
publication for both transports (mobile clients already exercised them).

The desktop probe scheduler's priority lease survives the move as an
AutomationService hook the IPC registration installs and the runtime
methods take, so Orca's own automation traffic still parks queued
external-manager probes.

External-manager scope arms and dispatch-loop plumbing stay on IPC by
design; automation change events keep their existing channels (renderer
ingestion already converges them by authority).

* Remove automation ghost SSH tombstone scanning

This functionality for synthesizing tombstones for automation-referenced SSH
targets is no longer needed as part of the automation system refactoring.

* Refuse orphan automations at dispatch time, not migration time

Remove migration-time disabling of orphan automations and the `enabledDecidedBy` field. Dispatch now refuses orphans at runtime instead, simplifying state management and UI. Orphans are left unstamped and enabled; dispatch refuses to run them via `resolveAutomationRunTarget`.

* Show all automations in flat table with unified filter menu

- Replace host picker component with comprehensive Filters menu supporting status, last run, agent, and host filters
- Flatten automation list layout to single table instead of host-grouped sections
- Add Host column to display execution host for each automation
- Display active filters as removable pills below toolbar
- Delete unused AutomationHostPicker* components

* Add automation owner fencing and destination validation

- New AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY for owner preconditions; legacy clients get owner metadata snapshotted at RPC boundary for compatibility
- Editor captures and revalidates automation destination before save, preventing silent retargeting if SSH infrastructure changes mid-edit
- SSH target types now isolate renderer-authored fields; generation is server-owned and stripped by IPC handlers

* Route automation recovery actions to the origin host

When an automation action fails due to owner fencing, recovery verbs
("Update server", "Reconnect") must run on the host where the refusal
originated: the row's captured owner for row operations, or the
destination the create dialog captured, not the list's filtered host.

* Remove external manager scope limitation notices

Consolidate create destination eligibility checks with a unified predicate
and fix the bug where desktop repo IDs could be sent to runtime hosts where
they cannot resolve.

* Persist only store-derived automation contexts, not client-perspective o

Store contexts must never be based on client-provided runContext or sourceContext
values—clients speak a different perspective (e.g., 'runtime:<id>' for host IDs
they assign), and persisting those makes the store projection orphan automations
it actually owns. Derived contexts now take precedence in create and update paths,
with explicit null still honored to clear a value. Tests verify this by simulating
drift after storage and confirming that moves re-derive while toggles preserve.
2026-08-26 09:50:12 -07:00
Jinwoo HongandJinwoo-H a9781a4118 STA-4150: client-hosted remote browser (consolidated) (#15448)
Co-authored-by: Jinwoo-H <jinwoo@stably.ai>
2026-08-25 15:36:51 -07:00
Neil 20c1a61401 fix(orcad): answer host paths honestly and ship the watcher child (#16369)
orcad's AppEnvironment implemented three of seven AppPathNames and returned the
userData directory for the rest — including 'exe', where a data directory is not
an executable. Every name now has a Node answer: 'appData' is the platform's
per-user application-data root, 'logs' lives inside the data root so a headless
deployment stays one removable directory, 'downloads' honours XDG_DOWNLOAD_DIR,
and 'exe' is the Node binary. getAppPath() is the directory orcad was launched
from rather than cwd, so children resolve against the bundle instead of wherever
the supervisor happened to be.

The watcher child was the load-bearing consequence: resolveWatcherProcessEntryPath
probed for the adjacent entry only when !isPackaged, so orcad resolved a desktop
out/main path that no deployment has — and build-orcad never emitted the child
anyway. isPackaged stays true (consumers read it as "production, not a dev
checkout" and it gates HTTPS-only skill downloads); the resolver now asks whether
the app root is an asar archive, which is the question it actually meant. The
child ships beside orcad.js, and the build forks it to prove it runs.
2026-08-24 23:01:17 -07:00
Neil e217fdd10f build(orcad): gate orcad's own graph, and prove it loads under plain Node (#16368)
* fix(orcad): close the browser-provider gaps

The providers landed without enforced coverage, so a regression in either path
would have landed silently.

- CI: the external-Chromium integration test was gated on ORCA_BROWSER_EXECUTABLE
  and nothing ever set it, so it skipped forever. It now runs in its own job
  against the runner's Chrome and FAILS when Chrome is absent rather than
  skipping, because an unset variable is exactly how it went uncovered. Timeout
  raised to 120s: a warm run is ~7s but the first launch against an unseeded
  profile took 30s and hit Vitest's default, and CI is always that cold case.
- Electron provider had no test at all. It is the path anyone with the desktop
  app hits.
- Browser unavailability reported one message for four causes, including telling
  an operator to set a variable they had already set.

Fixes a live defect found while covering it: the runtime advertises
browser.tabCreate.known-id.v1 unconditionally, so a web client sends a
provisional page id for a page that does not exist yet — and the sidecar's
generic requestedPageId branch ran require() on it first and threw. Every
known-id create against the Electron provider failed. The adoption logic was
already there; only the ordering was wrong.

Also updates the workflow-parallelism guard, which correctly caught the new job
missing from verify's required-check list, and asserts verify actually reads it.

* build(orcad): gate orcad's own graph, and prove it loads under plain Node

Two gaps the artifact's own comment asked for.

The ratchet measured only orca-runtime + runtime-rpc, but orcad imports ipc/pty
directly to install the PTY controller, so its graph is strictly larger. The gate
could read zero while the shipped artifact regressed. orcad's entry is now a
ratchet entry point, and the baseline stays empty with it included.

orcad cannot join plain-node-entry-guard — that is a rollup plugin keyed on
electron-vite input names, and orcad is an esbuild artifact. But the half that
matters here is the guard's smoke-load: scanning the metafile proves no module
NAMES electron, not that the graph resolves under plain Node. A dynamic require,
a missing native or a top-level throw all pass the scan and fail at runtime.
build-orcad now runs the bundle with a bogus flag and requires the argv rejection
that only a fully loaded graph can produce.

Verified: a bundle that builds but throws on load fails the gate.
2026-08-24 22:21:26 -07:00
Neil 09048c63d4 feat(orcad): add headless browser providers (#16193)
* feat(orcad): add headless browser providers

* fix(orcad): merge the duplicate runtime-browser type import
2026-08-24 21:11:45 -07:00
Neil 0bfd3808f6 test(pty): pin the renderer-liveness guard STA-2373 relies on (STA-5373) (#16345)
* test(pty): pin the renderer-liveness guard STA-2373 relies on (STA-5373)

STA-5373 reported that #15927 dropped the `webContents.isDestroyed()` half of
the guard #10065 added for STA-2373, and that the app therefore dies when a
daemon death fans out to every pane. The guard is present on main: #15172
restored it at both senders when it split the PTY monolith, and a per-file
count across v1.4.188..HEAD shows only relocation (pty.ts:4 ->
write-input.ts:2 + bind-listeners.ts:2). The reported 4 -> 0 was scoped to
`src/main/ipc/pty.ts`, now a 39-line barrel. No production fix is needed.

What was real is that nothing pinned the guard — it survived #15927 only
because #15172 happened to re-expand it — and the shared PTY test fake made
that invisible: its `webContents` had no `isDestroyed` at all, so both guards
passed vacuously in every suite. That is also why they are written defensively
as `typeof ... === 'function' && ...`.

- Give the fake an `isDestroyed` mock, re-stubbed to `false` each beforeEach
  rather than left `undefined`, so "alive" is stated rather than accidental.
- Cover both senders red/green: the daemon-death fan-out
  (bind-listeners.ts:37) and the per-write reporter (write-input.ts:57).
  Verified red against a window-only guard and green with the real one.

The per-write case needs a chunked write. A single-chunk write is already
fenced by `isPtyWriteEventFromMainWindow`, which rejects the event once the
WebContents is gone; only the multi-chunk path yields a macrotask mid-write,
letting the renderer die after the sender check passes. That is the sole route
to this sender with a dead WebContents, and what makes its own guard
load-bearing.

Also deletes `src/main/ipc/pty-renderer-surface.ts`: zero importers repo-wide,
and its `isRendererGone` is exactly the weakened predicate. Its comment claims
the headless path "now passes null", but register-headless-runtime.ts:26 still
fakes `{ isDestroyed: () => true }` — #15172 rolled that back too. Adopting it
would reintroduce STA-5373 for real.

Verified: 17 tests across the two pty write suites; full src/main/ipc run 3156
pass (4 pre-existing @parcel/watcher failures in filesystem-watcher-real and
worktree-base-directory-poller, unrelated and failing identically on a
pristine tree); pnpm typecheck clean; oxlint clean.

* docs(pty): correct headless-path comments the #15172 rebase left stale

#15927 made `registerPtyHandlers` accept `BrowserWindow | null` so the headless
path could pass null instead of faking a window. #15172 reverted that signature
while splitting the PTY monolith, but the comments describing it survived, so
three of them now document an API that does not exist.

- orcad-entry.ts: the module docstring claimed orcad installs its controller via
  `registerPtyHandlers(null, …)`. It uses `registerHeadlessPtyRuntime`, and null
  is not accepted. It also claimed desktop surfaces are "declared rather than
  faked" — true except for the renderer window, which is still faked.
- register-headless-runtime.ts: record why the fake is safe rather than leaving
  `isDestroyed: () => true` looking arbitrary. It is load-bearing: every
  renderer-liveness guard reads it and skips, so no send is attempted.

Also gives the fake a `webContents.isDestroyed`. The real guards check both, and
a missing method reads as "alive" — the same gap this PR fixes in the test fake.
No behavior change: the window-level check already short-circuits.

Verified: 45 tests across the liveness-guard, startup-barrier, management and
kill/exit suites; pnpm typecheck clean; oxlint clean.
2026-08-24 19:33:35 -07:00
Neil 838f5bfb75 fix(secrets): tell Linux users when their secrets are only obfuscated (#16033)
On Linux with no keyring, Electron falls back to the `basic_text` backend, which
"encrypts" with a hardcoded password. `isEncryptionAvailable()` returns true for
it, so Orca reported those secrets as sealed. They are not.

The obvious fix — returning false for basic_text — is wrong and would have been a
credential regression: `decryptWithStatus()` skips decryption entirely when
encryption is unavailable, so every already-stored secret would read back empty.
Sealing genuinely works on basic_text and must keep working.

So capability and trust are now separate questions. `isEncryptionAvailable()`
still answers "can this host seal and unseal", and `describeProtectionGap()`
(renamed from `describeUnavailable`) answers "is my data actually protected",
covering both no-sealing and weak-sealing.

That method had no production caller — the port documented a promise nothing
kept. `reportSecretProtectionGap()` now reads it at startup. A user-visible
surface is follow-up; this at least stops the silence.

Adds a bootstrap wiring guard over all nine host port installs. The no-op
defaults are correct for a renderer-less host and silently wrong for the desktop,
and a dropped or reordered install fails no existing test. Verified in both
directions: it fails when an install is removed, and when one moves after the
runtime is constructed.
2026-08-22 22:30:11 -07:00
Neil 03fcfdfb92 feat(orcad): boot the Orca runtime on plain Node (#15968)
* refactor(host): resolve the app root through the port in fork-reachable modules

`parcel-watcher-entry-path.ts` and `session-scanner-service-entry-path.ts` read the
app root via `require('electron').app` inside a try/catch that already returns null
when Electron is absent. They were therefore correct under plain Node at runtime and
only failed the *static* text check — which is real, not pedantic: the comment in
`ports/port-scan-command-client.ts:19` records that the plain-node-entry-guard fails
on that literal text, try/catch or not.

`hasAppEnvironment() ? getAppEnvironment() : null` gives the identical "no app root
here" answer without the text. That restores `hasAppEnvironment`, which an earlier
commit in this stack deleted as unused — it now has the caller it was waiting for.

Ratchet baseline 27 → 25.

Verified: 74 files / 458 tests; `pnpm typecheck` clean; `oxlint` clean.

* feat(orcad): boot the Orca runtime on plain Node

Closes the last two Electron couplings and makes `orcad` a working artifact:
a 4.43 MB Node bundle that boots, pairs, registers a repo, creates a real git
worktree and round-trips a PTY — with zero `require("electron")`.

Ratchet 2 -> 0, so `config/runtime-electron-baseline.txt` is now empty and its
test asserts exactly that: any reachable electron import is a regression.

- speech: inject the service factories, so importing ModelManager for its type
  no longer drags Electron's streaming net.request into the graph
- filesystem-watcher: add a WorktreeWatcherRemoval port. Every entry in those
  maps arrives through an ipcMain handler carrying a renderer sender, so a host
  with no renderer has nothing to close, restore or forget — the inert default
  is what the desktop code does against empty maps, not a stub hiding work
- user-data-path / profile-storage-paths: resolve userData through
  AppEnvironment. These surfaced only once orcad pulled the store in

Both host ports now anchor to a realm-global symbol. `vi.resetModules()` gives
the re-imported graph a fresh module copy, so a binding installed before the
reset silently read back as uninstalled.

The acceptance smoke drives both hosts through one code path (`--target
orcad|electron`) and seeds its own git repo, so it is hermetic and asserts the
same contract of each. Wired into PR CI.

* test(smoke): remove the seeded workspace container, not just the worktree

* test(smoke): surface the server's stderr when it dies before ready

* fix(smoke): build node-pty for Node before booting orcad in CI

* fix(smoke): drive the CLI built from this checkout, not one on PATH

* docs(ratchet): say the baseline must stay empty, not merely shrink

* build(orcad): externalize only the native modules actually in the graph
2026-08-22 21:47:46 -07:00