- Scope pruneLastVisitedTimestamps per-repo so not-yet-hydrated SSH repos retain persisted Cmd+J recency instead of being wiped at startup.
- Parse lastVisitedAtByWorktreeId leniently: drop only bad entries rather than failing the whole workspace session on one corrupted timestamp.
- Route sidebar agent-tab clicks through activateAndRevealWorktree so cross-repo activation and nav history are not silently skipped.
Co-authored-by: Orca <help@stably.ai>
Adds ssh:needsPassphrasePrompt IPC so tab focus / Cmd+J auto-connect
waits for user-driven connect when a passphrase dialog would otherwise
pop unprompted. No-passphrase targets continue auto-connecting.
Co-authored-by: Orca <help@stably.ai>
* feat(orchestration): transport keepalive + delivered_at split for check --wait
Implements the four §3 fixes from the check-wait design doc:
- §3.1 Transport keepalive: long-poll RPCs (orchestration.check --wait) emit
`{"_keepalive":true}` frames every 10s so neither server nor client tears
the socket down on idle. A `longPoll` admission counter capped at 16 fails
fast with `runtime_busy` when saturated; an AbortController wired through
the RPC dispatcher cancels the inner waiter the moment the socket closes.
- §3.2 delivered_at split: push-on-idle now stamps `delivered_at` instead of
flipping `read`, so the check caller remains the sole consumer of its
queue. Adds a synchronous idempotent schema migration that hard-fails on
error.
- §3.3 inbox/check parity: `orchestration inbox --terminal <handle>` and
`orchestration check --all` agree on the same rows (sequence DESC, no
mark-read). `check --unread=false` kept for one release as a compat shim.
- §3.4 CLI heartbeat: `orca orchestration check --wait` emits JSON heartbeat
lines to stderr every 15s so Claude Code's Bash tool sees continuous
output and doesn't auto-background the subprocess.
Tests: extends runtime-rpc, orca-runtime, envelope-schema, orchestration
method, and formatter suites; adds a subprocess test that spawns the built
CLI and verifies stderr line-flushing, heartbeat cadence, and stdout
cleanliness end-to-end.
Co-authored-by: Orca <help@stably.ai>
* feat(orchestration): preamble rules + heartbeat schema
- Preamble (#7, #15, #9): worker_done body ("3-sentence summary" + reportPath),
BEHAVIOR RULE #1 forbidding AskUserQuestion, heartbeat every 5 minutes with
taskId+dispatchId payload, AFTER YOU SEND grace window.
- Schema v2 migration: adds 'heartbeat' to messages.type CHECK, adds
dispatch_contexts.last_heartbeat_at, gated by user_version PRAGMA with
transactional rebuild + explicit CREATE INDEX to avoid silent perf regress.
- DB helpers: recordHeartbeat (dispatched-only), getStaleDispatches,
getThreadMessagesFor (thread+handle scoped for ask).
Co-authored-by: Orca <help@stably.ai>
* feat(orchestration): coordinator heartbeat + stale detector
Handle incoming 'heartbeat' messages by calling recordHeartbeat keyed on
payload.dispatchId (strict — log-and-skip if missing, no taskId fallback so
a straggler heartbeat from a previously-failed dispatch cannot mask a hung
retry per §5.3.4). On every tick after the 10-minute threshold, emit one
log per stale dispatched row — no auto-fail.
Also threads dispatchId through buildDispatchPreamble so workers can
attribute their heartbeats back to the correct dispatch context.
Co-authored-by: Orca <help@stably.ai>
* feat(orchestration): orca orchestration ask verb
Adds a CLI verb that sends a decision_gate message and blocks on the
coordinator's reply, scoped to the outbound message's thread. Group
addresses (@all, @idle, …) are rejected — fan-out questions must use
send --type decision_gate explicitly.
--json emits bare single-line JSON (bypassing printResult) so workers can
pipe `orca orchestration ask … --json | jq -r .answer` without unwrapping
an RPC envelope; human mode prints just the answer. On timeout the verb
exits 1 and returns {answer: null, timedOut: true}.
This is the CLI surface BEHAVIOR RULE #1 in the dispatch preamble points
workers at instead of AskUserQuestion.
Co-authored-by: Orca <help@stably.ai>
* feat(orchestration): QoL bundle — preamble visibility, status enum, dispatch cross-ref, inbox --full
Addresses four items from ORCHESTRATOR_FEEDBACK:
- #5 preamble visibility: `dispatch-show --preamble` regenerates the preamble
text for a task; `dispatch --inject --dry-run` previews without mutating
state; `dispatch --return-preamble` echoes the injected preamble in the JSON
response so coordinators can audit what a worker received.
- #6 status enum validation: CLI rejects unknown `task-update --status` values
with `invalid status '<x>', expected one of: pending, ready, dispatched,
completed, failed, blocked` before the RPC's generic Zod message. Valid
statuses are listed under Notes in `task-update --help`.
- #13 task-list dispatch cross-ref: `task-list --json` now includes
`assignee_handle` and `dispatch_id` for tasks in status=dispatched via a
read-only LEFT JOIN on dispatch_contexts. Non-dispatched rows keep their
legacy shape so existing consumers are unaffected.
- #14 inbox body visibility: `inbox --full` prints body + payload verbatim;
default output is unchanged (id/from/to/subject only).
No DB migrations; join-only change on dispatch_contexts so the sibling
preamble PR's `last_heartbeat_at` column addition will not conflict.
Co-authored-by: Orca <help@stably.ai>
* fix(worktree): prevent stale-base worktree creation and dispatch
Addresses feedback #16 per DESIGN_DOC_STALE_BASE_FIX.md §0. Four v1
components coordinated by a single shared fetch cache on the runtime:
1. Concurrent-fetch-with-gate in UI create path: `createLocalWorktree`
fires `git fetch` BEFORE the suffix loop / PR probe / path
resolution, then awaits right before `addWorktree` so the new branch
always spawns from a fresh remote tip. Renderer sees a two-phase
spinner via the new `createWorktree:progress` IPC event. The cache
is a `Map<repoPath::remote, Promise<void>>` + 30s success-only
timestamp on `OrcaRuntimeService` (§7.1 — shared with dispatch).
2. Dispatch pre-flight drift guard in `Coordinator.dispatchTask`:
probes `rev-list --left-right --count` against the target worktree
and silently returns (preserves `ready`, no circuit-breaker burn)
when `behind > 20` unless the task spec carries
`allow-stale-base: true`. Parsing strips the flag so it never leaks
into the worker's `--- TASK ---` block.
3. Preamble drift section: populated only when dispatch detected drift.
Workers see `--- BASE DRIFT ---` with the N-most-recent subjects
they don't have, so they can pull them in before running.
4. §3.3 Lifecycle: `.finally()` evicts Map entries on BOTH success and
rejection; timestamp is written ONLY on success. Prevents a single
DNS hiccup from wedging every future create on the repo until
restart, and keeps the freshness window honest.
Defers the DB `allow_stale_base` column (§0.2) and the create-time
warn toast; both can layer in later without migration.
Tests: 35 new/updated unit tests covering drift preamble, dispatch
refusal, spec-text flag parsing, fetch Map eviction after rejection,
freshness-window short-circuit, and concurrent-caller serialization.
Co-authored-by: Orca <help@stably.ai>
* test(orchestration): seed v2 DB in migration hard-fail test
After consolidating the schema bump, fresh DBs are initialized directly at
v3 via createTables(), so the v2→v3 ALTER TABLE is skipped on new installs
and the prior test's stub never fired. Seed a v2-shape file on disk so the
guarded ALTER actually runs and the "simulated migration failure" stub
propagates as intended.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Delete agent-nesting-options.html, agent-nesting-stacked.html, and
agent-row-design.html — leftover design mockups not referenced anywhere
in the codebase.
Co-authored-by: Orca <help@stably.ai>
* feat(shortcuts): cycle Cmd/Ctrl+Shift+]/[ within current tab type by default
Extract a shared getNextTabWithinActiveType selector into a new
tab-type-cycle.ts helper and route both useTerminalShortcuts.ts and
ipc-tab-switch.ts through it. Cmd/Ctrl+Shift+]/[ now cycles within the
active tab's type (terminal cycles only terminals, editor cycles only
editor tabs, browser cycles only browser tabs). Ctrl+PageDown/Up
remains the dedicated terminal-only chord (PR #1094) regardless of
focus.
useTerminalShortcuts gains optional activeBrowserTabId and
onActivateBrowserTab parameters so browser tabs can dispatch through
the same path. The default behavior matches VS Code's
TerminalContextKeys.focus-gated cycling and Superset's per-scope
chord registry referenced in the issue.
Adds tab-type-cycle.test.ts covering single-type, mixed-type, and
single-tab no-op cases. ipc-tab-switch.test.ts updated to assert
same-type cycling under the new selector.
Closes#1100
* fix(shortcuts): use direction-aware fallback when active tab is missing
When the active tab id is not present in the same-type subset (e.g.
during hydration when an editor is active and Cmd+Shift+]/[ targets
terminals), findIndex returns -1 and the previous modulo math made
backward navigation land on the second-to-last tab instead of the
last. Branch on the -1 case to return the last tab for direction=-1
and the first tab for direction=+1, so both directions are
predictable. Adds a regression test covering 3 terminal tabs with no
active match.
* feat(shortcuts): add Cmd/Ctrl+Alt+[/] to cycle across all tab types
Adds an "all types" variant of the tab cycle chord so users can page
through every tab in the active group regardless of type (terminal /
editor / browser), complementing the type-scoped Cmd/Ctrl+Shift+[/].
- Wires the chord through the main-window keydown handler, the browser
guest shortcut forwarder, and a new ui:switchTabAcrossAllTypes IPC
channel
- Factors getNextTabAcrossAllTypes alongside getNextTabWithinActiveType
and shares the ipc-tab-switch dispatch logic via resolveCycleContext
and applyNextTab helpers to avoid drift between the two chords
- Narrows useTerminalShortcuts activeTabType to 'terminal' | 'editor'
(the hook's unifiedTabs never contains browsers) and drops the
unsound TypeCyclableTab cast
- Updates ShortcutsPane to list the new chord and expands tests
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* fix(install): regenerate cpu-features buildcheck.gypi before rebuild
cpu-features@0.0.10 ships without buildcheck.gypi and relies on its
own install script to generate it via `node buildcheck.js` before
node-gyp runs. @electron/rebuild with force:true calls node-gyp
directly and bypasses that hook, so a missing gypi (fresh install,
store prune, or prior failed run) aborts postinstall with
"buildcheck.gypi not found".
Generate the file ourselves for every cpu-features copy under .pnpm/
before invoking @electron/rebuild on platforms where cpu-features is
not in ignoreModules.
Co-authored-by: Orca <help@stably.ai>
* fix(install): satisfy oxlint curly rule
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
- release-cut.yml: require `^v[0-9]` tag shape when picking "latest
stable" from GitHub releases. Previously only `-rc.*` was excluded, so
publishing `mobile-v0.0.1` made it the latest stable, `strip_pre()`
reduced it to `mobile`, `Number("mobile")` → NaN → 0, and a patch-bump
produced `v0.0.1` (run 25304336767).
- homebrew-bump.yml: drop the blanket `release.published` trigger and
add tag-shape guards. Any published release in this repo (including
mobile) would otherwise fire the tap bump. Workflow_call from the
desktop pipeline is the only intended path now.
- Inline create-release / build / publish-release / e2e / homebrew-bump
jobs from release.yml into release-cut.yml, and delete release.yml.
Co-authored-by: Orca <help@stably.ai>
Typing a path like `Documents/orca-internal` in the browse filter now resolves the path on the remote instead of producing "No matches". Supports `~`, absolute, and relative paths with live preview and Enter to navigate.
Co-authored-by: Orca <help@stably.ai>
* fix(cmd-j): rank empty-query worktrees by focus recency
Persist a per-worktree focus-recency timestamp and use it as the primary
ordering signal for Cmd+J's empty-query Worktrees section, so SSH and
other quiet worktrees surface based on user focus rather than background
activity. See docs/cmd-j-empty-query-ordering.md.
Co-authored-by: Orca <help@stably.ai>
* fix(tests): guard lastVisitedAtByWorktreeId and mock markWorktreeVisited
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Deleting an unselected, scrolled-past worktree snapped the sidebar back
toward the selected item. The virtualizer's viewport key includes row
count so it can remount cleanly when rows change (avoiding stale height
measurements that caused card overlap) — but the fresh mount started at
scrollTop 0.
Persist the scroll offset in a parent-owned ref, seed the new
virtualizer via initialOffset, and re-apply scrollTop in a
useLayoutEffect with a retry loop that tolerates the transient window
where the browser clamps scrollTop while the virtualizer is still
measuring rows.
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): kill all PTYs for a worktree on removal (design §4.3)
Worktree deletion only shut down renderer-tracked terminals, so PTYs owned
by background tabs, split panes, or pre-reload sessions survived the
removal and kept leaking memory. Introduce killAllProcessesForWorktree
with three sweeps (runtime leaves, provider-prefix scan of daemon session
ids, pty-registry by worktreeId) and wire it into both teardown paths:
the CLI-initiated removeManagedWorktree and the renderer-initiated
worktrees:remove IPC handler. OrcaRuntimeService gets a lazy
getLocalProvider thunk so construction order stays robust.
Co-authored-by: Orca <help@stably.ai>
* fix(renderer): purge worktree-scoped state on removal + hydration (design §4.4)
When a worktree is deleted, ~25 worktree-scoped maps (tabsByWorktree,
git caches, browser state, split-tab models, per-file editor drafts,
etc.) kept references to the gone worktree, so SessionsStatusSegment
kept mis-classifying orphaned PTYs as bound and dropdowns rendered stale
ids. Add purgeWorktreeTerminalState as a single atomic action that
wipes every scoped map plus cascades top-level actives. Fire it from
the worktrees:changed listener on the set-diff of removed ids, and once
more at hydration via fetchAllWorktrees to clean up persisted entries
from pre-fix sessions. The hydration-time purge is gated behind a
per-repo success check: a single transient IPC error or an all-empty
fetch defers the purge so a degraded launch cannot wipe legitimate
persisted state.
Co-authored-by: Orca <help@stably.ai>
* test(zombie-worktree): regression coverage for design §4.5
Adds tests for every layer of the zombie-worktree fix:
- worktree-teardown: unit coverage of the three-sweep helper including
best-effort error swallowing across provider/registry.
- orca-runtime: RPC-initiated removeManagedWorktree kills PTYs before
any git mutation + verifies the lazy getLocalProvider thunk resolves
on each call.
- worktrees IPC: renderer-initiated remove kills PTYs before git and
skips the kill helper for SSH-backed repos.
- renderer slice: fetchAllWorktrees defers the purge when any sibling
repo fetch fails or every repo returns empty (F1 regression);
happy-path fires the purge once and does not re-run on subsequent
calls. Direct purgeWorktreeTerminalState coverage pins the cascade
across worktree-keyed, tab-id-keyed, and file-id-keyed maps.
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): log worktree-teardown kill counts (design §4.4 observability)
Breadcrumb lets ops distinguish a renderer-state-induced leak (diff-path
purge non-empty) from a backend-induced one (nothing to kill but memory
still pinned). Emit only when the sweep actually shut anything down so
steady-state logs stay quiet. Added at both call sites —
removeManagedWorktree (CLI path) and the worktrees:remove IPC handler.
Co-authored-by: Orca <help@stably.ai>
* test(zombie-worktree): fix ptyIdsByTabId seed shape to match production type
The purge unit test seeded ptyIdsByTabId as Record<string, string> when
the runtime type is Record<string, string[]>. The unit tests passed
because they never hit the UI renderer, but live e2e surfaced a
TypeError: (ptyIdsByTabId[tabId] ?? []).some is not a function.
Corrected to arrays; 21/21 tests still pass.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Partially reverts #1359: drop the hideIdentityIcon flag on the inline
agent list so the Claude/Gemini/… glyph renders again. The expand
chevron stays hidden — clicking the row still jumps straight to the
agent, and the chevron was the redundant part. Identity is useful when
a worktree has more than one agent of different types.
Co-authored-by: Orca <help@stably.ai>
* fix(pty): release ptmx fd on natural exit + defuse SIGHUP-to-recycled-pid
Daemons accumulated ptmx fds over time because node-pty's UnixTerminal
only releases the master fd when destroy() runs. On the natural-exit
path (the common case — user closes a tab, shell runs `exit`) nothing
ever calls destroy(), so the fd leaks until GC. On macOS this
eventually hits kern.tty.ptmx_max=511 and all new terminals fail to
spawn.
Fix: release the fd synchronously on every teardown path (natural
exit, explicit kill, stale SSH spawn, daemon shutdown) and close the
concurrent SIGHUP-to-recycled-pid hazard inside node-pty's
UnixTerminal.destroy().
- src/main/daemon/pty-subprocess.ts: synchronous POSIX proc.kill
neutralization inside proc.onExit; dead guards on forceKill/signal
so they never target a reaped-and-possibly-recycled pid
- src/main/daemon/session.ts: new disposeSubprocess() for already-
exited sessions (fd release only, no SIGKILL) — avoids sending
SIGKILL to a recycled pid during daemon shutdown
- src/main/daemon/terminal-host.ts: dispose loop routes on isAlive —
live sessions get forceKillAndDisposeSubprocess (SIGKILL + fd
release), exited sessions get disposeSubprocess (fd release only)
- src/main/providers/local-pty-provider.ts: same POSIX kill
neutralization at top of onExit for the legacy local path
- src/relay/pty-handler.ts: same neutralization in wireAndStore;
disposed flag guards all public entry points; dispose() uses
SIGKILL (not SIGTERM) before destroy since the relay is exiting;
killTimer fallback + immediate-shutdown + stale-spawn cleanup all
call disposeManagedPty + ptys.delete so wedged children (D-state,
bad NFS) can't leak map entries against the 50-PTY cap
Windows is exempt everywhere — WindowsTerminal.destroy IS a kill()
call internally (closes the ConPTY agent), so neutralizing would
turn destroy into a no-op and leak the agent.
See docs/fix-pty-fd-leak.md for the full design.
Co-authored-by: Orca <help@stably.ai>
* fix(pty): patch node-pty native off-by-one leaking /dev/ptmx per spawn
node-pty 1.1.0's pty_posix_spawn on macOS walks low_fds[0..2] in an
allocation loop that breaks at the first fd >= STDERR_FILENO, then
cleans up via `for (; count > 0; count--) close(low_fds[count])`. In
the typical case (break at count=0) the cleanup body never runs and
low_fds[0] — a /dev/ptmx handle — leaks per spawn. Fixed upstream in
microsoft/node-pty af053f2 (PR #882), not in any 1.1.0 release.
Backport the 3-line cleanup-loop fix as a pnpm patch. E2E validated
against a dev daemon: 200 spawn/kill cycles kept the daemon's ptmx
fd count flat at baseline; prior runs reproduced linear 1-per-spawn
growth. Also documents the native root cause as a status addendum in
docs/fix-pty-fd-leak.md — the JS-side destroy() discipline previously
landed is still load-bearing for the SIGHUP-to-recycled-pid hazard and
for synchronous fd release on daemon shutdown.
Co-authored-by: Orca <help@stably.ai>
* fix(pty): capture stable kill spy ref in pty.test.ts
destroyPtyProcess reassigns proc.kill = () => {} on POSIX to defuse
the SIGHUP-to-recycled-pid hazard (see docs/fix-pty-fd-leak.md). After
that reassignment, proc.kill.mock is undefined and the assertions
crashed in CI. Capture a stable reference to the vi.fn() before it
gets reassigned.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(daemon): guard killStaleDaemon against pid recycling
The daemon's pid file carried only a bare integer, so killStaleDaemon had
no way to verify the current owner of that pid was still the process it
forked. Unix pids recycle — on macOS the default ceiling is ~2048 and
wraps in minutes under load — so blind SIGTERM/SIGKILL on a recycled pid
could hit an unrelated user process (editor, language server, background
task) silently.
Three moving pieces:
1. Pid file now carries startedAtMs. daemon-spawner exports DaemonPidFile
and serializeDaemonPidFile; daemon-init fills startedAtMs from
getProcessStartedAtMs(child.pid) right after the daemon signals ready.
parseDaemonPidFile tries JSON first and falls back to bare-integer for
backward compatibility (startedAtMs: null on legacy files).
2. isDaemonProcess takes expected startedAtMs. In addition to the cmdline
match, it consults getProcessStartedAtMs(pid) and compares with a
±1.5s tolerance. Null on either side is fail-open so the guard never
strengthens existing behavior negatively (Windows, legacy pid files,
kernel-thread /proc failures).
3. SIGKILL re-check. The SIGTERM-then-wait window is up to 3s — long
enough for the pid to be recycled if the original daemon dies during
the wait. Before escalating to SIGKILL, isDaemonProcess runs again;
on mismatch we log reason=pid_recycled and skip SIGKILL.
Carried forward from #1323 as a standalone safety fix — independent of
that PR's router/drain machinery, which is not shipping.
Co-authored-by: Orca <help@stably.ai>
* test(daemon): cover pid-recycling guard surface in daemon-health
Adds unit coverage for the new Phase 0 surface:
- parseDaemonPidFile: JSON round-trip, JSON without startedAtMs,
bare-integer fallback for legacy pid files, malformed-input rejection.
- startTimeMatches: null-expected fail-open, null-actual fail-open,
within-tolerance match, outside-tolerance rejection.
- killStaleDaemon: with a mismatched startedAtMs in the pid file,
assert that no SIGTERM/SIGKILL is sent even though the liveness probe
(process.kill(pid, 0)) runs.
startTimeMatches was promoted from module-private to exported so it can
be exercised directly — it's a pure function with no internal state.
Co-authored-by: Orca <help@stably.ai>
* feat(settings): manage sessions panel for daemon staleness UX
Add a Manage Sessions settings panel with list/kill-all/kill-one/restart
backed by a new pty:management IPC surface. Rows are hover-highlighted and
click to reveal the corresponding terminal pane, mirroring the bottom
status-bar sessions popover. Kill-all and restart-daemon are icon buttons
(Trash2, RotateCw) with tooltips so the restart action doesn't collide with
the row-refresh RefreshCw icon.
Co-authored-by: Orca <help@stably.ai>
* fix(settings): honest killAll counts + suppress post-kill spawn toast
killAll now snapshots the initial session IDs and polls listSessions every
100ms for up to 6.5s — past the daemon's 5s SIGTERM→SIGKILL ladder — so
well-behaved shells hosting long-running agents finish their SIGTERM
handlers before we classify them as "refused to exit." Shutdowns fire once
per initial session (no retry spam), and fresh session IDs that appear
mid-poll (renderer remounts) don't inflate remainingCount.
pty-transport's connect() catch now detects the adapter's
TerminalKilledError tombstone rejection ("...was explicitly killed") and
suppresses the red "file an issue" toast. After Kill All, a pane remount
would call pty:spawn on the dead session ID; surfacing the tombstone as
a scary error misrepresented an intentional user action. The pane still
renders "Process exited" via the normal lifecycle.
Co-authored-by: Orca <help@stably.ai>
* feat(daemon): foundation for pty:management IPC surface
Adds the plumbing the Manage Sessions settings panel depends on:
- daemon-init exports getDaemonProvider / replaceDaemonProvider /
restartDaemon / cleanupDaemonForProtocol so the pty:management
handlers can access the current provider and coordinate a clean
restart without importing window-services internals.
- daemon-pty-router exports getAllAdapters so the killAll / listSessions
handlers can fan across the current adapter plus any legacy-protocol
adapters still attached for in-flight sessions.
- daemon-pty-adapter gains a listSessions RPC and readonly
protocolVersion so the handlers can annotate each session with the
adapter it belongs to and route killOne back to the right adapter.
- types.ts exports DaemonSessionInfo (SessionInfo + protocolVersion)
as the shared shape the preload API surface mirrors.
- ipc/pty.ts, attach-main-window-services, TerminalPane and
terminal-search pick up the small bindings required to wire the
router through existing code paths without regressions.
Co-authored-by: Orca <help@stably.ai>
* fix(settings): remove high-session-count warning banner
The banner nagged at 20 sessions, which is well within normal use for
users with many open worktrees. Count is already visible in the header
bar, and the table supports per-row and bulk kills, so the banner added
noise without actionable value.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): add single-instance lock + owned-metadata clear to prevent orca-runtime.json corruption
Closes#1312.
Every AppImage/.app relaunch was booting a fresh Electron main that clobbered
`<userData>/orca-runtime.json` and `agent-hooks/endpoint.env`. When the newest
instance quit, metadata pointed at a dead pid and `orca status` reported
`stale_bootstrap` even though the original Orca was still running. SIGKILL'd
predecessors also left orphaned `o-<pid>-*.sock` files in userData.
Three surgical changes:
1. `app.requestSingleInstanceLock()` in a new
`src/main/startup/single-instance-lock.ts` helper, wired into
`src/main/index.ts` after `configureDevUserDataPath(is.dev)` so dev and
packaged runs lock in separate namespaces. Losing instances focus the
primary's window via `second-instance` and quit without touching userData.
2. `clearRuntimeMetadataIfOwned(userData, pid, runtimeId)` in
`runtime-metadata.ts` — compares both pid AND runtimeId against the
current file before clearing, so the auto-updater handoff window never
erases the replacement process's fresh bootstrap. Called from a rewritten
`will-quit` handler that folds `runtimeRpc.stop()` + owned-clear into the
same `Promise.allSettled([disconnectDaemon, …]).then(app.quit)` chain
(inside the `!daemonDisconnectDone` guard so the second-pass re-entry
can't re-invoke stop+clear).
3. `sweepOrphanedRuntimeSockets()` in `runtime-rpc.ts` runs at the top of
`start()` on POSIX, using `process.kill(pid, 0)` to probe liveness and
remove `o-<dead-pid>-*.sock` orphans left by SIGKILL/OOM-kill.
Tests (37 new/updated):
- `single-instance-lock.test.ts` (3): lock-failed does not register listener;
lock-acquired registers exactly one; callback dispatches correctly.
- `runtime-metadata.test.ts` (+4): clearRuntimeMetadataIfOwned matched /
pid-mismatch / runtimeId-mismatch / no-file branches.
- `runtime-socket-sweep.test.ts` (4): own-pid-skip / alive-retain /
dead-sweep / regex-miss separated via synthetic ownPid=1; two
regex-invariant tests assert the sweep regex matches the real
`createRuntimeTransportMetadata` output (including the 'rt' fallback).
Design doc: `docs/fix-missing-single-instance-lock.md`.
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): focus hidden windows on second-instance event
focus() alone is a silent no-op when the primary window is hidden
(close-to-tray on macOS via Cmd+W, or on a different macOS Space) or
behind other apps on Windows. Call show() before focus() so a second
launch attempt reliably surfaces the existing window regardless of
state.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* feat(browser): viewport-size emulation via CDP
Adds a Viewport Size submenu in the browser toolbar "…" menu.
Presets apply width/height/deviceScaleFactor/mobile/touch via
Emulation.setDeviceMetricsOverride + setTouchEmulationEnabled, and
swap the UA to a mobile iPhone CriOS string on mobile presets.
Responsive clears the override. Selection persists per-tab and
re-applies on dom-ready so it survives navigations.
Co-authored-by: Orca <help@stably.ai>
* chore(browser): rename 'Responsive' viewport option to 'Default'
Co-authored-by: Orca <help@stably.ai>
* fix(browser): harden viewport emulation — serialize, validate, client-hints
- Chain per-tab setViewportOverride calls to prevent rapid-toggle races
- Validate viewport metrics at IPC trust boundary (reject non-finite/out-of-range)
- Emit userAgentMetadata alongside mobile UA to avoid UA/CH mismatch
- Always reapply on dom-ready (incl. null) to clear stale emulation
- Persist viewportPresetId in session schema (optional+nullable for back-compat)
- Convert preset submenu to DropdownMenuRadioGroup for a11y
- Log debugger.attach failures and cover with a unit test
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(hooks): generate worktree setup-runner from target worktree's orca.yaml
Extend getEffectiveHooks, getSetupCommandSource, and runHook with an
optional worktreePath parameter. When provided, loadHooks reads the
yaml from that path; otherwise it falls back to repo.path. Update
the worktree-creation paths in worktree-remote.ts and orca-runtime.ts
to thread the new worktreePath through after the worktree exists, so
the generated setup-runner reflects the yaml at the tip of the target
worktree's branch instead of the primary checkout's stale yaml.
Add a regression test in hooks.test.ts that mocks two distinct
orca.yaml files (primary and worktree) and asserts the worktree's
content wins when worktreePath is passed.
The legacy hooks:check IPC handler keeps reading from repo.path
unchanged.
Closes#1256
* fix(hooks): skip auto-setup when worktree script differs from preview
Add setupScriptsMatch helper that compares the primary checkout's
setup script (what the renderer shows the user before worktree
creation) to the target worktree's script (what would actually run
after creation). When they differ, createLocalWorktree skips the
auto-launch and logs a warning, so a base-branch yaml that introduces
or modifies setup commands cannot execute under the trust granted to
the primary's preview. CLI-created worktrees use the worktree-bound
load directly because trust is granted by the CLI invocation context,
which is annotated in orca-runtime.ts.
Adds regression coverage for both matching and differing script cases.
* test(hooks): add setupScriptsMatch to worktree IPC test mocks
The new setupScriptsMatch import in worktree-remote.ts means the
existing vi.mock('../hooks') blocks in worktrees.test.ts and
worktrees-windows.test.ts now need to expose it. Default the mock to
returning true so existing tests continue to exercise the run-setup
path; the new behavior gating is covered by the dedicated
setupScriptsMatch unit tests.