mirror of
https://github.com/stablyai/orca.git
synced 2026-10-12 16:03:03 +00:00
fix/execution-host-resolution
1877
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b6ea3f17a9 |
refactor(shell): one portable Unix startup dialect instead of shell detection (#14863)
Orca had to guess which shell would parse a queued command line, then emit syntax for it. Guessing is unreliable for a remote or WSL host, and every dialect-dependent function is a place to get it wrong. Replace the guess. Everything emitted for a Unix shell is now built to be correct in sh, bash, zsh, dash, ksh and fish alike, so no detection is needed: - quoteStartupArg emits backslashes as "\\" and apostrophes as "'" between single-quoted runs. Both families read that identically, unlike the sh '\'' idiom, which fish silently halves and which makes a trailing backslash a hard syntax error. - clearEnvCommand emits a self-contained fish/sh branch. It deliberately does NOT call a helper defined by Orca's shell wrappers: Orca wraps only zsh, bash and fish, so an `sh`/`dash`/`ksh` login shell launches unwrapped — and the same text is copied to the clipboard and pasted into shells Orca never spawned. In both, a helper would be `command not found`, which is the exact failure this exists to avoid. Two guarded statements rather than `A && B || C`, because fish's `set -e` returns non-zero for an already-unset variable and would fall through to the sh branch; a trailing `true` pins the status, since this is the last statement of a launch line and the prompt renders it. - One tokenizer for Unix. The input is a settings string the shell never parses, so parsing it per-shell only made the same setting mean different things in different workspaces. AgentStartupShell loses its 'fish' and 'unix' members, and the three login-shell resolvers, the fish tokenizer and the agentEnv.SHELL probe go with them. Per-worktree shell history now actually works: - zsh on macOS was a no-op. /etc/zshrc assigns HISTFILE unconditionally before any wrapper Orca controls, so the injected value was already gone — and with ZDOTDIR still pointing at Orca's wrapper dir, history landed inside it. The intended path rides ORCA_HISTFILE and is restored after user config. Fixes #11044. - fish keeps history in its own data dir keyed by session name, since it ignores HISTFILE and has no custom-directory knob. Files are deleted rather than truncated, a symlinked ~/.local/share no longer disables cleanup, and a GC sweep reclaims orphans whose meta.json is gone. The sweep refuses an empty live-worktree set (indistinguishable from a store that failed to hydrate) and skips files younger than GC_MIN_AGE_MS, mirroring the tree GC's guard against the live-set snapshot race. Verified against real shells rather than asserted as strings: startup-shell-portability.live-shell.test.ts runs 194 assertions across sh/bash/zsh/dash/ksh/fish, and zsh-scoped-histfile.live-shell.test.ts drives a real login zsh through /etc/zshrc. Both are vacuity-checked. The same quoting corpus was replayed byte-exact on Linux, where /bin/sh is dash. |
||
|
|
fa9b20cb41 | feat(skills): reland private bundle sharing safely (#14934) | ||
|
|
9f3a912c1e |
fix(terminal): type Option-composed ASCII instead of reporting it as a chord (#14743)
* fix(terminal): preserve Option-composed ASCII input * fix(terminal): preserve Option keyboard protocol semantics * fix(terminal): complete Option keyboard event encoding * fix(terminal): harden Option input encoding * fix(terminal): close keyboard protocol fallback gaps * test(terminal): prove Option-composed ASCII reaches the pty end to end The Option-compose fix had unit coverage only. This drives a live Electron pane whose kitty flags are armed by the application's own CSI > 1 u and asserts the bytes at the pty boundary: composed `@` and Shift-layer `\` arrive as text, configured Option-as-Alt still reports the layout-resolved chord, and a non-ASCII glyph still reaches the app as its alt hotkey. Restoring the pre-fix policy fails exactly the two composed-text scenarios. Also records the ASCII rule's rationale where the rule lives, not only in a test comment. * refactor(terminal): drop the unread Option layers from the layout snapshot The native helper computed an Option and Option+Shift character for every key, shipped both over IPC, validated them in the parser and cached them in the renderer — but no production caller ever asked for them. Only the base and Shift layers are read, and Shift is the one the web layout map cannot supply, which is why the helper exists at all. Removing them halves the helper's UCKeyTranslate work per key and drops the option parameter that six signatures were threading through for nobody. |
||
|
|
3c8410d927 |
Revert "fix(browser): route every cookie-import write through CDP identities …" (#14942)
This reverts commit
|
||
|
|
ef1224c4f7 |
Revert "Preserve OpenCode session across command completion, control SessionS…" (#14943)
This reverts commit
|
||
|
|
bf6dc6fcba |
fix(browser): route every cookie-import write through CDP identities (STA-4300) (#14729)
* test(browser): repro STA-4300 CHIPS partition downgrade on native cookie import success path * fix(browser): route every cookie-import write through CDP identities (STA-4300) * test(browser): cover partition fidelity on both import write paths (STA-4300) * fix(browser): never stage unreadable cookie partitions (STA-4300) * fix(browser): gate partition skips on client support (STA-4300) * test(browser): anchor the client partition-skip capability assertion (STA-4300) * fix(browser): skip Firefox CHIPS cookies whose Chromium identity cannot be rebuilt (STA-4300) * fix(browser): tolerate a Firefox schema without originAttributes (STA-4300) * docs(browser): correct a comment that still named the removed cookies.set write * fix(browser): block lossy Firefox cookie import on old clients * fix(browser): read Firefox CHIPS from schema flag |
||
|
|
b60df2e3d6 |
Revert "feat(workspace): set project location from the create-worktree host p…" (#14912)
This reverts commit
|
||
|
|
763b1febeb |
Revert "feat(skills): add private bundle sharing (#14401)" (#14913)
This reverts commit
|
||
|
|
1da1bdc01c |
Preserve OpenCode session across command completion, control SessionStart emission (#14866)
* Preserve OpenCode session across command completion - Add session start events and launch token tracking to establish session boundaries - Defer retiring launch authority until OpenCode process actually exits, not just when a command finishes - Fence previous tokens after restarts to prevent status updates from stale sessions - Maps SessionStart as a session boundary for proper turn/state management * Emit SessionStart only from OpenCode, not mimo-code Restrict SessionStart lifecycle events to OpenCode exclusively. Mimo-code no longer emits SessionStart, as it should rely on OpenCode for session boundary signals. This prevents duplicate lifecycle events that could interfere with pane authority tracking and session state management. Also tighten foreground process result validation to reject stale results after title observation changes, fixing a race where a delayed foreground read from a previous cycle would incorrectly retire authority. |
||
|
|
e4e54a17d0 |
feat(workspace): set project location from the create-worktree host picker (#14868)
* feat(workspace): set project location from the create-worktree host picker Hosts that still say "Project location not set" now get an inline Set location action. It opens a nested dialog over Create worktree so the in-progress form stays put. * fix(workspace): replace unset-location status copy with a button Drop the redundant "Project location not set" caption and show a Set project location action with a hover tooltip instead. * refactor(workspace): tighten the set-project-location dialog - reuse CreateProjectParentBrowser instead of a second host-filesystem browse view - drop ProjectLocationBrowseTarget; parseExecutionHostId already models it - single setLocation path in RunTargetCombobox (row, button, Enter) - memoize the default clone URL instead of scanning on every store update - fix missing required props in the new composer-card test * fix(workspace): close the correctness gaps in set-project-location - Escape in the host browser now backs out to the form instead of dismissing the dialog and discarding the half-filled path/clone URL. Radix dismisses from a document-capture listener, so only preventDefault can stop it. - Drop the stopPropagation guards: window-capture (the composer's Escape handler) already ran by then, so they never protected it — the nestedDialogOpen gate does. They did silently kill RemoteFileBrowser's own key handling. - Hide Set project location for host-local repo:<id> projects (folder projects, git repos with no remote). Linking on another host matches by project identity, which those have none of, so the call could only ever toast an error. - Drop a standalone placeholder setup once a repo projection covers the same project+host, restoring the (projectId, hostId) uniqueness invariant. Setting a location on a host with a pending setup was leaving a ghost that sorts first and reads back as 'not set up'. - Existing-folder submit label matched a catalog string reading 'Importing...' * test(composer): follow the renamed local in the host-retarget source assertion |
||
|
|
757fae28d7 |
feat(skills): add private bundle sharing (#14401)
Co-authored-by: E2E Test <e2e@test.local> |
||
|
|
fd1dba9db9 |
fix(daemon): validate spawn cwd asynchronously so one dead share cannot freeze every terminal (#14848)
* fix(daemon): validate spawn cwd asynchronously so one dead share cannot freeze every terminal createOrAttach validated the working directory synchronously on the daemon's only thread. Measured on Windows 11 + Ubuntu-24.04: existsSync on an unreachable UNC share 21,022 ms wsl.exe probe, cold distro 1,266 ms wsl.exe probe, warm distro 59 ms existsSync/statSync on healthy \\wsl.localhost 4 ms / 1 ms A single unreachable share therefore blocks the whole RPC loop past the client's 30s request ceiling, so every other terminal stalls behind it and reports `DaemonProtocolError: Request createOrAttach timed out after 30000ms`. The main process already validates asynchronously and passes prevalidatedCwd (ipc/pty.ts); the daemon never got the same treatment. Add validateWorkingDirectoryAsync (one stat, not exists-then-stat, so an unreachable share is not paid for twice) and await it from the daemon spawn preflights. spawnSubprocess now returns SubprocessHandle | Promise<...>, which existing sync stubs still satisfy. Deliberately not bounding the stat with a timeout: the 30s ceiling comes from blocking the shared loop, not from the duration. A timeout cannot tell "slow share" from "gone share", so it would fail spawns that succeed today at 3-8s on a cold VPN mount, and trade an accurate "working directory does not exist" for a guess. The new await opened a race: it sits between the "already exists?" check and the sessions.set that publishes the session, so two concurrent creates for one session id both spawned. Gate creation per session id; distinct ids still spawn in parallel. STA-4470 * fix(daemon): fence async spawn lifecycle |
||
|
|
6cf6a7faff |
feat(crash-reporting): capture Crashpad minidumps and name the failing CHECK (#14823)
* feat(crash-reporting): capture Crashpad minidumps and name the failing CHECK 40% of renderer deaths report exit 0x80000003 (STATUS_BREAKPOINT) — a Chromium CHECK/DCHECK — and we captured only the exit code, so the cause was structurally unknowable. Nothing in the tree wired crashReporter at all. Start Crashpad pre-whenReady and lift the text signature out of the dump: Chromium stores the fatal log line in the LOG_FATAL annotation, so the check name, file and line are recoverable with no symbols and no minidump_stackwalk. Upload stays off. The existing transport is a user-initiated 4 MiB text bundle; raw dumps are multi-MB binary carrying process memory. Dumps stay on disk and only the signature rides the existing crash-report flow. - minidump-stream-reader: bounds-checked view; a truncated dump degrades - minidump-crashpad-annotations: allowlisted keys (switch-N carries command lines) - minidump-crash-signature: LOG_FATAL -> file/line, exception, faulting module - crashpad-capture: polls for the dump, which races process-gone delivery STA-4469 * refactor(crash-reporting): claim dumps once and match them to the dead process A newer dump from a different process could be paired to the wrong report, and two reports in one crash burst could both claim the same file. Match the dump's own ptype against the process Electron said died, and claim each dump once. Also let the fatal line use the stack-length budget: a CHECK message truncated at 240 chars can lose the condition, which is the diagnosis. Fix the child-type test to match the classifier: GPU exits are recoverable churn and never become reports, so they must not burn a dump poll either. STA-4469 * fix(crash-reporting): recover real Electron CHECK logs Electron 43 Windows dumps carry the Chromium CHECK line in captured memory but omit the claimed LOG_FATAL annotation. Recover only bounded Chromium-formatted fatal/CHECK lines, and prune raw dumps after crash events so suppressed child crash storms stay within the 128 MiB budget. * fix(crash-reporting): satisfy not-found lint |
||
|
|
83117f2860 |
refactor(integrations): split issue-tracker clients under the max-lines budget (#14704)
The GitLab, GitHub, Jira and Linear integration modules, their two IPC registrars, and the shared GitHub project types each carried a file-level `eslint-disable max-lines` and ran 351-614 counted lines against a 300-line budget. AGENTS.md calls for splitting rather than suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all eight suppressions and prunes their entries (341 -> 333). Pure move, no behavior change. Each client is cut along the seam it already had: per-operation modules for the issue APIs (create / update / comment / field options), and for Jira the request queue, site credential store, authenticated request, and site identity. The two IPC registrars keep their own handlers and delegate the rest to per-domain sub-registrars, so they remain real entry points rather than re-export shims. The IPC surface is proved intact rather than assumed: comparing (method, channel) multisets between HEAD and the split gives 52 registrations across 52 distinct channels on both sides. Provider-neutrality is preserved -- GitLab and GitHub keep separate, parallel module layouts rather than being merged behind a shared abstraction. Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green (the one remaining failure is a pre-existing load flake in an untouched file, green when re-run serially), no new runtime import cycles among 744 modules, and no lint suppression added anywhere. |
||
|
|
a234123751 |
feat(shortcuts): warn when macOS Mission Control captures digit chords (#14734)
* feat(shortcuts): warn when macOS Mission Control captures digit chords Mission Control's Switch to Desktop shortcuts (Ctrl+digit by default, present whenever the user has multiple Spaces) are consumed by WindowServer before the app receives the event, so Orca's digit-range shortcuts silently do nothing and the app can never observe the press. Detect the conflict instead: probe com.apple.symbolichotkeys through the live-prefs pipeline on Shortcuts pane mount and surface a standing conflict warning on the affected rows, counted into the Conflicts stat. Non-Darwin and the web client return no chords, and any probe failure yields an empty result so a missing signal can never show a false warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(shortcuts): harden Mission Control conflict warnings --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
5b7f44278a |
fix(workspace-cleanup): refuse removal when the owning host is not certain (STA-4343) (#14731)
* fix(workspace-cleanup): refuse removal when the owning host is not certain (STA-4343) * fix(workspace-cleanup): distinguish host collisions * fix(workspace-cleanup): recheck host at removal boundary |
||
|
|
2eb3e11327 | fix(terminal): make close and handles incarnation-stable (STA-4327) (#14590) | ||
|
|
9367169888 |
refactor(tests): split every oversized test file off the max-lines suppression list (#14728)
* refactor(tests): split oversized test files off the max-lines suppression list Every `*.test.ts`/`*.spec.ts` that carried an `eslint/oxlint-disable max-lines` directive is now split into focused, behavior-scoped suites that fit the 800-line test budget, with shared setup extracted into co-located `*-test-harness.ts` / `*-test-fixtures.ts` modules (300-line budget). 83 files became ~930; the largest output is 797 effective lines. `orca-runtime.test.ts` is intentionally untouched. Test bodies were moved by scripted line-range slicing rather than retyped, so assertions are byte-identical. The only permitted body edits were mechanical rebinding where a shared value moved into a harness (e.g. `tmpHome` -> `homes.tmpHome`). Registries that enumerate test files were updated in lockstep: - config/max-lines-baseline.txt: pruned 341 -> 258 entries (all 83 removed). - config/reliability-gates.jsonc: 33 gates repointed at the split files, with assertionRefs split per file where a gate's coverage now spans several. - .github/workflows/pr.yml: the real-zsh lane now lists the 4 split files that actually exercise zsh, so they keep running in the dedicated shell lane. Also renamed agent-hooks `server-test-fixtures.ts` to `server.test-fixtures.ts` so the global-fetch call-site audit keeps skipping it, and added `.js` extensions to the CLI suites' dynamic harness imports (node16 resolution) to unbreak `build:cli`. Verification: full suite 52,449 passing vs 52,448 at baseline with zero assertions lost; `pnpm lint`, `pnpm typecheck`, and `pnpm build:cli` all exit 0; the terminal-pane e2e spec runs 31/31 headless. * refactor(tests): split hook-idle arbitration suite that oxfmt pushed over budget The pre-commit oxfmt pass reflowed pty-connection-hook-idle-arbitration.test.ts to 811 effective lines, 11 over the test budget. Split the hook-completion side effect and replacement-agent veto cases into their own suite; both files now sit well under the cap and the 15 tests are unchanged. * test: port upstream test changes into the split files after rebase Rebasing onto main surfaced 27 tests that main had added to files this branch deleted, plus edits to tests that had already moved. Taking the deletion side of those modify/delete conflicts would have dropped that coverage silently, so each upstream change is ported into the split file that now owns the behavior — for example main's six orchestration mailbox tests land across orchestration-runs, -send, and -check. Also repoints `orchestration.notification-mailbox-consistency`, a gate main added after this branch's gate remap, at those same three split files, and re-prunes the max-lines baseline against main's (257 entries). Verified: all 27 upstream test titles present; full suite 52,761 passing with the only diff vs baseline being 12 tests main itself removed and 3 that moved from skipped to passing; lint and typecheck exit 0. * fix(test): flush pending continuations before tearing down terminal test globals CI shard 5/16 failed on both Node 24 and 26 with `ReferenceError: window is not defined` from pty-connection.ts, surfacing through pty-connection-daemon-snapshot-replay.test.ts. The reattach/settle chains `await` a real promise and then touch `window.api`. Under fake timers those continuations cannot run, so they only become schedulable once restoreTerminalTestGlobals() switches back to real timers — which previously happened immediately before `delete globalThis.window`, so a late continuation threw and failed the whole file. Flush async ticks in that window instead. This is latent in the source rather than new: the pre-split 25k-line file kept running other tests after these, which gave the chains time to settle before teardown. Splitting the file moved teardown directly behind them. * fix(test): keep an inert window after terminal test teardown instead of deleting it The async-tick flush was not enough: the reattach/settle chain can resolve after teardown regardless of how long we drain, so CI shard 5/16 still failed with `ReferenceError: window is not defined` from pty-connection.ts. A real renderer never loses `window`, so deleting it was the artificial part. Swap in an inert proxy whose properties resolve to callables and whose calls resolve to undefined, making a late `window.api.pty.*` call a harmless no-op. The next test replaces it wholesale via installTerminalTestGlobals(), and no test asserts that `window` is absent. |
||
|
|
375b735e9c |
fix(agent-launch): preserve cold Codex startup drafts (#14688)
* fix(agent-launch): preserve cold Codex startup drafts * fix(agent-launch): honor startup draft readiness budgets |
||
|
|
ab9d1a29a9 |
fix(worktree): never reissue a generated workspace name (#14350)
* fix(worktree): never reissue a generated workspace name
Generated workspace names were deduped only against currently-live
worktrees, so deleting a workspace returned its name to the pool. A later
workspace could draw the same name, land on the same directory path, and
inherit the previous occupant's agent conversation history — coding-agent
CLIs key their prompt history and transcripts by cwd.
Names are now retired permanently per repo. The registry is written in
main with the name Git actually used (the create loop can advance past a
requested name on collision), and seeded once per run from workspace
directories and surviving agent transcript buckets so already-spent names
are excluded from the start. Suggestions degrade to -2, -3 variants
instead of recycling, and those variants retire too.
User-typed names are untouched: retirement filters suggestions only.
* fix(mobile): honor retired workspace names, on one shared implementation
Mobile hand-duplicated the desktop name-suggestion algorithm and deduped
only against live workspaces, so a phone could still be offered a name
whose deleted workspace left agent conversation state behind at that path.
Both platforms now call one shared selector in src/shared, so the two can
no longer drift. The host publishes retired names as an optional field on
the existing worktree.list response, and mobile fetches them per selected
repo while the create sheet is open — mirroring the desktop hook.
Mobile never calls worktree.list for its catalog (it uses worktree.ps,
which carries rows only), so this is a targeted request rather than a
change to the catalog or its cache. Hosts predating the field omit it and
mobile falls back to live-only dedupe, which is the pre-change behavior.
* fix(worktree): close retirement consistency gaps
* test(worktree): cover retirement runtime contracts
* fix(worktree): retire generated collision names
* fix(worktree): enforce retired names at creation
* refactor(ai-vault): extract the Claude project-dir encoder
The bucket-name encoder and its scope-boundary check were private to the
session scanner, so a second consumer had to reimplement them — and got the
per-character encoding wrong. Move both to a shared module with direct tests.
* fix(worktree): make the retirement seed scan actually match buckets
The bucket encoder collapsed runs of non-alphanumerics while the real one
emits a dash per character, so every dot-path bucket missed and the Windows
default workspace root (C:\...) matched nothing at all. Reuse the shared
encoder and its boundary check, which also stops a repo absorbing a sibling
whose path merely shares its prefix.
Also:
- Derive the workspace leaf by stripping the known encoded parent instead of
guessing from trailing dash segments, which retired the parent directory's
name whenever a workspace was named numerically.
- Reuse isAutoGeneratedCreatureBranchName so the -10 and -100 tiers retire.
- Drop the .codex/sessions root: Codex keeps the cwd inside the transcript
rather than in a directory name, so the scan could only ever see a year
folder. Reading transcript contents is not a trade this feature justifies,
so the gap is documented instead.
- Honor CLAUDE_CONFIG_DIR, which relocates the bucket root.
- Delete the unused retirableLeafName export.
Tests write buckets with the real per-character encoding against a fake home,
covering POSIX, dot-directory, Windows drive and WSL UNC roots; all three
platform cases fail against the previous encoder.
* fix(worktree): retire only generated names, keyed by cwd namespace
Two problems in the host-side registry.
Retirement fired for every create, including names the user typed. The
creature pool contains ordinary words — orca, runner, sole, molly, oscar — so
typing a retired 'nautilus' silently produced directory and branch
'nautilus-2' and burned the name for good. Creates now carry an explicit
nameWasGenerated flag; both the skip and the retire are gated on it, and it
defaults to false so CLI and automation callers are unaffected.
The registry was keyed by repo id, but both readers already discarded the id
and unioned by the cwd collision key, because the collision this prevents is
on the path. Keying by that namespace directly fixes several things at once:
entries no longer orphan when a repo is removed, remove/re-add no longer loses
every retirement for an unchanged path, the missing removeProject prune is
moot, and the backfill promise no longer merges into only the first repo id it
saw. The feature is unreleased, so no migration is needed.
Also:
- Memoize the collision key. It runs computeWorktreePath, which for a WSL repo
is a blocking execFileSync('wsl.exe') whose failure path is uncached, and
the previous code recomputed it once per repo on every create and every
listRetiredNames call.
- Drop retiredNamesByRepo from the worktree list result. It had no readers and
leaked onto 'orca worktree list --json', and its awaited backfill sat on CLI
selector resolution. The dedicated listRetiredNames RPC keeps its consumers.
- Make the three RuntimeStore methods required. RuntimeStore is file-private
with two constructors, so the 'older embedders' the optionality protected do
not exist, and the optional chain silently returned no retirements.
- Revert the unrelated forceDeleteBranch rewrite, and make room under the
file's line budget by extracting the create-args mapping instead.
* fix(worktree): send name provenance and stop gating Create on the fetch
Desktop and mobile now mark a create as generated-name only when the user
typed nothing and the composer fell back to the suggestion, so the host knows
which names it may retire.
Remove the retired-names loading gate from every create path. The host already
skips retired candidates before doing any git work, so the client gate bought
nothing while it could disable Create for the length of a full mobile
reconnect ladder (the wait had no timeout) and blank the desktop button
between queued creates. The suggestion still waits; the button never does.
Also make the web client call worktree.listRetiredNames instead of hardcoding
an empty list — the method is registered and mobile-allowlisted, so the
comment claiming no wire call existed was wrong — and filter the mobile
response to strings so a malformed row cannot throw during normalization.
* fix(worktree): key retirement by repo id and prune it with the repo
Reverts the collision-key storage key. It was a function of workspaceDir,
nestWorkspaces, worktreeBasePath and repo.path, so toggling any one of those
orphaned every retirement for every affected repo at once — trading a rare
churn (remove/re-add) for a common one. The read path already unions by cwd
namespace at query time, so cross-repo sharing never depended on the storage
key.
Instead, address the growth and orphaning directly:
- Drop the registry in removeProject, and in removeProjectForHost once the last
host's copy of the repo id is gone, alongside the sparse-preset deletes that
already follow this convention.
- Bound each repo's registry. The cap sits far above the 552-name pool because
evicting inside it would reissue a name whose agent state is still on disk;
only -2/-3 tier accumulation can ever reach it.
- Carry retirements through profile transfer, re-keyed to the destination repo
id and dropped from the source, mirroring sparsePresetsByRepo.
Separately, fix the backfill merge: the scan promise is cached per cwd
namespace, but it closed over the first repo id that triggered it, so a second
repo in the same namespace received nothing. The scan stays shared; the merge
moves out of the cached promise and runs for whichever repo asked.
Local repos re-seed on re-add through that backfill. SSH repos do not — the
scan cannot see the execution host — which is now stated in the module.
* docs(worktree): spell out why the retirement bound sits above the pool
Names the trap directly: the neighbouring 50/200 bounds cap histories, so
lowering this one to match them would silently start reissuing names whose
agent state is still on disk. Also states that oldest-first eviction is a
deliberate least-bad choice rather than a neutral one.
* fix(worktree): send name provenance from the web runtime client
This client hand-enumerates worktree.create params, so the new optional field
was silently dropped and typecheck could not see it. On web and paired-desktop
the host therefore never received it: generated names were never retired, and
the host-side skip that backstops a stale suggestion was disabled too. The same
client does fetch retired names for suggestions, so it was filtering against a
registry nothing ever wrote to.
The test asserts both directions, and fails without the fix.
* fix(worktree): retire names that took more than one collision suffix
isAutoGeneratedCreatureBranchName strips exactly one trailing -N, which is
right for auto-rename eligibility but wrong here. Once the pool is spent the
suggester emits nautilus-2, and a collision on that yields nautilus-2-3 —
which a single strip leaves as nautilus-2, not a pool name, so retirement
no-opped at exactly the tier where every base name is already gone. Strip
repeated suffixes locally rather than moving the auto-rename predicate.
* perf(worktree): keep the retirement backfill off the blocking WSL probe
The backfill runs on composer repo-select, not just at create time, and it
derived the probe path synchronously — which for a WSL repo with a mirrored
workspace dir reaches getWslHome and its blocking execFileSync('wsl.exe').
A stopped distro froze the main process for up to 5s on composer open.
Adds an async twin of computeWorktreePath and uses it for the probe. Resolving
the home there also warms the shared cache, so later sync callers are free.
Also stops memoizing the collision key when the WSL home is still unresolved:
only the success path is cached upstream, so caching the fallback namespace
would strand the repo there for the rest of the session.
* fix(worktree): hold retired names across a refresh instead of blanking
refreshKey changes on every workspace-list mutation, so create-multiple
refetches after each create and the hook returned an empty list until the
refetch landed — precisely the window in which resetForNextCreate clears the
name field and a fresh suggestion is drawn. Keep the previous answer while
revalidating and reset only when the repo changes; a failed refresh keeps what
was already loaded rather than un-retiring everything.
Also makes the returned array referentially stable, so the suggestion memo
downstream stops rerunning on every refetch.
* refactor(worktree): put the retired-name cache rules on one implementation
The desktop and mobile hooks that fetch retired names had already drifted
four ways. The transports genuinely differ (IPC vs RPC), but the caching
rules must not, and mobile's copy reset to [] on any error -- which
un-retires every name for the rest of the sheet session, the one outcome
retirement exists to prevent.
Moves the rules into src/shared/worktree/retired-name-cache: response
normalization, the never-leak-across-repos rule, and the hold-previous-on-
failure rule. Pure, no React, because src/shared is on the main process's
import graph. Each platform keeps its own transport and effect.
Mobile moves up to desktop's behavior: it now holds the previous answer
through a failed refresh, and refetches when the workspace list changes
instead of never refetching after mount.
Also drops the unused `loading` return. Neither platform consumed it; its
only consumer was the Create-button gate reviewed out earlier, and removing
it makes that regression unexpressible.
* fix(worktree): import shared types from their real modules
Main dropped the src/shared/types barrel, so the retirement module's import
resolved locally but not against the PR's merge base.
* refactor(worktree): bound the retirement registry by tier compaction, not eviction
Retirement is a correctness guarantee — a spent name's directory may still hold
agent conversation state keyed by that cwd — so the 2000-entry cap was the wrong
shape: reaching it handed a name back. At the owner's measured rate (~6.6 pool
names retired per day in one repo) the cap was ~9 months out.
Names come from a fixed 552-entry pool and the suggester only reaches tier N+1
once every tier-N name is taken, so a completed tier is exactly a set that no
longer needs listing. A row is now a watermark plus the names above it: reads
answer at-or-below the watermark with no lookup, and compaction drops the 552
entries the watermark now covers. Bounded at one pool per repo forever, with no
eviction and nothing un-retired.
Tiers can complete out of order (a create-time collision can spend `nautilus-2`
while tier 1 is open), so compaction loops and higher-tier names simply wait.
The RPC result carries the watermark beside the names as a new field; a client
predating it reads the names only and under-retires the compacted tiers, which
degrades to the pre-retirement behavior rather than breaking.
* fix(worktree): preserve generated name retirement across failures
|
||
|
|
a275f5ad85 |
fix(skills): cancel and bound abandoned skill discovery scans (#14670)
* fix(skills): cancel and bound abandoned skill discovery scans A root on a stalled network mount never settles its readdir, so after 30s the coalescer starts a replacement walk. The abandoned walk kept running with no cancellation, and nothing counted it, so live filesystem work accumulated for the life of the process. Superseding a scan for age now aborts it, and `findSkillFiles` plus the candidate tasks bail on the signal — that cannot unblock a syscall already in the kernel, but it stops an abandoned walk issuing more of them. A budget caps how many abandoned scans may be live at once; past it a replacement is shed rather than started, and the stalled entry is left in place so the root recovers on its own once the mount answers. The budget counts abandoned scans rather than live ones on purpose: one discovery legitimately walks a dozen-plus roots at once, so a cap on live scans would shed healthy roots and empty the picker. A `refresh` still does not abort what it supersedes — on a healthy root that scan is about to finish and its callers want an answer, and the existing publish fence already stops it writing a pre-mutation result. Shed roots report the new `unavailable` skipped reason so they stay distinct from roots that genuinely are not there. * fix(skills): propagate a walk abort thrown through a symlinked directory The broken-link `catch` around the symlink branch also wrapped the nested `visit`, so an abort thrown from inside a symlinked subtree was swallowed. When that link was the last entry, nothing afterwards re-checked the signal and the walk returned a truncated listing as success — the exact outcome the abort path exists to prevent. Only the `stat` is guarded now. The test pins the narrow window by aborting inside the symlink's stat, after the entry loop's check and before the nested visit's. * fix(skills): degrade an aborted root instead of failing the whole discovery Aborting a scan abandoned for age gave the coalescer a way to reject that it did not have before: every error inside the walk and the candidate tasks was caught locally, so the task effectively could not fail. Callers already waiting on that scan now see it reject, and `scanRootShared` re-threw anything that was not a shed — so one slow root failed the entire discovery and emptied the picker for every healthy root beside it, which is exactly what the shed path was written to avoid. Both endings now mean the same thing to a caller that can degrade a single root, behind one predicate: shed before the walk began, or aborted after it was abandoned. `discoverSkillsOnTarget` runs a second coalescer over the whole target, where there is no partial answer to degrade to, so it converts both into a retryable error rather than leaking the internal class to IPC/RPC. It must not answer with an empty result — zero skills reads as "nothing installed" and re-offers installs for skills that are present. Also drops the scan key from the shed message, which carried an absolute workspace path to a paired client and the renderer's error string, and corrects the budget comment: it is global and per-abandonment, so one wedged root can spend it alone, one replacement per 30s window. * fix(skills): keep the original error as the cause of a stalled-target error The target layer replaces the internal error with a user-facing one, which discarded what actually went wrong. Attaching it as `cause` keeps that in logs while the message stays free of the host path. Also records why name-matching is narrow rather than broad: the walk and the candidate tasks catch every filesystem error locally, so the only AbortError that can escape a scan is the one its own signal raised. * test(skills): pin that a real abort produces the name the predicate matches `isSkillRootUnavailableError` decides on `error.name`, and the discovery and target tests fabricate that shape rather than driving a real abort. So nothing pinned the linkage: if `throwIfAborted()` stopped producing an `AbortError` that is an `instanceof Error`, every test would still pass while a stalled root began failing whole discoveries again. The walk test already drives a genuine abort, so it asserts the name, the Error subclassing DOMException relies on, and the predicate itself. |
||
|
|
e12b22c435 |
fix(terminal): bracket agent-pane pastes so a pasted newline can't submit the draft (#14456)
* fix(terminal): bracket agent-pane pastes so a pasted newline can't submit A paste of <=64KB is handed to xterm's term.paste(), which rewrites newlines to CR and wraps in ESC[200~/ESC[201~ only when its parser observed DECSET 2004. Windows ConPTY never forwards that mode, so an unbracketed pasted newline reaches a TUI agent as Enter and submits the draft parked in its composer. The existing force-bracket guard keyed on isWindowsUserAgent() - the client's platform - but the ConPTY can be on a remote host, so the guard was off in exactly the configuration that needs it. Gate on the pane's own TUI agent instead, applied to all four paste entry points; middle-click primary selection had no force flag at all. The agent status row is retained deliberately and is not lifecycle-driven, so it can outlive the agent. Veto on shell-confirmed foreground (OSC 133;D) and on a row rehydrated across an app restart. The freshness TTL and a state check are both unusable here - an idle-but-live agent sits at done and still needs bracketing. Refs STA-4294 * fix(terminal): key agent-paste bracketing on live evidence, not shellForeground Measured against a real pane: shellForeground is republished only at OSC 133 boundaries, so a shell without 133 integration leaves it latched true while an agent owns the foreground. Vetoing on it silently reinstated the submit bug the parent commit fixes - the parked draft was sent on paste with the gate in place. Prefer process-confirmed agent identity when present, keep the restart-rehydrated veto, and drop the shellForeground veto. Erring toward bracketing costs a literal ESC[200~ in a non-2004 program; erring the other way sends the user's draft. Refs STA-4294 * docs(terminal): record why paste bracketing keys on the agent, not the mode bit Measured in real ptys before taking the obvious alternative. A tri-state on DECSET 2004 (observed-on / observed-off / never-observed) does not work: zsh 5.9, fish 4.8.1 and bash >= 5.1 announce and withdraw the mode cleanly, but macOS /bin/bash 3.2, /bin/sh, bash 4.4 and any shell with bracketed paste disabled emit nothing at all, byte-identical to a bare `cat`. Silence cannot be read as consent. Agent identity disambiguates it in the one direction that matters: agents always enable the mode, so silence on an agent pane means the announcement was lost in transit, never an opt-out. Also measured: bracketing a program that never negotiated is worse than useless - the markers land as literal payload bytes and ICRNL still turns the CR into a submit - so the gate stays narrow. Refs STA-4294 * fix(terminal): guard the paste pane key and document the evidence policy Readiness review follow-ups, none behaviour-changing for reachable inputs. makePaneKey throws on a malformed leaf/tab id. It is unreachable today (pane.leafId is a minted UUID and the same pair is already called unguarded from a hotter site), but the failure mode was bad: the throw escapes before the paste helper's catch is attached, so the paste would be a silent no-op with no error surface. Degrade to the pre-fix path instead, with a test. Also record two things a future reader needs: the process-confirmed branch is dead for remote-runtime and SSH panes because foreground tracking is disabled there, so a remote pane's status row is its only evidence; and why this resolver deliberately omits the shellForeground/routingRevoked/routingTrusted gates its two siblings enforce - they route input bytes, this only wraps a paste whose payload is ESC-sanitized downstream. Refs STA-4294 * fix(terminal): encode Windows agent paste newlines as input records * fix protected paste handling in dashboard previews |
||
|
|
0e8d52912c |
fix(source-control-ai): stop duplicating singleton CLI flags in agent argv (#14585)
* fix(source-control-ai): stop duplicating singleton CLI flags in agent argv Recipe CLI arguments and agent command overrides were appended on top of Orca's generated flags, so a user-supplied --model produced a repeated flag. yargs collapses a repeated flag into an array, crashing OpenCode with "j.split is not a function"; clap rejects it outright for Codex. Declare the at-most-once option groups per agent spec and fold every user-supplied occurrence into the generated slot, with recipe args outranking a command-override prefix. Fixes #12305 * fix(source-control-ai): preserve command override argv order |
||
|
|
500b72d8ef |
fix(vm): harden provisioned root ownership and cleanup (#14477)
* fix(vm): verify provisioned root ownership * test(vm): retry transient removal menu * test(vm): stabilize provisioned root teardown * fix(vm): clarify recipe-owned cleanup * fix(vm): pin provisioned root source commit * fix(vm): make runtime cleanup user-cancellable |
||
|
|
32f46f9a24 | fix(vm): keep failed cleanup retryable (#14476) | ||
|
|
8b22f044f5 |
fix(vm): preserve runtime sidecar rollback compatibility (#14444)
* test(vm): reproduce runtime store rollback poisoning * fix(vm): keep runtime sidecar rollback-readable * fix(vm): harden rollback-compatible runtime persistence * fix(vm): publish rollback lifecycle authority first * test(vm): harden rollback compatibility coverage |
||
|
|
3a212584ec |
feat(native-chat): Extra high grok effort per model (#14577)
* feat(native-chat): offer Extra high grok effort per model Slice grok's reasoning-effort menu by each model's advertised ceiling so 4.6 can reach xhigh while 4.5 stays at high, and keep the untouched default at high so launch argv does not silently escalate. * fix(grok): parse dashed rows in grok models listing Grok stars only the default model and dashes the rest. A star-only bullet dropped 4.5 from the picker once discovered models were authoritative. |
||
|
|
9bb8836bb6 |
fix(agent-launch): wait longer for cold-boot Codex composer before dropping prompt (STA-3367) (#12853)
* fix(agent-launch): wait longer for cold-boot Codex composer before dropping prompt (STA-3367)
Continue-in-new-session pastes the handoff prompt once Codex renders its
composer glyph, gated on an 8s readiness budget. A cold/first-run Codex can
take longer than 8s to mount its composer, so the wait timed out and the
prompt was silently dropped into an empty terminal.
Marker-gated ready signals (Codex glyph, opencode show-cursor) are positive
proofs: the paste fires only when the marker actually renders, so a longer
budget can never paste prematurely — it only tolerates slow cold boots. Give
those signals a 20s budget while the markerless quiet-window signal keeps 8s.
* fix(agent-launch): share the composer-readiness budget across all three delivery owners (STA-3367)
The cold-boot fix was correct but landed as a single-path exception, and it
double-spent its own budget. Three follow-ups so the behavior is a system rule:
1. Split the PTY-spawn wait from the composer wait in pasteDraftWhenAgentReady.
Both were handed the same budget, so a codex tab took up to 41s to report a
dropped prompt. "Tab has a PTY" and "composer accepts input" are separate
states: spawn keeps a fixed 8s, and the readiness budget now starts once the
PTY exists, so a slow spawn can't shorten a cold composer's window.
2. Move the per-signal budget to draftPasteReadyBudgetMs() beside the shared
readiness scanner. The budget is a property of the ready signal — only that
module knows which signals are marker-gated — so all three delivery owners
(renderer tab paste, renderer startup paste, main runtime startup paste)
consume one policy instead of three hardcoded 8s constants.
3. Give the main-runtime startup paste the process-ownership fallback both
renderer paths already have. It resolved null on budget expiry, silently
dropping the prompt on worktree-create / CLI / remote-host delivery — the
same STA-3367 failure, on the path the original fix didn't reach.
Adds coverage for the main-runtime waiter, which had none.
Test: vitest src/main/runtime src/shared src/renderer/src/lib
src/renderer/src/components/terminal-pane — all green; tsc clean.
* test(agent-launch): consume the shared readiness budget instead of restating it
Hardcoding 20000 in the runtime waiter test meant it would keep passing if
OrcaRuntimeService stopped consuming draftPasteReadyBudgetMs — the exact drift
this PR exists to prevent. The literal values stay pinned once, in the scanner
test.
* refactor(agent-launch): collapse the readiness budget to one flat timeout
The per-signal budget (marker 20s / quiet-window 8s) tied the timeout to how
readiness is DETECTED. The budget is really a property of how slowly an agent
can boot — a marker, a quiet window, and a process check all wait out the same
cold start — so one number covers all three signals.
Replaces draftPasteReadyBudgetMs() with DRAFT_PASTE_READY_TIMEOUT_MS: drops a
constant, a branch, and two tests, and removes the only reason a delivery path
needed to know which signal class it was using.
Cost: a launch that never emits DECSET 2004 now surfaces its 'prompt not sent'
toast at 20s instead of 8s. That is the failed-launch path only; successful
markerless delivery still resolves on the 1.5s quiet window as before.
* fix(agent-launch): constrain cold Codex readiness budget
* fix(agent-launch): observe Codex readiness from PTY bind
* fix(agent-launch): anchor early Codex prompt to TUI screen
|
||
|
|
83e2123582 |
Add global worktree visibility source defaults (#14276)
* Add global external worktree visibility defaults * Expand global worktree visibility source defaults * Fix host-scoped visibility settings races * Fix global worktree visibility integration * Enable source visibility defaults on mobile * Polish external worktree settings navigation * Clarify inherited worktree visibility settings * feat(sidebar): replace the inherited-visibility switch with a Show/Hide picker Each source row now shows a two-segment Show / Hide control preselected to the global setting, and explains itself only where the project actually disagrees: an "Overriding global setting: <value>" card names the value being ignored. Picking the segment global already holds drops the override instead of pinning a duplicate, so the same control both overrides and reverts, retiring the separate "Use global" link. The dialog footer now lists every inheritable source with its global value. * fix(sidebar): preserve reset for matching visibility overrides |
||
|
|
266b5ae8f5 |
fix(mobile): match desktop project and run target picker (#14457)
* fix(mobile): disambiguate repository locations * fix(mobile): preserve explicit repository ownership * test(mobile): use explicit renderer type * refactor(mobile): match desktop project targets |
||
|
|
87bafb5e6a |
fix(terminal): ground the emulator to the snapshot's baseline after a byte gap (#14374)
A hidden-delivery byte gap can strand more than the SGR pen, and the reset #14241 added to the split alt-screen replay is undone before any content is painted: xterm answers `?1049l` with restoreCursor(), which reloads the pen, all four G-set designations, GL, origin mode and wraparound from the register saved at `?1049h`. - Bracket the buffer switch with the baseline: before, so `?1049h` banks grounded state rather than the gap's; after, so `?1049l`'s restore cannot reapply it. - Ground everything a serialized payload is diffed against, not just the pen: SGR, GL, all four G-sets, origin, autowrap, insert, the per-buffer scroll region, and the saved-cursor register. - Switch buffers only when the pane is actually on the other one. `?1049` is not a no-op otherwise — it still swaps the kitty flag registers, which would park the flags of an agent that negotiated them on the normal screen. - Return to the normal buffer when the gap ate the TUI's exit sequence; the restored history was painting into the alt buffer with scrollback left empty. - Restore the CAN #14241 dropped, so a control string the gap truncated is discarded instead of committed by the next ESC. - Ground the abandon path exactly once instead of twice. - Derive the parity/fuzz preambles from the same builder; they had drifted and were asserting against bytes production no longer emits. |
||
|
|
92b6ffd17d |
Terminate renderer graph reload generations and contain disposed-frame notifications (#14070)
* fix(runtime): terminate renderer graph reload generations * fix(runtime): harden renderer reload teardown * fix(runtime): fence renderer graph publication ownership * test(runtime): register renderer graph reload gate * test(runtime): record live reload validation * fix(runtime): ignore cancelled renderer navigations * chore: preserve main formatting during branch sync * chore: satisfy changed-code quality gate * fix(runtime): restore cancelled renderer reloads * fix(runtime): preserve committed reload fencing * test(runtime): prove cancelled reload timeout * docs(reliability): record reload cancellation oracle --------- Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> |
||
|
|
b82a8791f5 |
perf(runtime): resolve an explicit worktree id without scanning every repo (#14399)
`resolveWorktreeSelector` resolved every selector kind from the whole-fleet snapshot, so a targeted `id:<repoId>::<path>` lookup fanned `git worktree list` across every registered repo to answer a question about one of them. With a cold scan cache -- app startup, or the first lookup after a mutation clears the snapshot -- that is one subprocess per repo, ~17ms each, to find a worktree whose owning repo the id already names. Measured on a ten-repo fleet: one `id:` lookup scans 10 repos before and 1 after. Scope only `id:`. Every other selector kind is matched across the fleet and its `selector_ambiguous` contract is defined over all repos, so scoping `branch:`, `name:`, `issue:`, or a bare selector would silently pick a winner where they correctly refuse today. A test pins that: `branch:main` across ten repos still throws `selector_ambiguous` and still scans all ten. Lineage stays correct because edges are intra-repo by construction. The scoped path returns null and falls back whenever that does not hold: a repo id registered on several execution hosts, an unknown repo id, or a worktree the scoped scan does not contain. A warm fleet snapshot always wins. Row resolution moves out of orca-runtime.ts into repo-worktree-row-resolution.ts, which owns no state -- the cache-aware scan and folder-workspace stamping are injected. orca-runtime.ts ends up 65 lines shorter than before despite the added feature. |
||
|
|
2100fb2553 |
fix(runtime): cap remote git.diff and file previews at the transport budget (#14160)
* fix(runtime): cap remote git.diff and file previews at the transport budget A remote or mobile user who opens the diff of a large image loses their whole WebSocket, not just that request: the E2EE channel closes with 1013 when a reply exceeds the 4 MiB outbound envelope. Two producers can exceed it unaided. git.diff/branchDiff/commitDiff cap text with MAX_RENDERED_DIFF_COMBINED_CHARACTERS (6M chars) -- a *renderer* budget that sits above the transport limit -- and return base64 for previewable binaries bounded only by MAX_GIT_SHOW_BYTES, so a 10 MiB PNG changed in place is ~26.7 MiB in one envelope. files.readPreview inlines base64 up to 10 MiB, and mobile calls it for every image tab. Both now measure against a budget derived from the outbound limit. The check sits in orca-runtime-git.ts, downstream of the dedupe and of both the SSH-provider and local branches, so a payload forwarded verbatim by an old relay is covered by the same code and src/relay needs no change. Local and in-process callers pass no budget and keep full fidelity. Measuring raw bytes would not work, which is the whole reason this needs a module. JSON escaping turns one control byte into six (\u00XX), and binary-buffer.ts sniffs only for NUL in the first 8 KiB -- so a NUL-free file of 0x01-0x1f bytes is classified as *text*, would pass a raw-byte cap, and would then blow the envelope. The budget is escape-aware, with a three-branch fast path that keeps normal diffs at two native byteLength calls and scans only the ambiguous band. The SSH branch of readFileExplorerPreview had the same raw-vs-escaped gap: its stat gate sizes base64 binaries, but text crossed unbounded. It now honours the same decoded-text limit the local branch already enforced. No wire change: GitDiffResult is untouched -- no third kind, no new field. Old clients see an error for one request instead of a dropped connection. diff_too_large joins the structured passthrough codes and lands on an existing error arm in both mobile consumers and the desktop remote path; file_too_large was already handled on both. Instruments the 1013 close, which nothing measured before, so the incidence this cap is meant to drive to zero is finally observable. `emitter` separates a producer size bug from a wedged link. Known regression: remote image previews between ~3.096 and ~3.146 MB now return file_too_large. They only intermittently worked before -- above ~3.0 MB they killed the socket -- so this trades intermittent connection loss for a consistent error. Test: 10281 passed in src/main/runtime + src/shared + src/main/git; mobile 3427 passed. Each of the six budget-enforcement sites is independently mutation-killed. Escaping fixtures cover newline-dense, control-char, CJK, lone-surrogate and base64 content against native JSON.stringify. tsc clean for node, web and cli; oxlint clean. Co-authored-by: Orca <help@stably.ai> * fix(runtime): harden remote reply transport budgets * test(runtime): cover desktop remote preview budgets * test(runtime): close telemetry review gaps * chore(shared): repoint budget imports after the shared/types barrel removal Upstream #14447 dropped the shared/types barrel; GitDiffResult now lives in git-diff-compare-types and GlobalSettings in global-settings-types. Co-authored-by: Orca <help@stably.ai> * fix(ssh): surface an over-cap preview read as file_too_large The stream reader aborts an over-cap read with StreamProtocolError, whose numeric code falls through mapRuntimeError to a generic runtime_error carrying the raw "Reported totalSize N exceeds client cap M" string. Neither preview client recognizes that: runtime-file-client.ts and mobile-file-preview-response.ts both key on file_too_large. It also made the two file_too_large guards directly below the read unreachable on the streaming path. Gives the cap its own error type so the caller can translate it, keeping the bandwidth saving the cap exists for. A genuine protocol fault still propagates unmasked. Found by the readiness review. Mutation-verified: removing the translation fails exactly the new test. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
d137bb93e1 |
fix(agent-status): stop start-less child stops from minting phantom working (#14375)
* fix(agent-status): stop start-less child stops from minting phantom working buildClaudeCachedLeadStatusPayload fell back to 'working' whenever the pane had no cached lead-turn state. That default is right for a spawn or a child tool call, but the same helper serves SubagentStop and TeammateIdle, which end work and prove the opposite. claudeLeadStateByPaneKey is in-memory only, so every app restart empties it. A Claude session that outlives the restart reports its next child event into an empty map and the pane latches 'working' with an empty roster -- no Stop ever clears it, and the 30-minute window only decays the sidebar dot, never the stored state. Fall back by the event's evidence: terminating child events resolve to 'done', which still gates up through resolveClaudePaneState when the roster or background work proves the pane is busy. * fix(agent-status): require evidence for child completion * fix(agent-status): publish matched teammate idle * fix(agent-status): preserve confirmed child work * fix(agent-status): retain live restored teammates * fix(agent-status): reap unconfirmed siblings after child drain * fix(agent-status): preserve unmatched restored children * fix(agent-status): wait for lead completion after child stop * fix(agent-status): persist restored child transitions --------- Co-authored-by: Brennan Benson <brennan@stably.ai> |
||
|
|
9e5ee5ef8e |
feat(workspaces): rework cleanup discovery and dialog (#13413)
* fix(workspaces): support full cleanup scans * feat(workspaces): persist cleanup snapshots * feat(workspaces): add cleanup filter model * refactor(workspaces): remove cleanup presets * feat(workspaces): rework cleanup dialog * fix(workspaces): keep cleanup row ordering render-pure * refactor(workspaces): simplify cleanup browsing * refactor(workspaces): show cleanup facts * refactor(workspaces): surface cleanup row facts * fix(workspaces): remove misleading cleanup count * fix(workspaces): preserve full scan semantics * fix(workspaces): scope snapshot persistence * fix(workspaces): preserve cleanup browse compatibility * fix(workspaces): reconcile cleanup dialog state * test(workspaces): update snapshot store fixtures * test(workspaces): preserve cleanup scan modes * perf(workspace-cleanup): stream scan progress and size results * fix(workspace-cleanup): address review feedback * fix(workspace-cleanup): preserve host-scoped cleanup metadata * fix(workspace-cleanup): declare review source dependencies * fix(workspace-cleanup): align size scan banner * fix(workspace-cleanup): shorten scan action * perf(workspace-cleanup): avoid redundant scan IO * perf(workspace-cleanup): bound restarted evidence scans * fix(workspace-cleanup): satisfy scan queue lint * perf(workspace-cleanup): bound scan and snapshot work * perf(workspace-cleanup): serialize final enrichment * test(workspace-cleanup): assert final enrichment drain * fix(workspace-cleanup): stop progress after renderer teardown * perf: batch workspace cleanup git evidence scans * perf(workspace-cleanup): stop redundant snapshot and scan work * fix(workspace-cleanup): resolve review findings across scan, store, and dialog Correctness: - Chunk git-evidence dispatches at the shared 500-target limit and exclude queued/in-flight ids from target selection, so fleets past the limit can no longer strand rows permanently mislabeled as checked-but-unknown. - Key destructive selection pruning on the user's filter state instead of the per-tick matched-set identity; streaming reclassification no longer silently deselects rows. - Clamp the facet clock to max(scannedAt, open time): a stale hydrated snapshot no longer misbuckets idle thresholds or keeps dead agents fresh; row labels use the same clock. - Supersede and cancel the previous broad scan when a new one starts (renderer registry and same-sender guard in main) instead of racing two fleet scans. - Gate snapshot persistence on hasTargetedWorkspaceCleanupScan so worktreeIds: [] can never persist an empty fleet snapshot. - Re-apply dismissals at set-time in progress application so a dismissal landing mid-enrichment is not clobbered. - Record a one-off local snapshot prune for single (unbatched) remote deletes so removed workspaces cannot resurrect from cache. - Strip .exe when normalizing foreground process names so Windows agent processes match. Performance: - Cache per-candidate facet and review-info objects on candidate identity; no-op streaming ticks reuse the previous rows array and skip every downstream pass; matched-set identity is stable under equal membership. - Compute facet counts/options only while the filter popover is open. - Equality-bail git-evidence publishes; structural (non-stringify) facet-group comparison memoized in the toolbar. - Identity-token fast path for the enrichment cache (cache hits skip both JSON.stringify signatures); prune viewed/dismissal records on removal and expiry; bound the superseded-scan-id set. - Restore the no-op bail in removeWorkspaceSpaceWorktrees (regression). - Abort main-side scans when the renderer is destroyed; module-scope controller maps survive handler re-registration. - Batch removal preflight into one targeted scan (with refreshActivity) per 500 ids instead of one scan per row. - Scan repos at concurrency 2, report discovered counts upfront for honest progress, share fs-activity probes per path (folder workspaces), read only the reflog tail, and skip the snapshot read-before-write via a remembered scannedAt. Split workspace-cleanup-worktree-listing, workspace-cleanup-facet-row-caches, and workspace-cleanup-selection-model out of files that crossed max-lines. * fix(workspace-cleanup): address verifier findings - Fall back to a full reflog read when the newest record exceeds the 8KB tail window, so an oversized subject cannot hide recent ref activity. - Bound the single-removal snapshot prune batch id with a UUID; embedding the unbounded worktreeId silently failed main's 128-char validation and skipped the prune for long remote ids. - Key the main-side broad-scan supersession by sender AND scan mode so legacy suggestion-only and full-workspace scans stay isolated, matching the renderer registry. * fix(workspace-cleanup): own facet caches with useMemo instead of render-time ref writes React Doctor (CI changed-lines gate) correctly flagged the three cache refs written during render. Each per-candidate cache now lives in one memo with the derived context it is keyed on, so the memo deps are the invalidation and interior fills stay content-addressed; the matched-set identity stabilization is dropped since its only consumer reads through a useEffectEvent and never keys on identity. |
||
|
|
77f23b013f |
refactor(shared): drop the shared/types barrel and import from the real modules (#14447)
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as a re-export barrel so the import sites did not have to change. This removes the barrel: every consumer now imports from the module that actually declares the type, and `src/shared/types.ts` is deleted. Barrels hide where a type lives, make every consumer look like it depends on the whole domain, and let an unrelated edit invalidate a module that ~2,000 files transitively import. 2,323 import declarations across 2,321 files. Rewritten mechanically: each specifier was resolved to an absolute path via the TypeScript AST and recomputed, rather than string-substituted, so alias forms (`@/../../shared/ types`) and per-specifier `type` modifiers survive. Four cases the mechanical pass had to handle, each found by a gate rather than by reading the diff: - Modules inside `src/shared` import the barrel as `./types`, not `shared/types`. A pre-filter on the latter string skipped 176 of them and left imports dangling at a deleted file, which surfaced as confusing `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>` errors rather than "module not found". - The barrel RENAMED one type on the way through (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name in the owning module has to be re-aliased at each consumer. - Three test files put `;(globalThis as ...)` on the line after the import. TypeScript parses that `;` as the import statement's terminator, so replacing through `statement.getEnd()` deletes it and breaks ASI. The rewrite now stops at the module specifier. - A file that already imported directly from a module got a SECOND import from it, because the barrel re-exported those same names — which trips `import/no-duplicates` under `--deny-warnings`. A post-pass merges declarations sharing a specifier and type-only-ness; the `import type` plus `import` pair from one module is left alone, since that form is allowed. Splitting one barrel import into several genuinely adds lines, which pushed `terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character import must wrap, and neither local type collapses onto one line (101 and 116 characters). Rather than contort a type declaration to fit a line budget, `collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` — they are pure structural operations on the layout tree and independent of PTY ownership. `visible-worktrees.ts` similarly loses its own mini-barrel re-export of `isDefaultBranchWorkspace`, with the four real consumers repointed at the declaring module. No `max-lines` bypass added. Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted first — these projects are `composite: true` and reuse stale caches); the full `pnpm lint` green, not just bare oxlint — the narrower local check is what let the duplicate imports reach CI; max-lines ratchet OK at 344. |
||
|
|
cd6114ab7e | fix(browser): acknowledge paired tab before navigation (#14402) | ||
|
|
5a6837fce4 |
refactor(store): unify the duplicated catalog equality and identity-key helpers (#13804)
* refactor(store): unify the catalog structural-equality walks Three near-identical structural deep-equality walks had landed independently in the same window: areValuesEqual (#13744, repo-identity-reconcile.ts), areCatalogEntriesEqual (#13770, repos.ts — already folded into the first on this branch's base) and catalogValuesEqual (#13662, worktree-catalog-reconciliation.ts). All three walk plain records and arrays and fall back to reference equality for anything exotic. They are not interchangeable. Two axes genuinely differ, and each caller depends on its own side: - Own-key set. #13744/#13770 require equal own-key counts plus hasOwnProperty, so an absent key differs from a key present and holding `undefined`. #13662 compares the union of both sides' keys, so those are equal. The strict side is load-bearing: the repo/project merges branch on `'localWindowsRuntimePreference' in project` (repos-project-runtime.test.ts "clears stale local runtime preferences"), and projects are now reconciled with this comparator. The loose side is test-pinned by worktree-catalog-reconciliation.test.ts "reuses rows with equivalent nested catalog data", where a locally built row carries `optional: undefined` that the host omits. - Leaf comparison. #13744/#13770 use `===` (NaN never equal, 0 equals -0); #13662 uses `Object.is` (the reverse). So instead of picking a winner, src/shared/structural-value-equality.ts holds one walk parameterised by those two axes and exports the two policies as `structuralValuesEqual` and `structuralValuesEqualIgnoringUndefined`. Every caller keeps its exact current semantics; the ~40 duplicated lines and the silent divergence go away. src/shared/persisted-ui-equality.ts (a fourth copy with a Set branch and no plain-object guard) is deliberately left alone: it gates a disk write in main with no direct test coverage. Also folded, all provably behaviour-identical: - The `${hostId}\0${repoId}` composite key had three copies (getRepoHostIdentityForParts, repoOwnerKey, getEntryKey) that must agree or repos silently stop reconciling. Moved to src/shared/repo-host-identity.ts because one of them lives in src/shared; the renderer module re-exports it. - mergeFetchedReposForHost's hand-inlined upsert loop now calls mergeByIdentity. mergeByIdentity additionally skips replacing a structurally equal row, which cannot change the result here: reconcileFetchedRepos runs immediately after over the same identities in the same order and restores exactly those rows. - Renamed repos.ts's `catalogRowsUnchanged` to `arrayElementsUnchanged`. It is a pure element-identity compare, two files away from `catalogRowsEqual`, which is a full structural compare. src/shared/structural-value-equality.test.ts pins both policies over arrays, nested records, null-prototype records, absent-vs-undefined keys, symbol keys, and non-plain objects (Date/Map/Set/class) falling back to reference equality. * fix(store): keep merged sourceRepoIds order host-independent Prefixing the cross-host remainder made a cross-host project's sourceRepoIds order a function of the refreshing host, so the projects reconcile never reused the row. Also pins the repo-derived host-id contribution the new per-project slice feeds the host-id resolvers. Co-authored-by: Orca <help@stably.ai> * refactor(store): migrate call sites that landed after this branch github.ts and ai-vault-session-identity.ts began using areValuesEqual on main while this branch was stale, and repo-identity-reconcile's record reconciler still called its own deleted walker. All three now use structuralValuesEqual; reuseEqualCatalogRows keeps its duplicate-id cap and calls the ignoring-undefined variant, which is the key-union semantics catalogValuesEqual had. --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
583ab1601b |
refactor(shared): group worktree, github, and linear modules into folders (#14437)
`src/shared` is a flat directory of ~1,150 entries. The worktree, github, and
linear domains accounted for 71 of them, so finding the module you wanted meant
scanning a wall of same-prefixed filenames.
Move each domain into its own folder and drop the now-redundant prefix:
src/shared/github-pr-types.ts -> src/shared/github/pull-request-types.ts
src/shared/worktree-id.ts -> src/shared/worktree/id.ts
src/shared/linear-links.ts -> src/shared/linear/links.ts
This follows the existing `network/` and `new-workspace/` convention in the
same directory, which also drop the prefix inside the folder.
Whole clusters move, including tests. Foldering only part of a domain would be
worse than flat: a reader would have to check both `github/` and the flat
directory, and `github-auth-types.ts` / `github-project-types.ts` are type
modules that belong with the rest. No files with these prefixes remain flat.
Import specifiers were rewritten by resolving each one to an absolute path and
recomputing it, not by string substitution, so the `@/../../shared/...` alias
forms are handled correctly. 501 specifiers across 298 files.
Two things `tsc` cannot catch, handled explicitly:
- `github-project-types.ts` carries its own `max-lines` bypass, so its baseline
entry is REPOINTED to the new path rather than pruned. Pruning would drop the
bypass and then flag the new path as a fresh violation. Ratchet stays at 345.
- `mobile/` is outside `pnpm typecheck` and cannot be typechecked here
(`mobile/node_modules` is empty). Instead every relative specifier in the repo
was resolved against the filesystem: 174 unresolved before this change and 174
after — identical, so nothing broke in mobile either.
The pinned `tests/e2e/.cross-version-checkouts` fixtures are deliberately NOT
rewritten; they are a snapshot of an older release and still reference the old
paths.
Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches).
|
||
|
|
070572bfd7 |
refactor(shared): split shared/types.ts into per-domain type modules (#14397)
`src/shared/types.ts` was 3,981 raw lines (2,825 counted, 9.4x the 300-line budget) behind an `eslint-disable max-lines`, and is imported by 2,092 files — the single widest contract surface in the repo. Move all 320 top-level declarations into 46 per-domain modules (`repo-types.ts`, `worktree-types.ts`, `github-pr-types.ts`, ...) and reduce `types.ts` to an explicit re-export barrel, so the 2,092 import sites are untouched. `HostSettingOverrides` moves into the pre-existing `host-setting-overrides.ts` alongside the accessors that operate on it, which also removes that module's circular import back into `types.ts`. Named re-exports only, never `export type *`: with star re-exports a name exported by two modules is silently dropped, which would surface as a confusing "has no exported member" at a random call site. Verified lossless mechanically, not by inspection: - export parity — the module's resolved export set through the TS checker is identical before and after (396 names, no additions, no removals) - declaration parity — all 320 declarations compare character-identical modulo comments and whitespace, so no optionality, union order, or generic parameter drifted - `tsc --noEmit` green on the node, cli, and web projects - `oxfmt --write` is byte-identical, so the barrel is format-stable Drops the `max-lines` bypass and its baseline entry (ratchet 346 -> 345). |
||
|
|
400321edcd |
fix(workspaces): gate the GitHub palette number match on repo remote identity (#14413)
* fix(workspaces): gate the GitHub palette number match on repo identity `repoMatchesGitHubSlug` returned the permissive `'unknown'` whenever the repo displayName was not in `owner/repo` form and no upstream metadata existed — the common basename-named non-fork case. The caller only rejects on `false`, so a pasted issue/PR URL could activate a workspace in a different repo that happened to share the number, since issue/PR numbers are per-repo. Mirror the GitLab gate from #14381: fall back to the probed `gitRemoteIdentity.canonicalKey` before giving up, comparing host and owner/repo after normalizing port, `www.`, and case. An `upstream`-derived identity stays `'unknown'` because `deriveGitRemoteIdentity` ranks `upstream` above `origin`, so a fork's own origin is invisible and rejecting would drop URLs from the fork the user actually checked out. The canonicalKey compare runs after the displayName branch: displayName is compared host-agnostically, so mirrors and host aliases of the same owner/repo keep matching as they do today, and the probed remote only fills in where no name evidence exists. Refs STA-4237 * fix(workspaces): keep SSH host aliases matching in the palette identity gate `git remote -v` reports ssh.github.com, www., and ~/.ssh/config `Host` aliases verbatim, so comparing a probed canonicalKey against a pasted URL host rejected legitimate GitHub/GitLab remotes. Normalize the alias hosts both sides can fold offline, and downgrade a host-only mismatch to 'unknown' when the probed host is dotless (an unexpandable OpenSSH alias); dotted hosts like ghe.example.com still lose. Lifts the GitHub host normalizer into shared instead of a third copy. * fix(repos): keep the www host fold out of the derived project identity getProjectIdentityKey feeds the persisted Project id, so folding www. there re-keyed existing projects on upgrade and dropped localWindowsRuntimePreference. Restrict the fold to the palette's URL-vs-remote comparison, and pin the derived id for a www. remote so it cannot drift silently again. |
||
|
|
17690d49ea |
fix(repos): re-probe git remote identity so a stale snapshot stops misjudging identity gates (#14414)
* fix(repo-identity): re-probe resolved git remote identities on a long TTL A resolved gitRemoteIdentity was written once and frozen for the life of the repo record, so adding an `upstream` remote later — or a project rename or transfer — left identity gates judging against the path the repo had when it was added. Re-probe resolved repos on a 6h TTL, seeded 5 minutes after a repo is first seen in a process and capped at 4 refreshes per sweep so a restart cannot fan out a subprocess per repo. Only a successful probe that yields a different canonicalKey overwrites; failures and no-remote answers leave the existing identity alone. Also explain why the worktree-scan admin fingerprint timeout deliberately exceeds its caller budget, and log when that probe expires — expiry was silent and indistinguishable from "fingerprint unavailable". Refs STA-4247 * fix(projects): carry project state across derived project id changes A project id is derived from repo identity, so a remote re-probe (or a repo:->git:->github: promotion) rewrites it. The compatibility merge matched prior rows by id only, dropping the user's localWindowsRuntimePreference and leaving a ghost project row that independent host setups still pointed at. Both merge sites now fall back to the prior row whose sourceRepoIds overlap and re-point independent setups at the surviving project. |
||
|
|
eb22e497bb | Revert "fix(ssh): reapply the reattach-identity work and stop the fallback fence stranding moved panes" (#14395) | ||
|
|
d16092e503 |
fix(updater): recover renderer shutdown checkpoint (#14373)
* fix(updater): recover renderer shutdown checkpoint * test(updater): cover checkpoint recovery in Electron * fix(updater): keep staging failures blocking |
||
|
|
537864a248 |
Fix Codex hook trust before manual shell launches (#14326)
* fix codex hook trust before shell launch * fix packaged cli preflight dependency * fix codex shell preflight safety * fix Codex shell preflight settings and startup safety |
||
|
|
6a0c8fa541 |
fix(ssh): reapply the reattach-identity work and stop the fallback fence stranding moved panes (#14384)
* Reapply #13326 and #13928 (un-revert #14361) Restores the SSH reattach-identity and daemon-occupancy fixes. Reverting them reintroduced their P0s, filed as STA-4224, STA-4225, STA-4227, STA-4230, STA-4232, STA-4233 and STA-4234 against #14361. The tab loss that motivated the revert is fixed in the commits that follow, so this reapplication is not a straight redo. * fix(relay): stop the fallback attach fence refusing a pane that moved tabs The primary fence was moved to the shell's own incarnation precisely because paneKey/tabId froze the pane's LOCATION at spawn and refused panes that had merely moved. The fallback that older clients fall into kept the old rule, so the correction never reached it — the same 'the rule exists, but this path does not ask it' leak this work has hit repeatedly. A refusal here is not recoverable: an identity mismatch never grounds a respawn, so the pane keeps a live shell it can no longer reach and renders blank. Narrowed to paneKey, which is the identity; the tab is a location. Restoring the tabId comparison reddens the new test. |
||
|
|
f9f55075f6 | fix(vm): adopt provisioned SSH checkout roots (#14353) | ||
|
|
99d19d4635 | feat(vm): add provisioned root recipe contract (#14352) | ||
|
|
953cfab635 |
fix(terminal): make the bold font weight its own setting (#14368)
* fix(terminal): make the bold font weight its own setting
Deriving bold as max(700, regular + 200) silently destroyed bold. A family
exposes only a few real faces: the monospace the default chain resolves to on
macOS has exactly two, splitting at 600. Measured by rasterizing each weight to
a canvas — 100-500 are byte-identical (ink 3023) and 600-900 are byte-identical
(ink 3855), at every weight the same advance. So any base weight at or above 600
put both values in the same face and bold stopped existing, on 4 of the 9
positions the slider offers, with no error and nothing the user could do.
Arithmetic cannot fix it — on a two-face family there is no heavier face to
escape to. So bold is now user-owned: a new terminalFontWeightBold setting with
its own control, defaulting to 700. The default pair (500/700) straddles the
boundary, so existing profiles render exactly as before; a collision is now a
choice the user can see and undo.
The old test asserted 800 -> {800, 900} as 'keeps bold heavier', which is where
this hid: numerically heavier, identically rendered.
* fix(terminal): surface bold face collisions accurately
|