mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 10:27:33 +00:00
de3da487fa6bbfde6f9e4b53fba7672fada5322c
16002
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7fad71e448 |
fix(worktree): skip retirement backfill on every non-local host (#15023)
The backfill guard tested repo.connectionId, but a runtime-owned repo carries executionHostId with no connectionId, so it read as local. The scan then walked this machine's workspace and agent-transcript directories and filed the result under the runtime host's namespace — retiring names never used there while missing the ones that were. Guard on the execution host id instead. Ongoing retirement was already correct for these repos; only the one-time historical seed was wrong. |
||
|
|
1aa51f6914 |
fix(computer): preserve accessibility value types (#15031)
* fix(computer): preserve AX value types * fix(computer): preserve exact integer values * fix(computer): preserve exact integer exponent forms |
||
|
|
47a53b694b |
fix(ui): ignore IME composition Enter in the comment composer and replace field (#15057)
The keydown that exits a CJK composition carries isComposing: true (UI Events 3.6.5, row 5) and, when the IME is processing key input, keyCode 229 (7.3.1). Two handlers acted on it: - right-panel-comment-composer: Cmd/Ctrl+Enter posted the comment while the last syllable was still composing, so it went out truncated. - RichMarkdownSearchBar: Enter ran replace-current, mutating the document from a keystroke aimed at the candidate list; Escape closed the bar instead of letting the IME cancel the composition. Same in the find field. Guard all three with the existing isImeCompositionKeyDown helper, matching the rename and title inputs already on it. The held modifier does not change ownership: a composing Ctrl+Enter is still the IME's keydown, which is why useImeEnterGestureOwnership also owns it and only lets the post-compositionend redispatch chord through. Co-authored-by: BaeTab <bhwoo48@gmail.com> |
||
|
|
876e5b88a4 |
fix(ime): scope the composition route and deferred newline to owned sessions (#15056)
Two ways a terminal composition went wrong, both from state that was not scoped to the session it belonged to. The route called preventDefault() before checking whether it owned the session. The patched xterm treats that cancellation as "someone else will deliver this" and skips its own triggerDataEvent, so a route installed mid-composition — the connection effect re-runs, a reconnect swaps the transport, StrictMode remounts — suppressed the insertion and then returned without delivering anything. The commit vanished. Ownership is now decided first; preventDefault stays for sessions the route does own, including the ones it deliberately drops after a transport swap, since that drop is its decision to make. Pending-composition state was a bare per-element count, so a waiter could only ask "is anything composing", not "is what I was waiting for still composing". A composition the user starts after pressing Enter is behind that Enter, not in front of it, but it kept the count non-zero and held the newline anyway — `한` Enter `글` reaching the terminal as `한글\n`. The count is now reference-counted per session id, and the deferred send snapshots the sessions open when it starts and waits only for those. Reference counts rather than a set, so two overlapping routes owning the same session cannot clear each other's pending state. Co-authored-by: yeongjunyoo <47925973+yeongjunyoo@users.noreply.github.com> |
||
|
|
6387e5b8d3 |
fix(folder-workspaces): keep the broken-folder marker when a host sends a new reason (#15027)
FolderWorkspacePathStatus is cast, not decoded, off the runtime RPC wire -- runtime-rpc-envelope declares result: z.unknown(), so unwrapRuntimeRpcResult hands back whatever the host sent. Both title and description switch on status.reason with no runtime guard, so a newer host publishing a fifth reason matched nothing and returned undefined. FolderPathStatusIndicator's `!title` check then dropped the whole indicator, and a broken folder workspace rendered as healthy -- worse than the blank toast #15002 fixed, because there the warning was empty and here it is gone. Guard before each switch, the shape #15002 landed. A default: arm is not available: the type-aware config sets allowDefaultCaseForExhaustiveSwitch:false and rejects one with switch-exhaustiveness-check. Extract that guard into isHandledWireDiscriminant instead of hand-writing a third and fourth copy, and move #15002's two bespoke guards onto it. It takes unknown and checks typeof before Object.hasOwn -- hasOwn coerces its key, so a host that widened the field to an array sends ['missing'], which a hasOwn-only guard admits before the switch drops it straight back out. That was the P1 found in review on #15002; one implementation makes it structural instead of tribal. An unrecognized reason gets its own copy rather than reusing 'unavailable'. The unavailable remedy -- "Check the runtime or SSH connection and try again" -- is a false lead here: the host did check and reported the folder unusable, so retrying and inspecting a healthy connection wastes the user's time. Update Orca is the real remedy. Adding a fifth reason still fails typecheck in two places: TS2741 on the Record and TS2366 plus switch-exhaustiveness-check on both switches. |
||
|
|
7c798907c5 | fix(skills): harden cross-host bundle installs (#15000) | ||
|
|
04e7f5c805 |
fix(cli): relativize absolute POSIX paths against UNC worktree roots in WSL (#11406)
* chore: ignore worktrees directory * fix(cli): relativize absolute POSIX paths against UNC worktree roots in WSL * fix(cli): prevent double-prefixing UNC paths in WSL path normalization * fix(cli): gate the WSL path rewrite on a UNC worktree root WSL_DISTRO_NAME is also set for a plain Linux CLI inside the distro, where worktree roots are POSIX; prefixing there stranded every absolute path. Rewrite only when the root really is a WSL UNC path, and cover the legacy wsl$ alias, cross-distro paths, and the non-WSL case. * test(cli): pin WSL_DISTRO_NAME absent for the whole file-path suite Contributors run this suite inside WSL, where the inherited distro name would flip the rewrite on for every POSIX-root case. * fix(cli): never rewrite a Linux path that contains a backslash Backslash is a legal Linux filename character but a separator once the path reads as UNC, so `a\b.ts` relativized to `a/b.ts` — a different file. Such a path has no UNC spelling; let it fail the match instead. * test(cli): pin the WSL rewrite's negative space Sibling-prefix roots, Linux-tail case sensitivity, and Windows drive-letter workspaces all passed only by construction. * test(cli): pin the distro-case fold from the CLI side The negative-only case passed identically with the fold broken. * fix(cli): name the WSL distro from the invocation cwd when the env is absent WSL_DISTRO_NAME only reaches the CLI if interop forwards it across the PowerShell bridge, which nothing in the launcher guarantees. ORCA_CLI_CWD is set explicitly and its UNC form already names the distro. * test(cli): match the launcher's real cwd spelling and fix an over-claim wslpath -w emits a backslash UNC path; the fallback test now uses that shape. The aliasing test's comment described a state the || guard makes unreachable. * refactor(cli): spell the WSL rewrite with the shared toWindowsWslPath helper src/shared/wsl-paths.ts already owns "absolute Linux path in a known distro -> its Windows form" and has five production callers; the handler hand-rolled a fourth copy of the UNC template. Behavior is identical under the UNC-root guard, and passing distro as a real argument makes the null check a compile error rather than an untested branch. * test(cli): drop a WSL case that pins the guard shape and kills no mutant Deleting the distro null check left the case green — it asserts the same passthrough as 'does not rewrite when the CLI is not running under WSL'. The check is now enforced by the compiler instead. * chore: keep WSL path fix scoped --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
30b76e6edb | fix(orchestration): enforce task dispatch state invariant (#14961) | ||
|
|
453237cc57 |
fix(terminal): render the row tail the IME preedit overlay covers (#15014)
* fix(terminal): render the covered row tail inside the IME preedit overlay Closes #12545. Composing mid-line hid the character at the cursor for the whole composition. The preedit overlay is an opaque box anchored to the cursor cell, and nothing reaches the pty while composing, so those cells still held their characters — the box simply covered them. `CompositionHelper` now draws the rest of the row after the preedit inside the view, so the composition reads as inserted text pushing the tail right. Four details come with it: - The view is start-anchored while it carries a tail, so the preedit stays put and the pushed tail clips at the right edge; alone, `rtl` still keeps a long preedit's end in view. - It is themed from `options.theme` instead of the stock `#000`/`#FFF`, with any alpha dropped — the view masks the cells it draws over, so a see-through background would re-expose the very characters the tail stands in for. - The helper textarea syncs to the preedit's own bounds, so IME candidate dialogs anchor to the composing text rather than past the rendered tail. - A TUI can repaint the row under an open composition, so `updateCompositionElements` — which already runs on every render — re-reads the remainder and re-renders on change. A string compare adds no layout read. The tail is read with an explicit end column: the cacheable form of `translateToString` arms the line string cache's self-renewing idle-clear timer, and the composition path must own no timers. Geometry is not the cause. Two mature reference terminal implementations compose marked text into the grid rather than into a floating box, and both still blank the cells under it — one of them literally substitutes the marked characters into the row's character array before rasterizing. Moving off the overlay would not have fixed this report; rendering the covered tail is what does. The e2e arm asserts the invariant an opaque overlay owes the grid: it must render every committed cell its bounding rect covers. That is measured from the real rect against the real cell grid, so it fails on the unfixed build with `covers "하" / renders "가"`. Known limitation: the rendered tail is plain-styled while composing (theme foreground on theme background, no per-cell colors); colors return on commit. This is inherent to the overlay, and drawing the preedit into the cell renderer instead would be a far larger change. Co-authored-by: rayim <rayim@fxy.global> * test(e2e): assert the occlusion invariant, not the runner's cell width CI covered four columns where this machine covers two — 34.4px over an 8.43px grid against 12.3px over an 8px grid — so pinning the covered text verbatim pinned the font metrics rather than the behaviour. Assert instead that every committed cell the overlay covers appears in what it draws, which is the actual invariant and holds at any cell width. Still fails against main: covers "하" / renders "가". * fix(terminal): keep the rendered tail's spacing on the grid The composition view is white-space: nowrap, which collapses runs of spaces exactly like normal — it only suppresses wrapping. So a committed tail carrying padding drew its trailing glyph cells left of where the grid has them: measured in Chromium with xterm's own rule, twenty spaces plus a border rendered two cells wide instead of twenty-one. The visible case is Orca's most common IME context — composing inside an agent TUI input box, where the row is a prompt, padding, then a real border glyph the trim cannot drop. A stray border appeared a cell after the preedit while the real one stayed put. xterm sets white-space: pre on its grid rows for this reason; the view was only nowrap-safe while it held preedit text alone. The existing fixtures are all space-free, and the e2e invariant is that the overlay renders everything it covers — collapsing makes it cover less, so both stayed green. Pinned with a padded-row fixture. --------- Co-authored-by: rayim <rayim@fxy.global> |
||
|
|
8e9b5c908c |
fix(github): fail closed instead of running client git against a remote repoPath when the SSH provider is unregistered (#14945)
* fix(github): fail closed when the SSH git provider is gone getCurrentHeadOid and probeTrackedUpstreamBranches only routed through the SSH provider when one was registered. With connectionId set but the provider unregistered (dropped connection, not yet reattached) they fell through to client-side git with cwd pointing at the remote repoPath — on a machine with a same-named local path that silently answers for the wrong repository. getCurrentHeadOid feeds shouldHideMergedImplicitPR, so a wrong OID changes which PR the UI attributes to a worktree. Both now take their existing unknown path (null / probeFailed) instead, matching repo-default-branch.ts. Local and WSL routing is unchanged. * fix(github): preserve PR state when SSH probes fail * fix(github): keep failed SSH discovery unverifiable * fix(github): propagate SSH identity failures * fix(github): scope verified SSH identity probes * test(github): preserve tolerant resolver calls * fix(github): preserve indeterminate auth discovery * fix(github): isolate SSH repository probe generations * test(github): expose SSH probe generation in mocks |
||
|
|
7afce2ea41 |
fix(ssh): stop reporting a confirmed kill when the SSH provider is gone (#14977)
* fix(ssh): stop reporting a confirmed kill when the SSH provider is gone A detached relay PTY is designed to outlive the provider that addressed it (it ignores SIGHUP and ships with an unlimited grace), so "the SSH provider is no longer registered" is lost contact, never evidence the remote process stopped. Both stop primitives in the PTY controller returned `true` from that branch, and every caller downstream reported the fabricated success: the CLI printed "PTY killed.", worker-stop settled the dispatch as stopped, and — because the stop "succeeded" — the unstopped-PTY gate never ran, so worktree removal walked straight past a live remote agent. `kill`/`stopAndWait` now still tombstone the local lease but report an unconfirmed stop and record why, using the three-verdict vocabulary the worktree teardown gate already spoke (`live` / `unverifiable` / `exited`), promoted out of that module into `src/shared/pty-liveness-verdict.ts`. The close receipt, the CLI wording, worker-stop and the removal gate all read that verdict instead of inferring an exit from silence. The same rule fixes the mirror-image defect: the aggregate inventory only enumerates registered providers, so a dropped relay clears `connected` for every remote PTY at once. The sweep now separates the provider answering "absent" (an exit) from no provider being able to answer (lost contact), so worker-stop stops claiming `exited` from a disconnect. The `connected` wire field is unchanged in meaning and shape. * fix(orchestration): apply the same honesty to the federation stop path The federation host runs its own copy of the worker observation and stop logic, with the same two defects: `inspectRemoteAttachment` read a dropped relay's `connected: false` as `exited`, and `federationStop` settled the dispatch as stopped from a close it never confirmed — relaying a fabricated success all the way home to the coordinator. Two guards also had to move so the honest verdict does not become a new refusal. `federationRead` gated on `status !== 'running'`, which would have rejected a connected terminal the moment a stop lost contact with it; it now gates on `status === 'exited'`, which is equivalent for every pre-existing status given the two guards beside it. Local `workerStop` likewise still attempts the close when the verdict is `unverifiable` — losing contact is a reason to report the outcome honestly, never a reason to stop trying. The show observations now carry the reason alongside the status, so a bare `unverifiable` is actionable. Both are new optional fields. * fix(ssh): preserve unconfirmed stop verdicts across consumers * fix(ssh): use canonical live verdict wording * fix(ssh): refuse wrong-host teardown verification * test(orchestration): confirm worker release teardown * fix(orchestration): negotiate honest worker stop receipts * fix(agent-teams): fence uncertain teammate respawns * fix(ssh): avoid duplicate missing-provider teardown * fix(orchestration): preserve archives across release retries * fix(ssh): preserve verdicts across synthetic kill exits * fix(ssh): preserve liveness evidence across teardown * fix(agent-teams): replace panes only after confirmed stop * fix(ssh): distinguish host exits from relay loss * fix(ssh): narrow concurrent inventory verdicts * fix(orchestration): serve archives after uncertain release * fix(orchestration): expose unverifiable read liveness * test(ssh): align liveness assertions with verdicts * fix(ssh): preserve host scope across inventory failures |
||
|
|
b279f66c96 |
Include descendants in Pinned section when parent is pinned (#15035)
Descendants of pinned parents now appear in the Pinned section without individual pins. When an ancestor's pin state changes, reveal the active descendant to maintain focus and visibility. |
||
|
|
3bb87ff93b |
reland(shell): one portable Unix startup dialect, with both revert causes fixed (#15018)
* reland: portable startup-shell dialect, with the two revert causes fixed Relands #14863 (reverted by #14975) with fixes for both regressions the revert cited. 1. History GC deleted folder-workspace shell history. The live set was built from `getAllWorktreeMeta()` alone, but a folder workspace's PTY carries `folder:<id>` as its worktree id, so every live folder workspace looked orphaned. `getKnownWorktreeIdsForHistoryGc` now unions in `getFolderWorkspaces()`. Both consumers — the history-directory prune and the fish-history sweep — read that one set, so the fix covers bash, zsh and fish history alike. The directory prune had this gap since #1524; #14863 only widened its blast radius to fish files. 2. A copied Codex resume command aborted under `set -u`. Its leading clear statement has to test `$fish_pid`, and that unbound expansion takes the whole line — including the agent launch — down with it. Copied text runs in a shell Orca never spawned, so nothing can seed that variable first. The removal now rides on the agent itself as `env -u`, which needs no shell syntax and no expansion. Verified byte-identical under `set -u` in sh, bash, zsh, dash, ksh and fish. `env` cannot run the `cd` builtin, and a child `cd` would not move the agent, so the prefix is placed on the agent rather than on the whole `cd … && agent` chain. cmd and PowerShell have no nounset hazard and keep their clear ahead of the `cd`, which preserves `cd … && agent` — a failed `cd` still cannot launch the agent in the wrong directory. * fix(history-gc): stop three more paths from deleting live shell history Found by adversarial review of the reland. All three are the same class as the bug that caused the revert: a live set that is missing a category of real workspace, so the GC reads it as orphaned. 1. Profiles. The history root is `userData/terminal-history`, which has no profile segment, but the Store the GC consults is per-profile. So after a profile switch the live set condemned every other profile's history — and fish history, which lands in the user's own fish data dir, is shared by every profile on the machine. The live set now unions in the inactive profiles' worktrees and folder workspaces, read from their data files. A profile whose ids cannot be read reports the empty set rather than one that condemns real history. 2. No empty-set guard on the tree scan. `sweepOrphanedFishHistoryFiles` refuses an empty live set because it cannot be told apart from a store that failed to hydrate; the directory scan, which deletes more, had no such guard. A store that fell back to default state would have taken every worktree's bash and zsh history with it, across all roots including WSL. Four existing tests passed `new Set()` and relied on "empty means everything is orphaned" — exactly the behavior being removed — so they now pass a real live set. 3. Relay fish history. The relay isolates its history tree under its own root but wrote fish history into the shared fish data dir under the desktop naming, keyed by the CLIENT's worktree ids. On a machine running both Orca and a relay host, the desktop sweep deleted remote sessions' history once it went stale. Relay files are now `orca_relay_<hash>`, which the sweep's pattern deliberately does not match; the relay still deletes them by exact name when the worktree goes away. * fix(resume): enforce the env-removal invariants instead of documenting them Both found by adversarial review; both were unreachable from today's callers and silent if reached, which is exactly how they would survive to a caller that does reach them. - A pinned CODEX_HOME and the removal named the same variable, and `env -u` strips what the assignment just set — so the agent would have resumed against the real home and not found the session. The removal list now excludes any name the prefix pins, keeping the assignment authoritative as the old `clear…; CODEX_HOME=x agent` ordering did. Same fix in the git-bash twin. The PowerShell branch already clears before it assigns, so it was never affected. - Placement was keyed on the platform while the grammar it selects is keyed on the shell, so `platform: 'linux'` with `shell: 'powershell'` emitted POSIX `env -u` into a PowerShell line. PowerShell now routes to the PowerShell builder whatever the host, and the POSIX/cmd split below asks the shell rather than the platform. |
||
|
|
77ef6bb9ee | fix(terminal): verify agent prompt submission (#14962) | ||
|
|
cc74436b3c |
fix(skills): keep an unanswered skill root's last known skills instead of reporting none (#15015)
* fix(skills): keep an unanswered skill root's last known skills instead of reporting none
An aborted or shed root scan degraded to `{skills: [], unavailable: true}`. Every
consumer derives "installed" from the skill list, so an unreadable root read as
proof the skill was gone and an already-installed skill offered Install again.
- discovery: retain the last answered scan per root (5 min, LRU-bounded, dropped
on install/update invalidation and when a root answers as absent) and serve it
when a later scan goes unavailable.
- source inventory: an unanswered root reports `exists: true`, so check
`skippedReason` first rather than presenting it as a successful scan.
- useInstalledAgentSkills: when an in-scope root did not answer and the skill was
not found, say the status may be incomplete instead of a bare "Not installed".
* fix(skills): do not let install verification accept an unanswered root's retained scan
Retention makes discovery serve a stalled root's last completed scan, which is the
state before an install wrote. Verification read that as proof, so reinstalling a
skill into a root that stalled could report success without reading what it wrote —
turning a retryable false negative into a false positive.
A match now counts only when a root that actually answered reached the skill, so a
symlinked placement co-owned by a healthy root still verifies.
|
||
|
|
2fdaa10fd1 |
fix(terminal): give the composing-chord deferral an owner and a ceiling (#15017)
The chord held for a live composition waits with `fallbackMs: null`, so nothing but compositionend can end it. `sendTerminalInputAfterComposition` returned void, so nobody could stop it either: the two listeners it puts on the terminal element outlived the pane, and a later composition on that element flushed the stale chord. Return a disposer from the helper and put a sender in front of it that owns every pending chord, so blur and pane teardown drop them the way the Enter path already clears its state. The sender also bounds the wait — generous enough for a conversion candidate window, and it discards rather than sends, because a chord arriving mid-preedit is the corruption the wait exists to prevent. The Enter path needs none of this: its 200 ms fallback always runs, so its listeners cannot outlive it. Pinned so that stays true. Fixes STA-4476 |
||
|
|
81d7f9b24e |
refactor: split db.ts under 400 lines (#14979)
* refactor: split db.ts under 400 lines * rm plan * fix(orchestration-db): add safety guards to database operations Add status guards to UPDATE statements to prevent late operations from overwriting changes made by concurrent requests. Validate mutation results to surface silent no-ops. Extract circuit-break threshold, add transaction wrapping, and sanitize untrusted input. Bump schema version to v28. * Add transactional safety to dispatch and message operations Wrap dispatch failures and batched message updates with SAVEPOINTs to ensure atomicity and idempotency: - Dispatch failures now check status guards and roll back if the related task update fails, preventing partial state corruption - Message batches (across multiple 500-id chunks) roll back entirely if any batch fails, avoiding partial mutations - Add tests verifying idempotency and atomicity under failure conditions * Handle concurrent writes and improve transactional safety - Remote question answering: add classification check before and after UPDATE to safely detect concurrent modifications. Prevents false success when the UPDATE loses a race. - Transaction rollback: wrap in try-catch to prevent errors from masking the original failure. - Question thread reset: use status update instead of deletion to preserve message references. * test: add answer replay and race condition edge case coverage Add test cases for answer replay idempotency, conflict detection, and a race condition between concurrent answer updates in federation relay. Also verify local question state transitions during orchestration reset. |
||
|
|
d21f63e6fc |
fix(browser): keep cookie-import warnings readable from newer hosts (#15002)
* fix(browser): keep cookie-import warnings readable from newer hosts The import summary is cast, not decoded, coming off the runtime RPC wire, so a newer host can publish a warning code or undecryptable reason this build has never seen. Both switches then matched nothing and returned undefined, which rendered as a blank toast.warning(undefined). #14683 replaced the absorbing default: arm with case 'unknown' to satisfy switch-exhaustiveness-check, which forbids default: on an exhaustive switch. Guard before each switch instead: narrow the wire value against the handled set and route anything else to a generic message. The handled sets are keyed by the unions themselves, so a fourth member still fails typecheck here. * fix(browser): reject non-string cookie-import warning discriminants Object.hasOwn coerces its key, so the membership guards admitted any value whose toString() matched a handled variant. A host that widened reason to an array sends ['unknown'], which passed the guard and then fell straight back out of the switch -- the same undefined-to-toast.warning blank the guards exist to prevent. Take unknown and check typeof first, matching isTopLevelView and isTuiAgent. The Record membership sets still fail typecheck on a new union member. |
||
|
|
f660aaab0d |
Remap SSH leases per execution host, not globally (#15009)
* Remap SSH leases per execution host, not globally SSH lease leaf IDs are now remapped within their execution host partition, preventing silently empty tabs when the same tab name exists on multiple machines after restart. Also preserve folder workspace paths exactly as provided without trimming whitespace. * rm review file --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
8ca4ed945e |
feat(terminal): report execution host and listing scope in terminal list (#14973)
* feat(terminal): report execution host and listing scope in terminal list `orca terminal list` returned rows with no host identity and no statement of what the listing covered, so a scoped listing that saw nothing read as "nothing exists anywhere" — an agent reported a live remote worker dead. Each row now carries an optional `executionHostId` derived from the PTY id (SSH and paired-runtime ids embed their owner), and the result carries an optional `hostScope` naming the hosts covered and the known hosts skipped. Both are surfaced in `--json` and in the human-readable CLI output, where an absent field renders as `unknown` rather than `local`. Both row builders route through one resolver, so the rule lives in one place. * fix(terminal): preserve unverifiable host scope * fix(terminal): fail closed on unverifiable hosts * test(terminal): name unverifiable scope explicitly * perf(terminal): keep graph hydration host scans narrow * fix(terminal): reject blank foreign host owners * fix(terminal): validate inferred inventory hosts * fix(terminal): preserve paired folder host scope * fix(terminal): keep inventory host inference typed * fix(terminal): disclose paired folder hosts |
||
|
|
b36456007e |
fix(repos): never probe the client filesystem for a remote repo icon (#14947)
detectRepoFileIcon fell back to a LOCAL read whenever the SSH filesystem provider was absent, so a disconnected/not-yet-reattached remote repo whose path also exists on the client picked up the wrong repository's icon. Thread the connection identity through and fail closed, matching the rule already stated in connection-context.ts and repo-default-branch.ts. |
||
|
|
0ac2e77db1 |
fix(agent-hooks): default-form managed hook vars so a static precheck cannot reject them (#14994)
The managed hook command embedded a bare $SYSTEMROOT. Grok loads Claude's
settings.json hooks and statically prechecks env vars across the whole command
string, so the reference inside the never-taken Windows branch made it refuse
the hook on macOS on every event:
hook not executed: required env var(s) not set: ${SYSTEMROOT}
Grok fails these open, and Orca installs Grok's native hook separately, so no
status was lost -- the symptom is a swallowed failure line per tool call.
$VAR and ${VAR-} expand identically in POSIX shells absent set -u, so this has
no execution-time effect; only the static precheck observes it. Verified in Git
Bash on Windows that both guard forms resolve identically ($SYSTEMROOT is set
there and uppercase is the correct spelling -- $SystemRoot is undefined).
Also converts the three bare $HOME references so the regression test can assert
zero bare variable references with no exemption. A $SYSTEMROOT-specific check
would not have caught this class of bug being introduced elsewhere.
|
||
|
|
a9f93172cf |
Revert "fix(crash-reporting): give the parking census a retained breadcrumb slot without starving memory highwaters (#14867)" (#15005)
This reverts commit
|
||
|
|
fed6a7d4fd |
Rethrow update errors after lineage recovery attempt (#15008)
Previously silent failures are now rethrown after recovery attempts. Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
84784f5393 |
Split pull request page (#14853)
* refactor: split PullRequestPage.tsx under 400 lines Move the 5888-line PR page into nested domain modules under src/renderer/src/components/pull-request-page/ and leave a thin public barrel. No intentional behavior change. * rm plan * Improve React stability and remove manual ref caching - Stabilize React keys in CheckDetailsPanel using content fields instead of array indices to prevent unnecessary remounting - Remove manual ref-based entries cache in PRFilesCombinedDiffViewer, rely on useMemo dependency (diffEntrySignature) instead - Move sectionsRef assignment to useLayoutEffect to avoid render-phase ref writes - Refactor usePRFileSectionLoader to destructure args for readability * Improve PR page stability: add error handling and fix race conditions - Add error handling with user feedback (toast notifications) for comment submission, review comments, diff loading, and file view syncing - Internationalize hardcoded strings for PR state labels and error messages - Fix race condition in reviewer submission by using a ref-based guard instead of render-time state - Fix scroll restoration to avoid overwriting target positions with intermediate clamp values - Add effectiveRepoId parameter for proper repo context in review operations - Disable reviewer picker during submission to prevent concurrent requests * Improve PR page stability: add timeout and stable list keys - Add 45s timeout for diff loading to prevent indefinite hangs - Fix React list key generation for annotations/jobs using content-based keys with occurrence tracking - Refactor scroll position caching to properly handle mid-restore teardown - Replace interpolated error messages with full locale-specific strings for close/reopen actions * Fix PR diff viewer cache isolation and list key collisions - Changed list key generation from string concatenation to JSON serialization to avoid collisions with actual content keys - Added host-aware scoping to diff view caches so local and remote execution don't share entries - Optimized virtualizer keys to use lightweight revision counter instead of full serialized signature * Extract PR file state into entry-scoped hooks Replace manual state resets with custom hooks that automatically clear section heights and active section when switching PR entries. This prevents state leakage between files and simplifies the diff viewer component. Also validates the active section key exists before passing it to child components. * Improve PR page error messages, accessibility, and stability - Show actual error messages from failed operations instead of generic fallbacks - Add aria attributes for combobox/listbox patterns and proper option identifiers - Consolidate duplicate label/assignee update logic and fix event listener passive mode - Memoize GitHub source runtime to prevent stale closure in checks callbacks - Extract filled state badge tone for reuse and fix workspace attachment type - Guard textarea shortcuts against concurrent saves and add mention query test * Add explicit PR file content cache eviction Extract PRFileContentRequestArgs type and create evictPRFileContentRequest function to handle cache eviction explicitly. Call eviction on load timeout so retries fetch fresh content. Adds tests for cache behavior. |
||
|
|
71bbab72e1 |
fix(commit-message): keep Windows paths intact in agent command overrides (#14984)
* fix(commit-message): keep Windows paths intact in agent command overrides `tokenizeCustomCommandTemplate` applies POSIX backslash-escape rules on every platform. On Windows `\` is the path separator, so a native absolute path in an agent command override is silently destroyed: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -> C:WindowsSystem32WindowsPowerShellv1.0powershell.exe which is then reported as not found on PATH. The agent *startup* path already routes Windows shells to the Windows tokenizer, but the commit-message AI path still calls the generic tokenizer directly, so overrides, extra CLI args and custom commands there are all affected. The tokenizer gains an explicit `'escape' | 'literal'` mode rather than reading `process.platform`, because the same template can be parsed on one host and executed on another. `'escape'` stays the default, so POSIX behaviour — where `foo\ bar` is deliberately one token — is unchanged. `'literal'` is selected only where the command provably runs on native Windows: a LOCAL target, on win32, with no WSL distro. A WSL target runs a Linux binary inside the distro, and a remote target runs on a host whose platform this process cannot see; both keep POSIX escaping. Fixes #11375 * test: pin the platform decision for literal-backslash parsing commandBackslashMode is the only place that reads the platform, so it is where this can be wrong in the direction that matters — applying Windows rules to a command that will actually run under a POSIX shell. WSL and remote targets are pinned explicitly; both were previously untested. |
||
|
|
80b0ab16ff |
test(crash-reporting): keep ambiguous whole-tree kills reportable (#14667)
* test(crash-reporting): keep ambiguous tree kills reportable * test(crash-reporting): drop the stale sibling-settle deferral comment |
||
|
|
8b6d0231b2 | feat(i18n): localize Automations settings and navigation to Korean (#15004) | ||
|
|
226cf88ba6 |
fix(terminal): inset the grid inside the xterm surface (#14583)
* fix(terminal): inset the grid inside the xterm surface (#13252) Padding X/Y was applied as start-edge container margin, so the cell grid stayed flush on the trailing edges and a fractional background opacity stacked a darker gutter around the viewport. Put the setting on .xterm so FitAddon insets both axes and the themed background fills the pad. * fix(terminal): normalize padding before fit * fix(terminal): align stored and fitted padding * test(terminal): lock padding before fit * fix terminal padding opacity compositing * fix live terminal padding backgrounds * fix(terminal): preserve source-over alpha blending * fix(terminal): restore WebGL alpha blending * test(terminal): complete hidden retention pane fixture |
||
|
|
02ba70a847 |
fix(agent-hooks): make the Windows managed hook survive Claude-hooks-compat consumers (#14825)
* fix(agent-hooks): make the Windows managed hook survive Claude-hooks-compat consumers `~/.claude/settings.json` is not read only by Claude Code. Third-party Claude-hooks-compat layers (cursor-agent, Devin) import the same file and reimplement hook execution, so Orca's entry has to survive consumers that support strictly less than the documented schema. Three separate defects came from assuming otherwise. 1. The entry depended on `args`, which a compat consumer ignores. `args` is valid Claude Code syntax, but cursor-agent spawns `command` alone -- so `conhost.exe` ran bare, which opens an interactive console that never closes. Hook payloads were typed into those stranded shells (#14815). The entry is now one self-contained `command` string that depends on nothing optional. 2. `conhost.exe --headless` never relayed anything. It implements the ConPTY server protocol, not a generic no-window wrapper: it does not wait for the hosted process and relays neither exit code nor stdout. Measured directly -- `conhost --headless cmd /c "echo X& exit /b 42"` yields empty stdout and no exit code, while the replacement returns both and waits. So every hook was fire-and-forget, and whatever it printed was discarded. Replaced with `-WindowStyle Hidden`, which suppresses the window and keeps wait/exit-code/stdout intact. 3. The hook never wrote anything to stdout. Guards exited silently and curl's output went to nul. Claude Code documents empty stdout as "no decision", but cursor-agent treats PreToolUse as a permission gate, fails to parse empty stdout as JSON, and blocks the tool call -- so every shell command in every cursor-agent session on Windows failed (#14818). The script now writes `{}` first, on both the Windows and POSIX branches, which is documented to be identical to writing nothing for real Claude Code. Gemini and Antigravity already did this. Defects 2 and 3 are causally linked: `{}` cannot reach any consumer while conhost is swallowing stdout, so neither fix works without the other. Also fixed while establishing the contract: - The launcher's own missing-script fallback returned empty stdout, reproducing #14818 whenever `~/.orca` was cleaned or an install was half-finished. It now emits `{}` too. - PowerShell serializes progress records to stderr as CLIXML when stderr is redirected; a consumer merging stderr into stdout would see those bytes before the JSON. Every encoded payload now silences progress. - `runtime-home-hook-command.ts` built its own launcher without window suppression -- exactly the drift #14815 asks to prevent. All launcher construction now goes through `windows-powershell-hook-launcher.ts`, so the switch list cannot be present in one installer and missing in another. - Renamed `usesWindowsHeadlessHook` to `usesWindowsPowerShellLauncher`; nothing is headless anymore, and the flag selects a launcher. Testing: the new regression test asserts the effect a consumer observes -- it runs the exact `command` string from settings.json through both cmd.exe and Git Bash, across the guard-exit, reached-curl, and missing-script paths, and parses stdout. Verified it fails when `conhost --headless` is reintroduced. The previous tests all asserted installer intent, which is why they passed through all three defects. * fix(agent-hooks): close hook launcher review gaps --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
5e9e38fa75 |
fix(agent-status): announce Claude turn complete while background work runs (#14580)
* fix(agent-status): announce Claude turn complete while background work runs Lead Stop/StopFailure already ends the turn, but resolveClaudePaneState keeps the pane working for subagents, background shells, and session crons. That erases the working→done edge that mints the completion banner: subagent turns notify late with a stale body, and shells/crons never notify at all. Stamp turnCompletedAt on the gated lead Stop, announce immediately from that row, and pair the later all-clear done to the same end time so it cannot double-fire or collapse consecutive turns onto the pinned stateStartedAt. Fixes #13245 * fix(agent-status): suppress stamped turn replays * fix(agent-status): notify paired clients at turn end * Fix late-paired completion notification arming * test(agent-status): make the notification-id test fail on the pre-fix ordering The stored row inherited the helper's default codex agentType while the event named claude, so agentSnapshotMatchesExplicitTitle dropped it and freshStoredAgentStatus was undefined — the assertion held under either side of the `??`. Name the stored row's agent so the pinned working row survives and the snapshot-first precedence is what the test actually pins. * Suppress stamped completion tail replays * Prevent cross-coordinator title replays * Preserve stamped tails across fallback signals * Keep remount replay state while sibling lives * Scope OSC turn stamp preservation * Forward paired host completion stamps * Deduplicate paired completion tails * Bind paired completion tails to their turn * Preserve paired completion tail ownership * Keep paired tail replay state across remounts * Seed paired recovery without replaying completions * Seed startup replay and release stale fallback dedupe * fix(notifications): preserve stamped OSC repaints * fix(notifications): retain paired client turn boundary * fix notifications module import safety * chore: keep main integration focused * fix: preserve completion re-enable boundary |
||
|
|
e16a22ef58 |
fix(browser): return a real PermissionStatus from the query override (#14684)
* fix(browser): shadow PermissionStatus state instead of proxying it Rebased onto main after #14685 landed in the same file. Both changes coexist: #14685's Firefox gating and narrowed promptPerms are preserved untouched, and this change replaces only the query-override implementation. The previous Proxy bound every callable property, which broke three observable things: - onchange assignment threw "Illegal invocation" — the native setter received the proxy rather than the branded target. - Listeners were delivered with the real target and the NATIVE state, so on a real permission change event.target.state read 'granted' while the returned status still read 'prompt'. - Method reads returned a new bound function each time, so name became 'bound addEventListener', toString lost its name, and identity was unstable. Separately, the notifications state was captured once as a string, so an already-returned status went stale after requestPermission updated it. Shadow only 'state' on the genuine PermissionStatus with a lazy provider. The object the site holds IS the real one, so identity, brand checks, method fingerprints and native event delivery survive with nothing to keep in sync. Verified against real Chromium (Chrome 151) rather than only the vm stand-in, because the stand-in cannot show whether defineProperty succeeds on a branded instance: the instance is extensible, the prototype 'state' accessor is configurable, defineProperty succeeds, instanceof survives, addEventListener keeps its native name and referential stability, and onchange assignment works. New tests live in their own file rather than merged into anti-detection.test.ts, whose harness diverged on main. The fallback test targets 'camera' because #14685 narrowed the intercepted set, so a name outside it never reaches the fallback. * test(browser): cover intercepted PermissionStatus identity |
||
|
|
378c60071a |
fix(grok): strip quotes from GROK_HOME in the Windows hook (#14221) (#14985)
`setx GROK_HOME "C:\path\"` stores `C:\path"` — the CRT turns the `\"` into a literal quote. That quote unbalanced the trailing-backslash `if` operand, so cmd aborted grok-hook.cmd with exit 255 before curl and every Grok hook event failed. A quote elsewhere in the value closed curl's `grokHome=` argument early, swallowing the `^` continuation and dropping the `payload@-` line. `"` is illegal in a Windows path, so strip it during the copy. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
886dec1d2a |
fix(browser): report cookies an import could not decrypt (#14683)
* fix(browser): report cookies an import could not decrypt Supersedes #13193, which reported only the Windows v20 case. Nothing distinguished "decryption failed" from "no cookies present". A row that would not decrypt was folded into the generic `skipped` counter, and a profile whose rows all failed returned ok:true with importedCookies:0 and no warning — a green "Imported 0 cookies from Google Chrome." The two situations produce opposite result shapes and the worse one reported success. Attribute the cause at the point of failure, while the version prefix is still in hand, and surface it as one `cookies-undecryptable` warning carrying the reason. Covers all three known causes rather than one prefix: - app-bound-encryption: Chrome/Edge 140+ on Windows write `v20`, which only the writing browser can unwrap. The version gate is a FORMAT check (`/^v\d\d$/`), so v20 passed it and failed inside AES like corruption. - linux-keyring-unavailable: getLinuxEncryptionKey derived the v11 key from an empty password when both secret-tool lookups failed, so it never returned null and the "Could not access encryption key" guard was unreachable on Linux. - unknown: any other cause still warns instead of reporting success. Deliberately not a hard failure on Linux: Chrome falls back to the "peanuts" v10 key precisely when no keyring exists, so those profiles still import. Pinned by a regression test. Refs #13192, #14181 * fix(browser): attribute decrypt failures exactly and gate CBC by version Review-loop findings on the initial commit, all fixed here. - CORRECTNESS: v11 rows were attempted with the v10 key when the keyring was unavailable. AES-128-CBC is unauthenticated, so a wrong key that yields valid PKCS#7 padding was accepted — roughly 1 in 256 per row. Garbage values were written into the jar as real cookies, and because those rows counted as successes the warning this PR adds could never fire. Key eligibility is now explicit per version rather than implicit in key ordering. - CORRECTNESS: the CBC path returned an empty Buffer for a prefix-only value BEFORE checking eligibility. An empty Buffer is truthy, so an ineligible row counted as imported and reached the live-jar clear. Eligibility now precedes that branch and empty CBC ciphertext is rejected as malformed. - ACCURACY: a named cause reported the TOTAL failure count, so one v20 row plus one corrupt row claimed both failed to app-bound encryption. Counts are now exact per cause, with the remainder reported separately and a tie falling back to 'unknown'. Exact-count approach carried over from #13193. - The app-bound copy no longer dead-ends. It names the existing in-app file import without describing how to produce the file — Chrome has no native decrypted-cookie export, so concrete guidance would send users to an extension that can read their whole session jar. - Direct prefix edge tests carried forward from #13193. Repo-wide search found no second multi-key unauthenticated-CBC first-success site, so this pattern was one occurrence rather than a class. Co-authored-by: manuaudio <manuaudio@users.noreply.github.com> * fix(pr): preserve split worktree slice Remove the unrelated rollback of the worktree-slice split and its forbidden max-lines baseline addition from this cookie-import PR. * fix(browser): match the unknown decrypt reason explicitly CI's type-aware code-quality gate flagged the reason switch as non-exhaustive: the 'unknown' member was handled by `default:` rather than matched. Matching it explicitly keeps the behaviour identical today and makes the gate enforce the thing that matters — adding a new reason to the union now fails the switch instead of falling silently into a generic message that would not describe it. This gate is separate from `oxlint` and is not covered by running oxlint on the changed files, which is why it only surfaced in CI. --------- Co-authored-by: manuaudio <manuaudio@users.noreply.github.com> |
||
|
|
5e189d6081 |
feat(browser): add a WebAuthn account picker (#14687)
* fix(browser): prompt for WebAuthn account selection * fix(browser): scope WebAuthn cancellation to session * fix(renderer): keep WebAuthn render phase pure |
||
|
|
96b03bab4f | fix(terminal): align Cursor Agent IME preedit anchor (#14982) | ||
|
|
a324ee20d4 |
Reset terminal SGR state around restored output (#14700)
* fix(terminal): reset SGR around restored output * fix(terminal): preserve live replay styling * fix(terminal): ground dead reattach fallback |
||
|
|
85565a9302 |
reland(workspace): set project location from the create-worktree host picker (#14965)
* feat(workspace): reland set project location from the create-worktree host picker Relands #14868 (reverted by #14912) with a fix for the regression that caused the revert: setting a project location could change the path before Orca used it. The retarget-after-setup path read the raw store record to find a just-created setup, because the memoized picker options had not refreshed yet: useAppStore.getState().projectHostSetups.find( (candidate) => candidate.id === setupId && candidate.setupState === 'ready' ) That hand-rolls a second selection path that skips every rule the option builder applies — repo eligibility, ephemeral-VM and runtime-owned SSH host exclusion, and the one-setup-per-host dedupe whose own comment notes that resolveWorkspaceCreationTarget takes the first project+host match and ignores the rest. So the composer could be retargeted at a setup other than the canonical one for that host, pointing creation at a different location than the one chosen. Resolves through buildProjectHostSetupOptions against fresh store state instead, so the fallback and the steady-state picker agree by construction. STA-4547 * fix(workspace): sanitize the clone prefill and drop an abandoned set-location Review follow-ups on this PR. The "Clone from URL" prefill seeded the field with the verbatim `git remote` URL, which can embed a PAT (`https://x-access-token:ghp_...@github.com/...`). The clone then runs on the *target* host, writing that token into its .git/config — a credential the user never typed into this flow, now readable by anyone on a shared host. Strip it with the same sanitizer `getProvisionedRootRecipeRepoUrl` already applies to the ephemeral-VM recipe URL. Extracted to resolveProjectCloneUrlPrefill so the rule is directly testable. The dialog also stays dismissable while a submit is in flight, and an SSH clone is unbounded. A clone the user backed out of minutes earlier still called onReady, silently moving the run target and resetting start-from under a form they had since pointed at another host. Drop the result if the dialog went away. * fix(workspace): re-arm the abandoned guard on mount StrictMode runs mount/cleanup/mount, so latching `abandoned` on the first cleanup left it true for the rest of the session and permanently suppressed onReady — the app wraps its root in StrictMode. Reset it on mount. |
||
|
|
3f58d5cf9a |
fix(daemon): bound cwd validation per UNC route (#14967)
Async cwd validation dedupes by exact path but had no concurrency bound, and a dead UNC share answers `stat` in ~21s while holding one of libuv's 4 default fs threads. Four distinct paths on one unreachable server therefore starved every other async fs read in the daemon — including the cold-restore history replay running alongside them — which moves the head-of-line stall #14848 removed from the event loop into the thread pool. Adds a per-route lane of 2, reusing PrioritySemaphore and matching the per-distro lane in rate-limits/auth-filesystem-operation.ts. Keyed by the host that has to answer (WSL distro, or the `\\server` prefix) so many dead subdirectories of one share fold into one lane. Local-disk paths bypass the lane entirely: a global cap would queue a healthy local spawn behind a dead share. Moves PrioritySemaphore to src/shared. It has no imports, and reaching into src/main/daemon from src/main/providers inverted the dependency direction that already runs daemon -> providers. Note this bounds pool occupancy, which cancellation cannot: an aborted `stat` still holds its libuv thread until the OS returns. STA-4543 |
||
|
|
f070033156 |
Revert "refactor(shell): one portable Unix startup dialect instead of shell d…" (#14975)
This reverts commit
|
||
|
|
9c4627d1c6 |
Refactor: split GitHubItemDialog into lifecycle-organized modules (#14931)
* refactor: split GitHubItemDialog.tsx under 400 lines No intentional behavior change. * refactor: group github-item-dialog into lifecycle folders Reorganize the 50 flat files under src/renderer/src/components/ github-item-dialog/ into six lifecycle folders: load-item-details/ shared types, both caches, fetch/settle, state badge open-dialog/ dialog shell, headers, body, tabs, link copy discuss-item/ conversation tab, comments, composer, timeline edit-item-fields/ GH edit section, labels, assignees, status inspect-pull-request/ combined diff viewer, checks tab land-pull-request/ PR actions, merge menu, reviewers No intentional behavior change. All 50 files moved verbatim; the only edits are relative-import specifiers (sibling paths plus a depth bump for ../../../../shared) and the hardcoded module paths in the two source-boundary tests. Import graph stays acyclic: zero mutual folder pairs, no file importing 4+ sibling folders, no dest file importing the public barrel, and no per-folder index barrels. * refactor: split item references and improve diff-viewer remount logic - Break down full `GitHubWorkItem` props into discrete `itemId`, `itemNumber`, and `itemRepoId` in mutation and action functions to prevent over-memoization of callbacks and improve dependency clarity. - Extract `getPRFilesCombinedDiffSignature()` and use it as a component key to safely remount the diff viewer when the PR revision changes, replacing generationRef tracking. - Add `getKeyedCheckAnnotations()` and `getKeyedCheckJobs()` to generate stable, collision-resistant keys for check arrays that may contain duplicates. - Consolidate interpreter timeouts into a single `SPAWNED_INTERPRETER_TIMEOUT_MS` constant and apply it via describe options rather than per-test values. * refactor: improve github-item-dialog repo context and i18n coverage - Add repoId prop to ConversationTab for explicit repo context override - Internationalize UI strings in diff viewer and PR action components - Improve error handling with cache rollback and guard cleanup on sync failure - Enhance cache key validation for cross-window invalidation by repoPath - Add repository access validation before rendering diff viewer - Fix cross-platform issues: skip symlink test on Windows, normalize CRLF in test assertions * Refactor check button i18n key and update text - Replace hash-based key with semantic name for maintainability - Change button label to "Open in browser" for broader context |
||
|
|
d6703552c9 |
fix(crash-reporting): prune Crashpad dumps at startup and bound signature parsing (#14968)
A dying main process never delivers process-gone, so a crash loop never reached the only prune call site, and Crashpad's own pass runs in the handler child after a delayed first sweep. Prune on startup instead of behind the coalescing timer the loop outruns, and cap dump count alongside the byte budget. Signature parsing stayed on the main event loop after a crash. Reject on ptype before the whole-buffer scan, and bound the backward prefix search that could otherwise walk the entire dump only to discard the result past 96 bytes. Also keeps dumps already claimed by a persisted report from being pruned out from under the report's minidumpPath. STA-4544 |
||
|
|
1580ba14c4 |
fix(daemon): bound cwd validation and attach-only requests on a dead share (#14966)
* fix(daemon): bound cwd validation and attach-only requests on a dead share #14848 made cwd validation async so one dead share could not freeze every terminal, but left two ways for a single unreachable path to strand work. Canceling a create did not abort the probe: `isCanceled` is only polled between spawn steps, so `fs.stat` on a dead SMB/NFS/UNC path ran to completion. The create stayed in flight, later creates for that session queued behind it, and shutdown/idle waited on it. Carry an AbortSignal next to the existing poll so a canceled caller abandons the probe, and make the per-session queue wait abortable so a canceled queued request stops waiting too. The shared probe is left running for whoever still wants it, and the dedupe entry is evicted on a hard cap so a never-settling mount cannot poison that path across sessions forever. Attach-only requests registered no cancellable preparation, so the daemon could not match the client's cancel and the client had already cleared its timer — neither side bounded the request. Register the preparation for every createOrAttach and run spawn preflight only when it is not attach-only, and give the client a bounded grace window when the daemon reports an unmatched cancel. The cancellation reason now wins over a racing daemon rejection: callers key recovery off `client_disconnected`, and letting the race pick would roll back terminals it should keep (#7718). Splits working-directory validation out of local-pty-utils to stay under the line cap. STA-4541 * fix(daemon): keep a hung cwd probe to one thread and one cancel identity Review follow-ups on this PR. The 30s dedupe eviction retired an entry whose probe was still running. `fs.stat` is uninterruptible, so that freed no libuv thread — it only let the next caller pin another. A few retries against one dead mount exhaust the default pool of 4 and stall every other async fs read in the daemon, which is the cross-session freeze #14848 set out to remove. Drop the timer and clear on settle only: the AbortSignal added here already lets callers escape a shared hung probe, so sharing one no longer strands them, and a mount whose stat eventually returns still re-probes on the next call. The abort also introduced a second cancellation identity on the wire. WorkingDirectoryValidationAbortedError propagated out of createPtySubprocess as the request's error, and the client's mapping only recognizes the attach-cancel message — so a canceled create could reach the rollback branch that closes a terminal the user still has (#7718). Translate it at the daemon boundary so the wire carries one identity. |
||
|
|
17ef6ccce6 |
fix(terminal): clear the preedit overlay when an IME cancels a composition (#14758)
Backspacing over the last radical of a Cangjie composition empties the IME's marked text without reaching compositionend, and the vendored xterm CompositionHelper only dropped the overlay's `active` class there. The box stayed painted with whatever glyph it last held (#11951). Clear on the state rather than on the key, as native terminals do: an empty `compositionupdate` now hides the overlay instead of only ever showing it, and a key the IME swallows re-derives the preedit from the textarea once it settles so a composition emptied with no composition event at all is cancelled too. |
||
|
|
8e0dad8e21 |
fix(crash-reporting): decode POSIX wait statuses in crash-report display and record OS session ends (#14659)
* fix(crash-reporting): decode POSIX wait statuses for display and record Windows session-end reasons Chromium on POSIX hands render/child-process-gone the raw waitpid() status, so crash reports read "Exit code: 61696" where exit status 241 is meant (field: 61696=exit 241, 9=SIGKILL, 133=SIGTRAP+core, darwin crashed 5=SIGTRAP). Decode at the display layer only: the stored exitCode stays raw, Windows codes and launch-failed launch-error codes render unchanged, and the process-gone span gains a crash.exit_code_decoded attribute. Also durably record a system_session_end breadcrumb (with WindowSessionEndEvent reasons) when Windows session-end fires, so bundles can tell OS shutdown from a user task-kill in killed/exit-1 sweeps. * test(crash-reporting): pin the exit(0) no-suffix rendering Adversarial mutation review: removing the exit-0 suppression in formatCrashReportExitCode survived the suite — nothing pinned that a clean exit(0) renders without an '(exit status 0)' suffix. * test(crash-reporting): decode-attribute tests use synchronous child kills Renderer killed events gain a 250ms sibling-kill settle once the correlation branch lands, which (a) defers the span past the test's platform stub so the decode gate reads the real host platform, and (b) adds a deferred span that breaks the exact sink assertion. The decode gate is source-agnostic, and a non-recoverable child kill persists synchronously on every branch of the stack, so coverage is unchanged and the platform stub is deterministic on any CI host. * fix(crash-reporting): keep session-end reasons type-safe |
||
|
|
d5ef633adb |
fix(crash-reporting): stop post-mortem process metrics crowning a survivor as the crasher (#14662)
* fix(crash-reporting): keep a pre-gone process-metrics sample so the crashed process's working set survives its crash report * feat(crash-reporting): renderer peak/private bytes and gone-time system memory in crash details * fix(crash-reporting): macOS system-memory fields and an era-invariant pin for peak/private metrics * test(crash-reporting): kill five mutation survivors in the pre-gone sampler coverage Adversarial review found these mutations survived the suite: - dropping the immediate sample at startPreGoneProcessMetricsSampling() - removing the double-start idempotence guard - widening renderer peak/private aggregation to all buckets - a failed sweep erasing the previous good sample - the recorder hardcoding 'renderer' instead of event.processType Each now has a binding assertion; also documents that the crashed-process-absent flag is bucket-level only. * fix(crash-reporting): prove crasher absence by vanished pid, not bucket count alone The absent flag was bucket-level, so any surviving same-type process (a webview guest, the dashboard popout, another utility) silently cleared it — and webviewTag guests make multi-renderer sessions the norm. The pre-gone sample now keeps per-process pid/bucket/workingSet identities; a sampled same-bucket pid missing from the live set proves absence and reports the vanished process's own working set (processMetricsVanished*), so the crasher's size is no longer summed with surviving guests. Also: split gone-time system memory into its own module (max-lines), pin peak/private aggregation as a true max, clamp garbage negative working sets and backwards clocks, pin live-metrics precedence over incoming detail keys, and verify the sampler timer is unref'd by behavior. * fix(crash-reporting): bucket-aware vanished-pid check with consume-once attribution Loop-3 hardening of the vanished-pid logic: - Live pids now carry their bucket: a recycled pid living on as a different process type still reads as a vanished sampled process (the bare pid set misread the crasher as alive). - Vanished pids are attributed once. In a crash loop with no sweep between deaths, record #2 confidently inherited the FIRST crasher's pid and working set (dedupe window is only 2s, so both records ship); it now degrades to the honest bucket-count arm instead. - An ambiguous multi-process VanishedWorkingSetMB sum is bounded by VanishedLargestWorkingSetMB so no single-process reading of the sum survives triage. - Killer tests for the remaining mutation survivors: unreadable gone-time metrics prove nothing (flag/vanished stay off), pid-less sampled metrics never vanish, fractional-MB rounding, negative system-memory clamp, and full per-family precedence over colliding incoming detail keys. * fix(crash-reporting): flag consumed and blind-era vanished attribution instead of going silent Loop-4 hardening of the consume-once attribution: - processMetricsVanishedAlreadyReportedCount: a record whose vanished pids were consumed by a prior report now says so, instead of being indistinguishable from "nothing vanished" while its PreGone mirrors still show the prior crasher's era. - processMetricsVanishedAmbiguousWithEarlierCrash: consume-once only consumed when the gone-time read succeeded; a crash recorded blind (getAppMetrics threw) left its pid unconsumed, so the next record in the same era confidently emitted THAT crash's pid and working set as its own. Blind buckets now taint the era until a fresh sweep. - Pin two behaviors that were correct but unpinned: a failed sweep must not clear attribution state, and an ambiguous vanished pair is consumed too. - Document gone-time system memory reading healthier than at kill time, and the bound on the attribution set. - Split the suppressed-breadcrumb builder into its own module (max-lines). * fix(crash-reporting): extend vanished ambiguity to consumed eras and pin two unpinned behaviors Loop-5 findings: - processMetricsVanishedAmbiguousWithEarlierCrash fired only for the blind era; a partially-consumed era has the same shape (an earlier crash's unsampled respawn is as plausible a crasher as the newly vanished pid), yet emitted a confident VanishedPid with no flag. - Pin the > largest tie-break (first-enumerated wins) instead of re-accepting it as an equivalent mutant every loop. - The suppressed-breadcrumb type field had zero coverage after the module split — removing the whole block passed the suite. * refactor(crash-reporting): cut per-pid vanished attribution, keep the stateless absence proof Five review loops found defects in the same subsystem: the per-pid vanished attribution outputs (consume-once set, blind-era taint, consumed-era ambiguity). The absence proof they fed does not need any of it — a sampled same-bucket pid missing from the live enumeration (including cross-bucket pid recycle) is stateless and idempotent, so it stays true for every record of a crash loop with zero module-level attribution state. Dropped: processMetricsVanished{Count,WorkingSetMB,Pid, LargestWorkingSetMB,AlreadyReportedCount,AmbiguousWithEarlierCrash}, attributedVanishedPids, metricsBlindCrashBuckets, and the tests that existed only to defend them. Kept and still pinned: the 60s pre-gone sampler and its lifecycle, PreGone* mirrors + SampleAgeMs, renderer peak/private, gone-time system memory, the recorder processType binding, the live/PreGone era invariant, and the browser-pane case (webview guests keep the renderer bucket alive) that motivated the PR — now asserted via the absence flag plus PreGone mirrors alone. Documented the two honest limits: PreGone values are sample-time (up-to-60s understatement, bounded by AgeMs and lifetime peaks), and same-bucket pid recycle inside the sweep window is a false negative for the absence proof. * test(crash-reporting): pin PreGoneLargest to the crasher's own size, not the bucket's running sum Loop-6 mutation battery found one survivor in the cut's re-anchored suite: mutating Largest to carry the bucket's running sum survived every test, because no fixture put a same-bucket sibling BEFORE the largest process. That is the summing-bug family loop 2 found live. The webview-guest test now enumerates the guest first and asserts PreGoneLargest{Pid,Type,WorkingSetMB} carry the crasher's individual 4380, alongside the 4680 bucket total. Also restores the false-positive caveat the cut's comment dropped: a legitimately closed sampled process can trip the absence flag if the crasher's row somehow survives the live enumeration (pre-existing, unchanged by the cut). * fix(crash-reporting): mark pre-gone attribution ambiguous PreGone mirrors are whole-app snapshots, so a larger surviving Tab can own Largest and renderer-wide peak/private fields. Emit an explicit ambiguity boundary, prove the counterexample, and use Electron's pid plus creationTime identity to catch same-bucket PID reuse without adding stateful attribution. |
||
|
|
28c93da474 |
fix(crash-reporting): give the parking census a retained breadcrumb slot without starving memory highwaters (#14867)
* fix(crash-reporting): give the parking census a retained breadcrumb slot without starving memory highwaters * test(crash-reporting): pin retained slot starvation |
||
|
|
0abbecb17e |
fix(terminal): keep unresolved snapshot-capability verdicts re-askable (#14676)
* fix(terminal): keep unresolved snapshot-capability verdicts re-askable A daemon that could not answer the snapshot-capability probe within the ~91s startup ladder had its ptys settled 'no authoritative snapshot' permanently: settled ids were excluded from every later synchronization, and the only refresh callers run during renderer startup. Because the eviction-exemption predicate treats absent/unknown capability as exempt, one slow daemon start converted every local pty into a permanently eviction-exempt tab — the hidden-worktree retention budget could free nothing for the rest of the session (its own degenerate-case log: 'retention force-park freed no panes'). Three changes, none touching the safety direction (unknown still means exempt, panes stay mounted): - The retry ladder now decays to a slow 5-minute re-ask instead of a permanent verdict, so a recovered daemon is consulted again without any event wiring; the synchronization loop's existing timer carries it. - The ongoing collector now gathers the same fields the startup refresh does (layouts, pending reconnect ids), keyed on the sorted id set — synchronization prunes cached verdicts outside its collected ids, so the narrower collector could evict valid split-leaf answers back into the exempt-by-default state. - The degenerate-case log now carries per-route exemption counts (fail-open / foreign-worktree / capability-unknown / split-pane) and a matching crash breadcrumb, so field bundles can say which route dominates instead of a fourth investigation. Guard: exemption flips are a pin-unreachable input to the rendered park verdict, so capability verdicts must only change between commits — the new react185 harness force-parks a worktree, lets pane mounts write layouts/titles, flips capability repeatedly, and asserts every flip settles far from React's nested-update limit. Gates (previously red): a recovered resolver is re-consulted and the exemption clears; retained mounted tabs stay bounded by the retention limit as hidden worktrees accumulate after daemon recovery. Scope honesty: this bounds an unbounded-by-design retention residual; it is NOT the fix for the unexplained multi-GB field OOM cluster, whose retainer remains unidentified (field sessions crash with <= 1 mounted manager). * test(terminal): pin the widened capability collector and breadcrumb route buckets Review found two unpinned pieces of the re-settlement fix: reverting the ongoing collector to the narrow field set (dropping split-leaf layout and pending-reconnect ptys, which the sync would then prune back to exempt unknown) survived every existing test, and the force-park breadcrumb's route counters had no coverage at all. Both mutations now fail. * perf(terminal): memoize the capability collect key on its four store maps The widened collector ran collect+sort+join on every Terminal render and the memo-dep completeness was unpinned. Memoize the key on the four map identities (behavior-identical; the string-keyed second memo still damps layout-only churn) and pin the layouts dep with a staleness test — a missing dep silently drops new split-leaf ptys from the sync. * fix(terminal): keep a superseded capability pass's re-ask chain alive Review loop 2: a synchronization pass cancelled by a newer generation returned null, which ends the caller's timer chain — but the winner (the startup refresh) ignores its own return value, so unknowns it leaves behind had no scheduler left; recovery then waited on an id-set change. Return 0 instead: the superseded chain re-checks immediately and the early-outs collapse the re-check when nothing is pending. Also pin the collect-key memo's remaining deps at runtime (pending reconnect, tabs, pty ids) — exhaustive-deps is only a warn here, and loop 1's staleness test covered the layouts dep alone. * test(terminal): pin the same-identity re-ask path and closed-pty retry prune Review loop 3: mutation testing showed the chain's own re-ask path was unpinned — collapsing the first early-out to identity-only (returning null whenever the live-set reference is unchanged) passed every test, yet it kills the timer chain after one backoff: the hook refires with the SAME memoized array, so that mutation is the settled-forever bug in a worse shape. Pin it at both levels: a module test drives two passes over one array identity, and a hook test drives the real timer chain through a backoff refire with no id churn. Also pin the closed-pty retry prune (its removal survived the battery): the empty-set pass must return null — a leaked entry keeps a phantom 5-minute timer for the session — and a reappeared id must restart the 1s ladder rather than resume the decayed cadence. * fix(terminal): apply recovered snapshot capabilities * test(terminal): remove stale lint suppression * test(terminal): clean up capability prefetch hook |
||
|
|
f4319ba4f6 |
fix(crash-reporting): preserve coalesced repeat accounting (#14666)
* fix(crash-reporting): attribute coalesced repeats exactly once Two ways one burst's suppressed repeats were misattributed in forensics, found across two adversarial review loops on the sibling-correlation work but pre-dating it (the coalesce machinery shipped in #8800/#5818/#10729): - Double-claim: a crash report filed mid-window snapshots the ring, folding the suppressed repeats into the emitted crumb — but the next emit still re-claimed those repeats in its suppressedSinceLast, reporting one burst twice across two crumbs. - Mirror erasure: a fold resolving onto a re-emitted crumb overwrote the count that crumb was born carrying, deleting the previous window's repeats. Track what each crumb has claimed (carried at emit, resolved by folds) so every repeat is attributed exactly once. And never resolve into an evicted crumb: when a storm pushes the burst crumb out of the 30-entry ring — or past the retained-slot snapshot budget — mid-window, folding there would mark the repeats claimed by evidence no snapshot can see and the burst would vanish from the record entirely; drop the handle so the next emit claims them instead. Semantic note for trace-mirroring consumers: the returned suppressedSinceLast is now net of already-folded repeats, so exactly-once holds over the union of trace spans and report snapshots rather than within the trace stream alone. * fix(crash-reporting): preserve orphaned repeat debt on cleanup * fix(crash-reporting): preserve data-less repeat debt * fix(crash-reporting): make coalescing window monotonic |
||
|
|
b6ea3f17a9 |
refactor(shell): one portable Unix startup dialect instead of shell detection (#14863)
Orca had to guess which shell would parse a queued command line, then emit syntax for it. Guessing is unreliable for a remote or WSL host, and every dialect-dependent function is a place to get it wrong. Replace the guess. Everything emitted for a Unix shell is now built to be correct in sh, bash, zsh, dash, ksh and fish alike, so no detection is needed: - quoteStartupArg emits backslashes as "\\" and apostrophes as "'" between single-quoted runs. Both families read that identically, unlike the sh '\'' idiom, which fish silently halves and which makes a trailing backslash a hard syntax error. - clearEnvCommand emits a self-contained fish/sh branch. It deliberately does NOT call a helper defined by Orca's shell wrappers: Orca wraps only zsh, bash and fish, so an `sh`/`dash`/`ksh` login shell launches unwrapped — and the same text is copied to the clipboard and pasted into shells Orca never spawned. In both, a helper would be `command not found`, which is the exact failure this exists to avoid. Two guarded statements rather than `A && B || C`, because fish's `set -e` returns non-zero for an already-unset variable and would fall through to the sh branch; a trailing `true` pins the status, since this is the last statement of a launch line and the prompt renders it. - One tokenizer for Unix. The input is a settings string the shell never parses, so parsing it per-shell only made the same setting mean different things in different workspaces. AgentStartupShell loses its 'fish' and 'unix' members, and the three login-shell resolvers, the fish tokenizer and the agentEnv.SHELL probe go with them. Per-worktree shell history now actually works: - zsh on macOS was a no-op. /etc/zshrc assigns HISTFILE unconditionally before any wrapper Orca controls, so the injected value was already gone — and with ZDOTDIR still pointing at Orca's wrapper dir, history landed inside it. The intended path rides ORCA_HISTFILE and is restored after user config. Fixes #11044. - fish keeps history in its own data dir keyed by session name, since it ignores HISTFILE and has no custom-directory knob. Files are deleted rather than truncated, a symlinked ~/.local/share no longer disables cleanup, and a GC sweep reclaims orphans whose meta.json is gone. The sweep refuses an empty live-worktree set (indistinguishable from a store that failed to hydrate) and skips files younger than GC_MIN_AGE_MS, mirroring the tree GC's guard against the live-set snapshot race. Verified against real shells rather than asserted as strings: startup-shell-portability.live-shell.test.ts runs 194 assertions across sh/bash/zsh/dash/ksh/fish, and zsh-scoped-histfile.live-shell.test.ts drives a real login zsh through /etc/zshrc. Both are vacuity-checked. The same quoting corpus was replayed byte-exact on Linux, where /bin/sh is dash. |