mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
f10b6de2c799f156ecc77db5a1ed767aadbbc8ca
7440
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f10b6de2c7 |
fix(relay): tolerate wall-clock skew in host-proof validation (#10474)
* fix(relay): tolerate wall-clock skew in host-proof validation A few seconds of local clock lag made challenge issuedAt appear in the future, so host-proof rejected every handshake and Mobile Relay looped connecting forever. Allow ±30s skew while keeping the 10s challenge window (#10401). * fix(relay): preserve host-proof challenge bounds --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
ee7ec43149 |
fix(codex): keep a host account switch inside the host lane (#10992)
* fix(codex): keep a host account switch inside the host lane markLiveCodexSessionsForRestart walked every tab's PTYs and carded any pane whose foreground looked like Codex. There was no lane check anywhere in that path, so a host account switch raised a restart notice on live SSH/relay panes — and a notice mutes the pane, so the user's remote terminal went deaf. The notice was provably spurious: a remote spawn carries a connectionId, so isDaemonHostSpawn is false and no CODEX_HOME is ever injected. The remote Codex uses the remote machine's own credentials; a local selection cannot reach it. Scope marking by lane instead. A pane's lane is (machine, runtime): `host`, `wsl:<distro>`, `env:<id>` for a relay environment, or an SSH connection that no managed selection can name. A switch made while a runtime environment is active still cards that environment's panes, which is the case that made the old "mark everything" behaviour look right. WSL was the same defect, not a separate one. A Windows run saw a WSL pane correctly escape a host switch, but only because its foreground read `wsl.exe`, which fails the Codex-foreground test — the Win32 process table cannot see into a WSL2 VM. That is incidental: `codex`, `node` and `python3` foregrounds are all eligible today, so a WSL pane that surfaces one (WSL1 pico-processes are in Win32_Process) would be carded by a host switch. The lane is now what decides. Also stop queueing remote and SSH panes into the bind-driven stale sweep at all. recordCodexPaneAccountForSpawn bails on anything that is not a daemon host spawn, so listStalePanes can never report one stale, yet each pane still spent every rung on a 15s-timeout remote RPC — ~75s per pane since the ladder widened to five rungs. The lane vocabulary moves to shared/ so the renderer keys panes exactly as a launch does rather than growing a third copy of the rules. Refs #10757 * fix(codex): key a WSL pane by the distro its launch actually used The lane guard derived a pane's WSL distro from the workspace UNC path alone. A launch does not: pty.ts hands getCodexSelectionTargetForPty a third argument, the resolved runtime's distro, so a wsl.exe pane on an ordinary Windows-path worktree launches under `wsl:Ubuntu`. The renderer keyed that same pane `wsl:__default__`, so the Ubuntu switch never reached it — the pane kept the old account with no notice, which is #10757 returning by a new route on the exact platform the issue was reported from. Resolve the distro the way the spawn does: the project execution runtime first, then terminalWindowsWslDistro. Both are already in renderer state. Also match a distro-less WSL switch against the whole `wsl:` family. Two mutations reach the renderer as `{runtime:'wsl', wslDistro:null}` while writing concrete distro slots: selecting the system default clears EVERY wsl slot (setSelectedCodexAccountIdForTarget), and `add` stores the distro it discovered from the machine. Keying those to `__default__` missed the very panes they re-pointed. The residual cost is over-marking a sibling distro after an add, bounded to this machine's WSL panes and far cheaper than a stranded pane. An owner-less remote pane colliding with the host lane was untested — that collision is what would mute a working remote terminal, so pin the disjointness rather than the literal key. Refs #10757 * fix(codex): resolve a pane's lane the way its launch resolved it Three more places where the renderer's lane and the launch's lane disagreed. Each disagreement is silent: too narrow and a stranded pane never gets its notice (#10757 returns), too wide and a healthy pane is muted, because a notice makes onData drop every keystroke. Shell: main runs the request through resolveLocalWindowsTerminalRuntimeOptions, so an unset shellOverride still lands on WSL when that is the Windows default. Reading tab.shellOverride alone called such a pane `host` — a host switch would have muted a working WSL terminal. Gate on the renderer platform, as pty.ts gates on process.platform. Cwd: a terminal's startup cwd is deliberately not constrained to the worktree (resolveTerminalStartupCwd, #7685), and main keys the lane off that cwd. Follow it through the same shared call instead of reading the workspace root, so a pane split after `cd \\wsl.localhost\...` is keyed where it actually runs. The comment claiming a pane can never start outside its workspace was simply wrong. Family match: narrow the previous commit. setSelectedCodexAccountIdForTarget only nulls every WSL slot when the account is null AND no distro is named; any other write lands in one slot. So claim the family only when the change actually cleared them all, and let `add` pass the created account's concrete target rather than the row's "WSL default". Both call sites already knew which case they were in. Refs #10757 * fix(codex): derive the pane's project runtime the way main does The previous commit reached for getLocalProjectExecutionRuntimeContext as a stand-in for main's resolveLocalProjectRuntimeForWorktreeId. They are not the same function, and the differences both produce wrong lanes: - It falls back to `state.activeRepoId` when the worktree is not a git worktree, so a folder-workspace pane inherited whichever repo happened to be selected. That is not a property of the pane at all — the lane moved when the sidebar selection moved. On a WSL project it both muted a healthy host pane and hid the notice a host switch owed it. - It synthesizes a runtime from `inherit-global` where main returns undefined, and its host branch rewrites an explicit `wsl.exe` to powershell.exe, keying a live WSL pane `host`. Walk repo -> project directly instead, which is what resolveLocalProjectRuntimeForRepo does, and use it only to supply a distro — never to downgrade a shell. That also drops the throwing call out of this path entirely; the lane runs outside scanCodexPanes' inspection guard, so a throw there would have lost the notice for every pane in the batch, not just one. Also find the added account by diffing the roster. Reading it back through the row's active id returns null once two distro slots are filled, which sent the notice to `wsl:__default__` while `add` had written a concrete distro. Refs #10757 * fix(codex): key the lane off the runtime the renderer actually shipped Reverses the project-runtime half of the previous commit. That commit assumed main resolved the project runtime itself, so it re-derived one by hand. It does not: for a local pane the RENDERER computes it with getLocalProjectExecutionRuntimeContext and ships it with the spawn (pty-connection.ts), and pty.ts feeds that straight to getCodexSelectionTargetForPty. So the helper is not an approximation to be improved on — it is the launch. The hand walk dropped the global Windows runtime default, which is what turns an `inherit-global` project preference into WSL. A user who set their runtime default to WSL but left terminalWindowsShell alone would have had every live WSL pane keyed `host`: muted by a host switch, and missed by their own. It also disagreed on folder workspaces, where the launch really does resolve through the active repo. Keep the repair-required early return: that call throws, and it sits outside the scan's per-pane failure guard, so a throw would lose the notice for every pane in the batch rather than one. Separately, floating terminals have no workspace root, so their startup cwd is used verbatim (resolveTerminalStartupCwdForWorkspace). Resolving one against a root that does not exist yielded no cwd at all, keying a floating Codex pane on a WSL filesystem as `host`. Read its cwd directly. Require exactly one new account before trusting the roster diff — an unloaded prior roster makes every account look new, and Add Account is not gated on it. Refs #10757 * fix(codex): stop claiming a floating-terminal cwd the tab never has The floating-terminal branch read tab.startupCwd, which no floating creation path ever sets (FloatingTerminalPanel, FloatingTerminalWindowControls, floating-workspace-tab-creation all pass none). Its cwd is resolved over IPC from settings.floatingTerminalCwd and handed to the transport as a prop, so it never reaches the store at all. The branch was inert and its comment described main's handling of args.cwd rather than what the code read. Say what is actually true: a floating pane is keyed by its shell, and the configured-WSL-cwd-under-a-host-shell case is a known gap. Guessing from the unresolved setting would risk the mute direction, which is the expensive one. Also pin the repair-required early return. resolveLocalWindowsTerminalRuntimeOptions throws there, and the lane runs outside scanCodexPanes' per-pane failure guard, so without it Promise.all rejects and every pane in the batch loses its notice. That guard had no coverage; removing it now fails with the spawn error. Refs #10757 * fix(codex): trust the lane main recorded at spawn over a re-derived one The switch path re-derived each pane's Codex lane from current state while main had already written the resolved shell, cwd and distro at spawn. Four review rounds each found another divergence between the two, and the derivation still answers for a launch that never happened once the user edits a runtime preference. Prefer the recorded lane where one exists; keep the derivation for the panes main never records — pre-feature panes, LocalPtyProvider spawns and remote ids — and log when the two disagree. * refactor(codex): drop a redundant guard around the recorded-lane lookup |
||
|
|
2cf91b8e69 |
fix(skills): complete plugin-cache scans without false attention (#10865)
Fixes the P0 where skill cards showed an unclearable amber "Needs attention" while the Details dialog reported everything up to date. Root cause: when the plugin-cache scan tripped one of its own bounds it recorded an incomplete path, and inventorySkillFreshness expanded that into one fabricated placement per manifest skill at a path it never stat'ed. Those synthetic "inaccessible" copies lit the pill, were filtered out of the dialog, and could never be cleared because plugin-cache is not an updatable topology. - Removes the fabrication; reports typed scan issues instead. - Requires readable SKILL.md evidence before promoting a directory to a candidate, so a same-named foreign plugin (Codex's own computer-use) no longer flags. - Prunes skill payloads and node_modules so ordinary vendor caches stop tripping the depth and entry bounds. - Partitions scan reasons: only a real read failure raises a pill; bounds that ended the walk block an all-clear claim; the rest are Details-only. Fixes #10633. Refs #10659, #10904, #10918, #10775, #10791, #10813. |
||
|
|
025ceec04e | test(e2e): harden release latch and teardown coverage (#11036) | ||
|
|
e4b113b11d | fix(native-chat): keep known sessions loading until flush (#11032) | ||
|
|
dbbeae3512 | fix(terminal): clear retained overlay for editor tabs (#11030) | ||
|
|
84c70d6e29 |
fix(mobile): heal a stale input line before sending diff-review notes (#11035)
* fix(mobile): heal a stale input line before sending diff-review notes The stale-input marker is keyed by terminal handle, not by surface, so a paste orphaned on a terminal by native chat is still marked when the user sends diff-review notes to that same terminal — and those notes were submitted on top of it. Gate the send on the heal, as the native-chat answer send already does; when the clear fails, surface the error instead of dropping the note silently. Completes a follow-up deliberately deferred by #10480. * test(mobile): assert the post-heal send carries the notes, not another clear * style(mobile): trim the stale-heal comment to its non-obvious why Keeps the terminal-handle keying and the #10228 link (why a NativeChat-named helper runs in diff review) and the deviceToken rationale; drops the clause that restated the call. Addresses CodeRabbit review feedback. |
||
|
|
12ef12c55b |
chore(quality): ratchet Oxlint, React Doctor, and Zustand performance (#11034)
* chore(quality): ratchet lint and Zustand performance * fix(ci): stabilize React peer lock snapshot * fix(ci): isolate PR diff and React Doctor CLI |
||
|
|
9a8e21a47e |
fix(workspace-space): bound traversal memory and serialize local disk scans (#11026)
* fix(workspace-space): serialize local disk and cap traversal memory Prevent resource exhaustion during large workspace scans by limiting local disk access to one concurrent `du` call and capping portable traversal memory to 100k entries or 64 MiB per worktree. Fixes July 27 incident with 298 worktrees causing host stalls and renderer OOM. Portable traversals now use fixed-worker iterative frames instead of recursive promises. Capacity failures become unavailable rows. Behavior below limits is unchanged. * fix(workspace-space): bound concurrent SSH fallback traversals Desktop-side SSH fallback traversals run in the main process with independent admission budgets. Without limiting, up to six concurrent traversals could stack six 64 MiB budgets. Cap remote fallback traversals to 2 concurrent, keeping aggregate admission at 2 × 64 MiB. Also make capacity error messages reflect configured limits instead of hardcoded defaults. |
||
|
|
55e98e8182 |
fix(native-chat): auto-grow the composer input up to 8 lines (#10848)
Reviewed with an independent reproduction. Replaced the JS measure pass with layout-native CSS on both surfaces and removed the shared hook, fixing 72-142px of Linear issue title hidden after a window resize. |
||
|
|
60c7faf930 |
fix(browser): restore configured zoom after page reload (#10800)
Reviewed with an independent reproduction. Rewrote the reload-zoom reassert to be per-pane instead of sharing the value zoom in/out writes, fixing Cmd/Ctrl+0 reset and cross-tab zoom leakage, with E2E coverage proven to fail on revert. |
||
|
|
872a9c3930 |
fix(terminal): make copy work in the HTTP web client (#10534)
Reviewed with an independent reproduction over a real plain-HTTP origin. Replaced the hidden-textarea fallback with a capture-phase copy event so clipboard text never enters the DOM, and added rejection handlers to the surfaces that had none. |
||
|
|
b601643960 |
fix(sidebar): stop worktree cycling from reopening collapsed groups (#10513)
Reviewed with an independent reproduction. Replaced a redundant per-keypress layout rebuild with cycling over rendered sidebar rows; verified the chord is not hardcoded to metaKey. |
||
|
|
f1d54c123b |
fix(file-explorer): commit inline rename on outside click and stop double-click rename flicker (#10867)
Reviewed with an independent reproduction. Both fixes verified; added a flush for a dropped directory toggle and an integration test connecting blur/Escape through to renameFileOnDisk. |
||
|
|
c6076a507c |
ci(release): detach non-blocking E2E (#11031)
* ci(release): detach non-blocking E2E * test(release): pin E2E dispatch retries |
||
|
|
b316174525 |
fix(panes): stop a stray mouse hijacking a touch divider drag (#11021)
The drag predicate recorded only the active pointerId, never its type, so during a touch-started drag any primary mouse or pen event failed the id match but passed the non-touch fallback and was accepted as the active pointer. Record the type at pointerdown and require both ends to be non-touch, keeping the WSLg mouse-press/pen-motion relay working. |
||
|
|
edc6cc007d |
fix(new-workspace): center agent selection in create dialog (#11020)
* fix(new-workspace): center agent selection in create dialog Pin the Agent combobox mark to 14px, drop residual button padding, and use a full-width min-w-0 trigger so icon, label, and chevron align with Project/Name in the new worktree dialog. * fix(new-workspace): optically align agent picker content * fix(new-workspace): center selected agent content |
||
|
|
dc45f79465 |
fix(runtime): drain the session-tabs coalescer when a listener subscribes (#11022)
multi-client-navigation-isolation.integration.test.ts fails intermittently on
clean main — measured at 4-8/24 with a flush-window sweep — and has been taxing
unrelated PRs across the repo.
Mechanism, confirmed by instrumented trace (schedule t=23412, listener REGISTER
23460/23461, coalescer fire 23462): a ~4ms race between the 50ms session-tabs
notify coalescer and listener registration in onMobileSessionTabsChanged. When
the pending timer fires after the listener registers but before
session.tabs.activate is handled, it emits a stale
{type:'updated', activeTabId:'host-tab'} that the test consumes.
flushAll() at the top of onMobileSessionTabsChanged, before the listener joins the
set — mirroring the flushAll() its own unsubscribe closure already performs.
DRAINING rather than cancelling is load-bearing: cancel/dispose has no emit, so it
would silence the stale frame but DELETE the pending update for subscribers
already registered, leaving them stale until an unrelated next schedule(). That
would trade a flaky test for a real lost-update bug. The newcomer cannot miss an
update by having it drained — listMobileSessionTabs and the coalesced emit read
the same mobileSessionTabsByWorktree map, and snapshot-read to listener-add is
macrotask-atomic.
Sweep: 4-5/24 before, 0/24 after. The regression test uses fake timers and is
two-sided — it fails if flushAll is removed (stale frame delivered) and fails if
flush is swapped for dispose (nothing drained), on different assertions, so it
pins the specific choice rather than merely the presence of a change.
Reviewed independently and returned clean with both mutations re-run by the
reviewer. A latent flushAll re-entrancy double-emit exists in principle — fire()
does not re-check pending.has the way flush() does — but is unreachable: both
production callers' listeners only invoke the dispatcher reply, a shape that has
shipped since #8141 on the symmetric unsubscribe flush.
Not verified: live paired-device mobile/relay behaviour (no device available); the
mobile conclusion rests on a code trace.
|
||
|
|
4340781c9f |
fix(codex): drop the resume argv when session provenance is unverifiable (#10805)
Closes #10793. When Orca could not verify the originating Codex session file it either threw — a red per-pane toast and a failed spawn, reported as constant spam on #10757 — or returned null. Returning null did NOT start a fresh session: the renderer had already baked ['codex','resume',<id>] into the command and pty.ts never rewrote it, so CODEX_HOME simply fell through to whichever account was selected. The resume argv is now dropped so a plain `codex` launches, with a banner telling the user. The invariant — never run `codex resume <id>` under an account that does not own that rollout — is now satisfied by construction rather than by refusing to spawn. A verified resume is unchanged and still pins CODEX_HOME to the originating home. Reviewed over two adversarial rounds; seven defects found and fixed, including a HIGH where local-provider (non-daemon) spawns still carried ORCA_SEQUENCED_STARTUP_COMMAND with `resume <id>` — the wrong account behind a banner claiming it started fresh. `env` is now declared after the strip so no point in the handler can reach the pre-strip value. Live-validated in a real Orca dev build: all five cases proven on the SPAWNED PROCESS, including a real rollout under an untrusted home (the only shape that discriminates) and the local-provider path forced by stopping the daemon. An earlier CI failure on multi-client-navigation-isolation.integration.test.ts was investigated and is a PRE-EXISTING flake — a ~4ms race in the session-tabs notify coalescer that fails 5-8/24 on clean main, more often than on this branch. Fixed separately in #11022. Not verified: no Windows execution — its POSIX-only tests skip there and the #10757 reporter is on Windows. SSH is partial: no spurious banner or drop observed against a real target, but headless spawn does not deliver startup commands so the remote argv could not be read. The relay/mobile notice channel deliberately has no banner; the argv drop does happen there, so the invariant holds. |
||
|
|
bd5a991ce7 | fix(e2e): launch plugins with real app identity (#11024) | ||
|
|
9c5d827d6a |
fix(codex): keep history, restarts, and account identity across an account switch (#10770)
Fixes #10757. Switching Codex accounts broke three ways, all rooted in the self-contained per-account CODEX_HOME from #9501. HISTORY DISAPPEARED. Codex's own /resume picker only lists rollouts under the launch CODEX_HOME, and nothing bridged history into a per-account home — only the AI Vault's discovery scan knew about the other homes. Every other Orca-visible home's rollouts are now hardlinked in, on selection and again at launch, so one physical log is listed everywhere. THE RESTART PANEL STUCK. A queued restart was only drained by a mounted TerminalPane, but the prompt covered every stale pane in the worktree including parked and cold-deferred tabs. Requesting a restart now answers the prompt immediately while the pane keeps its pending restart, and a pane drains it when its reconnected PTY binds. PANES STAYED ON THE OLD ACCOUNT. CODEX_HOME is fixed in a shell's environment at spawn and the daemon keeps those shells alive across app restarts, while the restart notices are renderer state and are discarded. Each PTY's launch account is now recorded on disk and compared against the current selection at startup. Also merged in: #10802 (a dismissed notice no longer kills the pane's keyboard), #10803 (the sweep arms on real PTY binds, and launcher Codex panes are no longer filtered out by Windows deepest-process reporting), #10804 (a resume-pinned pane now says which account it is on), #10870 (the restart card no longer parks focus on its destructive Restart button), #10853 (the retry ladder is widened past the Windows worst case). Six independent reviews found real defects in every original PR, several of them dead-keyboard bugs and three introduced by the fix for another defect in the same loop. Live QA on macOS covered every PR; Windows was validated three times. WINDOWS: pass 1 found two defects that made the stale-account fix a no-op there (the sweep fired before any PTY was bound and never retried; launcher panes were filtered out). Pass 3 at the merged head: the prompt appears on its own after a restart — warm ~3.7-4.2s, cold ~21s needing rung 4, so #10853's widening was load-bearing rather than precautionary; an ordinary sentence typed into a healthy pane while another pane's card is up reaches that pane and kills nothing; a pane running vim after exiting Codex gets no card, still none 45s later. auth.json byte-identical across every pass. KNOWN GAPS, stated rather than implied: #10804 is unverified on Windows (auto-resume could not be manufactured there); cross-volume Windows is untested and expected to yield no bridged history (EXDEV, and Codex ignores symlinked rollouts); a cold-parked pane never binds so the sweep never covers it; the subagent-deepest launcher shape could not be reproduced on Windows, so that branch is fixture-verified only; WSL passed isolation but the resume mechanism is host-lane only. A host-account switch also marks and mutes live SSH remote panes — confirmed pre-existing on main by two independent QA runs — tracked separately in #10992. Related pre-existing defect filed as #10863. |
||
|
|
0956d5ca3a |
feat(skills): run skill updates in the background without a terminal (#10843)
* feat(skills): run skill updates in the background without a terminal The Update skills dialog had no primary action at all — its footer was only Re-check and Close, and the real action was a pre-filled command in an embedded PTY that the user had to press Enter on. Orca already builds and validates that command, so it now runs it. - Add a headless runner for `npx --yes skills update <names> --global -y`. Both --yes flags are load-bearing: npx's skips the package-install prompt, and the skills CLI's takes its own non-interactive branch. stdin is ignored so `process.stdin.isTTY` stays falsy, which is the other half of that gate. - Own the run in main so closing the dialog backgrounds it instead of killing it, and surface it in the status bar: spinner while running, a green check on success that clears itself, and a failure that persists until acted on. - Derive per-skill outcomes by re-scanning the freshness inventory after exit rather than parsing stdout. `skills update` has no --json (that flag exists only on `list`) and reports progress per-source, not per-skill, so the run bar is deliberately indeterminate instead of faking a percentage. When the re-scan has a verdict it outranks the exit code. - Drop the version trail from the rows and surface the skill list and skip reasons directly instead of hiding them behind a disclosure. Also fixes a width bug the collapsed disclosure used to hide: deep plugin-cache paths set the dialog's width and pushed the footer actions off-screen. * refactor(skills): use one row component across every update state The ready and running views were separate components with different row shapes, so pressing Update swapped the dialog's body for a different layout. They are now the same `SkillUpdateRow` instances throughout — only the status slot's contents change — and a test asserts the row is literally the same DOM node from "update available" through pending to the result. - Collapse each skill's locations behind its own disclosure. A skill with several plugin-cache copies was dumping every path inline and burying the actions; the row now shows a location count and expands on demand. - Put status in a single slot between the name and the count rather than a leading icon column. A leading icon has nothing to show in the resting state and reserving its box just indented every name past an empty gap. - Pin the running/finished run's names in `groupSkillFreshness` so a successful update doesn't drop its own rows the instant the re-scan lands. `skill-freshness-group.tsx` becomes `skill-location-chip-copy.ts` — only its chip label/tooltip helpers survived, and it no longer holds JSX. * fix(skills): place the status glyph left of the skill name Review feedback on the row header: the badge belongs immediately right of the name so it reads as part of it, and the run's status circle/check belongs to the left of the name rather than sharing the badge's slot on the far right. Name, glyph and badge are now one left-aligned group; the location count and chevron stay right-aligned. `available` still has no leading glyph — an empty reserved box only indents the name past a gap with nothing in it. * fix(skills): correct the headless update run's verdict, cancel path, and stopping copy Review fixes for the headless skill-update runner. Main process: - Judge per-skill outcomes on a positive signal. "Absent from eligibleUpdateNames" is not success: a deleted, half-written, or unreadable skill also leaves that list, so a corrupt update reported a green check. skillUpdateFailedNames now requires every convergent placement to come back current or newer-known. - Retire a child's handlers with a per-run token. A failed spawn emits error *and* close, so the second settle clobbered the real spawn ENOENT; a cancelled child could also settle, or write output into, the run that replaced it. The token guards the rescan's finish closure too. - Hold the run `running` until the killed process tree is actually dead. Releasing on the synchronous path let an immediate re-Update spawn a second npx writing the same bundles, with a watchdog so a sweep that never settles cannot wedge the run. - Kill the tree, not just the npx wrapper, via killWithDescendantSweep. - Publish an error instead of a silent `started: false` when the cmd.exe rail rejects the resolved npx path, which a profile directory containing & or % is enough to trigger on Windows. - Coalesce captured output into one push per tick instead of structured-cloning the whole buffer to every window on each progress frame. Renderer: - Keep rows on screen while the settling re-scan runs. Refreshing the inventory nulls it synchronously, so every row vanished at the moment the result appeared. Rows render off the last good scan; eligibility stays on the live snapshot so nothing is authorized off stale bytes. - Retry the names that failed, not the eligibility list that same re-scan has just emptied. - Add Stop, restoring the escape hatch the embedded terminal used to provide, and say "stopping" on every surface rather than claiming the update keeps running in the background. - Show a skipped skill's reason outside the disclosure, so it no longer depends on a mount-time defaultOpen a later re-scan can never re-fire. - Drop the summary line telling users to open "Update details", a control this PR removes; it was translated into four languages. - Keep the success linger from retiring a result the open dialog is showing. - Delete skill-location-chip-copy.tsx: an unreferenced copy of the old row component, colliding on basename with the module that is actually imported. * fix(skills): divide update list from summary |
||
|
|
72875bda24 | fix(ci): stabilize flaky terminal and SFTP tests (#11018) | ||
|
|
3d98cda5b2 | fix(release): accept lowered telemetry declarations (#11019) | ||
|
|
c75c04eaae |
fix(runtime): reclaim orca-runtime.json when it stops describing this runtime (#10840)
* fix(runtime): reclaim orca-runtime.json when it stops describing this runtime On macOS the Chromium single-instance lock is silently defeated whenever `SingletonSocket`/`SingletonCookie` go missing from the profile — and the socket they point at lives under `$TMPDIR` (`/var/folders/.../T`), which macOS purges after 3 days (`com.apple.bsd.dirhelper`, CLEAN_FILES_OLDER_THAN_DAYS=3). A launch that slips past the lock runs a full startup, republishes `orca-runtime.json` with its own pid, and leaves the CLI on a dead pid once it exits: `orca status` reports `stale_bootstrap` and every terminal command fails `runtime_unavailable` while the original app keeps serving. The owner now watches its own discovery record and republishes once no live runtime is described. Reclaiming only a dead pid is deliberate: two live runtimes sharing a profile would otherwise fight over the file. Reproduced on macOS with two real Orca main processes on one profile: the second instance took the lock and clobbered the record, and killing it left `stale_bootstrap` against the still-healthy first instance. With this change the owner reclaimed the record in ~2s and the CLI returned to `ready`. Refs #7848 * test(runtime): assert stop() clears the metadata ownership timer The republish guard alone kept the shutdown test green, so the watch teardown was unasserted. Also drop the doc claim of startup/activation callers that do not exist. * test(runtime): stand in a real live pid for the sibling-runtime case Windows never assigns pid 1, so the hardcoded sibling read as dead there and the watch would reclaim the record. Own a synthetic pid instead and let process.pid play the live sibling. |
||
|
|
c25a130236 | release: v1.4.160-rc.2 v1.4.160-rc.2 | ||
|
|
0edc95fa35 |
perf(editor): cut per-keystroke work on two rich-markdown paths (#10862)
* perf(editor): cut per-keystroke work on two rich-markdown paths Doc links: both plugins walked every text node and ran matchAll on each — the auto-convert appendTransaction once per keystroke, the preview decorations once per keystroke and again per caret move. A link needs `[[`, so gate on a native substring check first. The two walks had duplicated their guard sequence; they now share one predicate. 3.1x-3.8x over the repo's own markdown. Annotations: resolving a comment's block re-serializes the whole document (every node, plus every adjacent pair), and both the highlight-range and comment-at-position paths did that once per comment — O(comments x document). Build the blocks once and pass them down. On a 12-node fixture with 8 comments that is 184 serializations down to 23. * test(editor): pin the one-build serialization baseline Review feedback, all four points: - The serialize-count assertions compared many-comments against one-comment, so they would have passed if BOTH built blocks twice. Pin the absolute count (23 = 12 nodes + 11 adjacent pairs) derived from the fixture size, so a regression to per-comment building fails instead of comparing equal. Verified by reverting the hoist: 2 tests fail. - Skip an empty benchmark corpus instead of evaluating `index % 0` and dereferencing undefined. - Build fixture paths with path.join. - Condense the benchmark header to purpose plus parity guarantee. Co-authored-by: Orca <help@stably.ai> * test(editor): harden doc-link performance evidence Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
d547e278f9 |
fix(mobile): deliver the notifications a reconnect missed, and never persist a watermark past them (#10816)
* fix(mobile): keep the reconnect watermark alive across the app's own teardown The catch-up added in #8690 could never run. app/index.tsx unsubscribes the notification stream on every non-'connected' state and builds a fresh subscription on reconnect, so the closure holding the ready-counter, the delivered watermark and the seen-set is destroyed exactly when a reconnect needs them. Every reconnect looked like a cold open, `reconnectReadyCount` was always 1, and notifications dispatched while the socket was down were never fetched. Move that state to a per-host module-scope session so it survives the teardown. Refs #8591 Co-authored-by: Orca <help@stably.ai> * fix(mobile): tag the notification watermark with a counter epoch so a desktop restart can't kill catch-up The desktop's notification `seq` is a per-process in-memory counter that starts at 0 on every launch. The mobile client's watermark is persisted in AsyncStorage and monotonic. After a desktop restart the two index different counters, so a client holding seq 57 meets a fresh counter at 2, `57 >= 2` cuts everything, and reconnect catch-up dies silently until the new process out-dispatches the old watermark — 57 notifications later. Users see nothing and get no error (#8591). Stamp every dispatched notification with an epoch identifying the counter lifetime, ride it on the `ready` frame and the getMissedSince response, and persist it beside the watermark. A watermark whose epoch doesn't match the live counter is void: the client resets to 0 and the desktop returns its retained buffer instead of nothing. The epoch param is optional on the wire in both directions, so a client or daemon that predates it degrades to today's seq-only cut rather than erroring. Also extracts the OS-permission helpers to notification-permissions.ts (re- exported, so no importer changes) to keep mobile-notifications.ts under its max-lines budget. Mutation-tested: 3 mutations applied to the epoch logic, 3 killed — including the storage-seed race guard, whose first mutant survived until the deferred-read test was added. * fix(mobile): make the notification watermark atomic and counter-scoped Round-1 review found four ways the epoch fix could still lose notifications. All four are addressed here. 1. Seen-set survived an epoch change. Seen-keys are seq-derived, and terminal bells carry no notificationId (they key on `seq:N` alone). After a restart the fresh counter re-issues low seqs, so a replayed post-restart bell was dropped as a duplicate of a bell from the previous counter. The dedup window belongs to one counter lifetime, so it is cleared on epoch change. 2. Legacy watermarks were trusted. Pre-upgrade installs stored a bare seq with no epoch. Adopting the first observed epoch as "nothing changed" left that unprovenanced seq cutting a counter it was never measured against — #8591 through the upgrade path. An epoch-less seq no longer survives adoption. 3. seq and epoch were separate storage keys. A process death between the two writes left epoch-B beside seq-57-from-A: a pair that looks internally valid on the next launch and is therefore trusted. They are now one JSON value, which cannot tear, with a read-only migration from the legacy key. 4. Sessions were never retired. They live at module scope so they survive the subscription teardown a reconnect performs, so host removal is the only thing that can drop them. Removal now retires the session and its watermark. Mutation-tested: 3 mutations, 3 killed. The first version of the bell test passed with the fix removed — it exercised the live path, which only adds to the seen-set; only the replay path consults it. Rewritten against the replay path, it fails with `expected 1 to be 2`: the literal lost notification. Mobile notifications + transport: 355 passed. Desktop replay: 11/11. * fix(mobile): catch up on the first connection after a cold open Catch-up hung off 'has this process connected before', which is false on the first ready of a fresh launch — exactly the post-upgrade / post-eviction case that loses everything between the stored watermark and the next live seq. Wait for the persisted read, then catch up whenever this device has delivered for the host before; a first-ever pairing still gets no replay. Co-authored-by: Orca <help@stably.ai> * fix(mobile): serialize live delivery behind the watermark seed, and key catch-up on the record Co-authored-by: Orca <help@stably.ai> * test(mobile): pin the two catch-up mechanisms mutation testing found unguarded Mutating each mechanism of the #8591 fix in turn showed two survived with the suite still green: the seed's epoch-provenance check, and the host session outliving the subscription teardown. Both are load-bearing, so pin them. - seen-set survives teardown: the desktop's retained buffer replays a notification already delivered live, and only the session-scoped seen-set stops a duplicate banner. - a seed resolving after a live epoch was adopted must not reinstate the dead watermark. Not reachable through subscribeToDesktopNotifications today ('ready' awaits the seed first), so it asserts on the exported pair and says so. Co-authored-by: Orca <help@stably.ai> * fix(mobile): serialize notification delivery per host so the watermark can't outrun what was shown Addresses two MAJOR findings from review of this branch. MAJOR #1 — the watermark could be persisted past a notification the user never saw. `deliverLive` advanced `lastDeliveredSeq` before awaiting the local show, and replay + live delivery ran concurrently, so a live seq 11 handled while catch-up was still showing seq 6 persisted 11. A process death before 7..10 were shown lost them permanently: the next launch asks the desktop for seq > 11. This predates the branch — `origin/main` advances the watermark at the same point — so it is a residual this fix closes, not a regression the branch introduced. It is fixed here because the branch is what makes the watermark load-bearing. Three changes: - the advance moves AFTER the show/dismiss await, so the watermark means "everything up to here reached the user" rather than "was dispatched" - a per-host `deliveryTail` promise chain (`enqueueHostDelivery`) serializes deliveries, so a monotonic advance is also an in-order one - the catch-up batch is ONE queue entry, not one per event. Awaiting per event returns to the event loop between replays and let a live event slot in between seq 6 and 7 — which is exactly the interleave being fixed. The RPC stays outside the queue: `sendRequest` waits up to 30s and holding the chain for that would stall live delivery on a slow link. MAJOR #2 — every delivery awaits the persisted read, so an AsyncStorage read that never settled disabled the host's notifications for the whole app lifetime, with no error and nothing to see. The seed is now bounded at 3s; a late seed still applies when it lands. Proceeding unseeded is strictly better: the watermark stays 0, so catch-up over-fetches and the seen-set de-duplicates. Serializing removed an overlap the duplicate-suppression relied on: `showLocalNotification` deduped two same-id events by observing the first still pending when the second arrived. With deliveries serialized the first completes first, so the second saw no pending state and scheduled a second banner for the same notification. The claim moves to enqueue time, where the overlap is still observable. Dismisses are deliberately not claimed — a dismiss for a shown id is what retires it. Evidence — each mechanism disabled individually against the unchanged suite: - batch-as-one-entry -> reverted to per-item enqueue: ordering test fails - watermark advance -> moved back before the await: ordering test fails - seed timeout -> removed: wedged-read test fails - live-path claim -> removed: concurrent-dedup test fails - replay-path claim -> removed: cross-path dedup test fails Each kills exactly one test, so no mechanism is unguarded and none is redundant. `mobile-notifications.test.ts`'s local `flushAsync` drained 10 microtask ticks. Deliveries are now several awaits deeper, so a fixed tick count under-drains; it yields to the macrotask queue instead. Verified with real timers that the behavior it asserts is unchanged — only the drain depth was wrong. Full mobile suite: 344 files, 2499 passed, 2 skipped. tsc clean, oxlint clean. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
2dac0741b4 |
fix(terminal): stop answering mode-2031 toggles that the same chunk withdrew (#10817)
* fix(terminal): stop answering DECSET 2031 subscriptions fish already withdrew fish enables and disables mode 2031 around every prompt (tty_handoff.rs), so a single PTY chunk routinely carries `?2031h ... ?2031l`. All three responders answered the sticky "an h appeared anywhere" flag, so each prompt cycle wrote `?997;1n` into a shell that had already handed the tty to a child — it lands as literal text, or as stdin for whatever is reading. pty-connection.ts's hidden-pane responder already had the right shape (`finalState !== 'subscribed'`); this brings the other three in line: - shared tracker: gate the '2031-subscribe' fact on the chunk-final state - parked-tab byte sidecar: same guard - visible-pane xterm CSI handler: xterm dispatches mid-parse, so there is no chunk-final state to read. Defer the reply to a microtask and re-check the subscription, letting a same-chunk `?2031l` cancel it. Refs #9993 Co-authored-by: Orca <help@stably.ai> * fix(terminal): decide 2031 replies per PTY chunk, not per xterm parse The previous commit deferred the visible-pane reply to a microtask so a same-chunk `?2031l` could cancel it. That cannot work: xterm's WriteBuffer parses every queued `terminal.write()` synchronously in one batch before any microtask runs, so the microtask sees the net state of N PTY chunks, not of the one that carried the subscribe. A TUI that subscribes in chunk N gets no reply when chunk N+1 happens to withdraw, and a fish prompt straddling two writes still gets answered. Move the decision to where chunk boundaries actually exist — pty-connection's dataCallback, which receives one PTY chunk per call. It scans raw bytes with `scanMode2031Sequences`, carrying a tail across chunks so a CSI split mid- sequence still resolves, and replies only when that chunk *ends* subscribed. Ownership stays single: gate-managed PTYs are answered by main's '2031-subscribe' fact, so the chunk scanner returns early for them, and the xterm CSI handler now observes only panes the scanner does not own. The tail is dropped on PTY replacement — a partial prefix belongs to the stream that produced it. Removes the microtask responder and the seed-reply retry path it needed. Mutation-tested: 6 mutations applied, 6 killed. * fix(terminal): carry DECSET 2031 withdrawals as a side-effect fact The previous commit moved 2031 reply decisions to the PTY chunk boundary and gave gate-managed panes a single owner: main's '2031-subscribe' fact. But the fact union is subscribe-only, and that left the withdrawal unobserved. For a gate-managed pane, main drops renderer-bound bytes after model ingestion, the chunk scanner early-returns, and xterm's CSI handler is disabled. So when a TUI emits `?2031l` while hidden, nothing retires the subscription: paneMode2031 stays set, and the next theme flip has maybePushMode2031Flip push `CSI ?997;2n` into the shell that replaced the TUI — #9993 again, through the theme-change door. Before this branch, skipHiddenRendererOutput observed those withheld bytes; consolidating ownership removed that observer without replacing it. No renderer-side observer can close this: the bytes are gone before the renderer sees them. The state protocol has to carry the withdrawal, so add a '2031-unsubscribe' fact alongside the subscribe across the three fact unions (shared, provider, daemon). It fires only on a real chunk-final withdrawal — a chunk with no 2031 bytes scans to null and stays silent. The renderer handler clears both maps and sends nothing: a withdrawal is not a query. Also closes two gaps an adversarial review found by mutation, both previously resting on comments rather than tests: the lifecycle parser-ownership predicate (extracted as isPaneParserOwnedMode2031Observer so it is directly testable) and the scan-before-reconciliation ordering that lets a chunk the snapshot drops as a duplicate still answer its query. Mutation-tested: 12 mutations applied, 12 killed (6 from the prior round re-run, 6 new covering this fix and the two survivors). * fix(daemon): refuse 2031 authority from a daemon that cannot retract it Round-2 review found a wire-compatibility hole in the original #9993 fix. Daemons survive app updates, so a new desktop can drive a daemon that was started by the previous build. Pre-v29 daemons emit '2031-subscribe' but have no '2031-unsubscribe' fact at all. For a gate-managed pane, main drops the renderer-bound bytes before the renderer sees them, so main's transient facts are the ONLY thing that can retire a subscription. Against such a daemon a TUI exiting while its pane is hidden leaves the subscription registered forever, and the next theme flip injects CSI 997 into whatever shell replaced it -- #9993 all over again, reached through the upgrade path. Gate it: bump PROTOCOL_VERSION 28 -> 29, add MODE_2031_UNSUBSCRIBE_FACT_PROTOCOL_VERSION with supportsMode2031UnsubscribeFact(), and drop '2031-subscribe' from any daemon below that floor. Trade-off: a gate-managed pane on a preserved v28 daemon keeps renderer-scanner authority instead of daemon-fact authority. That is exactly the pre-fact behaviour -- correct for visible panes, no worse than today for hidden ones -- and it resolves on the daemon's next restart. Non-2031 transient facts (bell, etc.) are unaffected at every version. Tests: two adapter regression tests (v28 drops subscribe, v29 forwards it), plus a version-pin test asserting the floor sits above every entry in PREVIOUS_DAEMON_PROTOCOL_VERSIONS -- so adding a new preserved version cannot silently re-open the hole. Mutation-verified in both directions: `false &&` (under-block) and `true` (over-block) each fail the new tests. * fix(daemon): gate background delegation, not just the fact stream A pre-v29 daemon can announce a 2031 subscribe but never retract it. Filtering that fact is not enough: while a pane is visible main's own scanner registers the subscription, and scan authority only moves to the daemon when the session is backgrounded. So the gate belongs on setPtyBackgrounded — decline to hand a non-retracting daemon authority at all, and main stays authoritative over the whole stream. Co-authored-by: Orca <help@stably.ai> * fix(daemon): clear a preserved pre-v29 background hint at attach, not just at background Co-authored-by: Orca <help@stably.ai> * fix(terminal): don't answer a 2031 subscribe whose withdrawal straddles a chunk Review found the chunk-final-state fix left one hole open. When the kernel cuts fish's toggle pair mid-withdrawal — chunk 1 ends "...?2031h prompt ESC[?20", chunk 2 is "31l" — chunk 1 genuinely ends subscribed, so it answers, and the reply lands as literal text at the prompt. Chunk 2 then recognizes the withdrawal but cannot recall bytes already written. The same byte stream is safe or corrupting purely by where the kernel split it. The scanner already retains an incomplete private-mode tail; it just didn't tell the caller whether that tail could still resolve to 2031. It now does, and a subscribe is held one chunk while the answer is still in doubt. Only subscribes defer — retiring a subscription writes nothing to the pty, so withdrawals stay eager. Deferral is narrow: a trailing "ESC[?25" (cursor hide) can never become 2031, so a subscribe already seen in that chunk is still answered immediately. This case predates the branch — the old sticky-flag policy replied here too — so it is a residual this fix now closes rather than a regression it introduced. Tests: three cases pinned (split withdrawal, non-2031 partial must not defer, split re-subscribe answers once). Removing the deferral fails only the first. * fix(terminal): preserve mode 2031 reply decisions * fix(build): record daemon protocol v29 compatibility --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
a49d68f8c2 |
perf(git): overlap getBranchCompare's head-of-chain reads (#10895)
* perf(git): overlap getBranchCompare's head-of-chain reads
Four git spawns ran strictly in series before any compare work began:
branch --show-current, the base-ref probe, rev-parse HEAD, and rev-parse <base>.
Three are independent -- compareRef is display-only metadata and HEAD's oid does
not depend on the base ref -- so they now run concurrently. The fourth was
redundant outright: the probe already runs `rev-parse --verify --quiet
<ref>^{commit}` and discarded the oid it printed, which was then re-resolved by a
second spawn. resolveWorktreeBaseCommitOid returns that oid so it can be reused;
hasWorktreeBaseCommitRef now delegates to it, leaving its other 4 callers
untouched.
3.6-3.7x on a short remote base label (192ms -> 52ms), 1.44x on an
already-qualified refs/... base, which skips the probe by design.
Reuse is keyed by ref: resolveWorktreeAddBaseRef returns at its first successful
candidate, so only that ref's oid is ever read back. Peeling is safe because only
refs/heads and refs/remotes candidates reach the probe, where ^{commit} is a
no-op.
No new git features: this removes a spawn rather than adopting an option.
Co-authored-by: Orca <help@stably.ai>
* fix(git): preserve compare semantics across providers
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
|
||
|
|
6677b5f171 |
perf(cli): construct the runtime client only when a command needs it (#10919)
src/cli/index.ts was the only eager value-import of RuntimeClient, and five other eager modules imported just RuntimeClientError / RuntimeRpcFailureError from the runtime-client barrel -- dragging in client -> pairing -> zod -> ws -> e2ee on every invocation. Those error classes live in runtime/types.ts, which has zero children, so the five imports now point there and the client loads through the existing (already lazy by design) ctx.client getter. Eager modules 199 -> 46, with node_modules dropping 94 -> 0. `orca --help` 2.04x (59.6 -> 29.2 ms); the same for help, no-args, and both error paths, which return before constructing a client. Commands that DO construct one still gain 1.10-1.12x from not eagerly parsing the transport the local path never uses. Correction to an earlier note: websocket-transport alone is ~24 modules / ~8 ms, not the 107 / 28 ms once recorded -- that figure wrongly charged it for zod, which enters through shared/pairing on a different edge. Marginal cost, never isolated cost. Co-authored-by: Orca <help@stably.ai> |
||
|
|
077561f89a |
perf(terminal): measure stream byte length natively above a code-unit floor (#10916)
* perf(terminal): measure stream byte length natively above a code-unit floor The terminal RPC path counted UTF-8 bytes with a hand-rolled per-code-point scan that Buffer.byteLength does natively an order of magnitude faster. Routed through a small module rather than swapping the shared clipboard helper, which has ~50 renderer call sites and a partial-count contract on the over-limit path that must not change. 4.4x on an 8KiB batcher push, 4.2x on a 2MiB snapshot scan, 4.0x on the 48KiB chunk gate, and 1.26-1.33x on the adversarial early-trip shapes where the legacy scan bails after a third of the string. The floor is load-bearing, not defensive. Buffer.byteLength has a fixed ~14ns call cost against a scan iteration of ~1.5ns, so below the measured 8-12 code unit crossover the native call is a REGRESSION -- 4.2x slower at one code unit, which is keystroke echo, the most latency-sensitive PTY shape there is. Short inputs keep the scan verbatim; 16 leaves margin over the crossover so the worst sub-floor shape stays at parity. measureTerminalStreamByteLength takes the native count only when `length * 3 <= stopAfterBytes` proves the limit cannot trip, so the callers' truncated running total is never replaced by a full count. Co-authored-by: Orca <help@stably.ai> * test(terminal): benchmark production byte-length exports Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
025c242f0c |
feat(dashboard): tint agent cards by state, show the project as an icon, name the chat (#11012)
* feat(dashboard): tint agent cards by state, show the project as an icon, name the chat Three glanceability changes to the agent board: - The "Needs You" signal moves from the column border onto the cards themselves, and done agents get the same treatment in green. Idle cards that simply aren't running stay neutral, so a tint always means "this one wants you". - The repo is now its own icon with the name in a tooltip, instead of a mono label that truncated and competed with the worktree name. Icons ride the snapshot keyed by repoId — image icons are data URLs, and the snapshot republishes several times a second. - The user-message line is labelled with the tab's conversation name rather than "You", resolved through the same getAgentRowConversationName the sidebar's agent rows use. Status-only titles still fall back to "You". * refactor(dashboard): head the card with the session name, move the worktree beside the project The conversation name now sits next to the agent icon as the card's heading rather than prefixing the user-message line, and the worktree drops to the footer beside the project icon. The message line reads "You" again — the name moved up, so keeping it there said the same thing twice. Cards without a resolvable session name keep the worktree as the heading, and the footer omits it rather than repeating it. * fix(dashboard): thread settings into every snapshot builder caller Adding `settings` to DashboardSnapshotState left three callers constructing it without one. The in-window drawer's was a real defect, not just a type error: useLiveDashboardSnapshot derives its own snapshot rather than receiving the relayed one, so a dropped slice silently blanks generated conversation names in the drawer while the pop-out shows them. Bucket counts pass null deliberately — they never render a conversation name, so the sidebar stays unsubscribed from settings. Covers the drawer's wiring with a test, since `settings: null` type-checks and would blank names again without failing loudly. * test(dashboard): complete the terminal layout fixture TerminalLayoutSnapshot requires expandedLeafId; the neighbouring builder test hides this behind an `as unknown as` cast on the whole state object. |
||
|
|
a72068015f |
fix(panes): stop a stray touch hijacking a mouse divider drag (#11013)
Each pointer type has its own primary pointer, so a finger on a touchscreen arrives with isPrimary true while a mouse drag is in flight and satisfies the WSLg fallback in isActiveDragPointer. Exclude touch from that fallback; the pen path WSLg needs is unchanged and a touch-started drag still matches by pointerId. |
||
|
|
6943638053 |
perf(terminal): scan output frames by code unit, not per code point (#10915)
* perf(terminal): scan output frames by code unit, not per code point iterateTerminalOutputFrameChunks walked `for (const part of data)`, materializing a 1-2 character string per code point and calling terminalStreamByteLength on each, while accumulating the frame text with `chunk += part`. The accumulator was never needed: `chunk` only ever reconstructs the contiguous substring data[chunkStart..end), and `startSeq + chunkStartOffset + chunk.length` collapses algebraically to `startSeq + end`. Track two integer offsets and emit data.slice(chunkStart, end) instead, computing UTF-8 width inline from charCodeAt. Also short-circuits the cap gate on UTF-16 length before measuring UTF-8 bytes, which is sound because UTF-8 length is never below UTF-16 length. 2.4-6.1x across payload shapes, stable across reruns. This runs per terminal output batch and per snapshot chunk. Extracted to its own module along a real seam (the chunk-emission concern plus its two types and cap gate); methods/terminal.ts shrinks by 85 lines. No max-lines suppression added and the baseline is untouched. Co-authored-by: Orca <help@stably.ai> * fix(terminal): preserve chunk sequence rounding Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
f48cb78646 |
perf(store): index worktrees and tabs once per hydration loop (#10891)
* perf(store): index worktrees and tabs once per hydration loop Four sites in hydrateWorkspaceSession and reconnectPersistedTerminals rebuilt a flattened array inside a loop and linearly searched it: Object.values(worktreesByRepo).flat().find((e) => e.id === worktreeId) That is O(rows x ids) for O(rows + ids) distinct work. Build one first-wins index per loop instead. Neither loop sets state or awaits, so a single index over the store snapshot is valid for every iteration. 54.5x at a real 10-repo / 423-worktree session with 188 pending reconnects; 2.1x on a one-repo session. This runs synchronously on renderer cold start and gates workspaceSessionReady, which blocks terminal pane mounting. First-wins matters: Array.prototype.find returns the first match, so an index that overwrote on collision would resolve a different repo for a duplicated worktree id. Both the tests and the benchmark fixture carry a deliberate cross-repo duplicate so that difference is observable. Co-authored-by: Orca <help@stably.ai> * test(store): count the generated worktree rows instead of multiplying Review feedback: the table printed repoCount * worktreesPerRepo, which misses the duplicate id makeStore injects for multi-repo cases (that duplicate is what makes first-wins observable). Count the generated map, and say plainly that the fixtures are synthetic at real-world scale rather than a replay of a real session. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
1fd0f731fc |
fix(automations): bind agent terminal output before publishing, and launch SSH folder workspaces on their own host (#10818)
* fix(terminal): bind the agent PTY before the run tab is ever published Why: launchAgentBackgroundSession created the hidden run tab synchronously and only then awaited the agent spawn, so the store briefly held a tab with ptyId: null. Terminal.tsx re-renders on that write, and for an already-visited worktree the tab can neither cold-park nor defer — a TerminalPane mounts, finds no adopt candidate, and starts a fresh default shell. When the agent PTY finally resolves it is rebound in state but the mounted pane still holds the shell, so the user sees a bare prompt and the agent PTY is orphaned (#2989). Reserve the tab id before the spawn and create the tab already bound to the live PTY, with no await in between. * fix(terminal): resolve folder workspaces, and fail closed on a tab-id collision allWorktrees() reads only worktreesByRepo, so every folder workspace looked absent and its automation died at resolution. getKnownWorktreeById covers both. On a reserved-id collision, re-keying the tab could never work: ORCA_TAB_ID and ORCA_PANE_KEY are already baked into the spawned process, so routing and hook identity would permanently disagree. Retire the launch instead. Co-authored-by: Orca <help@stably.ai> * test(automations): split the background-session suite so it stays under the 800-line cap Co-authored-by: Orca <help@stably.ai> * fix(automations): route folder-workspace agent launches to their owning SSH host Review of the bind-before-publish fix surfaced a second defect on the path it newly makes reachable. A folder workspace has no repo row — its synthetic repoId is `folder-workspace:<groupId>` — so `repos.find(...)` returns null and every repo-derived launch input silently degraded to a local default: connectionId null, platform CLIENT_PLATFORM, isRemote false. The automation then spawned on the user's machine with a cwd that only exists on the SSH host. Before this branch that path threw before reaching the spawn, so the bug was latent; making folder-workspace automations work is what exposes it. Host resolution now goes through resolveAgentBackgroundLaunchHost, which falls back to the workspace scope (the same getFolderWorkspaceConnectionId that ordinary terminal creation uses) when there is no repo. Ambiguous scopes — mixed local/remote children — still resolve to a local launch rather than guessing a host. Extracted to its own module rather than inlined: the added branch pushed launch-agent-background-session.ts over the 300-line oxlint cap, and a max-lines disable is forbidden. Tests: an SSH folder workspace must spawn with its connectionId and remote cwd; a local one must stay local. Reverting the fallback fails the first and leaves the rest green. * fix(automations): close folder dispatch and adoption races Read live state before adopting a reserved tab so a collision that lands during the spawn is retired instead of re-keyed. Route persisted folder-workspace dispatch through its ambiguity-aware owner, including SSH auth, remote trust, quiet-shell fallback, and WSL shell selection. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
974447175f | feat(terminal): add manual parking developer action (#11016) | ||
|
|
79ec57d045 |
perf(relay): keep the PTY replay window as chunks, not a re-sliced string (#10900)
* perf(relay): keep the PTY replay window as chunks, not a re-sliced string appendReplayBuffer did `buffered += data` then `buffered.slice(-REPLAY_BUFFER_MAX)` once over the 100KB cap. It runs per raw node-pty emission -- before batching -- so once a PTY saturates the window (which a long-lived shell does almost immediately) every subsequent chunk copied the whole 100KB. Reuse RecentPtyOutputBuffer, which already solved this shape in the main process: keep chunks, drop from the head, defer the join to read(). The relay's three readers are attach, adopt, and revive only. 66-205x on the append path, per PTY, on the user's SSH host. RecentPtyOutputBuffer's limit is now configurable, because the relay retains 100KB where the main process retains 64KB. One arithmetic branch still used the hardcoded constant after that change and silently under-retained (100,800 of 102,400 code units); the equivalence tests caught it before it shipped, and the suite now pins the configured limit directly. Co-authored-by: Orca <help@stably.ai> * test(relay): exercise a real surrogate split; drop eval from the benchmark Review feedback, both valid: - The surrogate test never split a pair. The cap is even and a pair is two code units, so an emoji run alone always cuts on a pair boundary. A trailing single unit shifts the cut mid-pair, leaving a dangling low surrogate (0xDE00) -- asserted directly now, with the boundary-aligned case kept as its own test. - Parse REPLAY_BUFFER_MAX as a product instead of eval(). The regex already admits only digits, spaces and `*`, and eval tripped Biome's noGlobalEval regardless of the eslint suppression. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
7f3c95a585 |
fix(git-history): stop reading the option marker as the resolved ref name (#10906)
`rev-parse --verify` swallows --end-of-options, but --symbolic-full-name deliberately echoes it -- on every git version tested, 2.25 through 2.49: $ git rev-parse --symbolic-full-name --end-of-options feature --end-of-options refs/heads/feature resolveSymbolicFullName took the first non-empty line, so it returned the literal string "--end-of-options" instead of the ref. That value flows into gitHistoryRefFromFullName, matches none of the refs/heads, refs/remotes, or refs/tags prefixes, and every named branch and tag in git history was silently categorized as a plain commit with a garbage id. Skip the marker line. Version-independent bug; no test covered it. Also pins git's echo behavior in the real-binary compatibility suite, so if a future git stops emitting the marker the reason for the skip gets re-read rather than the assumption quietly rotting. Co-authored-by: Orca <help@stably.ai> |
||
|
+21 |
b31e66ed48 |
fix(browser): survive a transient Windows lock during cookie import (#10697)
* fix(browser): retry a transient Windows lock on the Chromium cookie snapshot copy The snapshot attempt loop only reacts to a `false` return, so a throwing copy escaped it entirely. On Windows, AV/EDR briefly opens a file literally named "Cookies" with FILE_SHARE_NONE, which turns an otherwise-fine copyFileSync into `EBUSY errno -4082 syscall=copyfile` and aborts the whole import (#9355). Route the main-database copy through the existing `copyFileWithWindowsRetry`, already used for the same AV window in #1507. It is a no-op off Windows (maxAttempts=1), so POSIX still fails fast and ENOENT is never retried on any platform. * fix(browser): degrade the cookie-import staging DB instead of aborting the import Staging exists only to back the cold-restart replay for cookies the in-memory path rejects, but three points in it were fatal to the whole import (#9355): - the staging copy from the live partition DB — also a file named "Cookies", so the same AV/EDR handle blocks it; - opening/PRAGMA-ing that staged file; - both were unguarded, so a throw escaped to the catch-all and returned `ok: false` even when every cookie could load in memory. Each is now non-fatal and diagnosed. Two invariants keep the degraded path honest: `imported++` moved out of the staging insert so the summary counts importable cookies rather than staged rows, and `setPendingCookieImport` is never called when staging is unavailable — registering a path that was never written would replay a missing or partial DB over the live partition on cold start. * fix(browser): stop a degraded cookie import from replaying a stale staged database Review round 1 found the staging-degradation path could leave an older pendingCookieImports entry registered while the import rewrote the live session, so the next cold start replayed the stale DB over fresh cookies. - add clearPendingCookieImport so a degraded import retires the old entry - degrade staging on BEGIN/insert/COMMIT failure instead of aborting the import - discard the staged cookie copy on every non-registering path - pin the stagingAvailable guard, which previously survived mutation Co-authored-by: Orca <help@stably.ai> * fix(browser): report a degraded cookie import honestly instead of as a clean success Making the staging failure non-fatal introduced a silent-loss path: the import clears the live jar before loading cookies, so when staging was unavailable AND Electron rejected cookies, the user lost their old jar, got none of the new cookies, and still saw "Imported N cookies". Adds an optional `warning` to the import summary, set only on that degraded branch, and routes every cookie-import toast through a shared emitter that raises a warning toast instead of an unqualified success. Also closes three test holes found in review: - clearPendingCookieImport had no direct tests; deleting the wrong partition key survived all 10 registry persistence tests. Now covered, mutation killed. - The staging-insert-failure test was vacuous (memoryFailed === 0 suppressed registration on its own). It now forces a memory failure. - No test pinned the success-path clear; removing it survived. Now covered. * fix(github): resolve owner/repo through SSH Host aliases (#10284) (#10361) * fix(github): resolve owner/repo through SSH Host aliases (#10284) Expand OpenSSH Host → HostName via ssh -G before classifying github.com identity so PR merge works when origin is git@alias:owner/repo.git. Transport URLs stay unchanged so IdentityFile selection is preserved. Do not long-negative-cache indeterminate ssh -G failures. * fix(github): harden SSH alias resolution * Update README downloads badge * fix(persistence): fsync state writes so a rename is actually durable (#10631) * fix(persistence): fsync state writes so a rename is actually durable `Store` wrote `orca-data.json` to a temp file and renamed it. rename() is atomic for readers but says nothing about durability: without an fsync the directory entry can reach disk before the data does. After power loss or a hard crash the file can come back holding the previous state or, worse, zero bytes — and `JSON.parse('')` throws, so an empty file takes the full corrupt-file path rather than degrading. This is the same empty-file symptom as #1158 from a different cause. That issue fixed a logic path that persisted empty state and added the .bak ring as a safety net; the ring also catches this, which is why it went unnoticed. Recovery costs up to an hour of tabs/layouts/session state (backups are throttled to >=1h spacing), and a user in their first hour has no backup slot yet, so they land on defaults indistinguishable from a fresh install. Both write paths now fsync the temp file *before* the rename, then fsync the containing directory. Directory fsync is best-effort by design: Windows cannot open a directory for fsync and some filesystems reject it, so it is swallowed. The file fsync is the load-bearing part and works everywhere. Measured cost on a 3 MB payload: ~0.2 ms per write, against a 1s debounce. The async path does not block the main thread. The syscall-order test mocks `node:fs` and counts fsync targets at the module boundary, asserting ['file', 'directory'] — proving the ordering rather than inferring it from reading the implementation, since a fsync after the rename would still pass every content assertion. * test(persistence): make the syscall proof platform-aware and actually prove the order Two problems, both found from CodeRabbit's Windows observation. The assertion hardcoded ['file', 'directory']. Directory fsync is deliberately best-effort — Windows cannot open a directory for fsync and some filesystems reject it — so on Windows the helper swallows the failure, only the file fsync is observed, and the test fails. The expectation now probes the real platform instead of assuming, keeping the guarantee tight where directory fsync works rather than dropping it everywhere. Worse, the test did not prove what its name claimed. Moving the fsync to *after* the rename still passes: the file is fsynced either way, and only fsyncs were recorded, so the correct and broken orders produced an identical log. Mutation-testing the "before rename" claim is what surfaced this — the mutation passed. The rename is now recorded in the same sequence, since it is the boundary the ordering is defined against. Re-running the same mutation fails, so the ordering claim is now backed by the test rather than asserted in a comment. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> * fix(window): stop burning macOS GPU on an invisible blur effect (#8482) (#10682) Co-authored-by: Orca <help@stably.ai> * feat(sidebar): distinguish and filter CLI-created workspaces (#10712) * perf(relay): stop snapshotting the whole pending-PTY map every drain tick (#10670) * perf(sidebar): share one worktree-keyed agent orchestration index (#10678) Co-authored-by: Orca <help@stably.ai> * fix(mobile): recover unreliable relay connections (#10709) * fix(mobile): recover unreliable relay connections * test(mobile): use valid raster preview fixtures --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> * release: v1.4.157-rc.0 * docs(relay): trim the pending-output drain comment (#10714) Co-authored-by: Orca <help@stably.ai> * fix(history): quarantine unreadable recovery generations. (#10713) * fix(history): quarantine unreadable recovery files * fix(history): preserve mixed recovery generations * fix(history): reanchor reconciled live sessions * fix(history): serialize final checkpoint queue * fix(history): drain sleep shutdowns before disconnect * fix(history): restore legacy wide sessions * fix(history): preserve malformed mixed logs * fix(history): preserve malformed log tails * refactor tests to reduce file size * refactor(history-recovery): extract freeze helper and improve test robus - Extract takeRecoveryFreeze to eliminate duplicated freeze-and-clear pattern across five call sites - Skip permission-mode tests on root CI containers (chmod 0o500 doesn't block root writes) - Replace fixed sleep with deterministic wait for queued exclusive checkpoints - Distinguish ENOENT (missing) from corrupt in history metadata reads - Add ceiling-dimension restore test and torn-tail exclusion assertion - Wrap chmod operations in try/finally to prevent leaked permissions from masking test failures - Add .catch() to checkpoint promise to prevent unhandled rejections from finally re-throws * test(history-recovery): consolidate checkpoint assertions Wait for both the checkpoint call and set clear atomically to avoid a timing race where the spy fires before the set is cleared. * fix(persistence): unbreak main by expecting the new 'cli' card property in fresh defaults (#10722) Co-authored-by: Orca <help@stably.ai> * fix(sidebar): stop worktree drag from spazzing when cards resize mid-drag (#10725) * fix(sidebar): make Cmd/Ctrl+1-9 match the rendered card order when the sidebar is closed (#10693) * fix(native-chat): wrap question text and option descriptions instead of truncating (#10025) * fix(mobile): pop to home when leaving a host so the back chevron animates backward (#9723) * fix(i18n/zh): correct technical literals and clear sense errors (#10048) * feat(speech): add Korean streaming zipformer STT model (#9893) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * feat(source-control): add copy relative path (#9018) * fix(gitlab): stop refresh button overlapping dialog close X (#9445) * fix(gitlab): stop refresh button overlapping dialog close X The GitLab item dialog's SheetContent renders its own close (X) at absolute right-4, but the header refresh button sat at the header's px-5 right padding and overlapped it. Reserve pr-10 on the header so the refresh button clears the close X, and lift it -mt-1.5 so its icon aligns with the close X on the same line. * fix(gitlab): integrate sheet controls into header --------- Co-authored-by: viniciussilva <vinicius.silva@plus10.de> Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> * fix(sidebar): raise selected workspace contrast in dark mode (#8321) * feat(editor): toggle Word Wrap from file tab actions and Alt+Z (#10086) * feat(editor): toggle Word Wrap from file tab actions and Alt+Z Long single-line and structured files wrap by default and misalign. Surface Word Wrap on the editor more-actions menu for normal file tabs (diff already had it) and add editor.toggleWordWrap (Alt+Z) so users can unwrap without opening Settings. Closes #9974 * fix(editor): toggle diffWordWrap for diff surfaces on Alt+Z CodeRabbit: Alt+Z previously always flipped editorWordWrap, leaving diff panes out of sync with the markdown actions menu. * test(editor): verify word wrap shortcut routing Cover editor/diff setting callbacks and the cross-platform Alt+Z binding. --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * fix(repo-icon): detect Tauri and WebP icons (#7942) Expand repository icon auto-detection to conventional Tauri and public/icon paths with PNG/WebP magic and dimension validation. Bound SSH probing while preserving candidate priority and PNG-only user uploads; SVG remains rejected. * Add bulk tab closing to mobile long-press sheets (Close Others / Left / Right) and complete the desktop tab context menus (#9323) * Add Close Tabs to the Left and complete Close Others across tab menus and mobile long-press sheets * Fold the per-sheet Close action into the bulk-close module (session route max-lines) * fix(mobile): preserve pinned tabs during bulk close --------- Co-authored-by: Tom de Bres <tomdebres@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * feat: implement Cmd+Enter as commit shortcut in Source Control (#9773) * feat: implement Cmd+Enter as commit shortcut in Source Control * test: add unit tests for commit shortcut and tooltip formatting * fix: address review feedback on modifier keys and test coverage * test: split mac and windows/linux shortcut and keydown tests --------- Co-authored-by: Andres Van Reepingen <andres.vanreepingen@datacamp.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * Add SenseVoice speech-to-text model (Korean/Japanese support) (#7436) * Add SenseVoice speech-to-text model (Korean/Japanese support) SenseVoice (zh/en/ja/ko/yue) is the only bundled local STT model with Korean and Japanese support. The existing local models cover only English and Chinese (Parakeet, Zipformer, Paraformer); Whisper Tiny is multilingual but trades accuracy for breadth. - Add 'senseVoice' to SpeechModelType - Register the sherpa-onnx SenseVoice archive in the model catalog (pinned SHA-256, single-file model.int8.onnx + tokens.txt layout) - Handle the senseVoice type in the STT worker via createOfflineRecognizer with the senseVoice model config (auto language detection + ITN) - Add model-catalog regression tests for the new entry Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(speech): use int8-only SenseVoice archive * fix(speech): refresh SenseVoice catalog metadata --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * fix(settings): show a way back to local accounts when a remote server owns provider-account scope (#8188) Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * feat(speech): add Parakeet TDT-CTC 0.6B JA voice model (#8207) * Add SenseVoice speech-to-text model (Korean/Japanese support) SenseVoice (zh/en/ja/ko/yue) is the only bundled local STT model with Korean and Japanese support. The existing local models cover only English and Chinese (Parakeet, Zipformer, Paraformer); Whisper Tiny is multilingual but trades accuracy for breadth. - Add 'senseVoice' to SpeechModelType - Register the sherpa-onnx SenseVoice archive in the model catalog (pinned SHA-256, single-file model.int8.onnx + tokens.txt layout) - Handle the senseVoice type in the STT worker via createOfflineRecognizer with the senseVoice model config (auto language detection + ITN) - Add model-catalog regression tests for the new entry Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(speech): add Parakeet TDT-CTC 0.6B JA to the speech model catalog * test(speech): cover stt-worker-model-config file resolution incl. single-file models * feat(speech): decode Parakeet TDT-CTC JA via sherpa-onnx nemoCtc offline recognizer * fix(speech): use int8-only SenseVoice archive * fix(speech): refresh SenseVoice catalog metadata --------- Co-authored-by: xsacdw <xsacdw@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * fix(terminal): stop switch bold flash and Windows lag (#10692) * fix(terminal): stop bold flash on worktree switch Worktree hide disposes WebGL and falls back to xterm's DOM renderer. On reveal, resume ran after paint and flushed backlog against DOM first, so the first frame used heavier CSS-AA glyphs before WebGL settled. Resume in useLayoutEffect and reattach WebGL before backlog flush so the first painted frame stays on the GPU path. No cold-park policy change. Co-authored-by: Orca <help@stably.ai> * fix(terminal): fit WebGL grid before backlog flush on resume Adversarial review: resume-before-flush alone wrote TUI backlog onto the transient DOM↔WebGL one-column-off metrics window. Order is now resume → fitAllRevealedPanes → flush on heavy reveal and window wake. Co-authored-by: Orca <help@stably.ai> * fix(terminal): latch viewport intent before WebGL wake resume Adversarial review: wake path synced intents after resume/fit, which can re-latch a pinned viewport as followOutput. Capture before reattach and drop the post-resume re-sync on heavy reveal (outer path already latched). Co-authored-by: Orca <help@stably.ai> * fix(terminal): complete visibility bookkeeping before PaneManager exists useLayoutEffect runs before the passive lifecycle creates PaneManager, so the mount-visible path never set hasCompletedVisibleResume. The first intra-worktree hide then wrongly suspended WebGL. Bookkeep completion even when managerRef is still null (extracted helper for max-lines). Co-authored-by: Orca <help@stably.ai> * fix(terminal): re-sync pin geometry after resume backlog flush Keep the pre-resume intent latch (reattach must not re-latch pins as followOutput), then re-sync after flush with preservePinnedAtBottom so scrollback trim updates absolute pin lines before enforce. Co-authored-by: Orca <help@stably.ai> * fix(terminal): drop same-tick post-flush intent re-sync flushTerminalOutput only queues terminal.write and returns before parse, so a same-tick re-sync read pre-parse resume/fit geometry and could overwrite pre-resume pins. Keep pre-resume latch + enforce only. Co-authored-by: Orca <help@stably.ai> * fix(test): expect default worktree card properties to include cli #10712 added 'cli' to DEFAULT_WORKTREE_CARD_PROPERTIES, but the fresh default-profile assertion still omitted it and fails verify. Co-authored-by: Orca <help@stably.ai> * perf(terminal): retain Windows WebGL across worktree hides * perf(terminal): bound retained WebGL contexts * fix(terminal): harden retained WebGL lifecycle * fix(terminal): preserve healthy WebGL on wake * fix(terminal): preserve reveal recovery ordering --------- Co-authored-by: Orca <help@stably.ai> * fix(mobile): clear native-chat composer optimistically at send time (#10226) * fix(mobile): clear native-chat composer optimistically at send time Over relay the send RPC round trip is visible and a lost ack (or a relay/direct cutover) could strand the sent prompt in the composer forever: the unconfirmed-send deadline dropped its tracking entry, so a late transcript echo could never clear the draft. Clear the draft at send time and restore it only on a definite rejection. holdUnconfirmedSend now only manages the delivery-unconfirmed notice; it no longer touches drafts. * fix(mobile): isolate question answers from composer drafts * fix(cli): bound orchestration ask timeouts (#10689) * fix(cli): bound orchestration ask timeouts * fix(cli): harden remote timeout boundaries * fix(crash-reporting): stop fit-retry bursts from erasing the pre-crash trail (#10729) * fix(crash-reporting): stop fit-retry bursts from erasing the pre-crash trail Windows renderer OOM F0BKR84AHEH (0xE0000008) arrived with a 30-entry breadcrumb ring in which two `terminal_safe_fit_retry_exhausted` bursts consumed 26-90% of the slots. Every hidden pane is `display:none` -> 0x0 -> unmeasurable, so one post-reload reattach wave exhausts the retry budget once per mounted pane inside ~60ms. The bursts were also uninterpretable: `pane.id` restarts at 1 per PaneManager and there is one manager per tab, so 34 identical `paneId: 1` crumbs cannot distinguish one pane looping from 34 panes firing once. Coalesce the crumb by name and carry the live-pane census on the payload instead, so the count survives without costing 34 ring slots. Same treatment for WebGL diagnostics, which were worse off: context-loss and atlas-reset crumbs only reached a DevTools-only ring (`window.n()`), so a renderer that dies takes them with it. That bundle had three GPU-process deaths in the 65s before the renderer OOM and zero WebGL evidence - absence of instrumentation, not absence of the event. Mirror them into the crash report, coalesced per kind so a routine atlas reset cannot mask a context loss. Evidence-only: no behavior, rendering, or lifecycle path changes. Co-authored-by: Orca <help@stably.ai> * perf(pane-manager): count panes without materializing public views The census runs on the crash path; getPanes() allocates a full ManagedPane projection per pane just to read .length. Co-authored-by: Orca <help@stably.ai> * test(crash-reporting): pin the fit-retry burst against the 30-entry ring Reproduces the F0BKR84AHEH ring loss directly: 10 pre-crash crumbs plus a 34-crumb per-pane burst. Uncoalesced, the burst takes all 30 slots and zero pre-crash crumbs survive; coalesced, it takes one slot, all 10 survive, and the pane count rides on the payload instead of on the crumb multiplicity. Co-authored-by: Orca <help@stably.ai> * fix(crash-reporting): name the WebGL census the same as the fit-retry census The context-loss crumb spread getLivePaneCensus() raw, so one ring described one measurement two ways: managers/panes here, livePanes/livePaneManagers on the fit crumb. Spreading also meant renaming the census return keys would silently reshape the crumb. Name the fields at the call site and pin them. Co-authored-by: Orca <help@stably.ai> * fix(crash-reporting): keep a hot coalesce key from being the first LRU eviction The suppression path returned before the delete-then-set that re-anchors recency, so a key hit continuously never moved from its original insertion slot and became the first eviction candidate — the inverse of the LRU's stated intent. `renderer_error` keys carry message+stack identity, so one noisy render loop mints unbounded distinct keys. Within a single 30s window that churn evicted the `terminal_safe_fit_retry_exhausted` key mid-burst, un-suppressing it and re-arming the exact ring flush the coalescing exists to prevent. Re-anchor position only; `recordedAt` is left alone so the suppression window still expires on schedule rather than renewing on every hit. Found while adversarially probing the LRU claim in #10729's own description, which asserted these keys "cannot evict live keys". * fix(crash-reporting): report the newest census of a coalesced burst The suppression path wrote nothing to the ring, so a coalesced burst froze its FIRST event. Panes mount progressively, so pane 1 exhausting alone legitimately measures livePanes: 1 -- and the 33 later crumbs, each carrying a truer census, were dropped. A 34-pane wave was recorded as `livePanes: 1` with no count: the exact "one pane looping" misread that coalescing by name was introduced to prevent. The existing burst test missed this because it fed a constant census on every crumb, making frozen-first and newest-wins indistinguishable. Stash the newest payload and fold it into the ring entry the key already owns: still one slot, now reading livePanes: 34 + suppressedSinceLast: 33. Resolution is deferred to snapshot time -- sanitizing per suppressed hit of a 1459/min crash loop measured 2194 ns/op vs 185 ns/op deferred. Two follow-on defects fixed alongside: an expiring key dropped its pending payload (it loses its only handle on the ring entry), and resolving the re-emitting key's own old slot double-counted a burst. --------- Co-authored-by: Orca <help@stably.ai> * fix(speech): download verified model artifacts directly (#10735) * perf(renderer): give owner-routed settings a stable identity (#10743) Co-authored-by: Orca <help@stably.ai> * perf(agent-status): validate hook payloads without the JSON round trip (#10752) * fix(gpu-fallback): make the crash window rolling, not launch-anchored (#10707) * fix(gpu-fallback): make the crash window rolling, not launch-anchored Software-rendering fallback only ever considered GPU child crashes in the first 30s after launch: `if (msSinceLaunch > this.windowMs) return`. Session 12e6ee64 crashed the GPU child 4 times (242s / 920s / 926s / 946s since launch). The last three span 26.0s — inside windowMs, exactly threshold — but every one was rejected because the burst began 920s in. The renderer died of process OOM (0xE0000008) 39s later. GPU work is demand-driven, so the first heavy compositing often happens minutes into a session. What distinguishes a broken driver from normal Chromium churn is that the crashes *cluster*, not when the cluster starts. Keep a sorted array of recent crash times pruned to windowMs behind the newest, and engage when the count reaches threshold. Measured against real field telemetry (341 distinct win32 launches with >=1 GPU crash, from process_gone_suppressed breadcrumb trails): the rolling window engages on 2/341 launches (0.59%), one of which is 12e6ee64. Max GPU crashes in any single launch is 4. The closest non-firing sequence ([0, 29531, 55136, 74178] — consecutive gaps that each fit the window but never put 3 inside it) is pinned as a regression test. Also destroy the Windows tray before app.exit(0) on this path, matching the app:relaunch IPC handler — app.exit skips before-quit, and this can now fire deep into a session rather than only in the first 30s. Mutation-tested: 6 mutants (launch-anchored gate, dropped pruning, dropped monotonic clamp, cutoff </<=, threshold >=/>, dropped engaged latch), all killed by the suite. Co-authored-by: Orca <help@stably.ai> * fix(gpu-fallback): ask before restarting --------- Co-authored-by: Orca <help@stably.ai> * feat(sidebar): add a filter to hide detached-HEAD workspaces (#10786) Adds "Hide detached HEAD" alongside the existing sidebar filters, wired through the same pipeline as Hide CLI-created: sidebar list, Cmd+J empty-query list, workspace board, active-filter badges, Clear/Reset Filters, and persisted UI state. The predicate reuses getWorktreeGitIdentityDisplay so the filter targets exactly what the card renders a Detached HEAD badge for. Requiring a real head (not just an empty branch) keeps folder workspaces and SSH-synthesized rows — which carry both empty — out of the filter. Activating a hidden detached workspace clears the filter, matching the existing reveal escape hatch for automation- and CLI-created workspaces. Splits the filter-state describes out of visible-worktrees.test.ts into sidebar-filter-state.test.ts to stay under the max-lines budget. Co-authored-by: Orca <help@stably.ai> * feat(daemon): add daemon_lifecycle replaced/retired telemetry event (#10058) * feat(daemon): add daemon_lifecycle replaced/retired telemetry event Implements STA-2376. Adds track('daemon_lifecycle', {transition, reason, live_session_count_bucket, version_skew?}) covering 'replaced' (unhealthy_resolver / stale_bundle / different_app_path / failed_health_check at daemon-init launcher sites) and 'retired' (died_respawn at the adapter respawn closures). Enum-only + .strict() + bucketed counts keep paths, versions, and raw counts off the wire; preserve-path transitions emit nothing. Cross-platform and SSH-safe; no-op in non-official builds. Test plan: affected vitest (158) green; typecheck/lint clean except pre-existing unrelated failures. * fix(daemon): prevent false lifecycle telemetry * test(daemon): restore once-ness on respawn reason assertions Keep STA-2376 reason checks without dropping concurrent-respawn coalescing coverage that prevents double died_respawn telemetry. * fix(daemon): emit replaced telemetry on runtime unhealthy_resolver respawn CodeRabbit: adapter-driven macOS resolver replacements forked a new daemon without a lifecycle event. Emit trackDaemonReplaced (not retired) so field diagnosis of #7936 covers the runtime path without mislabeling it as death. * fix(daemon): stop double-counting resolver replaces; drop redundant version_skew Three telemetry-correctness fixes to the STA-2376 daemon_lifecycle event. 1. The runtime macOS resolver respawn double-counted. doRespawn() disconnects but never kills the daemon, so the ensureRunning() that follows re-enters createOutOfProcessLauncher, which re-detects healthy + resolver-unhealthy + 0 sessions and emits the replace itself. The closure emitted a second one. It also emitted before the outcome was known, so a resolver that recovered mid-flight (or a session appearing) left a 'replaced' on the wire for a daemon the launcher went on to preserve. The launcher's emit is gated on a confirmed kill, so it is the correct sole emitter; this reverts the emit added in |
||
|
|
10ca89ac8b |
feat(updater): switch to validated local mac builds (#10889)
* feat(updater): switch to validated local mac builds * test(updater): cover local build recovery actions * fix(types): keep local build contract in project sources |
||
|
|
97cb32c1cc |
fix(terminal): release an abandoned synchronized-output frame on reveal (STA-2694) (#10907)
* fix(terminal): release an abandoned synchronized-output frame on reveal Alt-screen agent TUIs (OpenCode/OpenTUI, Codex, grok) bracket every repaint in `?2026h … ?2026l`. Hiding a pane mid-bracket — which a worktree switch or cold-park lands on routinely, since these brackets are written many times a second — leaves xterm's `decPrivateModes.synchronizedOutput` latched. RenderService.refreshRows checks that latch *before* rendering, so while it holds, every repaint Orca owns is a no-op: the forced render-pause repaint, the plain `refresh()` fallback, and the shared glyph-atlas rebuild all render zero rows while the xterm buffer is perfectly correct. Release the latch at the two reveal repaint entry points so those repaints actually paint. Also adds an OpenCode-shaped alt-screen e2e fixture and spec. The existing inline-TUI convergence spec covers the normal-buffer shape (live block glued to the bottom, history scrolling into scrollback); this covers the full-screen alternate-buffer shape, where nothing scrolls and so no row ever self-heals through the scroll path. Scope note: xterm arms a 1s watchdog that clears this latch on its own, so this closes a bounded window rather than the whole STA-2694 report. The e2e spec passes with and without the production change for that reason; the unit tests are what pin the behavior. Refs STA-2694. * fix(terminal): clear the render model on the plain-refocus repaint path `schedulePaneRevealPresent` — the atlas-preserving path a plain window refocus takes — only called `terminal.refresh()`. xterm's renderers are diff-based: `_updateModel` early-continues on any cell whose code/fg/bg/ext still match the cached model, so a refresh repaints nothing for a pane whose buffer never changed. When an occluded window loses its canvas contents while that model stays populated, the refresh skips exactly the cells that went stale and the pane keeps compositing pre-hide pixels — until a window resize reallocates the model, which is the repair users find by hand. Clear the model first (`RenderService.clear()` → renderer `clear()` → `_clearModel(true)`) so the refresh becomes a guaranteed full repaint. That drops cached cells and glyph vertices but NOT the texture atlas, which is shared by every same-config terminal and whose mid-stream wipe re-arms xterm's page-merge garble race (xterm.js #4480) — the reason this path is atlas-preserving in the first place. Also covers the DOM-renderer fallback in `resetWebglTextureAtlas`: `clearTextureAtlas()` is what invalidated the model on the WebGL path, so a pane without an addon had nothing invalidate it and hit the same skip. Scope note: the e2e spec guards buffer/geometry convergence across the hide/reveal boundaries and adds idle-agent and headful desktop-hide cases, but it cannot observe a stale canvas — both oracles built for that (canvas-vs-buffer ink sampling, screenshot-vs-forced-repaint) were proven blind by injecting the defect, and the spec header documents why. The unit tests pin the ordering and the atlas-preservation invariant. Refs STA-2694. Co-authored-by: Orca <help@stably.ai> * docs(terminal): hand off the STA-2694 reveal-artifact investigation Records both fixed defects with their xterm mechanisms, the reveal/wake call graph, why every e2e oracle for a stale canvas was proven blind, how to arm the in-app render-desync sentinel on real hardware, and the one unverified lead (dimension staleness) that would explain why a window resize specifically is the repair users find. Refs STA-2694. Co-authored-by: Orca <help@stably.ai> * Revert "fix(terminal): clear the render model on the plain-refocus repaint path" This reverts commit |
||
|
|
cef52c68be |
fix(runtime): stop rejecting ui.set when Linear or Jira resume fields are present (#10715)
Reviewed with an independent reproduction. Extended the fix to the UiUpdate parity gaps one level up, and added a typecheck-level assertion so future field drift fails the build instead of silently rejecting paired-client payloads. |
||
|
|
abcdc04f6b |
fix(ci): mirror missing lint steps in PR workflow (#10601) (#10623)
Reviewed with an independent reproduction. Added the allowlist entry that unblocked verify:localization-coverage on main, the 4th drifted step, and a parity gate that fails when pnpm lint's chain contains a script absent from pr.yml. |
||
|
|
3baffb49ff |
fix(runtime): refuse SSH hosts in project setup instead of acting locally (#10799)
* fix(runtime): refuse SSH hosts in project setup instead of acting locally projectHostSetup.clone and .setupExistingFolder threaded executionHostId all the way down but never used it for routing: cloneRepo runs a local mkdir plus a local gitSpawn, and addRepo probes the path with existsSync/statSync. An `ssh:` host therefore cloned and validated on the *local* machine and then registered the result as living on the SSH host. It only failed loudly here because the remote path did not exist locally. With a plausible destination the clone succeeds and writes a setup record pointing at the wrong machine. Nothing legitimate sends `ssh:` to these RPCs: the renderer maps every ssh host (including ephemeral-VM `ssh:runtime-ssh-*`) to the desktop IPC path, which dispatches to addRemoteRepoFromPath/cloneRemoteRepo, and the IPC handler symmetrically rejects `runtime:`. Only the CLI can reach here with `ssh:`. Fail closed until the RPC learns to route through the SSH providers. * test(runtime): make the SSH guard test observe the corruption it names The test asserted `gitSpawn` was never called and no repo was registered, but neither assertion could fail. `/home/brennan` is unwritable on macOS, so the pre-guard clone died at `mkdir` before reaching `gitSpawn`, and `/home/brennan/orca` failed `isGitRepo` before reaching `addRepo` — the exact side effects under test were unreachable either way. `rejects.toThrow` also aborted the test before those lines ran. Use a real temp destination and a real temp git repo, await both calls via `.catch`, and assert the side effects before the wording. With the guard disabled the test now fails on `gitSpawn` being called once with a real `git clone`, and on a repo registered stamped `executionHostId: 'ssh:openclaw'` — the silent local-clone-recorded-as-remote defect itself. `gitSpawn` is stubbed so a regression records the call instead of hitting the network. Also document the SSH restriction on `project setup-existing-folder`, which the guard now rejects. `setup-clone` already carried that note; its sibling did not. |
||
|
|
1fcbf8e5fe |
fix(dashboard): stop the agent icon shrinking on long card titles (#11001)
A bare <svg> flex item shrinks with its row, so kanban cards and the terminal dialog rendered a squashed ~9px agent icon whenever the worktree name overflowed. Wrap both in the shrink-0 span every other surface already uses. |
||
|
|
3716a7bb49 | fix(markdown): render task continuations as text (#11008) | ||
|
|
0ab5f499cb | fix(cmd-j): restore focus when issue match routing declines (#11010) | ||
|
|
a065db154c |
fix(sidebar): spin the worktree dot while Claude Code is thinking (#9040) (#10684)
Co-authored-by: Orca <help@stably.ai> |