mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
a872d0fe6600a7b1dd33f969579693b2ea046d4c
6125
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e9cf106769 |
fix(relay): make fenceAndCloseNow stop the liveness safety net (#12482)
The 5-minute liveness tick from #12432 survived fenceAndCloseNow(), so a tick landing between the pre-sign-out fence and the profile wipe could briefly resurrect a broker (benign but soft — the entitlement check bails afterward). The fence now clears the interval; the next auth mutation re-arms it via refreshDemand, and the safety net otherwise behaves identically. Found in release review of the #12432 cherry-pick. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
00867f06e2 |
fix(ssh): handle owner displacement and graceful shutdown (#12367)
* fix(ssh): handle owner displacement and graceful shutdown SSH connections can reconnect with valid session proof after network loss or device sleep. When the incumbent owner is still half-open, allow the reconnecting client to displace it outright rather than wait for socket closure — a window that may never close. Retain displaced deliveries for the new owner to rotate. During app shutdown, drain SSH sessions without terminating recovery operations, and retry pending owner grants in case a replacement commits mid-drain. * fix(ssh): handle owner displacement and graceful shutdown Make QuitTeardownStartGate a shared singleton so SSH connects use the same shutdown fence as the main quit path. Track test-connection probes to ensure they complete before final teardown. Guard owner displacement to prevent stale owners from clearing recovery state claimed by newer owners. * fix(ssh): fix flaky test sync and add error code safety check Test was using tick-based Promise.resolve() loops which don't guarantee the async operation has started. Replace with signal-based synchronization that waits for the actual lease flush. Also add nullish-coalescing to error code check to prevent crashes if error is null or undefined. * fix(ssh): fence reset transport opens during shutdown * fix(ssh): keep recovery leases stable across reconnects * fix(ssh): close transports owned by cancelled connect attempts When a connect is cancelled after its transport has opened, that cancelled attempt still owns the transport and must close it — otherwise it leaks. Add disconnectConnection() to close by identity (not by target ID) so a cancelled attempt closes only the transport it minted, without tearing down its replacement's live transport. Track priorConnection to detect whether this attempt opened a new transport or reused an existing one, and close only on abandonment if this attempt owns the session. * fix(ssh): fence old owner proofs and close superseded transports When an owner reconnects with a new proof while an old one is still live, the old proof is now fenced with SUPERSEDED_ERROR instead of retrying indefinitely. The relay also closes stale transports to signal that their recovery generation has been overtaken by a newer one. This ensures overlapping reconnect scenarios complete with the newest proof rather than getting blocked by stale recovery attempts. * fix(relay): re-pin stdin/stdout fds after closing to prevent recycling When the relay closes stdin/stdout to signal EOF to the SSH peer, the OS can recycle those fds (0 and 1) for new sockets or files. If Node still treats process.stdin/stdout as those numbers, subsequent operations corrupt socket clients and trigger shutdown errors. Re-pin the fds by opening /dev/null to keep them occupied and prevent recycling. |
||
|
|
8f29f58ab7 |
Keep showing your workspaces when a project is too slow to answer instead of reporting zero (#12222)
A stalled per-repo git scan no longer publishes a healthy-looking empty catalog. Adds an execution-host ownership gate so a degraded host cannot republish another host's worktree rows under its own id. Relates to #11869 — this fixes the stall-publishes-zero half. The issue stays open for the remainder. |
||
|
|
96e31e7bf8 |
Remove a paired computer's deleted projects from every connected device (#12215)
* fix(repos): remove a paired computer's deleted projects from every connected device A project deleted on a paired Orca host stayed in every connected client's sidebar and could not be removed there. Two independent defects: 1. Host-local repo IPC mutations only sent `repos:changed` to the host's own renderer (src/main/ipc/repos.ts:2711). The runtime client-event stream was fed only by mutations arriving over runtime RPC, and clients refetch a remote catalog only on a `reposChanged` event -- there is no polling on desktop -- so the deleted rows persisted indefinitely. The shared `notifyReposChanged` helper now also calls the new `OrcaRuntimeService.notifyReposChangedForRemoteClients()` (src/main/runtime/orca-runtime.ts:5175), mirroring the existing `notifyWorktreesChangedForRemoteClients` precedent. This covers every repo, project-group and folder-workspace IPC mutation, so renames, colors, reorders and adds propagate too. 2. Deleting the ghost row on the client routed `repo.rm` to the owner, which answered `repo_not_found`. `removeProject` wrapped its whole body in one try/catch, so the rejection aborted the local purge before the `set()` (src/renderer/src/store/slices/repos.ts:3466) and the delete button appeared to do nothing. Only `repo_not_found` is now tolerated; any other failure still keeps the row, and an opt-in `errorFeedback: 'toast'` makes it visible at the three single-project user-initiated entry points. Bulk and background callers keep today's silence plus their own aggregate reporting. Closes #11994 Co-authored-by: Orca <help@stably.ai> * fix(repos): revert inert RepositoryPane removeProject arg The settings pane's only render site drops the argument; the toast is already delivered by removeSettingsProjectFromAllHosts. Co-authored-by: Orca <help@stably.ai> * fix(repos): scope duplicate-repo-id deletes to the owning execution host Cover the cross-host collisions #11994's broadcast now fans out to every paired device. Same-name projects on different hosts were already isolated (per-host UUIDs, host-scoped catalog merge and purge) and are pinned by regression tests. Two same-repo-id paths were not: `repo.rm` with a `path:`/`name:` selector and `deleteProjectHostSetup` both resolved one row and then deleted by bare id, taking the sibling host's registration with it. Co-authored-by: Orca <help@stably.ai> * test(mobile): align the poll-interval rationale with the new reposChanged emission Co-authored-by: Orca <help@stably.ai> * fix(repos): resolve deleteProjectHostSetup's repo row only on the setup's own host The sibling-host fallback could only ever pick a row on a host the caller did not name; with no exact match the setup is stale and the existing path already drops just the setup. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
5f9a5b80b3 |
Show "Except default branch" filter only when "Hide sleeping" is on (#12460)
* Show the "Except default branch" filter only when "Hide sleeping" is on The exemption only takes effect during the hide-sleeping sweep, so its row now renders (and counts toward the filter badge) only while its parent toggle is ticked. Co-authored-by: Orca <help@stably.ai> * Restore the sub-option indent on filter toggle rows Tailwind v4 emits px-2 as padding-inline, which outranks the physical pl-6 override, so the indented row rendered flush with its siblings. Split the base padding so the indent actually applies. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
b86880a9c1 |
Keep finished and interrupted agent sessions resumable after sleeping a workspace (#12214)
* fix(agent-sleep): keep finished and interrupted agent sessions resumable after sleeping a workspace Manual workspace sleep ran a liveness filter over the panes it was about to kill (isValidManualSleepLiveAgentEntry), then wiped every pre-existing sleeping record in the worktree. A done, interrupted, typed-into, or >30-min-idle pane therefore lost its only resume handle and woke as a bare shell. A second filter at wake discarded any record carrying `interrupted: true`, which also killed interrupted sessions across an app restart. Capture now records every resumable pane, normalizing only `updatedAt` and `interrupted` and preserving the entry's real `state` so a finished pane keeps its passive record and resumes in place when its tab is opened instead of spawning a duplicate tab. The wake-side interrupted check is gone. A legacy-orchestration-worker block is carried onto the replacement record, and the Pi-compatible promoted checkpoint is no longer overwritten by a re-derived live record. Closes #11598 * fix(agent-sleep): keep durable slept records a repeat sleep cannot re-derive removeSleepingRecordsReplacedByManualWorktreeSleep wiped every record in the worktree, and only the freshly captured set was merged back. A slept `done` pane stays passive until its tab is opened, so a second sleep found no live status row to rebuild its record from and deleted the pane's only `--resume` handle with nothing written back — the original loss of #11598, one wake/sleep cycle later. The wipe now skips a record with no replacement in the new capture set when it is a durable capture (`origin` `worktree-sleep` or `quit`). Provisional `live`/legacy checkpoints are still cleared, so an unresumable Pi row does not survive a sleep it cannot back. Co-authored-by: Orca <help@stably.ai> * fix(agent-sleep): keep a slept workspace's finished panes out of the mobile wake fan-out A manual sleep now records every finished pane, and those passive records fed wakeSleepingAgentsForWorktreeInBackground step (b), which background-mounts one tab per passive record. A phone opening a slept 12-tab workspace would have cold-restored 12 agents at once, undoing the process shedding the sleep was for. Manual-sleep captures of finished panes carry restoreOnTabOpenOnly; step (b) skips them and the pane resumes in place when its own tab is opened, which is what desktop activation already did and what the phone's per-tab mount provides. Co-authored-by: Orca <help@stably.ai> * fix(agent-sleep): close the retained-row and shared-claim gaps in slept-session capture Three narrow holes left by the manual-sleep capture rewrite, all in the same "a slept session must stay resumable" contract: - The retained pass captured `retained.entry` verbatim, so a retained row - stale by construction, since it exists only after the pane's pty died - produced a `working` record that wake then discarded on the >30min staleness rule. It now takes the same `updatedAt`/`interrupted` normalization and the same `automaticResumeBlockedBy` carry-over as the live pass. - The retained pass also ran after the `origin: 'live'` promotion loop without its guard, so a pane holding both a promoted checkpoint and a retained row had the checkpoint (connectionId, transcript identity, active `working` class) overwritten by a re-derived passive record. - The mobile wake filtered `restoreOnTabOpenOnly` records *after* canonicalization, so a lazy record sharing a provider-session claim with an eligible hibernated record could win the claim, delete the hibernated record as a duplicate, and then be skipped - stranding the session with nothing mounted. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
f6878d660f |
Show Claude's AskUserQuestion card in desktop Chat when the agent runs on a paired headless server (#12223)
* fix(native-chat): show Claude's AskUserQuestion card when the agent runs on a paired headless host Three gaps kept the question card off the desktop when the agent ran on a remote `orca serve` host: - The `session.tabs` projection reduced HTTP agent-hook rows to identity only, hard-coding `state: 'done'` and an empty prompt, so `toolName` and the full `interactivePrompt` never left the host. It now publishes the newest fresh hook row's status fields, bounded by the same staleness window `agentType` uses, excluding `providerSessionOnly` resume rows, and yielding to live title evidence unless a question is actually pending. - Nothing republished `session.tabs` when only a hook row changed, and the re-emit carried an unchanged `snapshotVersion` that clients drop on their monotonic gate. Material hook transitions and pane/SSH status clears now bump the version and schedule a coalesced emit. - The desktop card resolved only from live status. It now falls back to the pending ask in the transcript, matching mobile, so a relay gap can no longer leave the composer mounted over a pane parked on a selector. Closes #11761 Co-authored-by: Orca <help@stably.ai> * fix(native-chat): date the hook-row recency guard against a real clock `resolveHookLiveAgentRow` compared a hook `receivedAt` (epoch ms) against title stamps that are title-observation sequence numbers, so the guard could never fire — any fresh hook row overrode live title-derived state, and a manual rename (the one epoch writer) inverted it. Stamp the live OSC title path with wall-clock ms and compare against that alone. The regression test fabricated epoch-valued title stamps production never writes; it now drives the title through `onPtyData`, and a new case pins the opposite direction (hook row newer than the title wins). Co-authored-by: Orca <help@stably.ai> * fix(native-chat): stop an orphaned tool call from pinning a dead question card extractPendingAsk pairs tool results to calls by a global FIFO (tool_use_id is dropped at decode time), so one call that never gets a result desyncs the queue for the rest of the transcript and strands an answered ask as pending. Real transcripts also hold asks the user escaped and typed past. On desktop that card replaces the composer, so the pane became unsendable. Drop in-flight calls at a turn boundary — a user turn or the decoders' interrupt row — since the turn that owned them is over. Claude's tool-result turns decode as role 'tool', so normal FIFO resolution is untouched. Co-authored-by: Orca <help@stably.ai> * refactor(native-chat): trim the headless AskUserQuestion projection Reuse rather than restate: the invalidator now takes the shared `AgentHookEventPayload` instead of a locally redeclared row shape, and the hook live row is a `Pick<>` of the retained OSC snapshot so one projection branch consumes either carrier. Fold the immediate/coalesced session-tabs emit into one method (also drops a redundant re-emit on the provider-session push). Drop card tests that re-route shared-parser assertions through React. Isolate pane-status-clear subscribers and prove the no-republish case by version arithmetic instead of a timed silence. Co-authored-by: Orca <help@stably.ai> * test(native-chat): pin the AskUserQuestion card render under real Electron Why: the 13 parser unit tests pin extraction, but nothing proved a card actually renders where an inert tool call used to. This spec reproduces the paired-headless topology from the client side — live status carrying agent identity and state 'working' but no interactivePrompt/toolName, with the pending ask present only in the transcript — and fails on main. Refs #11761 Co-authored-by: Orca <help@stably.ai> * test(native-chat): drop the unused testInfo parameter Why: oxlint no-unused-vars fails the lint gate on an unused test parameter. Co-authored-by: Orca <help@stably.ai> * test(native-chat): drop leftover proof scaffolding from the ask-card spec The env-var screenshot label and the fixed 2s settle only existed to make the pre-fix capture comparable; the card assertion already waits. Co-authored-by: Orca <help@stably.ai> * test(runtime): use a truly unresolvable pane key in the hook republish guard #11203 taught pane lookup to recover a reminted tab id by leaf id, so the old fixture (new tab id, live leaf id) resolved and bumped the snapshot a second time once this branch merged with main. Co-authored-by: Orca <help@stably.ai> * fix(runtime): refuse a hydrated unconfirmed hook row as live pane status #12346 landed on main after this branch was cut: a nonterminal row restored from last-status.json is stamped `restoredUnconfirmed` because its transition may have fired while no receiver was up, and every freshness gate treats it as never-fresh. The new headless `live` projection here only checked `receivedAt`, so a restart inside the 30-minute window would republish the hydrated row — resurrecting the AskUserQuestion card with no agent left to answer it. `agentType` still reads those rows: they prove identity, just not liveness. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: Neil <nwparker@users.noreply.github.com> |
||
|
|
c498d2d405 |
perf(renderer): index detectedWorktreesByRepo so SSH panes and cards stop walking the full detected catalog on every store write (#12421)
* perf(renderer): index detected worktrees for owner lookups * test(renderer): make detected index perf gate deterministic * test(renderer): cover detected index owner hot path |
||
|
|
9f638da62d |
perf(terminal): gate mobile overlay ticks by pane pty affinity (#12419)
The fit-override and driver emitters are global listener sets: every event reaches every mounted TerminalPane. Both tick callbacks re-rendered the tab before checking whether the event's pty belonged to it — the override branch only used its affinity filter to guard the rAF refit, and the driver branch had no filter at all. Remote handle rotation (replaceFitOverridePtyId / replaceDriverPtyId on resubscribe after a network blip) republishes state for every reconnecting pane, so the cost was quadratic in mounted pane count and remote-exclusive. Move the pty-affinity check ahead of both ticks, matching the guard shape already used by use-native-chat-can-send and pty-connection. The subscription pair is extracted into useMobileOverlayTicks so the fan-out is directly measurable under a real React render. |
||
|
|
29bfc1e22c |
Fix Windows git/gh attribution wrappers rejecting a piped stdin argument (#12440)
Co-authored-by: Orca <help@stably.ai> |
||
|
|
ac7f9a4fe1 |
fix(pty): require proven absence before retiring a stable pane owner (#12393)
* fix(pty): require proven absence before retiring a stable pane owner "Session not found" only proves the provider we asked has no such PTY. A degraded router answers unmapped session ids from the local fallback, which never owned a daemon session, so a live agent's PTY produced the same error a dead one does — and the pane was then given a synthetic exit, its durable pane->PTY binding deleted, and a duplicate spawned. For a single-pane tab the retirement also drops the tab from persisted state, which the renderer cannot add back. Gate the retirement on probePtyLiveness, which polls every possible owner and answers false only when all of them agree the session is absent. `null` (nobody could answer) is not absence. Providers without a probe are their own sole owner, so their refusal stays authoritative and their behavior is unchanged. Also move the retirement's durability barrier off writeToDiskSync: it fsyncs the whole multi-MB state from the main thread, and a restore that retired N dead panes paid that stall N times. * fix(pty): make the unproven-absence veto transient and route attaches to the real owner Review found the proven-absence gate correct in direction but terminal in effect: "probe says alive" and "probe says unknown" both became a hard spawn failure with no consumer, no retry, and a raw internal token in a toast. - Graceful teardown (should-fix): a session being killed reports alive (getAppliedSize answers while isAlive, and isTerminating does not flip _state) while createOrAttach already throws SessionNotFoundError, so for up to KILL_TIMEOUT_MS the pane failed instead of retiring the doomed binding. The veto now re-proves on a bounded 6s budget: the moment any owner answers "absent" the pane retires and spawns fresh exactly as before. - Unprovable owners (should-fix): DaemonPtyAdapter.probePtyLiveness never called ensureConnected, so a merely disconnected adapter answered null forever and one null poisons probePtyOwners; it also took no deadline, so a wedged daemon burned the client's 30s request timeout per attach. It now connects first (like listProcesses) and threads an absolute deadline through the fan-out. The thrown error is user-legible English instead of terminal_pane_owner_unverified. - Durability barrier (should-fix): flushPendingOrThrowAsync is not a twin of flushOrThrow — it defaults to the drain-to-stable-generation loop the sibling best-effort caller deliberately avoids, adds active-view and GitHub sidecar writes, re-serializes state per iteration, and rejects when writes are frozen where the sync path no-ops. Reverted to flushOrThrow, matching the sibling retire paths in orca-runtime. - Misrouting root cause (nit): DegradedDaemonFreshSpawnRouter.spawn sent an unmapped sessionId to the local fallback and DaemonPtyRouter.spawn sent it to the current daemon, both faking "Session not found" for a live session. Both now resolve an existing owner first (the same resolution every non-spawn path already uses); minted ids still route to the fallback/current daemon. The pane reattaches instead of only surviving. Tests: retry-until-proven, teardown-window retire, misroute-then-reattach, probe deadline threading, unconnected-adapter probe, and both routers' owner resolution. Each fails with only the source reverted. * fix(pty): drop the follow-up, keep the minimal proven-absence guard Reverts |
||
|
|
d8e5944b60 |
Stop a duplicate headless orca serve from crash-looping and exhausting AppImage FUSE mounts (#12212)
* fix(startup): stop a duplicate headless serve from crash-looping and leaking AppImage mounts A second Orca launch that loses the single-instance lock called app.quit() before `ready`. That quit is deferred, so the doomed process kept booting into Chromium's Linux display initialization, failed with "Missing X server or $DISPLAY", and died with SIGSEGV. systemd read that as a crash and restarted it forever; each restart re-mounted the AppImage and left the squashfuse mount behind, until the host hit the 1000-mount FUSE ceiling and every later launch failed. The lock-losing launch now calls app.exit(3), which terminates synchronously before any display init. Exit code 3 is a stable "another process already owns this userData profile" contract, and the documented systemd unit uses RestartPreventExitStatus=3 plus a real StartLimitIntervalSec/StartLimitBurst window so a permanently failing launch can no longer retry unbounded. Second-instance argv is now forwarded to the owner, and a duplicate `orca serve` no longer asks the live headless server to open a desktop window. Desktop activation for ordinary launches and macOS dock re-activation is unchanged. Closes #11935 * docs(headless): clear the start limit before the scripted service starts StartLimitIntervalSec=300/StartLimitBurst=5 rate-limits operator starts too, so after a crash-loop trips the burst systemd refuses a plain `systemctl start` for the rest of the window. The Upgrade and Roll back scripts run under `set -euo pipefail`, so that refusal aborted the rollback mid-flight and left the server down on the exact recovery path the doc prescribes. Both scripts (and their EXIT-trap recoveries) now run `systemctl reset-failed` first, the unit reference explains the interaction, and the crash-loop bullet points at it for manual starts. Co-authored-by: Orca <help@stably.ai> * test(startup): reproduce the #11935 duplicate-serve crash loop under real Electron The committed coverage for #11935 was source-text greps, so nothing gated the mechanism the fix rests on: pre-`ready` `app.quit()` is deferred, which is why the lock-losing headless `orca serve` kept booting into Linux display init. This runs two real Electron processes against one disposable profile. The duplicate executes the lock-loss gate's own `app.*` statement, lifted out of `src/main/index.ts`, so reverting to `app.quit()` fails the test. It also feeds the owner's real forwarded argv through `shouldActivateDesktopForSecondInstance`. Also record why the activation predicate matches `--serve` and not the `serve` subcommand: an AppImage launched as `orca serve` exits at the CLI redirect before requesting the lock. * test(startup): wait for the owner process to exit before removing its profile Windows holds the profile's handles for a beat after SIGKILL, so an immediate rmSync can fail with EBUSY/EPERM. Co-authored-by: Orca <help@stably.ai> * test(startup): pass the fixture marker path by env, not argv Chromium reorders argv and the duplicate's argv is itself under test, so a trailing positional was the wrong channel for it. Co-authored-by: Orca <help@stably.ai> * test(startup): only the activation case waits on the owner notification The exit-contract cases assert on the duplicate's own already-terminated process, so they should not block on cross-process delivery. Co-authored-by: Orca <help@stably.ai> * test(startup): drop the staged lock race, keep the real-Electron gate contract CI proved the two-process form cannot work on a display-less Linux runner: Chromium's ProcessSingleton needs the browser IO thread, which needs `ready`, which needs a display. The pre-`ready` owner looked stale and the duplicate took the lock (`expected [ 'DUPLICATE_WON_LOCK' ] to include 'DUPLICATE_LOST_LOCK'`). Lock acquisition and argv forwarding are already covered in single-instance-lock.test.ts. What only a real process can settle is what the loser does next, so that is all this file now runs -- display-independent. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
e599d924bd |
fix(editor): stop Close All / Close Others freezing the renderer on large tab sets (#12404)
* fix(editor): stop Cmd/Ctrl+Shift+T reopening a phantom editor tab `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. * fix(editor): make Close All linear instead of cubic in tab count closeAllFiles resolved a reopen position for every closing tab by calling getRecentlyClosedTabPosition, which itself rescanned the worktree's tab-bar order and group membership with a nested find per entry. That made the whole bulk close O(N^3) in open editor tabs, and because it runs inside a synchronous zustand set() the renderer is blocked for the entire duration: measured against the real store, 500 tabs took 1.2s and 1000 tabs took 13.0s of frozen UI. Sessions that accumulate tabs — long SSH/relay work, a mirrored host republishing open files, a folder workspace opened wide — hit this on a single menu click. Hoist the per-tab rescans into one precomputed index. The new createRecentlyClosedTabPositionIndex builds tab-bar, unified-tab, and group lookups in a single linear pass over the worktree's state, then answers each position query in O(1); getRecentlyClosedTabPosition now delegates to it so single-tab callers keep identical behavior. Positions, reopen order, and the tab-bar/group indices recorded by #12236 are unchanged — first-occurrence wins everywhere, matching the indexOf/find scans this replaces. Position derivation moves to its own module so recently-closed-tabs.ts stays under the max-lines limit without a suppression; existing importers are unaffected via re-export. Same measurement after the change: 500 tabs 44ms, 1000 tabs 142ms, and the instrumented scan count drops from 503,000,500 to 4,000 at N=1000. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
401f66939d |
fix(codex): do not silently run the previous account when managed auth is unreadable (#12407)
* fix(codex): never launch the previously synced account during the credential grace window The credential-absence grace window exists so a mid-rotation read of a managed auth.json does not deselect the account over a race. In the shared runtime-home lane it did that by returning early from syncForCurrentSelection, which leaves the runtime home holding whatever it held before — the previously synced account's credentials. So when the selected account's auth.json was missing or unreadable (home on a disconnected network drive, never provisioned, removed, or held under an EPERM/EACCES lock by Windows AV), the next launch ran Codex as the *previous* account while Settings showed the selected one: wrong quota, wrong identity, work attributed to the wrong account, and no signal to the user. Not being able to read the selected account's credentials proves nothing about the identity already mirrored in the runtime home. The grace window now only short-circuits when Orca can prove the runtime home already holds that same account (shared-runtime-auth provenance, falling back to the in-memory sync state on pre-provenance installs). Otherwise the runtime auth is cleared and the provenance fenced: the selection survives in case the absence heals, but the launch is logged out rather than logged in as someone else. Tests cover ENOENT, EACCES/EPERM, and a genuinely still-settling rotation of the selected account, which keeps its grace and its credentials. * fix(codex): keep a mirrored token refresh when refusing an unproven selection Clearing the shared runtime auth to avoid launching the wrong account can drop bytes Codex refreshed in place — on a cold start whose mirror still holds the previous account, no outgoing read-back has run yet, so those tokens exist nowhere else and OAuth refresh-token rotation would force a re-login. Read the mirror back to whichever managed account owns it (identity + freshness proven, as on an account switch) before removing it. * fix(codex): keep an unreadable managed home from dropping another account's mirrored refresh An account whose auth.json cannot be read aborted the whole read-back scan, so removing the runtime mirror for an unproven selection destroyed a token refresh it held for a different account. Skip the unreadable home instead. * fix(codex): prove mirror ownership from the credential, not just provenance Review found the unproven-selection clear was both too eager and too destructive: - A `fenced` provenance record (torn provenance file, or a crashed pending write) turned the grace window into a logout of the account the mirror actually and correctly held. - The pre-provenance fallback was tautological: initializeLastSyncedState seeds lastSyncedAccountId with the SELECTED account, so `missing && lastSyncedAccountId === id` is true on every cold start — the wrong-account launch still reproduced on installs with no provenance file. - The pre-delete read-back only rescued managed-owned bytes, so a system-default refresh living only in the mirror was destroyed with no path back to ~/.codex. - rmSync was unguarded on a branch whose entry condition is "the filesystem refused a read", so an EPERM/EBUSY lock could throw out of prepareForCodexLaunch, and the fence was written after the delete. - Swallowing managed-home read errors let an ambiguous candidate set collapse to a single match and be written to the wrong home. Ownership is now decided from the credential's own identity claims (codexAuthMatchesManagedAccount against the account record, no read of the unreadable home), then from the exact bytes Orca mirrored this run, and only then from provenance. The clear rescues a mirror-only system-default refresh to ~/.codex first (reusing the ownership rules syncRuntimeAuthWithSystemDefault already applies, now shared via resolveSystemDefaultMirrorClaim), fences before deleting, and never throws. An unreadable managed home whose record cannot rule it out now makes the read-back scan ambiguous instead of misattributing the bytes. Five tests added; each fails on the previous commit and passes here. * fix(codex): never let the fence cancel the delete, or unattributable bytes cause a logout Round-2 review found the previous response commit walked the guard backwards in three places: - `fence before deleting` put persistSharedRuntimeAuthProvenance and rmSync in one try, so a fence write the OS refuses (Windows AV lock on the provenance file) cancelled the delete and the launch ran with the other account's credentials — the original defect, reproduced. The delete now runs from a finally: the fence is still attempted first, but neither it nor the read-back rescue can skip it. - Credentials with no OAuth identity claims (apikey/agentIdentity/PAT/bedrock, all first-class in managed-codex-auth-readiness) can never satisfy a positive ownership proof, so a pre-provenance install deleted the mirror it correctly held during a transient absence. With no committed provenance record left, ownership is now refuted rather than proven: only claims that contradict the account rule the mirror out, and claims a credential does not carry contradict nothing. - A stale record email hard-failed the proof even when the account id matched exactly. Orca only refreshes account.email on add/re-auth, so a renamed ChatGPT account kept a stale email indefinitely and both lost its mirror and had its refreshes dropped instead of read back. A positive account-id match now outranks the email, in one shared rule both identity predicates use. Four tests added; each fails without its fix. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
ed7849eb7b |
fix(worktrees): stop silently switching existing Windows setup scripts to Git Bash (#12406)
* fix(worktrees): stop silently switching existing Windows setup scripts to Git Bash #6967 derived the Windows setup-runner shell from `terminalWindowsShell`. On upgrade, any Windows user whose terminal preference resolved to Git Bash had their existing `orca.yaml` setup script (and issue command) handed to bash instead of cmd.exe. Scripts authored against the cmd runner — `copy`, `xcopy`, `set VAR=value`, `if errorlevel 1`, `%VAR%`, backslash paths — broke with no migration and no warning, and the failure looked like Orca broke the project. The conflation is also wrong in the steady state: a terminal preference is per-user, so two people on the same repo got different interpreters for the same orca.yaml and no project could write a setup script that worked for all of its Windows contributors. The interpreter is now a property of the script, declared the standard way: a leading `#!` line. Native Windows keeps the historical `.cmd` runner unless the script declares a POSIX shell, so no existing script changes behavior. `resolveSetupRunnerShell` keeps its role as the feasibility gate — a bash runner still requires the terminal to resolve to Git Bash, since the launch command is typed into that shell and uses MSYS `/c/...` paths. `buildWindowsRunnerScript` now drops a leading `#!` line rather than `call`ing it, so a declared-bash script that falls back to cmd (Git Bash missing) fails on a real setup line instead of aborting on errorlevel at line one. WSL worktrees, POSIX platforms, and SSH hosts are untouched. * fix(worktrees): keep the cmd setup runner launchable from a Git Bash pane Adversarial review of this PR found that pinning the runner format per script reopened issue #6896 one layer down. - `WorktreeSetupLaunch.shell` had been redefined to mean "the format the runner file was written in". `resolveSetupRunnerCommand` consumes it as "the shell that types the launch command", so a Git Bash terminal with a batch setup script produced `cmd.exe /c "C:\...\setup-runner.cmd"` typed into a bash pane, where MSYS rewrites the `/c` switch into a drive path: cmd opens interactively and setup never runs. `shell` is the terminal's family again; the runner file's .cmd/.sh extension carries the format, and a batch runner launched from a POSIX pane reuses the existing PowerShell ProcessStartInfo launcher. - The cmd runner dropped a leading `#!` line and ran the rest as batch, so a bash script reaching cmd (PowerShell/cmd terminal, or any SSH-to-Windows host) got its interpreter-agnostic prefix executed before failing mid-way. It now prints why and exits 1 without running anything. - A `#!` line's option flags were discarded: `#!/usr/bin/env -S bash -euo pipefail` lost pipefail because the runner is launched as `bash <path>`. The generated posix runner now replays declared flags via `set` and drops the duplicate interpreter line. - Docs cover the per-user setup command in repository hook settings, which goes through the same `#!` rule, and describe what the `#!` line does and does not select. Tests: composed launch command for a POSIX pane + cmd runner (hooks, shared runner command, setup sequencing gate, observed-setup signal), the cmd runner's shebang refusal, and shebang flag replay. Each fails with the source reverted. * fix(worktrees): replay only real `set` flags and keep the gate in the pane's shell Two round-2 review findings: - `#!/bin/bash -l` replayed `set -l`, which exits 2 and aborted the runner under its own `set -e` before a single setup line ran (all platforms). Only the flags `set` documents are replayed now; a bare `-o` with no option name is dropped instead of dumping the shell-option table. - The wait-for-setup gate picked its language from the runner file, so a batch runner launched from a Git Bash pane got the PowerShell gate while the agent startup command was already POSIX-quoted — `Invoke-Expression` cannot parse `'\''`. The gate now follows the pane; the runner still launches through the ProcessStartInfo launcher, never through bash. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
141b1f43f6 |
fix(runtime): keep the listener on loopback for a "This computer only" pairing link (#12405)
* fix(runtime): keep the listener on loopback for a "This computer only" pairing link The runtime pairing URL handler called ensureNetworkExposure() for every offer, including one whose advertised address is loopback. Settings -> "Share this Orca server" offers a "This computer only" radio that pairs against 127.0.0.1 precisely so nothing is reachable off-host, yet choosing it rebound the WebSocket listener from 127.0.0.1 to 0.0.0.0 — and the widen never narrows back, so the runtime stayed exposed to the whole LAN for the rest of the process after the user picked the option that exists to avoid exactly that. Gate the widen on the advertised address: only a non-loopback endpoint (LAN, Tailscale, custom host) needs a listener reachable off this machine, so those paths keep widening exactly as STA-2370 intended. A loopback link is already served by the loopback listener, so it now mints without touching the bind. Classification reuses the shared pairing-address classifier, which also covers localhost, ::1 and 127.0.0.0/8 typed into the custom-address field. Tests: a real OrcaRuntimeRpcServer driven through the IPC handler asserts the bind host stays 127.0.0.1 after a local link and flips to 0.0.0.0 after a LAN one, plus handler-level cases for 127.0.0.1 / localhost / ::1. * fix(runtime): gate the pairing widen on the user's declared reach, not the address shape Review of #12405 found two ways the loopback fix misbehaved. 1. The guarantee died at the next launch. resolveInitialWebSocketBindHost() binds 0.0.0.0 whenever any device has lastSeenAt > 0, and MobileSocketWiring stamps that for EVERY authenticated socket — including the local browser opening a "This computer only" link. So the runtime was still published on every interface, one restart later. Grants now carry the reach they were minted for (DeviceEntry.pairingReach, persisted); a this-computer grant no longer counts as proof that an off-host client may reconnect. Registries written before the field default to network reach, so an already-paired phone still finds a wide listener after upgrading. A pending grant that is re-advertised for the network widens (never narrows) so its link survives. 2. The widen was gated on the shape of the typed address, which the renderer never sent the intent for. A Custom `127.0.0.1:8443` — the documented SSH tunnel / reverse proxy field — skipped the widen and produced a dead link, while `localhost:8443`, `[::1]:6768` and `ws://127.0.0.1:6768` widened, so the same loopback intent was handled three different ways. The renderer now sends the declared reach ('this-computer' | 'network') and main gates on it; the address is only used as a mismatch guard (a this-computer reach carrying an off-host address still widens rather than minting an unreachable link), resolved through resolveAdvertisedPairingHostname so every accepted address form classifies identically. Also corrected the ensureNetworkExposure invariant comment: the widen is no longer confined to the first pairing action, so it can now tear down live loopback sockets — they reconnect on the reused pinned port. Tests: reach-form matrix + tunnel/undeclared/mismatch cases in mobile.test.ts, real-server relaunch bind for both reaches, legacy registry compatibility, the pending-grant reach upgrade, a live-client port-stability guard, hostname resolver coverage, and the renderer reach plumbing. Reverting only the source fails 18 of them. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
dbb7804421 |
fix(updater): re-prove the retained Linux package before a privileged retry (#12395)
* fix(updater): re-prove the retained Linux package before a privileged retry The recovery card's "Try Automatic Install Again" handed electron-updater the cached .deb/.rpm path with no re-verification. That path is user-writable, so the digest proven when the card rendered says nothing about the bytes dpkg or rpm would read as root minutes later — and the card's other actions (copy command, reveal) validated while the one that actually installs did not. Re-hash the artifact immediately before the install, ahead of any destructive quit prep, and abort with copy that tells the user to download again. This narrows the window rather than closing it; only an immutable handoff would close it, which is a larger change. Also fix a macOS-only failure this suite gained with the platform-conditional pre-commit copy: the expectation hardcoded the non-Darwin string, so the suite was red on any Mac. * fix(updater): re-prove the retained Linux package on every install path Review findings on the original fix: 1. The abort force-sent its error status with no staleness guard, so a verdict from a hash that outlived its cycle overwrote whatever card had replaced it (a fresh 'available' from Check for Updates became a stale "package no longer matches" error). Now keyed on an install-cycle signature, the same protection failLinuxPackageRecovery already had. 2. 'read-failed' (EMFILE/EIO/EACCES mid-stream) was described as a digest mismatch and tore down the recovery card. It now reuses the accurate per-reason copy and keeps the card, exactly as the Copy/Show paths do for the same reason. It still fails closed: chmod 000 on a swapped file would otherwise be a one-line bypass, since root can read what we cannot. 3. The check was keyed on the recovery status, so it only covered the retry. The primary 'downloaded -> Restart to Update' install, whose window is hours rather than seconds, handed the same user-writable path to dpkg/rpm unverified. Moved into performQuitAndInstall keyed on the tracked artifact, so both paths are covered; non-Linux keeps its exact timing through a synchronous artifact guard. 4. The async prologue had moved the "quit timer is always cleared" invariant out of a try/finally. The re-proof now owns a flag cleared in finally, and a rejection fails closed instead of wedging the updater. 5. The install re-proof no longer joins an in-flight validation, so its proof cannot predate the click that asked for it. 6. The retry button gained the pending affordance the other actions have, since the click now streams the whole package before anything happens. Tests: real packages are staged in a real updater cache for the whole Linux block (a path that never existed would now abort every install); new cases cover the swapped primary install, the stale-verdict drop, the preserved card on read-failed, the rejecting re-proof, the concurrent second click, and the fresh-hash guarantee. Each was verified to fail with only its source change reverted. * fix(updater): tell the renderer when a stale verdict abandons the install The cycle guard that stops a stale digest verdict from clobbering a newer card also withheld the only signal the renderer has that the restart was called off. The preload abort relay keys on an 'error' status, so with the status suppressed the window stays restart-prepared for the rest of the session: Terminal/Settings skip their unsaved-work prompts and the shutdown checkpoint stays deduped, so a later real quit stages no fresh snapshot. Push the abandon from performQuitAndInstall's single return-false site, so it cannot depend on what the reporter decides about the status text, and relay it to the existing relay.abort() (a no-op unless the renderer armed a restart). The status stays cycle-guarded exactly as before. Tests: the stale-verdict and swapped-primary-install cases now assert the push, a committed install asserts its absence, and the preload relay test covers the new channel. Each fails with only its source change reverted. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
20a2901677 |
fix(worktree): tell the truth about live PTYs, and offer force for a wedged sweep (#12394)
* fix(worktree): tell the truth about live PTYs, and offer force for a wedged sweep Two gaps in the #11960 force path: The delete toast described every unstopped-PTY failure as "could not confirm every terminal has exited", including the case where verification positively watched them running. Force Delete proceeds either way, so the user was being asked to waive a doubt that did not exist while a running agent's uncommitted work died with it. The live verdict now gets copy that says so. A sweep that rejects before any per-PTY verdict exists (wedged daemon, dropped SSH channel) fails with a teardown-timeout message that the force classifier did not recognise, so no Force Delete button appeared — the exact dead end #11960 set out to remove. That error now carries the shared prefix and classifies. * fix(worktree): close the sweep-rejection wedge and stop racing the delete Review of #12394 found the fix covered only half the wedge it named, and routed users into a force path whose own safety comment was untrue. 1. Only the outer deadline was classifiable. When a provider *rejects* the sweep — dropped SSH channel, erroring daemon — settleBeforeDeadline rejects with the provider's original error, which carries no marker, so classifyWorktreeForceDeleteReason still returned null and no Force Delete button rendered. That is the exact case #11960 named. A rejected sweep on the destructive path is now reworded through the existing unstopped-PTY prefix (provider text preserved, original kept as `cause`), so old and new clients alike classify it as 'unstopped-pty'. 2. Force could delete files while a sweep was still running. The deadline rejects without cancelling run(), so allSettled resolved with shutdown() still in flight — by construction the deadline error can only fire while something is in flight. Force then deleted the directory a live PTY still held open (EBUSY / half-delete on Windows and WSL). Sweeps are now tracked so the forced path waits for the abandoned work, bounded by a 2s grace; force never wedges, and when the grace expires the warning says handles may outlive the delete instead of implying the sweep finished. 3. The toast test named for the classifier passed the reason in as a literal, so it never exercised it. It now derives the reason exactly as the store does, and fails against main. 4. Added the missing unstoppedPtyLive key to the English catalog. 5. isProvenLivePtyRemovalError anchored the 'still live:' marker to the detail separator, so a worktree path can no longer spell out a live verdict and flip the toast to the destructive copy. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |