Remove one-off incident docs and committed test-results noise, move
dev/repro/bench tools under tests/tools, and relocate i18next config
into config/ so the GitHub root scrolls to the description faster.
* fix(runtime): avoid immediate WebSocket heartbeat sweep
Defer the first heartbeat sweep until the interval tick.
The immediate sweep can close a newly accepted WebSocket before the E2EE handshake completes on Linux ARM64.
* test(runtime): update heartbeat expectations for deferred sweep
* docs(runtime): update heartbeat initialization comment
Clarified comment regarding socket pinging during heartbeat.
* fix(runtime): arm heartbeat after socket listeners
* test(runtime): pin shared heartbeat cadence
* chore(runtime): preserve reliability gate formatting
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* feat(plugins): Orca plugin system — kernel, content packs, panels, workers, marketplace v0 (experimental)
Adds Orca's experimental plugin system behind a settings flag: a
supervised kernel, declarative content packs (VM recipes, commands and
keybindings, language packs), sandboxed iframe panels, forked worker
hosts, and a Git-backed marketplace v0 with consent, provenance and
kill-list enforcement.
Theme, icon-theme and terminal-theme contributions are deferred to a
follow-up pass.
* fix(plugins): make unsupported marketplace listings unreachable by key
findPlugin() backs preview/install/previewInstalledUpdate via
requireListing(), so filtering only listPlugins() hid the catalog card
while leaving the dead install path reachable one click later.
* fix(plugins): fan Pi session-only status out to plugin subscribers
The providerSessionOnly early-return in applyNormalizedStatus emitted to
onAgentStatus (main-window fanout) but skipped enrichedStatusListeners, so
plugins subscribed to agent.status.changed silently missed every Pi
session_start event. Route both emit sites through one helper so a future
early return cannot drop the plugin tap again.
Co-authored-by: Orca <help@stably.ai>
* plugins: drop dead code and hoist duplicated trust-boundary patterns
Cleanup pass over the P1 diff, no behavior change:
- Delete `readPluginTreeSnapshot`/`readSnapshotFile` and their types, plus
the now-vestigial `directories`/`signal` plumbing in `collectFiles`.
- Delete `resolveContainedPluginDirectory` (no callers).
- Delete `plugin-content-load-pool.ts`; it reimplemented the existing
`mapWithConcurrency`, whose index arg also removes the pairing wrapper
in `buildPluginList`.
- Hoist `PLUGIN_CONTENT_HASH_PATTERN` and `PLUGIN_COMMIT_PATTERN` into
the install-lockfile module; 11 sites hand-rolled these identically.
- Point the new reliability gate at the PR instead of gitignored docs
paths, matching every other gate's link form.
* fix(plugins): retry plugin state renames on Windows AV/EPERM locks
Six plugin write paths (lockfile, provenance, current pointer, kill
list, marketplace cache, staged install dir) did a plain rename, so an
antivirus or indexer holding the target open surfaced as a failed
install. The repo already retries this hazard for issue #1507, but only
through a sync helper; these paths are all async.
Adds one bounded async retry + atomic write used by all six, and trims a
consent-provenance header that restated its own JSX.
* test(plugins): cover the Windows rename retry path
The retry loop shipped untested: both existing cases hit the non-retry path,
and the temp-cleanup test passed identically with the `finally` removed.
Mock `rename` to queue errno codes so CI can exercise locks it cannot provoke.
Co-authored-by: Orca <help@stably.ai>
* fix(plugins): pin bundled plugin resources to LF
Windows CI checks out with autocrlf, so the byte-hashed launch tree arrived
as CRLF and verify-packaged-plugin-resources rejected it — the packaged build
could never pass on Windows. Reproduced locally: CRLF yields the exact CI
error, LF verifies clean. Files are already LF, so nothing renormalizes.
Co-authored-by: Orca <help@stably.ai>
* test: guard the bundled-plugin LF pin against a CRLF checkout
The byte-hash mismatch only surfaced in Windows packaging CI. Assert the
.gitattributes pin and that a CRLF tree is rejected, so a regression fails
on any platform instead of waiting for a packaged Windows build.
Co-authored-by: Orca <help@stably.ai>
* ci: trigger packaged-build check on bundled plugin resource changes
The launch tree is byte-hashed during packaging, but no trigger path covered
it — so the CRLF fix for that check would not have re-run the check. Add the
resources, verifier and .gitattributes paths that can break packaging.
Co-authored-by: Orca <help@stably.ai>
* perf(plugins): rebuild the panel frame only when its baked theme values change
The revision keys the panel iframe, so every bump destroys the sandboxed
frame and its in-panel state. It counted root attribute mutations, but
--workspace-sidebar-live-width is written every rAF of a sidebar drag, so
dragging with a panel open blanked it ~60x/sec. Compare the two values the
shell actually bakes in instead.
Co-authored-by: Orca <help@stably.ai>
* test: stop pinning a plugin name in the CRLF guard
The CRLF case rewrites every launch file, so the reported mismatch is
whichever plugin sorts first. P2 adds theme plugins that sort ahead of
orca-navigation-shortcuts, which broke the assertion there.
Co-authored-by: Orca <help@stably.ai>
* style: drop stray blank lines left by the rebase resolutions
Both sides of the agent-hooks and orca-runtime conflicts contributed a
trailing blank, which oxfmt rejects. Whitespace only.
Co-authored-by: Orca <help@stably.ai>
* test(plugins): stop the startup budget failing on machine load
P95 runs 16-34ms idle but exceeds the 50ms bound under full-suite
parallelism, so the gate flaked. Widen it to catch an order-of-magnitude
regression instead; the no-worker/no-plugin-code assertions are the real
guarantee. Verified a 400ms regression still fails.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(status-bar): seed Resource Manager closed badge from daemon inventory
The closed chip counted tab/layout PTY wake hints (inflating terminal
count) and never fetched memory until the popover opened (showing "—").
Cache listSessions for the badge, seed memory on session ready, and drop
the wake-hint closed selector.
* fix(status-bar): update session inventory ref in an effect
CodeRabbit/React Doctor flagged mutating sessionInventoryRef during
render; keep the write in useEffect after commit.
* fix(status-bar): follow daemon session lifecycle events
* perf(status-bar): skip inventory scans for known PTYs
* perf(status-bar): bound resource inventory refreshes
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Credits remote terminal output only after parse or intentional discard, with bounded adaptive windows, fair draining, recovery cleanup, and RTT/parser benchmarks.
* test(e2e): prove the terminal daemon survives a main-process crash on Windows (#7742)
Add a win-crash-survival e2e harness (sibling to win-update-e2e) that
force-kills ONLY the packaged app's real Electron main (resolved via
app.evaluate -> process.pid, /F no /T) and asserts the detached
orca-terminal-daemon.exe plus its ConPTY shell survive with no pwsh
0xE9 FailFast, then that a relaunch re-adopts the SAME daemon and the
reattached UI binds to the SAME survivor shell (proved via a per-shell
env sentinel read back through the restored terminal).
This guards the #7742 fix (standalone relocated daemon that outlives
main death) against regression. A directional `--expect orphaned`
profile fails on a fixed build, keeping the survival assertions honest.
Windows-only; reuses win-update-e2e app-driver/daemon-process modules.
* test(e2e): harden Windows crash-survival proof
* test(ci): keep crash survival gate durable
* test(e2e): tolerate restart hydration navigation
* test(e2e): prove exact shell input after crash
* perf(ci): avoid crash harness installer rebuilds
* test(ci): harden crash survival evidence and cost
* test(e2e): fail closed on authoritative crash target
* test(e2e): fail closed on crash liveness evidence
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(pty): stop daemon reattach/kill race from orphaning restored terminals
Two cold-start defects let a PTY kill silently orphan a live daemon session
after a renderer/app restart — the PTY-orphaning-on-restart half of #7742:
- DaemonPtyAdapter.shutdown() was the only session op that skipped
ensureConnected(). The daemon client connects lazily, so a kill arriving
before the first spawn (startup sleep/hibernation sweeps, closing a
restored tab) threw "DaemonProtocolError: Not connected" against a
HEALTHY daemon; the renderer swallows the rejection and the live session
survives detached. Fix: connect first, like every other session op.
- pty:kill had no startup barrier while pty:spawn did. A kill issued before
initDaemonPtyProvider swapped the provider resolved against the pre-daemon
LocalPtyProvider, whose shutdown() silently no-ops on daemon ids; the
handler then reported success and sent a synthetic pty:exit, permanently
clearing the persisted tab->ptyId binding so the session never reattaches.
Fix: pty:kill awaits the same getLocalPtyStartupPromise gate as spawn.
Both reproduced red-first (real DaemonServer socket rig for the adapter;
deferred-barrier IPC rig for the handler) and pass after the fix.
Scope: this restores the reattach/teardown invariant. It does NOT address
the renderer crash trigger or the daemon-process-death path that produces
the Windows PowerShell FailFast symptom (separate follow-ups).
* fix(pty): gate runtime shutdown on daemon startup
* fix(pty): keep shutdown independent of hook startup
* feat(terminal): add flag-gated render-desync sentinel for WebGL panes
Detects the buffer-clean/render-stale glyph garble class in the field: per
visible WebGL pane, compare the cells the xterm buffer says hold glyphs
against the ink actually present on the canvas, sampled in the same task as
a forced synchronous redraw so a divergence proves the render model/atlas is
wrong rather than a missed present. A trip requires the same screen cells to
stay divergent across three samples (real desync is pinned; scroll lag moves),
then records a webgl-render-desync breadcrumb, stashes evidence (canvas PNG +
buffer text) for bug reports, and runs the same shared-atlas recovery a tab
reveal performs, so a stuck-garbled pane self-heals within seconds.
Off by default; arm on any build via
localStorage.setItem('orca:render-desync-sentinel', '1') and reload.
* fix(terminal): invalidate glyph cache on atlas replacement
Reproduce the WebGL atlas identity mismatch with two live terminals and force cached geometry to rebuild whenever a different shared atlas is attached. Persist flag-gated render-desync evidence and retain the investigation tooling used to validate the field signature.
* fix(terminal): harden render desync diagnostics
* docs(reliability): clarify Linux WebGL evidence gap
* feat(native-chat): add native chat view across mobile
* fix(native-chat): address review findings and CodeRabbit threads
Correctness:
- Restore an independent initial readSession seed and surface initial-drain
errors as snapshot frames so the chat view can never strand on 'loading'
- Pair mobile tool results to calls by ordinal FIFO (parallel calls no longer
misgraft results); clear a pending ask only when its own call resolves
- Show a new streaming reply immediately (same-turn suppression, not length)
- Delegate mobile noise filtering to the shared harness-injected classifier
- Admit soft-leaving mobile clients in beginMobileInputFloor (parity with
mobileTookFloor) so grace-window writes aren't dropped
- Self-heal a stale 'working' status once this turn's reply lands
- Catch RPC rejections in mobile file-open helpers; guard sanitizeToolInput
key collisions; settle web/runtime transports on unrecognized first frames
and forward snapshot errors
Perf:
- Throttle the mobile streaming bubble (50ms) so per-part status frames stop
re-parsing the whole accumulated markdown
- Short-circuit markdown path detection on dot-less or oversized runs
(quadratic backtracking guard)
UX/minor:
- Wire hold-mode dictation through the native chat composer
- Allow scoped-package (@) paths in file-path detection
- Move caret after mid-text autocomplete insertion; index-prefixed ask option
keys; single scroll-to-end effect; bounded wait + toast when image attach
races a resubscribe; count-based pending reconciliation; cache-hit search
cancels stale debounce; chat-tab toggle wins over in-flight preference load
- Share shouldStepNativeChatAskAnswer between desktop and mobile; import
block guards/source priority from shared instead of local copies
- Defensive non-positive transcript limits; test strengthening (TTL expiry,
post-unsubscribe stale frame, lease readiness, filtered console.error)
* refactor(native-chat): share desktop/mobile chat logic in src/shared
Extract the parity-mirrored native-chat modules into shared implementations
both surfaces re-export: ask parsing (registry, parseAskFromStatus,
extractPendingAsk, formatAskAnswer), answer stepping offsets/scheduler, diff
detection/parsing, harness-noise filtering, tool fold/pair/split, and tool
summaries. Removes the hand-synced copies and their stale Metro comments.
Divergence reconciliations take the safer side of each: diffs truncate at
120 lines/32KB everywhere (desktop previously unbounded), tool-run summaries
cap at 3 parts with bounded-depth previews, nameless tool calls are skipped,
and basenames split on both separators.
Also: settle and kill every sibling quick-open pass when one reaches
maxResults (main rg/git and relay git; relay rg already did) so a capped
search cannot leave a scan walking a huge tree; fold window-bounding into
the shared merger's applyAppend; localize the web 'Pair a host' snapshot
error.
* fix(native-chat): address CodeRabbit follow-ups on shared modules
- Attachment lease gate re-checks connection/target/tab after the bounded
wait, so a tab/host switch or disconnect mid-wait can't send into a stale
terminal; a moved-away target drops silently like the pre-wait guard and
only an unrecovered lease surfaces the toast. Adds hook tests.
- extractPendingAsk parses transcript tool-calls through the same
registered-parser + canonical-shape fallback as live status, so a custom
question tool that rendered live survives reconnect/replay.
- Direct unit tests for the shared ask parser (FIFO ordering, fallback,
malformed payloads) and tool-summary bounded preview (depth/collection
caps, circular refs, basename/command branches).
* fix(native-chat): treat initialLimit 0 as a valid empty window
Both engine guards used truthiness, so an explicit zero limit skipped the
bounded tail reader and fell back to an unbounded incremental read. Latent
only (every caller clamps positive), hardened for consistency with the
tail reader's non-positive-limit handling.
* fix(mobile): native-chat composer lock UX + send-failure feedback
- Distinguish input-lock reasons: transport 'disconnected' shows Reconnecting…
instead of mislabeling a reconnect as locked-by-another-client
- Guard the composer lock behind a 600ms hold so connState blips / lease
hand-offs don't flicker the placeholder; unlock stays instant
- Surface a rejected send inline above the composer (a bottom toast hides
behind the keyboard); auto-dismisses after 4s
- waiting-session hint invites the first message instead of implying the
agent is still starting
* test(mobile): sync answer-send pacing test to the 500ms advance buffer
Missed in merge 8fe3c391c, which carried main's NATIVE_CHAT_ADVANCE_BUFFER_MS
300->500 (#8568) into the shared stepping module that mobile derives from.
* fix(mobile): restore terminal stream after chat cold start
* fix(native-chat): harden retries, optimistic sends, and file scans
* fix(mobile): deliver AskUserQuestion answers by option number (STA-1860)
Port #8840's fix to the mobile native chat: the Ask card now tracks
per-question option INDICES (+ free text) and the answer-send hook drives
Claude's arrow-navigate selector with buildAskAnswerKeys keystroke groups —
option numbers, next-tab arrows, Enter — paced one selector step apart, instead
of pasting label text that the selector ignores (which silently committed the
default option). Non-Claude agents keep the pasted-label path via the
selection-based formatAskAnswer.
Backcompat: keystrokes are built client-side and written through the EXISTING
terminal.send passthrough with enter:false — the same contract the permission
card already uses — so an older desktop runtime (SSH/relay included) replays
them verbatim; no RPC/contract change in either update order. Free text is
newline-sanitized because terminal.send has no paste framing.
Drops the now-unused formatCompleteAskAnswer from the shared module.
* fix native chat send and runtime races
* fix mobile native chat formatting
* fix(native-chat): mobile empty state matches desktop copy
Mobile showed a single generic line ('Send a message to get started') where
desktop shows a titled two-line empty state naming the agent ('Start a chat with
Claude' + 'Ask Claude to inspect code, explain output, or make a change.'). Align
them from one source of truth so they can't drift again:
- Extract the agent-type label map + formatAgentTypeLabel to
src/shared/agent-type-label.ts (desktop re-exports; mobile imports).
- Add src/shared/native-chat-empty-state.ts with the canonical English copy;
desktop uses it as its i18n fallbacks (localization unchanged — en/es/ja/ko/zh
keys still win), mobile substitutes the agent label and renders it directly
(mobile ships English only).
- Mobile: render title + subtitle for waiting-session AND ready-but-empty (both
are 'start a chat'), error copy for errors; keep the loading spinner.
Live-verified on the iOS sim against a pn-dev of this branch. typecheck node/web
+ mobile tsc clean; 30 mobile + 428 desktop/shared native-chat tests green.
* style: oxfmt the empty-state parity test (line wrap)
---------
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(runtime): retain watcher and PTY teardown ownership
* fix(runtime): restore watchers after interrupted cleanup
* fix(runtime): prevent stale watcher revival
* test(runtime): cover watcher shutdown ownership
* test(daemon): model physical PTY exit
* fix(daemon): keep shutdown terminating when disposal cannot prove exit
A rejecting host.dispose() (unreapable child past its exit deadline) left
the shutdown RPC without its process.nextTick(shutdown) and skipped socket
cleanup in shutdown(), stranding the daemon as an unreachable orphan after
the stale-daemon replacement flow unlinks its socket. Log and continue:
daemon exit reparents the child to init instead of blocking on it.
* fix(runtime): keep local watching alive after an idle-kill deadline miss
An idle child that outlived the exit deadline set shutdownRequested on the
shared desktop supervisor, which has no retire-and-replace path — every
later subscribe rejected supervisor_disposed and the roots were cached
unwatchable, silently ending local file watching for the session. The idle
path owns zero records, so there is no double-watch hazard; the zombie
keeps its capacity reservation until physical exit and the next subscribe
gets a fresh child.
* fix(renderer): resync replayed paired-web file watches
Transparent replay removed the implicit resync the old close-and-rebuild
path provided: a replayed files.watch only reports changes from its own
native setup, so changes during the reconnect gap were silently lost.
Deliver a conservative overflow to consumers once the replayed watch is
ready, matching the overflow-after-interruption contract everywhere else.
* fix(runtime): address teardown review findings
* fix(runtime): retry watches after teardown deadlines
* Fix PTY descendant leaks on forced teardown
* Fix jitter-sensitive terminal lifecycle test
* fix(terminal): kill agent descendant processes on session teardown (STA-1800)
Agent CLIs spawn tool children in detached process groups that PTY
SIGHUP can never reach. Killing an agent session (tab close, retire,
sleep) left those children running as orphans — eight orphaned git
processes burned ~8 cores for up to 11.5h under the agents-running
keep-awake and drained a battery to 8%.
New pty-descendant-termination module: snapshot the ppid tree BEFORE
signalling (a dead root's descendants reparent to pid 1 and become
unfindable), SIGTERM the root group and every descendant, then after a
2s grace SIGKILL survivors gated on a pid+start-time identity re-check
so a recycled pid is never signalled. Snapshot is bounded and never
rejects; failures degrade to today's shell-only kill.
Wired for agent sessions only (plain terminals keep nohup semantics) at
all three POSIX kill sites: local provider shutdown, daemon
TerminalHost immediate kill (the pty:kill path — force-kill bypassed
Session.kill entirely), and daemon Session graceful kill.
Verified live in the built app: an agent pane with a detached-pgid
child; the child survived on the unwired build (three control runs) and
dies within ~5s with the fix. Windows ConPTY and SSH-hosted PTYs keep
the previous foreground-tree contract (documented follow-ups).
* fix(terminal): harden descendant teardown
* fix(terminal): require fresh process snapshots
* fix(terminal): close descendant teardown races
* refactor(terminal): preserve teardown line budget
* fix(terminal): keep descendant teardown fresh and identity-safe
* docs(reliability): record integrated descendant E2E
* fix(terminal): bound descendant teardown work
* fix(terminal): share descendant snapshot indexes
* docs(reliability): record descendant review evidence
* revert: remove speculative descendant hardening
* perf(terminal): defer cold worktree activation tab mounts until first reveal
Activating a worktree mounted a TerminalPane for every saved terminal tab
in one render pass. Each mount replays scrollback through xterm, attaches
a WebGL renderer, and issues a sync-IPC snapshot read, so a worktree with
many agent-session tabs froze the renderer for tens of seconds (field
trace: 200+ replay-guard stall releases inside one activation window,
plus restore-marker feedback loops re-fetching snapshots at ~4/sec).
Cold activations now mount only the tabs the user can see (active tab,
each split group's active tab, activity-portal tabs, pending spawns) plus
tabs that are already live or that parked byte watchers cannot cover.
Every other tab defers like a cold-parked tab from birth: no view until
first reveal, with the parked byte watchers owning bell/title/completion
side effects meanwhile. The restriction reuses the targeted background
mount mechanism and lifts once every tab has been revealed.
Deferral only engages when more than four tabs would mount cold, and only
while hidden-view parking is enabled, so small worktrees and the parking
kill switch keep today's behavior.
* test(terminal): prove cold-activation deferral end-to-end; unblock it past hydration's blanket spawn flag
The new e2e (child worktree, 8 tabs, renderer reload against live daemon
sessions) caught the deferral never engaging: session hydration marks
every persisted tab pendingActivationSpawn, and treating that flag as
must-mount-now put all tabs in the immediate set. Only explicit queued
startups (pendingStartupByTabId) mount eagerly; a deferred tab's reveal
consumes the hydration flag exactly like an activation mount would.
Sleep/wake respawn flows go through targeted background mounts and are
unaffected (terminal-sleep-wake-restore passes).
The spec asserts the visible tab mounts, deferred tabs stay unmounted
with parked byte-watcher coverage, and a revealed tab mounts on demand.
* fix(terminal): harden deferred activation lifecycle
* docs(reliability): correct SSH startup assertion evidence
* fix(runtime): preserve terminals during headless desktop activation
* rm design doc
* Fix desktop activation launch ordering and blocked-window status resolut
- Check desktopWindowStatus before spawning the Orca app so a blocked
runtime no longer launches a doomed second instance.
- Reuse resolveDesktopWindowStatus for remote runtime status so it
honors the same authoritativeWindowId fallback as local status.
- Re-check the authoritative window at spawn time instead of trusting
a possibly-stale snapshot, since it can be destroyed mid-await.
- Harden the e2e activation spec against silent spawn failures.
---------
Co-authored-by: bbingz <zzb@gxsmjx.com>
* fix(editor): avoid undo history for read-only live tails
* Add reliability-gate evidence for read-only live-tail undo-history fix
- Records the passing vitest run that verifies live-tail appends leave
canUndo false while ordinary external updates stay undoable, backing
the recent editor undo-history fix.