mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 16:03:45 +00:00
3f378abcbc2b6070f970124391a491f12a458422
3506
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3f378abcbc |
fix(automations): match recovery turn evidence by run-id marker, not prompt body
A reused occurrence run keeps the dispatchPromptPreview built at creation, while a later dispatch submits the current (possibly edited) automation prompt. Exact-equality matching then pins recovery to a prompt that never ran and disables the promptless fallback, so the run dies as 'agent never started' despite the agent working. Turn identity is the embedded ORCA_AUTOMATION_RUN_ID marker: match evidence by marker run-id equality (a superset of exact equality for marker prompts), and stop backfilling legacy previews from the current automation prompt -- absence now routes those runs through the time-based fallback instead of a prompt nobody proved was submitted. |
||
|
|
93b24a236f | Merge branch 'main' into brennanb2025/sta-2939-codex-scheduled-automation-launch-r1 | ||
|
|
fc8c981103 | fix(browser-preview): enforce canonical runtime grants (#16975) | ||
|
|
cf5e087246 |
fix(github): bound PR-refresh alias fan-out and hosted-review cache growth (#16943)
* wip: memory-growth * fix(github): preserve newer refresh candidates |
||
|
|
cb848647e5 |
fix(browser-preview): require explicit preview capabilities (STA-5758) (#16921)
* fix(browser-preview): require explicit preview capabilities (STA-5758) Scope document reads to approved directories, confirm external links before opening them, revoke grants with tab lifecycle, and keep document-preview session state rollback-safe across mixed client/runtime versions. * Harden document preview lifecycle and permissions * Document preview DNS prefetch residual * Make preview E2E guest focus explicit * fix(browser-preview): entry-file-only authority for root-level docs, contained chip layout, re-issued gate paths (STA-5758) A grant whose document directory is its own request base — a doc at the workspace root, or outside any workspace — now reads nothing but the entry file until the reader approves a directory, at both the lexical and the canonical containment pass. The DNS-prefetch residual can only beacon what the page can read, and a root-level document could previously read the whole worktree silently. The identity chip's host badge overflowed the chip's layout box under squeeze (Linux CI): every row member can now shrink and truncate, verified by a width sweep in isolated Chromium down to ~120px chips. The Allow banner says what it grants: 'Allow folder', reading files in the named directory, for the life of the preview. The reliability-gate manifest command, testFiles entry, assertion refs and dated evidence naming the deleted doc-preview-external-link-bridge.test.ts are re-issued at doc-preview-external-link-confirmation.test.ts with a fresh 189/189 run; the focus-gate assertion text follows the shipped gate. * fix(browser-preview): hide the chip identity row below 24rem instead of clipping it, ellipsize the host badge, catalog the new i18n keys (STA-5758) CI's preview pane leaves the chip ~40px: no truncation shows anything there, so the Workspace-file label and host badge now hide whole below a 24rem container threshold sized so that visible implies contained. The badge text gains an inner text box — text directly inside the flex pill clipped both ends with no ellipsis. The e2e geometry oracle asserts containment when the row shows and the threshold when it does not. verify:localization-catalog: the hardening's new preview keys (and the renamed allowDirectory) join en.json via sync:localization-catalog. * feat(browser-preview): batch blocked folders into one access decision (STA-5758) Sequential per-folder banners trained the allow reflex without adding judgment — a reader cannot weigh assets/ against data/. The banner now accumulates every folder a load surfaces, names them (three, then a count, full list in the title), and grants exactly that set with one Allow-N-folders click and one reload. Dismiss fences the whole named set. The map lives behind a ref with a version tick so a dismissal fences an offer landing in the same event batch. |
||
|
|
b19a397d3e |
feat(browser-preview): reland remote HTML document previews (STA-5758) (#16920)
Reapply the reverted remote HTML document preview implementation so remote workspace files render locally over the orca-preview scheme. |
||
|
|
c54bf92181 |
fix(window): stop will-navigate from blocking the lazy-chunk recovery reload (#16944)
* wip: lazy-chunk-reload * fix(window): reject non-document navigation schemes |
||
|
|
3d796b82cb | fix(claude-usage): bill 1-hour cache writes at 2x base input (#16878) | ||
|
|
52ade074a9 |
Display host on automation details and dialog (#16823)
* Show automation host in details and support moving between hosts - Rename AutomationCreateDestinationField to AutomationDestinationField to reflect dual use in create and edit modes - Add host display to automation detail view, showing storage authority - Allow editing automations to move them to different hosts within same authority; project list filters to available projects on chosen host - Update copy from create-only terminology to mode-agnostic wording * Display automation host and support cross-authority moves Users can now move automations to different storage authorities. The destination picker shows all available hosts, and selecting a new one displays a warning about the move. The save creates the automation on the destination and deletes it from the source; if deletion fails, both copies remain and the user is notified. * Remove cross-authority move support for automations An automation's authority (the Orca instance that stores and schedules it) cannot change; edits now only offer hosts within the same authority and move logic is removed entirely. This simplifies the destination picker and removes move-specific UI messaging. * Fix undefined selectedRowKey in automation host recovery Replace references to the undefined selectedRowKey variable with selectedRow?.key to properly access the row's key when recovering automation runs across hosts. * Support moving automations across execution authorities Allows users to move automations between different authorities (desktop ↔ runtime environments) during editing. A save to a different authority creates the automation on the destination and deletes the original with its run history. Includes clear messaging about the move operation, proper handling of workspace id resets, and graceful error handling when deletion fails. Supports destination-aware project and worktree fetching. * Reuse creationKey across move retries when schedule changes When retrying a failed automation move, dtstart is minted fresh each attempt, changing the payload. Previously, operationKey included the full payload, so retries would mint new creationKeys and risk duplicate automations on the destination if the initial create failed in transport. Now key only by the move (source + destination) to ensure stable creationKey across retries. Also fix workspace auto-selection to use authority-scoped worktrees instead of the merged cache, preventing unwanted restoration of source-host workspaces after switching authorities. * Rename `note` to `moveWarning` for automation host moves Clarifies that the field specifically warns when an automation would move to another host, replacing the plain storage line. |
||
|
|
25fa6a9920 |
fix(ssh): a relay that reports a PTY absent must authorise a fresh spawn (#16763)
* fix(ssh): a relay that reports a PTY absent must authorise a fresh spawn reattachSshPtySession flattened the relay's `PTY "<id>" not found` into a bare `SSH_SESSION_EXPIRED`, discarding the evidence. isPtyAlreadyGoneError classifies on that message via /PTY ".+" not found/, so it could never match — and attachStablePaneOwner's existing already-gone fallback (retire the pane binding, onPtyExit, spawn fresh) is gated on exactly that predicate. The recovery machinery was already correct; only the classification was broken, which is why `pty:spawn` rejected outright after a relay restart renumbered its PTYs from pty-1. SshPtyAbsentFromRelayError carries the verdict the message cannot: a reachable relay answered for this exact id and reported it absent. Both thrown messages stay byte-identical to the previous ones, so all six message-based consumers are untouched; only callers that can act on the stronger verdict test the class. Deliberately not raised for a transport failure, timeout, disposed multiplexer, identity mismatch, or restoreRequired — none of those observe the process, and treating them as absence would orphan live remote work. * test(ssh): cover the absence fallback against real persistence The first cut passed no store/worktreeId, so attachStablePaneOwner's guard short-circuited and retireTerminalSurfaceFromPersistence — which deletes the parent tab and its layout when the retired leaf is the last one — never ran. retirePersistedStablePaneOwner and attachStablePaneOwner had no coverage at all. |
||
|
|
2b391652b1 |
fix(terminal): a close the host never heard must survive the reconnect (#16752)
An enterprise user: "Every day I open orca and it opens more tabs daily at a linear scale." Three reports over a week, told on 08-19 that a PR had fixed it, reported twice more after. STA-4658 (P0), GH #12447, #15136, #10342, #9585. One install held 39 zombie tab records. The revived tab's sleeping-agent record still holds the pre-close session id, so it boots `claude --resume <old id>` -- two agents on one transcript. ## The chain, measured Reproduced deterministically in `ssh-lost-kill-tab-resurrection.spec.ts`: close an SSH tab, kill the relay daemon in the container so `pty.kill` rejects with a transport-class error, reconnect. drop 2 resurrected the closed tab <id>: baseline=1 drop1=1 drop2=2 (closed tab returned) drop3=1 The trigger is narrow and had to be measured rather than assumed: killed relay daemon reproduces **6 of 6 runs**; an orderly `ssh.disconnect` **passes**. Only an ungraceful loss -- network partition, host reboot, relay crash, a laptop sleeping mid-session -- strands the close with the RPC rejecting on a transport-class error. Both variants live in the spec behind one `runResurrectionCycles` parameterized solely by the disruption, so the difference is attributable to that single variable. What actually carries the tab back, from the pull path (`workspace.get` -> `getRemoteSnapshot`, `remote-workspace-relay-sync.ts:29`): pullSnapshot rev=3 tabs={repo:["16c4a3e1","06aba6b6"]} pullSnapshot rev=4 tabs={repo:["16c4a3e1","ff72768e"]} <- ff72768e IS the resurrected tab pullSnapshot rev=5 tabs={repo:["16c4a3e1","ff72768e","da21b76c"]} The client uploaded the session containing the tab; the user closed it; the kill RPC rejected so the close never reached the host; the host's snapshot still lists it; the client pulls it back and the merge restores it -- **correctly, by its own rule that the host is authoritative for what it knows.** A pane then mounts, respawns, and takes the recycled pty id. Client-side correlation from the same run, two controls and one positive in one run differing in exactly one variable: | Tab | Close events observed | Resurrected? | |---|---|---| | `6305cc07` | `user` + `pty-exit` | No | | `ed56f66c` | `user` + `pty-exit` | No | | `2036e760` | `user` only | **YES** | ## The fix `src/shared/closed-terminal-tab-tombstones.ts` (99 lines). A client-recorded close is first-party intent and must survive until the host acknowledges it. Per `docs/reference/ssh-execution-boundary.md` the remote verdict is `unverifiable` -- which may not authorise declaring the process dead, but equally must not authorise resurrecting the tab. This is SSH-v3 principle P2, "durable tombstones with a monotonic per-scope revision", reusing the existing `RemoteWorkspaceSnapshot.revision` rather than adding a twelfth per-tab identity field (the codebase carries eleven, 784 refs, that SSH-v3 Phase 3 deletes). - **Recorded** only on `closeReason === 'user'` (`terminal-tab-close.ts:69`). - **Suppresses** a host-sourced tab only when `tabId in tombstones && !currentTabsById.has(tabId)` -- a live local tab always wins, because deleting a live pane is the one outcome the merge exists to avoid. - **Retires** on positive acknowledgement: `!hostKnownTabIds.has(tabId) && hostRevision > observed`. Strictly newer, so a pull already in flight at close time cannot ack a close it predates. - Three never-retire guards: no revision retires nothing; a worktree the snapshot has no row for retires nothing; the first omitting snapshot only stamps the watermark. - TTL (30d) + cap (500) are **backstops** for a target the user never returns to, not the mechanism. - **Client-local only** -- never crosses the wire, so there is no mixed-version exposure. - Suppression is scoped to `replaceWorktreeIds`, which is what makes the live-tab check meaningful. A final whole-map sweep over the assembled `tabsByWorktree` would break that (a live tab is absent from `currentTabsById` outside the scope and would look suppressible); it is deliberately not there, and the comment at the top of the function says so. ## Evidence The load-bearing evidence is an A/B control on one tree, not the oracle's assertion. Flipping `isSuppressedByClose` to `false` -- one character -- reproduces the resurrection on demand: --repeat-each=2: 1) drop 2 resurrected the closed tab ab0e305d-…: baseline=1 drop1=1 drop2=2 2) drop 2 resurrected the closed tab 51533e34-…: baseline=1 drop1=1 drop2=2 2 failed With suppression on: **0 occurrences of "resurrected the closed tab" across five runs plus one independent run by a second agent.** Provenance verified positively, not by mtime: `closedTerminalTabTombstonesByTabId` appears 13x across 3 renderer chunks including `store-Do3KBvRE.js`; for every red control run `mayCreate` appeared 0 times in `out/main/index.js`. At the unit layer, disabling the same predicate: 3 failed | 39 passed. Restored: 42 passed; 287 across the workspace-session, terminal-store, remote-workspace, shared-tombstone and profile suites; 24 in the four tombstone suites. ## The oracle spec: GREEN in the full lane `ssh-lost-kill-tab-resurrection.spec.ts` passes both tests at this commit. Full Docker-SSH lane, clean tree: BUILD_SHA=49bb96e0b4c DIRTY=0 PROVENANCE tombstone=13 hasLocalTabsRow=2 hostAuthority=4 mayCreate=3 14 specs / 20 tests -> 17 passed, 2 failed, 1 skipped (10.7m) [12/20] :178 does not resurrect tabs whose kill was lost to a killed relay daemon PASSED [13/20] :190 does not resurrect tabs closed while the host is disconnected PASSED grep -c "resurrected the closed tab" (whole lane) -> 0 It passes WITHOUT PR 7 in the build (`mayCreate` present, `SshPtyAbsentFromRelayError` absent), so the bug-2 fix below is not required for it. Test 1 fails intermittently in ISOLATED single-spec runs, where a third defect blocks its cycle-2 setup. The resurrection assertion itself has never failed with this fix in place -- the intermittent failure is always a setup failure, never a resurrected tab. A reviewer running the spec alone may see it red; that is not this fix regressing. Three defects sit under STA-3374 and should not be conflated: - Bug 1 -- the closed tab resurrects. Fixed here. - Bug 2 -- `ssh-pty-session-reattach.ts:227-231` rewrites the relay's `PTY "pty-1" not found` into a bare `SSH_SESSION_EXPIRED`, so `isPtyAlreadyGoneError`'s `/PTY ".+" not found/` cannot match and `attachStablePaneOwner:242`'s already-correct fallback never runs. Owned by PR 7 (`nwparker/ssh-07-absent-from-relay`). Not required for the oracle above. - Bug 3 -- after the daemon is killed and the client launches a replacement, the client's OWN SSH transport drops and does not reconnect within 60s: no "delay step 2/9", no handshake failure, nothing. `ssh-connection.ts:1533` only logs on an SSH-level close. Unfixed, its own ticket. This is what makes test 1 intermittent in isolation. Discriminator for bug 3, measured in the isolated runs (the lane above ran without `ORCA_E2E_FORWARD_APP_LOGS=1`, so it was not re-confirmed there): `[ssh-relay] Socket probe result:` reads "DEAD" on every cycle of test 1 (daemon killed, a NEW relay must be launched) and "ALIVE" on every cycle of test 2 (daemon survived). Whenever a new daemon must be launched, the SSH transport drops afterwards and does not recover. An earlier reading blamed `kill.ts:82-84` for skipping `finishPtyShutdown` on a non-already-gone error. That was eliminated by direct test: the implied fix, `markSshRemotePtyLease(…, 'expired')` in that branch, was implemented, changed nothing, and was reverted rather than shipped unproven. Recorded so the path is not re-walked. The `SSH_SESSION_EXPIRED` rejection is real but fires during cycle 1 for the baseline pane, after which cycle 1 completes; the 60s silence begins only after `Relay channel lost ..., triggering reconnect`. The spec is claimed by the Docker-SSH lane, and that lane does not gate merges today. ## Persistence: the tombstone must survive a relaunch `closedTerminalTabTombstonesByTabId` is declared on `WorkspaceSessionState` but was missing from `workspaceSessionStateSchema` (`src/shared/workspace-session-schema.ts`), which is the load boundary for BOTH partitions -- `normalize-loaded-state-collections.ts` for `local` and `workspace-session-partitions.ts` for `ssh:<target>`. Zod strips unknown keys and the write side does not validate, so the map reached disk and was discarded on the next launch. Measured with the repo's own parser: input : closedTerminalTabTombstonesByTabId: { 'tab-1': {...} } ok = true tombstones after parse = undefined That made the fix ineffective in the exact reported scenario: close an SSH tab with the transport down, QUIT, relaunch, reconnect -- the merge runs with an empty map, the host still lists the tab, and it resurrects. "Every day I open orca and it opens more tabs" is a claim about restarts. Neither the green oracle nor the A/B control could see it: both run entirely inside one app process. It also made the 30-day TTL and the 500 cap unreachable. Fixed by adding the field with a `salvagingRecord` matching its sibling `terminalSurfaceTombstonesByPaneKey`, so one malformed entry drops that entry rather than the map. `workspace-session-schema.ts` was one line under its 300-line max-lines limit, so adding the field required room rather than a suppression (the project forbids max-lines disables and per-file bumps). Two value schemas were extracted to modules named after what they contain: `terminal-tab-id-schema.ts` and `terminal-surface-tombstone-schema.ts`. The closed-tab tombstone's own schema is colocated with its type in `closed-terminal-tab-tombstones.ts`, which is where it belongs -- omitting it from the session schema is exactly the drift that caused this bug. `workspace-session-schema-field-coverage.test.ts` is the ratchet. Two sibling tables already pin themselves with `satisfies Record<keyof WorkspaceSessionState, ...>`; this schema had no such guard and is the one that fell behind. The new file adds both halves -- a `satisfies` list that makes a forgotten field a compile error, and a runtime assertion that names it -- plus a `parseWorkspaceSession` round-trip. Without the schema entry: 3 failed. With it: 3 passed. ## A host tab the user never closed could be deleted `tabId in closedTerminalTabTombstonesByTabId` answers true for every `Object.prototype` key even on an EMPTY map, because the map is a plain object from `Object.fromEntries`. A host tab whose id is `toString` was filtered from the reconciled list, blocked from the host-unknown branch, and stripped of its layout and session id. Tab ids are validated only as non-empty and colon-free, and `createTab` honours caller-supplied id hints, so the id is reachable rather than theoretical. This was the only path in either direction that could delete a tab the user never closed. Now `Object.hasOwn`, as the same file already uses elsewhere. Suppression is also scoped structurally: `isSuppressedByClose` compares the tombstone's stored `worktreeId`, which it already carried, so it cannot reach another workspace's tab. The two sweeps that have no worktree in scope (`terminalLayoutsByTabId`, `remoteSessionIdsByTabId`) now consult the set of ids this merge actually suppressed rather than re-deriving a verdict without that scope. The scope comment at the top of the function was also wrong and is corrected. It claimed every use of suppression sits inside `replaceWorktreeIds`; it does not -- the tabs pass walks all of `orderedWorktreeIds` and the two sweeps cover the whole remote maps. What actually makes it safe is that `closeTab` strips the id from every worktree row before recording the tombstone, plus the worktree match above, plus `closeReason === 'user'` being the only writer. Real guarantee, different from the documented one. ## Divergences from open PR #16571 #16571 implements the same concept. Three deliberate changes: 1. It never retires on acknowledgement -- TTL+cap only, so it never converges. Ack retirement added. 2. It crosses the wire and lets a HOST-sourced tombstone delete a LOCAL tab in a final whole-map sweep. After #14361 that is the wrong risk; dropped. This also removes the mixed-version regression its own body flags. 3. Its hydration unions rather than replaces the map -- a union resurrects every tombstone the merge just retired, so it never converges. Its `activeTabId` nulling is also dropped as redundant: `workspace-terminal-hydration.ts:99-105,126-138` already revalidates both pointers against the tab rows it just built, and nulling twice would add a second rule that has to stay in step with the first. ## Can a tab the user did NOT close disappear? No, but the guarantee needs stating precisely. The only writer is `recordClosedTerminalTabTombstone` (`terminal-tab-close.ts:69`), reachable only on `closeReason === 'user'`; suppression additionally requires the tab not be live locally. Reopen (`recently-closed-tabs.ts:122-166`) calls `createTab` and restores cwd/shell/title/color/position, never the old id. **Caveat, stated because the slogan is not literally true:** `createTab` honours a caller-supplied id hint (`terminal-tab-creation.ts:53-65`, used by `useIpcEvents` for host-admitted tabs), so "tab ids are uuids that never recur" does not hold in this codebase. The guarantee rests on the `closeReason === 'user'` writer plus the live-local-tab check, not on id uniqueness. ## Risk Renderer-side, client-local, no wire change. The blast radius is `mergeDirectSshRemoteWorkspaceSession` and the persisted session field. Worst case if the ack logic were wrong in the retiring direction: a tombstone outlives its usefulness and suppresses a host tab whose id the host re-issues -- bounded by the live-local-tab check, the 30d TTL and the 500 cap. Worst case in the other direction is today's behaviour. `profile-project-session-field-disposition.ts` records the new field as `notRepoScoped` / `notTransferred` residue, bounded by the same TTL and cap. ## Verify pnpm test src/shared/closed-terminal-tab-tombstones.test.ts \ src/renderer/src/lib/workspace-session-closed-tab-tombstones.test.ts \ src/renderer/src/store/terminals/terminal-tab-close-tombstone.test.ts \ src/renderer/src/hooks/remote-workspace-session-merge-close-tombstones.test.ts To reproduce the bug this fixes, set `isSuppressedByClose` to `() => false` in `remote-workspace-session-merge.ts` and run `pnpm test:e2e:ssh-docker -- tests/e2e/ssh-lost-kill-tab-resurrection.spec.ts --repeat-each=2`. |
||
|
|
a7b1da9e14 |
fix(ssh): a reattach may bind a pane but must never create one (#16751)
`reattachKnownPtys` treats every non-terminated lease as live and calls `persistPtyBinding`, which had no way to say "bind only". Two of its branches then rebuild UI the user is not asking for: - `pty-binding-persistence.ts:133-143` -- `if (args.incarnationId)` unconditionally deletes the pane's close tombstone. - `:145-160` -- on a tab-not-found it mints one via `createMinimalPersistedTerminalTab`. The in-code comment names its only intended caller: "pty:spawn can beat the debounced writer." Spawn. Reattach took the same branch. This is the mechanism canceled ticket STA-4268 described: "Leases have no pane incarnation and upsert only by target/PTY. Every nonterminal lease is reattached; frozen coordinates are passed to persistPtyBinding, which creates and flushes missing tabs and layout leaves." It was fixed in #13326, reverted by #14361, re-applied by #14384, and reverted again by #14395 (opened and merged nine seconds apart, empty commit body). `grep -rn "mayCreate" src/` returns nothing on main -- the mechanism is genuinely out of the tree. Four parts: 1. `mayCreate` (default true). When false, one pre-mutation check mirrors all four creating branches and returns `false` without mutating, so a refusal leaves nothing half-written. 2. The authority gate -- the part both prior attempts lacked, and probably why both were reverted. `mayCreate: false` alone refuses in two situations: "the user closed it" AND "the renderer has not published its layout yet". The second is routine on disconnect->reconnect and is almost certainly the #14361 tab-loss mechanism. The fence was not wrong; it was UNCONDITIONED. It is now passed only when `hasHostAuthoritativeTerminalMembership()` says the persisted membership speaks for this worktree, reusing the function already guarding the same question at `orca-runtime.ts:8608`. Losing a tab is worse than keeping a duplicate, so an unauthoritative session still gets the creating write. Authority is read from `local` because that is the partition the write lands in -- it is local's absence being interpreted. But a pane the `ssh:<target>` partition still holds is not gone, so it keeps its creating write; refusing there would strand a live pane behind a binding reattach can no longer reach. (SSH spawns bind into `ssh:<target>` while this reattach binds into `local` -- GH #12721/#12723, STA-3980. This does not fix that split; it refuses to judge from one side of it.) 3. `findTerminalTabIdForLeaf` -- bind resolves the tab from the live layout instead of the lease's frozen `tabId`. Only the leaf half of a pane key is remint-stable: `detachTerminalPaneToTab` moves a live pane into a new tab, so a stored tabId names the tab the pane left. Identity vs location. 4. Pane-keyed supersession -- retires siblings on `(targetId, worktreeId, leafId)` to `expired`, guarded by the durable binding, which reads `local` then `ssh:<target>` so it is correct whichever partition the binding landed in. `upsertSshRemotePtyLease` matched `(targetId, ptyId)` alone (`ssh-pty-lease-operations.ts:34-36`), so a new relay pty id on reattach minted a SECOND lease instead of updating the first, leaving the predecessor non-terminated with nothing to retire it. On refusal the lease goes `expired`, never `terminated` -- `expired` records that this shell has no surface to reach it through; `terminated` would assert an exit nothing here observed, which `docs/reference/ssh-execution-boundary.md` forbids. The remote process is left running. Deliberately NOT done: - A collision guard for `upsertSshRemotePtyLease`. Built, tested, and REMOVED -- its own test passed with the guard disabled, i.e. vacuous. Telling "same lease" from "recycled id on a different shell" needs a relay-start identity, which would be a twelfth per-tab identity concept; the codebase already carries eleven (784 refs) that SSH-v3 Phase 3 deletes. Left as an in-code NOTE. This handles lease DIVERGENCE, not COLLISION. - A port of #13324. Its own authors deleted its load fold and reverted its local-only reader in #13326 ("a headless-owned pane still gets a vote before its lease is retired"); porting it ships a state-destroying migration they removed. - `bindPaneShell` from #13325. Its purpose is making `isSupersededPtyId` live, and that fence does not exist in main. It would have been a refactor plus a silently-ignored `mayCreate` -- TS drops excess props through spreads (verified), which is why `mayCreate` is passed as a conditional spread here. Honesty about scope: this does NOT close the daily-tab-growth report. The deterministic e2e repro (`ssh-lost-kill-tab-resurrection.spec.ts`, later in this stack) is byte-identical before and after, and instrumentation shows why -- in that scenario `restoreReattachedPtyRuntime` is never called at all (`CREATING TAB` 9 hits, `reattach gate` 0, `BYPASS` 0). The fence is on a path that bug does not take. It is a real, separately-provable defect; it is not the headline fix, and must not be claimed as one. Evidence, A/B on this tree. Disabling the authority gate (`mayCreate = true`) and the supersession call by hand: **8 of 12 fail**, including "does not resurrect a tab whose closing pty.kill failed with a transport error" and "holds the live lease count flat across ten reconnects of one pane" (`[ 'pty-0', 'pty-1', 'pty-2', …(7) ]` vs `[ 'pty-9' ]`). The 4 that pass both ways are the over-refusal tripwires, which is the point of having them. Restored: 12 passed; 2,439 passed across `src/main/ssh`, `src/main/persistence` and `src/main/runtime/workspace-session`. |
||
|
|
f864223723 |
fix(remote-workspace): carry defaultTerminalTabsAppliedByWorktreePath through the normalizer (#16747)
`defaultTerminalTabsAppliedByWorktreePath` is declared on the wire session
(`src/shared/remote-workspace-types.ts:16`), emitted by the renderer's export
(`remote-workspace-session-projection.ts:101-134`) and read back by its importer
(`:195`) -- but `normalizeRemoteSession` rebuilds the session as an object
literal and never copied it. Measured on a snapshot carrying `{ '/r': true }`:
normalizeSnapshot(...).session.defaultTerminalTabsAppliedByWorktreePath
=> undefined
remoteWorkspaceSessionMatchesSnapshot(withFlag, withoutFlag)
=> true
Two consequences. `getRemoteSnapshot` normalizes everything the relay returns
(`remote-workspace-relay-sync.ts:29`), so a client could never receive the flag
even though the write path sends `session` verbatim. And
`patchRemoteWorkspaceSession` short-circuits on
`remoteWorkspaceSessionMatchesSnapshot` (`:51`), which normalizes both sides --
so a change that only sets this marker was invisible to the dirty check and
could never be written at all.
The marker is the sole guard on `applyDefaultTerminalTabs`
(`worktree-default-terminal-tabs.ts:34`), so losing it re-applies the whole
default-tab template over the user's tabs.
This has never worked in either direction since the field was introduced.
`normalizeRemoteSession` predates `372469b7528` under a different path -- it
lived in `src/main/ipc/remote-workspace.ts` until `15e1ba3f84a` split it out for
max-lines, and at `372469b7528` it already copied all four sibling optional
fields and simply never gained a fifth. No test caught it because that commit
tested the projection round-trip (correct in isolation) while the normalizer sat
in another file with no test of its own.
The new test file closes that gap two ways: a case per consequence, plus a
`Required<RemoteWorkspaceSession>` fixture asserted key-for-key against the
normalizer's output. Because the normalizer is an object literal, the next field
added to the wire type and forgotten there is now a compile error in this test
rather than silent data loss.
Wire compatibility: safe, but not by the plain "new optional field" rule -- this
is an already-declared field that was silently stripped on every read. Both
readers treat it as optional; the relay is not a reader at all
(`src/relay/workspace-session-handler.ts:145-151` stores `patch.session` as an
opaque record and returns it verbatim), so no relay version can strip it. A
half-upgraded pair is one-directional: an old client still drops the key on read
and behaves exactly as today.
Before: 3 failed | 1 passed. After: 4 passed.
|
||
|
|
2ac23c29be |
Add parent worktree selection when creating workspace (#15420)
* feat: select parent worktree for nesting when creating workspace Enable users to pick a parent workspace in the composer's Advanced drawer, organizing newly created worktrees hierarchically in the sidebar. The backend validates lineage relationships and gracefully retries without the parent if it becomes unavailable during creation. * feat: select parent worktree for nesting when creating workspace - Record app-picked parents as manual actions, not CLI-flag equivalents, ensuring the same user action carries consistent cleanup semantics across hosts - Gracefully retry without parent if the selection goes stale, warn the user instead of failing - Refactor create into modules: parent resolution, payload building, state merge * Allow selecting parent worktree when creating workspace - New parent worktree picker filtered by execution host and project - Preserve concurrent local writes to lineage by comparing per-record state instead of key-set membership * Allow selecting parent worktree when creating workspace - Rename "Parent workspace" label to "Parent worktree" - Filter candidates by execution host and project to prevent nesting across hosts - Wire parentWorktreeId through composer state and creation request pipeline - Update translations and add new copy for nesting-related messages |
||
|
|
2060af4de9 |
test(automations): pin thrown-error and mid-wait disconnect behavior for ordered waits
Answers the open review thread on run-completion-watcher error handling: a thrown (non-timeout) waitForTerminal / hasTerminalAgentWorkedSince error propagates to the watcher's truthful dispatch_failed 'stopped watching' close and is never coerced into 'completed' or 'agent never started'; a mid-wait disconnect keeps the ordered wait pending (reconnect may still resolve it) and bounds it at the wait timeout. Also wraps the PTY waitForTerminal registration branch in the same cleanup try/catch the leaf branch already has, so an inspection throw after registration can never strand the waiter and its timeout. |
||
|
|
e8d14ec1e3 | fix(automations): identify dispatched agent turns | ||
|
|
f96dfd5810 | Extract CDP auxiliary commands (#16781) | ||
|
|
fc90a0b6c4 | fix(automations): bind completion to dispatched turn | ||
|
|
f48c038a93 | Extract local PTY launch helpers (#16778) | ||
|
|
e16f3943fd | Extract daemon launch path helpers (#16777) | ||
|
|
22f9f07468 | fix(automations): require agent start before completion | ||
|
|
2aec442f42 | Split Linear issue service by operation (#16770) | ||
|
|
f27ea956f2 |
Extract speech lifecycle and editor animation modules (#16767)
* Extract speech worker lifecycle helpers * Split editor feature-wall animation |
||
|
|
9a0a2b1c31 |
fix(orchestration): settle worker release without web layout state (#16842)
* fix(orchestration): tolerate missing terminal layout partitions * fix(orchestration): handle legacy release without layouts |
||
|
|
a22a26008b | Split Hermes cron run history (#16766) | ||
|
|
551fbb9ac7 |
Revert "feat(browser-preview): render remote HTML docs locally over an orca-preview scheme (STA-5557) (#16679)"
This reverts commit
|
||
|
|
6c0de76ced | Split port scanning and terminal helpers (#16765) | ||
|
|
7ee8b5e1a6 | Refactor lower max-lines modules (#16760) | ||
|
|
bb77a2b159 |
Add coverage for skill lock release and fix WebRTC test flakiness (#16846)
* test: add coverage for skill lock release and simplify WebRTC test - Add test for cleanupReleasedSkillInstallLock handling rmdir races - Improve error handling to cover all documented directory removal error codes - Simplify WebRTC egress test to use localhost addresses consistently * test: use network interface address for WebRTC egress probe - Discover the first non-internal IPv4 address instead of hardcoding localhost, allowing the test to work in CI and varied environments - Update proxy rules to use loopback designation for clarity - Bind UDP socket to all interfaces (0.0.0.0) to receive on the discovered address |
||
|
|
350423b7cb |
Speed up PR CI with path skips, native caches, and fewer shards (#16863)
* Speed up PR CI with per-job path skips and native caches Skip git-compat, xterm, packaging, and shell jobs when their inputs are unchanged, reuse the composite install action (including Windows node-pty cache), skip compiling the Windows CLI launcher on a cache hit, and cut the test matrix from 16x2 to 8x2 shards without dropping coverage. * Widen PR job skip prefixes for orcad browser and live shells Chrome session/tab modules and zsh/fish wrapper templates are inputs to required jobs the classifier previously skipped. Include that implementation graph so those jobs still run when the files they load change. * Fix CI cache safety and required gates * Build scriptless Windows addons explicitly * Preserve node-pty Windows support prebuild * Remove duplicated Windows launcher unit lane |
||
|
|
64f537f442 |
test(terminal): pin that Hangul is two cells under every unicode provider (#15477)
* test(terminal): pin that Hangul is two cells under every unicode provider #15192 turned out to be an upstream Antigravity CLI defect, but the investigation re-litigated Orca's Hangul cell width three separate times before ruling it out. This makes that negative result durable. The first test closes a real gap rather than restating the others. Nothing verified which provider actually ends up active in production: pane-lifecycle.test.ts asserts activeVersion=11, but its terminal mock has no _core, so activateOrcaTerminalUnicodeProvider can only ever take the fallback branch there. This asserts on a real terminal, in pane-lifecycle's order, that the Orca provider is reached. The sweep matters because it is what makes the width theory unavailable rather than merely unproven: all 11,172 precomposed syllables budget two cells under v6, v11 and the Orca provider, against both wcwidth and the packed charProperties bits. Even total activation failure leaves them wide. Also pins the wide-cell test oracle's disagreement with xterm on conjoining jamo U+1160..U+11FF. Only decomposed Korean reaches them and no fixture writes NFD today, so nothing mis-asserts now - but a repaint test would silently assert against a wrong oracle if anyone added NFD text. Refs #15192 * test(terminal): drop the version-sensitivity list as redundant Its output is a strict subset of the oracle-divergence test's, measured a different way, for about one bit of information across twenty lines. Refs #15192 * test(terminal): assert both widths in the Hangul oracle divergence, drop the pane-lifecycle-order claim The oracle divergence only asserted that xterm and the fixture disagree, not how: widening the fixture's jamo range from one cell to two left the expected list byte-identical, so the tripwire it exists to be would not have fired. Record both widths in the run key. Test 1 claimed to pin pane-lifecycle's activation order, but it mirrors that order rather than importing it — deleting the call at pane-lifecycle.ts:88 or moving it before loadAddon leaves it green (pane-lifecycle.test.ts covers both). Retitled to what it does pin: that xterm's live _core shape still reaches the non-fallback branch. Also oxfmt. |
||
|
|
81ae98e10d |
fix(mobile): honor host worktree create retention (#16342)
* fix(mobile): honor host worktree create retention * fix(mobile): cover malformed worktree retention policy * fix(mobile): fail closed on malformed retention policy * fix(mobile): fail closed on missing dedupe ttl |
||
|
|
6ba6d58cd2 |
fix(orchestration): route @agent messages by resolved identity, not terminal title (#16237)
* feat(agent-status): add the pane agent identity resolver
Four ladders answer "which agent is in this pane" independently — the tab icon, the
open-tab/search occupant, the sidebar title rows, and the sidebar hook-row fallback — and they
disagree. Two consult the terminal title before the launch record, so a string Orca parsed
outranks a fact Orca owns.
resolvePaneAgentIdentity is the single ranked answer. Two rules, one of which is not an ordering:
1. Evidence is ranked by how directly it observes the process; a display title is last.
2. Each observation carries the runId of the agent run it describes. Evidence from a superseded
run is INELIGIBLE, not merely outranked.
Rule 2 is the part reordering could never supply. A completed hook naming A plus a title naming
B is either a bug (hook right, title stale) or a legitimate pane reclaim (title right) —
identical signals, opposite correct answers. Run ids make them different facts: in the bug both
belong to the current run; in the reclaim the hook belongs to a previous one. That pair ships as
a test asserting the two produce opposite answers from the same evidence.
Missing run ids are treated as eligible. Absence means "this peer does not publish them", not
"this is stale", so an old host's rows are never blanked. Sibling evidence is opt-in so
pane-scoped consumers cannot inherit another pane's agent.
No consumer imports this yet; each migrates separately with its own evidence.
Verified non-vacuous: reversing the authority order fails 10 of 18 assertions and removing the
run filter fails 3.
* fix(agent-status): close three resolver contract holes found in review
**Duplicate evidence of one source resolved by array order.** `eligible.find(...)` returned the
first match, so two live hooks naming different agents were settled by input position — the exact
property this resolver exists to remove. The original order-independence test only used DISTINCT
sources, so it never exercised it. Conflicting same-class evidence now returns null with
`ambiguousAt`, and does NOT fall through to a weaker source: letting a title answer whenever two
hooks disagree is worse than saying nothing.
**A bare numeric runId collided across authority restarts.** `incarnation` is a total order only
within one `authorityId` (agent-status-observation.ts states this), and the id is regenerated per
authority instance, so a restarted host counting from its own floor would report `1` and match an
unrelated live run 1. The run key now carries its authority, and evidence from a DIFFERENT
authority is treated as incomparable — kept, like an absent key — rather than as stale.
**Title stayed reachable by consumers that authorize writes.** Ranking it last makes misuse
unlikely; `minimumSource` makes it impossible. An action consumer passes `'launch'` and weaker
evidence is dropped before ranking, so routing or delivery cannot name a target from a parsed
string even by reordering its inputs. Display surfaces omit it and are unaffected.
Also restores the generic agent-vocabulary parameter, which lives on the routing branch and was
lost when this branch was rebased.
Each fix is mutation-verified: first-match restored fails 3, ignoring authority fails 1, dropping
the floor fails 2. The authority test was itself vacuous on the first attempt — both sides used
`incarnation: 1`, so a resolver ignoring authority still passed on the numeric compare. It now uses
differing incarnations.
The remaining review finding, that `process > launch` has no freshness bound, is NOT fixed here:
it needs an observation timestamp the evidence type does not yet carry. Recorded rather than
silently dropped.
* fix(orchestration): route @agent messages by resolved identity, not terminal title
`@claude` picked its recipients with `buildAgentNameRe('claude').test(title)`, so any pane whose
TITLE contained the word received Claude's messages. Terminal titles carry task text, and people
describe agent work in them, so this is the ordinary case rather than a contrived one: the
recorded title "Switch Claude and Codex off the load balancer… - grok" is a Grok pane that
received both @claude and @codex. Misdelivered instructions, not a cosmetic slip.
The cause is that `RuntimeTerminalSummary` carried no identity at all — `title` was the only
identity-ish field on it, so routing by title was the only option available. Fix the input:
- `RuntimeTerminalSummary.agentIdentity?: TuiAgent` — optional, host-resolved from launch and
foreground-process evidence the host owns, with the title ranked last and contributing only
when the evidence parser finds an unambiguous name. A title that merely mentions an agent
yields no evidence, which is the whole point.
- `resolvePublishedPaneAgentIdentity` in `src/shared` rather than inside the runtime class, so
the decision is testable without a runtime and so routing, delivery and the UI cannot drift.
- Groups match `agentIdentity`; the title matcher and its bespoke Cursor predicate are deleted.
Unknown fails closed. `agentIdentity` is absent when the host predates the field or had no
evidence beyond the title, and delivery is an action: not delivering is visible and recoverable
(the sender sees no recipients), while delivering to the wrong agent is neither. The optional
field is additive, so an old client simply ignores it (wire rule 1).
This is also the first real caller of the evidence parser and the identity resolver.
Tests: 27 in groups, 8 for the publisher, 3 RPC fan-out cases updated to the new contract. The
`@cursor`-must-not-match-"text cursor blink" hazard is now excluded structurally instead of by a
per-agent predicate.
Verified non-vacuous by mutation: swapping the process/title ranks fails 2 publisher assertions,
and reverting groups to title matching fails 15 of 27. One earlier mutation silently failed to
apply after formatting reflowed the block — the file was checked before trusting the result.
* perf(runtime): reuse terminal title during summary build
* fix(orchestration): refuse title evidence when publishing identity for routing
Rebuilt on current main so this carries the hardened parser from #16148 and the corrected
resolver from #16157 (authority-scoped run keys, no order-dependent duplicate resolution).
Applies the resolver's new `minimumSource` floor at the publisher. What this publishes authorizes
an action — routing decides which real agent pane receives a message — so ranking title last is
not enough; the floor removes it from consideration entirely, and no amount of reordering by a
caller can bring it back.
The trade, stated because it is a real capability loss: a hook-less agent over SSH that Orca did
not launch, and whose foreground process the host cannot read, is no longer addressable by @agent.
Accepted because a message delivered into the wrong agent's prompt is unrecoverable while an
undelivered one is visible — the sender sees zero recipients. Whether real panes actually carry
launch/foreground evidence is the open question, and is what live validation must answer.
* fix(pty): preserve agent identity on daemon reattach
* fix(runtime): retire stale pane agent identity
* chore: normalize runtime types formatting
* fix(agent-status): identify a pane from its own hook, not from how it was started
Two defects, one cause: identity was inferred from the outside instead of read from the agent.
**Hook evidence was never plumbed in.** The publisher considered `process`, `launch` and `title`
and contained zero hook references — while the resolver ranks `live-hook` first. The top rung of
the ladder was never connected.
That made identity depend on Orca having launched the agent. Most agents are started by typing
`claude` or `codex` at a shell, which leaves no launch record. On macOS the foreground process
still names them, so the gap was invisible. On WSL the Windows host reads the foreground process
as `wsl.exe` — the distro wrapper, not the agent inside it — so those panes had no signal at all
and became unaddressable by `@agent`.
A hook is the agent reporting itself, so it survives both: no launch record needed, and no
dependency on reading a process across the WSL boundary.
**`launch` outranked `completed-hook`.** Ranking is now by TENSE rather than by how authoritative
a source sounds:
present: live-hook > process
past: completed-hook > launch > sleeping-session > sibling > title
A launch record is an event, not a state — it stays true after the agent exits, which is why a
pane reused after closing its agent kept reading as the old one. A completed hook at least proves
the agent actually ran in that pane; a launch record only proves Orca tried to start one.
Neither rank was covered: all 392 existing tests passed unchanged after reordering. Mutation now
fails 2 on the old order and 4 with hook evidence removed.
Known remaining gap, deliberately not papered over: a hand-started WSL agent with no managed hooks
has no identity signal at all. Restoring a title guess there would reinstate the misdelivery this
PR exists to prevent.
* fix(orchestration): restore title as the last resort, not a forbidden source
An earlier revision passed `minimumSource: 'launch'` so routing could not see a title at any rank,
reasoning that a display string must never authorize a write. That conflated the evidence parser
with the raw substring match it replaced.
`buildAgentNameRe('claude').test(title)` was the misdelivery. `collectAgentTitleEvidence` returns
null on exactly those shapes: "Review the Claude session-history fix" on a Codex pane yields
nothing, and "Switch Claude and Codex off the load balancer… - grok" yields grok from its owner
suffix. Ranking title last is therefore sufficient; refusing it is not necessary.
Refusing it had a real cost. An agent a user starts by hand inside an Orca WSL terminal has no
launch record, no readable foreground process (the Windows host sees `wsl.exe`, not the agent in
the distro), and — until managed Codex hooks install there — no hook either. An unambiguous title
was the only thing left, and dropping it made that pane unaddressable by @agent where the previous
code could reach it. That is a regression, and most agents are started that way.
End-to-end coverage added at the routing layer with title allowed: @claude still does not reach a
Codex pane whose task text names Claude, @codex still does not reach a Grok pane whose task text
names Codex, and a pane identified only by an unambiguous title is reachable again.
* revert(agent-status): keep launch above completed-hook until run keys exist
Reverts the tense-based reorder from this branch. The reasoning behind it was sound as far as it
went — a launch record is a past event, not an observation, which is why a reused pane kept reading
as its previous agent — but it fixed one staleness by opening a worse one.
A completed hook is past tense too, and without an agent-run key it never expires at all. Ranking
it above `launch` lets a stale hook from a previous agent outrank the launch record Orca stamped
for the process running NOW. pane-agent-owner.ts already says this in its own comment: "Ranking
launch/live-hook above the completed/sleeping records keeps a genuine pane on its real agent and
stops a stale record from hijacking it."
The reorder belongs with authority-scoped run generation, which is what makes any past-tense
evidence expire. It is staged in the migration plan rather than shipped here.
What this branch keeps: hook evidence feeding pane identity (so an agent a user starts by hand is
identified from its own report rather than needing a launch record), and title restored as a
genuine last resort behind the evidence parser.
* fix(runtime): guard the pane key so terminal.list survives a non-UUID leaf
`makePaneKey` throws on a leaf id that is not a UUID. The hook-evidence lookup called it unguarded
inside `buildTerminalSummary`, so a single such leaf took down `terminal.list` for the whole list
rather than degrading that one pane — 136 tests across 5 files, and the native code-quality gate
tripped separately on a duplicate test title.
Both were mine, and both were caught by CI rather than by me: I ran the focused suites before
pushing instead of the affected directories.
* fix(runtime): declare published terminal agent identity
* fix(runtime): demote completed hook identity evidence
|
||
|
|
fe71487895 | test(main): isolate deferred worktree watcher setup (#16836) | ||
|
|
efb4050f4d |
fix(ai-vault): index Cline sessions (#16814)
* fix(ai-vault): index Cline sessions * fix(ai-vault): constrain Cline session discovery |
||
|
|
419e3b4496 |
Fix terminal reads that flatten composer drafts into output (#16711)
* fix(terminal): separate composer drafts from read output Rendered screen reads treated cursor-line suggestion overlays as PTY output. Detect composer-owned text from cell attributes and cursor context, remove it from tail, and expose it as structured draft metadata. * fix(terminal): handle wrapped composer overlays * fix(terminal): preserve draft wrapping and tail alignment * fix(terminal): preserve composer wrap boundaries * fix(terminal): preserve draft continuations with middle dots * fix(terminal): recognize configurable Codex status lines |
||
|
|
913509edeb |
fix(orchestration): prevent slow worker-start stalls (#16300)
* Extend orchestration agent submission timing budgets * fix(orchestration): preserve mutation recovery identity * fix(orchestration): preserve recovery executable identity * fix(orchestration): keep worker starts and recovery commands safe * test(orchestration): cover federated worker preflight * fix(orchestration): harden mutation recovery * fix(orchestration): redact dispatch recovery credentials * chore: preserve upstream skill dialog formatting * test(orchestration): stabilize agent prompt submit e2e * fix(orchestration): validate federated start receipts * perf(runtime): cache unchanged prompt verification tail * fix(orchestration): reject worker-start timer overflow * fix(orchestration): normalize worker-start timeout defaults * fix(orchestration): normalize worker-start readiness budgets * fix(orchestration): normalize federated readiness timeout * test(runtime): tolerate current-main degradation exports * chore: preserve current-main orcad formatting * chore: drop unrelated formatting carryover |
||
|
|
b241a68ae4 |
Fix worktree identity collisions across hosts (#16691)
* fix(workspaces): add collision-safe worktree identity * fix(workspaces): read worktree metadata per host and repair ambiguous identities The canonical identity store landed write-only: getWorktreeMetaForHost had no production callers while setWorktreeMetaForHost kept the legacy projection only for the first known owner, so a second host's edits persisted and were never read back. Wire the listing paths through host-qualified reads. An ambiguous alias was also unrecoverable — reads returned undefined and writes threw forever, and the throw escaped the detected-worktree loop, emptying the whole repo's sidebar. Fail open onto the most recently active instance instead. - collapse ambiguous aliases deterministically and persist the repair - reclaim identity rows in the metadata GC so they cannot outlive their locator or resurrect onto a worktree recreated at the same path - drop every host's rows when a locator is removed outright, not just the owner's - honour an explicit instanceId so the stale-lineage rotation guard still works - scope a rename to the moving host; other hosts keep their own locator - prefer the project host setup matching the repo's own execution host, so a repoId registered on two hosts no longer stamps the wrong one durably - reject an unencoded `|` in a host id, the invariant the alias delimiter needs - drop the never-populated hostGeneration from the canonical key * fix(workspaces): close remaining identity review gaps * fix(workspaces): close remaining review gaps * fix(workspaces): address review and CI regressions * test(workspaces): update host-qualified metadata expectations * fix(workspaces): preserve ambiguous identity records * fix(workspaces): snapshot metadata during listing * test(workspaces): mirror listing metadata snapshot in windows fixture * fix(workspaces): preserve identity routing for metadata writes * fix(workspaces): scope stale metadata cleanup by host * fix(workspaces): rekey identities on SSH readoption * fix(workspaces): fail closed for ambiguous board ids * perf(workspaces): snapshot metadata across catalog listing * fix(workspaces): retain neighboring manual order updates * test(workspaces): cover ambiguous board id index * fix(persistence): harden host-qualified worktree metadata * refactor(shared): split project host setup lookup * refactor(workspaces): simplify host-qualified metadata |
||
|
|
0a72e71dae |
Fix STA-5661: prevent rolldown const-folding of bridged exports (#16869)
Rolldown miscompiles `export let fn = noop` by const-folding initializers and dropping setters. Refactor to use null-initialized impl vars behind wrapper functions instead, and add test to prevent regression. |
||
|
|
249d93bc5d | feat(browser-preview): render remote HTML docs locally over an orca-preview scheme (STA-5557) (#16679) | ||
|
|
f400f8fd5f |
fix(macos): opt out of press-and-hold so held keys repeat (#14746) (#15589)
* fix(macos): opt out of press-and-hold so held keys repeat (#14746)
macOS routes press-and-hold to the accent picker unless an app sets
ApplePressAndHoldEnabled=false for its own bundle, so holding j in vim
inserted one character instead of repeating. Orca never set it.
Written at most once, and never over an explicit value: `defaults read`
is domain-scoped and exits 1 when the key is absent, which is the only
way to tell "unset" from a deliberate false — Electron's
systemPreferences.getUserDefault reports false for both. A recorded
decision in userData keeps a later launch from re-clobbering a user who
deletes the key to get the accent picker back.
* docs(macos): record the revert hazard and CI's macOS test gap
Two things a reader of this module cannot otherwise know.
A revert leaves the key written in every user's domain forever. AppKit reads
the plist, not this file, so removing the code alone keeps press-and-hold
disabled for everyone who ran an affected build. The sibling period-substitution
module carries the same warning because that fix was already lost once this way.
And the real-binary test file that pins the defaults(1) exit-code semantics this
design rests on never runs in CI: the e2e workflow and both unit-test jobs are
ubuntu and windows, and the only macOS runners in the repo are build and
packaging jobs that run no tests. Those six tests plus the real-bundle e2e case
pass on a developer Mac and execute zero times in a green PR, so the comment
should not imply enforcement that is not there.
Refs #14746
* feat(macos): let users turn the accent menu back on (#14746)
Orca disables press-and-hold for its own preferences domain so held keys
repeat. That is the right default, but the way back was a `defaults write`
buried in a source comment: nothing in docs/ or the README mentioned it, and
the preference is per-application, so it silently takes the accent picker
away from the Markdown editor and every other text field too.
Terminal -> Advanced now carries a "Character Accent Menu" switch, macOS and
desktop only. A web client cannot write a macOS preference for the machine the
user is looking at, so the control and its search-index entry are both gated on
that, not on the client's platform alone.
Precedence, which is the part that is easy to get wrong: the setting is
`undefined` until the user touches it, which is what keeps a hand-run `defaults
write` in charge for everyone who never opens the toggle. Once used, Orca owns
the key and writes exactly what the switch asks for -- `ApplePressAndHoldEnabled`
*is* the accent-menu switch, so it maps straight through with no inversion. The
choice is compared against `appliedSetting` in the existing decision record
rather than against the domain, so a `defaults write` made *after* using the
toggle is still the newer choice and survives the next launch. Re-asserting the
value every launch would have reintroduced the clobbering the record exists to
prevent.
The write lands for the next launch, since AppKit reads the preference as the
process starts, so the toggle shows the same restart banner the window-blur
setting uses. That banner is now a shared component, keeping its original
translation keys.
docs/reference/macos-press-and-hold.md records the precedence rules, the
`defaults read` rationale, the revert hazard, and the fact that none of this
executes in CI: every macOS job builds or packages and runs no tests, so the
real-binary and e2e coverage here passes only on a developer Mac.
* docs(macos): stop asserting when AppKit re-reads the press-and-hold key
Five places stated "AppKit reads the preference as the process starts" as
fact. That is the reason given for requiring a relaunch, and it is not
something this change ever measured.
Evidence points the other way: terminal emulators that register this key
after their process has started get key repeat in that same launch, which a
read-once-at-startup model cannot explain.
The relaunch requirement itself still looks right, but for a different and
verifiable reason: the write goes out through a separate `defaults` process,
so this app's own cached copy need not observe it. That is what the comments
now say, with the AppKit question left open rather than answered.
Refs #14746
* docs(macos): correct the startup comment's launch-timing claim
The comment said this call site is "the last point that can still matter for
this launch", which contradicts the rest of the module: the write is assumed
to land for the next launch because it goes out through a separate `defaults`
process. Reported on the PR by @innocarpe, who also supplied the replacement
wording.
Co-authored-by: innocarpe <innocarpe@users.noreply.github.com>
* refactor(macos): probe press-and-hold through the shared spawn chokepoint
`src/shared/child-process/child-process-import-boundary.test.ts` forbids a
direct `node:child_process` import outside its allowlist, and the allowlist only
shrinks — so this module moves to `runProcessSync`, which exists for callers
that genuinely cannot await. This one runs before `app.whenReady()`.
`runProcessSync` returns a non-zero exit instead of throwing it, so the
three-way read decision is re-expressed against `ProcessResult`: exit 0 is an
explicit value, exit 1 is a missing key, and a timeout, a signal kill, any other
exit, or a child that never started all stay 'unknown'. The throw path is now
inside `interpretDefaultsRead` so a spawn failure is reachable from a test
rather than hidden in an untested catch, and the write checks the exit code —
a refused `defaults write` no longer looks like success.
Both boundary-test failures were the same import: with it gone the offender
count returns to 155, so no ratchet baseline is bumped.
* Revert "feat(macos): let users turn the accent menu back on (#14746)"
This reverts commit
|
||
|
|
3558cf943f |
fix(codex): heal WSL hooks before typed launches (#16535)
* fix(codex): heal WSL hooks before typed launches * test(codex): keep launcher fixture type-safe on Windows * fix(build): list codex-home-wsl-env in the CLI typecheck project `managed-home-shell-preflight.ts` is already in the CLI project's include list and now imports `wslCodexRuntimeHomeForGuestHome` from `src/main/pty/codex-home-wsl-env.ts`, which the list did not cover — TS6307, so the CLI typecheck failed on every push. Added the single module rather than a `src/main/pty/**` glob: it is a 31-line leaf with no imports of its own, so it does not widen what the CLI bundle can reach. * fix(codex): converge the two WSL hook install lanes onto one writer Two independent readiness reviews agreed the Orca-terminal boundary holds, but Codex Sol found a P1 the other rated P2: the new just-in-time repair raced the existing relay installer and the two produced DIFFERENT hook and trust representations for the same managed home. Two unserialized writers emitting different formats is worse than the bug this PR fixes, because it fails intermittently rather than cleanly — a pane works or does not depending on which lane won. - Relay Codex installs now delegate to the runtime-home writer, so there is one canonical representation instead of two. Redirected scripts use the runtime path, the readable wrapper, and the prepended group. - `installForRuntimeHomeSerialized` puts every asynchronous WSL caller for a given home on one queue (`wslInstallQueues`), so concurrent panes cannot interleave writes. Also rewrites the stale pin test the new `-x` guard broke. It asserted the defect — "would run the impostor if the preflight carried an unqualified command name", expecting the hijack marker to exist. The guard is a security improvement, so the test now asserts the contract: an unqualified preflight is skipped and the marker is never written. Rewritten to the new behavior, not loosened or deleted. 818 tests pass across the affected suites; typecheck clean. The changed-file quality gate could not run locally — its pnpm engine-warning JSON parser fails under Node 26 — so CI covers it. The boundary both reviews verified is untouched: paired/relay/mobile clients stay hard-blocked from the RPC, params remain shape-locked to the managed home suffix with traversal rejection, nothing is written outside the managed home, and macOS/Linux stay inert. * fix(codex): serialize resolved WSL hook homes * fix(codex): recover managed WSL homes after restart * fix(wsl): translate Codex preflight through WSLENV * fix(cli): cover bounded WSL Codex repair * fix(codex): coalesce duplicate WSL hook repairs * fix(codex): verify reconstructed WSL homes |
||
|
|
9635e6822f |
Normalize project catalog rows defensively (#16826)
* wip: normalize untrusted project catalog rows at load boundaries
* fix(catalog): make ProjectHostSetup field types true at the ingest boundary
Crash 3bcc5be3: a setup row whose repoId arrived null reached Settings'
projectByRepoId memo and threw on .trim(). The type said `string`; persisted
JSON and remote hosts on other versions can disagree.
Normalize project/setup rows where untrusted data enters typed code — the
persisted-state load (marking dirty, which is the migration), profile
transfer reads, the repo-derived projection, and the renderer's IPC/RPC
ingest and adoption steps — instead of re-guarding each consumer. Also
covers `setup.path`, whose identical crash is on the sidebar render path.
Coercion only: never drops a row, never adds or removes an optional key, and
returns input references when a row already conforms, so selector and useMemo
identity is unchanged.
* refactor(catalog): drop the `as` casts the normalizer introduced
A change whose thesis is "stop the declared types from lying" should not use
`as` to paper over types.
The four source casts all came from the row normalizers returning `readonly`
arrays into mutably-owned fields. Take and return mutable arrays instead, and
copy at the one caller that holds a readonly projection — where identity is
not load-bearing, unlike the persistence path, whose dirty check compares it.
Tests built deliberately malformed rows by casting a literal. Build a valid
row and `Reflect.set` the bad value onto it, which says outright that the
fixture violates its type; parse the non-array case from JSON, which is how
it actually arrives. Fixtures that were merely incomplete needed no cast at
all — `Repo` requires only the five fields they already had.
Also narrow normalizeLoadedProjectCatalog to the two fields it reads.
* refactor(profiles): validate untrusted profile JSON instead of asserting it
Removes the last introduced cast and the unsound pre-existing ones in the
file this change already touches.
The test cast is gone because narrowing normalizeLoadedProjectCatalog to the
two fields it reads made `{}` assignable on its own.
arrayOrEmpty and recordOrEmpty checked the shape and then asserted the
element type, which is the same "declared type is a promise the data does not
keep" problem this change exists to fix. Array.isArray already narrows on its
own, and a generic isRecord narrows the value part, so both assertions delete
outright. JSON.parse returns any, so annotating the binding beats asserting
its result.
The two remaining `as const` in project-host-setup-actions are untouched and
deliberate: a literal assertion narrows a type rather than overriding it.
* Make project catalog normalizers handle null rows defensively
Instead of crashing when corrupt or null catalog rows are encountered,
the normalizers now gracefully repair them with default values. Refactored
helper functions for clarity and added type predicates to improve type
narrowing.
|
||
|
|
cc384c5a3d |
fix(agent-hooks): post posix payloads as json (#11292)
* fix(agent-hooks): post posix payloads as json * fix(agent-hooks): mark header merged envelopes * docs(agent-hooks): describe header merge envelope * fix(agent-hooks): encode posix metadata headers * test(agent-hooks): update WSL JSON hook assertions * fix(agent-hooks): negotiate raw JSON transport * fix(agent-hooks): preserve packed metadata in POSIX shells * test(agent-hooks): include hook envelope in relay boundary inventory --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
fecdf0bde8 | fix(daemon): recover concurrent spawns after disconnect (#16818) | ||
|
|
7c3bfe72d7 |
fix(windows): stop a wedged process-table reader retaining a callback per cooldown (#16696)
* fix(windows): stop a wedged process-table reader retaining a callback per cooldown The vendored reader pushes every callback onto a module-global queue and drains it only when the request holding its `requestInProgress` latch completes. When a Toolhelp32 snapshot never comes back, that latch is stuck for the life of the process, so the 30 s cooldown -- which let one probe through per window -- bounded the rate of new callbacks but not the total: one more closure retained every 30 s, forever, plus a full 3 s deadline block on whichever caller drew the probe. Gate on the outstanding read instead. Once a read misses its deadline and has not called back, every further read is refused until that read's callback fires, which bounds retention at exactly one callback. Nothing is given up on recovery: a probe queued behind the latch could never have observed the drain anyway, whereas the stuck callback firing IS the drain, so the reader now resumes the instant it recovers rather than up to 30 s later. It matters more on a relay, which binds the bare addon with no JS queue to absorb the retries. Each read there is a `Napi::AsyncWorker`, so a wedged one holds a libuv threadpool slot for good and one probe per window would have pinned all four default threads inside ~2 minutes -- hanging every async `fs` and DNS call in that process, not just the process table. A wedge still does not engage the PowerShell fallback, and a wedged read still rejects rather than resolving empty, so "unavailable" stays distinguishable from "nothing is running" on every host. Fixes STA-5499. * fix(windows): invalidate stale reader deadlines on reset |
||
|
|
928d306b53 |
fix(agent-hooks): stop the Windows hook launcher spelling the AV-denied flag pair (STA-5237) (#16739)
* fix(agent-hooks): stop the Windows hook launcher spelling the AV-denied flag pair (STA-5237) `-WindowStyle Hidden` + `-EncodedCommand` is denied at CreateProcess by Kaspersky on Windows 11, whatever the payload decodes to. Bash reports it as `Permission denied` and every managed hook event fails, so agent status never arrives; the parent shell also briefly cannot spawn anything afterwards, so a denied hook can take the user's next command down with it. Measured on the reporting host (#16003), with a harmless `exit 0` payload: -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand 126 -NoProfile -WindowStyle Hidden -EncodedCommand 126 -WindowStyle Hidden -EncodedCommand 126 -NoProfile -EncodedCommand 0 (5/5) #16576 removed `-ExecutionPolicy Bypass`, which is the one flag of the three NOT in the signature, so hooks kept failing after that fix. The pair that has to stop being spelled is `-WindowStyle Hidden` + `-EncodedCommand`. Because the change is to the shared switch constant, it covers every site that spells the denied pair in one edit: Claude via `wrapWindowsPowerShellEncodedCommand`, gemini/cursor/droid/command-code/copilot via `wrapWindowsHookCommand`, the `runtime-home-hook-command` unsafe-HOME fallback, and the spaced-path fallback for codex/grok/devin/antigravity. Only a flag is removed, so parser and payload compatibility is unchanged for every executor: the string is still a PowerShell command line, still one self-contained token, still base64-shielded. The tradeoff, recorded rather than hidden: `-WindowStyle Hidden` was the shipped fix for #14815 (+#14828, #15117, #15447, #15767), and this removes it. Its suppression was never measured — #14825 confirmed it visually, #16576's author stated it "remains unverified on a real box", and #15506's author argued it cannot help a `.cmd` child with no console to inherit. The console is allocated by the parent chain, not by this command line. A live window measurement is still outstanding and is called out in the PR. Also adds `windows-hook-payload-delivery.test.ts` to the PR CI Windows leg, which had never run it. * test(agent-hooks): keep launcher token out of source grep |
||
|
|
642607bfa7 |
feat(diagnostics): name the code driving a React commit cascade (#16730)
* feat(diagnostics): name the code driving a React commit cascade React #185 reports blame whichever component dispatched after the root-global counter tripped. react-update-depth-attribution already tells the report that boundary_id names a bystander; nothing recorded what the real driver was. Count commits through react-dom's devtools commit hook — the only per-commit seam that survives minification. Profiler's onRender is compiled out of the production bundle, and a dependency-less root layout effect fires per render of its own component, not per commit (measured: a root effect saw 1 of 11 commits a leaf drove). Mirror React's own reset rule rather than a time window: a commit that leaves no sync lanes pending ends the cascade, and a different root restarts it. The steady-state cost is a mask, a compare and an increment, with no clock read and no allocation. Stack sampling arms only once a cascade is already deep, so ordinary work never pays for it. * fix(diagnostics): remove the install-order trap and guard the write path Adversarial and perf review of the cascade diagnostic: The install-order ratchet guarded the wrong thing. The observer self-installs at the bottom of its own module, so it only ran after its transitive graph evaluated — one new import reaching react-dom would have killed the diagnostic in production with every test green. The entries now import the import-free shim instead, which only has to make the global exist; wrapping the callback is timing-independent because react-dom re-reads it per commit. The store write probe called the sampler unguarded, so a throw there dropped the write on the app's universal write path. Guarded; the try/catch measured free at +0.005ns. Report the frames that name the driver instead of capturing eight and reporting one, arm the self-check on the paths where install fails, bind the sample cap to the write count rather than a V8-only API, and stop defining the devtools global for every test file to serve one. The cascadeRoot comment claimed a strong reference cannot retain; a WeakRef probe disproved it. It is still not a leak — the next non-cascading commit clears the slot — so the comment now says that instead. * test(diagnostics): close the ratchet holes guarding the cascade hook Adversarial review loop 2: The install-order ratchet only saw imports whose `from` shared a line with the keyword, so a multi-line `import { createRoot } from 'react-dom/client'` in the shim passed it — and that is the one edit that kills the diagnostic in production. 43% of files in this directory use the multi-line form. Scan the shim source directly as well as walking the graph. The 4000-char budget for the driver frames is bought by the key ending in `stack`, but the only test asserting that emitted its own literal key, so renaming the real one truncated the frames with the suite green. Assert the name the renderer actually emits. Also correct the comment on the `installed` placement: the self-check never reads that flag, it arms because it sits outside the try. * test(diagnostics): stop the shim ratchet firing on prose Adversarial review loop 3 caught two flaws in the guards added last commit. The source-scan regex used an unbounded `[\s\S]*?` after an anchor that also matched the shim's own `export type`, so it degenerated to "does the word `from` appear later in the file" — rewriting a doc comment to say "reads the hook from the global" failed the ratchet. A guard that fails on prose is a guard someone deletes, and this one is what stands between a reshuffled import and a silently dead diagnostic. Require a quote after `from`, tolerate comment obfuscation, and catch `await import(...)`, which makes the shim async so react-dom evaluates before the hook is installed. The 4000-char budget assertion matched `/stack$/i` against the raw key, but the real rule camel-splits first — so `driverstack` would pass while shipping truncated frames. Assert through sanitizeCrashReportDetails, resolving the key from the payload rather than hard-coding it. |
||
|
|
07b7e9e68d |
Open target=_blank links and unnamed popups in new Orca tabs (#16720)
* feat(browser): open target=_blank links and unnamed popups in new Orca t - Treat target=_blank as a new-tab request matching browser behavior - Route unnamed, featureless window.open() calls to Orca tabs instead of native popups - Add rate limiting to prevent page-initiated tab loops - Inherit session profiles when opening links to maintain isolation boundaries * fix(browser): deny new-tab window.open when renderer is destroyed Move deny action outside conditional to ensure new-tab intents are safely rejected even if renderer vanishes mid-open, preventing native popup fallthrough. Add test coverage and simplify comments. * Share page-initiated tab budget across opener popup tree Prevent pages from bypassing the new-tab rate limit by chaining popup windows. The page-initiated tab quota is now shared by all popups in an opener tree (root + named children), so child windows inherit their root's budget instead of each getting a fresh allocation. |
||
|
|
026389a3bc |
Suppress default quit on window-all-closed in DNS probe (#16683)
* Suppress default quit on window-all-closed in DNS probe Destroying the probe window awaits stopLogging, which yields to the event loop long enough for the default window-all-closed exit (non-macOS) to trigger before the result can be written. Preventing this default behavior allows the result to complete and exit cleanly. * Preserve Japanese Skills UI labels; fix test flakiness and deps - Add skipKeyPrefixes filter to Japanese phrase fixes to prevent automatic translation of UI labels (e.g., keep エージェント in Skills components) - Wrap timing-dependent tests in vi.waitFor to eliminate race conditions during reconciliation and scheduler boundary ticks - Complete useEffect dependency arrays to resolve React hook warnings * Fix updater startup scheduling test flakiness Set last update check to 23h ago instead of null to make timing deterministic. The startup check arms its own 24h timer; by pre-setting the last check time, only the result handler's re-arm can produce the expected check 24h later, eliminating race conditions. |