mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
225a47533dbfd7a76d17611d5c2000ee66f387bb
10389
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
225a47533d |
fix: preserve paired host sessions during startup residue cleanup (#18922)
* fix: preserve paired host sessions during startup residue cleanup
* refactor(persistence): tighten the paired-host retention pass
Dedupe the owner-key -> repo-id extraction the retention and seeding
passes both needed, and name the `runtime:*` check instead of repeating
the parse three times.
Reach the session walker directly by exporting
`addWorkspaceSessionWorktreeOwners` rather than fabricating a
`{ workspaceSession }` state slice to get at it.
Correct the docstrings: `runtime:*` also covers a serving host's own
partition, and the "authoritative removal" they promised has no product
caller on a paired client today, so say what the exemption actually
costs.
Add a survived-load assertion to the explicit-removal test, which
otherwise passed against the pre-fix sweep -- the partition was already
empty before the removal ran.
No behavior change beyond the docs and the test assertion.
|
||
|
|
a272a1eeaf |
fix: preserve terminal command probes across control frames (#19006)
* fix: preserve terminal command probes across control frames * refactor(terminal): make the command-probe output flag explicit Hoist the duplicated Output/OutputSpan predicate in the binary frame handler, and require carriesOutput on recordInbound so no future call site can silently disarm the command-response probe by omitting it. Rework the control-frame regression into a named table so the fit-override and driver-changed cases send valid event payloads instead of stubs that returned before dispatch. |
||
|
|
8dad5958c8 |
fix: preserve overlay focus during terminal mounting and layout (#18982)
* fix: preserve overlays during terminal mounting and layout * fix(terminal): stop a dismissed overlay from blocking pane focus Overlay primitives animate out (data-[state=closed]:animate-out, up to 300ms on sheets), so a dismissed dialog stays mounted and painted well past the point it should stop owning focus. The rAF-deferred focus in activateTabAndFocusPane lands inside that window, so revealing an agent from the dashboard drawer or a menu left the terminal unfocused. Treat data-state="closed" as gone, matching the [data-state="open"] convention already used by AgentDashboardDrawer and useWorkspaceBoardPanel. Also revert unrelated comment churn on scheduleRevealRepaint and note the new focus consumer in the hasVisibleOverlay doc comment. * refactor(terminal): scope the dismissed-overlay rule to pane focus Gate the data-state="closed" exclusion behind an ignoreDismissed option that only focusPanePreservingOverlays passes, leaving Escape semantics for the four existing hasVisibleOverlay callers unchanged. The focus race this fixes is specific to deferred focus (activateTabAndFocusPane defers by one rAF, landing inside the overlay's exit animation). Escape is synchronous and does not need the rule: Radix's useEscapeKeydown is capture phase, so every Escape caller runs while data-state is still "open". Avoids any behavior change on the Settings Escape path, which unlike the other three callers is bubble phase on document with no ordering guarantee. |
||
|
|
4ba8ddce48 | fix: prefer retained provider snapshots during hidden terminal recovery (#18972) | ||
|
|
1ef75d79d7 |
fix: avoid starting browser helpers just to reset absent sessions (#18952)
* fix: avoid starting browser helpers just to reset absent sessions * refactor(browser): tighten the session-reset skip guard and its tests Drop the platform and absolute-path guards: ownsSocketDirectory is already false on Windows and for inherited directories, and an Orca-derived directory is always absolute. Fold the empty-name and traversal checks into agent-browser's own session-name rule. Stop lstat state leaking between lifecycle tests, and pin the probed socket path so the skip test cannot pass on an unwired mock. |
||
|
|
deebe05ff0 |
fix: open editor rename after context menu releases focus (#18934)
* fix: open editor rename after context menu releases focus * refactor(editor): tighten rename focus-handoff comments and test setup Correct the rename-input focus comment that still credited the animation frame with outrunning menu teardown, clarify why the rename now runs from onCloseAutoFocus, and fold the repeated menu-close invocation in the tab tests into one helper. |
||
|
|
8d8b9dad78 |
fix: keep macOS shell ownership proof within recovery budget (#18932)
* fix: keep macOS shell ownership proof within recovery budget * fix: parse the shell-proof column set with its own anchored parser The narrower macOS capture (`pid ppid pgid tpgid stat command`) was fed to the shared lenient parser, whose optional tty/start pair has no `tty=` column left to absorb it. It then eats the head of any argv shaped `python 3 app.py` (parsing command as `app.py`, tty as `/usr/bin/python`), and turns a command-less row into a garbage pid/stat pair. Either can flip a shell ownership verdict, which is what gates dead-TUI recovery. Give the column set a named constant and a parser anchored to exactly those six columns, beside its `CHEAP_PS_ARGS` sibling. A capture that yields no rows now raises `empty_capture` rather than reading as a machine with no processes. Update the `confirmShellForegroundProcess` fixtures from the 4-column legacy shape to the 6 columns the darwin reader actually emits; that describe block already forces `platform=darwin`, so the stale fixtures were failing. |
||
|
|
e8496f810a |
fix(cmd-j): pass browser tab ownership into palette search (#18925)
* fix(cmd-j): pass browser tab ownership into palette search * test(cmd-j): cover restored browser recency in the ownership regression The same unifiedTabsByWorktree map that establishes host ownership also feeds lastActiveAt, which orders Open Tabs and renders the row's session age. That half of the fix had no coverage, so assert it alongside the execution host. |
||
|
|
c13d37036a |
fix(terminal): preserve ordinary foreground command names (#18882)
* fix(terminal): preserve ordinary foreground command names * refactor(terminal): reuse the non-shell foreground check in inspection Fold the duplicated isShellProcess call into one binding shared by the ordinary-name fallback and hasChildProcesses. No behavior change; the focused daemon inspection suites still pass. |
||
|
|
57d4f63ac3 |
test: refresh palette identities and structured-session journal fixtures (#19165)
* test: persist palette fixture names across inventory refresh * test: locate palette workspaces by host-qualified identity * test: supply journal activity clocks in branch-rename fixtures |
||
|
|
8b197ffdc2 | Update README downloads badge | ||
|
|
2e8fa3fe9b |
test: exercise packaged browser compatibility in scheduled CI (#19157)
* test: exercise packaged browser compatibility in scheduled CI * test: record final packaged workflow participation evidence * test: expose manual packaged revision and simplify executable check * test: reject missing package checksum assertion |
||
|
|
c49345d358 |
Fix native chat completion sorting and restored activity timestamps (#19144)
* Fix structured native chat completion sorting and timestamps * Preserve native chat activity across settled updates and host upgrades --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
b7b6ea3942 |
fix(native-chat): auto-rename the workspace on a structured chat's first turn (#19138)
* fix(native-chat): auto-rename the workspace on a structured chat's first turn Structured native chat (Claude and Codex) never reached the first-work workspace rename. The orchestrator has a single production caller, the agent-hook server listener, and structured sessions never set ORCA_PANE_KEY, so no hook event could ever be attributed to one. The renderer knew this and suppressed pendingFirstAgentMessageRename for structured launches at three sites, which also closed the gate the folder-workspace title rename depends on. The host's status feed already computes the exact edge: status 'working' with a latestPrompt normalized the same way the hook payload is, and a workspaceId that IS the worktree id. Publish that projection to the host, thread it out to the runtime, and hand it to the same orchestrator the hook path uses. Re-projections of state the host already knew (restore, an arriving subscriber) are flagged as replays and map to the orchestrator's existing isReplay gate, so a host restart cannot rename off a stale journal. One host and one journal serve both providers, so this covers Claude and Codex together. Verified in a live Electron instance, worktrees created through the real composer and prompts sent through the real chat composer: Codex langouste -> retry-helper-exponential-backoff Claude prowfish -> parse-csv-headers * fix(native-chat): preserve first-work rename across runtime and queued turns * fix(native-chat): skip branch rename for folder projects --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
41934759ea |
fix(windows): reject stale parent PID links in shutdown snapshots (#19149)
* fix(windows): reject stale parent PID links in exit snapshots * refactor(windows): share the walk's pid index in the stale-link filter Resolve parent links through the same index the descendant walk builds, so a table that repeats a pid answers both the same way, and drop the non-null assertion on the walk by keeping the "cannot see" null contract. Pin the two filter branches nothing exercised: the root surviving its own recycled ppid, and the root's start bounding a link whose claimed parent denied its creation time. * test(windows): pin the root creation-time floor and its tie The floor clause survived deletion: for a chain of timestamped rows the per-parent check already enforces order transitively, so it only does work below a row that denied its creation time -- admitted unchecked, and its children then find no parent time to compare against either. Cover that chain with a child at the root's exact timestamp, which a same-millisecond spawn produces routinely, and one that predates the root. Also pin that pruning a link drops the unidentified rows beneath it from the count, since a retained one would cap the verdict at unverifiable over a process the root never owned. Record why ties pass, what the floor is for, and the clock monotonicity the filter assumes. * docs(windows): say why the pid index is shared with the walk The index is not reused across the two calls -- the walk indexes the filtered array -- so name the actual reason: a repeated pid must resolve first-wins, the way the walk resolves it, rather than last-wins as a Map over the rows would. * docs(windows): describe why both pid lookups share one index * docs(windows): put each pruning rationale on the code it justifies --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
6fd03a74ef | fix(ui): ignore the persistent workspace list when detecting overlays (#18881) | ||
|
|
f7d5216016 |
Show provider activity in chat turn tails (#19055)
* feat(chat): show turn-scoped activity tail * fix(chat): keep turn activity broad * feat(chat): surface provider activity in turn tail * fix(chat): keep reasoning headline as activity and widen redaction A Codex reasoning summary streams as a bold headline followed by body text. Folding the whole summary into the tail leaked literal ** markers and body prose; only the first non-empty line is activity copy, and an unterminated bold header mid-stream is unwrapped too. Redaction used a hyphen for GitHub token prefixes (they use an underscore), and missed fine-grained GitHub tokens, AWS access key ids, JWTs, URL userinfo passwords, and bare token= values. * fix(chat): wait for a complete reasoning headline A bold headline still streaming has no closing marker yet; holding the previous activity copy until it lands avoids flashing a half word. * refactor(chat): drop bespoke secret redaction from activity copy Reference agent hosts render provider-derived status text unredacted; this table was the only one of its kind and its GitHub pattern matched no real token. Bounding and the reasoning-headline extraction stay. * Bound provider headline updates and clear activity on reconnect --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
ad4dc353f3 |
fix(native-chat): settle a structured send the provider proves it received after the ack window (#19140)
* fix(native-chat): settle a structured send the provider proves it received after the ack window A send waits a bounded window for the provider to echo the message it was given. On timeout the dispatch resolves `unknown`. The echo that arrives later IS matched — `recoverLateIdentity` uses it to repair the session's turn identity — but nothing tells the journal, and `unknown` is terminal there. The submission stays unknown for the life of the session. Two consequences, both reachable on any ordinary session: - The composer renders "Message delivery is unconfirmed." with a Retry, forever, for a message that was delivered and answered. - Retry redispatches, because the host only replays a recorded outcome unless `retryUnknown` is set, which that button is the only thing that sets. So the banner is a duplicate delivery armed and waiting for a click — and a user who believes the banner and resends is doing exactly that by hand. Every send made while a turn is already running takes this path: the provider does not echo a queued message until the running turn ends, which is far past the 10s ack window. Sends made while idle are unaffected, which is why this reads as intermittent. Carry the `clientMessageId` on the dispatch waiter and settle the journal submission `accepted` when the late echo proves delivery. Deliberately unfenced against the dispatch sequence: that fence decides which turn owns the identity, while delivery is settled either way. Already-terminal rows are untouched. * fix(native-chat): persist late dispatch receipts before session close --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
ade9718557 |
fix(native-chat): suppress provider user echoes in Claude and Codex (#19136)
* fix(native-chat): keep provider user echoes out of the conversation * fix(native-chat): retain input beside Codex skill context --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
c7bcfa750a |
fix: restore the full sidebar agent row for structured native chat (#19137)
* fix: restore the full sidebar agent row for structured native chat The host status feed projected only state, prompt, and agent type, so a structured Claude/Codex row fell back to the tab title and the agent-type label where a hook-reported row shows the running tool, the agent's last message, and the model. Project the tool line and the newest assistant prose from the journal, and take the model from the session record's acknowledged options. The tool scan stops at the live turn's lifecycle row and only runs while a turn is running, so an abandoned call from a crashed turn is never reported as live work. The assistant line is bounded to the shared preview cap rather than the hook field's 8 KB body: a streamed reply re-projects on every journal checkpoint, and the row renders one line of it. * fix: keep structured session status current --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
75c1f32f81 | fix(gh): log when gh/glab is killed at its deadline (#18555) | ||
|
|
51a17db7e3 |
fix(ui): keep source control headers readable in narrow sidebars (#19146)
* fix(ui): contain source control header actions in narrow sidebars * fix(ui): preserve source control headings and conflict status at narrow widths * chore(ui): rely on shared section toggle padding |
||
|
|
20eea184cc |
feat(native-chat): offer the link-action popover for chat links (#19130)
* feat(native-chat): offer the link-action popover for chat links A plain click on an http(s) link in a native chat transcript opened the system browser outright, ignoring the link-routing preference the same link honors in the terminal. Chat now shows the terminal's destination popover, with the modifier chords routing straight to a destination. The popover, its request type, the destination policy and the routed open move out of terminal-pane so both surfaces share one implementation; the catalog keys keep their original namespace because they carry shipped translations. Chat resolves its link owner from the session workspace (runtime, then SSH, unresolved stays unknown) so a remote transcript only offers Orca Browser when that host's managed browser route is eligible. The existing toggle now governs both surfaces, so it is retitled; with it off a chat link still opens on a plain click instead of going dead. * Fix native chat link popover lifecycle and keyboard anchoring * test(native-chat): use one store mock for link actions * fix: update reliability gate for shared link popover tests --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
a224e2da74 |
Improve cmd j ranking (#19005)
* Refactor Cmd+J ranking to semantic-first ordering with activity bucketin Replaces the old score-based ranking with a semantic-first contract that compares destination, recovery, word match, coverage, strength, and placement before using age buckets and recency to break ties. Adds explicit field roles (primary, secondary, alias, container), identity encoding, and activity-based bucketing so recent activity never overrides semantic relevance. Removes the substring-elision deduplication of secondary fields. This fixes the fixture where titles like "atlas-follow-up.md" beat recently active "Clarify Atlas action items". * Encode palette IDs and display secondary matches as badge - Structured identity encoding for consistent ID handling - Badge+tooltip reduces clutter of additional secondary matches - Reorder activation to refocus group after state updates * Encode tab palette identities to resolve collisions across hosts and wor - Use composite keys (executionHostId, worktreeId, tabId) to uniquely identify tabs - Validate tab accessibility before activation to prevent mutation on invalid state - Extract getActivatableBrowserWorkspaceTab for consistent browser workspace validation - Refactor workspace tab validation with stricter collision and ownership checks - Remove unused comparePaletteActivity and mergeCandidateSummaries functions * Update palette identity tests to use encodePaletteIdentity Replace manual command-item ID construction with encodePaletteIdentity() to include host and worktree context, ensuring tests match the encoding scheme. Also adjust component styling (flex-1→flex-auto) and make HighlightedText highlight class customizable for secondary match badges. * rm design doc * Use stable field identity and field objects for ranking optimization - Add proofIdentity field to enable consistent tiebreaking in matches - Pass field objects in FieldHit instead of fieldId strings - Encode metric keys as numbers via bitwise operations - Eliminate document lookups for field coverage calculation * Reject hostless tabs when worktree IDs are ambiguous When worktree IDs collide across hosts, hostless tabs cannot be safely attributed. Refuse activation to prevent accidental host switching. Improve badge accessibility by keeping it out of tab order and exposing secondary matches through screen reader text only. * Improve cmd-j palette ranking with token-count tiebreakers and identity Add containerOnlyTokenCount and recoveryTokenCount fields to distinguish entities when match quality is equal, enabling better ranking of results that rely on container fields or recovery mechanisms. Cache paletteIdentity in search results to avoid repeated encoding during sorting. Extract omnibox field filtering and open-tab capping into reusable functions. Optimize evidence-unit iteration to only process matched units. Strengthen worktree ambiguity checks to reject hostless tabs when IDs collide across hosts. * Add clarifying comments to palette ranking retention logic - Document why capPaletteSection retains the selected match - Explain retainedResultId's role in keeping keyboard selection visible - Clarify secondaryMatches exposes additional match offsets * Centralize palette identity and unify host ownership resolution - Compute palette identity at search result level instead of constructing ad-hoc - Include folder workspaces in palette ownership via getPaletteOwnershipWorktreeIds - Add duplicate detection to filter colliding tab, page, and file IDs - Refine ranking with containerOnly metric and source-order tiebreakers - Improve secondary matches badge accessibility for keyboard users * Route same-target SSH worktrees through paired runtime owners - Centralize worktree palette identity resolution via getPaletteWorktreeIdentity and getPaletteWorktreeExecutionHostId, which use runtimeOwnerEnvironmentId when present instead of physical hostId - Deduplicate worktrees by palette identity to keep same-target SSH worktrees distinct when paired with different runtime environments - Replace scattered getWorktreeHostIdentity calls with new palette-specific resolution functions across palette components and search logic - Fix accessibility: move badge out of tab order, expose extra matches through row text instead of interactive tooltip * fix static analysis |
||
|
|
5a46703ce5 |
fix(native-chat): stop seeding a stray terminal beside a chat create (#19123)
* fix(native-chat): stop seeding a stray terminal beside a chat create A native-chat worktree create activates with `providesInitialSurface: true`, meaning "I open my own primary surface, don't seed a shell". Activation only honoured that when there was no other activation work, so any repo returning a setup script fell through to `ensureWorktreeHasInitialTerminal`, which created a bare terminal purely to act as the primary tab before giving setup its own tab. The user landed on `Terminal 1` + `Setup` + `Claude Chat`. The bare terminal was never needed for a new-tab setup: `queueSetupAndIssueCommands` only uses the primary tab there to restore focus to it. Forward `providesInitialSurface` into seeding as `callerProvidesSurface`, and skip the shell when the launch work needs no host tab. A terminal is still seeded when something has to attach to it: a startup command, issue automation, a split-mode setup script, `createNewTerminalForStartup`, or configured default tabs. * Fix background native chat setup terminal seeding * Avoid passive terminal seeding during native chat launch --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
4120501979 |
perf(store): detect Zustand rerender churn the current audit cannot see (#19059)
* perf(store): detect Zustand rerender churn the current audit cannot see The app-store-performance audit only understood inline selectors passed to a hook imported literally as `useAppStore`, so three shapes went unlinted: - a selector referenced by name (`useAppStore(selectRows)`), including one hoisted below its call site — resolved now via a Program:exit pass - the sibling store hooks (`usePluginPanelsStore` and friends), matched by the use<Name>Store convention on local imports; React's `useSyncExternalStore` matches that shape and is excluded - a fresh reference nested inside a `useShallow` projection, which is the worst case of the three: the comparator runs on every write and can never match, so the memo silently buys nothing `no-nested-fresh-under-shallow` covers the last one. `src` is clean against all four rules today, so this is a ratchet rather than a cleanup. The write side stays undecidable statically — whether a `set()` reallocated for nothing depends on the payload — so it gets a runtime probe instead. withStoreIdentityChurnProbe counts writes that replace a field's reference while its value stays equal, and can name the calling site. Cost when disarmed is one boolean load per write, matching react-commit-cascade-write-probe. * perf(store): scope the churn probe's scan to the write's own keys recordWrite iterated Object.keys of the full post-write state, so the armed cost scaled with the store's top-level field count (hundreds) rather than the size of the write. `set(partial)` merges, so no field outside the partial can have changed. The wrapper now resolves a functional updater itself and iterates the resolved partial's keys. Same function, same argument, called once — there is a test pinning that, since calling it twice would double any work a slice does inside its own updater. A replace write drops absent fields, so that path still scans every field. Disarmed cost is unchanged: one boolean load. * perf(store): follow a selector one hop into its helper Review feedback: both the lint rule and the manual sweep it was checked against only looked at the inline selector body, so neither could see a fresh allocation made inside a helper the selector calls — and delegating to a module-scope helper is the idiomatic shape here. Two methods sharing a blind spot is not corroboration. The two fresh-reference rules now resolve a single hop into a module-scope helper. The predicate used across that hop is deliberately stricter than the inline one: it requires EVERY returned expression to allocate unconditionally, so the common `cache.get(k) ?? buildFresh(state)` identity-caching shape is not flagged. An unresolvable helper is left alone rather than guessed at. Still zero hits across 20,330 files, so this stays a ratchet. * perf(store): keep the churn probe off the shipped write path Review hardening for the churn probe and the widened lint rules. Probe: it no longer resolves a functional updater itself. Zustand keeps sole ownership of when and with what argument an updater runs, so the middleware cannot double-invoke it or hand it a stale state. Object partials still scope the scan to the write's own keys; updater and replace writes fall back to the full field list, which costs one Object.is per untouched field and nothing more, since the deep compare only runs on replaced references. store/index.ts installs the probe only when import.meta.env.DEV or e2eConfig.exposeStore is set, the same gate as __store exposure. Nothing in the app arms it, so a shipped build was paying a wrapper frame per write for a diagnostic it could never read. The cascade probe stays unconditional because crash telemetry arms it in the field. Site capture now skips any *-probe.ts frame; under the real composition the first non-node_modules frame was the cascade probe's wrapper, so every churn was attributed to react-commit-cascade-write-probe.ts:32 instead of the caller. Plugin: named-selector recording is restricted to module scope. A component-local `const selectRows = ...` used to overwrite the entry for a same-named imported selector and flag an unrelated useAppStore(selectRows). The any-branch and every-branch allocation predicates are one function with a flag, the Object.* static list is a Set, and import recording is a single pass. Tests: updater called once with live state, identical-state writes ignored, disarmed path forwards exact arguments without calling get(), full composition with the cascade probe (no drop, no double, correct site), and the module-scope shadowing case for the plugin. |
||
|
|
c00d20a8f2 |
perf(terminals): keep shutdown maps' identity when there is nothing to clear (#19112)
* perf(terminals): keep shutdown maps' identity when there is nothing to clear
commitTerminalShutdownState spread nine maps unconditionally. Sleeping a worktree
whose panes already exited is the normal case and clears nothing, so each map came
back with a new identity and identical contents.
ptyIdsByTabId is the costly one: six components select it whole, and
selectLivePtyIdsForWorktree memoizes per sidebar card on its identity, so churning
it rebuilt that record once per card. It also wrote a fresh [] for every tab even
when the entry was already an empty array.
Every map now uses the copy-on-write shape the four unread/input maps in this same
function already had.
Two correctness points the guards encode:
- an absent ptyIdsByTabId key is NOT an empty array; the spread this replaces
created the key, so only an already-empty entry may be skipped
- an absent pendingPtyShutdownIds owner count meant `delete` of a missing key,
which changed nothing, so those are skipped rather than copied
- a layout whose ptyIdsByLeafId is already empty keeps its entry instead of
getting a fresh {} with the same value
* refactor(terminals): fold the shutdown maps' copy-on-write into one record helper
Nine hand-rolled lazy-clone blocks become copyOnWriteRecord: delete of an absent
key is a no-op there, so the identity guard lives in one place. The two guards
that are not plain deletes stay explicit — ptyIdsByTabId must still create an
absent entry, and pendingPtyShutdownIds only decrements an existing owner count.
* style: format the shutdown identity test with oxfmt
Committed with --no-verify, so the pre-commit formatter never ran on it.
|
||
|
|
aa23747f34 |
perf(terminals): stop closing a tab from replacing maps it never touched (#19060)
* perf(terminals): stop closing a tab from replacing maps it never touched closeTab spread-then-deleted ~20 per-tab store maps on every close. A tab has an entry in only a few of them, so the rest came back with a new reference and identical contents, rerendering everything that selects them. Tab close is one of the most frequent actions in the app. The file already knew this mattered — unreadTerminalTabs, unreadTerminalPanes and the pending snapshot maps were hand-written copy-on-write, one with the comment "keep the same reference ... so unrelated closes don't force full-state selector re-eval". This extends that treatment to the rest, reusing omitRecordKey / omitRecordKeys, and gives activeTabIdByWorktree and tabBarOrderByWorktree the same copy-on-write shape their neighbours already had. Same keys removed, same values, same order. * refactor(terminals): route closeTab's pane-key sweeps through removePaneKeysByTabPrefix The four hand-rolled copy-on-write loops (unread panes, unread agent completions, last-input timestamps, cache timers) and the unreadTerminalTabs guard all reduce to the existing prefix-removal helper, which already preserves identity when nothing matches. Also asserts identity for the three unread maps in the map-identity test. * refactor(terminals): port closeTab to the merged omitRecordKeys API #19058 landed with omitRecordKey folded into omitRecordKeys, so this branch's 27 call sites no longer compiled once rebased onto main. They now go through one hoisted closingTabIds array behind an omitByTabId closure, matching the shape that PR established in the sibling teardown file, rather than allocating a fresh [tabId] at each site. |
||
|
|
b0a39c64da |
perf(selectors): stop two always-mounted selectors allocating per store write (#19113)
* perf(selectors): stop two always-mounted selectors allocating per store write
Both run inside useShallow, so their cost is paid on every store write, once per
retained worktree — not once per render.
collectBrowserPageIds returned a fresh [] for a worktree with no browser tabs,
which is the common case. NO_BROWSER_PAGE_IDS already existed two lines below for
exactly this reason but was only used on the disabled branch; the function now
returns it too, so the comparator takes the Object.is path.
selectWatcherReconciliationStoreInputs allocated a throwaway {} per tab just to
call Object.keys().join(',') on it, which is ''. It now checks for the record
instead.
Deliberately unchanged: that joined key string is NOT replaced with the record
reference. The join is equal across record-identity changes when the key set is
unchanged, so swapping in the ref would rerender more often and invalidate the
getWatcherReconciliationStoreInputsKey memo.
* perf(github): share the closed duplicate-picker's empty result
Two byte-identical selectors — one per task-page table row, one per open item
dialog — returned a fresh [] on the closed branch, which is nearly always. Under
useShallow that compares equal, so nothing was broken; it just forfeited the
Object.is fast path once per row per store write.
Only the closed branch is touched. The open branch still rescans workItemsCache
on every write, which is the larger cost, but fixing it needs a cache keyed on
that map's identity and the result has to stay live for optimistic patches —
work-item-fetch-actions.ts already preserves entry refs for exactly that reason.
Not free, so not here.
* refactor(github): share the duplicate-candidate selector and make the empty singletons readonly
|
||
|
|
2d770c8af7 |
perf(worktrees): stop worktree removal from replacing maps it never touched (#19058)
* perf(worktrees): stop worktree removal from replacing maps it never touched
applyRemoveWorktreeSuccessState spread-then-deleted about 50 store maps on every
worktree removal. A removed worktree has an entry in only a few of them, so the
rest were handed back with a new reference and identical contents — rerendering
every component selecting them, git status caches and split-tab layout included.
The sibling purge path already had the right contract (`return changed ? out :
obj`) inlined into nine near-identical closures. That contract moves to
omitRecordKey/omitRecordKeys, the removal cascade adopts it, and the purge
omitters drop their duplicated copies.
The one behaviour to preserve carefully: `{ ...undefined }` normalised an omitted
slice to `{}`, and some worktree-isolation callers do hand over states with
slices missing. The helper keeps that, so a nullish record still yields `{}`
rather than throwing on `in` or leaking undefined into the store.
* refactor(worktrees): fold removeWorktree cleanup onto one omitRecordKeys helper
Drop the single-key omitRecordKey twin and build the removal patch inline
from three scoped omitters (worktree / tab / file), keeping every purged
field and its why-comment. 273 -> 137 lines.
* style: format the teardown files with oxfmt
The review pass reformatted these with prettier — semicolons and double quotes —
which is not this repo's formatter. oxfmt --check failed on all three.
|
||
|
|
afce0c85cf |
perf(mobile): skip the agent-status projection join when nothing changed (#19115)
* perf(mobile): skip the agent-status projection join when nothing changed An agent-status ping replaces one entry and re-spreads the map, so the projection already reuses every unchanged entry's serialization. It then joined them anyway, which is O(total serialized bytes of every live agent status) — up to ~100KB of string rebuilt per ping at realistic agent counts, to produce a string that is only ever `===`-compared. When every entry was reused AND the entry count matches, the joined string is character-identical to the cached one by construction, so the cached string is returned outright. An added pane already fails the reuse test; a removal is what the count check catches; the sort makes a matching key set imply a matching order. Not a hash: the string feeds an equality test that gates mobile publication, so a collision would silently drop a publication with no later write to heal it. This is exact. Only covers the "map re-spread, no entry content changed" case. A genuinely changed entry still rebuilds; making that incremental is a design change. * perf(mobile): short-circuit the agent-status projection before the sort Compare the new map's entries against the cached Map (size + per-key identity) before sorting, so an unchanged re-spread skips the O(N log N) sort as well as the join, and refresh the cache's source identity on that path so a repeat call with the same map hits the identity early-out. |
||
|
|
ef7079b432 |
perf(tabs): keep tab-model identity when reconciliation changed something else (#19063)
* perf(tabs): keep tab-model identity when reconciliation changed something else The reconciliation gate fires when ANY of tabs / groups / active-group / layout / orphans changed, and then writes all of them. An orphan cleanup alone therefore handed unifiedTabsByWorktree, groupsByWorktree and activeGroupIdByWorktree new identities with unchanged contents, rerendering every component selecting them. Two halves: - writeBatchedWorkspaceRecordEntry spread the map even when the entry already held that exact value. It now returns the map untouched, and — importantly — does not claim ownership of a map it never cloned, so a later real change in the same fold still copies instead of mutating the caller's map. - the projection handed over freshly built arrays that were element-wise equal to the stored ones. It already computes tabsChanged and groupsChanged, so an unchanged one now passes the stored array back. Safe because the filter and the group mapping above both preserve element identity. An absent key is still stored, undefined value included; dropping it would change Object.keys, which the spread this replaces did not do. * perf(tabs): fold stored-identity reuse into validTabs/nextGroups Rather than computing validTabs/nextGroups and then separately substituting the stored arrays back in, make validTabs and nextGroups themselves resolve to the stored array when nothing changed. tabsChanged/groupsChanged then read as plain identity checks and the two stored* locals go away. Adds a projection-level test that an orphan-only cleanup leaves unifiedTabsByWorktree/groupsByWorktree/activeGroupIdByWorktree at their prior identities and omits layoutByWorktree. |
||
|
|
ad10cb5b83 |
perf(store): keep the repo list's identity through workspace hydration (#19057)
* perf(store): keep the repo list's identity through workspace hydration
buildRuntimeSessionPlaceholders opened with `repos.slice()`, so every workspace
session hydration handed the store a brand-new `repos` array — including the
common case where the session referenced no unknown runtime workspace and the
contents were identical. `repos` is selected whole at 46 sites, so each
hydration rerendered all of them for no data change.
The appends below already build a new array rather than mutating, and the
sibling `nextWorktreesByRepo` in the same function was already copy-on-write;
this just gives `repos` the same treatment. No consumer of the returned array
mutates it in place.
* perf(store): keep worktreesByRepo identity through workspace hydration too
addHydratedSshWorktreePlaceholders opens with `{ ...sourceWorktreesByRepo }`, the
same unconditional copy as the repos.slice() above it, in the sibling function the
same hydration calls. A session needing no SSH placeholder is the common case, so
worktreesByRepo got a new identity on every hydration with identical contents.
15 sites select that map whole, the sidebar worktree list among them.
* chore(store): tighten the copy-on-write comments in hydration placeholders
|
||
|
|
b8311d509a |
Revert "skills: rewrite the seven non-orchestration guides to one outcome-first standard (#18724)" (#19126)
This reverts commit
|
||
|
|
1478101342 |
fix(windows): unblock structured native chat by exposing process creation time (#18986)
* fix(windows): guard process creation times
* fix(windows): ask the relay's bare addon for creation times too
The relay addon build now emits creationTimeMs, but the runtime binding
for the bare addon still declared only CommandLine, so a Windows relay
host requested flag 2 and every row came back without a creation time.
That leaves captureWindowsDescendantSnapshot returning null and
verifyWindowsProcessIdentity false forever on those hosts -- the relay
half of the patch was unreachable.
Naming CreationTime in the adapter is safe because the bare addon is a
content-hashed relay artifact: it ships in the same immutable relay
directory as the bundle reading it, so it can never be older than the
code asking for the bit.
Also bound the win32 guard test on our own row, which the addon can
never fail to answer, so an unconverted FILETIME or a 1601-epoch stamp
fails instead of satisfying a bare count.
* fix(windows): make the compiled addon prove its own CreationTime support
CI caught the real defect: the win32 guard test read
isWindowsProcessStartTimeAvailable() as true and then found 0 rows
carrying creationTimeMs. Unlike node-pty, this package publishes a
prebuilt .node at the same build/Release path node-gyp writes to, so
pnpm patches the source tree and leaves that binary alone. A host then
holds a patched lib/index.js -- ProcessDataFlag.CreationTime and all --
over a binary that ignores flag 4, and neither a load check nor a path
check can see the difference.
So the binary now says so itself: addon.cc exports
supportedProcessDataFlags, lib/index.js re-exports it, and
- windows-process-tree-creation-time.cjs asserts it during install,
which is what forces a from-source rebuild. It is shared by the Node
probe in ensure-native-runtime.mjs and the Electron probe in
rebuild-native-deps.mjs, exactly as node-pty-job-ownership.cjs is --
the Electron half matters because that probe decides onlyModules, so
without it the packaged app would ship the stale prebuilt.
- isWindowsProcessStartTimeAvailable() gates on the reported bit, not
the enum. Believing the enum is worse than reporting false: the
descendant snapshot returns null forever and the exit proof latches
unverifiable while structured chat believes it has a reaper.
rebuildNodeRuntimeModules could not actually have rebuilt this package:
the patched binding.gyp includes deps/node-addon-api, which the tarball
does not ship, and node-gyp must run from the physical dir.
Also closes the relay repair path's divergence: repairCreationTimeSources
wrote the C++ but not the buildNode splat or the tree-node typing, and
assertPatchApplied checked neither, so a repaired tree passed as patched
with buildProcessTree silently dropping the field.
The guard test is unchanged.
* fix(windows): keep the process-tree patch LF-only
windows-process-tree-patch-contract.test.mjs requires the patch file to
carry no CR bytes. Regenerating through pnpm patch-commit emitted 199 of
them, because the creation-time change is the first to touch files the
package ships as CRLF (src/process.h, src/process_worker.cc,
src/addon.cc, lib/index.js, lib/index.ts, the typings) -- and #17886's
own hunks over binding.gyp and src/process_commandline.cc carry the rest.
Stripping them is safe and changes nothing the lockfile records: pnpm
hashes patches CRLF-normalized, so the digest stays
e66202cc623996d02040c93449eb9ae353fddadf426cb53202a59ee710ee6fe7 and now
equals the file's plain sha256 too. It also still applies -- verified
against a deleted store entry, not a warm one -- and the precedent was
already there: the previous patch was LF-only and had been patching
those same CRLF files all along.
ensure-native-runtime.test.mjs stages the siblings the script loads at
module scope into its temp project. The import walk added by #17886 sees
`from './x.mjs'` only, so the createRequire'd .cjs siblings still have to
be named, and this PR adds a second one.
---------
Co-authored-by: Merge Sim <sim@local>
|
||
|
|
d5613b8e24 |
fix(browser): select full URL on initial address bar click (#19118)
* fix(browser): select full URL on initial address bar click * fix(browser): preserve initial address bar drag selection |
||
|
|
59fe8266bd |
fix(orchestration): keep worker lineage across app restart (STA-6366) (#19121)
* fix(orchestration): keep worker lineage across app restart (STA-6366) Terminal handles are minted per process, so after a restart the projected parent (coordinator or creator) named a handle no live row carried and every worker rendered as a top-level row. The projection now resolves the parent from the durable pane keys (runs.coordinator_pane_key, tasks.created_by_pane_key) whenever the stored handle is not one this process minted, re-resolves it to the live handle for that pane, and omits stale handles so they cannot mismatch a row. The creator-pane incarnation gate is untouched: it still decides mutation authority, and display lineage no longer depends on it. Dispatch lookup also passes the pane identity so a worker's own dispatch resolves once its handle is reminted. * test(orchestration): compare lineage without the merged attention field |
||
|
|
6fcd82918d |
Update mobile 0.0.48 Android download links (#19117)
* Update mobile 0.0.48 Android download links * Update the mobile docs page APK link to 0.0.48 The docs page the READMEs link to still pointed at 0.0.46, two releases stale. --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
3da1c5b2b1 |
fix(ci): stop Android release notes exceeding the GitHub body limit (#19114)
* fix(ci): stop Android release notes exceeding the GitHub body limit
gh release create --generate-notes let GitHub pick the previous tag. Release
tags live on side branches, so 0.0.46 and 0.0.47 are not ancestors of main and
detection reached back to 0.0.44, generating four releases' worth of notes:
130413 characters against a 125000 limit, which 422'd the publish after a full
Gradle build. The span grows every release.
Pin the comparison to the previous mobile-android release (0.0.47 -> 81862
characters) and cap the body so an unexpected span can never fail the publish.
* fix(ci): fall back when release-notes generation returns an HTTP error
gh writes the JSON error body to stdout on a failed request, so the redirect
left it in the notes file. The non-empty check then treated that blob as valid
notes and skipped the fallback, publishing {"message":...} as the release body.
Gate on exit status instead. Also match the current tag literally when picking
the previous release, so the dots are not regex wildcards.
* fix(ci): reuse the shared character-safe release-body truncation
The byte-based cap could split a multi-byte character at the boundary.
config/scripts/create-draft-release.mjs already exports truncateReleaseBody
with the same 120000 cap and a truncation notice, and the desktop release path
uses it. Import is side-effect free; its main() is guarded.
---------
Co-authored-by: Merge Sim <sim@local>
|
||
|
|
15d0f8aedf |
skills: rewrite the seven non-orchestration guides to one outcome-first standard (#18724)
<!-- orca-pr-loc -->
<!-- Programmatic LoC summary. Do not edit by hand; rewritten on every commit. -->
| | Files | Added | Deleted | Net |
| :--- | ---: | ---: | ---: | ---: |
| Test | 6 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$544 | $\color{#cf222e}{\Huge{\mathbf{−}}}$49 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$495 |
| Prod | 36 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$1719 | $\color{#cf222e}{\Huge{\mathbf{−}}}$1703 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$16 |
<!-- /orca-pr-loc -->
## ELI5
Orca ships eight skill guides that agents read before running the CLI. Seven of them (everything except `orchestration`, which #16904 rewrites) were command catalogs that had drifted from the binary. This PR rewrites them so an agent reads the outcome, the done bar, and the safe-failure rule first, loads reference material only at the step that needs it, and never sees a command or flag the installed CLI does not define.
## What changed
- **Seven guides rewritten** to one standard: outcome spine first (Result / Done / Safe failure), conditions instead of case lists, one done bar, one autonomy envelope, references loaded at the point of use via `skills get <topic> --full`, every runnable invocation spelled `ORCA`. `orca-cli` is 424→260 always-loaded lines with three references (browser, automations, publishing); `orca-per-workspace-env` is 794→397 with five (provider-vercel, ssh-host, docker-ssh, windows-scripts, failure-modes).
- **Defects fixed in shipped guides:** `emulator camera` (no such command), iOS `permissions` (backend refuses it), Android pane described as "in development" (shipped in June), `relayGracePeriodSeconds: 0` documented as immediate teardown (it is unbounded), doctor `ok: true` hiding `warn`, an SSH exemplar setting both `jumpHost` and `proxyCommand`, a provisioned-root fetch from `origin`, the Linear unconfirmed-write rule keyed on four verbs when ten emit it. Linear and emulator descriptions dropped embedded commands and angle-bracket placeholders (651→329, 732→404 chars).
- **Generator bundles references.** `skill-guides/<name>/references/*.md` is appended to `--full`; `skills get` help says compact by default, full with references.
- **Stubs single-authored.** The resolver ladder, placeholder rule, and older-binary fallback shared by all eight installable `SKILL.md` files come from one `skill-stubs/_shared/cli-resolution.md` fragment composed by the generator. Projections were byte-identical before the content fixes.
- **Guards:** every `ORCA <cmd>` and flag in every guide and reference resolves against `COMMAND_SPECS` (this found the camera defect); descriptions ≤1024 chars with no angle-bracket tokens; reference routing checked both directions; an always-loaded size ratchet (300 lines) that guides may leave but never join. `orchestration` (440 lines on main) is recorded as an exception until #16904 lands its kernel.
## Relationship to #16904
Split out of #16904 so that PR carries only the orchestration guide. On main, `terminal send` has no `--wait-submit` / `--retry-request` and the orchestration kernel still carries the resolver ladder and worktree-selector rule, so this branch pins `accepted: true` for handoff receipts and leaves the orchestration pins where main has them. The merge in either direction is mechanical: #16904 rebased on this becomes a one-file `orchestration.md` change plus dropping the two exceptions.
## Standard
Compound Engineering's portable skill-authoring guidance (outcome spine, conditions not cases, pinned fragile commands with an ordered hatch, references at point of use). NVIDIA SkillEvaluator Tier 1 (`schema,pii,license,quality,unicode,lint`) was run on every guide; its deterministic checks pass, its template nudges (Instructions/Examples sections, 50–150 char descriptions) do not apply to Orca's stub architecture and were not applied.
## Testing
- `pnpm typecheck:tsc:cli` clean; `check:code-quality:changed` and `check:react-doctor:changed` 0 findings
- `pnpm verify:bundled-skill-guides` and skill-bundle manifest verify clean
- vitest over `config/scripts`, `src/cli/skill-guide-cli-parity.test.ts`, `src/cli/skills.test.ts`, `src/cli/specs/skills.test.ts`, `src/cli/help.test.ts`, `src/main/skills`: 240 files / 2,019 pass
- Live smoke on the built CLI of every `skills get <topic>` and `--full`, every emulator, linear, and vm verb named in the guides, and every projection's resolver, GNOME warning, and bounded fallback (done on the #16904 branch before the split; the guide bodies are identical here except the send-receipt vocabulary noted above)
## Deferred product decisions
Merging `orca-emulator` and `orca-emulator-android` into one skill with a platform branch; collapsing `linear-tickets` to a guide alias; a `skills get --reference <name>` selector so a gate table can load one file; a fresh-agent routing eval before trimming the `orca-cli` (1,015 chars) and `orchestration` descriptions, whose quoted triggers each fixed a routing misroute.
|
||
|
|
08b96ed1b3 |
Seed Cmd-J filter from sidebar scope (#19036)
* feat(palette): seed Cmd+J filter from sidebar show scope When opening Cmd+J, the palette's host and project filters now initialize from the sidebar's current Show scope, so results match the user's sidebar view. The palette can still be cleared or changed per open; sidebar never reads back palette filters. * refactor: pass app state to palette filter builder Let the builder function extract the sidebar scope it needs instead of requiring callers to destructure and pass individual properties. This reduces coupling and simplifies the data flow through the palette initialization lifecycle. * Make palette filter repo-granular to preserve sidebar scope Filter options now list individual repositories instead of grouping multi-repo projects into single rows. This preserves the exact repository scope shown in the sidebar when opening Cmd+J, rather than widening selections to entire projects. Removes per-field selection cap and stale-value reconciliation, simplifying the filter lifecycle. * Clarify filter naming and seed from sidebar scope on palette open - Rename projects→repositories in PaletteFilterModel for semantic accuracy - Rename rawFilter→filterState for clearer intent - Initialize filter from sidebar scope in local state, refresh on open - Remove redundant filter reset from selection lifecycle * Seed Cmd-J filter from sidebar scope and reset on close The palette now opens with the sidebar's host and repository scope applied. Filter changes are temporary: closing discards them, and reopening reseeds from the sidebar's current state. - Repository filtering is now granular (individual repos) - Support shared repository IDs across multiple hosts - Disambiguate duplicate repository names by path * Add comment clarifying Projects terminology Document the naming convention for repository-granular filter choices to help future maintainers understand why "Projects" is used as the user-facing term. * Remove redundant Escape press from worktree palette filter test |
||
|
|
d07c47593d |
feat(mobile): structured native Claude chat (#18741)
* feat(mobile): structured native Claude chat Mobile already spoke the structured agent-session protocol for Codex, and the host already had a Claude capability gate — mobile just never advertised it, so `projectAgentSessionTabsOut` stripped every Claude tab before it left the desktop. The structured lane in mobile/ turned out to be agent-agnostic already (shared reducer, message projection, option catalog, prompt tokens), so this opens the gate rather than building a second lane: - advertise `agent-session.structured.claude.v1` - resolve any structured provider in `resolveMobileNativeChat` via the shared `isAgentSessionHandleProvider`, instead of a `'codex'` literal - widen the `agent-session` route type off `'codex'` - route bare Claude launches through `agentSession.createSupport` like Codex, which still degrades to a terminal when the host refuses (remote, WSL, win32, managed-account mismatch, or structured chat switched off) Deduplicate the create envelope. Renderer and mobile each assembled the `agentSession.create` params by hand; the fingerprint has to be computed over the same fields the host recomputes, so both now build it in one shared `structuredAgentSessionCreateParams`. Mobile's Codex-only launcher becomes `createMobileStructuredAgentSession(client, worktreeId, agent)` and reuses the shared display-name map; two copies of a random-UUID fallback collapse into one. Answer grouped Claude questions. A Claude AskUserQuestion carrying more than one question — or one multi-select question — is emitted with the real content in `body.questions` and the flat `options` left EMPTY, so mobile rendered a card with nothing to tap and the turn stalled with no way out. Codex never emits this shape. The phone has room for one question at a time, so the group is answered as steps and submitted once, reusing the shared `encodeAgentSessionQuestionAnswers` / `isValidAgentSessionQuestionAnswers` rather than a second encoding. Prompt responses move into `useMobileStructuredPromptResponses` because grouped questions carry a multi-step draft the rest of the session does not touch, and the session hook was at the 300-line cap. Pin the mobile capability list against the host's parser bounds: it fails closed to NO capabilities when the array exceeds 64 entries, which would look exactly like an old client. Re-pin mobile-session-route-parity: the create-actions edit drops one runtime string literal and changes one nested function body. Ablated to confirm that file is the sole cause. * fix(mobile): derive the grouped-question draft instead of clearing it in an effect The React Doctor gate flagged the session-change reset as a state adjustment after a prop change, which renders the stale draft for a frame. Store the session the answers were collected in alongside them and check it on read, so a session switch drops the draft during render with no effect at all. * test(mobile): pin that grouped steps key apart when the questions read identically Claude can ask the same text twice in one group (once per file, say). The view keys the question card by its projected content, so identical wording must still key apart or step 1's checkboxes would be submitted as step 2's answer. * fix(mobile): harden grouped Claude question answers * fix(mobile): retry transient structured support probes * fix(mobile): preserve grouped prompt response compatibility * fix(mobile): preserve tokenless duplicate choice identity * fix(mobile): point the launch tests at the generalized create API The rebase onto #18697 brought its definitive-refusal tests in cleanly, but they call the pre-rename createMobileStructuredCodexSession, and mobile tsc excludes test files so nothing caught it. Retarget them and give the agent-copy test a code that is actually in the definitive allowlist - agent_session_refused now correctly stays unknown, so it never reached the failure copy it asserted. * test(mobile): re-pin route parity after the rebase onto main Main moved its own runtime-string pin to 547; this branch drops the 'codex' literal from the create-actions gate. Ablated against main's pins to confirm that file is the sole cause before re-deriving. --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
2283f8ba4e |
docs(orchestration): never pick a worker model the user did not name (#19109)
The sonnet examples were added for a test cohort. Orchestration must not choose a model on the user's behalf: pass --model only when the user named one, otherwise inherit the configured agent default. |
||
|
|
298571ad9f |
fix(codex): uncap app-server stdio records (#18590)
Co-authored-by: Merge Sim <sim@local> |
||
|
|
ebaa01e42c |
Recover branch compare on visibility change (#19021)
* Recover branch compare on visibility change Add recovery mode that reuses cached branch comparison data when the window regains focus instead of clearing results and forcing a refresh. This preserves the diff display during operations like rebasing that may cause the window to go to the background. * Retry failed branch comparison results Cached branch comparison results with error status are now excluded from the cache-hit check, ensuring they are retried rather than silently reused. This fixes missing diffs during rebasing. * Decouple branch compare recovery from refresh kinds Recovery is now a dedicated callback invoked independently on visibility changes, rather than a refresh kind. This allows pending recoveries to queue during in-flight requests, improving handling when the window regains focus during rebasing or other operations. |
||
|
|
0c33f58e8a |
fix(ssh-relay): daemon owns the endpoint credential; a losing start never rotates it (#19052)
<!-- orca-pr-loc -->
<!-- Programmatic LoC summary. Do not edit by hand; rewritten on every commit. -->
| | Files | Added | Deleted | Net |
| :--- | ---: | ---: | ---: | ---: |
| Test | 19 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$962 | $\color{#cf222e}{\Huge{\mathbf{−}}}$136 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$826 |
| Prod | 18 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$295 | $\color{#cf222e}{\Huge{\mathbf{−}}}$116 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$179 |
<!-- /orca-pr-loc -->
## Symptom
Live 2026-09-05 (Orca 1.4.198 client, Ubuntu host): both relay processes `kill -STOP`ped for 20 s, then `-CONT`. The client redeployed while the host was frozen. Its fresh daemon lost the socket bind (`Socket path already in use`) but had **already rewritten** `relay-<id>.sock.credential`. The surviving daemon kept its in-memory credential, so every later `--connect` got `Endpoint credential mismatch; closing socket`, then `Grace started … timeoutMs=0 … ptys=1, clients=0` every ~20 s, forever. Only a manual `kill -TERM` cleared it. Receipts: `review-archive/orchestration-v3-pr16904/smoke-receipts-t012b/E16,E17,E18,E24`.
Three independent defects kept the wedge alive; each is fixed at its own seam.
## Fix
**1. The relay daemon owns credential publication (race-free under two concurrent starters).**
`relay-daemon.ts` binds the socket first, then publishes via the new `src/relay/relay-endpoint-credential-publication.ts`: adopt a valid pre-existing file (older clients still pre-write), else mint 32 random bytes and write temp+rename at 0600. A start that loses the bind exits inside `listen()` and never reaches the file. Why this option and not restore-on-loss or a client-side write: the only process that can *prove* ownership is the one whose `listen()` succeeded, and that proof is atomic with the bind. The client-side pre-write (`ssh-relay-endpoint-credential.ts`) and the launch-command `chmod 600`/`icacls` are removed on POSIX and Windows. The racing test also exposed that macOS reports a mid-bind collision as `EEXIST` rather than `EADDRINUSE`; `relay-socket-ownership.ts` now treats both as "held or stale".
**2. The client distinguishes "no daemon" from "daemon present but not answering", and never rewrites.**
A credential refusal is now typed on the wire: the daemon replies `orca-relay-handshake-credential-mismatch` (same frame type, no new opcode) and the bridge exits **43**; `waitForSentinel` maps it to `RelayCredentialMismatchError`, which the takeover treats as handshake-refusal evidence exactly like exit 42. A relay that holds the endpoint but **never refused** (the stalled-host shape: kernel backlog accepts the probe, handshake gets no answer) is now `RelayEndpointUnresponsiveError`, routed to the relay-lost backoff instead of the terminal Reset Relay path. Silence is not a decision (`docs/reference/ssh-execution-boundary.md`).
**2b. Deploy honours the verdict.** The 40 s live run exposed that the `--connect` catch block in `deployAndLaunchRelay` predates the incumbent probe and swallowed both verdicts as "probe failed, launch fresh", so a fresh daemon was still launched over the live one (it lost the bind by luck, which is exactly the collision in the incident). Held and Unresponsive now propagate; the session backs off on Unresponsive and surfaces Reset Relay on Held. Red-first in `ssh-relay-deploy-incumbent-verdict.test.ts`.
**3. The daemon cannot be wedged by a rotated file, because nothing can rotate it.**
The credential lives in the content-hashed relay dir, and after (1) the only writer is the daemon that owns the socket, so the "file changed under a live daemon" state the incident depended on is no longer reachable in-product. The credential is therefore fixed for the daemon's lifetime, as a plain secret should be. A hand-edited file is refused with the typed reply until restored (tested). Startup adoption of a pre-written file applies an owner-only + same-uid rule (review finding): anything else is replaced by a fresh mint. An earlier revision of this PR also re-read the file on mismatch and adopted it; that was removed as unreachable machinery that turned the credential into a per-handshake file-ownership check.
**3b. Fail closed between bind and publication.** A client that arrives after `listen()` resolves but before the credential is set is refused, not admitted as `unproved`. Nothing can be delivered in that window today; the guard makes the boundary structural instead of an event-loop ordering fact. Red-first in `relay-reconnect-listener-credential-gate.test.ts`.
**Wire compat.** New optional handshake reply only; an old `--connect` hits `Unknown handshake type` and exits 1 pre-sentinel, which it already treated as a generic failure. New daemon adopts an old client's pre-written file; new client still passes `--credential-file` so an old daemon reads it as before. Absence of exit 43 is never used as evidence.
**Also.** `terminal create` on a reconnecting SSH host now says what to do instead of a bare `No PTY provider for connection "<id>"` (prefix preserved; the renderer matches it).
## Tests (red first)
- `src/relay/subprocess.test.ts`: two `--detached` starts race one socket + credential file → exactly one reaches the sentinel, loser exits 1 with `Socket path already in use`, file valid + 0600, a `--connect` reading it reaches `relay.status` and reports the winner's pid. Red before (both starters died: daemon required a pre-existing file), green 6/6 after.
- `src/relay/relay-endpoint-credential-publication.test.ts`: mints after bind; adopts a pre-written 0600 file; replaces a pre-written 0644 file with a fresh mint; refuses a stale credential with exit 43 while still serving the real one, and keeps refusing a rewritten file until it is restored.
- `src/relay/relay-reconnect-listener-credential-gate.test.ts`: a client in the bind-to-publish window is refused and never attached; after publication the right credential is accepted and a wrong one refused; a daemon launched without a credential file is not gated. Red without the guard.
- `ssh-relay-deploy-incumbent-verdict.test.ts`: live-but-silent incumbent → `RelayEndpointUnresponsiveError`, refused → `RelayEndpointHeldError`, and in neither case is `--detached` launched; a failed `test -S` probe still launches fresh. Red 2/3 without the deploy change.
- `ssh-relay-deploy-helpers.test.ts` (exit 43), `ssh-relay-endpoint-takeover.test.ts` (refused → Held even with no `lsof`; silent → Unresponsive, nothing unlinked or signalled), `ssh-relay-session-terminal-error.test.ts` (Unresponsive → `onRelayLost`, not terminal). Deploy/namespace/native-deps tests updated to assert the client writes **no** credential.
## Live proof
New `tests/e2e/ssh-docker-relay-stall-credential.spec.ts` (claimed in `run-ssh-docker-e2e.mjs` and PR source routing), two cases: `kill -STOP` every relay pid in the container, send input during the freeze, hold **20 s** (the incident's duration, which races the mux liveness timeout) or **40 s** (past it for sure), `kill -CONT`; assert status back to `connected`, same pty, same daemon pid, same credential inode and content, relay.log did not shrink (a relaunch truncates it) and has zero `Endpoint credential mismatch` / `Socket path already in use` lines, in-stall input delivered at most once.
Run output (local, fixture image `orca-e2e-ssh-relay:3a864c665ba2cefd`, `ORCA_E2E_SSH_DOCKER=1 SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 … --project electron-headless --workers=1`, head `c2c20fd994`; re-run identically on the final head after the credential-lifetime change, 2 passed (1.7m), same annotations, and the bind-to-publish refusal never fired):
```
✓ keeps the same daemon and credential across a 20s relay freeze (38.3s)
relay-processes-stopped: 2 relay-processes-continued: 2
bridge-pids-before-after: 480 -> 480
socket-clients-accepted-before-after: 1 -> 1
in-stall-input-delivered: 1
✓ backs off and reattaches, never relaunching, across a 40s relay freeze (57.5s)
relay-processes-stopped: 2 relay-processes-continued: 4
bridge-pids-before-after: 480 -> 1202
socket-clients-accepted-before-after: 1 -> 3
in-stall-input-delivered: 1
2 passed (1.6m)
```
Client log in the 40 s case shows the new path end to end: `Relay channel lost … reconnect attempt 1/6` → `Socket probe result: "ALIVE"` → `Socket reconnect failed … Relay failed to start within 10s` → `Relay endpoint incumbent: … verdict=live evidence=accepted-connection holders=unenumerable` → `Failed to re-establish relay … A relay still owns … but did not answer the handshake … Orca will retry` → `reconnect attempt 2/6` → `Reconnected to existing relay via socket`. The 20 s case never left the frozen bridge (same bridge pid, one accept), so it exercises the "silence is not death" side of the same race. The 20 s case passed 6/6 across the session; the 40 s case was red on the prior head (`Socket path already in use` + `Startup failed: listen EADDRINUSE` in relay.log from the swallowed verdict) and is green after 2b. Before the fix the same injection produced a fresh daemon that rewrote the credential and a survivor refusing every client.
The `relay-processes-continued` count exceeds `stopped` in the 40 s case because the timed-out client's `--connect` bridge and the loser-side processes are parked behind the frozen listener when `CONT` runs; they exit on their own once it resumes.
## Gates
`pnpm test src/relay src/main/ssh` 332 files / 3884 tests pass · `pnpm typecheck:tsc:node` clean · `check:code-quality:changed` 0 findings · `check:react-doctor:changed` 0 findings · `pr-e2e-gate-contract.test.mjs` 42 pass · no lint disables or max-lines bumps added.
## Noted, not fixed here
- `terminal list` `orphaned:false` / `terminal close` `ptyKilled:true` for a pane whose relay is gone (`orca-runtime-stop-explicitly-closed-tab-ptys.ts`): different seam, `@ts-nocheck` characterization-covered file.
- On a host with no `lsof`, a stalled relay still cannot be enumerated as the holder; it is now retried rather than declared held, but a relay frozen past the backoff budget still ends in the existing "reconnect manually" banner.
|
||
|
|
06a607a1d7 |
feat(orchestration): make multi-agent workflows durable (#16904)
<!-- orca-pr-loc -->
<!-- Programmatic LoC summary. Do not edit by hand; rewritten on every commit. -->
| | Files | Added | Deleted | Net |
| :--- | ---: | ---: | ---: | ---: |
| Test | 225 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$21666 | $\color{#cf222e}{\Huge{\mathbf{−}}}$2820 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$18846 |
| Prod | 348 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$17107 | $\color{#cf222e}{\Huge{\mathbf{−}}}$4706 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$12401 |
<!-- /orca-pr-loc -->
## ELI5
Orca now treats orchestration like a durable control plane instead of inferring success from terminal keystrokes. Agents can tell whether a prompt was accepted or a turn started, replay an ambiguous request without sending twice, and recover coordinator mail after a crash. Completed workers can be inspected, released, or retained, and their panes no longer auto-resume as if the work were still running.
## What changed
- **Run receipts** from `run-create/use/current/show/list` are the row without routing plumbing (`home_database`, `coordinator_pane_key`) and without the duplicate `binding` object.
- **`terminal send` receipts are honest and idempotent.** `input_accepted` and `turn_started` are the only stages; `--wait-submit` observes without resending; `--retry-request <uuid>` replays the exact request against the same process incarnation. A transport timeout keeps the retry ID; only a different runtime answering strips it. Value-less or non-UUID `--retry-request` is rejected on the CLI and the SSH shim.
- **Mailbox delivery is committed before wakeup.** Pointer writes are staged in the DB before any PTY byte, replayed once after restart, and never emit a naked Enter. The watermark that parks concurrent deliveries is released with the DB reservation. Restart rescans pointer-pending and `dispatch:` mailboxes.
- **Lifecycle is a guarded transition graph** (`lifecycle-transition.ts`) with a table-driven test over every caller edge. Task reopen/overturn stays in the public contract. A PTY exit during `worker-stop` is the stop succeeding, not a failure.
- **Worker lifecycle CLI:** `worker-start` (`--spec` creates Task + attempt in one call), `worker-show`, `worker-read` (provider transcript first, bounded terminal fallback with a typed reason, local/WSL/SSH), `worker-stop`, `worker-abandon`, `worker-release`, `worker-retain`, `worker-list` (rowid-fenced pagination, fleet liveness, `attention`, literal `nextAction`).
- **Release is an explicit ownership table** (`decideWorkerTerminalRelease`): only an `owned` resource can be settled, the archive is mandatory where reachable, and an owner whose process is proven exited can always get out of `retained` via `archive_status: unavailable`. User-taken-over, external, and transferred panes stay retained.
- **Settled-worker resume fence** (folds in #17651): a settled dispatch whose pane is still open is fenced at settlement, on stop/abandon/exit, and at startup; lifted on release, retain, takeover, and pane reuse.
- **Liveness is `live` / `unverifiable` / `exited` only**, from execution-host evidence. Fleet projection reads the evidence clock, not the relay delivery clock. A host-certified exit outranks the worker's settled state. `unverifiable` never authorizes stop, abandon, retry, or release, in code or in the guide.
- **Federation:** structured reads negotiate by `method_not_found` so every shipped host keeps transcript-first output; exited remote workers are closed before being reported closed; epoch fencing holds across peer restart, downgrade, and pairing rotation; no per-second forced capability probe.
- **Schema v35:** repairs databases stamped v34 by the pre-fix branch (mailbox_handle default, index predicates), drops the write-only `lifecycle_transition_receipts` ledger and five never-read v31 identity columns.
- **Schema v36:** `dispatch:<id>` mailboxes get a real consumer generation on `dispatch_contexts` and `remote_dispatch_attachments`, bumped and fenced in the same transaction on every re-attach (manual inject, worker-start, federated attach). A stale worker whose Dispatch moved to another process now gets `consumer_fenced` instead of silently acking the new worker's Delivery. Run mailboxes already worked this way.
- **Schema v37:** `dispatch_contexts` records its creator (`creator_handle`, `creator_pane_key`), so a coordinator's context-only self-dispatch is bookkeeping rather than a nesting parent; before this, one self-dispatch made every later `worker-start` from that coordinator fail the depth cap. Pre-v37 rows keep counting (fails closed).
- **Dispatch-mailbox ownership is checked, not inferred.** A `check` from a process whose pane no longer holds the Dispatch, or whose last Attempt was abandoned/failed and moved to another terminal, gets `consumer_fenced` instead of an empty inbox that reads as "no mail yet". `--peek`/`--all` stay readable. A paneless caller still gets `stable_pane_required` with the rebind recovery.
- **Liveness certification is stricter:** a `process_exited` stage whose termination reason is `unknown` (a stop that was issued but never observed) projects `unverifiable`, not `exited`. Federated `worker-show` carries the execution host's verdict and host kind instead of a local guess. A live, ready worker with nothing pending has `nextAction: none` rather than pointing at the `worker-show` that produced it.
- **Wire:** `workerShow` keeps `dispatch.task_id` next to `taskId` for shipped CLIs. `ask --json` uses the standard `{ok, result}` envelope like every sibling verb.
- **Migration start-version detection** treats the two v32 recovery columns as versioned. Before this, every shipped database stamped below 32 resolved to the v6 floor and replayed the whole chain (the v23 backfill synthesized 68 phantom retained workers on a real v30 profile). Verified on a copy of a real 62 MB v30 profile: starts at 30, no row delta, integrity ok, 11 ms.
- **Skill guide** rewritten as a ≤200-line kernel plus seven references, to the outcome-first standard (Result / Done / Safe failure first, conditions not case lists, one done bar, references loaded at the point of use). The canonical loop uses `worker-start --spec`, names `worker-list` for completion accounting, documents `--retry-request` / `request-show` / `--wait-submit`, and requires positive evidence before any stall action. The other seven guides get the same treatment in #18724, split out so this PR stays orchestration-only.
- **`rpc/methods/orchestration-*`** (126 flat files) regrouped into `orchestration/{worker,federation,messaging,runs,gates}/`.
## Why
User reports showed the same boundary failures: false `agent_prompt_stalled` causing duplicate sends (#15180), coordinators unable to trust screen scrapes, cold-parked terminals receiving a pointer without the submit, settled workers accumulating as live tabs and auto-resuming after restart, and no way to tell a stalled worker from a working one.
## Linked issues
Fixes #15180. Fixes #17935 (orchestration skill description is 866 characters; a guard now caps every bundled skill at 1,024). Supersedes #17651 (fence folded in). Advances #16660, #16522, #14907, #13047.
## Review record
This PR was reviewed adversarially after revival: eight independent lenses (lifecycle, mailbox, send, worker, federation, transcript, complexity, live ergonomics), each required to prove findings with a failing test. That produced 16 proven blockers, all fixed with red-then-green regression tests, followed by two re-review rounds and a third fix wave that caught 3 regressions introduced by the fixes and 7 fixes that missed their target; all closed. A final pass (five lenses incl. a live built-runtime smoke, then a re-review of the fix wave) found and fixed seven more, chiefly the stale-worker mailbox steal, the self-dispatch depth wedge, and the unproven-exit certification. Three independent Codex (gpt-6-astra) passes followed: the first found nothing new, the second found and fixed 3 defects (task-status reachability, WSL-local host classification, peer-capability epoch), the third found and fixed 6 (production PTY controller never installed settled writes, ambiguous in-flight pointer failures allowed duplicate replay, SSH/relay deadlines cut off a valid `--wait-submit`, stop-vs-exit race during inspection, and two release-recovery paths for vanished or exited terminals). The full record (findings, proof tests, triage, declines with reasons) is archived outside the repo.
**Rework after the live smoke.** A first live cross-host run on the shipped adhoc build (this Mac, a paired Windows host on the same build, a paired Mac on 1.4.195, and an SSH host) found a P1: a running local worker read `unverifiable`/`missing_status` because the fleet snapshot rows lacked the terminal handle the matcher keyed on. A 59-row failure table over every bug fixed during review showed the same two classes recurring: a fact dropped in transit through optional fields, and two authorities for one fact. Two blind designs (Opus, Codex) converged on the same mechanisms, and the scoped tranches landed here with red-then-green seam tests from the real producer to the real consumer, faults injected only at the transport or hook-ingest boundary:
- **Settlement (data-loss class):** one three-valued `WriteSettlement` (`accepted | refused{reason} | unverifiable{reason, bytesHandedToTransport}`) from the SSH multiplexer through daemon client, providers, controller, to pointer staging. No boolean, no rejection-as-third-state. The two silent degrades that fabricated a handoff are deleted; a provider that cannot settle refuses before any effect. Pointer text and Enter share the contract; a partial flush is `unverifiable`, never `refused`.
- **Evidence identity (false-liveness class):** fleet agent-status evidence is a tagged union (`binding: worker | pane | unresolved{reason}`, `clock: observed | delivery`) minted once at ingest, so a hook row captured on one process incarnation can never bind to a later dispatch on the same pane. The matcher's `!worker.paneKey ||` defaults are gone. One host-scope parser replaces two.
- **Small pre-merge items:** `capability_unsupported` from an old peer is no longer relabelled `host_unavailable`; a producer census test asserts every agent-status consumer path projects a pane-only hook row as `live`.
Two ergonomics defects the second live run surfaced on a real database are fixed here too: a pre-v3 dispatch already marked `completed` projected as `outcome_unknown` / `requiresAction: true` forever (three copies of the outcome ladder disagreed on legacy rows; now one resolver, legacy `completed` reads `succeeded` with nothing to act on, legacy `failed` stays actionable on the failure), and an unscoped `worker-list` enumerated the entire database (now defaults to the Run bound to the calling terminal, `--run` overrides, and the receipt's additive `scope` field says which).
A third live round on the shipped adhoc build of `b082443e1f` (same four hosts) plus an unscripted run in the user's own prompt style (a plain Claude Code shell, `/orchestration`, three workers, zero errors, bound-Run default confirmed) found two more branch defects, fixed with red-then-green tests: a worker freshly started on a paired server projected `unverifiable`/`host_indeterminate` with `requiresAction` for ~3 minutes, including after its own `worker_done`, because the host's federation observation returned `missing_liveness_verdict` for any PTY the liveness register had not yet swept (the host now reads a connected pane it owns locally as `live`; disconnected or SSH-scoped panes stay `unverifiable`); and six pre-v3 completed rows still carried an `input` category because settling through the task-status path or `failDispatch` never closed the Dispatch's pending question threads (both paths close them now, and schema v38 closes threads already pending on settled rows). The guide's `worker-start` examples now show `--model sonnet`, since an omitted model inherits the launcher's default.
A Codex adversarial pass on the tranche diff found one real design hole (identity minted at read time instead of ingest, now closed) and two daemon settlement paths that threw instead of settling (fixed). Two `@ts-nocheck` runtime mixins on these paths were extracted into checked modules; the repo-wide `@ts-nocheck` count is unchanged at 171.
Deletions during review: ~1,900 lines (write-only ledger, unread columns, dead v1 archive path, test harnesses shipped in prod, duplicated liveness and state-machine copies, self-capability checks that were compile-time true).
## Testing
- `pnpm typecheck:tsc:node|cli|web` clean
- `pnpm run check:code-quality:changed` 0 findings; `check:react-doctor:changed` 0
- `pnpm verify:bundled-skill-guides`, `verify:skill-bundle-manifest`
- full `pnpm test` on the integrated head: 72,332 pass / 292 skipped; the only failures were three non-PR files (two zsh live-shell suites hit a node-pty spawn-helper ENOENT while a concurrent native rebuild ran, 44/44 in isolation; `release-checkout.unit.test.ts` is a known 30 s load timeout that passes in isolation on `origin/main` too).
- CI on
|
||
|
|
7ac194a634 |
Add persistent turn-scoped chat activity indicator (#19044)
* feat(chat): show turn-scoped activity tail * fix(chat): keep turn activity broad --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
c36c23df5f |
fix(chat): stop terminal focus recovery from stealing Cmd+C in the Chat UI (#18751)
* fix(chat): preserve message copy focus * fix(chat): scope covered-xterm focus guard to the chat leaf Chat view mode is a tab flag, but only the chat leaf's xterm is covered. In a split chat tab with a terminal leaf active, the tab-level guard skipped the terminal's resume focus and the tab-wide deferred focus then landed on the covered chat xterm. Decide per pane: resume and window-wake read the active pane's container, and the surface focus query skips leaves hosting the chat root. * fix(chat): close covered terminal focus fallbacks --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
3be526c5e6 |
test: cover SSH reattach replay and enable deterministic Codex CI (#19106)
* test: cover SSH replay replies and run deterministic Codex restore scenarios * test: register replay probe unit command in reliability gate |