mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 16:02:15 +00:00
relay-split/setup-node-cache
10019
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1ef83986a1 |
fix(codex): stop a surviving descendant wedging the Codex home process lock forever (#12410)
* fix(codex): stop a surviving descendant wedging the Codex home process lock The per-Codex-home process lock was released only by the child's 'close' event. 'close' fires when the child AND every process that inherited its stdio have exited, so a grandchild that outlives codex (an MCP server, a helper, an `sh -c` wrapper) keeps the pipe open and 'close' never arrives. killProcessTree only SIGKILLs the direct child on POSIX, so Stop and the 60s generation timeout both leave the lock held. Every later AI commit message, PR field, branch name, and quota probe for that home then queued forever with no error and no recovery until the app restarted. Release the hold on 'exit' as well as 'close': once the codex process itself is gone it can no longer rotate that home's auth.json, which is the race the lock exists to prevent, so waiting on descendants bought nothing. As a backstop for a child that never reports exit at all (a kill that does not land), bound the hold: while another run is queued behind it, a hold that outlives CODEX_HOME_PROCESS_LOCK_MAX_HOLD_MS releases the queue. The cap is armed only when someone is waiting and only counts time the entry actually holds the lock, so uncontended runs arm no timer and queueing behind a slow predecessor never shortens a run's own budget. * fix(codex): preserve home lock exclusivity --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
bdafe840f7 |
fix(browser): keep the chrome inset when a page viewport shell is rebuilt (#12401)
The chrome inset that pushes a browser guest below the toolbar was applied only from BrowserPane's layout effect, whose deps cover the first mount (page id, active, paintable, slot readiness). Every path that rebuilds the viewport shell without re-running that effect therefore produced a shell with a zero-height inset: guest recovery replacing a lost guest (replacePersistentWebview -> removeBrowserPageViewport -> render-time ensureBrowserPageViewport), a session-profile switch that recreates the guest for a new partition, and an overlay slot-root remount (STA-3228). The recovered page then painted underneath the browser chrome until an unrelated resize or tab switch forced a recompute. Make the geometry a property of the viewport instead of the mount: remember the last synced inset height per page and apply it whenever a shell is built. This also fixes the ordering case where the chrome is measured before the shell exists (the height used to be dropped on the floor). Test: browser-page-viewport.test.ts covers the rebuild-after-replacement and measured-before-shell cases; both fail without the change. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
0f052a5010 |
fix(editor): stop Cmd/Ctrl+Shift+T reopening a phantom editor tab (#12399)
`openFile` honored a recently-closed snapshot's `reopenId` whenever no open file currently held that id, without checking whether the same (path, mode, owner) was already open under a different id. Editor ids are not symmetrical across worktrees: the first worktree to open an absolute path gets the bare path as its id, and any other owner of the same path gets a namespaced `editor:<wt>:<runtime>:<path>` id. So a snapshot captured under one shape could be restored while the live tab carried the other. When that happened `openFile` took the `existing` branch and mapped over `openFiles` looking for the stale id, matching nothing — no OpenFile was created or updated — yet it still set `activeFileId`, the per-worktree active id, the tab bar order and a unified tab entity to that stale id. The user got a tab rendering nothing and a dangling active editor. Resolve the id from the already-open file whenever one exists, so capture and restore agree on canonicalization. This is a no-op for the non-reopen path, where `resolveEditorFileIdForOwner` already returns `existing.id` via the identical predicate. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
5a2b329d8d |
chore(mobile): bump to 0.0.37 (versionCode 10) (#12365)
Completes the 0.0.37 release attempted on 2026-08-03 (run 30791649691 failed on the version assertion). Ships the post-0.0.36 transport fixes: relay session recovery when the LAN endpoint is unreachable (#12344, #11368, #11465, #11690) and honest worktree-catalog failure states (#12235) — the released-app defect class verified live tonight. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>mobile-android-v0.0.37 |
||
|
|
665b85047b |
fix(relay): revive a dead relay broker without user interaction (#12432)
Broker deaths that end with closeNow() — an auth refresh failing past token expiry (laptop sleep) or a transient context read at open — left no retry timer, so Relay stayed offline until the user clicked Retry or auth state changed. Adds a dead-man's switch: - RelayAuthCoordinator.ensureLive(): reconciles only when there is no live broker, no scheduled retry, and no open in flight - DesktopRelayService arms a 5-minute liveness interval and exposes ensureLive() for wake signals - powerMonitor 'resume' triggers ensureLive (sleep is the common cause) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
e8d3043107 |
fix(mobile): stop unrenewed-grace rotation churn and gate cadence gaps (#12426)
- skip proactive rotation when the resume confirmation reports renewed=false (a re-resume provably returns the same unchanged deadline; rotating churned one session replacement per clamp floor, ~60/hour, until a fresh credential) - armCredentialReprobe under a held gate mints the tick's pass token so the effective reprobe cadence stays 60s..15min instead of doubling to ~30min - registerFailure honors scheduleRetry=false in gate branches: no reprobe timer is armed while backgrounded/stopped; foreground resume re-arms - extract RelayRetryDelays and supervisor test fakes into their own modules (max-lines) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
e8d4818b5a |
test(env): keep happy-dom MutationObserver callbacks alive across GC (#12438)
happy-dom holds each observer's internal callback in a WeakRef, so a GC pause under sharded CI load permanently kills a still-connected observer. That is what made the activity portal readiness test flake: once the observer died, no amount of re-applying ready DOM could release the latch. Co-authored-by: Orca <help@stably.ai> |
||
|
|
ed4d6979b1 |
fix(app): await durable checkpoints before restart actions (#12433)
* fix(app): await durable checkpoints before restart actions * fix(app): clear restart latch after refused reload * fix(persistence): invalidate hash after stale rename |
||
|
|
dbbeca0916 |
Let a timed-out remote terminal pane reconnect when the host session is still alive (#12213)
* fix(terminal): let a timed-out remote terminal pane reconnect again A remote-runtime PTY pane that missed the 60s auto-recovery window latched to "disconnected" permanently, even while the host kept the session alive and streaming. The cutoff destroyed every path back at once: it dropped the pending retry and evicted the pane from the shared retry registry (so window `online` and system-resume became no-ops), and the transport tore down the accepted host-snapshot listener. The listener was also inert for the common case, since a host that keeps publishing the same live handle never rotates it. The cutoff now only stops the retry timer; the pane keeps its pending retry and stays revivable. The accepted-snapshot listener survives the latch, and a post-cutoff snapshot is accepted as reattach evidence whether the host rotated the handle or republished the same one. The require-replacement published-wait guard is scoped to its own recovery epoch so Reconnect and online/resume are no longer swallowed. No new polling: a latched pane still issues zero self-initiated RPCs, and the same-handle reattach consumes a snapshot the client already receives. Closes #12097 * fix(terminal): drop the settled attach retry when the recovery cutoff lands The recovery cutoff now keeps a pending retry so online/resume can revive a latched pane, but the host-session attach wait schedules a single-shot closure that the cutoff itself resolves. Retaining it left the pane registered as revivable work: 'online' would bump the epoch, arm a fresh 60s deadline and flip the phase to 'recovering' while invoking a no-op, hiding the working Reconnect button and the same-handle snapshot reattach for a full minute. The attach wait now discards its own scheduled retry as it settles, so a pane latched on that path stays 'disconnected' and Reconnect keeps working. Retries scheduled by the resubscribe paths re-enter real work and are unaffected. Co-authored-by: Orca <help@stably.ai> * fix(test): type the reattach host snapshot factory so status narrows to 'ready' Co-authored-by: Orca <help@stably.ai> * fix(terminal): keep an exhausted remote pane reattachable through Reconnect and resume Reconnect and the online/system-resume trigger both opened a fresh recovery epoch, which switched off the accepted-snapshot reattach path (gated on the 'disconnected' phase) for 60s while the require-replacement inventory wait dead-ended without scheduling anything. Gate the same-handle reattach on a spent auto-recovery window instead of the live phase, consumed once per window, and park an unarmed retry at the require-replacement dead end so online/resume/Reconnect have work to revive. Co-authored-by: Orca <help@stably.ai> * test(terminal): pin latched-pane retention to one listener and one registry entry The recovery cutoff now keeps the retry-registry entry and the accepted-snapshot listener alive, so cover the two module-global collections that could accumulate: destroy/detach cycles, concurrent latched panes, revive storms and snapshot churn all return to baseline. Co-authored-by: Orca <help@stably.ai> * refactor(terminal): use the returned reattach epoch and pin the no-RPC claim Addresses both CodeRabbit nitpicks: subscribeToHandle now takes the epoch begin() returned rather than re-reading currentEpoch, and the latched attach-wait test asserts the runtime call count is unchanged instead of only the advanced count. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
dc0cb1806a |
feat(repo-icon): expand the project emoji picker (#7989) (#12058)
Replace the hardcoded 12-emoji grid in the repo icon settings with the full searchable, category-navigable emoji picker, reusing the existing emoji-picker-react dependency in a lazily-loaded chunk. Every pick is re-validated through sanitizeRepoIcon, so an over-cap ZWJ/skin-tone sequence surfaces a toast instead of silently no-op'ing. Skin tones stay selectable. Maintainer follow-up: use lazyWithRetry so a failed chunk cannot permanently blank the Settings page, disable autoFocusSearch on this inline picker, scope font-family to descendants (the library sets sans-serif on every child), cover the uncovered dark-mode picker variables, match the app scrollbar, and assert the result in the DOM per tests/e2e/AGENTS.md. New strings are translated in es/ja/ko/zh. Co-authored-by: chucoding <chucoding@users.noreply.github.com> |
||
|
|
e59a319ffe |
fix(sidebar): keep each project's entry-point workspace visible under "Hide sleeping" (#12257)
"Hide sleeping" swept each project's main workspace out of the sidebar as soon as it had no live PTY, browser tab or agent — even with "Hide default branch" off. For a project whose only row is that workspace (a folder workspace, a fresh clone, a detached-HEAD main), the entire project vanished with no in-place way back. Adds a shared `isSleepingSweepExemptWorkspace` predicate keyed on `isMainWorktree` rather than the branch name, so folder workspaces (no branch), detached-HEAD mains, and SSH rows whose head/branch are blanked while a provider is disconnected all stay put. Wired into `computeVisibleWorktreeIds` (sidebar, Cmd+1-9, workspace board), the jump palette's duplicate inline pass, and mobile's `filterWorktrees`. Ships default-on with an escape hatch: a persisted `alwaysShowDefaultBranchWorkspace` setting surfaced as "Except default branch" under "Hide sleeping". Explicit "Hide default branch" still wins, since it filters before the sleeping sweep. Mobile reads the setting but never writes it back, so a desktop opt-out can't be clobbered by a filter tap before the ui.get roundtrip lands. Combines the two PRs open against #8873. #8966's exempt set is a strict subset of this one, so its production diff was subsumed rather than ported; its jump-palette render harness and e2e spec were carried over, and are the only such coverage here. Fixes #8873 Closes #8966 Co-authored-by: Rod Boev <rod.boev@gmail.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
0927b9c156 |
fix(gitlab): load pipeline job traces in the Checks side panel (#7732) (#12266)
* test(repro): demonstrate #7732 GitLab pipeline job details never load in Checks panel Co-authored-by: Orca <help@stably.ai> * fix(gitlab): load pipeline job traces in the Checks side panel (#7732) Expanding a GitLab pipeline job in the Checks panel always showed "No inline details are available for this check.": the mapper dropped the numeric job id, `PRCheckDetail` had nowhere to carry it, and every consumer called the GitHub check-runs API, which returns null for a GitLab job. - carry `gitlabJobId` on `PRCheckDetail` and add the `gitlab-job:` branch to all three identity ladders (panel rows, editor tabs, fix-prompt keys) so same-stage jobs with no web_url stop colliding - add a runtime-routed trace client so SSH/remote workspaces work, not just local IPC, and thread the MR's `projectRef` for fork pipelines - bound the trace in main via the existing `sliceCheckLogTail` (now shared, not GitHub-only) so a multi-megabyte CI log never crosses the 1 MB transport frame cap; strip ANSI/section markers up to the CR only, which keeps each section's visible header and command echo - render the excerpt inline instead of "Log tail available in full details." - feed GitLab traces to "Fix with AI", which previously sent bare check names - skip the fetch for jobs that cannot have a trace (created/manual/skipped) so GitLab's 404 does not replace the benign empty state, and re-arm a failed load when the job's state changes since the panel has no retry Co-authored-by: Orca <help@stably.ai> * fix(gitlab): treat a missing job log as an empty log, not an error (#7732) Round-1 review follow-up. - a job canceled before it started (or whose log was erased/expired) is `completed`/`cancelled`, so the panel fetched its trace, GitLab answered 404, and `classifyGlabError`'s issue-edit copy ("Issue not found — it may have been deleted.") landed verbatim on the auto-expanded check row; main now maps that 404 to an empty trace so the row keeps its benign empty state - keep a missing project a real error (GitLab masks unauthorized projects as 404) and add `classifyJobLogError` so 403/unknown failures stop borrowing issue-edit wording on a job-log read - broaden the empty-log copy in all five catalogs: it now covers erased and expired logs, not only jobs that never ran - e2e: derive the repro screenshot dir from `process.cwd()` (or an env override) instead of a hardcoded POSIX path to a throwaway worktree - bound the raw trace before the ANSI/section passes so a multi-megabyte log is not scanned in full on the main-process event loop - drop the redundant `if (repo)` in `handleFixChecksWithAI` and the now-dead "Log tail available in full details." catalog entry Co-authored-by: Orca <help@stably.ai> * fix(gitlab): address review — project ref on reload, retry re-arm, IPC timeout - Carry the MR's GitLab project ref on the check-details tab so reloading a fork/cross-project job tab fetches the trace from the pipeline's own project. - Re-arm the sidebar retry when a details load resolves to null, not only when it throws; a detail-less row otherwise never retried after the job moved on. - Bound the local `gl.jobTrace` IPC call with the same 30s timeout the runtime RPC path uses — glab runs without a subprocess timeout in main. - Document that the trace 404 -> empty-log mapping is deliberately broad. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
ba83a71e30 |
fix(terminal): apply the running-process close confirmation to every tab close path (#10142) (#12272)
* test(repro): demonstrate #10142 tab X close bypasses running-process confirmation
Unit repro: closeTerminalTab (the X-button/middle-click entry) never consults inspectRuntimeTerminalProcess and drops a tab with a live child.
E2E repro: Cmd+W shows 'Stop running command?' for a tab running sleep 300; cancelling then clicking the tab X closes it silently.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): confirm running-process close on every tab close path (#10142)
The tab-strip X button, middle-click and the tab context menu closed a
terminal with a live child process without asking, while Cmd+W raised
"Stop running command?" for the same tab. The probe lived only in
TerminalPane's pane-level close handler; every mouse entry point reaches
closeTerminalTab(), which guarded pinned tabs and nothing else.
Move the decision into closeTerminalTab, above the web-runtime branch so
paired/remote host-backed tabs are covered too, and give the last-pane
keyboard close back to it instead of probing twice:
- running-terminal-close-guard.ts probes every live PTY of the tab and
fails open on a rejected probe or a stale remote handle, matching what
Cmd+W already did. No live PTY ids => fully synchronous close, so idle,
parked and hibernated tabs keep today's behavior.
- shouldConfirmRunningTerminalClose keeps lifecycle echoes, bulk closes,
CLI/RPC closes and the post-confirmation re-entry off the modal path.
- A standalone confirm store drives RunningTerminalCloseDialog, which
reuses the existing CloseTerminalDialog (no new user-visible strings).
The request carries the tab label because a tab-strip close can target
a tab the user is not looking at, and dedupes by tab id.
- TerminalPane.handleRequestClosePane now delegates the last pane to
closeTerminalTab. Its transport ptyId is nullable by design, so the old
path silently skipped the prompt mid-reattach; the pane keeps its own
probe only for closing one pane of a split.
- Agent panes win the dialog copy when a split has both an agent and a
plain command busy, instead of depending on PTY spawn order.
- Tab-group closeItem ran leaveWorktreeIfEmpty synchronously after a close
that can now defer; it moves to onClosed and still honors skipEmptyCheck.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): close the running-process confirmation gaps on every path (#10142)
Follow-up hardening on the tab-close confirmation, from review of the first
pass:
- A pinned tab with `confirmClosePinnedTab` off never got the running-process
prompt on any path, including Cmd+W, which is a regression against the old
pane-level behavior: the pinned branch short-circuited on pinned-ness alone
and re-entered with `force`, which the running guard excludes. The pin prompt
now supersedes only when it will actually appear; with the setting off the
close falls through to the running guard.
- The probe chain had no `.catch`, so a throw in the decision (a copy-kind
lookup on a tab id makePaneKey rejects, a store subscriber) left the tab
silently unclosed with no user feedback. It now fails open, as the pane path
it replaced did.
- A wedged remote inspect RPC could leave the X button looking dead for its
full 15s timeout. The probe is now bounded; every close path shares the
bound, so keyboard and mouse still behave identically.
- The agent-vs-command copy had two resolvers on exactly the keyboard/mouse
seam this issue is about. terminal-close-copy-kind.ts is now the single
policy; TerminalPane and the tab-strip guard both call it.
- The running queue is async while the pinned queue is synchronous, so both
could be pending at once and stack two modal overlays. The running dialog now
waits for a visible pinned confirmation.
- Deduping a repeat close request dropped the second caller's callbacks; it now
folds them in, so both closes resolve from one prompt. Ticking "don't ask
again" also drains queued prompts instead of showing one the user just opted
out of, and a queued prompt no longer inherits the previous tab's tick.
closeTerminalTab drops its private pinned predicate for the shared
isUnifiedTabPinned, whose only consumer the previous commit had removed.
* test(e2e): wait for `sleep` to own the terminal before closing it (#10142)
The running-process close specs polled `hasChildProcesses` to decide the tab
was busy, but macOS starts the shell under `login`, so an initialising terminal
already reports a child before `sleep 300` runs. Both specs could therefore
press close against a shell that never started the command: the probe correctly
saw an idle terminal and closed without asking, and the adjudicated repro failed
against a correct fix.
Wait for `foregroundProcess === 'sleep'` instead. Assertions are unchanged, and
the repro still fails at the pre-fix baseline (
|
||
|
|
086a5d6495 | fix(browser): rerun guest retention after veto release (#12430) | ||
|
|
e43bd6c4ad | fix(worktrees): preserve folder PTY owner scope (#12429) | ||
|
|
5bd2f59d29 |
fix(runtime): open files from sibling workspaces (#11369)
* feat(runtime): match files to workspace owners * fix(runtime): resolve terminal paths through sibling workspaces * fix(editor): route restored sibling workspace files * fix remote sibling file ownership routing * fix(editor): migrate restored sibling file owners * fix(editor): revalidate restored owner activation * docs(review): record PR 11369 correction evidence * fix(editor): reject collision before activation prep * docs(review): record PR 11369 final correction * fix(editor): retain projected reconciliation narrowing * chore(review): keep verification artifacts out of PR * fix(editor): harden restored owner migration * fix(runtime): resolve workspace root terminal paths --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
9ba293cb74 |
fix(mobile): keep relay runtime recovery alive without direct connectivity (#12374)
* fix(mobile): keep relay runtime recovery alive without direct connectivity A phone paired over the relay whose direct LAN endpoint is unreachable (e.g. a Tailscale IP with Tailscale off) could lose the runtime channel permanently: the reconnect controller's recovery gates parked with no timer and no logs, the supervisor snapshotted relay credentials once at start (dying silently if the read failed and dialing stale tokens after rotation), and the only path that cleared a rejected-credential gate required a working direct connection. Field symptom: home card shows "Connected - Orca Relay" (or "Can't connect - check Tailscale") while the host page sits at zero worktrees forever. - gates (fresh-credential, external-signal) now arm a slow 60s reprobe instead of parking; each gated attempt re-reads the durable credential bundle and adopts it when its version is fresher than the rejected one - supervisor start no longer dies for the process lifetime when the initial Keychain read fails or the bundle is expired - every recovery decision now reaches logcat and the in-app connection log ([relay] lines); previously the whole relay dial path was silent - direct-return probing extracted to mobile-direct-return-probe.ts, credential selection to mobile-relay-credential-selection.ts Regression suite mirrors the field failure (rejected outer credential, unreadable bundle at start, expired bundle, E2EE rejection without a UI nudge) plus real-rpc-client failover integration tests; the four deterministic scenarios fail on the previous code. * fix(mobile): adopt durable relay credentials by outcome, not version Adversarial review caught two blockers in the version-comparison rule: renewals extend expiresAt without bumping current.version, and a re-pair restarts the version counter — both left the durable bundle unadopted and reproduced the original outage. Selection now adopts the disk bundle exactly when it yields a dialable (unexpired, non-rejected) credential while memory does not, which also keeps revoked versions unresurrectable. Also from review: the gate reprobe cadence now escalates 60s -> 15min ceiling with 0.75-1.25x jitter (no fleet phase-alignment, no permanent one-minute beacon); clearing a gate drops its timer, pending tick, and cadence so an orphaned reprobe cannot swallow the next fast backoff; the reprobe tick token is only minted while its gate still holds; and a merely missing/expired bundle uses a plain cooldown instead of the fresh-credential gate so it cannot force rotations on direct reconnects. New regression tests (all red on the previous code): renewal without a version bump, re-pair with a restarted counter, orphaned-timer backoff swallowing, escalating gated cadence, and background/foreground recovery after an E2EE rejection. * fix(mobile): reset gated relay cadence on app resume Review round 2: an escalated fresh-credential gate kept its cadence across background/foreground, so reopening the app could wait out a 15-minute tick (measured 11.25min to first attempt after a 2h background) — indistinguishable from the outage itself. A resume now resets the streak even when it cannot lift the credential gate, and a successful direct connection does the same in resetForDirectConnection. Also: the streak now advances once per fired tick instead of once per armed-delay computation (three arms per cycle escalated 60s -> ceiling in ~7 minutes instead of the documented eight steps); delay computation is a pure read. * fix(mobile): rotate relay sessions on resume expiry, not attach deadline Live phone verification of the failover fix exposed a second defect the old latch had been masking: the relay-hello's leaseExpiresAt is the cell's attach-reservation deadline (now + 10s for resumes, credential-store.ts:213 server-side), but the supervisor scheduled proactive rotation from it with a 30s margin clamped to 1s — so every relay runtime session force-replaced itself ~1s after connecting (measured every ~2.5s on device, 253 dials per 5 simulated minutes in the red test). Any RPC slower than the cycle could never complete, which is the "Worktree list unavailable" symptom. The session now captures resumeExpiresAt from the hello (updated by the resume confirmation) and rotation keys off it. Test fakes previously used a 120s lease, which is why no suite ever reproduced the loop; they now mirror the production 10s attach deadline, and a churn regression holds one session across 5 minutes with direct unreachable. * fix(mobile): clamp lease rotation delay on both ends Adversarial review of the resume-expiry rotation fix caught an int32 setTimeout overflow: production resumeTtlMs is 30 days, and 30d - 30s = 2,591,970,000ms exceeds INT32_MAX, so Node (and vitest's fake timers) clamp the timer to 1ms — 3001 relay dials and credential writes in 3 simulated seconds, ~2500x worse than the churn being fixed. The delay is now clamped to [60s, 6h]: the ceiling makes overflow unreachable regardless of server TTL (a harmless re-resume every 6h on long sessions), and the floor bounds any bad deadline to one forced rotation per minute instead of a sub-second loop — which also disarms the Math.max(1000, ...) landmine for return-unchanged-grace resumes whose stored expiry can be arbitrarily near. Also from review: getLeaseExpiresAt is renamed getAttachDeadlineAt (it had zero production callers left; the plausible name is how the churn bug happened), the expired-vs-missing bundle cases now log distinct strings, and both test fakes use production constants (10s attach deadline, 30-day resume TTL) — fictional fake values hid all three defects in this subsystem. The four forced-rotation lease tests are retimed to the 60s floor with direct pinned unreachable so return probes cannot race their windows. * style(mobile): merge duplicate imports in relay failover test * style(mobile): use T[] array syntax in credential selection --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
3d6d6dd67d |
fix(orchestration): scope agent lineage to its owning run (#11203)
Use durable Task-to-Run ownership and current pane, process-incarnation, and Run-generation authority for sidebar agent lineage. Add schema migrations, bounded lookup indexes, fail-closed renderer cleanup, and runtime/RPC regression coverage. Co-authored-by: Jaeyoung22 <89302528+Jaeyoung22@users.noreply.github.com> |
||
|
|
a6b14eb04c |
fix(terminal): reset stale mouse tracking on cold restore (#12101); stop OSC color-reply echo leak in POSIX agent panes (#12112) (#12202)
* fix(terminal): reset stale mouse tracking on cold restore (#12101); stop OSC color-reply echo leak in POSIX agent panes (#12112) #12101: a force-killed TUI never emits its DECRST reset, so its armed mouse mode is latched into the on-disk checkpoint and re-derived into the replacement process's emulator via the cold-restore history seed -- through both rehydrateSequences and SerializeAddon's own mode trailer. The revived bare shell then echoed SGR motion reports at the prompt. Seed a RESET_MOUSE_REPORTING segment after the snapshot (before the torn escape tail), only when there is real recovered content so the empty-array "nothing to recover" sentinel survives. #12112: agent panes arm a main-side PtyStartupIngress that answered opencode's startup OSC 10/11 queries synchronously inside node-pty's onData, while the POSIX tty still had ECHO on. The line discipline echoed Orca's own reply back out as visible text. Echo suppression existed but was gated on windows-conpty. Add PtyStartupReplyDelivery: POSIX defers the write off the query's turn and recognizes its own echo anywhere in a span (bounded, non-destructive); ConPTY keeps its synchronous write; windows-wsl is byte-identical to before. Fixes #12101 Co-authored-by: Orca <help@stably.ai> * fix(terminal): read the slave's ECHO bit before answering a color query The startup color reply was written into a PTY still in cooked mode, so the line discipline echoed it back as visible junk (#12112). Whether that will happen is readable state on the slave rather than something to infer from returning bytes, so the reply now waits until the ECHO bit is observably clear instead of guessing at echo shapes. Two echo sources exist and only one is readable. A `quiet` verdict proves the kernel will not echo, so it retires the caret projection; readline echoes a master write in software with the tty already raw, so that projection stays armed on every path. Scoping `quiet` narrowly is the whole correctness argument here: reading it as "no suppression needed" reintroduces the bug at a plain shell prompt. Polling is bounded by a wall-clock budget rather than an attempt count, because each probe is a subprocess and a multi-pane restore serializes them on fork. Withholding measures flat at ~210ms from 1 to 100 panes. Also resets a cold-restored pane's mouse reporting (#12101). The armed mode is re-derived from the dead process's own persisted bytes through two channels, so the daemon seeds a reset into recovered history and the renderer stops trusting a persisted "live agent" signal after a cold restore. The reset literals move to one shared profile module. Fixes #12101 Fixes #12112 Co-authored-by: Orca <help@stably.ai> * test(terminal): pin the cold-restore reset on the spawn-adopted reattach path A spawn can be answered with an adopted session, which reaches the reattach handler by a door that skips the restored-session path. Pin that the cold-restore signal survives it, so #12101's junk cannot come back through it. Co-authored-by: Orca <help@stably.ai> * test(terminal): note why the adopted-reattach snapshot leaves the cursor visible Co-authored-by: Orca <help@stably.ai> * fix(terminal): harden startup reply delivery --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
9a97e737f5 |
test(e2e): pick the board marquee start point from live geometry (#12409)
* test(e2e): start the board marquee off the board's padding cliff `selects the full lane across a single large marquee scroll jump` failed the changed-e2e-specs job with `Received: 0` — no card ever previewed. The CI trace shows the drag started at (284, 195) and the failure screenshot shows a native text selection with no marquee overlay, so the board never accepted the pointerdown. An element scan across that row shows why: the marquee may only start on empty board space, and the usable strip is only x 280-298 — the board's own left edge on one side, the first lane's cards on the other. `selectionBox.x + 4` aims 4px inside that strip, so a layout that rounds a few pixels differently lands outside the board entirely, where the pointerdown never reaches the handler and the browser text-selects instead. Aim at the middle of the measured strip, assert the start point really is empty board space before pressing, and assert the selection rect appears so a rejected gesture fails immediately instead of surfacing 15s later as "0 cards previewed". Also carried over from the first attempt at this deflake: the lane scroll now jumps until the virtualizer stops moving the bottom, because a measured card is much taller than the row estimate and a fixed pass budget commits the marquee short of the last cards; and the final assertion checks the badge's text so a short selection reports its count. * test(e2e): pick the marquee start point from live geometry, not a precomputed one Round 2 fired the new guard: `marquee start point (290, 195) must be empty board space` with `onSurface: false` AND `onIgnoredTarget: false` — so elementFromPoint returned something outside the board entirely, even though (290, 195) sits inside the measured 280..299 strip. Aiming at the middle of the strip is still aiming at a point computed before the probe runs, and the sheet, sidebar and lane fill keep resizing that strip afterwards. Stop precomputing the point. Read the geometry and scan for an empty point in the same DOM turn, walking a grid across the strip between the board's left edge and the first card, over the lane's top rows only — the marquee anchors its range in content space, so a start below the first card would drop it from the 102. Take the first point the board itself reports as empty, and require two consecutive probes to agree so a frame mid-relayout cannot win. Keep the precondition guard, now reporting the live geometry and the elements that blocked each rejected point. If no empty point exists at all, skip with that reason instead of failing on a layout the test cannot drive. * test(e2e): start marquee from an explicit empty lane * test(e2e): find visible board space for marquee start * test(e2e): quarantine unstable marquee hit test |
||
|
|
50594c55a9 |
Stop the Windows Orca CLI from crashing when the environment carries both PATH and Path (#12218)
* fix(windows): stop the Orca CLI dying on a duplicated PATH/Path environment The packaged Windows `orca.exe` launcher read `ProcessStartInfo.EnvironmentVariables`, whose lazy getter copies the case-sensitive process block into a case-insensitive dictionary via `.Add`. An inherited block carrying both `PATH` and `Path` threw `ArgumentException: Item has already been added. Key in dictionary: 'PATH'`, so every `orca` invocation exited 1 before Electron started (native/windows-cli-launcher/OrcaCliLauncher.cs:46, printed at :67). The launcher now mutates its own environment with `Environment.SetEnvironmentVariable` and never touches either `ProcessStartInfo` env property, so `CreateProcess` passes a NULL environment block and the child inherits the live one verbatim. Orca was also minting the duplicate itself. `applyTerminalAttributionEnv` read `baseEnv.PATH` and unconditionally wrote `baseEnv.PATH`, so a Windows PTY that inherited `Path` got a second spelling; which one the child resolved was non-deterministic. `createLaunchEnv` did the same and, because its read always missed on Windows, shipped Agent Teams terminals a `PATH` containing only the tmux shim dir. `resolvePathEnvKey` (extracted from the existing precedent in windows-environment-path.ts) now drives every PATH read and write in the PTY env pipeline, and attribution collapses Windows onto the single OS-resolved spelling. Off Windows the resolver always returns `PATH`, so POSIX behavior is unchanged and a case-sensitive POSIX `Path` variable is never touched. Closes #12046 * test(windows): track the launcher's own-environment marker The #12046 fix moved ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER and ORCA_CLI_COMMAND off ProcessStartInfo.EnvironmentVariables, but this asset test still pinned the old dictionary writes and failed. Co-authored-by: Orca <help@stably.ai> * fix(windows): follow the host block's PATH spelling on sparse daemon env patches Resolving a path-less Windows env to `Path` handed the daemon's own `{...process.env, ...opts.env}` merge both spellings when the host block spelt `PATH`. Fall back to the host block's own key, and collapse again inside the daemon since that merge happens after attribution. Co-authored-by: Orca <help@stably.ai> * fix(windows): resolve the live PATH spelling by block order, not casing Win32 resolves a duplicated variable by taking the first case-insensitive match in the block, so `resolvePathEnvKey`'s hardcoded `Path`-first preference targeted the shadowed spelling on the reporter's own `["PATH","Path"]` block. Drop the attribution-side collapse with it: it deleted the other spelling's value, and deleting the live key promotes the shadowed one, so an env that stripped down to empty lost both. * chore: drop unrelated merge formatting --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
79d3c847bd |
fix(runtime): attribute destructive close requests (#12238)
Attribute destructive runtime and daemon close diagnostics to the requesting client and exact target, record outcomes only after completion, and add reliability-gated attribution regressions. |
||
|
|
026ed921c1 |
fix(mobile): keep main-buffer TUI footer above the iOS keyboard (#9178)
* fix(mobile): keep main-buffer TUI footer above the iOS keyboard The iOS keyboard-avoidance lift anchored on the terminal cursor row. Pi's TUI renders in the main screen buffer (not the alternate screen) with its footer/status rows below the input caret, so the altScreen full-lift branch was skipped and those rows stayed under the raised dock / keyboard. Anchor the lift on the bottom-most non-blank viewport row instead of just the cursor: the WebView now emits contentBottomRow, and the lift uses max(cursorY, contentBottomRow). This generalizes the alt-screen case, keeps short output at the top put, and matches prior behavior for a scrolled shell prompt. Extracted the lift into a pure, unit-tested function (terminal-keyboard-avoidance-lift.ts) and moved metrics parsing into a tested helper on the contract. * fix(mobile): preserve keyboard metrics through notification dispatch * fix(mobile): harden terminal keyboard metrics * fix(mobile): ignore unstyled terminal whitespace * fix(mobile): preserve decorated terminal whitespace --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
a7ed5a45c2 |
fix(mobile): render Mermaid diagrams in MobileMarkdown (#11185)
* fix(mobile): render Mermaid diagrams in MobileMarkdown (#11141) Co-Authored-By: Grok Companion <noreply@x.ai> * fix(mobile): keep streaming mermaid fences as raw code until the fence closes * perf(mobile): memoize MermaidDiagram and add a CDN load watchdog * fix(mobile): escape mermaid source before embedding in WebView script JSON.stringify leaves </script>, &, and U+2028/U+2029 raw, so a diagram source containing </script> broke out of the inline script and ran arbitrary WebView JS. Diagram source is untrusted (agent output, PR/chat content), and this component now renders from chat and markdown preview, not just the PR sidebar. Escape those chars to \uXXXX; the literal still parses back to the exact source. Adds an adversarial buildHtml test. * fix(mobile): embed the mermaid engine instead of fetching it from a CDN The diagram WebView loaded mermaid from jsdelivr at runtime: offline and constrained-network renders always fell back, the stalled-load watchdog existed only to paper over that, and an unpinned floating-major CDN script with no integrity check ran inside the WebView. Embed the lockfile-pinned package's prebuilt bundle via a postinstall generator (same mechanism as the terminal WebView engine) so the document loads nothing external; the watchdog is removed as obsolete and a no-external-URL gate pins it. * chore(deps): align mermaid at 11.16.0 across desktop and mobile Desktop floated ^11.15.0 while the mobile embedded engine resolved 11.16.0. Raise the desktop floor so both lockfiles resolve the same version, and pin mobile exact: the generated WebView engine embeds the package bytes, so an implicit range bump would silently change what ships. * fix(mobile): block Mermaid diagram network requests Mermaid image-node URLs can initiate subresource requests even with the engine embedded. Keep the WebView offline by restricting resource types through its document CSP. * style(mobile): format Mermaid routing test * fix(mobile): use stable keys for Mermaid diagrams * fix(mobile): keep duplicate Mermaid keys distinct Combine each diagram source with its sibling occurrence so identical diagrams remain unique while source edits still remount the WebView and later streaming prose does not. * fix(mobile): keep Mermaid transitive within release-age policy --------- Co-authored-by: Grok Companion <noreply@x.ai> Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
49dc113a0f |
Fix terminal corruption after restored snapshot replay (#12363)
* fix(terminal): preserve restored snapshot fidelity * test(terminal): align legacy history handoff snapshot expectation * fix(terminal): keep legacy snapshot panes mounted * fix(terminal): refresh snapshot capability after startup * fix(terminal): refresh snapshot capability in degraded startup * fix(terminal): await snapshot provider authority |
||
|
|
0db12feee8 | fix(runtime): deliver subscription close when retiring a remote transport (#12384) | ||
|
|
c052ca10a3 |
fix(gitlab): expire project-ref negatives instead of caching them forever (#12390)
GitLab's project-ref cache stored `null` forever and returned any cached value straight from the map, so a repo probed before `origin` was configured — or before `glab auth login` ran for its self-hosted host — kept hosted-review provider detection stale until app restart. The negative-TTL work that shipped for Azure DevOps / Bitbucket / Gitea skipped it. Mirror `createRemoteRefProbeCache`'s semantics: negatives expire on the shared interval, positives stay, the SSH provider generation joins the cache signature so a reconnect re-asks, and a probe abandoned as stale can no longer publish over its successor. Transient git/SSH failures stay uncached. Expiring negatives would otherwise turn `glab auth status --hostname` into one spawn per repo per interval on the hosted-review poll, since a non-GitLab remote reaches it too, so remember the unauthenticated answer per host — not per repo — on the same clock. |
||
|
|
a69ca6d5b7 |
fix(ui): always show Onboarding and Restart Orca in Help (#12379)
* fix(ui): always show Onboarding in the Help menu Users could not find "Show onboarding again" because it required holding Alt. Surface it on the default Help menu so replay is discoverable without reinstalling. * fix(ui): always show Restart Orca in the Help menu Surface Restart next to Check for Updates by default and drop the Alt-only admin gate, which no longer hid anything else. |
||
|
|
64b81cde50 |
perf(runtime): memoize mobile snapshots for mounted terminal worktrees (#12383)
The per-worktree rebuild memo from #12207 refused to skip any worktree with a registered TerminalPane, because builders read live PaneManager/DOM state the memo could not witness. Hidden worktrees stay mounted (8 parked + 12 retained by default, unbounded with terminalHiddenViewParking off), so that set rebuilt on every publication — on the always-on hosts the memo targeted, ~20 worktrees paid the full content build plus the new inputs build and deep compare every tick. Capture the live reads instead: snapshot pane leaf ids, the live active leaf, the serialized pane tree, and per-pane numeric/pty bindings once per publication into MountedTerminalSurfaceCapture, compare captures by value in the memo, and have the builders read only the capture. Live state outside the capture is now unrepresentable in a builder, so the skip stays provably complete — and a mounted worktree whose panes did not change reuses its snapshot like any other. |
||
|
|
194e1a8d4d |
fix(persistence): make the renderer unload checkpoint durably flush before reporting success (#12387)
The sync before-unload checkpoint staged renderer state and then queued store.flushPendingAsync() fire-and-forget, so reload/restart/update paths navigated while the staged session, scrollback and UI state were still only in memory. Quit is covered by the will-quit flush barrier; those paths were not. Keep staging synchronous (no sync durable writes), but record the flush outcome and expose it on app:await-before-unload-checkpoint. Restart, updater install and lazy-chunk recovery reload now join that write before navigating and abort the attempt when it fails or outlives a 20s deadline. |
||
|
|
9e5bd5fb84 |
fix(worktrees): fence SSH worktree deletion PTY teardown to the owning host (#12388)
Destructive worktree removal swept PTYs by worktree id alone. Worktree ids are `repoId::path` and the store keeps one per host, so deleting an SSH worktree could stop a same-id local (or other-connection) workspace's terminals — or fail outright with `selector_ambiguous` when two hosts owned the id. Every destructive teardown now names its owner (resolvedWorktreeId plus the connection/runtime environment), matching the already-hardened forget-local path: - IPC `worktrees:remove` (git + folder workspaces) - runtime `removeManagedWorktree` (CLI/mobile `worktree.rm`, git + folder) - missing-worktree terminal reconciliation, including its no-provider fallback The #11960 allowUnverifiedStop force-delete gate is untouched. |
||
|
|
e39cdb897d |
perf(agent-status): coalesce live status bursts into one render pass (STA-3328) (#12377)
* perf(agent-status): coalesce live status bursts into one render pass (STA-3328) Each live agent-status event arrives as its own IPC task, so a multi-agent burst paid one full React render pass per event (200-488ms main-thread tasks under heavy orchestration). Same-task store commits batch to a single render pass, so buffer a burst for one 33ms window and apply it in one task. The leading event still applies synchronously (zero added latency for single events), only an applied event arms the window (dropped/pending events stay immediate), and both clear paths purge queued sets so a coalesced event cannot resurrect a removed status. * fix(agent-status): preserve queued completion on teardown |
||
|
|
73e7df7764 |
fix(browser): track guest recency across kill-switch off, release download veto on interrupt (#12232)
Two robustness follow-ups from the #12194 safety-review loop (pass 3), staged by the reviewer before its session ended; both fail toward extra retention only: - recency bookkeeping now runs while the retention kill switch is off, so re-enabling evicts in true LRU order instead of worktree-list order - interrupted downloads release the eviction veto instead of pinning the worktree as downloading forever |
||
|
|
c4ae923baf |
fix(ci): vet the adhoc build ref before running it with release secrets (#12161)
The adhoc workflow checked out any requested ref and ran its scripts and electron-builder config with MAC_CERTS, the notary password, and the adhoc publisher token in reach — including refs/pull/* fork code a maintainer could dispatch in one innocuous-looking click. Vet the ref before checkout: PR refs are refused, branches/tags resolve in a bare tree:0 scratch fetch, raw SHAs must be reachable from a repo branch or tag (a partial clone lazily serves PR-only commits by SHA, so name resolution alone is not a trust test), and checkout pins the vetted SHA so a race push cannot swap the commit. Also reference an adhoc-mac-build environment so the secrets can later be fenced off from stale workflow copies via repo settings. |
||
|
|
ce8b778d31 |
perf(runtime): withhold unchanged mobile snapshots from the graph payload (#12245)
* perf(runtime): withhold unchanged mobile snapshots from the graph payload Every graph sync structured-cloned all 222 worktree snapshots to main even when none had changed: 374 KB and ~5 ms per clone, paid twice because Electron clones on serialize and again on deserialize. That transport cost — not the renderer rebuild — is the bulk of a publication. The renderer now sends only the snapshots main has not acknowledged and names the rest in unchangedMobileSessionWorktrees. Detection is object identity, not a deep compare: an unchanged worktree already returns its cached snapshot object. Main seeds nextWorktrees from that list so its prune keeps withheld worktrees live instead of removing them. The call itself is unconditional. syncWindowGraph is not a one-way publish — its return value is the only channel carrying agentOrchestrationByPaneKey to the renderer, and the handler adopts pre-allocated handles, merges detached leaves, refreshes writable flags, and drains graph-sync callbacks on every sync. Skipping it would starve all of that. Two failure modes are closed explicitly. The memo advances only after main acknowledges, so a publication that throws is resent in full rather than silently withheld forever. And a worktree main dropped on its own — worktree metadata removal — comes back in mobileSessionResyncWorktrees, which also clears the accepted-revision record so the republish is not rejected as a no-op. Unchanged republish at 222 worktrees / 787 tabs: 374 KB to 3.4 KB, 5.08 ms to 0.02 ms per clone. One changed worktree: 5.3 KB. * fix(runtime): resync stale withheld mobile snapshots * fix(runtime): align accepted mobile snapshot membership --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
8ab7d8a110 |
fix(updater): base dev builds on published tags, not main's package.json (#12376)
main's version only moves on `release:` commits, and stable patches are cut from release branches that never merge back. On 2026-08-03 main read 1.4.165-rc.0 for twenty hours while 1.4.165, 1.4.166 and 1.4.167 all shipped, so every hourly built in that window was stamped 1.4.165-hourly.* while carrying code newer than 1.4.167 — and sorted below the stable its user was already running. Resolve the base from the main repo's published tags instead, taking the patch above the highest shipped stable. package.json stays a floor for the case where main leads the tags. Co-authored-by: Orca <help@stably.ai> |
||
|
|
c9c1683838 |
fix(ci): seed the adhoc release repo so releases can be tagged (#12375)
gh repo create leaves the repo empty, and publishing a release creates a git tag that needs a commit to point at. The adhoc build got all the way through notarization and upload before failing on "Repository is empty". Co-authored-by: Orca <help@stably.ai> |
||
|
|
0586bab4f9 |
fix(mobile): bound terminal viewport resubscribe loop with backoff (STA-3337) (#12362)
* fix(mobile): bound terminal viewport resubscribe loop with backoff (STA-3337) An empty scrollback frame with absent host dims was coerced to 80x24, which never equals a phone viewport, arming a zero-delay unsubscribe/resubscribe loop (~25/s) that broke long-press gestures and drained battery. - Absent host dims now hold the stream instead of resubscribing. - Fit resubscribes are budgeted per handle (3 attempts, escalating backoff) with an absence-gated refill mirroring the chat-side rearm bound; on exhaustion the view degrades visibly via toast instead of hot-looping. - A fresh post-measure match counts as convergence instead of resubscribing. - setTerminalModes keeps the Map identity when the mode is unchanged, so same-mode frames no longer re-render the session route. - Host emits the subscriber viewport as scrollback dims when the snapshot and PTY size are both unavailable, so current hosts converge immediately. * fix(mobile): cancel stale viewport retries after convergence |
||
|
|
637c7e94c9 |
Add SSH config host picker to add-host dialog (#12334)
* feat(ssh): add SSH config host picker for add-host form Users can now click 'Fill from ~/.ssh/config…' to browse available SSH config hosts in a picker, select one, and have the form automatically prefill with resolved connection details (hostname, port, username, auth). Previously, an 'import' button provided bulk sync on this form—confusing and unhelpful when everything was already synced. That action is now available as a secondary 'Add all' option in the picker. * fix(ssh): import filter preservation and label fallback - Reuse search loader on import completion to preserve active filter inside generation guard - Fall back to hostname when manual host has no label, not empty string - Make alias duplicate detection case-insensitive to match config picker behavior - Validate host availability when restoring project group selection - Add aria-selected attribute to picker options for accessibility * fix(ssh): harden config picker import, alias folding, and host targeting Review findings on the ~/.ssh/config picker + bulk add: - Guard config-host resolution with a generation counter so a late resolve cannot overwrite a later pick or a form the user backed out of; freeze the other rows while a pick resolves. - Stop "Add all N" from re-adopting deleted hosts — it now imports without reAdopt, matching the new-host count it advertises. Settings → Import keeps the explicit re-adopt path. - Fold SSH aliases through a shared normalizeSshConfigAlias for import ownership, delete tombstones, reclaim, picker search, and the save-time duplicate check, which now occupies configHost *and* label like the picker. - Persist GSSAPIAuthentication only when a parsed Host entry asks for it, not when `ssh -G` merely echoes the /etc/ssh system default. - Fail closed with unavailable/setup-not-found when an explicit projectHostSetupId names a non-actionable host instead of silently creating the workspace on a sibling host. - Cache the parsed config for the picker session (refresh on open/retry) so filter keystrokes no longer reparse and Include-expand the file, keep the filter usable during loads, add a Retry on load errors, explain an empty Identity file after a config fill, and drop the always-false aria-selected. * refactor(ssh): centralize host result limit and extract folder group val Move SSH_CONFIG_HOST_RESULT_LIMIT to shared types so the renderer's limit message cannot drift from the host's query limit. Extract findActionableFolderProjectGroup to avoid repeating the folder-host-availability check across the composer hook. * fix(ssh): pass -F to ssh -G when HOME differs from passwd home In E2E tests and sandboxes, isolated HOME can differ from the system passwd home. OpenSSH resolves the default config via getpwuid (passwd), while Node's loadUserSshConfig uses os.homedir() (HOME-aware). Pass -F to explicitly specify the config path when they diverge, so ssh -G and the picker resolve the same file. * fix(ssh): verify config host exists before resolving with ssh -G When a user edits ~/.ssh/config and removes a host, the import picker should not fall back to ssh -G's echoed response (which treats any alias as valid). Check the reloaded config file before resolving. - Force reload config on each resolve to catch user edits post-open - Reject aliases not in the current config before calling ssh -G - Add test for deleted alias edge case - Fix workspace-target fallback to honor explicit host selection * fix(ssh): let tombstoned aliases be re-picked in the config picker Allow users to reclaim a deleted SSH host by re-picking it from ~/.ssh/config. Tombstoned aliases now appear in the picker with a "Removed from Orca" badge and remain pickable, but don't count toward "Add all" operations — ensuring passive import never resurrects a deleted alias while still giving the user a recovery path. |
||
|
|
9ec4907cfb |
fix(agent-status): restore hydrated nonterminal statuses as unconfirmed (#12346)
* fix(agent-status): restore hydrated nonterminal statuses as unconfirmed A hook transition that fires while Electron is down has no receiver and is discarded, so last-status.json can restore a stale 'working' as confirmed truth for up to the 7-day hydrate TTL. Stamp hydrated nonterminal rows with restoredUnconfirmed, carry it through both IPC paths, and treat such rows as never-fresh in the shared and renderer freshness gates so the sidebar, worktree.ps, and the raw snapshot all present the same degraded semantics. Terminal states restore as-is; any accepted live event clears the flag; the flag itself is never persisted. Interrupt/question inference refuses to fabricate transitions onto unconfirmed rows. * fix(agent-status): shed unconfirmed marker when the liveness sweep verifies done The restored-subagent reaper's reconciled entry spread carried restoredUnconfirmed onto a process-probe-verified 'done', making freshness gates suppress a legitimate completion. Keep the marker only while the reconciled state stays nonterminal. * fix(agent-status): let live evidence replace hydrated rows * fix(agent-status): keep restored rows degraded Sort accepted live evidence after hydrated rows even across wall-clock rollback. Let unconfirmed rows own their preserved pane titles without asserting live state, while retaining independently live sibling evidence. * fix(agent-status): suppress unmapped restored titles Treat a single runtime title as covered by the single restored hook row while layout identity is unavailable. Preserve ordinary age-stale fallback and mapped sibling-pane evidence. |
||
|
|
0ea3c9c31f |
fix(renderer): drive agent working spinner on the compositor (STA-3328) (#12359)
* fix(renderer): drive agent working spinner on the compositor (STA-3328) The shared 12Hz JS clock wrote el.style.transform on every mounted spinner (41 live = ~490 main-thread style writes/s), keeping style perpetually dirty and delaying keystroke dispatch (measured typing inputDelay p90 363ms; 19ms with the writes stopped). Replace it with a steps(12) CSS transform animation so rotation runs on the compositor; one mount-time animation-delay write preserves the shared-epoch phase sync. * fix(renderer): lock agent spinner phases to document time * fix(renderer): tolerate missing spinner animation API |
||
|
|
f4b2b782b5 |
feat(orchestration): coordinator-driven release of settled worker terminals (STA-905) (#12355)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
98e8d1c41d |
fix(terminal): reconcile status after escape interrupt (#12345)
* fix(terminal): reconcile status after escape interrupt * fix(terminal): preserve absent interrupt baseline * fix(terminal): ignore stale input acknowledgments * fix(terminal): order interrupt acknowledgments |
||
|
|
13f033f091 |
chore(daemon): disambiguate audit observations (#12343)
* chore(daemon): disambiguate audit observations * fix(daemon): reject future audit protocol roles * fix(telemetry): protect daemon audit observations |
||
|
|
d7fe9d6bcc |
fix(ai-vault): support session scanning in SSH worktrees (#11004)
* fix(ai-vault): support session scanning in SSH worktrees Add relay-native aiVault.listSessions scanning that discovers agent sessions on SSH hosts. Includes fallback to filesystem crawl for legacy relays, full cancellation support, result validation, and scan coalescing to reduce redundant work. * fix(ai-vault): scan sessions in SSH worktrees with coordinated cancellat - Extract batching logic to `mapRemoteScanBatches` for reuse and proper cancellation checkpoints - Move `AiVaultScanCoordinator` from relay to main to handle concurrent same-key requests with individual cancellation signals - Report scope path truncation consistently across relay and SSH fallback paths - Gracefully degrade relay handler on unsupported platforms instead of aborting startup - Refactor issue display to separate blocking errors, scope notices, and skipped transcript counts * fix(ai-vault): stabilize SSH session scan CI Swallow async WSL relay stdin EPIPE so the live hook-relay shard no longer fails after all tests pass. Merge main, resolve scan/relay conflicts, and align cancellation/host-issue reporting with IPC expectations. * fix(ai-vault): harden session scan cancellation, relay timeouts, and preemption Thread the abort signal through every scan and parse path so superseded or cancelled scans stop promptly instead of parsing every remaining transcript for a caller that already left. Replace the fragile message-text relay timeout check with a typed error code so unrelated errors carrying the phrase "timed out after" no longer suppress the filesystem fallback. Fix scan coordinator preemption so a forced Refresh in one window no longer re-enters as a spurious cancellation in another. Add a host-leg cache for the all-hosts view and cap filesystem concurrency so a single slow remote home cannot stall the whole merge. Co-authored-by: Orca <help@stably.ai> * fix(ai-vault): use stable React keys for scan issue banners Drop array-index keys so react-doctor/no-array-index-as-key passes. Uniqueness comes from host, kind, agent, path, and message. * fix(ai-vault): SSH session scanning with configurable depth limits Implement depth-aware caching and proper scan boundaries to make SSH session scanning reliable in worktrees. Users can now select between faster (250 sessions) and comprehensive (unlimited) history scans. The scanner: - Deduplicates scans across relay, host leg, runtime, and renderer layers - Reuses larger scans to serve smaller depth requests - Properly bounds in-scope discovery per-limit - Fixes timeout enforcement when SSH providers ignore abort signals * Move sessionLimit ref update to useLayoutEffect Keep render pure for React Doctor by deferring ref updates to a layout effect, which still executes before render-dependent effects that consume the ref. * fix(adhoc): stamp version prefix from main, not the feature branch Adhoc builds check out arbitrary refs whose package.json often lags version bumps (e.g. 1.4.165-rc.0 while main is 1.4.168-rc.1). Hourly always builds main so it already tracks the product line; adhoc now resolves the base version from origin/main (or ORCA_ADHOC_BASE_VERSION) so branch builds share that prefix. * Revert "fix(adhoc): stamp version prefix from main, not the feature branch" This reverts commit a26a18eb3fd83f7e7d2db9a6a7c3e02e0f79089a. * fix(ai-vault): fix scoped backfill and coordinator race conditions Resolve race where the last waiter leaving could abort an already-settled scan (add `settled` flag). Redesign scoped session backfill to keep searching through newer files until the scope reaches its requested session quota instead of stopping at the candidate limit; out-of-scope files no longer consume the scope budget. Centralize scan limit normalization and fix error classification for cancelled scans using the proper helper instead of checking Error.name. Disambiguate cache keys using JSON and add cancellation check after scope discovery phase. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
a7282fed40 |
Fix Codex launch draft delivery after delayed setup (#12347)
* fix: preserve Codex launch drafts through setup * test: enforce single startup draft delivery * test: name startup draft release behavior |
||
|
|
5941c6c9db |
Improve send comments to ai (#12150)
* Post fixing replies after launching PR comment resolution agents Orca now automatically replies to selected comments after launching an agent to resolve them. Review-thread comments get nested replies; review summaries and issue comments get top-level @-replies. Payloads are snapshotted at queue time and posted only after prompt delivery succeeds. Bounded concurrency (4) prevents SSH slowdown. * Batch conversation replies instead of posting per-comment Combine N unresolvable comments into a single PR conversation reply to avoid timeline spam. Review threads still get one nested reply each. Refactor helper functions to pr-comment-fixing-reply-body.ts and change replyAsConversation(comment, body) → replyAsConversation(body). * Handle bot account mentions in PR conversation replies Use buildPRCommentConversationReplyBody to properly handle GitHub App bot accounts, which don't resolve as @-mentions. Enforce delivery semantics with type-safe PendingPRCommentAiAck payloads and improve error handling around agent launch to prevent stranded state. * Fix React Doctor ref-mutated-during-render errors in ChecksPanel Move comment-resolution payload ref clears and latest-callback mirrors into effects so render stays pure for the static analysis gate. |
||
|
|
ea68d97c28 |
Optimize worktree parent picker: conditional mount, virtualization, lazy status loading (#12225)
* Optimize worktree parent picker: conditional mount, virtualization, lazy - Mount WorktreeParentPickerPopover only when open to avoid hundreds of unmounted instances subscribing to lineage and worktree store updates. - Virtualize the candidate list and resolve activity statuses only for visible rows, eliminating redundant status subscriptions. - Extract filtering, placement calculations, and row rendering into separate modules for testability and clarity. * Memoize worktree parent picker search handler Wrap search state update in useCallback to stabilize the handler across re-renders. Reduces unnecessary effect runs and enables better memoization of child components. * Optimize worktree parent picker: defer unmount, memoize IDs - Defer unmount until exit animation completes (200ms) to prevent premature teardown - Memoize visibleWorktreeIds to prevent status hook from rebuilding its selector on every render |
||
|
|
25213ec04d |
test(terminal): harden daemon upgrade adoption (#12353)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
031115b0a5 |
test(ssh): freeze FrameDecoder clock in framing unit tests (#12356)
Default 4ms maxTurnMs can defer later frames via setImmediate under CI load, so multi-frame assertions after a single feed were flaky. |