mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 16:03:26 +00:00
c485acb355ff7ebfe44e1f0962e7aec79030c916
6156
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6f607fae7f |
fix(ssh): let terminate reach remote PTYs the app gave up reattaching (#12642)
Every SSH reattach failure abandons the remote terminal without shutting it down, and abandoned terminals then became structurally unreachable — excluded from reattach enumeration and, critically, filtered out of the user-facing "Terminate sessions" action, so a user could not kill them even manually. The core problem is a naming trap: `expired` never meant the remote shell died. It means the app gave up reattaching. It is written on reattach failure, on spawn-time expiry, and in bulk by a relay reset inside a `finally` that runs even when the force-stop threw. So the leases most likely to name a still-live orphan were exactly the ones the terminate path excluded. This change is reachability only. Expired leases are now reachable by an explicit user-initiated terminate, with the relay's response used as evidence: a shutdown that reports the PTY gone tombstones the lease, and leases already proven terminated are left alone. **No automatic kills were added.** Every abandon path was enumerated and none of them proves abandonment: attempts-exhausted knows nothing (the relay never answered), identity mismatch means a *live* PTY belongs to a different pane so killing it would destroy someone else's terminal, and not-found is the one branch with real proof of death — where the process is already gone and needs no shutdown. Per the rule that unprovable liveness never authorizes destroying a session, the abandon paths deliberately leave the process running. Relay-side automatic collection of unattached PTYs is deliberately NOT implemented: the relay cannot distinguish an abandoned terminal from a deliberately detached one, and the unlimited default grace exists precisely so long-running work survives disconnects and host sleep. Any bounded reaper would be killing on absence of evidence. Verified: 3 tests red on main. Negative tests assert each abandon path leaves the shell running and the lease terminable, proven real by mutation — adding a shutdown to the exhausted branch or expiring on identity mismatch each turns them red. Fixes STA-3376. |
||
|
|
817164d3d1 |
fix(ssh): preserve agent generation timeout budget (#12644)
AI commit-message and PR-field generation over SSH failed deterministically at exactly 30 seconds whenever the remote agent CLI took longer, reporting "Claude could not be reached on the remote PATH. Try again after the SSH connection recovers." That message was wrong twice over: the SSH connection was healthy (terminals and git kept working on it) and the agent binary existed — it was simply still running. Cause: the SSH channel multiplexer applies a 30s default deadline when a request omits its own, and the generation call passed none, even though the operation itself carries a 60s budget. The shorter transport deadline always won, and the resulting rejection was then mapped onto the generic connection/PATH error. Fix: derive the transport deadline from the operation's own budget plus a margin at that call site (the global default is deliberately unchanged, since other callers depend on it), and classify a transport timeout as a timeout — reporting that the agent exceeded its budget and may still be running — while genuine connection and PATH failures keep their existing guidance. Verified red on main first: a 45s response rejected by the 30s default, and a typed timeout mapped to the PATH message. Both green after. Caller audit covered commit messages, PR fields, branch naming and model discovery. Fixes STA-3073. |
||
|
|
f742d99ed9 |
fix(remote-runtime): materialize the host surface when reconnecting a terminal pane (#11542)
Reconnect could never recover a terminal pane whose host-side process was gone (host restarted, or the workspace was never opened there): recovery only polled the tab inventory, which can never create the surface it is waiting for, so Reconnect spun for ~60s and gave up permanently. Verified with a deterministic reproduction: on main the recovery path issues 51 inventory polls and zero activations across both an automatic online trigger and a manual Reconnect click; with this change the pane re-materializes, rebinds and accepts input. Review found and fixed three further defects beyond the original change: - an activation answered with a stale ready handle left the loop polling forever instead of re-activating; - a non-missing activation failure (e.g. an older host without the method) never fell back to inventory; - host-side, activating a parked surface permanently deleted the host tab, because an already-absent persisted binding was read as a competing owner *after* the destructive retirement had already run. Independent review confirmed by mutation testing that every production change is covered by a test that fails when it is reverted, that only an authoritative inventory can retire a pane, that the loop is bounded under every failure mode, and that the unknown-liveness guard (proven death required before retirement) is intact. Fixes STA-3002. |
||
|
|
87d29262f8 |
fix(terminal): attach never-activated daemon sessions on remote subscribe and provider-read fallback (#12589)
* fix(terminal): attach never-activated daemon sessions on remote subscribe and provider-read fallback A daemon-backed terminal whose tab was never activated in the host UI was never attached, so the daemon emitted no bytes: paired clients rendered blank/frozen panes and `terminal read` returned an empty tail while the PTY was alive. - Runtime: first remote view subscriber of a known-but-unattached local daemon session triggers an attach through the pty controller — attach-only, no resize, no renderer mount/focus, headless-safe, deduped across concurrent subscribers, and never detached on release. Excludes SSH-scoped ids and sessions a local spawn already published this generation. - Read path: withVisibleSnapshotFallback now falls back to the provider tail for an empty-tail never-attached live local session; unprovable state stays empty, never an error. - pty controller: expose attach with getProviderForPty-style routing, answering false on doubt; local daemon provider only. - Daemon adapter: attach rides the session's applied size instead of a hardcoded 80x24, sends attachOnly, and retires a pre-v31 daemon's accidental spawn instead of publishing it. Deterministic harness drives the real terminal.multiplex handler against a real OrcaRuntimeService with an injected daemon-model controller whose data events are gated on attach; covers snapshot-capable and snapshot-null daemons, concurrency, release, replacement-spawn exclusion, and negative safety. Red on base, green with the fix, red again with the fix reverted. * fix(terminal): refuse degraded-provider attach fallback and surface failed legacy-spawn retire Verifier follow-ups on subscriber-driven daemon attach: - DegradedDaemonPtyProvider.attach routed unknown ids to the in-process fallback, whose no-op attach resolves — the runtime then pinned a subscriber-driven attach as succeeded while the stream stayed blank. Attach now refuses any route that resolves to the fallback (a fallback pty cannot own a daemon-surviving session), so the controller answers false, no sticky success is recorded, and a later subscriber attaches once a daemon adapter proves the id. Session-probe adoption moved to degraded-daemon-session-routing alongside the new refusal. - The pre-v31 attach-only TOCTOU retire (accidental legacy spawn kill) now logs a warning with the sessionId on kill failure instead of swallowing it, so an orphaned replacement shell is diagnosable. Regressions: degraded provider refuses unowned/fallback-owned attach and routes to a daemon once it proves the id (red on previous commit); runtime harness pins refused-attach retry for a later subscriber; adapter test pins the surfaced kill failure. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
6d9b529027 |
fix(runtime): seed terminal previews and titles from restore payloads (#12579)
* fix(terminal): seed list/read records from reattach restore payloads After an app relaunch the PTY daemon survives and spawn silently reattaches, but the restore payload (reattach snapshot, cold-restore scrollback, relay replay, lastTitle) arrives as a spawn RPC result and never passes through runtime.onPtyData — the only feeder of the terminal records behind `terminal list`/`terminal read`. Every restart therefore left connected terminals with empty title/preview/lastOutputAt and a zero-line read tail, blinding orchestrators that poll terminals. The spawn flow now calls runtime.seedTerminalRestoreTail with the restore text and lastTitle, unconditionally of the renderer-authority emulator gate (the records are main-side only). The seed reuses the live path's normalize/tail/preview pipeline on a capped 256 KiB suffix (re-anchored at a line boundary so a cut escape cannot leak), only fills records that never saw output (a remount reattach cannot re-apply history), routes titles through the applySeededAgentStatus precedent (state writes only — no waiters, no side-effect facts), and never stamps lastOutputAt or waitBlockedAt: restored bytes are historical, not fresh activity. lastTitle is threaded from the daemon reattach snapshot and cold-restore checkpoint into PtySpawnResult; relay replays seed preview only. SSH and runtime-controller paths are unchanged — seeding is gated on the fields existing. * fix(terminal): seed restore records on the controller spawn path and prime the wait baseline Follow-ups to the restore-record seed, from independent verification: 1. The runtime-controller spawn flow (createTerminal background creates — headless `orca serve`/CLI — and pane splits) never consumed restore payloads, so the exact orchestrator-blindness this fix targets survived on the topology that needs it most. The extraction now lives in one helper called from both spawn choke points (renderer pty:spawn and the controller flow); the runtime's empty-record guard makes overlapping seeds a no-op. 2. The throttled per-PTY wait scanner starts with a null baseline, so a permission prompt visible only in seeded HISTORY read as newly gained on the first benign live chunk and stamped waitBlockedAt "now". Seeding now primes the scanner baseline from the seeded tail without stamping; only a signal appearing in genuinely new output counts. 3. Cap re-anchoring accepts \r as well as \n (newline-free CR-redraw streams), consuming a full \r\n pair so the seed does not start with a phantom blank line. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
0edefde989 |
fix(runtime): reject leaf terminal sends only on controller-proven PTY absence (#12578)
* fix(runtime): reject leaf terminal sends only on controller-proven PTY absence orca terminal send to a leaf whose ptyId no provider in this process owns was a silent no-op reported as success: the graph mirror answers writable=true, every provider write to an unknown id is accepted fire-and-forget, and bytesWritten is computed from the payload rather than delivery. sendTerminal and sendTerminalAgentPrompt now consult a controller liveness probe when the provider does not synchronously know the id (hasPty), and throw terminal_not_writable only on an exact false — unknown liveness, probe errors, SSH/remote scopes, and probe-less providers never reject (#12393's rule: null is not absence), so a restored daemon session still accepts writes before its pane remounts. Push-on-idle orchestration delivery gains the same gate so a proven-dead leaf keeps its messages queued instead of marking them delivered into a void. The pty controller now exposes probePtyLiveness, routed like write: a provider probe is preferred, the in-process local provider's refusal is authoritative (sole owner), and remote-scoped or SSH ids without a probe answer null after awaiting the cold-start daemon swap. Proven-absent verdicts cache 15s per ptyId with in-flight dedupe, superseded the moment the provider re-learns the id. * fix(runtime): arm one probe-deferred delivery continuation per pty Review (GPT verifier) confirmed: triggers arriving during one in-flight absence probe each attached a continuation to the deduped probe promise, and since Claude-target delivered_at stamps only after the delayed Enter, every continuation re-read the same unread rows — double payload injection and two armed Enters. Single-flight the deferred continuation per pty; the one armed continuation re-reads fresh rows when it fires, so nothing is lost, and the guard clears on settle so later triggers defer again. The narrower pre-existing 500ms sync-path window is unchanged and out of scope. * fix(runtime): single-flight the whole orchestration delivery window per pty The probe-continuation guard cleared at probe settle, but Claude-target delivered_at stamps only in the delayed-Enter callback ~500ms later — a trigger landing in that gap armed a fresh probe cycle, re-read the same un-stamped rows, and re-injected the payload. The identical window existed on the pure sync path pre-PR (two triggers within 500ms double-deliver). Hold a per-pty delivery-in-flight flag from before the payload write until delivery settles: entry-checked before reading unread rows, cleared through one settle point covering the failed write, the sync-stamped coordinator and Cursor branches, any sync throw, and the delayed-Enter callback on submit, refusal, and throw alike. A trigger arriving mid-flight is not dropped — it parks the latest leaf per ptyId and re-runs delivery once on settle, so rows inserted mid-flight deliver without waiting for the next idle event. The probe single-flight stays; the new guard subsumes its post-settle gap, and no trigger site bypasses it. Both strengthened tests are red on the previous commit (first subject injected twice) and green here: in-window re-trigger on the probe path and sync-path double-trigger each deliver the first batch exactly once, with the parked second row delivering alone after settle. * fix(runtime): retire the armed delivery Enter on pty exit; guard fire-time on current state Two variants of one root cause — the delayed-Enter callback outliving the session it was armed for: 1. Cold restore respawns under the same session id. onPtyExit never cancelled the armed Enter or the in-flight delivery state, and onPtySpawned flips the same leaf writable again — so an exit + same-id respawn inside the 500ms window let the stale callback inject \r into the replacement session and stamp rows it never received, then settle against a newer same-id flight. 2. Graph resync replaces leaf objects, so onPtyExit flips writable=false only on the current replacement; a callback trusting its closed-over snapshot still read writable=true and fired after exit with no respawn. The flight record now carries its armed Enter timer and serves as settle identity: onPtyExit clears the timer and drops the flight and any parked re-delivery without stamping (rows stay unstamped and re-deliver on the replacement's next idle — the existing contract), and settle no-ops unless its own flight is still current, so a stale settle can never clear a newer same-id flight or flush its parked trigger. At fire time the callback re-resolves the leaf by key and requires the same ptyId binding and current writability instead of reading the closure snapshot. All three regressions are red on the previous commit: same-id respawn saw \r plus a false delivered_at stamp, exit leaked the flight and parked state, and the orphaned-snapshot resync variant fired Enter after exit. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
7e66467a60 |
fix(terminal): truthful handle liveness + no forked resume tabs for hidden restorable panes (#12574)
* fix(runtime): report terminal handles disconnected on controller-proven PTY absence leaf.connected mirrors the renderer graph (ptyId !== null), so a restored surface whose PTY died with a prior process was listed connected/writable forever with empty title/lastOutputAt/preview — the exact signature automation saw on run6 workspaces after a restart. listTerminals now threads the controller inventory it already fetches into buildTerminalSummary and demotes only on proven absence, only for locally-scoped ids; unknown liveness and SSH/remote scopes never demote, and no session or pane is retired. * fix(terminal): stop forking hidden restorable panes into replacement resume tabs paneWillConnectOnActivation still assumed the pre-keep-alive mount model, but every non-parked tab of the active worktree mounts and connects hidden at 0x0. Activation therefore appended a replacement resume tab per non-group-active agent pane and handed it the sleeping record, stranding the hidden pane as a bare shell — or forking two live surfaces onto one provider session when the old PTY survived in the daemon. The predicate now answers "will mount and connect": any non-web-mirror tab of the active worktree qualifies; non-active worktrees still answer false so background wake keeps its append-based resume. Contract change: reverses the hidden-tab expectation from #6800, whose premise (hidden panes never connect) no longer holds; that test is updated in place. * test(terminal): pin the remote-scope exemption and the web-mirror ownership exception CodeRabbit flagged both exclusions as untested: a remote-runtime-scoped leaf absent from the local inventory must stay connected (its inventory lives on the remote host), and a web-mirror tab must not own sleeping-session recovery (it never mounts a local pane), so the appended replacement remains its correct resume path. * fix(terminal): rescue just-spawned ptys from absence demotion; unpark panes owning sleeping records Review (GPT verifier) confirmed two gaps: - listTerminals demoted a live just-spawned PTY when listProcesses snapshotted before session registration (the sweep's hasPty rescue is leaf-gated), and federation reads one connected:false as exited. The summary's proven-absence check now also consults the provider's sync hasPty. - Ordinary per-tab cold parking (30s hidden) kept a non-group-active pane unmounted, so a sleeping record it owns under the new ownership predicate could not cold-restore until the user revealed the tab. Per-tab parks now exempt panes owning a sleeping-session record; worktree-level parks are untouched (they clear on activation). * fix(terminal): reconcile the daemon session cache on inventory; scope the park exemption to consumable records Round-2 review confirmed two holes in the round-1 fixes: - DaemonPtyAdapter.hasPty is cached activeSessionIds membership, and a successful listSessions never removed ids the authoritative inventory omitted — an exit missed while the socket was down kept hasPty true forever, and the new spawn/list-race rescue would trust it, reopening connected-forever for that pty. listProcesses now drops pre-request cached ids the inventory does not list alive (ids spawned mid-flight are snapshot- protected). - The park exemption covered records a pane can never consume (automaticResumeBlockedBy, passive-completed evidence), pinning hidden panes mounted indefinitely. The exemption now lives in sleeping-record-park-exemption.ts and requires a consumable record. Also pins the web-mirror replacement's resume claim and startup command (CodeRabbit round-2). --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
afd765d5a8 |
fix(runtime): make sibling-workspace terminal-path resolution an explicit client opt-in
(cherry picked from commit
|
||
|
|
999e3a3a6d |
feat(sidebar): link Linear issues from Edit Worktree Details (#12380)
* feat(sidebar): link Linear issues from Edit Worktree Details The Issue field only accepted GitHub numbers, so a workspace tracking a Linear issue had no way to say so from the dialog — the link could only be set at creation time or through `orca worktree set --linear-issue`. Replaces the field with one provider-aware row: a chip suffix inside the input selects GitHub or Linear, and pasting a URL flips the chip to match. A bare key never steers the provider — Linear and Jira issue keys are byte-identical in shape, so shape alone cannot decide one. One issue per workspace. A changed field displaces the other provider's slot and the row names what Save is about to unlink. GitLab and Jira links are left alone: the row cannot display them, and nothing else in the UI could restore one it dropped. - Folder workspaces read-only (their link is creation-time only) - Remote runtimes assert the capability before writing or clearing, since `worktree.set` parses in strip mode and would silently drop the keys - `updateWorktreeMeta` now reports failure so the dialog can stay open instead of closing over a save that refetch reverted - Parses are length-bounded — `matchGitHubItemPath` strips trailing slashes with an unanchored regex that is quadratic on a large paste * fix(sidebar): respect one-issue-per-workspace rule conditionally Only clear displaced issue links when they actually existed, preventing unnecessary Linear keys in GitHub-only workspaces. Skip comment updates when unchanged to avoid workspace reordering. Add accessibility to displacement messages and improve folder workspace error handling. * fix(sidebar): resolve workspace ambiguity and improve Linear issue linki The same workspace ID can exist under multiple hosts — the owner index reports this as ambiguous rather than guessing. Dialog callers now pass their repoId so lookups are unambiguous. Linear identifiers without an org key are resolved across all workspaces (not just the active organization). Added race-condition protection for async issue lookups and better change detection to avoid clearing work-item titles when re-saving an identifier in different spelling. |
||
|
|
88549619d2 |
feat(sidebar): inline SSH reconnect control on workspace cards (#12396)
* feat(sidebar): inline SSH reconnect control on workspace cards Replaces the blocking SshDisconnectedDialog with an inline pill in the workspace card title row, and unifies the SSH connect vocabulary across the sidebar card, terminal overlay, host-header menu, and status-bar row. - new src/renderer/src/ssh/ modules: typechecker-total status predicates (recoverability), a shared in-flight connect registry, the promoted UI connect timeout, and the shared connect verb table - WorktreeCardSshHostControl: one 16px pill shape for every state, icon-only in compact/new card modes, passive glyph for connected/null/removed hosts - migrates the four duplicated status predicates to the shared module - deletes SshDisconnectedDialog (and its window-capture Enter handler) * fix(ssh): address review round 1 on the inline reconnect control - reconnect surfaces get a 180s UI connect fence instead of the composer's 20s: main allows 120s for an interactive passphrase before the 30s connect even starts, so the short cap toasted "timed out" and ran the stale-metadata resync against a host that was about to connect fine - carries the existing es/ja/ko/zh translations onto the shared connect verbs (they were en-only, regressing four locales) and drops the dead SshDisconnectedDialog key namespace - migrates the three remaining copies of the connecting predicate (SshStatusSegment, SshTargetRow, external-automation-source-availability) - SshTargetStatusRow and SshTargetRow now join the shared in-flight registry, so a connect started on one surface disables the others immediately - drops the per-card aria-live region that duplicated the button's own label * fix(ssh): address review round 2 on the inline reconnect control - guard sshTargetRemoved on isRuntimeOwnedSshTargetId: runtime-owned targets are filtered out of ssh:listTargets, so absence is not evidence of removal (every ephemeral-VM card otherwise read "SSH host removed") - drop the card-root SSH dim: an ancestor opacity composited the control's destructive tint and spinner down to an illegible alpha - aria-disabled instead of disabled while connecting, so the pill stays pointer-reachable for its tooltip and focus - stop Enter/Space propagation so WorktreeList's container key handler does not steal activation - prefer a live connection over a stale removal tombstone - reclaim title width in labeled mode (no leading icon, no min-width floor) - register composer connects in the shared in-flight registry * temp checkin of files * fix(ssh): hold connect lock for backend request, not UI timeout Replace manual begin/end pairs with trackSshConnect wrapper that holds the lock for the full backend request duration. Previously, the lock was released after the UI timeout fired, even though the backend was still dialing — a second click on any surface for this host would trigger a second connect and a duplicate credential prompt on passphrase-gated targets. The wrapper survives unmount, unlike a finally block in a component handler. * fix(ssh): scope connect locks to their acquisition, not target A tracked request settling after its lock was cleared (via reset or explicit end) must not unlock a newer connect on the same target. Lock IDs ensure only the owning acquisition releases, preventing stale settlements from clearing active locks. |
||
|
|
1816d3eee2 |
test(terminal): pin IME chord survival across mid-composition re-renders (STA-3291) (#12357)
Busy panes render mid-composition (title updates drive renders), and the modified-Enter chord owner must survive those renders or held-modifier CJK input leaks newlines. Wires the hook TerminalPane-shaped (dep objects rebuilt per render) and interleaves re-renders through hold, composition, and auto-repeat; fails under pre-fix churn wiring (verified: redispatch not absorbed), passes with memoized actions. |
||
|
|
e138d28fa6 |
Fix Linear filter chips showing UUIDs after dropdown closes (#12564)
Fetch metadata when filters are selected, not only while the popover is open, so chip labels remain readable after closing the dropdown. |
||
|
|
bc1feb5a4f |
Add search by name, project, and prompt for automations (#12561)
* Add search by name, project, and prompt for automations Split the monolithic automations page into focused modules: extract dialog logic, list panel rendering, search functionality, and utility helpers into separate files. Introduce deferred search matching to keep the input responsive, with proper bounds checking to reject oversized pastes. The page stays unfiltered when search is inactive or too large, preserving the original list view in those cases. * fix(i18n): add missing automation search localization keys Sync en.json keys used by AutomationListSearchField and the no-matches empty state so static analysis localization catalog check passes. * Localize remaining automation strings and optimize search - Add 12 i18n keys for automation labels, counts, and usage display - Extract AutomationPaneTab and SelectedExternalRunPage types to shared automation-page-state module - Optimize search fingerprint by truncating prompts to indexed prefix for bounded performance per tick - Improve escape-key handling in search field to clear input before blurring - Remove deprecated getAutomationListSearchQuery function |
||
|
|
4734428654 |
fix(i18n): stop the repair policy de-localizing CJK UI labels
The build-time repair layer carried overrides that rewrite already-correct CJK values back to English. Most are inert against today's catalogs but fire on the next regeneration, so they read as latent regressions rather than policy: - zh workspace status picker (Play/Flag/Zinc/Rose/Emerald/Amber/Violet/Sky/ Blue/Neutral) and `sheet`/`page` were pinned to English while every sibling option, and ko/ja/es, stay translated — half a Chinese picker. - The zh `蓝色的`/`琥珀色`/`中性的` phrase fixes correctly flagged the adjectival 的 form but replaced it with English instead of the bare color noun. - ja `Play` was pinned to English though the catalog already reads 再生. - A value-wide zh `Open: '进行中'` mapped every "Open" to "in progress", including the button that opens an MCP config file. "Open" is a verb (打开) on buttons and a state (开放) beside 已关闭, so no single mapping fits. Catalog corrections in the same area: - The GitHub/PR state picker key override read 진행 중 / 进行中 for ko and zh while ja already had the correct オープン; now 열림 / 开放, matching 닫힘 / 已关闭 on the sibling entry. - The terminal cursor-color group is the on-screen cursor, not the Cursor editor; ja already had カーソル, ko/zh now get 커서/光标 instead of "Cursor". - Tailwind swatch labels 天空 (the sky) and 锌 (the metal) do not read as colors; now 天蓝/锌灰, and ja 空 becomes 空色. - The ko disk-usage heading was pinned to bare "Space" while its own description says 저장 공간; both now use 저장 공간. Two policy tests pinned the Play de-localization. They diagnosed the input correctly — 玩 / 遊ぶ are wrong for a play icon — so the expectations move to 播放 / 再生 rather than English. Destructive drift (localized -> English on regeneration) drops from 40 to 21 for zh, 6 to 5 for ja, and 5 to 3 for ko. What remains is deliberate: search qualifiers, path and filename literals, and product names. |
||
|
|
cc9eb9e572 |
fix(i18n/ko): standard loanword transcription and terminology fixes (#8816)
Cherry-picked the unambiguous subset of #8816: - 디렉토리 -> 디렉터리 and 쉘 -> 셸, the standard loanword transcriptions - Milestones: 이정표 (a signpost) -> 마일스톤, the Linear product noun - Permission granted: 허가 (a licence) -> 권한 - "privacy envelope" rendered as 봉투, a paper envelope - commentTooLarge: 너무 커서 (too bulky) -> 너무 길어, across 9 composers The rest of the PR is left out: 83 leaves are agent -> Agent recapitalizations that fight the catalog convention, 17 are reverted by the repair policy, and several change meaning (viewed -> 읽음 "read" on the GitHub file checkbox, which means seen; "resolve PR base" read as fixing a problem; 차이점 -> diff, which also breaks ko search recall). Co-authored-by: tykimseoul <tykimseoul@gmail.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
1aa5e7f899 |
fix(i18n): localize the remaining pull-policy and PR-action reasons (#5640)
Most of #5640 landed independently, but 12 strings were still falling through to English: the ko "Diverged" pull-policy notice, and the ja source-control primary-action blocked reasons. Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
174011dd1e |
fix(i18n): format relative times with the configured UI language (#8662)
Eight renderer call sites built Intl.RelativeTimeFormat(undefined, ...), which resolves to the OS locale, so relative timestamps rendered in Korean on a Korean-locale machine even with the UI language set to English. Unlike DateTimeFormat, RelativeTimeFormat emits language words, so it must follow the UI language. Rebased onto main: the formatter now resolves through getIntlLocale() (#12105) rather than i18n.resolvedLanguage, so the synthetic plugin<hex> resource language cannot reach the constructor and throw. GitHubItemDialog and PullRequestPage no longer own their formatter — main moved it into work-item-state-presentation, which is converted instead, along with the newly added site there. Co-authored-by: moseoh <azqazq195@gmail.com> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
705e17a2a0 |
fix(i18n): correct semantic errors in Korean UI copy (#11169)
Nine ko values said something other than the English source. The riskiest is
SourceControl.6d7f2a47e5 "Discard folder", rendered as 폴더 삭제 ("delete
folder") next to a sibling delete-untracked action. Others: "Only branches Orca
named itself" read as "branches named Orca"; "staged changes" as 단계적
("phased"); "first-party cloud" as the mojibake 1方클라우드; "discard the
deletion" as "the deletion is deleted"; "Stage all changes" as a sentence
meaning "prepare"; and Recipes as 조리법 (cooking recipes).
EphemeralVmsPane.skillTitle is dropped from the PR's test and override — the
key was renamed to cloudVmSkillTitle on main, so the assertion would resolve to
undefined.
Co-authored-by: ShinSungkyu <kxu4583@naver.com>
|
||
|
|
d4f2ae1454 |
fix(i18n): correct ja Push/Pull button translations (#12301)
The source-control primary action rendered 押す ("press") and 引く ("pull a
physical object") for Push/Pull. プッシュ/プル match the sibling フォースプッシュ
and 同期 labels. A guard test pins both so bootstrap re-translation cannot
silently regress them.
Co-authored-by: Iris-Fla <103801589+Iris-Fla@users.noreply.github.com>
|
||
|
|
38490ee6c1 |
fix(i18n): localize browser load-failure and certificate copy (#10672)
The browser.loadFailure.* keys were still raw English in es/ja/ko/zh. en.json is
untouched; every {{value0}} token and the Orca/HTTPS brand terms are preserved.
13 of the zh keys were already covered by #12368, so only the 6 it did not
reach are taken here.
Co-authored-by: MumuTW <42820974+MumuTW@users.noreply.github.com>
|
||
|
|
1b1743c3e0 |
fix(i18n): translate remaining zh.json strings (#12368)
185 zh values were still verbatim English, plus the 6 VoiceMicrophoneSetting keys were missing. Placeholders, "X of Y" counts, and key ordering are unchanged; the only edits to already-translated values are punctuation. Applied at key level rather than as a branch merge — the PR was cut from an older base and conflicted only on JSON context, with no value drift against main. Co-authored-by: 闲人 <38777313+qiuyongjin@users.noreply.github.com> Co-authored-by: jake <qiu5630@163.com> |
||
|
|
5adc5d06c8 |
fix(i18n): pin the Orca Mobile "New" badge override and correct ja
The badge value is pinned in locale-key-overrides.mjs, so the ko/zh fix from #10664 would have been reverted by the next catalog repair. ja carried the same defect — 新規 reads as "create new" — and is corrected alongside. |
||
|
|
1cc84b9f6f |
fix(i18n): correct the ko and zh Orca Mobile "New" badge (#10664)
SidebarNav.c86d83b5c3 is the onboarding pill rendered beside Orca Mobile, so
"New" marks a new feature. Both locales had translated it as a create action —
ko 새로 만들기 ("create new"), zh 新建 ("create new") — which reads as a button.
The PR's other hunk (zh GH PR) already landed on main and resolved to a no-op.
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
|
||
|
|
eed74724ac |
fix(i18n): localize automation contextual tour (#12270)
The shared Automation tour copy was rendered without passing through translate(), and the overlay surface hardcoded its default Next and Done labels. Copy is keyed off the step id rather than its position, so inserting a step ahead of them cannot shift the text onto the wrong step. Co-authored-by: 5Hyeons <ohs2251@naver.com> |
||
|
|
6bf34421d2 |
fix(i18n): localize the Link Routing description in browser settings (#9444)
The description was assembled from a bare template literal, so it stayed
English under every language pack. It now lives in the catalog as two entries —
description with a {{shortcut}} placeholder and descriptionBase for the
invert-on variant — following the copy-module pattern from #10991.
Co-authored-by: 조재중 <126754298+m-a-king@users.noreply.github.com>
|
||
|
|
4e8f136052 |
fix(i18n): localize the integration card status pills (#12208)
Every card under Settings -> Integrations built its status pill from bare literals inside the JSX call, so the pill stayed English beside a translated description. audit-localization-coverage.mjs inspects JSX attributes and object properties, not conditional expressions handed to a prop, so the gate stayed green while the strings shipped untranslated. The three token-configured cards share tokenProviderStatusLabel(); Gitea passes optional: true because it works read-only without a token, which is why its unconfigured state reads Optional setup rather than Not configured. Co-authored-by: Evgenii <kumiro@me.com> |
||
|
|
439a8c46cf |
fix(plugins): let language packs translate plugin chrome (#12455)
protectedTranslation refused every language-pack key under auto.components.settings.plugin*, which caught 104 keys that carry no trust meaning — section titles, empty states, Refresh, Add path. A 35-path exact allowlist opens those while consent, provenance, and every *Failed string stay protected; anything new stays protected until it is added deliberately. PluginsSettingsSection.experimental is held back from the contributed allowlist: the "Experimental" chip is a trust badge, which the module's own boundary comment places out of scope. Co-authored-by: Evgenii <kumiro@me.com> |
||
|
|
17d88e098d |
fix(i18n): format the stats date with the selected UI language (#12105)
formatTrackingSince() used the OS locale rather than the language chosen in Settings. getIntlLocale() resolves the active i18n language to a BCP-47 tag, mapping the synthetic plugin<hex> resource language that Intl rejects back to the pack's real locale. Co-authored-by: Evgenii <kumiro@me.com> |
||
|
|
02afd3557c |
fix(sidebar): keep the toolbar labels in the selected UI language (#11889)
SidebarToolbar is a React.memo boundary whose props are shallow-equal on a language switch, so without its own useTranslation() subscription it kept the English copy it rendered at boot — the persisted locale is applied asynchronously, after the lazy catalog loads. Co-authored-by: 5Hyeons <ohs2251@naver.com> |
||
|
|
2548b816c0 |
Keep the app responsive when security software slows process creation (#12217)
* fix(ports): keep the app responsive when security software slows process creation Orca ran the workspace port scan's probe commands (lsof/ps on macOS, netstat + powershell.exe on Windows) directly in the Electron main process. libuv performs process creation inline on the calling event loop, which in the main process is the browser UI thread, so an endpoint-security module hooking CreateProcessW froze the whole window for the length of the spawn. The same stall also produced a false diagnosis: the 4s command watchdog was armed before execFile (local-workspace-port-scanner.ts:389 -> :410), so its deadline had already passed by the time the command started. Every scan on a hooked host reported a command timeout, tripping the 60s -> 5min backoff and the "Port scanning is temporarily paused after a command timeout" banner even though the commands themselves were healthy. Probe commands now run on a lazily created, unref'd worker thread with FIFO one-at-a-time dispatch, and the watchdog is armed after execFile returns so it measures the command rather than the spawn. Node's own execFile timeout kill (killed: true) is classified as a command timeout, keeping the backoff working for genuine hangs. A scan that observes a stalled spawn skips its optional metadata commands for that cycle, capping a hooked-host scan at roughly one stall instead of three. Closes #11161 * fix(ports): keep advertised URLs when a stalled spawn skips port metadata Review follow-up on #11161. The stalled-spawn early return handed scanWorkspacePorts raw ports with no cwd/commandLine, so every port failed attribution and reconcileAdvertisedUrls told the watcher each worktree's listeners had vanished. shouldEvictAfterScan then deleted every cached advertised URL and broadcast a removal event; those URLs are only ever captured from live PTY output, so the dev-server link was gone until the server restarted. The scanners now report metadataAvailable, and reconciliation is skipped for a scan that never gathered attribution evidence. The skip is also no longer self-perpetuating: on an EDR-hooked host every spawn stalls, so gating purely on the current scan's spawnMs made every port permanently external (Stop refused with 'Only workspace-owned local processes can be stopped here.'). Metadata is now re-probed on the scan after a skip, matching what the comment and test name already claimed. Co-authored-by: Orca <help@stably.ai> * test(windows): stop a temp-dir lock from failing the CLI launcher smoke test The native launcher assertions passed on windows-latest, but teardown's rmSync raced Windows' release of the image handle on the exe the test had just executed and threw EPERM, failing the job. Cleanup now retries and, on Windows only, tolerates a residual lock code instead of reporting it as a launcher regression. Co-authored-by: Orca <help@stably.ai> * fix(ports): scope the metadata skip away from attribution-dependent scans The metadata skip was a process-wide parity flag, so Stop and the localhost-label allowlist could land on a degraded cycle and reject a port the panel had just shown as workspace-owned. Give those callers an explicit requireMetadata option, and carry the previous cycle's listener metadata forward so a skipped background scan no longer republishes workspace ports as external. Also pin the watchdog ordering: the stall in the execution test was shorter than the watchdog budget, so a watchdog armed before execFile still passed. * build: guard worker-thread entries against electron imports (#11161) Electron's module is not registered on worker threads, so require("electron") throws "Cannot find module 'electron'" inside a main-process worker and kills it at startup (verified on Electron 43.1.0). plain-node-entry-guard covered only forked plain-Node entries, so the five worker entries relied on hand-written "must stay electron-free" comments. The port-scan probe worker is one import away from port-scan-command-client.ts, which deliberately contains require('electron'). A violation there fails closed at runtime while every unit test still passes, because the client's require is try/caught on the main thread. Covers stt-worker, warp-theme-parser-worker, session-scanner-opencode-sqlite-worker-entry, main-thread-hang-watchdog-entry and port-scan-command-worker-entry. The scan is transitive over the emitted chunk graph, so a shared chunk that reaches electron is caught too. Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * test(windows): retry teardown for main's duplicate-PATH launcher fixture Main's new csc-compiled harness runs an exe from the temp tree, which is exactly the image-handle/AV lock the merged-in removeFixtureTree retry exists for; its bare rmSync would report a teardown lock as a launcher failure. Co-authored-by: Orca <help@stably.ai> * test(ports): pin the packaged-asar worker entry path resolveWorkerEntryPath's packaged branch never runs in dev or e2e, so the path construction had no coverage. Split the electron read out of it and unit-test both layouts. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
e1071f59e9 |
Say why an update install failed instead of stalling for three minutes (#12224)
Surfaces the real install-failure cause instead of letting a failed elevation stall silently, and keeps the reconnect wait inside its total budget by recomputing the remaining time after each awaited RPC. Relates to #11906 — this fixes the observability half. The functional half (a .deb/.rpm host cannot elevate and can never self-update) is unchanged, so the issue stays open. |
||
|
|
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> |