Commit Graph
737 Commits
Author SHA1 Message Date
e59a319ffe fix(sidebar): keep each project's entry-point workspace visible under "Hide sleeping" (#12257)
"Hide sleeping" swept each project's main workspace out of the sidebar as soon as
it had no live PTY, browser tab or agent — even with "Hide default branch" off.
For a project whose only row is that workspace (a folder workspace, a fresh
clone, a detached-HEAD main), the entire project vanished with no in-place way
back.

Adds a shared `isSleepingSweepExemptWorkspace` predicate keyed on
`isMainWorktree` rather than the branch name, so folder workspaces (no branch),
detached-HEAD mains, and SSH rows whose head/branch are blanked while a provider
is disconnected all stay put. Wired into `computeVisibleWorktreeIds` (sidebar,
Cmd+1-9, workspace board), the jump palette's duplicate inline pass, and mobile's
`filterWorktrees`.

Ships default-on with an escape hatch: a persisted
`alwaysShowDefaultBranchWorkspace` setting surfaced as "Except default branch"
under "Hide sleeping". Explicit "Hide default branch" still wins, since it
filters before the sleeping sweep.

Mobile reads the setting but never writes it back, so a desktop opt-out can't be
clobbered by a filter tap before the ui.get roundtrip lands.

Combines the two PRs open against #8873. #8966's exempt set is a strict subset of
this one, so its production diff was subsumed rather than ported; its jump-palette
render harness and e2e spec were carried over, and are the only such coverage here.

