mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
946627f2cedb0d76a7b6026d3fbb459ff2cbfd50
7894
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
946627f2ce |
fix(runtime): route runtime filesystem commands by resolved execution host (#18325)
`ResolvedRuntimeFileTarget` carried `connectionId?: string` and no host id, so `undefined` spelled three different answers at once — "runtime: host", "unresolved" and "genuinely local". Its sole resolver read `store.getRepo(worktree.repoId)?.connectionId` and never looked at `worktree.hostId`, which outranks every repo row, so one arbitrarily chosen row decided the execution host for ~30 filesystem dispatches. This is #18307's defect in the same file family; it was deliberately left out of that PR rather than doubling an already-36-site diff. The target now carries `executionHostId: ExecutionHostId` (never null, never optional), resolved through `resolveWorktreeHostRouting` — the same adapter #18307 added — and dispatched through #18296's `resolveFilesystemRouteForHost`. Dispatch sites call `requireRuntimeFileProvider`, where `null` means exactly one thing: the host is `local` and the read happens here. Four answers that used to collapse into one: - `ssh:x` with a rival row on `ssh:y` — routes to x. Previously the first row won. - `local` with a surviving `connectionId` — a row contradicting itself; no SSH connection is handed out. - `runtime:<env>` — throws `ExecutionHostNotDispatchableError`. Its repo row's connection names a target in the *server's* namespace; reading it here reaches a same-named target on this client. - rival rows disagreeing with no worktree host — `worktree_execution_host_unresolved`, matching the launch and Git paths rather than guessing a row. Two further reads stop degrading. `assertRuntimeFileMutationExpectation` recomputed the host from `connectionId`, so a client's host expectation could pass against a host the workspace never named; it now compares the resolved host. And the cross-workspace terminal tap coalesced `knownWorkspaceTarget?.connectionId ?? connectionId`, so a sibling workspace resolved as `local` inherited the origin worktree's SSH target and statted a local path on the remote box; a non-optional host id replaces rather than coalesces. An unreachable SSH host still throws `SSH_FILESYSTEM_PROVIDER_UNAVAILABLE_MESSAGE`; loss of contact is never evidence of locality (docs/reference/ssh-execution-boundary.md). Quick-open listing and path search keep degrading to empty for an unreachable host — that is a false negative, not a local answer — and now do so only for a host that really is remote. The whole `runtime-file-commands-*` family carries `@ts-nocheck` from a mechanical class split, so removing the field could not raise the compile errors that made #18307 safe. `runtime-file-command-target.ts` is deliberately checked, and a ratchet test stands in for the errors the family cannot produce. No wire change: `ResolvedRuntimeFileTarget` is main-process internal, and the SSH watcher-release and grant keys are byte-identical to before. |
||
|
|
21210aad34 |
fix(native-chat): make structured Codex launches race-resistant (#18251)
* fix(native-chat): cancel close-racing structured launches * fix(native-chat): make structured launches observable and recoverable * fix(native-chat): reconcile merged session tab publications * refactor(native-chat): unify host snapshot versioning * refactor(native-chat): complete launches from host snapshots * fix(native-chat): replay unknown launches by intent * fix(native-chat): guard duplicate launches and bound sync recovery * test(native-chat): type owner fixture * test(native-chat): type owner fixture * fix(native-chat): back off structured session resubscribe * fix(native-chat): fence delayed local session snapshots * fix(native-chat): retry initial session sync safely * fix(native-chat): refresh before sync retry * test(native-chat): cover folder sync cursor cleanup * fix(native-chat): retry failed structured session subscriptions --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
40d9927f01 |
fix(native-chat): show images in Codex structured chat (#18266)
* fix(native-chat): render structured image refs from their runtime owner * fix(native-chat): keep transcript image keys stable * fix(native-chat): memoize the image runtime owner * fix(native-chat): keep image preview observation scoped * fix(native-chat): resolve runtime-only image owners * fix(native-chat): retain image preview cache leases --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
d5750648c2 |
fix(runtime): route runtime Git by resolved execution host, not repo connectionId (#18307)
`RuntimeGitTarget` carried `connectionId?: string` and no host id, so `undefined` spelled three different answers at once — "runtime: host", "unresolved", and "genuinely local". Its sole resolver read `store.getRepo(worktree.repoId)?.connectionId` and never looked at `worktree.hostId`, which outranks every repo row, so one arbitrarily chosen row decided the execution host for 36 downstream dispatches. The target now carries `executionHostId: ExecutionHostId` (never null, never optional), resolved through the shared rule that landed with #17909/#17919 and dispatched through the host-keyed routes from #18296. Dispatch sites call `requireRuntimeGitProvider`, where `null` means exactly one thing: the host is `local` and the command runs here as free functions. Four answers that used to collapse into one: - `ssh:x` with a rival row on `ssh:y` — routes to x. Previously the first row won, which is the reproduced cross-host leak. - `local` with a surviving `connectionId` — a row contradicting itself; no SSH connection is handed out. - `runtime:<env>` — throws `ExecutionHostNotDispatchableError`. Its repo row's connection names a target in the *server's* namespace; dialling it here reaches a same-named target on this client. - rival rows disagreeing with no worktree host — `worktree_execution_host_unresolved`, matching the launch path rather than guessing a row. An unreachable SSH host still throws `SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE`; loss of contact is never evidence of locality (docs/reference/ssh-execution-boundary.md). `resolveWorktreeLaunchHost` keeps its exact signature and now delegates to `resolveWorktreeHostRouting`, the same resolution answering "which host is this on" rather than "what may this client dial" — the git target needs the first question because `local` and `runtime:` are two different non-SSH answers. No wire change: `RuntimeGitTarget` is main-process internal, and the SSH and local model-discovery host keys are byte-identical to before. `RuntimeFileTarget` has the same defect in ~30 filesystem dispatches and is deliberately left for a follow-up. |
||
|
|
9cda5a9dc0 |
fix(worktrees): stop a resolved-worktree snapshot answering for repos it never saw (#18295)
* fix(worktrees): stop a resolved-worktree snapshot answering for repos it never saw `listResolvedWorktrees` caches one fleet-wide snapshot for RESOLVED_WORKTREE_CACHE_TTL_MS (1s) and reuses it on time alone. Nothing invalidates it when a repo is registered, so for up to a second after a repo row lands, every caller reads a snapshot computed before that repo existed -- and reads the gap as a verdict. The visible failure is the SSH skill install. `resolveSkillSshTarget` resolves a workspace-scope destination through that snapshot, so installing into a worktree on a host connected moments earlier threw `skill-install-workspace-not-found`: the client asserting a remote workspace is absent on the strength of client-side bookkeeping that had never looked at the host. That is the shape `docs/reference/ssh-execution-boundary.md` rules out -- absence from a client-side set is not evidence about the execution host. It made `tests/e2e/ssh-skill-installation.spec.ts:108` fail 3 runs in 4 locally and deterministically in the Docker SSH lane, where connect-then-install lands inside the one-second window every time. The snapshot now carries the repo-registration revision it was computed under and is only reused while that revision still holds. The counter is the one `bumpLocalWorktreeScanGeneration` already advances on every repo add, removal and update, so the check is O(1) and cannot drift from the mutation sites. * fix(worktrees): key the snapshot on repo mutations only, not on generation reads Two things the headless-reattach lane surfaced. The revision I keyed the snapshot on was `generationSequence`, which `getLocalWorktreeScanGeneration` also advances when it mints a key for a repo id nothing has scanned yet. That is a read, not a mutation, so a read path could discard a snapshot that was still perfectly valid -- the mirror image of the staleness this fixes, and a way to make a lookup fail that would otherwise have succeeded. The counter now advances only where the scan generation is actually bumped: repo add, removal, update, and scan-cache invalidation. Separately, `pty-restore-record-seeding.test.ts` primed the cache by writing its private `resolved` field with a literal spelling out `worktrees`, `platformByRepoId` and `expiresAt`. That literal is a second copy of the cache's freshness contract, so adding a field to the real entry left the fake one failing the check: the primed snapshot was rejected, resolution fell through to a real scan, and the headless fixture -- which has no git -- got `selector_not_found`. It now primes through `getSnapshot` so the cache stamps its own entry and the two cannot drift again. The revision never moved during that test (0 before and after), so nothing was being invalidated; the fake entry simply never satisfied the contract. |
||
|
|
953df47fc4 |
feat(providers): dispatch git and filesystem providers by execution host (#18296)
`const c = repo.connectionId; c ? sshProvider(c) : local()` overloads `null` to mean both "resolved: local" and "could not resolve", so every path that cannot determine the host silently runs remote work on the client (#11163). It also cannot express a `runtime:` host at all. Add a host-keyed dispatch whose input is an `ExecutionHostId` — never null — with `local`, `ssh` and `runtime` as three symmetric entries, and which throws on an id that names no host instead of degrading to this machine. `ssh` carries `provider: null` for "remote, currently unreachable", which is now a different answer from "local" rather than the same one. `runtime:` is a distinct entry rather than a provider because main does not execute runtime hosts at all: they are forwarded over the environment transport, and a runtime row's `connectionId` names a target in the server's namespace. Dialing it from this client's SSH table would trade a silent-local bug for a silent-wrong-host one. First migrations, both to rows resolved via `getRepoExecutionHostId`: - repo-worktrees: an `executionHostId: 'ssh:*'`-only row no longer lists, root-matches, or strict-lists against a same-named local path. - workspace-space-repo-scan: same for the size scan, and `isRemote` no longer contradicts the `executionHostId` emitted beside it. |
||
|
|
d084a2a36a |
fix(ssh): decide remote-vs-local from the resolved execution host, not a raw field (#18294)
`repoIsRemote` read `repo.connectionId` directly. That is one of four spellings of host ownership, so the predicate was wrong in both directions: a row carrying only `executionHostId: 'ssh:<target>'` read as local and got the Linux-only `orca-ide` rename it cannot resolve through the relay shim, while a row that declares itself `local` with a stale `connectionId` read as remote and lost the rename it needs on a Linux desktop. The predicate now resolves the host first and asks "does an SSH target hold this row's files" via `getRepoSshConnectionId`. That keeps a `runtime:` host's nested SSH target remote (that machine reaches the files through its own relay shim) while a runtime with no nested target - a full Orca install - stays local, as do WSL and local. Its call sites did not all want that question: - The four launch-scope sites in main already hold the resolved PTY route on `TerminalWorkspaceLaunchScope.connectionId`. `scope.repo` is documented display metadata and can be a row from a different host than the worktree names, so they now read the route they will actually spawn on. A launch shape that disagrees with its own route is the bug, not a second predicate. - `launchAgentInNewTab` picked its repo row with a host-blind `store.repos.find`, so a worktree that names its own host could be shaped by another host's row. It now resolves through `getConnectionIdFromState`, the same rule the file already used for transcript readability. - `resolveAgentBackgroundLaunchHost` derived the route, the trust write and the launch shape from three reads of the raw field; one resolution now feeds all three. Also converts the raw `repo.connectionId` agent-detection probe eight lines above `buildWorktreeStartupForDraft`'s launch shape, which #17919 deferred precisely because converting it alone would have left that file internally inconsistent. Tests cover two distinct SSH hosts (a single-host fixture passes even when the answer comes off the wrong row, which is how the `ssh:m4air` -> openclaw leak survived review) and a `runtime:` host carrying a nested SSH target. |
||
|
|
7c94d12190 |
fix(ssh): route four host-blind seams through the resolved execution host (#17919)
* fix(host-routing): resolve the execution host before reading a connection Three issues in one defect class: a resolver reads one spelling of one arbitrarily chosen row instead of resolving the worktree's execution host, so something local answers a question about a remote. returned that row's connectionId. With duplicate repo rows for one repo id it could pair a runtime owner with a client-owned SSH connection. It now resolves through the same ambiguity-aware index getRuntimeEnvironmentIdForWorktree uses, prefers the repo row for the host the worktree names, and derives the connection from the resolved host. Conflicting rows return `undefined` (this module's documented "cannot determine the host"), never `null`. `store.getRepo(worktree.repoId)?.connectionId ?? null`. `getRepo` is host-blind and the same repo id can exist on local, SSH and runtime hosts, so a remote worktree could spawn its PTY on the client with the remote cwd. resolveWorktreeLaunchHost picks the row for the worktree's host and reads the connection off that host; conflicting rows are unresolved, not local. session-partition owner maps that contradict each other. Both now compute through one shared function whose argument records the divergence. No behaviour change on either side: converging needs a read-both migration, since both partitions hold real data written by shipping builds. * fix(host-routing): keep nested SSH connections resolvable under a runtime host getRepoSshConnectionId read only the resolved execution host, so a repo row owned by a runtime that reaches a nested SSH target (connectionId: ssh-*, executionHostId: runtime:*) resolved to no connection — answering 'local' for a remote worktree, the same defect #17909 fixed in the other direction. * fix(host-routing): resolve both sides of the execution host through one rule The renderer resolver leaked between two different SSH hosts: a worktree on `ssh:m4air` whose only indexed repo row belonged to `openclaw` answered 'openclaw', because the host-scoped lookup missing fell through to an id-only one. Main's resolver, in the same change, answered 'm4air' — two resolvers, one right and one wrong, on identical input. Both sides now adapt one shared rule (`worktree-execution-host-resolution.ts`): the worktree's own host outranks every repo row, and a row on a different host is never evidence about this one. The renderer's WeakMap index becomes the memoizing adapter it always was; `resolveWorktreeLaunchHost` becomes main's mapping of unresolved onto its throw. Settles the rule the change previously answered two ways. `getRepoSshConnectionId` and `getSshTargetIdForExecutionHost` disagreed for a runtime host carrying a nested `connectionId`; they now compose, so the execution host is the single authority. On a `runtime:*` row that field is a paired HUB's private SSH target, spread through by `repoWithFetchedOwner` and unaddressable from this client — the project-first successor of the row nulls it for exactly that reason. That also fixes the `kind !== 'ssh'` fallback, which fired for `local`: a row declaring itself local handed out an SSH connection. * fix(ssh): resolve the execution host in the worktree scan and managed create The worktree scan and createManagedWorktree both picked remote-vs-local from repo.connectionId, so a row stamped only executionHostId: 'ssh:*' was scanned and created on the client against a remote path. The folder branch returns before the check, so its agent-trust write landed locally too. Refs #11163 * fix(ssh): stop over-rejecting and refusing SSH hosts the process owns runtimeRepoMatchesExecutionHost rejected an unstamped SSH repo against its own ssh:<connectionId>, so repo-add/clone dedupe could register a second row for a path the host already owns. assertHostIsSupported made the CLI/runtime RPC refuse --host ssh:* while the same process's IPC handler routed it correctly; setupExistingFolder now shares that registration. Clone still refuses, because nothing in this process clones onto an SSH host. Refs #11163 * test(ssh): retarget the SSH host-setup guard spec at the substitution it prevents setupProjectExistingFolder now registers the remote path through the same addRemoteRepoFromPath the desktop IPC uses, so it fails on the host's terms (connection not registered) rather than a categorical refusal. The local clone/probe side effects it exists to catch are still asserted absent. Refs #11163 * fix(cli): require an absolute path when setting a project up on an SSH host Routing --host ssh:* to the remote registration made relative paths newly reachable there, and they were resolved against the client cwd — registering a path that names the wrong machine. Refs #11163 * fix(repos): read the SSH registry directly so the runtime stays Node-bootable Routing runtime project setup through addRemoteRepoFromPath dragged ipc/ssh -- and its 25-module electron graph -- into the runtime bundle. ssh-target-registry already exists for exactly this; ipc/ssh only re-exports it. * fix(ssh): close the agent-launch and session-export host-blind twins Three sites left on the legacy spelling, all the same shape as the ones this branch already fixed: - `launchAgentTerminal` did `getRepo(worktree.repoId)` then wrote agent trust with that row's `connectionId`. Host-blind, so a repo id carried by two SSH hosts wrote a remote path into the *client's* Codex/Cursor/Copilot config and the agent on the host never saw the trust. Every sibling call site already passes the resolved `workspace.connectionId`; this was the last that did not. - `targetForWorktree` (workspace-session export) fell back to the same host-blind read, so a session could be published to a machine that never owned the worktree. Unresolvable ownership now exports to nobody. - `addRemoteRepoFromPath` minted `connectionId`-only rows while being the routing path this branch adds, so it kept creating rows in exactly the spelling the branch works around. It now stamps `toSshExecutionHostId(connectionId)` at creation; `reassignSshTargetId` already migrates both spellings, so target rename stays correct. Tests cover two *different* SSH hosts throughout — the case none of the earlier duplicate-row tests had, all of which were local-vs-ssh or runtime-vs-ssh. |
||
|
|
c61ca56a9b |
fix(ssh): resolve the worktree's execution host instead of guessing from one repo row (#17909)
* fix(host-routing): resolve the execution host before reading a connection Three issues in one defect class: a resolver reads one spelling of one arbitrarily chosen row instead of resolving the worktree's execution host, so something local answers a question about a remote. returned that row's connectionId. With duplicate repo rows for one repo id it could pair a runtime owner with a client-owned SSH connection. It now resolves through the same ambiguity-aware index getRuntimeEnvironmentIdForWorktree uses, prefers the repo row for the host the worktree names, and derives the connection from the resolved host. Conflicting rows return `undefined` (this module's documented "cannot determine the host"), never `null`. `store.getRepo(worktree.repoId)?.connectionId ?? null`. `getRepo` is host-blind and the same repo id can exist on local, SSH and runtime hosts, so a remote worktree could spawn its PTY on the client with the remote cwd. resolveWorktreeLaunchHost picks the row for the worktree's host and reads the connection off that host; conflicting rows are unresolved, not local. session-partition owner maps that contradict each other. Both now compute through one shared function whose argument records the divergence. No behaviour change on either side: converging needs a read-both migration, since both partitions hold real data written by shipping builds. * fix(host-routing): keep nested SSH connections resolvable under a runtime host getRepoSshConnectionId read only the resolved execution host, so a repo row owned by a runtime that reaches a nested SSH target (connectionId: ssh-*, executionHostId: runtime:*) resolved to no connection — answering 'local' for a remote worktree, the same defect #17909 fixed in the other direction. * fix(host-routing): resolve both sides of the execution host through one rule The renderer resolver leaked between two different SSH hosts: a worktree on `ssh:m4air` whose only indexed repo row belonged to `openclaw` answered 'openclaw', because the host-scoped lookup missing fell through to an id-only one. Main's resolver, in the same change, answered 'm4air' — two resolvers, one right and one wrong, on identical input. Both sides now adapt one shared rule (`worktree-execution-host-resolution.ts`): the worktree's own host outranks every repo row, and a row on a different host is never evidence about this one. The renderer's WeakMap index becomes the memoizing adapter it always was; `resolveWorktreeLaunchHost` becomes main's mapping of unresolved onto its throw. Settles the rule the change previously answered two ways. `getRepoSshConnectionId` and `getSshTargetIdForExecutionHost` disagreed for a runtime host carrying a nested `connectionId`; they now compose, so the execution host is the single authority. On a `runtime:*` row that field is a paired HUB's private SSH target, spread through by `repoWithFetchedOwner` and unaddressable from this client — the project-first successor of the row nulls it for exactly that reason. That also fixes the `kind !== 'ssh'` fallback, which fired for `local`: a row declaring itself local handed out an SSH connection. |
||
|
|
4b2d9b5aac |
fix(path): stop seeded user bin dirs from outranking the inherited PATH (#18265)
`patchPackagedProcessPath` prepends every seeded directory, so `~/bin` and `~/.local/bin` land ahead of the PATH a GUI-launched Electron inherited. That does more than make a tool findable, which is what seeding is for -- it re-ranks binaries the user already has, and those two directories are user-writable and can hold a wrapper for any system tool. On the #18234 reporter's box `~/.local/bin/gh` wraps `mise x gh -- gh`. Seeded ahead of /usr/bin we ran the wrapper where their own shell ran the real binary, and the wrapper's inner bare `gh` resolved back to itself. Measured in an Ubuntu 24.04 container: with their shell's ordering the chain exits in 22ms; with ours it never terminates and creates ~1,500 processes/second. Seed order now follows the rule the WSL twin already documents in posix-version-manager-bin-dirs.ts -- append, never prepend, because a login PATH that did resolve is authoritative. Version-manager shim dirs keep leading, since an nvm/mise/asdf user's runtime must still beat a system install; the generic user bin dirs move behind the inherited PATH. `getVersionManagerBinPaths` carries `~/bin` and `~/.local/bin` too (bun and pnpm install there), so they are filtered out of the leading group by name rather than by which list produced them. |
||
|
|
b8b7a6be9d |
fix(activity): persist the agents unread filter and grouping (#18255)
* fix(activity): persist the agents unread filter and grouping The Agents view's "Show unread threads only" toggle and Group-by select were plain component state in the sidebar and the Activity page, so both reset on every mount — including app restart — while their neighbours in the same toolbar (compact rows, show child agents) survived via the persisted UI store. Promote both to `agentsReadFilter` / `agentsGroupBy` persisted UI preferences, wired through the same seams as `agentsCompactMode`: shared type, default, strict client RPC schema, pairing-local field census, web read pin, store contract/actions, and hydration normalizers that reject unknown values. Both consumers now read the store, so the sidebar and the Activity page share one filter the way they already share compact mode. * refactor: centralize thread filter value domains Establish filter and groupby value domains as the single source of truth, with types derived from them to prevent drift between valid values and their normalizers. Extract common validation logic into a shared isMember helper to keep the two normalization functions in sync. * refactor: centralize thread filter value domains Consolidate filter value definitions in agents-view-thread-filters and use them in Zod schema validation to ensure consistent, persistent serialization of filter state. |
||
|
|
510305e574 |
fix(relay): signal capacity loss instead of dropping, hanging, or truncating (#17870)
Three failures with one shape: a payload past a fixed capacity was met with silence, with a wait that never ends, or with a prefix presented as a whole. **The workspace snapshot was silently dropped.** `workspace.changed` carries the tab/session list, and a snapshot past the producer frame capacity (12288 B on a Node <=21 remote) was dropped with only a relay stderr line, so the client kept a stale list forever. The relay now publishes per client and, for a client whose sink refused the frame, sends a compact `workspace.stale` marker on the control lane; the client re-reads through `workspace.get`, whose lane is budgeted in megabytes rather than in one producer frame. A new JSON-RPC notification rather than a new field on `workspace.changed`: `normalizeSnapshot(undefined, ns)` yields revision 0 and an empty session, so a Rule-1 field would make an old client replace its tab list with nothing — worse than the drop. An old client ignores the unknown method and is exactly where it is today. The marker retention/retry machinery is extracted from the `fs.changed` overflow path and shared by both. **The Windows upload hung, and the fix for it could truncate.** `#16432` was attributed to `[Console]::In.ReadToEnd()` materializing the base64 bundle. That is not what the reporter measured: he also measured `new IO.StreamReader([Console]::OpenStandardInput())` — an incremental reader — hanging at 1 MB. The limit is in the stdin the host hands PowerShell over a non-pty ssh exec, not in the string the script builds. - `uploadFileViaSystemSsh` — the user file-import path — was piping a whole file into one Windows stdin, unchunked and untimed. That is the path large files take; it now chunks into 32 KB writes and bounds each wait. - The Windows directory upload reuses that single-file path rather than repeating a weaker copy of chunk-read + write-buffer; the `ino`/`dev` TOCTOU verification comes with it. - A Windows write needing more than one exec lands on a `.orca-partial` staging path and is published by rename, so a failed chunk cannot leave a truncated artifact under the real name. `exclusive` is enforced once at the rename, not on the first chunk, where a retry met its own leftovers. - The mkdir batch reads stdin through the stream reader the reporter measured surviving 50 KB, not `[Console]::In`, which he measured wedging at that size. - `waitForChannelClose` takes an optional bound. A wedged PowerShell stays alive at idle CPU and never closes, so without one the promise is simply never settled and the caller waits forever with no error to show. **Quick Open showed a prefix as the whole workspace.** The mechanism "a full page means there is more" only works if the caller named the cap, and the failing UI named none — it hardcoded `truncated: false`. Quick Open now names `QUICK_OPEN_LISTING_MAX_RESULTS` on both the Electron IPC hop and the runtime-RPC hop (the field #17954 added to `files.listAll`), and reads a full page as truncation. The local hop honours the cap too, which it previously ignored. Rebase note on `fs.listFiles`: an earlier revision of this work also clamped the host unconditionally, and #17934 escalated an uncapped request to an explicit error. #17954 has since landed and made an oversized reply streamable, which removes the premise — the host no longer has to choose between a prefix and a refusal, so it returns the whole listing when no limit is named and only clamps a limit it was given. Keeping either would have regressed #17954 and hard-failed three in-tree callers that deliberately pass no options (`runtime-file-commands-search-runtime-files.ts:81`, `filesystem-read-handlers.ts:125`, `runtime-file-commands-constructor.ts:41`). |
||
|
|
7f8eb90ac3 |
Align worktree host labels across desktop and mobile (#18237)
* refactor: align worktree host labels across clients * fix(mobile): expose safe host display labels * fix(mobile): preserve legacy mixed-host labels --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
278f9ee876 |
fix(ssh): answer every MFA stage, stop dialling an unclaimed alias, and say where a clone failed (#17946)
* fix(ssh): answer every MFA stage, not just the first ssh2 walks one flat auth-method list exactly once, so keyboard-interactive could only ever be offered a single time. A host running `AuthenticationMethods keyboard-interactive,keyboard-interactive` (or any ladder ending in a second challenge) partial-succeeds the first stage and then finds the list exhausted, which the user sees as "All configured authentication methods failed" — the reports in #8622 and #16820. Orca's own auth handler now runs for every target instead of only multi-key ones, and rebuilds its queue on each SSH_MSG_USERAUTH_FAILURE that carries partial success, narrowed to the methods the host still offers. Narrowing also stops keys being re-offered after the host has moved past publickey, which is what exhausts MaxAuthTries before the challenge is ever shown. Covered by a real ssh2 server fixture that stages partial success. * fix(git): say where a failing clone ran and why nothing could prompt Clones go through nonInteractiveGitEnv, so `ssh` runs with BatchMode=yes and an emptied SSH_ASKPASS. On a remote or paired-runtime clone that produces `fatal: Could not read from remote repository.` while the same `git clone` typed by hand on that box succeeds — the divergence in #14533. Nothing in the message said the clone ran on the other machine, under its keys, with the prompt deliberately disabled. getGitCloneFailureMessage now appends that fact, and names the two recognisable shapes: a publickey refusal (load the key into an agent there) and a host-key failure (record the key in that machine's known_hosts). Unrecognised SSH failures still get the where-it-ran note; non-SSH failures are untouched. One builder, so the SSH-target relay path and the runtime path both get it. * fix(ssh): stop dialling a bare alias no ssh_config block claims A wildcard `Host *` block supplies ProxyCommand/ProxyJump for every alias, so shouldUseSystemSshTransport picks the system transport for an alias whose own Host block was renamed or deleted, and buildSshArgs then dials that alias verbatim: no -l, no -p, no Hostname. Orca connects as the wildcard's user to the wildcard's host and discards the endpoint it stored (#11746). The signal #11746 assumed (hostBlockMatch, from the still-open #11707) does not exist, and `ssh -G` cannot supply it — it prints the merged config and answers for unknown aliases too. The config file is the only source of truth, so: - parseSshConfigAliasClaims retains raw Host patterns and flags Match blocks, which parseSshConfig discards because it mints importable targets. - sshConfigMayClaimAlias is sound in the negative direction only: an unreadable file, any Match block, or any non-catch-all pattern that might match all answer "claimed", so absence of evidence is never read as evidence of absence. Only a proven-unclaimed alias licenses an override. - buildSshArgs then states Hostname/Port/User, and only those: the wildcard is still the route, and -o Hostname does not change block selection, so the proxy keeps applying and %h expands to the host we mean. The verdict is injected rather than read inside buildSshArgs, so an arg builder does not answer differently per machine. Default is today's behaviour. Scoped to the system-SSH transport and the connection's own command/transport path. Port-forward processes and the ssh2 transport (#11707) are unchanged. * fix(ssh): read a negated Host group as uncertainty, and gate clone SSH guidance `Host * !prod` applies to every alias but `prod`, yet skipping both the catch-all and the `!` pattern answered "unclaimed" for `stage` — which licences overriding Hostname/Port/User against a block the user wrote. Any negation now makes the whole group uncertain; the function is only sound in the negative direction. Also require an ssh(1) diagnostic beside "could not read from remote repository" before appending the SSH clone note: git prints that same line for the HTTP remote helper, where advice about keys and agents is simply wrong. * fix(i18n): restore the activity-options key the rebase dropped * fix(i18n): union en.json with main so the rebase cannot drop keys |
||
|
|
3a8f806d6c |
fix(remote-terminal): keep the stream stall deadline armed on unacknowledged credit (#17871)
* fix(remote-terminal): keep the stream stall deadline armed on unacknowledged credit A paired-runtime terminal could stall silently with a live socket, a live PTY and no transport error (#11265). Two compounding defects on the read side: - The stream watchdog re-armed its 30s delivery deadline from zero on every settled delivery, so sibling traffic postponed the verdict indefinitely, and it cleared the timer entirely once renderer parse credit hit zero. Re-arming required inbound output -- the exact thing an exhausted host ACK window stops -- so once an ACK went missing nothing could ever detect the stall. The deadline is now anchored to the oldest unsettled delivery and stays armed while delivered bytes remain unacknowledged to the host. - flushOutputAcknowledgement zeroed pendingAckBytes before knowing the ACK frame was accepted, permanently shrinking the host's send window. Unsent bytes are re-charged and the flush timer re-armed. Recovery still reports onTransportClose({recoverable:true}); no path claims the PTY exited. * fix(remote-terminal): stop rearming the ack flush for a stream the failed send dropped A failing ACK send tears the stream down inside sendFrame, so the re-charge path armed a 4ms timer on an unregistered stream whose watchdog was already disposed; every later send returned false on !ready and rescheduled again. Also adds the missing integration coverage for the real ack -> watchdog flow. * fix(i18n): restore the activity-options key the rebase dropped The branch's en.json predates #18245, which added both the translate() call and its key. Rebasing took the branch copy wholesale, silently dropping the key and failing verify:localization-catalog. * fix(i18n): union en.json with main so the rebase cannot drop keys |
||
|
|
39330c5aca |
fix(relay): retire PTYs the host proves are gone, and stop two per-poll scan storms (#17832)
* fix(relay): stop three CPU growth terms in a long-running remote session pty.resize gated only on `managed.disposed`, which is bookkeeping rather than liveness. A shell that exits without node-pty's `onExit` leaves an undisposed entry holding a closed master fd, and UnixTerminal.resize has no fd guard, so the ioctl threw `ioctl(2) failed, EBADF` into the dispatcher's generic parse-error catch. Nothing retired the entry, so it stayed advertised and kept activePtyCount above zero -- which is what stops a relay with an unlimited grace from reaching its idle-no-ptys exit (#12423). Probe liveness with the same helper attach/listProcesses use, retire a provably dead pid, and contain an ioctl failure over a live-or-unverifiable process. processHasChildren forked `pgrep -P` per pane per inspection poll, uncached. procps-ng opens six procfs files per process to resolve one ppid, so each call cost O(host process count). Answer from the TTL-cached `ps` table the same RPC already captured for the foreground lookup (#13537). The remote AI Vault scanner had no parse cache at all, so every forced rescan re-read and re-parsed the whole transcript corpus, including files untouched for a month. Give it the mtime+size keyed memo the local scanner has (#13753). * fix(pty): invalidate the descriptor when node-pty gives up the handle (#17930) Carried forward from PR #17930, which merged into this branch. Rebased onto current main; main's newer node-pty-fd-leak test is kept as-is. * fix(ai-vault): refresh codex titles on the remote parse-cache reuse path The remote cache keys on the transcript's (mtime, size, host), but codex titles live in $CODEX_HOME/session_index.jsonl and are written after the rollout — so a cache hit froze the fallback title forever. Mirrors the local scanner's existing reuse-path refresh via a shared core. * fix(relay): publish the exit a reap performs, and rescan for close decisions Two review findings on the CPU work. reapExitedPty told only the relay-internal exit listener, so a retirement left the client's pane mounted against a session the relay had already forgotten -- the next attach answered `PTY "<id>" not found` with nothing before it to explain why. Pre-existing on three probe paths; resize made it user-triggered. Publish the same pending-exit the natural onExit path publishes, carrying -1 ("gone, status unrecoverable"), and skip it when onExit already reported the real code. processHasChildren now answers from a 500ms TTL-cached table. That is right for pty.inspectProcess, which every tracked pane polls, but pty.hasChildProcesses gates the window-close confirmation and workspace cleanup's idle evidence -- one destructive decision per answer, where a child started inside the window would be killed unasked. Give that RPC a fresh scan; pgrep used to. * fix(relay): publish a reap's exit only on proven-exited evidence The publication is a verdict the client acts on by retiring the pane, so it must not be reachable from the disposed-record sweep, which retires off our own bookkeeping rather than the host's process table. Only ESRCH earns it. * fix(i18n): restore the activity-options key the rebase dropped * fix(i18n): union en.json with main so the rebase cannot drop keys |
||
|
|
033a2a64e1 |
fix(remote-runtime): make every advertised recovery attempt reachable, and stop two recovery latches (#17822)
* fix(remote-runtime): derive the recovery budget and stop faking a spent window #11305: RECOVERY_DELAYS_MS summed to 60,750ms against a hand-written REMOTE_RUNTIME_AUTO_RECOVERY_TIMEOUT_MS of 60,000ms, so the ladder's tail was unreachable. Derive the deadline from the schedule plus one RPC timeout per step so a half-open link can actually reach every backoff step, and pin the relation with a test that fails if the sum ever outgrows the budget. #12683: markDisconnected() is a UI latch, not proof the auto-recovery window ran out. Track deadline expiry on the recovery state and only let that license the same-handle reattach that bypasses require-replacement fencing. #12684: a recoverable connect() failure latched 'disconnected' with no armed retry, no parked retry and a Reconnect button that returned false. Schedule a bounded retry (which the deadline parks for online/resume) and let the button fire a parked retry. * fix(remote-runtime): stop a post-latch connect failure from re-arming the recovery window The last attempt's RPC budget expires at the same instant as the deadline, so a silently dropped link rejects after phase latched to 'disconnected'. begin() then started a fresh full-length window, so the budget never actually expired. Park the retry under the latched epoch instead, which keeps online/resume/Reconnect armed even when the deadline lands mid-attempt with nothing scheduled. Also fences the same-handle end-reuse window on its own 60s constant so the derived recovery budget no longer silently triples an unrelated stale-handle check. * fix(i18n): restore the activity-options key the rebase dropped * fix(i18n): union en.json with main so the rebase cannot drop keys |
||
|
|
7458c39181 |
fix(ssh): declare a wedged relay link lost, and stop reading silence as a verdict (#17817)
* fix(ssh): declare a wedged relay link lost instead of suppressing the dead-link check * fix(ssh): make the Windows deps probe exit 0 on a real load failure, like its POSIX twin * fix(relay): reap a client that has stopped answering instead of holding its leases forever * test(relay): feed the primary before asserting the reaper exemption holds * fix(ssh): keep a lost link's verdict unverifiable instead of reporting absence * refactor(ssh): read the exec timeout from its typed code, not the message text * fix(relay): bound a client that clears the handshake and then never frames anything * fix(i18n): restore the activity-options key the rebase dropped * fix(i18n): union en.json with main so the rebase cannot drop keys |
||
|
|
07e1f953a5 |
fix(ssh): log an unanswered native-deps probe instead of launching silently (#18000)
* fix(ssh): log an unanswered native-deps probe instead of launching silently The wrongful rebuild used to be the only visible symptom of a dropped exec channel; #17979 removed it, so a real transport failure now leaves no trace. Matches the install-path sibling, whose callers log the same class of failure. * fix(i18n): restore the activity-options key the rebase dropped * fix(i18n): union en.json with main so the rebase cannot drop keys |
||
|
|
7104056984 |
fix(watcher): route relay watch-root capacity refusals off the fast ladder (#17950)
* fix(ssh): stop two unrecoverable relay refusal loops A relay refusal that is a pure function of state the client cannot change was being retried forever, on two different paths. - pty.openClient: a superseded owner proof is refuted evidence, not a transient fault. The client kept re-presenting the identical proof, so every reconnect reproduced the same refusal until the relay was redeployed (#12895, #12931). It is now dropped exactly as a stale lease already is, and the claim re-asked without it. - fs.watch: the relay's watch-root capacity refusal was classified 'unavailable' and retried at 1 Hz per root for 60s, re-armed indefinitely. A folder workspace with more repos than the cap turns that into a permanent install storm scaled by the excess root count (#11196). It is now its own 'capacity' result that goes straight to the existing dormant backoff, mirroring what the local watcher path already does. * fix(watcher): route relay watch-root capacity refusals off the fast ladder A full watch-root cap is a decision, not a fault, so a 1 Hz reinstall per refused root only bills the relay the load that keeps the cap busy (#11196). Capacity refusals now go straight to the dormant backoff. The relay side no longer refuses on a slot it is about to hand back: an over-cap caused by roots still unsubscribing waits once on the teardowns settling — the release event, mirroring WatcherSupervisorCapacityWait — before it answers. A parked waiter is excluded from the accounting so it cannot take a slot from the root already reclaiming one. Drops the SSH owner-recovery half of this branch. Its premise — that a -32043 SUPERSEDED refusal is permanent — is false: the refusal fires only while the incumbent is 'active', and assertPtyConsumerOwnerRecovery explicitly admits the identical lower-generation proof once the incumbent flips to 'disconnected' (relay-pty-consumer-owner-displacement.test.ts proves it). The remedy could not work either: the proofless re-ask routes into refuseHeldPtyConsumerOwner, which is declared `: never` and, with sameClient true by construction, always throws. It would have traded one refusal loop for another, minus the checkpoints and minus the proof that resumes the claim once the relay reaps the incumbent. * fix(i18n): restore the activity-options key the rebase dropped * fix(i18n): union en.json with main so the rebase cannot drop keys |
||
|
|
7c6c8ef85e |
fix(ssh): stop a late SFTP stream error crashing main, and keep the relay socket inside sun_path (#17862)
* fix(ssh): stop SFTP stream errors crashing main and bound the relay socket path
inside the protocol parser. Every transfer removed its listener on settle, so a
STATUS reply that arrived late - the normal case behind a jump host that chroots
its SFTP subsystem - threw synchronously out of Socket.emit('data') and killed
the main process. Keep one durable listener per stream, and report a sandboxed
SFTP namespace with an actionable message instead of a bare 'file does not
exist'.
104 macOS) and bind failed with a bare 'listen EINVAL'. Fall back to a per-uid
base whose length does not depend on $HOME, keeping the hashed socket name
intact.
* fix(ssh): validate the short socket dir before mutating it
* fix(ssh): keep the SFTP session guarded, scope the relocated socket, narrow the chroot verdict
Three review findings.
The CLI-launcher install ran writeStringViaSftp in a loop over a bare conn.sftp().
That helper removes its own session 'error' listener at each settle, so between
files and after the last one the emitter carried none -- and ssh2 raises a late
STATUS reply synchronously out of Protocol.parse, which is the uncaught exception
that kills main (#15479). The inline loop it replaced leaked one listener per file
and covered this by accident. Extract writeStringsViaSftp, which owns the session
latch, and share that latch with runSftpFallbackTransfer.
SSH_FX_PERMISSION_DENIED is a mode/ownership refusal on a path the subsystem can
see, not evidence of a chroot; sftp-namespace-resolution already treats only
NO_SUCH_FILE as conclusive. Narrow the predicate to code 2 so a read-only home
stops being reported as a bastion misconfiguration.
The relocated socket had no version dimension. relaySocketNameForInstanceId hashes
the target, not the build, and under $HOME the enclosing relay-<fullVersion> dir
supplied the rest -- so the short form made the path stable across updates. The
next build would bind the path the previous relay still holds, the handshake would
mismatch, and a relay holding live work would raise RelayEndpointHeldError with no
way through. Add a hashed version segment under the short base, mirroring the
relay-*/<sock> shape so one pattern serves both, and teach the superseded sweep and
force-stop about that base. The relocated tree now also gets reclaimed: nothing
else walks it.
* fix(i18n): restore the activity-options key the rebase dropped
* fix(i18n): union en.json with main so the rebase cannot drop keys
|
||
|
|
31007c0d86 |
fix(ssh): reclaim relay PTYs the client has provably lost, on host attestation only (#17831)
* fix(ssh): reclaim relay PTYs the host attests this client orphaned (#9819) Orca could lose track of terminals running on an SSH relay until the 50-slot cap refused to open any more. This reclaims them, and the whole design is built around the fact that getting it wrong destroys a user's running process on their remote machine: the failure mode is leak, never kill. A stop requires all nine of: 1. the relay published an `ownerClientInstanceId` read from the live authenticated consumer grant of the connection that requested the spawn — never from a spawn parameter, since an echoed claim is no evidence; absent means skip 2. that id equals this client's persisted consumer identity 3. this connection holds the negotiated `session-owner` grant 4. `paneBound === true`, host-published 5. no `agentSessionOwners` — the host still advertises it as adoptable 6. `hostAgeMs >= 30s`, measured on the host's clock 7. this client has no route: not reattached, no lease outside terminated/expired, no pending kill, and no `expired` lease either — an expired lease is the record of a process deliberately left running, never a licence to kill it 8. every stop is fenced on the incarnation the same listing published, and on the owner identity, both re-checked by the host 9. a pass wanting to stop more than 8 refuses entirely Absence from a client-side set is `unverifiable` by construction (docs/reference/ssh-execution-boundary.md): a second machine attaches to the same relay and displaces the session owner, and its live agents are missing from this client's store for exactly the reason a genuine orphan is. So the host has to attest ownership, and the host has to attest that nothing is running. That second attestation is measured over the pane's whole tty, not its foreground process group. `tpgid == pgid` is foreground-only: on a real `bash -i` on a real pty, a shell holding `sleep 300 &` and a shell holding a Ctrl-Z'd job both read `pgid == tpgid`, `Ss+` — byte-identical to an idle prompt, with only the job's own row differing. A foreground-only gate therefore attests `pnpm build &` and a suspended editor as idle, and the stop that follows SIGKILLs every process group on the tty. `shellOwnsEveryTtyProcessGroup` is measured over that same set of groups, so the evidence and the kill describe the same thing. No new probe: `tpgid` already identifies the terminal, because a process group belongs to one session and a session to at most one controlling terminal. The freshness field is real rather than decorative. `capturedAgeMs` is stamped from when the capture was taken, deliberately as an upper bound since the process table is TTL-shared, and the sweep refuses an observation older than its own pass budget, counting its own elapsed time since the listing arrived. Stale evidence degrades to "do not sweep", never to "sweep". The display consumer of the same measurement keeps no age budget, as a stated decision: a stale pane title costs a redraw and self-corrects. `pty.shutdown` is authorized on the host that owns the process. `pty.spawn` and `pty.attach` both take a request context and check it; the one irreversible call took none, so the rule above lived entirely on the client that decided to make the call. It gains an optional `expectedOwnerClientInstanceId` and refuses unless the connection still authenticates as that identity AND this host recorded it at spawn. Finally, a reattach refusal now says whether it observed the process. Three refusals carry the same `SSH_SESSION_EXPIRED` text and only one is absence; `restoreRequired` means the PTY is live and only its source stream is not. Testing that text with `.includes()` expired the lease and deleted ownership for a running process, erasing this client's only record of it — and a PTY with no record is one the sweep may stop. Wire compatibility: four new optional fields and one new optional param on existing methods, no new method and no new stream opcode (Rule 1, and Rule 2 does not apply). Rule 1's caveat is discharged explicitly — no reader requires any of them, each absence is a named skip reason, and an ordinary pane teardown must omit the owner fence because a revived PTY carries no attested owner at all. New client plus old relay stops zero PTYs; old client plus new relay never reads the fields. Windows relay hosts publish no evidence and therefore never sweep. Verified by joining the real publisher to the real client reader over `ps` captured verbatim from a Linux container, and by driving a real group-for-group SIGKILL against a real pty: backgrounded and suspended jobs survive by pid, and an idle shell is still reclaimed, so the narrowed predicate is not a silent no-op. Squashed deliberately. The sweep is unsafe at every intermediate commit of its own history — before the foreground gate it reaps a hand-launched `claude`, and with a foreground-only gate it reaps a backgrounded build — so this ships as one commit with no bisectable state that kills live work. Refs #9819. Folds in #17939. * fix(i18n): restore the activity-options key the rebase dropped * fix(i18n): union en.json with main so the rebase cannot drop keys |
||
|
|
fb48a9771b |
fix(gh): reap the whole gh/glab process tree at the deadline on POSIX (#18258)
`gh` and `glab` on PATH are routinely shims — mise, asdf, volta, or a hand-written wrapper — so a timed-out invocation has a chain to stop, not one process. `execFileCapture`'s POSIX kill path signals only the direct child; the descendants are orphaned to init and keep running. #18234 is exactly that shape: `bash ~/.local/bin/gh` -> `mise x gh` -> `gh`, where the reporter found the tail reparented to `systemd --user` and still at 100% CPU nearly two hours later. The 15s deadline #18239 added bounds Orca's semaphore slot and its promise; it does not bound the CPU burn. Route both CLIs through `execFileCaptureToTermination`, the primitive git's barrier path already uses: POSIX children spawn `detached`, the deadline signals `-pgid` and escalates to SIGKILL, and the promise waits for verified termination. Windows behaviour is unchanged (`taskkill /t` either way). Switching primitives also swapped execFile's hard maxBuffer failure for `runProcess`'s silent clipping, which would have turned an oversized gh response into a shorter valid-looking one. `ProcessResult` now reports truncation and the capture rejects on it, restoring the old contract and closing the same latent gap on git's barrier path. |
||
|
|
4376154993 |
perf(sidebar): stop re-allocating 423 workspace descriptors and 193 bucket projections per recompute (#18241)
Layers three identity memos onto the row cache #18222 landed, without changing what the four sidebar numbers say in any state. - The active-workspace descriptor list is memoized on the four slices `collectActiveDashboardWorkspaces(state, false)` actually reads. - Each worktree's bucket tally is memoized on its rows plus the acknowledgement map, so a ping that rebuilds one worktree no longer re-projects the board. - The `useShallow` selector becomes a module-level 14-identity gate, which allocates nothing on the unchanged path. - The counts object is reused by identity when all four totals hold. |
||
|
|
02a417c04b |
perf(renderer): stop six timers from ticking behind a hidden window (#18134)
* perf(renderer): stop six timers from ticking behind a hidden window
IntensiveWakeUpThrottling is disabled in this app, so a renderer interval
really does fire at full rate with the window hidden. Six of them had
nothing to observe them:
- NativeChatWorkingStatus ran a 1s interval + setState per in-flight turn
purely to advance an elapsed-seconds counter. Deleted the effect and
derived elapsed during render from the shared, visibility-gated
useNow(1_000) clock, so N turns collapse onto one tick.
- The chromium-error fallback poll (250ms) kept probing a stuck-loading
guest to write a loadError nobody could see.
- The contextual-tour full-pass interval (500ms) woke twice a second to
queue a rAF a hidden window never paints.
- Three feature-wall animation timers (3600/2400/2400ms) kept committing
React renders for animations nobody was watching.
- The landing preflight poll (30s) kept forcing IPC refreshes.
All five gated timers reuse installWindowVisibilityInterval. Each either
resumes where it left off (animations) or re-derives from durable state on
the becoming-visible run, so hiding and re-showing is observationally
identical to never hiding.
* test(git): stop two empty commits in the divergence fixture from hashing alike
`counts drift in both directions` builds 100 empty commits, resets to the fork
point, then adds one more — expecting 100 ahead + 1 behind to clear the cap of
100. An empty commit's hash covers only parent, tree, message and a
one-second-granularity timestamp, and every commit in the fixture reuses
`commit ${index}` starting from 0. On a runner fast enough to finish the whole
build inside one wall-clock second (CI: 1059ms for the case, ~7ms per commit),
the post-reset `commit 0` hashed identically to the first `commit 0` of the
chain, so Git handed back that same object and left the branch 99/0 apart
instead of 100/1 — `within`, not `exceeded`.
Numbering the empty commits across calls makes the fixture build the 101
distinct commits it already claimed to. Reproduced deterministically by pinning
GIT_AUTHOR_DATE/GIT_COMMITTER_DATE, which forces the timestamp collision the
fast runner hits by chance: fails with the exact CI assertion before, passes
after.
|
||
|
|
6d8dc0c97a |
perf(diff): window the combined-diff file tree rows on large reviews (#18236)
`combined-diff-file-tree.tsx` had three unvirtualized `rows.map(...)` sites, so a 900-file review mounted all 931 tree rows at once. Route the three through a `CombinedDiffFileTreeRows` wrapper over the existing `SourceControlVirtualFileList`, reusing its `SOURCE_CONTROL_VIRTUALIZE_MIN_ROWS = 50` threshold and scroll-margin machinery, with the tree's own 24px row estimate. `SourceControlVirtualFileList` gains one optional `estimateRowHeightPx` prop that defaults to its current constant, so source control is unchanged. Below the threshold the rows stay in natural flow and the markup is unchanged. Above it, find-in-page, select-all-copy and Tab order see only the mounted window — the same trade already accepted for the source-control panel. |
||
|
|
1d34d76f28 |
fix(i18n): add the three activity keys #18245 left out of en.json (#18250)
verify:localization-catalog and verify:localization-extraction both exited 1 on main. The failure was masked: the Lint step failed first on max-lines, so every later static-analysis step was skipped. |
||
|
|
817827be5b |
perf(renderer): drop react-markdown and the emoji catalog off the boot path (#18149)
The sidebar pulled react-markdown, remark/rehype and DOMPurify onto the eager module graph through two static importers -- WorktreeCardMeta's hover-card notes and DashboardAgentRowMessage's inline agent preview -- and built a 3,979-key emoji shortcode catalog at module scope in both the renderer and the main process. Neither is needed before first paint. Route both markdown surfaces through one shared lazyWithRetry boundary that preloads on pointer-enter (250ms hover open delay) and on agent-row mount, with a same-box raw-text Suspense fallback so a pre-load paint cannot shift layout. Memoize the emoji catalog behind loadCatalog() so import costs nothing. Eager renderer JS: 5,569,446 B / 331 chunks -> 5,198,787 B / 325 chunks (-370,659 B, -6.7%). Emoji catalog module eval: ~19.6 ms median -> 0 ms, paid once on renderer boot and once on main boot. |
||
|
|
4ff96df2b5 |
Auto e2e tests autofix scheduled ci 1h run 32 20260902T0700 (#18227)
* Fix flaky e2e tests with improved locators and synchronization Add explicit waits, use more robust element selectors, and simplify test setup to reduce race conditions. Replace file-based fixtures with programmatic browser creation, use parent-scoped locators for menu interactions, and poll for stable state before assertions. * Add E2E failure triage report for run 33564563164 - Reconciles 14 failed tests against job logs and trace artifacts - Categorizes failures: 8 product bugs, 2 flaky tests, 4 test updates - Documents test-maintenance fixes and diagnostic findings - Files 8 Linear issues with owners and fresh recurrence evidence - Provides next actions for product owners and repository maintenance * rm artifact notes * Refactor browser creation E2E test to use UI interactions - Click through menu instead of manipulating internal store state - Use Playwright's locator and toBeVisible() assertion patterns * Record E2E browser creation pageId before barrier check Move createdPageId assignment before the barrier arm/fire checks. This ensures the pageId is recorded unconditionally when tracking is enabled, allowing tests to distinguish between creations rejected before the host attempt vs those that failed after creation. * Remove browser page reclamation assertion from restart test Simplifies test by removing page ID tracking and poll checking if pages persist after paired runtime restart. |
||
|
|
d8ca420cdd |
perf(remote): apply the no-evidence inspection cadence to remote panes (#18146)
A visible remote/SSH terminal that has never run an agent inspected its execution host every ~2s forever for a strictly negative answer — ~30 RPC round trips per minute per pane, each a network hop plus a host-side foreground process scan. The `no-evidence` 15s cadence tier exists to bound exactly that volume, but `isProcessInspectionCostly` gated it on local Windows only and explicitly excluded remote-execution-host PTYs — the most expensive inspection shape in the codebase. Extract the predicate to `agent-process-inspection-cost.ts` and treat a remote-execution-host PTY as costly on every client platform. The local branch (Windows costly, POSIX cheap) is byte-identical. Client-side timer choice only: no wire change, no new field, no opcode. Activity (output/title/hook) re-arms the 2s cadence, agent evidence returns the tier to active/idle, and the `unavailable` branch and its error backoff are untouched. |
||
|
|
ef4e9c40ab |
fix(terminal): replay paired-runtime snapshots at the host's grid (#18132)
* fix(terminal): replay paired-runtime snapshots at the host's grid A paired remote pane parsed the host's authoritative terminal image at whatever grid its own xterm happened to have. The host dimensions every snapshot it publishes, but only the REQUESTED snapshot path ever read `cols`/`rows` back — both PUSH paths (initial subscribe and server recovery) dropped them, so `onSnapshot` handed the transport an image with no grid and the drain wrote it as-is. Serialized frames are grid-relative: rows are newline-fed and the frame ends in an absolute CUP. Parsed at a different grid they re-wrap and clip, and because an alternate-screen TUI has no scrollback the rows scrolled off the top are gone. An idle agent never repaints, so the pane stays wrong until the next byte arrives — which for a finished Claude Code session is never. Carry the grid the host already publishes through the multiplexer and transport, then reuse the choreography the reattach payload already follows: resize to the source grid, replay, fit back to the pane, and push the resulting grid to the PTY. A host that publishes no dimensions reads as unknown and keeps today's behaviour, so no wire change and no capability negotiation is involved. * fix(terminal): keep the source-grid fit correct under mobile fit overrides Two follow-ups on the source-grid replay: - A mobile fit override skipped the post-replay fit entirely, stranding the pane at the host's replay grid. Fit without the PTY grid push instead, matching applyMainBufferSnapshot. - Reset the source-grid flag when a drain is scheduled: a transaction whose restore was skipped never runs afterRestore, and the stale flag would fit a later drain that never left the pane's own grid. * perf(terminal): clear the replay buffer before the source-grid resize The drain resized xterm to the host's serialization grid and only then wrote the clearing `2J`/`3J`/`H`. `clearBeforeReplay` is true for every pushed remote snapshot, so a column change reflowed a full scrollback that the next sequence discarded microseconds later — on the recovery push that lands under output flood, when the renderer is already loaded. The clear is grid-independent, so running it first is equivalent: the resize then operates on an empty buffer. Verified identical end state (content, cursor, buffer type, baseY) across cols-change, rows-change, alt-screen, no-scrollback and equal-grid shapes. Interleaved 25-run medians on a 10k-line scrollback: 6.19ms -> 2.48ms on the normal buffer, unchanged on the alternate screen (where `3J` cannot free the normal buffer's history, so the reflow is paid either way). |
||
|
|
0886db2b90 |
refactor(process-table): extract the correlation indexes into their own module (#18246)
`src/shared/process-table-snapshot.ts` is 308 code lines against the 300 cap for `**/*.ts`, so `static analysis` is red on `main` and every open PR inherits it. Neither PR that grew the file crossed the cap alone. #18151 took it to 427 raw lines; #18166 added ~35 more. #18166's branch predated #18151, so the head CI linted was 428 raw lines and passed, while the squash onto main is 463 -> 308 code lines. The gate lints the PR head, not the merge result, so nothing linted the sum until it was on main. Pure move, no behaviour change: the generic index machinery (ProcessIdentityRow, ProcessTableIndexOf, buildProcessTableIndex, collectDescendantsFromIndex, lookupProcessTableIndex, getProcessTableIndex and its WeakMap) moves to process-table-index.ts. `ProcessTableIndex` and `scoreForegroundCandidateRow` stay behind because they need `ProcessTableRow`, which keeps the new module free of any import back and so introduces no cycle. |
||
|
|
623d58e386 |
fix(native-chat): show pasted images while they save, and make them previewable (#18118)
* fix(native-chat): show pasted images while they save, and make them previewable Pasting an image into the native chat composer showed nothing until the clipboard image finished being written to disk, and the resulting chip could never render the image at all. Preview was blocked by path authorization, not by rendering. Clipboard pastes are written to the OS temp dir, which sits outside every allowed root, so the composer's own `fs:readFile` of the file Orca had just written was denied. `saveClipboardImageBufferAsTempFile` now authorizes the path it writes, the same way other Orca-produced external files are handled. The delay is the macOS paste route: Cmd+V is intercepted in main and delivered through the app-menu paste channel, which has no clipboard blob in hand, so the composer only learned an image existed after the save round-trip. A new `clipboard:readImageThumbnail` probe reads the clipboard in memory and returns a downscaled preview; it runs alongside the save rather than before it, so text paste gains no latency. The DOM-paste route needs no probe — it mints a blob URL from the clipboard file on the same tick. Attachments now carry `pending` and `previewUrl`: the chip appears immediately with the real image dimmed under a spinner, then settles in place on the saved path. Send is blocked while anything is pending, because a pending chip has no agent-readable path yet. Pending chips are kept out of the pane attachment cache so a mid-save unmount cannot strand one, and blob previews are revoked on remove/clear. SSH pastes now carry their connectionId onto the chip so remote previews read over SFTP. Verified in a real Codex native chat under an isolated dev instance: the chip appears in 42-61ms with a spinner, settles at ~141ms, three rapid pastes produce three independent chips with Send disabled throughout, and the lightbox opens the full 5120x2880 image read from disk. Ablation confirms the authorization fix: the written path reads back, an unauthorized sibling in the same temp dir does not. Claude-Session: https://claude.ai/code/session_01NnEfY8NpfFtVnboLKnmgdW * fix(native-chat): avoid stale image attachments and preview cache growth --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
c2fce80289 |
Fix agent dashboard setting configure (#18245)
* Make agents activity always-on; toggle via bell icon - Remove optional showAgentsSidebar setting - Replace sidebar view-toggle with bell-button for activity access - Agents activity now always accessible in sidebar - Preserve migration flag for introduction to existing users - Remove visibility inference utilities * Simplify sidebar when agents view active: hide workspace options, add to - Hide workspace options menu and add project button when agents view is active, reducing UI clutter in that mode - Add tooltip to the activity bell button for better discoverability - Localize sidebar search field text - Move search and filter toggles to local state in SidebarAgentsList, removing unused callbacks from thread list components - Manage search input focus properly when opening |
||
|
|
e3de6b2ce8 |
Add automation runs dashboard with pagination and filtering (#18226)
* Add automation runs dashboard with pagination and filtering Adds a new Runs view in the Automations page that lets users browse all runs across automations with status/host filtering, search, and pagination support. Includes virtualized table rendering for efficient handling of large run histories and summary cards showing 24h/7d success/failure counts. * Fix missing dependencies in useCallback hooks and imports Missing dependencies in useCallback can cause stale closure bugs. This adds missing state setters to dependency arrays and consolidates type imports for consistency. * Use keyset pagination for stable automation runs pages Pagination now uses createdAt:id boundaries instead of offsets, so new runs arriving between pages don't shift the window. Maintains backwards compatibility with legacy offset cursors. Move pagination to shared module, fix outcome counting for future-dated runs, and improve hook state tracking on authority re-pairing or target changes. * Extract automation run details to top-level page view Moves run display from detail pane to dedicated page, establishing three-level navigation (Automations → Runs → Run Details) and simplifying the detail pane component. * Fix pagination stability when automation runs share createdAt - Define a stable total order with createdAt and id tiebreaker to prevent runs tied on createdAt from being dropped when the boundary run is pruned between page requests - Retain cursor on failed pagination so pages remain retryable - Update ownerNotice type to AutomationActionNotice * Extract automations list panel and worktree map logic Split AutomationsPageSurface into smaller, focused modules for better maintainability and reusability. Move list panel UI rendering to AutomationsPageListPanel component and worktree map selection logic to a standalone utility function. * Add i18n strings for automation runs dashboard Adds localized strings for the automation runs dashboard view, including search, filtering by host and status, run counts for 24h/7d windows, and empty state messaging across all supported languages. * fix missing translation * fix missing translation |
||
|
|
a0de2fde0b | fix(terminal): confirm an unrecognized foreground before downgrading agent prompts (#18238) | ||
|
|
974f3164a0 |
Make terminal error overlays opaque (#18231)
* Make terminal error overlays opaque * fix(terminal): keep opaque error toast text readable * fix(terminal): keep toast fallback opaque on older browsers --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
fda9cae5e2 |
perf(renderer): build useRef seeds once instead of every render (#18159)
* perf(renderer): stop re-running useRef initializers and ref-mirror effects every render React evaluates the argument you pass to `useRef` on every render and discards every result after the first. 30 renderer sites did real work in there — walking every browser page/tab across all worktrees, building activation-order maps, and minting `crypto.randomUUID()` per render on browser pages and the AI vault. Also moves 12 verbatim ref-mirror Effects to render-phase assignment, and routes the `tab.rename` shortcut straight to the focused tab instead of through a store field every mounted tab subscribed to. * perf(renderer): drop Fix 2 (render-phase ref mirrors) to satisfy no-ref-current-in-render * perf(renderer): convert the four lazy-useRef sites that landed on main |
||
|
|
efe3305490 |
test(editor): isolate prefix bleed in the batched model sweep, drop a dead export (#18240)
Readiness-review follow-ups to #18144. The parity test in closed-editor-tab-disposal.test.ts cannot see prefix bleed: buildScenario closes tab-0..tab-99, so tab-10 is in the closed batch too and the per-tab oracle disposes its models via tab-10's own prefix. Batched and oracle agree and the assertion passes even with a bleeding predicate. Verified by mutation: replacing the boundary probe with a naive startsWith leaves all five of that file's tests green. Adds a test through the batched disposeClosedEditorTabs entry point with a still-OPEN tab-10 alongside a closed tab-1, which does fail under that mutation. Also records the `boundary + 1` advance in hasPaneScopeOwner as load-bearing for `:::` runs, with a test that fails under a `+ 2` "tidy-up", and removes disposeUnattachedMonacoModelsByPathPrefix, which #18144 left with zero production callers and a comment claiming it was kept for callers that do not exist. Finally, documents why title-derived rows carry `startedAt: 0`, which is the sole reason their `now` stamps cannot move a dashboard bucket. |
||
|
|
1910ab9c9c |
fix(github): bound and coalesce the Orca star check so gh children cannot pile up (#18239)
`checkOrcaStarred`, `starOrca` and `getAuthenticatedViewer` were the only gh call sites that reached for the legacy `execFileAsync` instead of `ghExecFileAsync`, so they ran with no deadline, no process-tree kill and no coalescing. A `gh` that never exits therefore ran forever and never released its slot in the 4-wide GitHub semaphore in gh-utils. Route all three through `ghExecFileAsync`, coalesce concurrent star checks onto one child, and hoist the Landing star-state effect out of the conditionally rendered footer so a repo-catalog rewrite no longer remounts it and re-forks gh. Adds a ratchet test asserting no file outside the command runner names `gh` as a spawned program. Fixes #18234 |
||
|
|
c7886e7a5f |
perf(diff): stop redrawing the whole combined-diff file tree on every section load (#18140)
Both PR files viewers rebuilt the section-index Map with `useMemo(..., [sections])`. An on-demand section load replaces the sections array while the section keys stay identical, so every load handed the file tree a new Map identity — a memo miss for all ~900 `CombinedDiffFileTreeRow`s. `useCombinedDiffTreeNavigation` already cached the map behind an entry-signature + per-index key comparison. Extract that into `useCombinedDiffSectionIndexMap` and use it from all three call sites. The extracted hook seeds its cache from a `useLayoutEffect` rather than during render, so a render React discards cannot leave behind an entry describing sections that never committed. |
||
|
|
d4fa091714 |
perf(terminal): repaint only the rows an agent redraw touched (#18169)
Forced foreground repaints asked xterm for rows 0..rows-1. xterm's render debouncer unions ranges, so one full-grid request widened every frame to a whole-viewport `_updateModel` cell walk even when the write changed five rows. Re-issue the repair over the parse's own dirty span instead, keeping the whole grid for viewport scroll, alternate-screen flips, and any write whose span cannot be observed. |
||
|
|
6b1cbe54a1 |
fix(process-table): fail a short ps capture loudly, and stop a resume spending 49 of them (#18166)
The POSIX process-table capture ran `execFile('ps', ...)` with no `maxBuffer`,
inheriting Node's 1MB default. Measured at 1,460 processes the capture is 326KB
with a 5,116-char longest row — ~3x headroom, which a busy host clears.
Two separate defects follow, fixed here:
1. `parseProcessTableRows` drops unparseable lines, so any short capture reads
as a COMPLETE table whose missing processes simply are not running. Verified:
a capture cut at 4KB parses to 59 of 1,463 rows, and an empty capture parses
to `[]`, both with no error — and `resolveAgentForegroundProcessWithAvailability`
then answers `available: true`. That is the `unverifiable` -> `exited` collapse
the execution boundary forbids. The capture now rejects with
`ProcessTableCaptureError` on a ceiling-length or row-less capture, so both the
lenient and strict views fail loudly and callers report unavailable.
2. `maxBuffer` is now an explicit 32MB, matching the sibling reader in
`pty-descendant-termination.ts` and its stated reasoning. Without it a 4,000-
process host fails EVERY capture, degrading the whole subsystem permanently.
Separately, `readStructuredTuiProcessIdentity` polled a fresh whole-machine `ps`
every 50ms for up to 5s. Each capture costs ~0.065 CPU-s, and the 5s ceiling is
only reached when the child never appears — where the tight interval buys
nothing. The interval now holds at 50ms for the first second, then doubles to a
500ms cap. Identification latency is unchanged for any child appearing inside
that window, and the 5s ceiling is unchanged.
|
||
|
|
698584f6ec |
perf(pty): stop startup history GC freezing the main process for seconds (#18165)
`runHistoryGc` walked every terminal-history directory synchronously ten seconds after launch: `readdirSync` on the root, then per directory a `statSync`, a `readdirSync`, a `statSync` per file, an `existsSync`, a `readFileSync` and a `JSON.parse`. On a real 613 MB root (2,776 dirs / 6,703 files) that is ~20,000 syscalls and 2,774 parses in one uninterruptible pass — the main process was frozen for the whole of it, 7.5-10.6 s on the reporting machine. Move the enumeration to `fs/promises` behind the existing `forEachWithConcurrency` fixed-worker pool over an iterative frontier, yielding through the shared `yieldToEventLoop()` every 32 entries. The pass is cancellable and a second call joins the in-flight one rather than racing its tombstone renames. Max main-thread gap over the real root: 122-155 ms -> 1.1-1.9 ms idle, 522 ms -> 1.1 ms under load. Syscalls per pass 20,578 -> 17,803. Total elapsed is lower too (88-108 ms vs 126-154 ms warm), so nothing is smeared into a longer tail. The prune decision logic and the tombstone path are untouched. A new suite asserts the new walk removes exactly the set the synchronous walk chose over a fixture covering every decision shape, and covers the races async introduces: a directory removed mid-walk, a half-written `meta.json`, and malformed/truncated/oversized metadata. All of those resolve to "keep", matching what the sync version did on a read error. |
||
|
|
b00ec20731 |
perf(startup): stop an unreachable SSH host from gating local terminal restore (#18164)
* perf(startup): stop an unreachable SSH host from gating local terminal restore An asleep or unreachable SSH target held the terminal-restoration gate for the full 15s reconnect timeout, so no terminal restored — local ones included. Startup now awaits only the target that owns the active workspace's tabs and lets the rest connect in the background, folded into the existing deferred path that reattaches their PTYs on tab focus. Also splits the renderer's git-environment fence out of the first-window PTY services barrier: worktree hydration needs shell-PATH generation and the managed WSL CLI registration, not a daemon PTY spawn or a hook-server bind. Terminal restoration still fences on the first-window services via app:prepareTerminalStartupRestoration. Measured with tests/tools/benchmarks/startup-time-bench.mjs (382 restored tabs, 28k-file profile, medians of 3): unreachable SSH host: 17.27s -> 1.34s to renderer-startup-hydration-done all-local: 1.98s -> 1.33s * fix(startup): restore the startup-ordering oracle and keep a connected background SSH target undeferred app-startup-routing.test.ts pinned the old step names, so the two ordering cases went vacuous-then-red when the barrier split. Repoint them at the steps that now carry the same fences: 'git-environment-barrier-await' (shell PATH + managed WSL, the fence host Git needs) before hydration worktrees, and 'prepare-terminal-startup-restoration' (which awaits firstWindowStartupServicesReady in main) before terminal reconnect. Both still fail against main's hydration source. Also: the timed-out-eager rewrite of the deferred list re-added background targets that had already connected, undoing removeDeferredSshReconnectTarget and sending fresh panes on a reachable host down the cold-restore path. |
||
|
|
d22d5c80a6 |
fix(terminal): stop a hidden pane's cursor blink deterministically (#18152)
A retained hidden pane keeps a live WebglRenderer, and the only thing that stops its 600 ms cursor-blink timer is a real DOM blur event. Today that arrives incidentally from display:none/visibility:hidden; under a hide mode that keeps focus (opacity:0 without inert) it never fires and the pane blinks — redrawing its whole cursor row per toggle — until the 5-minute idle timeout. Park terminal.options.cursorBlink on suspend and restore the parked value on resume, so the property holds regardless of which CSS hid the pane. Settings writes land on the parked value while hidden, so a mid-hide settings change cannot re-arm the timer behind the surface, and a user who disabled blink never gets it back. |
||
|
|
53f105827b |
perf(windows): stop asking the process table for memory, and share one projection per snapshot (#18151)
Two costs on the Windows process-table hot path, plus the EDR doc that described neither of them accurately. 1. The snapshot set `ProcessDataFlag.Memory` and surfaced `memoryBytes`, which nothing read. The addon serves that flag with a second `OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ)` and a `GetProcessMemoryInfo` per process (process.cc:47-63), so the flag was one wasted handle per process per snapshot. 2. The shared TTL cache gave every pane the same native rows array, but each pane still ran `native.map(toProcessRow)` over the whole table, rebuilt a `childrenByPpid` Map from scratch, and did two linear scans. The `.map()` also handed `getProcessTableIndex` a new array each call, defeating the POSIX memo by construction. Both now cache per snapshot identity, and the POSIX resolver drops its duplicate descendant walk. `getProcessTableIndex` / `buildProcessTableIndex` are generic over the row shape so the Windows rows reuse the existing pass instead of a parallel one. No behavior change: same rows in, same rows out, same descendant ordering and same has-children answers. |
||
|
|
9377214b4b |
perf(renderer): reconcile hydrated workspaces in one store write (#18150)
* perf(renderer): reconcile hydrated workspaces in one store write Session hydration reconciled each workspace with its own set(), so a 193-workspace session fanned 193 writes out to every non-React store subscriber and re-spread three whole workspace-keyed maps per workspace. Fold the whole session into one patch, release the string-keyed terminal scroll-intent entries on pane close, and drop the per-workspace/per-tab reconnect debug logs. * fix(test): make the hydration fixture bucket switch exhaustive oxlint --type-aware flags the default arm; naming the editor case clears it. |
||
|
|
89bd18990d |
perf(renderer): stop three always-mounted selectors rescanning the store (#18136)
Zustand reruns every subscriber's selector on each store write. Three selectors did an O(N) scan of a store collection inside that path, so at 10 repos / 423 worktrees / 382 tabs they were paid thousands of times a second while the app sat idle. - getLocalWorktree / getLocalRuntimeRepoForWorktree now read the shared WeakMap indexes (getIndexedWorktreeById, getIndexedRepoMap) instead of `Object.values(worktreesByRepo).flat().find(...)` and `repos.find(...)`. SidebarTaskNavButton is always mounted and calls this on every write. - selectRepoByIdForActiveWorkspace caches its host-scoped resolution in a WeakMap keyed on the `repos` array, mirroring getIndexedRepoMap. - getProjectRuntimeSessionSummary memoizes per (tabsByWorktree, ptyIdsByTabId, agentStatusByPaneKey, repoId) and reuses the existing identity-cached getTabIdToWorktreeId index. |
||
|
|
dff7f91401 |
perf(sidebar): stop rebuilding per-card selector records on every store write (#18133)
Zustand re-runs every mounted subscriber's selector on every store write. The
per-worktree sidebar selectors built a fresh Record per call, so 15 visible
cards x 6 reads x every write allocated a record each time even when nothing
they read had changed.
- Add createWorktreeRecordSelector: gates the build on the source slice
identities, memoizes per worktree id, and carries the previous generation
forward so a rebuild with equal contents keeps its reference.
- Route the pane-title, live-PTY, layout-root and terminal-layout selectors
through it, and return a shared frozen empty when a worktree has no tabs.
- Swap useWorktreeAgentRows' inactive-branch `[]`/`{}` literals for the shared
frozen constants so the `active` gate actually short-circuits on identity.
- Identity-cache the sidebar pending-worktree-creation key list, which ran
Object.values(...).map(...) from an always-mounted subscriber.
- Drop `key={text}` from TruncatedSidebarLabel so a label change remeasures in
place instead of remounting the span and rebuilding its ResizeObserver.
- Remove the non-compositable `width` from the board drop indicator's
will-change hint.
|