Without this, agent rows the user already visited come back bold every relaunch now that the rows themselves survive restart (per docs/agent-dashboard-retention-restart.md). Hydrate sanitizes input field-by-field (rejects null/non-object/array, prototype-pollution keys, non-finite/non-positive values) and applies a 7-day TTL paralleling HYDRATE_MAX_AGE_MS in agent-hooks/server.ts so hard-quit/crash paths can't grow the persisted map forever.
Co-authored-by: Orca <help@stably.ai>
Apply review-fix corrections on the agent-dashboard restart-persistence
work:
- Split dropStatusEntry from clearPaneState so renderer-driven dismiss
IPC no longer wipes lastPromptByPaneKey/lastToolByPaneKey for a
still-alive pane.
- Validate paneKey shape at the IPC boundary (isValidPaneKey).
- Let getSnapshot errors propagate instead of silently returning [] —
matches the renderer's existing .catch and avoids masking a broken
persistence path.
- Trust main's authoritative timing.stateStartedAt unconditionally on
same-state pings; fall back to existing only when timing is absent.
- Use strict < on the snapshot/live updatedAt guard so two events in
the same millisecond don't drop the second one (a <= guard regressed
two existing slice tests).
- Don't reset snapshotRequestedForReadyWindow in the catch handler;
combined with the per-store-update subscriber it would retry-storm
on persistent IPC failure.
- scheduleStatusPersist now resets the timer on each call (true
trailing-edge debounce) instead of leading-edge throttle.
- Fix doc references that named clearPaneState in dismiss/IPC context
where the implementation uses dropStatusEntry; add type-level JSDoc
on AgentStatusIpcPayload.
109/109 in-scope tests pass.
Co-authored-by: Orca <help@stably.ai>
- Drop hydrate entries older than 7 days (HYDRATE_MAX_AGE_MS) so stale
rows from worktrees archived weeks ago do not pile up forever. PTY-
teardown eviction handles closed panes; the TTL covers daemon-restored
PTYs that never re-attach and crash-recovery paths.
- Reject hydrate entries whose `tabId` field diverges from the paneKey's
tab segment. Cheap defensive add against future renamer/shape drift.
Doc updated to move TTL out of the follow-ups list (now in scope).
Tests: new "drops hydrate entries older than the TTL cutoff" and "drops
a hydrate entry whose tabId disagrees with the paneKey prefix"; existing
hydrate fixtures now use a `recentTs()` helper instead of fixed 2023
timestamps.
Co-authored-by: Orca <help@stably.ai>
Reopening a GitHub issue/PR drawer paid full IPC + `gh` startup latency on
every open. Two changes here:
1. Module-level SWR cache in GitHubItemDialog.tsx keyed by
(repoPath, issueSourcePreference, type, number). Reopening within 30s
paints cached data instantly; older entries paint stale-then-refresh.
Concurrent opens dedupe on a shared in-flight promise. Mutation
handlers invalidate by (repo, type, number); a cache-generation
counter prevents in-flight refetches from resurrecting stale data
after a mid-flight invalidation.
2. Collapsed GraphQL query for issue details replaces 3 serial `gh`
subprocesses (REST issue + REST comments + GraphQL participants) with
one round-trip. Falls back to the legacy fan-out on any GraphQL error
so historical contract is preserved.
Cross-window invalidation rides a new `gh:workItemMutated` IPC broadcast
that skips the originating sender (the source already updated its cache
optimistically — re-broadcasting would race the optimistic write).
`addIssueComment` now takes a `type` so the broadcast scopes correctly
when a PR shares its number with an issue.
Co-authored-by: Orca <help@stably.ai>
* fix(skills): remove broken doc references in orca-cli SKILL.md
Three files listed under ## References do not exist in the repository:
docs/orca-cli-focused-v1-status.md, docs/orca-cli-v1-spec.md, and
docs/orca-runtime-layer-design.md. Agents following the skill would hit
dead links when trying to resolve ambiguous CLI behavior. Replace the
stale list with a general pointer to the docs/ directory.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* docs: remove stale Orca CLI doc index
---------
Co-authored-by: claude[bot] <claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* Support Codex pet sidekick animations
* Refine Codex pet defaults and drop unused failed animation state
Always default spritesheetPath to spritesheet.webp when a manifest
omits it, even when explicit frame/animation metadata is present, so
non-Codex bundles still benefit from the standard sheet name.
Drop the 'failed' animation entirely — Orca has no failure state, so
mapping interrupted completions to 'failed' would conflate user
cancellation with agent failure. Codex spritesheets can still expose
a 'failed' row as an asset contract, but it's never selected at runtime.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
Address review findings on the retention-restart branch:
- Wrap agentStatus:getSnapshot and agentStatus:drop IPC handlers in
try/catch so a throw cannot surface as an unhandled invoke rejection
(silent startup-hydration failure) or crash main from a fire-and-
forget listener.
- runStatusPersist no longer permanently suppresses gate-off deletion
retries on transient unlink errors (e.g. EPERM); deletedOnDisable
now flips only on success or ENOENT.
- Tighten tests: stale-version-hydrate now asserts the warn message
content; getSnapshot test uses toEqual; drop-handler test rejects
null/{}/[] in addition to the prior bad inputs.
Co-authored-by: Orca <help@stably.ai>
Hydrates the hook server's per-pane lastStatusByPaneKey from
userData/agent-hooks/last-status.json before binding the HTTP listener,
mirrors mutations to disk via a 250ms trailing debounce, and flushes
synchronously on stop(). Renderer dismissals fan out a new
agentStatus:drop IPC so the on-disk file evicts the entry and a
relaunch cannot resurrect it. Adds a bounded bootstrap queue in
useIpcEvents so events replayed by setListener() during window creation
are not dropped while App.tsx is still hydrating tabsByWorktree.
Gated on settings.experimentalAgentDashboard. Done, blocked, and quiet
working rows now all survive across restart.
Co-authored-by: Orca <help@stably.ai>
Persist transient Tasks page position (GitHub mode, active preset/query,
Linear preset/query) in PersistedUIState so reopening Tasks restores the
user's working context instead of falling back to defaults. Source, repo
selection, team selection, and active project keep using their existing
settings paths.
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>
* 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(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>
Renames the experimental pet overlay to "sidekick" with themed character
names (Claude the Mage, OpenCode the Rogue, Gremlin the Trickster).
Covers IPC channels, preload API, persisted UI state, settings flag,
on-disk userData path, components, and types.
Deletes the standalone pet-overlay design mock.
Co-authored-by: Orca <help@stably.ai>
* feat: add experimental pet overlay
Adds an opt-in 3D pet overlay pinned to the bottom-right. Gated behind
an experimental flag so three.js + GLB models stay out of the renderer
bundle for users who never enable the feature. Ships with four bundled
models plus user-uploaded custom GLBs via a pet:import IPC; a status-bar
segment provides model picker + hide toggle.
See docs/design/pet-overlay.md for the full design.
Co-authored-by: Orca <help@stably.ai>
* refactor(pet): replace 3D GLB models with 2D webp images
Co-authored-by: Orca <help@stably.ai>
* chore(pet): compress pet webp images and remove unused assets
Reduce resources/claude.webp, gremlin.webp, opencode.webp sizes; drop
the obsolete pet-overlay design doc and compress-pet-glb script now
that the GLB pipeline has been replaced by 2D webp images.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
getConnectionId returns undefined during store hydration, which was
misclassified as remote by `!== null` check. Changed to `typeof === 'string'`
so unhydrated state falls through to client-OS quoting, consistent with
terminal-drop-handler.ts behavior.
Co-authored-by: Orca <help@stably.ai>
* Wip
* fix(terminal): make split-pane cwd resolution reliable on macOS
Coalesce and cache per-pid lsof calls in the main process, and raise the
renderer's IPC timeout to 1s so cold lsof lookups (typically 100–500ms,
occasionally ~1s) don't fall back to the worktree root.
Co-authored-by: Orca <help@stably.ai>
* chore: remove stray screenshot and harden daemon getCwd test
- drop orca-before-split.png accidentally committed in the Wip commit
- accept null OR string from getCwd in the daemon test, since the new
terminal-host fallback calls resolveProcessCwd(pid) and the mock pid
55555 could resolve against a live process on CI
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Orca's terminal already encodes Shift+Enter as the kitty CSI-u sequence
`\x1b[13;2u`, but without `vtExtensions.kittyKeyboard` xterm.js never
answers the `CSI ? u` probe. CLIs that gate enhanced input on that
handshake (Claude Code, Codex, etc.) therefore drop the extended bytes
and treat Shift+Enter as a plain Enter — most visibly when running
inside tmux, which strips extended-key encodings by default.
- Enable `vtExtensions.kittyKeyboard` in the default terminal options
(matches VS Code's xtermTerminal).
- Lock in the flag with a regression test in pane-lifecycle.test.ts.
- Add docs/terminal-extended-keys.md explaining the Orca side and the
tmux-side `set -s extended-keys on` + `terminal-features xterm*:extkeys`
users need for nested Shift+Enter to reach a CLI.
Verified end-to-end in Electron: `cat -v` + Shift+Enter now prints
`^[[13;2u`, and `printf '\e[?u'` elicits the expected `CSI ? 0 u` reply
from xterm.js.
Co-authored-by: Orca <help@stably.ai>
Widens worktree nav history entries to string | 'tasks' so the titlebar
back/forward buttons and Cmd/Ctrl+Alt+Arrow shortcut work from the Tasks
page. openTaskPage records a 'tasks' entry; closeTaskPage rewinds the
index when parked on one; goBack/goForward dispatches Tasks entries via
a separate view activator to bypass openTaskPage side effects.
Co-authored-by: Orca <help@stably.ai>
Extracts rg and git-grep search logic into src/shared/text-search.ts so
the local main and SSH relay paths stop reinventing arg construction,
JSON parsing, submatch regex, and accumulator/truncation semantics.
Fixes silent truncation in the relay: searchWithRg used execFile with
a 50MB maxBuffer cap that rg --json easily exceeds on large repos,
dropping matches with no error surfaced to the user. The relay now
streams via spawn, matching the local path.
See docs/design/share-text-search.md for full rationale.
Co-authored-by: Orca <help@stably.ai>