Fixes #8873
Closes #8966

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-08-03 22:55:17 -07:00
NeilandOrca 0927b9c156 fix(gitlab): load pipeline job traces in the Checks side panel (#7732) (#12266)
* test(repro): demonstrate #7732 GitLab pipeline job details never load in Checks panel

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

* fix(gitlab): load pipeline job traces in the Checks side panel (#7732)

Expanding a GitLab pipeline job in the Checks panel always showed
"No inline details are available for this check.": the mapper dropped the
numeric job id, `PRCheckDetail` had nowhere to carry it, and every consumer
called the GitHub check-runs API, which returns null for a GitLab job.

- carry `gitlabJobId` on `PRCheckDetail` and add the `gitlab-job:` branch to
  all three identity ladders (panel rows, editor tabs, fix-prompt keys) so
  same-stage jobs with no web_url stop colliding
- add a runtime-routed trace client so SSH/remote workspaces work, not just
  local IPC, and thread the MR's `projectRef` for fork pipelines
- bound the trace in main via the existing `sliceCheckLogTail` (now shared,
  not GitHub-only) so a multi-megabyte CI log never crosses the 1 MB
  transport frame cap; strip ANSI/section markers up to the CR only, which
  keeps each section's visible header and command echo
- render the excerpt inline instead of "Log tail available in full details."
- feed GitLab traces to "Fix with AI", which previously sent bare check names
- skip the fetch for jobs that cannot have a trace (created/manual/skipped)
  so GitLab's 404 does not replace the benign empty state, and re-arm a
  failed load when the job's state changes since the panel has no retry

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

* fix(gitlab): treat a missing job log as an empty log, not an error (#7732)

Round-1 review follow-up.

- a job canceled before it started (or whose log was erased/expired) is
  `completed`/`cancelled`, so the panel fetched its trace, GitLab answered 404,
  and `classifyGlabError`'s issue-edit copy ("Issue not found — it may have been
  deleted.") landed verbatim on the auto-expanded check row; main now maps that
  404 to an empty trace so the row keeps its benign empty state
- keep a missing project a real error (GitLab masks unauthorized projects as
  404) and add `classifyJobLogError` so 403/unknown failures stop borrowing
  issue-edit wording on a job-log read
- broaden the empty-log copy in all five catalogs: it now covers erased and
  expired logs, not only jobs that never ran
- e2e: derive the repro screenshot dir from `process.cwd()` (or an env
  override) instead of a hardcoded POSIX path to a throwaway worktree
- bound the raw trace before the ANSI/section passes so a multi-megabyte log
  is not scanned in full on the main-process event loop
- drop the redundant `if (repo)` in `handleFixChecksWithAI` and the now-dead
  "Log tail available in full details." catalog entry

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

* fix(gitlab): address review — project ref on reload, retry re-arm, IPC timeout

- Carry the MR's GitLab project ref on the check-details tab so reloading a
  fork/cross-project job tab fetches the trace from the pipeline's own project.
- Re-arm the sidebar retry when a details load resolves to null, not only when
  it throws; a detail-less row otherwise never retried after the job moved on.
- Bound the local `gl.jobTrace` IPC call with the same 30s timeout the runtime
  RPC path uses — glab runs without a subprocess timeout in main.
- Document that the trace 404 -> empty-log mapping is deliberately broad.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-03 22:48:37 -07:00
Brennan Benson e43bd6c4ad fix(worktrees): preserve folder PTY owner scope (#12429) 2026-08-03 22:29:19 -07:00
Jihwan KimandOrcaWin 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
50594c55a9 Stop the Windows Orca CLI from crashing when the environment carries both PATH and Path (#12218)
* fix(windows): stop the Orca CLI dying on a duplicated PATH/Path environment

The packaged Windows `orca.exe` launcher read
`ProcessStartInfo.EnvironmentVariables`, whose lazy getter copies the
case-sensitive process block into a case-insensitive dictionary via `.Add`.
An inherited block carrying both `PATH` and `Path` threw
`ArgumentException: Item has already been added. Key in dictionary: 'PATH'`,
so every `orca` invocation exited 1 before Electron started
(native/windows-cli-launcher/OrcaCliLauncher.cs:46, printed at :67).

The launcher now mutates its own environment with
`Environment.SetEnvironmentVariable` and never touches either
`ProcessStartInfo` env property, so `CreateProcess` passes a NULL environment
block and the child inherits the live one verbatim.

Orca was also minting the duplicate itself. `applyTerminalAttributionEnv`
read `baseEnv.PATH` and unconditionally wrote `baseEnv.PATH`, so a Windows
PTY that inherited `Path` got a second spelling; which one the child resolved
was non-deterministic. `createLaunchEnv` did the same and, because its read
always missed on Windows, shipped Agent Teams terminals a `PATH` containing
only the tmux shim dir.

`resolvePathEnvKey` (extracted from the existing precedent in
windows-environment-path.ts) now drives every PATH read and write in the PTY
env pipeline, and attribution collapses Windows onto the single OS-resolved
spelling. Off Windows the resolver always returns `PATH`, so POSIX behavior
is unchanged and a case-sensitive POSIX `Path` variable is never touched.

Closes #12046

* test(windows): track the launcher's own-environment marker

The #12046 fix moved ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER and ORCA_CLI_COMMAND
off ProcessStartInfo.EnvironmentVariables, but this asset test still pinned the
old dictionary writes and failed.

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

* fix(windows): follow the host block's PATH spelling on sparse daemon env patches

Resolving a path-less Windows env to `Path` handed the daemon's own
`{...process.env, ...opts.env}` merge both spellings when the host block spelt
`PATH`. Fall back to the host block's own key, and collapse again inside the
daemon since that merge happens after attribution.

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

* fix(windows): resolve the live PATH spelling by block order, not casing

Win32 resolves a duplicated variable by taking the first case-insensitive
match in the block, so `resolvePathEnvKey`'s hardcoded `Path`-first
preference targeted the shadowed spelling on the reporter's own
`["PATH","Path"]` block. Drop the attribution-side collapse with it: it
deleted the other spelling's value, and deleting the live key promotes
the shadowed one, so an env that stripped down to empty lost both.

* chore: drop unrelated merge formatting

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-03 20:45:23 -07:00
BingZ 79d3c847bd fix(runtime): attribute destructive close requests (#12238)
Attribute destructive runtime and daemon close diagnostics to the requesting client and exact target, record outcomes only after completion, and add reliability-gated attribution regressions.
2026-08-03 20:35:40 -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
OrcaWinandBrennan Benson 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 0586bab4f9 fix(mobile): bound terminal viewport resubscribe loop with backoff (STA-3337) (#12362)
* fix(mobile): bound terminal viewport resubscribe loop with backoff (STA-3337)

An empty scrollback frame with absent host dims was coerced to 80x24, which
never equals a phone viewport, arming a zero-delay unsubscribe/resubscribe
loop (~25/s) that broke long-press gestures and drained battery.

- Absent host dims now hold the stream instead of resubscribing.
- Fit resubscribes are budgeted per handle (3 attempts, escalating backoff)
  with an absence-gated refill mirroring the chat-side rearm bound; on
  exhaustion the view degrades visibly via toast instead of hot-looping.
- A fresh post-measure match counts as convergence instead of resubscribing.
- setTerminalModes keeps the Map identity when the mode is unchanged, so
  same-mode frames no longer re-render the session route.
- Host emits the subscriber viewport as scrollback dims when the snapshot
  and PTY size are both unavailable, so current hosts converge immediately.

* fix(mobile): cancel stale viewport retries after convergence
2026-08-03 17:55: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 BensonandOrcaWin 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
JinjingandOrca d7fe9d6bcc fix(ai-vault): support session scanning in SSH worktrees (#11004)
* fix(ai-vault): support session scanning in SSH worktrees

Add relay-native aiVault.listSessions scanning that discovers agent
sessions on SSH hosts. Includes fallback to filesystem crawl for
legacy relays, full cancellation support, result validation, and
scan coalescing to reduce redundant work.

* fix(ai-vault): scan sessions in SSH worktrees with coordinated cancellat

- Extract batching logic to `mapRemoteScanBatches` for reuse and proper cancellation checkpoints
- Move `AiVaultScanCoordinator` from relay to main to handle concurrent same-key requests with individual cancellation signals
- Report scope path truncation consistently across relay and SSH fallback paths
- Gracefully degrade relay handler on unsupported platforms instead of aborting startup
- Refactor issue display to separate blocking errors, scope notices, and skipped transcript counts

* fix(ai-vault): stabilize SSH session scan CI

Swallow async WSL relay stdin EPIPE so the live hook-relay shard no longer
fails after all tests pass. Merge main, resolve scan/relay conflicts, and
align cancellation/host-issue reporting with IPC expectations.

* fix(ai-vault): harden session scan cancellation, relay timeouts, and preemption

Thread the abort signal through every scan and parse path so superseded or
cancelled scans stop promptly instead of parsing every remaining transcript
for a caller that already left.  Replace the fragile message-text relay
timeout check with a typed error code so unrelated errors carrying the
phrase "timed out after" no longer suppress the filesystem fallback.  Fix
scan coordinator preemption so a forced Refresh in one window no longer
re-enters as a spurious cancellation in another.  Add a host-leg cache for
the all-hosts view and cap filesystem concurrency so a single slow remote
home cannot stall the whole merge.

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

* fix(ai-vault): use stable React keys for scan issue banners

Drop array-index keys so react-doctor/no-array-index-as-key passes.
Uniqueness comes from host, kind, agent, path, and message.

* fix(ai-vault): SSH session scanning with configurable depth limits

Implement depth-aware caching and proper scan boundaries to make SSH session
scanning reliable in worktrees. Users can now select between faster (250
sessions) and comprehensive (unlimited) history scans. The scanner:
- Deduplicates scans across relay, host leg, runtime, and renderer layers
- Reuses larger scans to serve smaller depth requests
- Properly bounds in-scope discovery per-limit
- Fixes timeout enforcement when SSH providers ignore abort signals

* Move sessionLimit ref update to useLayoutEffect

Keep render pure for React Doctor by deferring ref updates to
a layout effect, which still executes before render-dependent
effects that consume the ref.

* fix(adhoc): stamp version prefix from main, not the feature branch

Adhoc builds check out arbitrary refs whose package.json often lags
version bumps (e.g. 1.4.165-rc.0 while main is 1.4.168-rc.1). Hourly
always builds main so it already tracks the product line; adhoc now
resolves the base version from origin/main (or ORCA_ADHOC_BASE_VERSION)
so branch builds share that prefix.

* Revert "fix(adhoc): stamp version prefix from main, not the feature branch"

This reverts commit a26a18eb3fd83f7e7d2db9a6a7c3e02e0f79089a.

* fix(ai-vault): fix scoped backfill and coordinator race conditions

Resolve race where the last waiter leaving could abort an already-settled scan (add `settled` flag). Redesign scoped session backfill to keep searching through newer files until the scope reaches its requested session quota instead of stopping at the candidate limit; out-of-scope files no longer consume the scope budget. Centralize scan limit normalization and fix error classification for cancelled scans using the proper helper instead of checking Error.name. Disambiguate cache keys using JSON and add cancellation check after scope discovery phase.

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-03 16:17:00 -07:00
Jinwoo HongandOrcaWin a4944f5343 fix(orchestration): retain update settlement authority (#12336)
* fix(orchestration): retain update settlement authority

* test(orchestration): register update settlement gate

* fix(orchestration): close update settlement audit gaps

* test(orchestration): correct update settlement evidence

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-03 14:00:27 -07:00
Brennan Benson 056c2d9496 fix(runtime): bind mobile WS listener to loopback until pairing (STA-2370) (#11956)
The runtime RPC WebSocket listener bound to 0.0.0.0:6769 at startup, so a
desktop with no paired device was reachable from the whole LAN before the
user opted in. Default the bind to 127.0.0.1 and widen to all interfaces
only on an explicit opt-in:

- createMobilePairingOffer / getRuntimePairingUrl widen (ensureNetworkExposure)
  before advertising a LAN endpoint; the rebind reuses the resolved port so an
  already-issued offer stays valid, and concurrent offers share one rebind.
- orca serve and E2E set exposeNetworkByDefault to bind wide at startup.
- A previously-connected device (lastSeenAt > 0) rebinds wide at startup so
  reconnect after restart keeps working; a pending/never-connected offer does
  not persist exposure across a restart.

The advertised pairing endpoint still resolves to a concrete interface address,
never the 0.0.0.0 bind host.
2026-08-03 13:06:11 -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
Brennan Benson 922268af10 fix(native-chat): stop clipping assistant text blocks at the tool-preview cap (#12159)
* fix(native-chat): stop clipping assistant text blocks at the tool-preview cap

Long assistant messages read over a paired connection (headless orca
serve viewed from desktop or mobile) were cut at 4,000 chars with a
'… (truncated)' marker and no way to read the rest. The mobile payload
diet in nativeChat RPC applied the tool-preview char cap to text blocks,
which are the fully rendered message body. Give text blocks their own
64k safety ceiling so real replies pass through whole while pathological
multi-hundred-KB blocks still can't freeze the phone.

Fixes STA-3230

* test(native-chat): cover long text stream frames
2026-08-02 19:17:31 -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
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
NeilandOrca 484273844a feat(updater): add an adhoc release channel for branch builds (#12051)
* feat(updater): add an adhoc release channel for branch builds

Hourly covers main. This covers everything that is not main yet: a
dispatchable macOS build of an unlanded branch, published to
stablyai/orca-adhoc, so the team can run an experimental feature for a
few days instead of reasoning about it from a diff.

Adhoc sits at the bottom of the version order — 'adhoc' < 'hourly' <
'rc' < stable — so no routine check can walk anyone onto somebody's
branch; only an explicit pinned jump reaches one. It gets its own repo
rather than sharing orca-hourly's, because a branch build must not
appear in the list a developer riding main is looking at.

Signed and notarized exactly like hourly, for the same reason: macOS
anchors a notarized app's TCC grants on identifier + team, so an
unnotarized build reads as a new client and silently loses file access
under Documents/Desktop/Downloads.

Tags stamp to the second rather than the minute. Hourly runs under a
concurrency group and cannot overlap itself; adhoc builds are dispatched
on demand, so two people cutting from different branches inside one
minute is ordinary — and a minute-resolution tag would collide and fail
the second build after its whole pack-and-notarize run.

Channel-specific behaviour now derives from one DEDICATED_REPO_CHANNELS
list: repo mapping, macOS-only support, and UpdateSource. The RPC schema
that validates releaseChannelOverride was a hand-copied enum missing the
new channel, which would have rejected the override on its way to the
main process; it reads the predicate now.

* fix(updater): merge the duplicated shared/types import

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

* fix(ci): default the adhoc build ref to the dispatch branch

The Actions UI puts its own "Use workflow from" branch picker directly
above the ref field, and picking a branch there is what most people read
as "build this". Making the field optional means the obvious action is
also the correct one; naming a branch explicitly still wins, so main's
copy of the workflow runs rather than a stale one on an old branch.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-02 01:46:51 -07:00
Jinwoo HongandOrcaWin 5390224bf7 fix(relay): declare reconnection to the director's verified fast lane (#12086)
Recovery and broker open now send the optional reconnect hint so the
director admits already-assigned hosts through its bounded fast lane
(orca-cloud#212) instead of the placement queue that starved session
recovery during the 2026-08 incident. A rolled-back director that
rejects the hinted field gets one unhinted retry.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-02 01:39:03 -07:00
OrcaWin 4a76565a35 fix(terminal): bound paired-client renderer work (#12081) 2026-08-02 01:02:26 -07:00
Jinwoo HongandOrcaWin 714bcbe43f fix(relay): make desktop control lifecycle provable and self-healing (#12076)
- RelayControlOrigin.activate rejects controls whose socket closed before
  activation (hello-ack and close in one ws parser turn previously published
  a dead control with no recovery path)
- RelayControlClient gains a 75s inbound-silence watchdog mirroring the
  relay's ping contract, so dead or server-side-unindexed sockets terminate
  and trigger origin recovery
- RelayAuthCoordinator only republishes 'registered' when the owned broker
  proves a live control, and logs reconcile failures instead of swallowing
  them; the origin pool logs recovery-attempt failures
- Host-proof validation reports the failing check by name (never values),
  keeping main's 30s skew bounds
- isLive() plumbed client -> origin -> pool -> broker

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-02 00:40:32 -07:00
NeilandOrca 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
Jinjingandgatsby74 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
JinjingandOrca 5c0195af64 Bound remote watcher fan-out and defer File Explorer refreshes (#11908)
* batch remote watcher events and defer File Explorer refreshes

Remote filesystem watcher events now batch with the shared 150ms trailing and 500ms
max-wait window, coalescing per-path like local events. File Explorer tree and
directory refreshes are scheduled with debounce and transport-aware concurrency caps
(16 local, 8 runtime, 4 SSH). Stale directory cache tracking prevents trusting
collapsed listings skipped by full refresh; they are re-read on re-expansion. Relay
implements a 15-minute idle-only grace cap for zero-PTY relays via PTY pool
lifecycle tracking, independent of explicitly configured grace time.

* fix(watch/relay): bound remote watcher fan-out and read the live relay grace

Three P1 fixes from the SSH/remote freeze audit:

- Remote watchers now debounce on the same 150/500 window as local ones
  (finding D), and every teardown path drops the trailing flush timer
  instead of letting it fire into a dead watch. The deferred send is
  wrapped so a frame disposed mid-window can't escape as a fatal
  main-process exception.
- File Explorer refreshes are scheduled and concurrency-capped rather
  than fanned out unbounded over expanded dirs (finding C). Local
  transports use a zero window, since main already coalesced the burst.
- relay.startGrace reads ptyHandler.configuredGraceTimeMs instead of the
  launch-time argv closure, so a grace raised after launch is honored.
  The branch selection moves to relay-grace-branch.ts because relay.ts
  has no exports and calls main() at import, making it untestable.
  Consequence: a host-sleep relay holding zero PTYs now exits after the
  idle cap. Pinned by test and documented in
  docs/reference/relay-grace-time-reconfiguration.md.

Also drops the duplicated 150/500/5000 constants in the runtime-RPC
batcher in favor of the shared window module.

* docs(relay): correct grace-reconfiguration line numbers after the relay.ts edit

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

* refactor(file-explorer): use useMemo for paths; remove relay reference

Replace manual ref-based caching with proper React hooks for content-stable path memoization. Remove outdated relay grace-time reference documentation from code review cycle.

* rm design doc

* fix(remote-watcher): prevent stranded timer after close

An in-flight provider receive can land after the batch is torn down.
Without a guard, pushing events to a closed batch would re-arm a timer
that would never be cleared, stranding the task indefinitely. Track the
closed state and skip pushes after close().

Relay.ts comment clarifies why pool watches remain registered during
grace-period shutdown deferral — the socket server stays listening so
a reconnecting client can cancel the grace and resume.

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-01 11:55:58 -07:00
NeilandBrennan Benson 6e7ceafd07 perf(mobile): avoid unchanged worktree catalog payloads (#11735)
* perf(mobile): avoid unchanged worktree catalog payloads

* fix(mobile): isolate catalog snapshots by limit

* review: reassert host truth on unchanged polls; content-address snapshots

Client — the `changed` gate meant an unchanged poll skipped setWorktrees /
setLastKnownWorktrees / setCachedWorktrees, so optimistic local edits
(togglePin, handleDeleteWorktree's failure re-add) and the #8498 cache guard
were no longer repaired while the host catalog was stable. The gate bought
nothing: setCachedWorktrees is an in-memory Map write and areWorktreeListsEqual
already ran every poll, so the steady state still short-circuits on array
identity. All wire savings are unaffected. admit() now just returns the
confirmed rows and HostScreen applies them exactly as it did pre-PR.

Also on the client:
- a stale response from a superseded client/host no longer clears the token the
  current client/host just established
- discriminate on `worktrees` rather than on `'unchanged' in response`, so a
  future catalog field named `unchanged` can't reclassify a full response
- useRef over useMemo for the snapshot client; React may discard memoized values
- hoist WORKTREE_PS_FULL_LIMIT so the truncates-at-200 rationale travels with it

Host — replace the per-limit snapshot cache with a content-addressed id (ETag
semantics). Ownership lives in the id, so concurrent clients, differing limits,
and runtime restarts are correct by construction; this drops the LRU, the
eviction policy, the per-runtime WeakMap, and the retention of up to 8 full
catalogs. The remaining cache is a pure memo: because ids derive from content,
dropping or thrashing it costs CPU and nothing else. Keeping the memo also
keeps the measured steady-state cost — hashing every poll instead measured
2.24ms vs 0.75ms for the compare on a 310KB catalog.

Verified: mobile 2784 passed / 3 skipped, src/main/runtime/rpc 1064 passed,
node + mobile typechecks, oxlint, oxfmt, max-lines ratchet.

* fix(runtime): isolate catalog snapshot memo

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-31 23:12:13 -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
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
Jinjing 1c8908b791 Fix orchestration gate authorization to scope by Run binding (#11802)
* fix(orchestration): gate methods route calls to the caller's Run with `f

Gates are Run-scoped state; every gate command now resolves the caller's active Run
(via pane binding or explicit --from flag) and authorizes within that Run's scope.
Settled adopted work no longer requires --takeover-legacy, and the legacy coordinator
fence respects both binding-based and attestation-based proof of authority.

* fix(orchestration): gate methods route calls to the caller's Run with at

Gate and run methods now verify that declared terminal handles match the caller's
attested identity, preventing spoofing of other coordinators. Extracted shared
`resolveRunScope` to enforce one authorization rule across all orchestration
mutations. Added comprehensive regression tests for #11745.
2026-07-31 10:56:20 -07:00
NeilandOrca d34bbd7917 fix(orchestration): route the legacy coordinator gate at the caller's own Run (#11745)
* fix(orchestration): route the legacy gate at the caller's own Run

The retained-legacy-coordinator gate treated an unnamed Run as the adopted
Run, so callers with no relation to it were fenced with legacy_read_only,
and the adopted Run's NULL coordinator made the owner escape hatch
unreachable.

Resolve the caller's bound Run first and keep the adopted Run only as the
unbound fallback, and treat an unclaimed adopted Run as free — the same
rule bindingMatches() already applies 100 lines down.

* refactor(orchestration): pass the open db handle into boundRunId

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-31 02:48:07 -07:00
OrcaWin 9a2676023c fix(orchestration): prefer current authority over legacy fallback (#11737) 2026-07-31 01:56:13 -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
Neil f998f7ec62 feat(updater): add hourly dev channel and build switching (#11250)
* feat(updater): add hourly dev channel and build switching

Adds an hourly macOS build channel plus a dev-only surface for switching
update channels and jumping to any published build, including older ones.

Hourly builds publish to a separate stablyai/orca-hourly repo. The routine
update path resolves tags from the main repo's releases atom feed, which
exposes only its 10 newest entries — 24 hourly tags a day would evict every
stable/RC entry there and leave real users with nothing to update to.

Hourly artifacts carry the release bundle id and Developer ID signature so
Squirrel.Mac can swap them in place; only notarization is skipped, which
in-place updates never check.

Version tails are stripped to the base (1.4.160-hourly.<stamp>, not
1.4.160-rc.3-hourly.<stamp>) so hourlies sort below both rc.N and stable and
are reachable only by an explicit pinned jump, never by an ordinary check.

The picker is revealed by Option-clicking the Updates header, matching the
Help menu's existing hidden admin affordance. Pinned jumps set allowDowngrade
and release the feed on every settle path so a jump can never leave background
checks permanently deferred.

* chore(hourly): create orca-hourly and add token provisioning script

Adds setup-hourly-release-token.sh, which provisions HOURLY_RELEASE_TOKEN
without the value ever reaching stdout, argv, or shell history: it is read
with `read -rs`, passed to gh through GH_TOKEN in the environment rather than
as an argument (argv is world-readable via ps), piped into `gh secret set` on
stdin, and scrubbed by an EXIT trap.

Verification creates and deletes a draft release in orca-hourly to prove
Contents:write for real rather than trusting the permission checkbox. Drafts
are absent from the releases atom feed, so the probe cannot disturb users.

Refuses to run without a controlling terminal instead of falling through
having set nothing, and refuses to run under xtrace, which would echo the
token on every expansion.

* fix(updater): address review feedback on the hourly channel

Renderer:
- Guard listBuilds against out-of-order responses. activeChannel flips once
  getVersion resolves, and rapid channel clicks stack requests, so a slower
  earlier load could land last and fill the list with builds from a channel
  the picker was no longer showing.
- Selecting the running build's own channel now clears the override instead
  of pinning it. There was previously no way back to "follow this build's
  channel", so merely opening the panel left background checks pinned.
- Validate releaseChannelOverride on hydration, matching every other
  enum-like field in that function.

Main:
- Exclude pinned jumps from recordCompletedUpdateCheck() in update-available.
  A dev browsing the picker was persisting lastUpdateCheckAt and suppressing
  the next real background check for a full day.
- parseHourlyVersionStamp now anchors on the whole version and round-trips
  the parsed fields. It accepted garbage prefixes, and Date.UTC rolled
  impossible dates forward, so ...hourly.202602300000 rendered as March 2.

Workflow:
- Publish into a draft and flip it live only after the manifest check. The
  window between creating the release and verifying its assets previously
  exposed a tag the picker would offer and the download would 404 on; a
  draft is invisible to listReleaseBuilds, so a job that dies in that
  window — including a hard kill by the job timeout, which runs no cleanup
  step — leaves nothing user-visible behind.
- Add a failure handler that discards the draft, gated on the publish step
  not having succeeded so a later prune failure cannot delete a live release.
- Align retry budgets with the job timeout (was 60min against a worst case
  of ~185min, so a mid-retry kill skipped the cleanup that step exists for).
- Exclude drafts from the freshness and retention queries.
- persist-credentials: false; the job only reads this repo and never pushes.

* refactor(hourly): authenticate with a GitHub App instead of a PAT

A fine-grained PAT expires, and the hourly build would then fail silently on
a schedule nobody watches. A GitHub App's private key has no expiry, so this
is set up once. It is also owned by the org rather than by the person who
created it, so the credential survives that person leaving.

The workflow mints a short-lived installation token via
actions/create-github-app-token and passes it as GH_TOKEN. Installation
tokens live one hour, which is ample: this job runs no tests, no
notarization, and no Windows signing, so it is pack + upload. The retry
budgets and job timeout are re-sized to that reality rather than copied from
the release pipeline, whose 3x45 publish budget exists for notarization and
SignPath.

setup-hourly-release-token.sh now provisions HOURLY_RELEASE_APP_ID and
HOURLY_RELEASE_APP_PRIVATE_KEY. The key is redirected from a file straight
into `gh secret set` on stdin, so its contents never enter a shell variable,
argv, or the terminal.

* fix(hourly): make the xtrace guard fire and cover cancelled runs

The xtrace guard disabled tracing before testing for it, so `[[ -o xtrace ]]`
read the state the previous line had just cleared and never fired. `bash -x`
ran straight through, tracing exactly the key handling the guard exists to
prevent. Test first, then disable.

The draft cleanup only ran on failure(), but a run stopped from the Actions
UI is cancelled(), not failed — a manual cancel mid-publish stranded the
draft. Cover both.
2026-07-30 22:53:02 -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
NeilandOrca 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
NeilandOrca 6442a9f649 fix(persistence): backfill the jira-issue workspace-card property for upgraded profiles (#11618)
Co-authored-by: Orca <help@stably.ai>
2026-07-30 17:56:58 -07:00
Brennan Benson d4cfee76be Add audit-only daemon incarnation evidence (#11606)
* feat(daemon): add audit incarnation evidence

* fix(daemon): isolate audit evidence observers
2026-07-30 16:02:08 -07:00
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
Jinjing 9eede0084d fix(relay): refuse silent fallback when pairing invite fails (#11528)
* fix(relay): refuse silent fallback when pairing invite fails

When Orca Relay pairing fails, don't silently degrade to a LAN-only QR under the Relay label. Instead, surface structured failure information so the UI can clearly inform the user and offer recovery options.

* fix issues
2026-07-30 02:13:47 -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