Commit Graph

506 Commits

Author SHA1 Message Date
Jinwoo Hong 950985645d
fix(runtime): retire an exited pane's surface in its owning host partition (#12671)
`retireMobileSessionSurfacesForPty` called `getWorkspaceSession()` / `setWorkspaceSession()` with no host id, so every retirement wrote to the LOCAL partition — while its sibling `retirePersistedStablePaneOwner` correctly scopes to the SSH execution host.

For an SSH pane exiting cleanly this is not a harmless misdirected write. Measured on main: the write went to the local partition instead of `ssh:conn-1`; the SSH partition still held the dead PTY binding; the local partition gained a bogus topology revision for an SSH repo; and the published tab list contained a RESURRECTED leaf hydrated back from the stale SSH partition. The wrong-partition write was accepted — a tombstone recorded and the revision advanced for a surface that could not be found.

Found during independent review of #11542; pre-existing, not caused by it. Fixes STA-3463.
2026-08-05 00:51:42 -07:00
Jinwoo Hong 003114dfad
fix(remote-runtime): stop the host tab mirror from fighting renderer-owned agent status (#12641)
A remote-paired terminal tab flickered several times per second between the agent-generated title with a running status, and the plain title "Terminal" with "Done - Claude" in the sidebar.

Two writers owned the same state. For remote panes the client parses agent status out of the terminal byte stream, while every host tab snapshot rebuilt the mirrored tab WITHOUT the client's generated title and re-decided status by comparing timestamps taken on two different machines. The host also treated a neutral live title ("Terminal") as proof the agent had finished, and re-stamped that conclusion with the pane's last-output time — so it advanced with every output byte and always looked newer. Neither writer could ever win.

This removes the second writer rather than trying to arbitrate two clocks: the client is authoritative for panes whose status it parses (only while attached, released on teardown), the host no longer invents a finished state from a neutral title, and the generated title is carried through snapshot rebuilds. The client was chosen as the authority because the host snapshot format carries no generated title at all — making the host authoritative would permanently lose generated titles on paired clients.

Purely local and plain SSH panes are structurally unaffected: they have only one writer.

Verified with a reproduction that is red on main (frames show done -> working -> done with the label flipping on every publication) and green with the fix. Independent review additionally found and fixed a defect where a superseded pane's late cleanup could permanently strip a live pane's authority, reinstating the very flap being fixed.

Deferred follow-up STA-3455: host `blocked`/interactive-prompt states can still pierce the fence and fall back to cross-machine timestamps; fixing that properly needs an origin marker on the status entry.
2026-08-04 23:21:34 -07:00
Jinjing fe72eeb75c
Add linked issue guidance and ELI5 sections to PR generation prompts (#12613)
* Add linked issue guidance and ELI5 sections to PR generation prompts

Include linked GitHub issues in PR descriptions with Fixes/Refs guidance, and require ELI5 Problem and Solution sections before implementation details. Tests verify linked issue substitution and prompt structure enforcement.

* Include linked issue details in PR description generation

- Fetch the linked GitHub/GitLab issue title and body so generated PRs reference real issue context instead of just a number
- Use provider-specific reference syntax (Fixes/Refs, Closes/Related to, AB#) and label the issue by the active provider
- Feed issue title and description into the generation prompt while treating them as untrusted context, never as instructions
- Fall back to a cached work-item title when the provider lookup fails, and skip cross-provider issue attachment
2026-08-04 20:05:30 -07:00
Brennan Benson 39c3c58d55
perf(runtime): gate terminal.list visual layouts (#12450)
* perf(runtime): gate terminal.list visual layouts and stop the false writable claim

visualLayouts is ~31% of a large terminal.list payload (44,208 B of 137,412 B on a live 134-terminal remote runtime) and has exactly one consumer: the human-readable CLI formatter. Gate it behind an includeVisualLayouts request param that defaults to included, so pre-flag clients are unaffected, and have every --json/internal caller opt out.

Also drop the record-backed builder's writable, which was a verbatim copy of connected. terminal.show now states writability explicitly as exactly what terminal.send's PTY gate enforces.

* test(runtime): type the payload-size fixture arrays for tsc

* fix(runtime): preserve terminal list compatibility

* test(runtime): guard terminal list optimization

* fix(cli): preserve agent access to terminal layouts
2026-08-04 17:50:52 -07:00
Jinwoo Hong 72245918a1
fix(terminal): attach never-activated daemon sessions on remote subscribe and provider-read fallback (#12589)
* fix(terminal): attach never-activated daemon sessions on remote subscribe and provider-read fallback

A daemon-backed terminal whose tab was never activated in the host UI was
never attached, so the daemon emitted no bytes: paired clients rendered
blank/frozen panes and `terminal read` returned an empty tail while the PTY
was alive.

- Runtime: first remote view subscriber of a known-but-unattached local
  daemon session triggers an attach through the pty controller — attach-only,
  no resize, no renderer mount/focus, headless-safe, deduped across
  concurrent subscribers, and never detached on release. Excludes SSH-scoped
  ids and sessions a local spawn already published this generation.
- Read path: withVisibleSnapshotFallback now falls back to the provider tail
  for an empty-tail never-attached live local session; unprovable state stays
  empty, never an error.
- pty controller: expose attach with getProviderForPty-style routing,
  answering false on doubt; local daemon provider only.
- Daemon adapter: attach rides the session's applied size instead of a
  hardcoded 80x24, sends attachOnly, and retires a pre-v31 daemon's
  accidental spawn instead of publishing it.

Deterministic harness drives the real terminal.multiplex handler against a
real OrcaRuntimeService with an injected daemon-model controller whose data
events are gated on attach; covers snapshot-capable and snapshot-null
daemons, concurrency, release, replacement-spawn exclusion, and negative
safety. Red on base, green with the fix, red again with the fix reverted.

* fix(terminal): refuse degraded-provider attach fallback and surface failed legacy-spawn retire

Verifier follow-ups on subscriber-driven daemon attach:

- DegradedDaemonPtyProvider.attach routed unknown ids to the in-process
  fallback, whose no-op attach resolves — the runtime then pinned a
  subscriber-driven attach as succeeded while the stream stayed blank.
  Attach now refuses any route that resolves to the fallback (a fallback pty
  cannot own a daemon-surviving session), so the controller answers false,
  no sticky success is recorded, and a later subscriber attaches once a
  daemon adapter proves the id. Session-probe adoption moved to
  degraded-daemon-session-routing alongside the new refusal.
- The pre-v31 attach-only TOCTOU retire (accidental legacy spawn kill) now
  logs a warning with the sessionId on kill failure instead of swallowing
  it, so an orphaned replacement shell is diagnosable.

Regressions: degraded provider refuses unowned/fallback-owned attach and
routes to a daemon once it proves the id (red on previous commit); runtime
harness pins refused-attach retry for a later subscriber; adapter test pins
the surfaced kill failure.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 17:25:20 -07:00
Jinwoo Hong 51ca82d028
fix(runtime): seed terminal previews and titles from restore payloads (#12579)
* fix(terminal): seed list/read records from reattach restore payloads

After an app relaunch the PTY daemon survives and spawn silently
reattaches, but the restore payload (reattach snapshot, cold-restore
scrollback, relay replay, lastTitle) arrives as a spawn RPC result and
never passes through runtime.onPtyData — the only feeder of the terminal
records behind `terminal list`/`terminal read`. Every restart therefore
left connected terminals with empty title/preview/lastOutputAt and a
zero-line read tail, blinding orchestrators that poll terminals.

The spawn flow now calls runtime.seedTerminalRestoreTail with the restore
text and lastTitle, unconditionally of the renderer-authority emulator
gate (the records are main-side only). The seed reuses the live path's
normalize/tail/preview pipeline on a capped 256 KiB suffix (re-anchored
at a line boundary so a cut escape cannot leak), only fills records that
never saw output (a remount reattach cannot re-apply history), routes
titles through the applySeededAgentStatus precedent (state writes only —
no waiters, no side-effect facts), and never stamps lastOutputAt or
waitBlockedAt: restored bytes are historical, not fresh activity.

lastTitle is threaded from the daemon reattach snapshot and cold-restore
checkpoint into PtySpawnResult; relay replays seed preview only. SSH and
runtime-controller paths are unchanged — seeding is gated on the fields
existing.

* fix(terminal): seed restore records on the controller spawn path and prime the wait baseline

Follow-ups to the restore-record seed, from independent verification:

1. The runtime-controller spawn flow (createTerminal background creates —
   headless `orca serve`/CLI — and pane splits) never consumed restore
   payloads, so the exact orchestrator-blindness this fix targets survived
   on the topology that needs it most. The extraction now lives in one
   helper called from both spawn choke points (renderer pty:spawn and the
   controller flow); the runtime's empty-record guard makes overlapping
   seeds a no-op.

2. The throttled per-PTY wait scanner starts with a null baseline, so a
   permission prompt visible only in seeded HISTORY read as newly gained
   on the first benign live chunk and stamped waitBlockedAt "now".
   Seeding now primes the scanner baseline from the seeded tail without
   stamping; only a signal appearing in genuinely new output counts.

3. Cap re-anchoring accepts \r as well as \n (newline-free CR-redraw
   streams), consuming a full \r\n pair so the seed does not start with
   a phantom blank line.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 17:23:01 -07:00
Jinwoo Hong 3d8131d7ea
fix(runtime): reject leaf terminal sends only on controller-proven PTY absence (#12578)
* fix(runtime): reject leaf terminal sends only on controller-proven PTY absence

orca terminal send to a leaf whose ptyId no provider in this process owns
was a silent no-op reported as success: the graph mirror answers
writable=true, every provider write to an unknown id is accepted
fire-and-forget, and bytesWritten is computed from the payload rather than
delivery. sendTerminal and sendTerminalAgentPrompt now consult a controller
liveness probe when the provider does not synchronously know the id
(hasPty), and throw terminal_not_writable only on an exact false — unknown
liveness, probe errors, SSH/remote scopes, and probe-less providers never
reject (#12393's rule: null is not absence), so a restored daemon session
still accepts writes before its pane remounts. Push-on-idle orchestration
delivery gains the same gate so a proven-dead leaf keeps its messages
queued instead of marking them delivered into a void.

The pty controller now exposes probePtyLiveness, routed like write: a
provider probe is preferred, the in-process local provider's refusal is
authoritative (sole owner), and remote-scoped or SSH ids without a probe
answer null after awaiting the cold-start daemon swap. Proven-absent
verdicts cache 15s per ptyId with in-flight dedupe, superseded the moment
the provider re-learns the id.

* fix(runtime): arm one probe-deferred delivery continuation per pty

Review (GPT verifier) confirmed: triggers arriving during one in-flight
absence probe each attached a continuation to the deduped probe promise, and
since Claude-target delivered_at stamps only after the delayed Enter, every
continuation re-read the same unread rows — double payload injection and two
armed Enters. Single-flight the deferred continuation per pty; the one armed
continuation re-reads fresh rows when it fires, so nothing is lost, and the
guard clears on settle so later triggers defer again. The narrower
pre-existing 500ms sync-path window is unchanged and out of scope.

* fix(runtime): single-flight the whole orchestration delivery window per pty

The probe-continuation guard cleared at probe settle, but Claude-target
delivered_at stamps only in the delayed-Enter callback ~500ms later — a
trigger landing in that gap armed a fresh probe cycle, re-read the same
un-stamped rows, and re-injected the payload. The identical window existed
on the pure sync path pre-PR (two triggers within 500ms double-deliver).

Hold a per-pty delivery-in-flight flag from before the payload write until
delivery settles: entry-checked before reading unread rows, cleared through
one settle point covering the failed write, the sync-stamped coordinator and
Cursor branches, any sync throw, and the delayed-Enter callback on submit,
refusal, and throw alike. A trigger arriving mid-flight is not dropped — it
parks the latest leaf per ptyId and re-runs delivery once on settle, so rows
inserted mid-flight deliver without waiting for the next idle event. The
probe single-flight stays; the new guard subsumes its post-settle gap, and
no trigger site bypasses it.

Both strengthened tests are red on the previous commit (first subject
injected twice) and green here: in-window re-trigger on the probe path and
sync-path double-trigger each deliver the first batch exactly once, with the
parked second row delivering alone after settle.

* fix(runtime): retire the armed delivery Enter on pty exit; guard fire-time on current state

Two variants of one root cause — the delayed-Enter callback outliving the
session it was armed for:

1. Cold restore respawns under the same session id. onPtyExit never
   cancelled the armed Enter or the in-flight delivery state, and
   onPtySpawned flips the same leaf writable again — so an exit + same-id
   respawn inside the 500ms window let the stale callback inject \r into
   the replacement session and stamp rows it never received, then settle
   against a newer same-id flight.
2. Graph resync replaces leaf objects, so onPtyExit flips writable=false
   only on the current replacement; a callback trusting its closed-over
   snapshot still read writable=true and fired after exit with no respawn.

The flight record now carries its armed Enter timer and serves as settle
identity: onPtyExit clears the timer and drops the flight and any parked
re-delivery without stamping (rows stay unstamped and re-deliver on the
replacement's next idle — the existing contract), and settle no-ops unless
its own flight is still current, so a stale settle can never clear a newer
same-id flight or flush its parked trigger. At fire time the callback
re-resolves the leaf by key and requires the same ptyId binding and current
writability instead of reading the closure snapshot.

All three regressions are red on the previous commit: same-id respawn saw
\r plus a false delivered_at stamp, exit leaked the flight and parked
state, and the orphaned-snapshot resync variant fired Enter after exit.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 17:22:10 -07:00
Jinwoo Hong 27da04d50d
fix(terminal): truthful handle liveness + no forked resume tabs for hidden restorable panes (#12574)
* fix(runtime): report terminal handles disconnected on controller-proven PTY absence

leaf.connected mirrors the renderer graph (ptyId !== null), so a restored
surface whose PTY died with a prior process was listed connected/writable
forever with empty title/lastOutputAt/preview — the exact signature automation
saw on run6 workspaces after a restart. listTerminals now threads the
controller inventory it already fetches into buildTerminalSummary and demotes
only on proven absence, only for locally-scoped ids; unknown liveness and
SSH/remote scopes never demote, and no session or pane is retired.

* fix(terminal): stop forking hidden restorable panes into replacement resume tabs

paneWillConnectOnActivation still assumed the pre-keep-alive mount model, but
every non-parked tab of the active worktree mounts and connects hidden at 0x0.
Activation therefore appended a replacement resume tab per non-group-active
agent pane and handed it the sleeping record, stranding the hidden pane as a
bare shell — or forking two live surfaces onto one provider session when the
old PTY survived in the daemon. The predicate now answers "will mount and
connect": any non-web-mirror tab of the active worktree qualifies; non-active
worktrees still answer false so background wake keeps its append-based resume.

Contract change: reverses the hidden-tab expectation from #6800, whose premise
(hidden panes never connect) no longer holds; that test is updated in place.

* test(terminal): pin the remote-scope exemption and the web-mirror ownership exception

CodeRabbit flagged both exclusions as untested: a remote-runtime-scoped leaf
absent from the local inventory must stay connected (its inventory lives on
the remote host), and a web-mirror tab must not own sleeping-session recovery
(it never mounts a local pane), so the appended replacement remains its
correct resume path.

* fix(terminal): rescue just-spawned ptys from absence demotion; unpark panes owning sleeping records

Review (GPT verifier) confirmed two gaps:
- listTerminals demoted a live just-spawned PTY when listProcesses snapshotted
  before session registration (the sweep's hasPty rescue is leaf-gated), and
  federation reads one connected:false as exited. The summary's proven-absence
  check now also consults the provider's sync hasPty.
- Ordinary per-tab cold parking (30s hidden) kept a non-group-active pane
  unmounted, so a sleeping record it owns under the new ownership predicate
  could not cold-restore until the user revealed the tab. Per-tab parks now
  exempt panes owning a sleeping-session record; worktree-level parks are
  untouched (they clear on activation).

* fix(terminal): reconcile the daemon session cache on inventory; scope the park exemption to consumable records

Round-2 review confirmed two holes in the round-1 fixes:
- DaemonPtyAdapter.hasPty is cached activeSessionIds membership, and a
  successful listSessions never removed ids the authoritative inventory
  omitted — an exit missed while the socket was down kept hasPty true
  forever, and the new spawn/list-race rescue would trust it, reopening
  connected-forever for that pty. listProcesses now drops pre-request cached
  ids the inventory does not list alive (ids spawned mid-flight are snapshot-
  protected).
- The park exemption covered records a pane can never consume
  (automaticResumeBlockedBy, passive-completed evidence), pinning hidden
  panes mounted indefinitely. The exemption now lives in
  sleeping-record-park-exemption.ts and requires a consumable record.

Also pins the web-mirror replacement's resume claim and startup command
(CodeRabbit round-2).

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 17:20:09 -07:00
Neil 7956335cea
fix(setup): stop caching an unreadable orca.yaml as "no setup script" (#12469)
* fix(setup): stop caching an unreadable orca.yaml as "no setup script"

`checkRepoHooks` returned `{hasHooks:false, hooks:null, mayNeedUpdate:false}` with no `status` field when the SSH filesystem provider was unavailable, and inside a blanket catch for any read error. The renderer only bails on `status === 'error'`, so that status-less false negative was cached as an authoritative "no setup script" and the prompt stayed on screen.

Mirror the `hooks:check` IPC twin exactly: `status:'error'` for a missing provider, ENOENT-aware in the catch, `status:'ok'` on the folder-repo, binary, SSH-success and local branches.

Fixes #8752

Co-authored-by: Orca <help@stably.ai>

* test(e2e): add recordable proof for setup-script-prompt-false-negative

Fails on origin/main, passes on this branch.

Test: recovers from an unreadable orca.yaml instead of pinning the failed verdict

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-04 17:06:50 -07:00
Neil 8f29f58ab7
Keep showing your workspaces when a project is too slow to answer instead of reporting zero (#12222)
A stalled per-repo git scan no longer publishes a healthy-looking empty catalog. Adds an execution-host ownership gate so a degraded host cannot republish another host's worktree rows under its own id.

Relates to #11869 — this fixes the stall-publishes-zero half. The issue stays open for the remainder.
2026-08-04 01:26:52 -07:00
Neil 96e31e7bf8
Remove a paired computer's deleted projects from every connected device (#12215)
* fix(repos): remove a paired computer's deleted projects from every connected device

A project deleted on a paired Orca host stayed in every connected client's
sidebar and could not be removed there.

Two independent defects:

1. Host-local repo IPC mutations only sent `repos:changed` to the host's own
   renderer (src/main/ipc/repos.ts:2711). The runtime client-event stream was
   fed only by mutations arriving over runtime RPC, and clients refetch a remote
   catalog only on a `reposChanged` event -- there is no polling on desktop -- so
   the deleted rows persisted indefinitely. The shared `notifyReposChanged`
   helper now also calls the new
   `OrcaRuntimeService.notifyReposChangedForRemoteClients()`
   (src/main/runtime/orca-runtime.ts:5175), mirroring the existing
   `notifyWorktreesChangedForRemoteClients` precedent. This covers every repo,
   project-group and folder-workspace IPC mutation, so renames, colors, reorders
   and adds propagate too.

2. Deleting the ghost row on the client routed `repo.rm` to the owner, which
   answered `repo_not_found`. `removeProject` wrapped its whole body in one
   try/catch, so the rejection aborted the local purge before the `set()`
   (src/renderer/src/store/slices/repos.ts:3466) and the delete button appeared
   to do nothing. Only `repo_not_found` is now tolerated; any other failure still
   keeps the row, and an opt-in `errorFeedback: 'toast'` makes it visible at the
   three single-project user-initiated entry points. Bulk and background callers
   keep today's silence plus their own aggregate reporting.

Closes #11994

Co-authored-by: Orca <help@stably.ai>

* fix(repos): revert inert RepositoryPane removeProject arg

The settings pane's only render site drops the argument; the toast is
already delivered by removeSettingsProjectFromAllHosts.

Co-authored-by: Orca <help@stably.ai>

* fix(repos): scope duplicate-repo-id deletes to the owning execution host

Cover the cross-host collisions #11994's broadcast now fans out to every paired
device. Same-name projects on different hosts were already isolated (per-host
UUIDs, host-scoped catalog merge and purge) and are pinned by regression tests.

Two same-repo-id paths were not: `repo.rm` with a `path:`/`name:` selector and
`deleteProjectHostSetup` both resolved one row and then deleted by bare id,
taking the sibling host's registration with it.

Co-authored-by: Orca <help@stably.ai>

* test(mobile): align the poll-interval rationale with the new reposChanged emission

Co-authored-by: Orca <help@stably.ai>

* fix(repos): resolve deleteProjectHostSetup's repo row only on the setup's own host

The sibling-host fallback could only ever pick a row on a host the caller
did not name; with no exact match the setup is stale and the existing path
already drops just the setup.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-04 01:26:48 -07:00
Neil f6878d660f
Show Claude's AskUserQuestion card in desktop Chat when the agent runs on a paired headless server (#12223)
* fix(native-chat): show Claude's AskUserQuestion card when the agent runs on a paired headless host

Three gaps kept the question card off the desktop when the agent ran on a
remote `orca serve` host:

- The `session.tabs` projection reduced HTTP agent-hook rows to identity only,
  hard-coding `state: 'done'` and an empty prompt, so `toolName` and the full
  `interactivePrompt` never left the host. It now publishes the newest fresh
  hook row's status fields, bounded by the same staleness window `agentType`
  uses, excluding `providerSessionOnly` resume rows, and yielding to live
  title evidence unless a question is actually pending.
- Nothing republished `session.tabs` when only a hook row changed, and the
  re-emit carried an unchanged `snapshotVersion` that clients drop on their
  monotonic gate. Material hook transitions and pane/SSH status clears now
  bump the version and schedule a coalesced emit.
- The desktop card resolved only from live status. It now falls back to the
  pending ask in the transcript, matching mobile, so a relay gap can no longer
  leave the composer mounted over a pane parked on a selector.

Closes #11761

Co-authored-by: Orca <help@stably.ai>

* fix(native-chat): date the hook-row recency guard against a real clock

`resolveHookLiveAgentRow` compared a hook `receivedAt` (epoch ms) against
title stamps that are title-observation sequence numbers, so the guard could
never fire — any fresh hook row overrode live title-derived state, and a manual
rename (the one epoch writer) inverted it. Stamp the live OSC title path with
wall-clock ms and compare against that alone.

The regression test fabricated epoch-valued title stamps production never
writes; it now drives the title through `onPtyData`, and a new case pins the
opposite direction (hook row newer than the title wins).

Co-authored-by: Orca <help@stably.ai>

* fix(native-chat): stop an orphaned tool call from pinning a dead question card

extractPendingAsk pairs tool results to calls by a global FIFO (tool_use_id
is dropped at decode time), so one call that never gets a result desyncs the
queue for the rest of the transcript and strands an answered ask as pending.
Real transcripts also hold asks the user escaped and typed past. On desktop
that card replaces the composer, so the pane became unsendable.

Drop in-flight calls at a turn boundary — a user turn or the decoders'
interrupt row — since the turn that owned them is over. Claude's tool-result
turns decode as role 'tool', so normal FIFO resolution is untouched.

Co-authored-by: Orca <help@stably.ai>

* refactor(native-chat): trim the headless AskUserQuestion projection

Reuse rather than restate: the invalidator now takes the shared
`AgentHookEventPayload` instead of a locally redeclared row shape, and the
hook live row is a `Pick<>` of the retained OSC snapshot so one projection
branch consumes either carrier. Fold the immediate/coalesced session-tabs
emit into one method (also drops a redundant re-emit on the
provider-session push). Drop card tests that re-route shared-parser
assertions through React. Isolate pane-status-clear subscribers and prove
the no-republish case by version arithmetic instead of a timed silence.

Co-authored-by: Orca <help@stably.ai>

* test(native-chat): pin the AskUserQuestion card render under real Electron

Why: the 13 parser unit tests pin extraction, but nothing proved a card
actually renders where an inert tool call used to. This spec reproduces the
paired-headless topology from the client side — live status carrying agent
identity and state 'working' but no interactivePrompt/toolName, with the
pending ask present only in the transcript — and fails on main.

Refs #11761

Co-authored-by: Orca <help@stably.ai>

* test(native-chat): drop the unused testInfo parameter

Why: oxlint no-unused-vars fails the lint gate on an unused test parameter.

Co-authored-by: Orca <help@stably.ai>

* test(native-chat): drop leftover proof scaffolding from the ask-card spec

The env-var screenshot label and the fixed 2s settle only existed to make
the pre-fix capture comparable; the card assertion already waits.

Co-authored-by: Orca <help@stably.ai>

* test(runtime): use a truly unresolvable pane key in the hook republish guard

#11203 taught pane lookup to recover a reminted tab id by leaf id, so the old
fixture (new tab id, live leaf id) resolved and bumped the snapshot a second
time once this branch merged with main.

Co-authored-by: Orca <help@stably.ai>

* fix(runtime): refuse a hydrated unconfirmed hook row as live pane status

#12346 landed on main after this branch was cut: a nonterminal row restored from
last-status.json is stamped `restoredUnconfirmed` because its transition may have
fired while no receiver was up, and every freshness gate treats it as never-fresh.
The new headless `live` projection here only checked `receivedAt`, so a restart
inside the 30-minute window would republish the hydrated row — resurrecting the
AskUserQuestion card with no agent left to answer it.

`agentType` still reads those rows: they prove identity, just not liveness.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Neil <nwparker@users.noreply.github.com>
2026-08-04 00:48:58 -07:00
Brennan Benson e43bd6c4ad
fix(worktrees): preserve folder PTY owner scope (#12429) 2026-08-03 22:29:19 -07:00
Jihwan Kim 5bd2f59d29
fix(runtime): open files from sibling workspaces (#11369)
* feat(runtime): match files to workspace owners

* fix(runtime): resolve terminal paths through sibling workspaces

* fix(editor): route restored sibling workspace files

* fix remote sibling file ownership routing

* fix(editor): migrate restored sibling file owners

* fix(editor): revalidate restored owner activation

* docs(review): record PR 11369 correction evidence

* fix(editor): reject collision before activation prep

* docs(review): record PR 11369 final correction

* fix(editor): retain projected reconciliation narrowing

* chore(review): keep verification artifacts out of PR

* fix(editor): harden restored owner migration

* fix(runtime): resolve workspace root terminal paths

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-03 21:41:58 -07:00
오영택 3d6d6dd67d
fix(orchestration): scope agent lineage to its owning run (#11203)
Use durable Task-to-Run ownership and current pane, process-incarnation, and Run-generation authority for sidebar agent lineage. Add schema migrations, bounded lookup indexes, fail-closed renderer cleanup, and runtime/RPC regression coverage.

Co-authored-by: Jaeyoung22 <89302528+Jaeyoung22@users.noreply.github.com>
2026-08-03 21:21:03 -07:00
Brennan Benson 9e5bd5fb84
fix(worktrees): fence SSH worktree deletion PTY teardown to the owning host (#12388)
Destructive worktree removal swept PTYs by worktree id alone. Worktree ids are
`repoId::path` and the store keeps one per host, so deleting an SSH worktree
could stop a same-id local (or other-connection) workspace's terminals — or fail
outright with `selector_ambiguous` when two hosts owned the id.

Every destructive teardown now names its owner (resolvedWorktreeId plus the
connection/runtime environment), matching the already-hardened forget-local path:

- IPC `worktrees:remove` (git + folder workspaces)
- runtime `removeManagedWorktree` (CLI/mobile `worktree.rm`, git + folder)
- missing-worktree terminal reconciliation, including its no-provider fallback

The #11960 allowUnverifiedStop force-delete gate is untouched.
2026-08-03 19:18:37 -07:00
OrcaWin ce8b778d31
perf(runtime): withhold unchanged mobile snapshots from the graph payload (#12245)
* perf(runtime): withhold unchanged mobile snapshots from the graph payload

Every graph sync structured-cloned all 222 worktree snapshots to main even when
none had changed: 374 KB and ~5 ms per clone, paid twice because Electron clones
on serialize and again on deserialize. That transport cost — not the renderer
rebuild — is the bulk of a publication.

The renderer now sends only the snapshots main has not acknowledged and names
the rest in unchangedMobileSessionWorktrees. Detection is object identity, not a
deep compare: an unchanged worktree already returns its cached snapshot object.
Main seeds nextWorktrees from that list so its prune keeps withheld worktrees
live instead of removing them.

The call itself is unconditional. syncWindowGraph is not a one-way publish — its
return value is the only channel carrying agentOrchestrationByPaneKey to the
renderer, and the handler adopts pre-allocated handles, merges detached leaves,
refreshes writable flags, and drains graph-sync callbacks on every sync. Skipping
it would starve all of that.

Two failure modes are closed explicitly. The memo advances only after main
acknowledges, so a publication that throws is resent in full rather than
silently withheld forever. And a worktree main dropped on its own — worktree
metadata removal — comes back in mobileSessionResyncWorktrees, which also clears
the accepted-revision record so the republish is not rejected as a no-op.

Unchanged republish at 222 worktrees / 787 tabs: 374 KB to 3.4 KB, 5.08 ms to
0.02 ms per clone. One changed worktree: 5.3 KB.

* fix(runtime): resync stale withheld mobile snapshots

* fix(runtime): align accepted mobile snapshot membership

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-03 18:11:30 -07:00
Brennan Benson 9ec4907cfb
fix(agent-status): restore hydrated nonterminal statuses as unconfirmed (#12346)
* fix(agent-status): restore hydrated nonterminal statuses as unconfirmed

A hook transition that fires while Electron is down has no receiver and is
discarded, so last-status.json can restore a stale 'working' as confirmed
truth for up to the 7-day hydrate TTL. Stamp hydrated nonterminal rows with
restoredUnconfirmed, carry it through both IPC paths, and treat such rows as
never-fresh in the shared and renderer freshness gates so the sidebar,
worktree.ps, and the raw snapshot all present the same degraded semantics.
Terminal states restore as-is; any accepted live event clears the flag; the
flag itself is never persisted. Interrupt/question inference refuses to
fabricate transitions onto unconfirmed rows.

* fix(agent-status): shed unconfirmed marker when the liveness sweep verifies done

The restored-subagent reaper's reconciled entry spread carried
restoredUnconfirmed onto a process-probe-verified 'done', making freshness
gates suppress a legitimate completion. Keep the marker only while the
reconciled state stays nonterminal.

* fix(agent-status): let live evidence replace hydrated rows

* fix(agent-status): keep restored rows degraded

Sort accepted live evidence after hydrated rows even across wall-clock rollback. Let unconfirmed rows own their preserved pane titles without asserting live state, while retaining independently live sibling evidence.

* fix(agent-status): suppress unmapped restored titles

Treat a single runtime title as covered by the single restored hook row while layout identity is unavailable. Preserve ordinary age-stale fallback and mapped sibling-pane evidence.
2026-08-03 17:22:43 -07:00
Brennan Benson f4b2b782b5
feat(orchestration): coordinator-driven release of settled worker terminals (STA-905) (#12355)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-03 17:17:26 -07:00
Brennan Benson 866bcda465
fix(terminal): recover degraded daemon spawn routing (#12277)
* fix(terminal): recover degraded daemon spawn routing

* fix(terminal): preserve fresh-session recovery semantics

* fix(terminal): avoid retaining exited recovery sessions
2026-08-03 12:47:41 -07:00
OrcaWin cd68a8b00c
fix: preserve live agent PTYs through graph hydration (#11789) 2026-08-03 11:11:14 -07:00
Neil 339045b150
fix(runtime): coalesce concurrent host terminal focus (#11841)
Bound exclusive host navigation to a generation-aware latest-wins
single-flight so bulk open and switch fan-out stay responsive on large
remote fleets. Add freeze repro harnesses and navigated settlement.
2026-08-03 02:18:05 -07:00
Brennan Benson b4d9ae44a5
fix(mobile): deliver the agent launch command when a create settles over a bare renderer PTY (#12197)
A mobile New Tab -> Codex create resolves the launch command and hands it
to the renderer, but when the renderer's startup queue is lost (the #7587
stall class) the pane spawns a plain shell and the create still settles
ready via PTY adoption - silently binding the phone to a bare terminal
forever, since the ready status also disables the #7837 activation-time
materialize recovery.

Record the resolved launch command on the pending create and, at every
renderer-backed settle point, deliver it to the adopted PTY when no spawn
command was recorded for it. Spawn commands are noted per PTY by both
spawn IPC handlers, so a missing record on the locally registered live
PTY proves the launch never ran; delivery types the command exactly like
the create would have, and the note prevents double delivery.

Fixes STA-3214
2026-08-02 22:47:44 -07:00
OrcaWin 525ffc5ae0
fix(worktree): stop the PTY gate from permanently wedging workspace removal (#12153)
Destructive worktree removal proves every PTY is dead before touching the filesystem. When a stop
RPC failed, it re-listed the provider to check whether the PTY had already exited — but on the
same deadline the sweeps had just spent, so it timed out without ever asking and read "could not
verify" as "still live". The sweep spends that budget every run, making the refusal deterministic;
--force never reached the gate, so the workspace was unremovable forever.

- Verification gets its own budget instead of an exhausted remainder.
- Verdicts split into exited / live / unverifiable; the error names the blocking PTY ids and why.
- A reachable escape hatch: allowUnverifiedPtyStop, set only by genuine Force Delete affordances
  and the CLI's --force — never by the force the ordinary delete confirmation already sets — with
  an 'unstopped-pty' classifier reason so the desktop actually offers the button.
- Force also survives a sweep that cannot complete; the non-force path still fails fast.

Fixes #11960
2026-08-02 19:16:58 -07:00
OrcaWin 95c431f5c3
fix(orchestration): worker-start launches the configured agent CLI, not the raw agent id (#12148)
Worker-start passed the Orca agent id straight to the shell as the worker terminal command, so `--agent cursor` ran `cursor` — which on Windows resolves to Cursor IDE's cursor.cmd and opened the desktop app, leaving a blank shell that timed out at agent_readiness. The same gap hit every agent whose CLI binary differs from its id (continue/aug/kiro/qwen-code/mistral-vibe/antigravity/trae/mimo-code/hermes/command-code/claude-agent-teams).

Adds TerminalCreateOptions.startupAgent so callers name the agent outright; createTerminal then builds the launch from the TUI agent config (command, agentCmdOverrides, default args/env, preflight trust) instead of sniffing the command string. Also covers repo-less folder workspaces, which previously skipped resolution entirely, and fails loudly instead of spawning a bare shell when an explicit agent cannot resolve.

Fixes #11926
2026-08-02 18:00:20 -07:00
Rod Boev 8c5371ebad
fix(worktrees): respect Windows shell for setup runners (#6967)
* Honor configured shells during worktree setup

* Align setup launch paths with selected Windows shells

* Carry setup shell selection through deferred launches

* Prove Windows setup shell routing at its real adapters

* Ground remote PowerShell proof in the real writer

* Preserve Git Bash across deferred setup launches

* Harden Windows setup runner shell selection

- Resolve remote PowerShell binary without local pwsh probe: for SSH/remote
  Windows worktrees, isPwshAvailable() reflects only the LOCAL host, so an
  'auto' implementation could route the remote runner to a pwsh.exe the remote
  lacks. Add resolveSetupRunnerShell(..., { probeLocalPwsh: false }) so remote
  auto keeps the always-present powershell.exe; explicit pwsh.exe still honored.
- Preserve native exit codes in the PowerShell runner by checking
  $LASTEXITCODE before $?, so a failing native command surfaces its real code
  instead of a generic exit 1; $? still catches cmdlet soft-failures.
- Write the PowerShell runner with a UTF-8 BOM so Windows PowerShell 5.1 (the
  new default powershell.exe) reads it as UTF-8 instead of ANSI, preventing
  non-ASCII setup-script corruption.
- Add unit tests for the remote-probe behavior.

* Restore setup-shell scope narrowing over the rebase

The force-pushed rebase dropped five review-fix commits that were already
on this branch; this reapplies their combined effect on top of the new
base and the hardening commit:

- Keep SSH setup shell selection remote-owned (no local terminalWindowsShell
  or pwsh routing for remote hosts; supersedes the probeLocalPwsh guard)
- Preserve cmd setup compatibility outside POSIX shells (no .ps1 runner
  family, so the BOM/exit-code hardening is no longer applicable)
- Route WSL setup runners from the project runtime
- Avoid blocking PowerShell probes during setup creation
- Correct SSH and WSL background setup fixtures

* Satisfy the changed-code gates for the setup-shell runner

- createWorktreeRunnerScript took 7 positional parameters, tripping the
  changed-code max-params gate; move it to a single options object.
- hooks-runner.test.ts deep-equals the createSetupRunnerScript result, so
  assert the cmd shell now returned for native Windows worktrees.

* Carry the setup launch shell through observed and issue runners

- buildObservedSetupCommand takes the runner's launch shell so WSL-routed
  Windows-drive setup replays use /mnt/c instead of Git Bash /c
- resolveSetupRunnerShell gates the posix runner on the same Git Bash
  resolution the PTY uses, so a missing or non-MSYS bash keeps the cmd runner
- issue-command runners carry their launch shell, and the renderer passes it
  when building the queued command
- treat a bare `bash` shell setting as POSIX like `bash.exe`

Co-authored-by: Orca <help@stably.ai>

* fix(worktrees): close counsel P1 gaps for Windows setup shells

Route windowless/headless creates through the shell-aware setup runner when a
PTY controller is available, existence-check explicit Git Bash paths before
committing to .sh runners, thread the resolved shell into issue-command
runners, and document the intentional Git Bash interpreter flip with a narrow
scope table.

* Convert setup env to MSYS form and harden the bare cmd runner launch

C3: a Git Bash setup runner now receives ORCA_*/CONDUCTOR_*/GHOSTX_* path
values in /c/... form, matching the runner path and the shell's own HOME/PWD.
C5: extension-less `bash` resolves to Git Bash everywhere, matching how
resolveWindowsShellStartupFamily already classifies it.
C7: runner paths carrying characters that cannot be quoted on a cmd command
line launch through a delayed-expansion PowerShell shim instead, and the batch
runner disables inherited delayed expansion so `!` in setup lines survives.

Co-authored-by: Orca <help@stably.ai>

* docs: note MSYS ORCA_* paths and bare bash Git Bash resolution

Keep the setup-shell release note aligned with C3 env conversion and C5 bare
bash resolution so the published claim matches runtime behavior.

* revert: drop windows-setup-shell doc allowlist and AGENTS link

Keep the counsel P1/P2 product fixes without expanding the docs allowlist
or AGENTS.md guidance surface.

* fix(plugins): contain Parcel unsubscribe rejections under Vitest

Dev plugin watchers fire-and-forget unsubscribe, and in-process Parcel
can reject when temp watch roots are already deleted. Catch those
rejections so they cannot fail the suite as unhandled errors.

* fix(plugins): keep in-process unsubscribe rejection surface

Swallowing Parcel unsubscribe errors broke mocked unsubscribe tests
that return non-Promises and expect rejections. Contain failures only
in PluginDevWatcher fire-and-forget paths.

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-08-02 17:40:58 -07:00
OrcaWin 4a76565a35
fix(terminal): bound paired-client renderer work (#12081) 2026-08-02 01:02:26 -07:00
Neil 73c5009b82
chore(dead-code): drop ~2k lines of unreachable exports and orphan modules (#12077)
* chore(dead-code): drop 2k lines of unreachable exports and orphan modules

Ran knip across every build entry (main, preload, renderer, popout, web,
cli, relay, workers, forked sidecars, config scripts) and removed what no
entry graph can reach.

- 11 orphan modules nothing imported, plus one test that only covered them
- 159 unused exports/types, with their now-dead helpers, imports and tests

Each candidate was verified against dynamic references before deletion.
42 knip hits were false positives and are kept: shared modules consumed by
the mobile/ workspace, the src/shared/plugins/** public API, vendored
shadcn primitives, and relay wire-protocol constants held for compatibility.

Adds knip.json + `pnpm audit:dead-code` so this stays measurable.

Verified: pnpm typecheck, pnpm lint, and 2081 tests across the 73 affected
test files all pass.

* chore(dead-code): move knip config under config/

Root-level additions are blocked by the root directory guard.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-02 00:33:57 -07:00
Jinjing 2fe655de72
Pr 9364 update (#11684)
* fix(workspaces): forget deleted remote mirrors

* fix(workspaces): tighten orphan cleanup guards

* fix(workspaces): avoid duplicate remote teardown after delete

* fix(workspaces): prevent orphaned filesystem auth on removal

When a worktree is deleted, especially from remote hosts, the filesystem
authorization cache was not being invalidated, leaving the path accessible
even though the workspace was gone. Use persisted host ownership to scope
cleanup to the correct partition and invalidate the auth cache when removing
a workspace to prevent orphaned authorization in host-partitioned scenarios.

* Fix orphaned worktree cleanup to trust persisted ownership and clean all

When a remote worktree or project is deleted, the local metadata cleanup must work even when the owning repo can no longer be resolved. The removal was incorrectly trusting a caller's potentially-stale hostId over the authoritative metadata, causing:
- SSH workspaces to be cleaned from only the local partition, stranding the remote partition with an un-bumped topology fence
- Sibling worktrees of the same repo to get rebased and lose unsaved tabs
- PTYs in orphaned workspaces to never stop when the selector can't resolve
- File watchers to keep firing events indefinitely

Now the cleanup trusts the persisted owner hostId, cleans all affected session partitions where tabs might live, intelligently gates topology fence bumps to avoid rebasing siblings, and passes the exact worktreeId to PTY sweeps that can't resolve the selector.

* Pass removal host ID to fix teardown of ownerless remote worktrees

When deleting an ownerless remote worktree, args.hostId may be absent.
Without an explicit host ID, the session teardown would incorrectly clear
the local session instead of the remote. Derive removalHostId from the
repo (the canonical owner) and pass it to every removeWorktreeMetadataAndTransientState
call to ensure the correct session is torn down.

* Scope worktree teardown to the owning host connection

- Orphaned SSH worktrees now sweep through the host's PTY provider instead of only the local one, so remote terminals die when the repo is gone
- Terminal ownership is scoped by resolved connection/runtime environment, preventing a same-id workspace on another host from being swept
- Persisted ownership beats stale live routing for in-flight keys and topology fences
- Renderer fails closed and never forgets a row whose removal route turns ambiguous mid-flight

* Fix worktree removal to scope session cleanup to the owning host

When a worktree is removed, its metadata purge must resolve the same owner
as the teardown sweep, or SSH/runtime partitions keep workspace state
forever. Additionally, materializing never-persisted host partitions
during removal can rebase sibling worktrees. Scope cleanup to owning host,
skip unwritten partitions, and detect transport-wrapped error codes that
Electron IPC re-wraps and strips causes from.

* Fix worktree removal to scope session cleanup to owning partition

- Only the owning partition may fence on emptiness; spill partitions
  that never held the worktree must not claim repo authority to prevent
  data loss when the renderer owns tabs elsewhere
- Tighten error code detection to require message boundaries (": " or
  newline) instead of matching trailing tokens, preventing false
  positives from triggering the destructive forget-local fallback

---------

Co-authored-by: gatsby74 <166927047+gatsby74@users.noreply.github.com>
2026-08-02 00:01:23 -07:00
Jinjing 2f104d8713
Tier GitHub PR lookup polling to prevent quota exhaustion (#12013)
* Tier GitHub PR lookup polling to prevent quota exhaustion

The selected worktree (O(1)) checks per-minute; card list (O(N)) per-15-minutes.
Introduce process-wide cache to collapse concurrent polling and gate lookups on
available rate-limit budget with exponential backoff on failure.

- Preserve last-known review during backoff
- Invalidate cache when Orca opens a PR
- Stop coordinator from double-charging

* Tier GitHub PR lookup polling to prevent quota exhaustion

- Return the latest reset time when both GitHub API buckets are rate-limited, preventing premature retries against still-blocked buckets.
- Serve the last known review on transient lookup failures, preventing reviews from blinking out on temporary errors.
- Discard in-flight lookups that predate an invalidation so stale answers cannot overwrite newly opened reviews.

* fix: give rate-limit reset tests unique titles

oxlint vitest/no-identical-title was failing static analysis because two
cases shared the same describe title.
2026-08-01 15:50:54 -07:00
Brennan Benson b04c695750
fix(runtime): drop stale local agent rows from worktree.ps after tab close (#11464)
* fix(runtime): drop stale local agent rows from worktree.ps after tab close

attachAgentRowsToSummaries attached every hydrated hook row by worktreeId
with no check that the pane/tab still exists, so agents from closed tabs
(last-status.json hydrates for days) kept showing on mobile as current
activity. Local rows now require the tab in a session/runtime graph or a
connected PTY; remote rows are exempt since their tabs may only exist on
the remote host.

Fixes #6072

* fix(runtime): resolve legacy numeric pane keys through the stale-row filter

Non-UUID leaves produce tabId:paneRuntimeId keys with no tabId field;
without parsing them the stale filter was bypassed entirely for such rows.

* fix(runtime): filter stale WSL agent rows

* fix(runtime): ignore persisted tabs for agent liveness

* fix(runtime): restore session-tab liveness and thread OSC transport through the stale-row filter

Review loop pass 1 (3 independent same-model reviewers, findings converged):

- Revert a829e8f9cf's `!this.tabs.has(tabId)` to `mirroredWorktreeId ===
  undefined`. The renderer graph is structurally empty under headless serve
  (index.ts publishes {tabs: [], leaves: []}), is cleared by
  markGraphUnavailable, and omits unvisited/cold-parked workspaces, so
  graph-only existence dropped live agent rows in all those states and broke
  worktree.ps/session.tabs.list parity. Every close path prunes the persisted
  tab, so session tabs remain valid liveness evidence; the stale-persisted-tab
  premise did not survive tracing.
- Restore the rename and legacy-pane-key tests to their session-only fixtures
  (the graph syncs added with the flipped predicate masked the contract
  change) and pin the restored contract in a named test.
- Thread the pane's connectionId through RuntimeAgentRowSnapshot so
  OSC-retained rows keep the SSH exemption; previously a fresher OSC ping
  hardcoded null and stripped it.
- Pin each rescue conjunct individually (paneKey-only, tabId-only, ptyId after
  binding clear), the WSL keep direction, the unresolvable-paneKey guard, and
  row presence in the freshness cases.

* fix(runtime): carry the OSC-observed ptyId when a hook row wins the freshness race

Hook payloads have no ptyId field, so overwriting the rowSources entry
discarded the OSC-observed one and the connected-PTY ptyId rescue went dead
for hook-fresh panes during a binding-clear window (pass-2 review P3). Also
corrects the incarnation-change comment on the OSC rescue test.
2026-08-01 11:57:24 -07:00
OrcaWin 33ad64b1c8
fix(runtime): bound persisted graph hydration (#11832) 2026-07-31 17:56:40 -07:00
Brennan Benson 9bf05b0a9c
Prevent Agent sleep while orchestration dispatch is active (#11808)
* fix(agent-sleep): keep active dispatch workers awake

* fix(agent-sleep): harden background work detection
2026-07-31 16:35:33 -07:00
Yunqian Fan 475f63ea1b
fix(remote): scope renderer throttling to paired terminal publication (#11581)
* fix(remote): unthrottle host renderer while serving a paired client

A paired desktop host left in the background could not open or close
agent sessions for its remote/relay client: the action stalled and
eventually failed with the host-side "Timed out waiting for terminal
surface after creation" (10s) error, while an already-live terminal's
keystrokes stayed fast.

Root cause: creating/closing a session routes through the host
renderer's setTimeout-coalesced graph sync to publish the terminal
surface, but the host window runs with Electron background throttling
(the hidden-window default, reaffirmed on macOS). When the window is
backgrounded/occluded, those renderer timers are throttled to a crawl
and the surface publication misses the 10s deadline. Live keystrokes are
unaffected because PTY I/O flows through the main process, never the
renderer.

Keep the authoritative renderer unthrottled while at least one remote
client is connected and restore the throttled power-saving default once
the last one disconnects. Connect/disconnect are driven from the shared
MobileSocketWiring onReady/onClose, so both direct-WS and cloud-relay
clients are covered; headless serve has no window and is a safe no-op.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(remote): tidy renderer-throttle comment and test per review

Address automated review nits on #11581:
- Trim the module-level rationale comment to the non-obvious contract,
  matching the repo's concise-comment guideline.
- Drop the dead `detachedThrottle` variable from the reapply test; the
  detached-target scenario is already covered by the lazy-resolution
  test, so the case now asserts only what it exercises.

No behavior change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(remote): scope paired terminal publication throttling

Keep headed paired terminal creation and close renderer-owned so host inventory, input routing, ACK recovery, and cleanup retain the established lifecycle. Hold a reference-counted background-throttle lease only while the renderer publishes a paired operation, and epoch-fence async resolution so renderer reloads reject before any request or PTY spawn. Preserve headless main ownership and prevent paired clients from falling back to a local terminal.

* test(e2e): verify minimized host terminal repaint

* fix(remote): preserve paired terminal inventory through graph gaps

---------

Co-authored-by: fanyunqian.1 <fanyunqian.1@bytedance.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-31 16:01:44 -07:00
Brennan Benson 4f00b21186
fix(mobile): restore Codex chat session identity (#11636)
* fix(mobile): restore Codex chat session identity

* fix(mobile): reconcile native chat session ownership
2026-07-31 00:40:11 -07:00
OrcaWin 6ae19be723
[P0] fix(terminal): pause hidden paired output (#11665) 2026-07-30 20:58:11 -07:00
Brennan Benson 0281496c6f
fix(remote): keep resumed agent tabs stable on headed hosts (#11448)
* fix(remote): keep resumed agent tabs stable

* fix(remote): retain resume identity from older snapshots

* fix(remote): refresh mirrored resume attribution

* fix(remote): preserve headed runtime agent tabs
2026-07-30 19:07:53 -07:00
Neil eb35c7fa3e
[P2] fix(runtime): stop broadcasting terminalSideEffects to clients without consumers (#11619)
* fix(runtime): stop broadcasting terminalSideEffects to clients without consumers

Co-authored-by: Orca <help@stably.ai>

* fix(runtime): keep mobile subscribers counted for side-effect availability

Excluding phones from the consumer-availability count added a new flip edge
(last desktop client leaving a phone-attached host), and the flip's tracker
rebuild cancels armed stale-working-title timers — stranding a 'working'
spinner on the phone. Availability counts all subscribers again; the
broadcast fix stays in the per-listener fan-out skip, now applied inside the
delivery callback so live-Set unsubscribe semantics and allocation-free
iteration are preserved.

Co-authored-by: Orca <help@stably.ai>

* fix(runtime): separate mobile title tracking from side-effect scans

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-30 17:58:09 -07:00
Dominik Mery 650dd48ec9
feat(cli): add `orca account add` / `account list` for headless hosts (Claude + Codex) (#9177)
* feat(cli): add `orca account add` / `account list` for headless hosts

The desktop "Add account" UI is disabled when the renderer drives a remote
runtime (isRemoteAccountScope === kind:'environment'), so a headless server
reached from a remote desktop/web client has no way to register managed
Claude accounts. Add a host-local CLI path that reuses the existing capture
logic:

- ClaudeAccountService.addAccountFromConfigDir(): register a managed account by
  capturing credentials from an already-authenticated CLAUDE_CONFIG_DIR instead
  of spawning the interactive browser login (extracted persist/rollback helpers
  shared with the existing add flow)
- RPC accounts.addClaudeFromConfigDir, bridged via OrcaRuntime; rejected for
  mobile device tokens (host-local only)
- `orca account add` runs `claude login` in the user's own terminal into a temp
  CLAUDE_CONFIG_DIR, then registers it via the local runtime; `orca account list`
  lists managed accounts

Switching (select) already works from a remote client; only adding was blocked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cli): support Codex in `orca account add` / `account list`

Mirror the Claude headless-account CLI for Codex:

- CodexAccountService.addAccountFromHome(): register a managed Codex account by
  importing auth.json from an already-authenticated CODEX_HOME, reusing a shared
  persist helper extracted from doAddAccount (no interactive login spawned here)
- RPC accounts.addCodexFromHome + OrcaRuntime.addCodexAccountFromHome bridge,
  rejected for mobile device tokens (host-local only)
- `orca account add --agent claude|codex` (default claude); `orca account list`
  now renders both Claude and Codex managed-account blocks

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: cover headless account-add capture paths (Claude + Codex)

- ClaudeAccountService.addAccountFromConfigDir: registers a managed account by
  capturing an authenticated CLAUDE_CONFIG_DIR; rejects and rolls back when the
  dir has no .credentials.json
- CodexAccountService.addAccountFromHome: imports auth.json from an
  authenticated CODEX_HOME into a managed account; rejects when auth.json is
  missing

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review on headless account-add flows

- CLI login spawn uses a shell on Windows so `.cmd` agent shims resolve without
  ENOENT (args are fixed literals, no injection risk)
- Claude capture skips the `.credentials.json` precheck on macOS, where creds
  live in the Keychain and captureAuthFromConfigDir reads them
- Claude add rollback is best-effort: a failed rematerialization no longer skips
  managed-auth cleanup or masks the original add error
- Codex persist restores the prior account/selection if a post-write sync or
  rate-limit refresh fails, so a failure can't leave a dangling managed account
- Codex sync passes the account's selection target (correct runtime for WSL)
- Add JSDoc to the new public service methods and CLI functions

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): harden headless account capture

* fix(cli): correct account command flag surface and interrupt cleanup

- `account` commands no longer accept or advertise the browser `--page`
  flag; `supportsBrowserPageFlag` allow-listed them by omission, so
  `orca account list --page x` was silently accepted and `--help`
  rendered a browser-only option
- account specs declare GLOBAL_FLAGS, so `--help`/`--json` render in the
  Options block like every other command
- `--agent` on `account add` documents the account provider instead of
  the terminal TUI-agent meaning inherited from the shared flag table
- a SIGINT/SIGTERM during the interactive login now removes the temp
  login dir (and restores the macOS Keychain item) before exiting 130;
  Node terminates without unwinding `finally`, which stranded live OAuth
  credentials on disk

* perf(cli): stop `account list` forcing a provider usage refresh

`accounts.list` awaited refreshAccountsForMobile(), which runs
fetchAll({ force: true }) — bypassing both the poll throttle and the
per-provider Retry-After gate — then O(N) serial per-account round
trips. `orca account list` renders only emails and the active ids, so
all of that work was discarded. The RPC now takes `refreshUsage`
(default true, so mobile and web keep the forced lane) and the CLI opts
out. Older hosts declare `params: null` and ignore the field, so a newer
CLI degrades to the previous behavior rather than failing.

Also documents on `account list` that `--environment` does not retarget
it, matching the host-local behavior of shouldIgnoreRemoteSelection.

* fix(cli): survive repeated and hangup signals during account add

withInterruptCleanup latched cleanup behind a boolean, so a second signal
got an already-resolved promise and its process.exit fired while the first
cleanup was still inside a Keychain call (3s each) — the temp dir's OAuth
credentials and the swapped macOS Keychain item both survived. Memoize the
cleanup promise so every signal awaits the same run, and register with
`on` instead of `once` so a second Ctrl-C cannot fall through to Node's
terminate-immediately default mid-cleanup.

Handle SIGHUP too. This flow exists for headless/SSH hosts, where the most
likely interrupt is the connection dropping, which hangs up the login's
terminal and previously ran no cleanup at all.

Warn when the interrupt lands after sign-in completed: the runtime finishes
the add independently of this process, so exiting 130 silently would tell
the user it was cancelled when the account may exist.

Reject a valueless `--agent`; the parser turns it into boolean true, which
silently ran a full OAuth login for Claude when the user asked for another
provider.

Also lock two behaviors the refactor changed but left uncovered: a WSL Codex
add must sync the WSL runtime lane rather than the default host lane, and
rename the account-spec help test to describe the Options block it actually
asserts rather than the usage string it never reads.

* fix(build): bundle the main modules the account CLI imports

electron-vite cleans out/main and emits only its declared entries, and
`build:desktop` runs it after `build:cli`, so the tsc-emitted copies of
`claude-accounts/keychain`, `codex-cli/command` and `win32-utils` were
deleted before packaging. Both `orca account add` and `orca account list`
then died at require time with "Cannot find module
'../../main/claude-accounts/keychain'" — reproduced against a real
`--serve` host. `agent-hooks/managed-agent-hook-controls` already carried
an entry for exactly this reason; these three were missing.

Adds a parity test so any future CLI import of a `src/main` module fails
in CI rather than at a user's shell after packaging.

* test: cover the desktop add-path behavior this PR changes

Both changes ride in the persist/rollback helpers the existing GUI add
flow shares with the new headless path, and neither had coverage:

- Claude: rollbackAddAccount now guards forceMaterializeCurrentSelection-
  ForRollback, so a rejecting rematerialization no longer replaces the
  real add error nor skips safeRemoveManagedAuth. Asserts the original
  error surfaces and the throwaway auth dir is gone.
- Codex: the desktop add now passes the account's selection target to
  syncForCurrentSelection, matching reauthenticate and select. Asserts
  the host target alongside the existing WSL assertion.

Both fail when the corresponding change is reverted.

* fix(cli): close the remaining account-add interrupt and preflight gaps

The round-1 interrupt fix detached the signal handlers before running the
finally-path cleanup, so the very window it was meant to protect — the two
serial 3s `security` calls plus rmSync on the success/error path — was
still covered only by Node's terminate-immediately default. Both review
lanes reproduced it independently. Await cleanup first, detach in a nested
finally, and stop a cleanup failure from replacing the error that actually
explains why the add failed.

Do not burn the interactive login when the runtime is unreachable. The
RuntimeClient is lazily constructed and the first call was the registration
RPC itself, so "Requires the Orca runtime to be running" was discovered
only after the user completed a full OAuth round trip. Preflight with the
now-cheap `accounts.list { refreshUsage: false }`.

Reject `--environment` / `--pairing-code` on `account add`.
shouldIgnoreRemoteSelection pins account commands to the local runtime, so
`orca account add --environment homelab` silently registered the account on
the laptop instead of the headless host it names.

Survive a daemon that cannot spawn `claude`. `allowFailure` is honored in
onClose but not onError, and unlike the GUI flow nothing has run `claude` in
the daemon before this point — so a launchd/systemd daemon with a minimal
PATH hard-failed an add the user had already signed in for, even though
identity resolves fine from the config dir's oauthAccount.

Also align the `--agent` help description with the global flag column.

* fix(cli): reject runtime selectors on `account list` too

`orca account list --environment homelab` was accepted and silently
listed the LOCAL machine's accounts, because shouldIgnoreRemoteSelection
pins account commands to the local runtime. Documenting that in --help
does not reach someone who already typed the flag, and answering with the
wrong host's accounts is the specific wrong answer they would act on.

`account add` already errors; this makes the new command group internally
consistent. The other groups in shouldIgnoreRemoteSelection keep their
existing silent-ignore behavior — changing those is not this PR's job.

* test: harden account-add signal tests and cover cleanup failure

- Identify the handler under test by set difference instead of
  `process.listeners(sig).at(-1)`. Vitest installs its own once-wrapped
  SIGINT teardown, so the positional lookup could grab the wrong listener;
  the helper also asserts exactly one new listener was added.
- Mock rmSync while keeping the real implementation by default, so the
  temp-dir assertions elsewhere stay honest.
- Cover that a cleanup failure in the `finally` does not replace the error
  explaining why the add failed. Fails when that guard is removed.

Completes the review loop's final round; the loop died on an API error
before it could commit this, and its `import()` type annotation would
have failed oxlint.

* fix(cli): harden interactive account add

* test(cli): make account cancellation coverage portable

* fix(cli): preserve merged skills runtime modules

---------

Co-authored-by: Dominik <marketing@gavaplast.sk>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-30 12:50:07 -07:00
Brennan Benson bbb3e7e5ee
fix(native-chat): mirror multi-line launch drafts into the chat composer (#11253)
* fix(native-chat): mirror multi-line launch drafts into the chat composer

seedNativeChatLaunchDraftForAgentTab rejected any text containing a newline,
so every Linear launch ("Linked Linear issue: X\n<url>") and any GitHub launch
with a typed note was invisible in chat. The rejection existed because the send
path pre-cleared the TUI with a single Ctrl+U, which cannot clear a buffer with
embedded newlines.

Orca injects the draft itself, so when the composer still holds exactly what was
injected the buffer already IS the message: the send becomes the submit key
alone — no clear, no paste, nothing that can concatenate, and multi-line submits
as one turn for free. Only the edited case needs real buffer replacement, and
that now clears every line and verifies against the agent's rendered input line
instead of firing blind.

Measured on real PTYs against Claude Code and codex (both agree exactly):
clearing N logical lines costs 2N-1 Ctrl+U. See src/shared/agent-tui-input-clear.ts
for the law, the sequences that do NOT work, and why an upper bound is safe.

* fix(native-chat): send the mobile clear burst as its own write

Live QA caught the bundled form failing: a multi-line burst prefixed onto the
body in the SAME terminal.send reached the agent as LITERAL Ctrl+U characters,
so the parked draft survived and the message arrived as
draft + 21x \x15 + body. Sending the burst as its own non-submitting write —
the shape the image paste has always used — clears as intended.

The body write's own single-Ctrl+U prefix is dropped once that dedicated clear
ran, for the same reason: a Ctrl+U immediately followed by body text in one
write lands as a literal control character and headed the received message.

Re-verified live end to end: received prompt is exactly the draft, one turn,
zero control characters.

* test(native-chat): invert the multi-line Linear launch-draft mirror expectation

The Linear work-item launch seeds `Linked Linear issue: ENG-42\n<url>\n`.
This test pinned the pre-relaxation rule (multi-line drafts withheld), which
the send path no longer needs now that it submits the TUI buffer in place or
clears every line first — so it asserted the exact behavior the fix removes.

Assert the seeded payload instead of absence, so the test fails if the mirror
regresses to single-line-only.

* fix(native-chat): preserve launch draft send contents

* fix(native-chat): preserve confirmed send queue ordering

* fix(native-chat): preserve send pacing after renderer stalls

* test(native-chat): align activation with multiline draft mirroring

* fix(native-chat): clear launch drafts from any cursor

* fix(native-chat): retire mobile-consumed launch drafts

* test(mobile): stabilize QR capacity boundary fixture
2026-07-30 11:08:56 -07:00
Neil 0fe1278244
fix(sidebar): stop background workspace creation from scrolling the sidebar (#11530)
* fix(sidebar): stop background workspace creation from scrolling the sidebar

Creating a workspace in the background still spawns its terminals, and the
renderer treated "no presentation stated" as "point the user at this
terminal" -- revealing (scrolling to) the owning workspace.

Split adoption from surfacing with an explicit surfaceOwner flag: background
worktree creates and worker dispatch adopt their tabs silently, while
`orca terminal create` keeps its discoverability reveal.

* fix(sidebar): keep split-mode setup panes silent, tighten surfaceOwner

Review catch: with setupScriptLaunchMode split-vertical/horizontal the Setup
terminal goes through splitTerminal, whose reveal payload had no surfaceOwner,
so a background create still scrolled the sidebar in that configuration.

Also narrow surfaceOwner to `false` so "surface it" can only be expressed by
omitting the key, and fold the repeated conditional spreads into ownerSurfacing.
2026-07-30 02:08:01 -07:00
Neil 5f642841fd
fix(worktrees): stop terminals after external deletion (#11237)
* fix(worktrees): stop terminals after external deletion

* fix(worktrees): request teardown per caller and revalidate uncached

Two defects let the original fix silently strand PTYs:

- teardown rode the scan's coalescing promise, so any caller that joined an
  in-flight scan purged its renderer state without ever asking for a sweep;
  it now runs per caller against its own known-id snapshot, deduped on the
  request it actually produces so fan-out still shares one host sweep.
- the runtime's authoritative recheck was served from the 30s worktree-scan
  cache, which can still list a directory git already dropped. The renderer
  purges either way, so a stale miss leaked those processes permanently.

Co-authored-by: Orca <help@stably.ai>

* perf(worktrees): enumerate the host once per teardown sweep

An agent cleaning up N workspaces made killAllProcessesForWorktree issue one
full provider enumeration per missing worktree: O(N) relay round-trips carrying
O(N^2) rows. At 30 worktrees over an 80ms-RTT SSH link that is 30 scans and
~1.3s of stalled teardown; it scales linearly from there.

Share one point-in-time process list across the sweep — every worktree in it is
already known-missing, so a single snapshot answers all of them. A failed scan
is never shared: it falls back to a per-caller scan so one transient relay error
cannot suppress the sweep for the whole batch. Pinned requirePhysicalStop:false
since that path re-lists after shutdown and must not read a pre-shutdown snapshot.

Co-authored-by: Orca <help@stably.ai>

* test(worktrees): pin the disconnected-SSH no-teardown invariant

main's new directSshAuthority gate bails before any refresh when an SSH target
is not connected. That is exactly the #10562 safety rule — "host unreachable"
must never be read as "worktree deleted" — so pin it: a disconnected target
issues no teardown RPC and keeps its renderer state.

Co-authored-by: Orca <help@stably.ai>

* fix(worktrees): keep selector grammar intact when scoping by connection

resolveRepoSelectorForConnection matched the selector as a bare repo id, so an
explicit connection identity silently changed the grammar: `path:` and `name:`
selectors resolved to repo_not_found on that path alone, losing the whole sweep.
A connection identity should only *narrow* the candidate set.

Extract the selector matching both paths now share, and stop re-resolving an
already-resolved repo: teardown rescanned via `id:<repo.id>`, which throws
selector_ambiguous when an id is duplicated across hosts even though the
caller's own selector was unambiguous.

Reported as a P2 by Greptile (as redundant work); it is load-bearing.

Co-authored-by: Orca <help@stably.ai>

* fix(worktrees): keep the shared snapshot out of provider internals

The snapshot proxy passed itself as the Reflect.get receiver, so prototype
methods invoked through it ran with `this` bound to the proxy. A provider whose
own shutdown() re-read state via `this.listProcesses()` would then silently get
this sweep's cached snapshot instead of the live host — batching leaking past
the calls it was built for.

Bind non-listProcesses members to the target so only the sweep's own calls share
the snapshot. No shipped provider does this today; the point is that adding one
must not quietly change teardown semantics.

Raised by Greptile as an undocumented implicit constraint; closed structurally
rather than by comment.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-30 02:05:13 -07:00
Brennan Benson f8b553b7d5
fix(agent-hooks): skip unavailable agent homes (#11442)
* fix(agent-hooks): skip unavailable agent homes

* refactor(agent-hooks): separate Pi and OMP home fix

* test(agent-hooks): update merged protocol harnesses

* fix(agent-hooks): avoid redundant reconciliation

* fix(agent-hooks): harden reconciliation and detection

* test(agent-hooks): cover settings reconciliation

* fix(agent-hooks): hydrate PATH for paired clients
2026-07-29 20:19:18 -07:00
OrcaWin bf894ef150
fix(remote): recover and safely park paired terminals (#11416) 2026-07-29 20:04:55 -07:00
Jinjing 74563b6498
feat(jira): link Jira issues from the workspace create dialog (#11296)
* Link Jira issues from workspace create dialog

Add Jira issue linking to workspace creation, matching existing GitHub and Linear workflows. Users can paste Jira issue URLs in the smart name field to auto-populate workspace names and link the issue to the created workspace/worktree.

Linked Jira issues appear on workspace cards via the new 'jira-issue' card property. Implements cancellable searches and summary reads to prevent stalled requests from blocking the shared Jira pool. Persists paired issue + source context metadata with validation of provider/site identity.

Fixes git-username rate-limit handling to reject malformed JSON responses so garbage never becomes branch prefixes.

* feat(jira): link issues during workspace creation

- Display linked Jira issues on worktree cards
- Fetch issue summaries and timestamps via Jira API
- Gate Jira linking behind runtime capability check
- Preserve user-typed names during async lookups

* Enforce git check-ref-format rules in login validation

Extend isBranchSafeHostedLogin to reject usernames that git rejects as
invalid branch components: trailing dots, consecutive dots, and .lock
suffix. Prevents invalid branch names from login usernames.

* Enforce filesystem filename cap for branch-safe logins

Loose refs store logins as single filenames, so the real constraint is the
255-byte filesystem cap, not git check-ref-format rules. This allows longer
provider-agnostic logins while staying platform-safe.
2026-07-29 19:50:18 -07:00
Yunqian Fan 791577861b
fix(project-host-setup): carry identity across hosts (#9413)
Allow setup when the selected project exists only on another host by carrying its validated provider identity with the request instead of reverse-parsing project IDs. Preserve host-qualified provider identity and reject mismatched payloads before linking.

Make linking atomic for local and runtime imports, including clone setup: roll back only newly registered repos and invalidate the same caches as canonical removal. Cover local, runtime, host-qualified identity, mismatch, clone rollback, and renderer routing paths.

Co-authored-by: fanyunqian.1 <fanyunqian.1@bytedance.com>
2026-07-29 18:57:50 -07:00
Brennan Benson 5517bfcbd2
fix(native-chat): make the launch-draft mirror reachable (#11222)
* fix(native-chat): make the launch-draft mirror reachable

Seed the chat-composer copy of unsent launch context on every originating
draft path, then let those launches open in chat by default.

Three paths delivered a draft to the TUI without mirroring it into chat:
folder-workspace create, the local argv-prefill branch of launchAgentInNewTab,
and the web-host equivalent. The first was invisible; the other two were hidden
only because draft launches were forced into terminal view.

The view-mode decision now gates on the same predicate as seeding
(canMirrorLaunchDraftToNativeChat), so a draft can never open in chat with a
composer chat would refuse to fill.

* fix(native-chat): gate draft view mode on argv-prefill launches too

The draft view-mode gate read `startup.draftPrompt`, which only the
post-ready-paste delivery sets. An argv-prefill launch carries its draft
inside `launchCommand`, so the gate never saw one and the tab opened in
chat unconditionally — a multi-line draft was correctly not seeded yet
still opened chat, leaving an empty composer beside a filled TUI input.

Adds `launchDraftText` to the activation startup payload as a view-mode-only
field, deliberately distinct from `draftPrompt` so it cannot double-deliver
the draft through pty-connection's bracketed paste, and sets it at all four
originating producers.

* fix(native-chat): reconcile backend draft launch tabs
2026-07-29 18:28:17 -07:00
Jinjing cbe8635f46
fix(worktrees): prevent deletion from blocking Orca (#11233)
* fix(worktrees): prevent deletion from blocking Orca

* test(worktrees): loosen async history-delete event-loop bound for CI

The main-thread safety check failed on a loaded runner when a single
timer gap hit ~48ms under the prior 30ms threshold. Keep the bound well
below a recursive sync-rm stall without treating CI jitter as a block.

* test(worktrees): measure history-delete critical path, not timer gaps

setInterval gaps during async rm of thousands of files still flake under
CI scheduling. deleteWorktreeHistoryDir is sync and must only rename, so
assert that critical-path wall time stays well below a recursive walk.

* fix(worktrees): prevent deletion from blocking Orca

Add timeout-based draining of watcher closes so SSH round-trip delays don't
indefinitely block the worktree removal path. Also: order durable temp-file
sweeps ahead of writes to reclaim orphans before accumulation, skip own-process
temps to avoid deleting live writes, swallow persistence errors so disk failures
don't cascade to query callers, and measure history-deletion progress by loop
turns rather than timer gaps to detect blocking on CI runners.

* fix(worktrees): prevent deletion from blocking Orca

Worktree deletion can now proceed even if filesystem watchers or history cleanup operations hang, preventing Orca from freezing. Changes:

- Fence install slots with tokens instead of counters so removals can abandon wedged installs without corrupting later removals
- Timeout-bound watcher unsubscribe operations with a shared drain budget
- Move JSON serialization of large usage caches from queue-time to write-time to avoid blocking main thread
- Async tombstone + schedule history tree deletion instead of blocking recursive rmSync during GC, preventing main-thread stalls ~10s after startup

* Extract usage cache writer into reusable durable snapshot class

Consolidates serialized durable-write and generation-veto logic from
three usage stores into UsageCacheSnapshotWriter. Eliminates duplication,
centralizes multi-MB JSON serialization on the main thread via write-queue
serialization, and vetoes superseded snapshots to avoid wasted rewrites.

* fix(worktrees): prevent deletion from blocking Orca

Worktree deletion used to recursively delete large session trees (hundreds
of MB) on the critical path, stalling the event loop. Instead, rename trees
into a `.pending-delete` tombstone queue and reclaim them asynchronously
off the removal's critical path.

Extracted host tree removal into a reusable helper (`removeHostTree`) that
centralizes Windows retry logic. Added usage-cache flush on quit to prevent
data loss when scans complete right before shutdown. Improved watcher
removal deadline management with reserved tail slices for the final
unsubscribe, and added retry logic for tombstone removals that fail once
under transient Windows locking.

* fix(history): retry failed session tree removals

Tombstoned session trees whose removal fails transiently (e.g., EBUSY
under Windows AV) are now re-queued in-process with bounded exponential
backoff instead of sitting until the next HistoryManager construction.
Prevents a single stuck tree from blocking the entire Orca process.
2026-07-29 18:21:26 -07:00
Jinjing 5f7807497e
feat(ssh): bound relay PTY output end to end (#11005)
* docs: design SSH relay PTY backpressure

* fix(ssh): bound relay frame decoding

* fix(relay): bound PTY output publication

* fix(ssh): bound PTY model admission

* fix(ssh): settle closed model admissions

* feat(ssh): negotiate bounded PTY consumer sessions

* fix(ssh): fence exit on renderer settlement

* feat(ssh): track PTY source credit end to end

* fix(ssh): recover bounded PTY output across reconnect

* feat(ssh): complete relay PTY output backpressure

* fix(ssh): close final PTY source credit races

* docs(ssh): record final backpressure validation

* feat(ssh): complete relay PTY source-credit lifecycle

* test(ssh): complete provider notification fixture

* fix(ssh): preserve terminal source credit across rotation

* fix(ssh): fail closed on recovery cancellation

* fix(ssh): prioritize mux control writes after drain

* fix(ssh): retire canceled relay restore deliveries

* fix(ssh): order exit cancellation cleanup

* fix(ssh): gate provisional source activation

* test(ssh): register mux drain-priority coverage

* fix(ssh): type stale owner recovery mismatches

* fix(ssh): close projection replacement races

* fix(relay): contain streaming edge failures

* fix(ssh): secure relay endpoint credentials

* docs(ssh): reconcile final backpressure lifecycle

* fix(ssh): bound main IPC output lifecycle

* fix(ssh): close recovery ownership gaps

* docs(ssh): record exact artifact validation

* fix(ssh): reject reclaimed snapshot replacements

* fix(ssh): fence model admission across reconnect

* fix(ssh): contain migration failure per PTY

* docs(ssh): record final exact-head validation

* test(ssh): align deploy fixtures with credential publication

* feat(ssh): add per-target bounded output setting

* fix(ssh): close source recovery review gaps

* fix(ssh): latch source credit environment override

* feat(ssh): make PTY source credit the default

* docs(ssh): record always-on relay validation

* docs(ssh): bind validation to current main

* test(ssh): grant source credit in IPC fixture

* test(ssh): grant source credit in fake relay

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-29 17:03:15 -07:00
OrcaWin 363e478909
fix(orchestration): preserve active workers across updates (#11271)
* fix(orchestration): preserve active workers across updates

* test(ssh): model absent legacy adoption

* test(orchestration): align compatibility contracts

* fix(windows): escape updater PowerShell booleans

* fix(windows): restore stock uninstall process check

* fix(orchestration): keep recovery off renderer startup barrier

* fix(orchestration): harden legacy recovery migration

* fix(orchestration): close recovery review gaps

* fix(orchestration): complete legacy worker cutover recovery

* fix(orchestration): preserve legacy workers across updates

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-29 11:31:35 -07:00