* fix(renderer): raise renderer V8 heap toward the 4GB pointer-compression cage
Renderer OOM ('renderer crashed'/'oom', exit 5 / 0xE0000008 / SIGTRAP) is the
dominant crash in the crash channel: the renderer JS heap reaches Chromium's
default V8 old-space ceiling (~RAM/4) and V8 aborts. Two adversarial leak hunts
(13 agents across every renderer subsystem) found no unbounded GB-scale leak, so
this is a capacity ceiling, not a leak.
Chromium sizes the renderer heap at ~RAM/4, leaving 8-15GB machines well under
V8's ~4GB pointer-compression cage (an 8GB machine caps near 2.2GB). Reclaim that
unused headroom via --max-old-space-size in a focused startup module, gated on
physical RAM (>=8GB, ~40% of RAM, floor 3072MB, capped at the real 4096MB cage).
16GB+ machines are already at the cage so this is a no-op for them; low-RAM
machines keep the default to avoid trading a clean OOM for OS memory-pressure
kills.
Overridable with ORCA_RENDERER_HEAP_MB (number to force, default/off/0 to opt
out). Verified on Electron 42.3.3: the main-process js-flags switch propagates to
the renderer V8 and is honored up to the 4096MB cage (5000/12288 -> 4096).
Co-authored-by: Orca <help@stably.ai>
* fix(renderer): address CodeRabbit — floor-to-0 override + Linux 8GB gate
- parseRendererHeapOverrideMb: a fractional override in (0,1) floored to 0 and
emitted an invalid --max-old-space-size=0; treat floored-to-0 as an opt-out.
- Lower the RAM gate from 8 to 7.5 GiB: os.totalmem() on Linux reports MemTotal
(excludes kernel/firmware-reserved RAM), so a real 8 GB box reports ~7.7 GiB
and was wrongly excluded from the headroom — the exact crashing population.
7.5 still cleanly excludes 6 GB machines (report ~5.7 GiB).
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
110 files carried an eslint/oxlint-disable max-lines directive but are
already under the default max-lines budget (300 .ts / 400 .tsx / 600 .mjs
/ 800 test), so the suppression is dead. Removing it restores real
max-lines coverage on these files with zero behavior change.
Each removed directive had max-lines as its only rule; verified via a
full oxlint run (0 max-lines violations, 0 new errors). Diff is pure
deletions (200 lines, 0 additions) — no code touched.
Co-authored-by: Orca <help@stably.ai>
Bump marketing version 0.0.22 -> 0.0.24 and Android versionCode 4 -> 5.
The 0.0.22 base was never committed after prior releases, so the Jul 6
builds carrying the show-all-worktrees fix (#7500) regressed below the
0.0.23 already on TestFlight (iOS) and collided with the existing
0.0.22/versionCode 4 APK (Android, no upgrade signal). Committing the
bump makes app.json authoritative again so 0.0.24 supersedes both.
Co-authored-by: Orca <help@stably.ai>
Two independent pieces, no behavior change to terminal handling:
1. Daemon lifecycle file log. The detached daemon runs with stdio
ignored, so field failures have zero daemon-side evidence. The daemon
now writes rotated NDJSON lifecycle events (startup/ready/hello
accept+reject/session create/attach/exit/kill/shutdown/uncaught
exceptions) to logs/daemon.log via a new optional --log-file fork arg.
Fail-open (any fs error disables logging), adoption-neutral (old
daemons without the arg keep working, protocol untouched), and the
diagnostic bundle collector now includes the file, bounded by the same
lookback window as trace spans.
2. tools/win-update-e2e: a packaged NSIS update proof harness. Installs
version N, drives the installed app (isolated userData), plants a
canary marker session, silently updates to N+1, relaunches, and
asserts an explicit expectations profile: --expect cold-restore
(today's behavior) or --expect survival (the Phase 1 target). Window
flashes are detected by baseline-diffed window enumeration with
canary-title attribution; daemons are identified by command-line
marker, never exe name. Refuses to run when a pre-existing Orca app is
running or (without --allow-existing-install) installed, and only
uninstalls an install it fully owns.
Ensure agent CLI startup and draft launch commands use the correct quoting
format based on the user's configured local Windows shell (e.g., cmd.exe).
This avoids using host settings for remote/SSH targets where local shell
preferences do not apply.
Issue #7236 reported that any non-empty worktree Setup Script failed on
Windows PowerShell with a "missing terminator" parser error, regardless
of content. Root cause: in pre-encoded builds the setup-runner command
(`cmd.exe /c "<runner>"`) was typed into PowerShell as raw stdin, where a
dropped/unbalanced double quote got re-parsed as an open string.
Encoded-command delivery (base64 UTF-16, shipped in v1.4.81) already
fixes this by passing the command as a shell argument with quotes intact.
This adds a regression test tying resolveSetupRunnerCommand to
resolveWindowsShellLaunchArgs: the real setup-runner command must reach
PowerShell via -EncodedCommand (startupCommandDeliveredInShellArgs),
never raw stdin, with its quotes preserved verbatim.
Co-authored-by: Neil <neil@stably.ai>
* Clarify Orca orchestration tool boundary and sidebar lineage
Add a "Tool Boundary" section to the orchestration skill, requiring
explicit Orca runtime state instead of generic subagent tools or
chat-only parallel workers. Also add tests to verify the tool boundary
and clarify sidebar lineage for same-worktree workers.
* Clarify worktree lineage guidance and parent-child boundaries
Update orchestration guidance and tests to clarify when to use child versus
top-level worktree lineages, and when to prefer same-worktree workers.
* Require stating the desired Orca lineage before creating a worktree from
an active feature branch.
* Limit child worktrees to conceptually stacked or dependent tasks.
* Prefer same-worktree workers unless isolated checkouts are explicitly
needed and do not require uncommitted changes.
getProcessTableSnapshot deduped the ps fork (#6288/#6667) but cached only the
raw stdout string on POSIX, so every concurrent agent pane re-ran parsePsRows
over the identical output within each 500ms TTL window — O(M*P) redundant
tokenization + row allocation. The Windows reader already caches parsed rows;
this makes the POSIX default reader do the same by parsing inside the deduped
scan and returning ProcessTableRow[]. Collapses the duplicate parsePsRows in
the main and relay foreground resolvers into one shared parseProcessTableRows.
Co-authored-by: Orca <help@stably.ai>
* fix(emulator): remove destroyed listener on stream stop to stop webContents leak
Both emulator stream IPC handlers register owner.once('destroyed', ...) per
start but never remove it on stop. .once only self-removes when the event
fires (window close), so every emulator tab show/hide cycle leaked a closure
on the long-lived main-window webContents — ~11 cycles trips Node's
MaxListenersExceededWarning and the closures grow unbounded until the window
dies. Store the handler on the session/subscription and removeListener on stop.
Co-authored-by: Orca <help@stably.ai>
* chore(emulator): trim why-comment to 2 lines, drop no-op afterEach
Review polish: honor AGENTS.md 1-2 line comment guidance and remove a
vi.clearAllTimers() that is a no-op without fake timers.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
The empty-state copy chooser only handled idle PR-refresh statuses inside the
ambiguous-hosted-review guard. When a background PR refresh went active
(queued/in-flight) or errored, it fell through to the publish-branch branch and
rendered 'Branch not published' on a no-upstream branch. As the refresh cycled,
the panel flip-flopped between the two messages (most visible on Windows, where
local git latency widens the active window).
Resolve the whole empty state inside the ambiguous guard so the copy is stable
across the entire refresh lifecycle: 'error' -> 'Could not refresh pull request',
every other status -> 'Pull request status unavailable'. The ambiguous state can
no longer surface publish guidance.
Co-authored-by: Orca <help@stably.ai>
Enable three unicorn rules — one correctness, two performance — and fix every
existing violation repo-wide so the rules pass as errors.
prefer-number-properties (76 sites)
- parseInt/parseFloat/NaN -> Number.* : safe aliases (autofixed).
- isNaN -> Number.isNaN (12 sites, hand-converted): global isNaN coerces its
argument, Number.isNaN does not. Verified every call site already passes a
number (Number.parseInt results, number-typed fields, Date.getTime()), so the
conversion is behavior-preserving today and guards against a future non-numeric
argument silently coercing.
prefer-array-find (26 sites)
- .filter(pred)[0] -> .find(pred); .filter(pred).at(-1) / .pop() -> .findLast(pred).
Drops the intermediate array and short-circuits.
prefer-array-index-of (5 sites)
- .findIndex(x => x === v) -> .indexOf(v).
Verified: typecheck (node/cli/web) clean, 53 affected suites pass (1679 tests),
oxlint clean repo-wide. mobile/ uses findLast safely (already ships ES2023
.toReversed()); config scripts and e2e helpers run on Node 24.
* Validate ORCA_TERMINAL_HANDLE and fall back to active terminal if stale
Long-lived shells can retain a stale ORCA_TERMINAL_HANDLE environment
variable after the runtime remints a pane handle. This can cause commands
to bake obsolete terminal handles into coordinator preambles or tasks.
- Check if the environment-provided handle is live via terminal.show before
using it in dispatch, task-create, or run operations.
- Fall back to resolving the active terminal/implicit sender if the environment
handle is stale.
- Map raw "no_active_terminal" errors to a helpful user-facing error message
suggesting the use of the "--from" flag.
* Remint stale orchestration terminals via pane key instead of focus
Resolve stale environment-provided terminal handles using the caller's
pane key (ORCA_PANE_KEY) via terminal.resolvePane instead of falling
back to the active focused terminal. This prevents commands from being
dispatched from or credited to the wrong terminal pane if focus has
changed.
Additionally, handle graph or pane resolution failures gracefully during
task creation since creator handles are best-effort lineage metadata.
* refactor orchestration tests to use helper stubs for stale handles
Consolidate repetitive mocking boilerplate for stale terminal handle
reminting and failure flows using new helper functions.
* perf(runtime): memoize onPtyData tail wait scan to halve per-chunk work
onPtyData runs per raw PTY chunk (hundreds/sec during verbose builds and
agent token streaming). For any terminal past the 2000-line / 256KB tail
cap it built the full wait text (a map/trim/filter/join over the entire
retained tail) and lower-cased + scanned it twice per chunk — once for the
pre-append tail and once for the post-append tail — producing hundreds of
KB of transient string allocation per chunk and steady main-process GC/CPU
pressure under load.
Cache the post-append wait state (text + lower-cased blocked-signal scan)
on the pty/leaf record and reuse it as the next chunk's pre-append state.
The prior chunk's post-append tail *is* this chunk's pre-append tail, so
the cached scan is exact; reuse is gated on fromTail so the empty-tail
preview fallback (which depends on a value updated after append) is never
reused stale. This drops per-chunk full-tail scans from 2N to N+1.
Adds an equivalence test proving the memoized stamping is byte-for-byte
identical to the recompute-both-sides reference across split prompts,
partial lines, ready-after-blocked demotion, and tail eviction, plus a
count assertion (memoized N+1 vs reference 2N scans).
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): clear memoized wait cache when a disconnected transcript is pruned
pruneDisconnectedPtyTranscript empties a disconnected PTY record's retained
tail but left the new tailWaitState memo untouched. If such a record resumed
output (adoption/reattach while a leaf keeps it alive), onPtyData would reuse
the stale pre-prune wait state (fromTail=true) as the next chunk's previous
state and could miss or mis-time the waitBlockedAt stamp on that first chunk.
Clear tailWaitState in the prune reset so the resumed chunk recomputes from the
emptied tail.
Adds a runtime guard (prune clears the cache) and a sim equivalence test
covering prune-then-resume stamping.
Co-authored-by: Orca <help@stably.ai>
* docs(runtime): reword wait-scan comment that named a removed function
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix: prefer Claude-generated titles in AI Vault
Agent Session History labeled Claude Code sessions with a truncated
first prompt even when the session already had a Claude-generated name
(the ai-title shown in /status and the tab title). Reserve the top
title slot for a user-set custom-title and rank the generated ai-title
above the first prompt: custom-title > ai-title > first prompt > meta.
New sessions still fall back to the first prompt until the ai-title is
written.
Also prune <session>/subagents/ during discovery via an injected
directoryPredicate so Task subagent transcripts, which share the parent
sessionId and are not independently resumable, stop appearing as
separate untitled history rows. Pruning at the directory level avoids
readdir'ing the excluded subtree and is cross-platform safe.
* Use latest generated Claude title in session scanner
Ensure the scanner updates the generated session title when Claude
revises it, rather than only keeping the first parsed 'ai-title'
record.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* Show all worktrees across all hosts on mobile
Avoid honoring desktop's host-filtering settings since mobile lacks the
UI to manage or unhide them. This prevents worktrees from being silently
hidden under certain host scopes.
Additionally, this removes worktree filtering based on repo metadata, which
previously caused worktrees to vanish when same-named repos on different
hosts collapsed to a single ID.
* fix(daemon): preserve promisify.custom type through wrapChildProcessApi
The windows-hidden-console-children test (from #7499, admin-merged with a
failing verify) failed tsgo: promisify(wrapped) resolved to its zero-arg
overload because the wrapper erased its argument to a bare variadic function
and the fake never statically carried promisify.custom. Preserve the wrapped
type via a generic overload (accurate: the wrapper copies the call signature
and symbols verbatim) and build the fake as a real CustomPromisify, so
promisify routes through the custom overload as it does in production.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
- Instruct workers to stop and idle or exit immediately after sending
`worker_done`, rather than running a 10-minute polling loop.
- Distinguish instructions based on worker kind: prompt-returning
agents should remain idle for re-engagement, while bare-shell
workers should exit.
- Prevent infinite polling overhead since the coordinator re-engages
workers via fresh terminal input instead of inbox polling.
Replace the hand-rolled `AbortController` + `setTimeout(() => controller.abort())`
+ `clearTimeout` in `finally` pattern with `AbortSignal.timeout(ms)` across the
main-process fetchers, updaters, and hosted-provider clients. This removes a
timer-leak footgun (a thrown/early-returned path that skips the finally leaks the
timer) and ~3-4 lines of bookkeeping per site. `AbortSignal.timeout` is Node
17.3+ (Electron main is Node 22+).
Two sites compose a caller-cancel signal with the timeout via `AbortSignal.any`
(Node 20.3+) instead of a manual abort listener:
- git/fork-sync.ts: also fixes a latent bug — the caller's `options.signal` was
spread into the git options then immediately clobbered by `signal:
controller.signal`, so caller cancellation was silently dropped. `AbortSignal.any`
restores it.
- rate-limits/claude-fetcher.ts (fetchViaOAuth external signal).
hosted-review-api-request.ts: `AbortSignal.timeout()` rejects with a
`TimeoutError`, not an `AbortError`, so the timeout-detection branch is updated
(otherwise `timedOut` would never be set).
minimax-fetcher.test.ts: its timeout test drove the abort with fake timers, which
cannot advance `AbortSignal.timeout`'s internal timer. Rewritten to fire the
timeout with an already-aborted signal so it genuinely exercises the abort path.
Deliberately NOT migrated:
- src/relay/git-handler.ts: the relay targets Node 18 (`build-relay.mjs`,
MIN_NODE_MAJOR = 18); `AbortSignal.any` needs Node 20.3+, and timeout-only would
drop the request context signal.
- ipc/feedback.ts: its timeout-driven fallback is verified with fake timers, which
can't advance `AbortSignal.timeout`; kept on the manual pattern.
getIssueComments loaded the issue, then its comments, then awaited c.user
inside a for-loop. Accessing .user on the Linear SDK's Comment model lazily
issues a fresh user(id) GraphQL query, so a comment-heavy issue did issue +
comments + N sequential user round-trips — a visible multi-second stall on
open, burning the complexity-based rate limit and holding one of only 4 shared
Linear concurrency slots (acquire/release) for the whole N*latency window.
Replace with a single rawRequest that fetches each comment's author inline
(first: 50, matching the SDK default page the code already relied on), the same
pattern the rest of this file uses. createdAt is passed through as the ISO
string rawRequest already returns (no re-serialization), and null avatarUrl is
normalized to undefined — output shape is unchanged.
Test asserts one request regardless of comment count and correct author
mapping (present user, null avatar, absent user).
Co-authored-by: Orca <help@stably.ai>