After #7750 removed the containment guard, the residual #7239 failure mode
is a persisted/inherited startupCwd whose directory no longer exists: every
spawn dies with the provider's missing-directory error. Fresh local renderer
spawns now opt in (cwdFallback: 'worktree') to recover at the workspace root
with a generic in-terminal notice; reattach, SSH, remote-runtime, runtime/API
and mobile callers keep exact cwd semantics, and existing directories —
including outside the worktree (#7685) — spawn as requested.
Co-authored-by: Orca <help@stably.ai>
* Sweep all persisted carriers of a removed SSH target id on re-adoption
reassignSshTargetId re-pointed repos and worktree metas but left the old
target id embedded in persisted session pty ids (ssh:<id>@@pty-N in tabs,
layouts, remoteSessionIdsByTabId), the startup reconnect list
(activeConnectionIdsAtShutdown, replayed via ssh.connect at boot — the
exact 'SSH target not found' in STA-1468), sleeping-agent resume records,
provisioned project host setups, sidebar host-scope arrays, and relay pty
leases. Any survivor resurfaces later as a failing connect or reattach.
New ssh-target-id-migration module re-points every carrier in one pass,
wired into reassignSshTargetId with per-carrier unit and store-level
round-trip tests.
Co-authored-by: Orca <help@stably.ai>
* Bridge SSH connection state to paired remote clients
The SSH surface was desktop-only: ssh:state-changed went to the host's
own BrowserWindow and the web client's ssh API was a no-op stub, so a
paired client's reconnect overlay never learned the host connected and
its target labels stayed empty (STA-1468 — overlay stuck on 'please
connect' over a live terminal).
- New sshStateChanged runtime client event, emitted from broadcastSshState
through OrcaRuntimeService onto the existing clientEvents stream.
- New ssh.listTargets / ssh.listRemovedTargetLabels RPC methods next to
the previously unused ssh.getState / ssh.connect.
- Web preload now routes listTargets / listRemovedTargetLabels / getState
/ connect to the paired host's runtime RPC instead of stubbing them.
- useIpcEvents applies sshStateChanged on paired web clients through the
same guarded path as desktop ssh.onStateChanged; desktop clients ignore
the event since a foreign runtime's targets would pollute their local
SSH store.
Co-authored-by: Orca <help@stably.ai>
* Harden the SSH reconnect overlay against stale or unknown target state
- Only present the destructive 'SSH host removed' state on positive
evidence (a removal tombstone label, or a hydrated non-empty target
list lacking the id). A client whose SSH state never hydrated has an
empty labels map for every id and must not offer workspace removal.
- After a failed Connect, resync target metadata so a stale overlay
converges to the ghost/re-adopted state instead of offering the same
failing Connect forever (the repeated 'SSH target not found' toast
loop in STA-1468).
Co-authored-by: Orca <help@stably.ai>
* Address CodeRabbit review on #7767
- Re-key workspaceSessionsByHostId partitions stored under a removed SSH
host id during re-adoption (no writer keys partitions by ssh host today,
but the schema tolerates it — re-key instead of stranding; live partition
wins when both keys exist).
- Track SSH target-list hydration explicitly (sshTargetsHydrated) instead
of inferring it from a non-empty label map, so a legitimately empty
target list still counts as removal evidence and a never-hydrated client
still never offers destructive removal.
- Apply the refreshed target list before the best-effort removed-labels
fetch in the overlay resync, so a labels failure can't discard it.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix: address review findings
* Improve split divider contrast and refactor tab split context menus
- Increase contrast of `--tab-group-split-divider` colors in light and
dark modes to achieve at least 3:1 contrast against `--card`.
- Refactor `TerminalTabSplitMenuSection` to use the shared
`TabWorkspaceLayoutMenuSection` for moving tabs between splits.
- Clarify terminal-specific split action labels in the context menu.
- Remove redundant icon margins in `EditorFileTabContextMenu`.
* fix terminal IME candidate selection and text commit on Linux
Sogou Pinyin and fcitx on Linux failed in Orca's terminal because bare
229 keydowns were swallowed, and empty composition updates prematurely
deactivated tracking. This led to dropped Chinese text or leaked Space/digit
candidate-selection keys reaching the PTY.
- Allow bare 229 keydowns to bypass suppression on Linux so xterm can diff
and commit text.
- Prevent empty compositionupdate events from prematurely deactivating
the composition tracker.
- Suppress and preventDefault candidate-selection keys (Space and digits)
during active composition and a brief post-composition window.
- Add comprehensive unit tests and an Electron CDP-driven E2E repro.
* fix: register IME gate command as direct spec-file invocation
The reliability-gate checker rejects --grep title selectors and requires
every evidenceRun command to match a gate command. Drop the --grep from
the e2e gate command and its evidence run, and remove the stale 3-file
evidence run superseded by the full 7-file run.
Co-authored-by: Orca <help@stably.ai>
* Guard overlapping and post-composition Linux IME candidate keys
- Track pending candidate key releases in a Map instead of a single
slot to support overlapping selector key events without stranding.
- Apply the candidate selection guard to post-composition key releases
that arrive after compositionend, preventing digits/Space from
leaking into the PTY.
- Restrict the Linux/Sogou candidate selection guard to Linux to
prevent interference on macOS and Windows.
- Exclude Shift+Space from candidate selection key checks.
* Guard held-key IME candidate repeats and scope policy to desktop Linux
- Keep auto-repeat keydowns for a candidate key suppressed past the
250ms guard window until its corresponding keyup event is received.
- Clear stale pending releases on fresh non-repeat keydowns to avoid
guarding the wrong key events.
- Exclude Android and ChromeOS user agents from desktop Linux-specific
IME candidate key suppression behaviors.
- Ensure the composition tracker is activated unconditionally on
compositionupdate events.
* Clean up IME reference and extract shared test event fixture
- Remove the obsolete Linux Sogou Pinyin IME reference document.
- Extract the fully-defaulted XtermBypassEvent helper into a shared
fixture file to keep the policy test suites in sync.
- Add a test verifying that Shift+Space (fcitx full-/half-width toggle)
is not suppressed as an IME candidate key.
---------
Co-authored-by: Orca <help@stably.ai>
* Robustify SSH terminal reconnect and session restore recovery
- Release the replay guard after a fallback timeout to prevent permanent
keyboard input lockouts when an unmounted terminal never parses.
- Verify backing process liveness on PTY attach and reap stale entries
so dead shells cleanly trigger a fresh pane spawn.
- Normalize connection IDs during restore to prevent spurious mismatch
errors, and treat true mismatches as expired sessions instead of crashing.
* Route disconnected SSH terminal panes through deferred connection gate
Avoid spawning SSH terminal processes against disconnected targets,
which otherwise throws "No PTY provider" and leaves panes stranded.
- Intercept spawning via a new connect gate that triggers the deferred
connection flow when the SSH target is disconnected.
- Fall back to composite worktree IDs during cold-start hydration to
ensure deferred SSH session IDs are properly stashed.
- Retry spawning and remounting terminal panes upon SSH reconnect if
they are stranded or failed to spawn.
When returning to a worktree in the sidebar, the active tab reset to the
first tab instead of the one the user left on. Three fallback sites derived
the active tab from the first tab rather than the per-worktree remembered
selection (activeTabIdByWorktree):
- reconcileWorktreeTabModel: promoting legacy runtime terminals into a
freshly-ensured group seeded activeTabId from restoredLegacyTabs[0].
- Terminal.tsx active-terminal repair: reset to tabs[0]; a repair firing on
a transient worktree-switch render permanently clobbered the selection to
Terminal 1.
- hydrateLegacyFormat: used the global session.activeTabId, so every
worktree except the last-focused one lost its terminal on restart.
All three now honor activeTabIdByWorktree before falling back to the first
tab. Adds fails-old/passes-new regression tests.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
recentlyClosedAgentStatusTabIds (agent-status store) suppresses late
hook/status events for a just-closed terminal tab. It was only ever
added to — one `true` entry per agent-tab close, keyed by the ephemeral
tabId, never deleted or capped — so it grew for the renderer's whole
session. It's the renderer twin of the main-process
closedAgentStatusTabIds set that #7561 already FIFO-capped.
Fix: bound it to the 1024 most-recent closed tabs via delete-then-set
LRU with oldest-key eviction (Record key order is insertion order),
mirroring #7561. A status event for a tab closed >1024 tabs ago cannot
still arrive, so suppression behavior is unchanged.
Test (red->green): closing 1029 tabs leaves exactly 1024 markers with
the oldest evicted and the most-recent retained; without the cap all
1029 persist.
Two module-level caches in native-chat-pending.ts capped their per-key
arrays (8 entries) but never bounded the KEY count:
- commandMarkerCache — keyed by paneKey\0agent\0sessionId; sessionId
changes on every /clear and paneKey embeds an ephemeral leafId, so a
distinct key was stranded per (pane, session) for the renderer's whole
life. Only test-only clear() ever removed keys.
- pendingSendCache — self-cleans on the normal empty-drain path, but a
pane closed with an unconfirmed send (agent crash / early close) left
its non-empty entry keyed forever.
Route both writes through the existing setBoundedScopeCacheEntry LRU
helper (cap 128) that #7566 already applied to the draft/attachment
caches in the same folder — same pattern, same file family, was just
missed here. Values are tiny so this is a slow leak, but real and
unbounded.
Tests (red->green): appending 133 distinct scopes evicts the oldest and
keeps the most-recent 128; without the cap all 133 survive.
* perf(pty): cap the unsent main→renderer PTY backlog (Win/Linux GB leak)
The per-pty `pendingData` string in ipc/pty.ts (main's unsent
main→renderer output queue) had no size cap. The 512 KB/pty + 8 MB
in-flight caps bound only SENT-but-unacked bytes; `onData` appends to
`pendingData` unconditionally and `flushPendingData` merely `continue`s
(no drain) while ack-gated. On Windows/Linux the main renderer is
background-throttled — `setBackgroundThrottling(false)` is set only for
`process.platform === 'darwin'` — and Chromium freezes hidden pages
after ~5 min, so a backgrounded Orca with an active/verbose agent stops
ACKing: in-flight pins at 8 MB, the flush gates, and `pendingData` grows
at raw PTY throughput → MB→GB in the main process. macOS is unaffected.
Fix: cap the backlog to the most-recent PENDING_DATA_MAX_CHARS (2 MB,
matching the daemon pendingOutput and renderer scheduler caps),
advancing startSeq by the dropped-char count (the same arithmetic
flushPendingData uses when slicing), and set a `droppedBacklog` flag on
the next payload. The renderer's dataCallback sees the flag and calls
the existing markHiddenOutputRestoreNeeded(), so on hidden→visible it
rebuilds the dropped span from the main headless snapshot — no output is
lost within scrollback depth. Background agents keep writing full-speed
into the runtime buffer; only the redundant delivery copy is trimmed.
The flag threads through the same layers as the existing `background`
field: main payload → preload types → dispatcher PtyDataMeta → renderer.
Tests (red→green): a daemon pty emits 5 MB while the renderer never
ACKs. Without the cap the flag is never set (fail); with it the first
emitted chunk carries droppedBacklog exactly once and delivery stays
bounded. Ordinary small output never sets the flag.
Note: leak + fix are Win/Linux-only (macOS is throttling-exempt), so not
reproducible at runtime on macOS; evidence is the code trace + tests.
* test(pty): clarify that the sent-total assertion is a sanity bound, not the cap proof
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(settings): keep full installed font list
* fix(settings): bound font picker rendering
* Fix redundant font queries and handle partial state in worktree purge
- Latch the loaded state even when the returned font list is empty, preventing font-less systems from reissuing font listing calls on every picker interaction.
- Add null-tolerance for `ptyIdsByTabId` when building worktree purge state to support callers that pass partial AppState.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Deleting a config-sourced SSH target had no lasting effect: the Manage-SSH pane
re-imports ~/.ssh/config on open, and the import was a pure upsert with no record
of deletions, so the just-deleted host was re-inserted verbatim from the config
that still exists on disk.
Persist a `deletedSshConfigAliases` tombstone set:
- Deleting a config-managed target (source 'ssh-config', or an adopted legacy
import) records its alias; manual targets are never tombstoned.
- The passive on-open sync skips tombstoned aliases, so a deleted host stays
deleted.
- Re-adding or editing a target reclaims its alias, and the explicit Import
action (`reAdopt`) clears all tombstones to deliberately re-adopt config.
This also fixes the edit-then-reappear case: editing a config host to `manual`
already reserved its current alias, and reclaim covers alias changes.
* fix(github): route PR merges through repo owner
- Use repo-owner runtime settings before source-context overrides in PR actions
- Keep headless/runtime repos on runtime RPC instead of local gh:mergePR
- Add source-boundary coverage for merge routing and Windows-safe newline matching
* fix(github): preserve repo-owner merge routing
- Apply task-source runtime overrides only when they resolve to a runtime host
- Keep runtime-owned repos from being downgraded to local gh:mergePR
- Guard the routing contract with source-boundary coverage
- Important because non-runtime source contexts could recreate the issue #6957 access-denied path
* test(github): add behavioral regression test for repo-owner merge routing
Mirrors PRActionsPanel's merge-routing decision through the real
repo-runtime-owner/task-source helpers so a revert to source-only
routing (issue #6957) fails here. Covers the runtime-owned repo with a
local GitHub source view and the local-repo-while-runtime-focused case.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
Regression guard for the runtime-agent-list fix: asserts the composer's
detectedAgentList selector and detection effect resolve SSH -> runtime ->
local in that order, so a runtime-owned repo shows the paired runtime's
agents instead of the local machine's.
Co-authored-by: Orca <help@stably.ai>
* feat(ai-vault): add OMP sessions to the AI Vault session browser
Adds OMP ("Oh My Pi") to the AI Vault/Agents catalog so historical
.omp/agent/sessions/**/*.jsonl transcripts are discovered, parsed, and
resumable from the right-sidebar session browser — locally and over SSH.
- Discovery mirrors Pi (OMP_CODING_AGENT_DIR env, WSL home roots, per-agent
limit) in both the local scanner and the remote/SSH scanner.
- Parses OMP's message-graph JSONL via the shared graph parser (new
MessageGraphAgent type), capturing the model from model_change.model (OMP's
key, not Pi's modelId) or the assistant message, and tokens from usage.
- Routes OMP through the incremental parse cache so ~5s rescans resume from
the last byte instead of re-reading whole transcripts.
- Resumes by absolute transcript path (`omp --resume <path>`) so it resolves
regardless of which session-dir root (custom OMP_CODING_AGENT_DIR / WSL
store) the file was discovered under; threaded through both the scanner and
the renderer's local resume/copy rebuild.
- Renderer reuses the existing OmpIcon/catalog/grouping; adds overflow-x-hidden
so long worktree chips never widen the sidebar.
Generalizes normalizePiSessionsDir -> normalizeAgentSessionsDir. Verified
end-to-end against 10 real ~/.omp transcripts and rendered in the app.
Co-authored-by: gatsby74 <166927047+gatsby74@users.noreply.github.com>
* fix: make AI Vault parse-cache agent switch explicit
---------
Co-authored-by: gatsby74 <166927047+gatsby74@users.noreply.github.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
retainedAgentsByPaneKey snapshots a completed agent (a full AgentStatusEntry
— up to ~24KB of prompt/message text — plus a TerminalTab) per ephemeral
paneKey. paneKeys never recur, and the map is pruned only on worktree removal
or manual dismissal, so a long-lived worktree in a busy multi-agent /
orchestration session grows it without bound. This is the dominant
large-payload driver of the renderer JS-heap OOM seen in the Windows crash
bundles (heap climbing to the 3586 MB V8 old-space limit under continuous
multi-agent work).
Cap the map with insertion-order (== retention-order) FIFO eviction, mirroring
capRecordByInsertionOrder in github.ts: the newest completions survive; the
oldest simply stop showing in the recently-completed overlay. retainAgents is
the only path that grows the map, so capping there fully bounds it.
Adds a leak regression test that drives the production retainAgents path with
distinct ephemeral paneKeys and asserts the map stays bounded — fails before
this change (grows to N), passes after (capped at MAX_RETAINED_AGENTS).
Co-authored-by: Neil <neil@stably.ai>