mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 01:57:57 +00:00
99788a91fa47cecb0e59e8535d40430283dc71d4
370
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0ed0caa663 |
fix(wsl): run wsl.exe probes from the system folder, never the profile (#29853)
* fix(wsl): run wsl.exe probes from the system root, never the profile WSL holds wsl.exe's working directory busy for about its idle timeout after the probe exits. Since #29209 the orcad startup probe ran in the profile, so a temporary orca serve profile could not be deleted for about a minute (diagnosed in #29845). * test(orcad): drop the WSL profile-hold wait from serve parity Reverts #29845's extended EBUSY retry: wsl.exe no longer runs in the profile, so nothing outside the test's process tree holds it. * build(cli): list the WSL spawn-directory module in the CLI typecheck project |
||
|
|
23b60a5d07 |
fix(worktree): offer Force Delete with a plain warning when a checkout has submodules (#28878)
* fix(worktree): remove a clean checkout that contains submodules Git refuses to remove any worktree with an initialized submodule unless --force is set, even when nothing is uncommitted. After that specific refusal, re-check the checkout including files inside the submodule and pass --force once. A dirty submodule still blocks the delete. A second --force is not added, because that would also clear a Git lock. * fix(worktree): offer Force Delete for a checkout Git refuses over submodules Drop the automatic --force retry. A forced removal deletes the submodule's repository under .git/worktrees/<id>/modules, including commits, local branches and stashes that were never pushed, and the parent's dirty check cannot see them (the retry #24602 removed for the same reason). Instead classify Git's "working trees containing submodules cannot be moved or removed" refusal as a new Force Delete reason, 'submodules', after the lock check and the force guard. The renderer classifies the error string, so older SSH and paired hosts get it too with no wire change. The toast, the delete dialog and the Space manager row show one plain warning naming what Force Delete would lose instead of Git's text. Keep --ignore-submodules=none in the pre-teardown dirty check so a user's ignore setting cannot hide submodule edits from it. Replace the retry test with one real-Git test: a clean checkout whose submodule holds a local-only commit is refused without --force, stays registered, keeps the commit, and its refusal classifies as 'submodules'. * fix(worktree): clarify the submodule Force Delete warning and hide View for it The warning now says Git deletes the workspace only with Force Delete, the submodule refusal no longer offers View (there are no changes to show), the real-Git test pins that Force Delete then succeeds, and a comment no longer describes the removed retry. * fix(worktree): name submodules when a changed submodule blocks the delete, and clear a stale delete toast With a commit inside a submodule that the parent has not recorded, the parent sees `M vendor`, so Orca's dirty check refused first with the generic "It has changed files". Force Delete then destroyed the never-pushed submodule commit without warning. On a dirty refusal the check now asks `git status --porcelain=v2` whether a changed entry is a submodule, and if so reports a refusal the app shows with the submodule Force Delete warning. The detail keeps the dirty sentence, so older apps still offer Force Delete. After the Space manager deleted a workspace (its row Force, or a later delete), the failure toast for that workspace stayed up and still offered Force Delete. A Space delete now dismisses it. * fix(worktree): warn about submodules on any refused delete of a checkout that has them Review found the warning still vanished when a submodule commit was already recorded and some other file was dirty: the parent's status shows no submodule change, yet Force Delete destroys the commit. The changed-files check now asks whether the checkout has a populated submodule (a .gitmodules path holding a .git, as Git's own refusal checks) instead of whether one changed. The refusal keeps the changed paths after the detail, and View stays available since there are changes to view. * test(worktree): mock the .gitmodules probe as missing in the preflight unit tests * refactor(worktree): attach the dirty-check status without a type assertion * fix(worktree): key the delete failure toast by host and worktree id A Space delete on one host must not dismiss another host's failure toast for a worktree with the same id. * fix(worktree): dismiss the failure toast by the store row's host after a Space row Force |
||
|
|
088fefbb5a |
fix(wsl): bound the line-ending sample's index scan, not just its matches (#29552)
* fix(wsl): bound the line-ending sample's index scan, not just its matches The WSL-on-Windows-drive line-ending sample piped ls-files --eol through grep then head, so when no entry was eligible (* text=auto) head never closed the pipe and every file was read over drvfs until the 15 s timeout, which stage, discard and commit waited on after each backoff. Cap the raw ls-files output before the grep. Follow-up to #27778. * docs(wsl): describe the line-ending sample as the first index entries |
||
|
|
99064a5e11 |
fix(worktree): a worktree created from a remote branch tracks it (#26344) (#29275)
Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
131986aa16 |
feat(worktree): show checkout progress while a worktree is created (#24093)
* feat(worktree): show checkout progress while a worktree is created A local create spends most of its time in the checkout `git worktree add` performs, and the card only said "Creating worktree…" for that whole time. Git already writes its "Updating files: N%" meter into our stderr pipe, so the add call reads it passively (same command line and environment) and the create card shows "Checking out files… N%" with a thin bar. All six create labels are translated so a create never switches language mid-flight. Rebuilt on main from the version stacked on the create-gating branch. On main a create can also claim a prepared checkout; that path never runs `git worktree add`, so it keeps today's "Creating worktree…" label with no bar, and a missed claim falls back to the plain add, which shows the bar. * test(worktree): pin that finalizing a prepared checkout reports no checkout progress Claiming a prepared checkout never runs `git worktree add`, so the create card keeps its label with no bar; this pins that at the finalize step, including the retarget `reset --hard`. * test(worktree): keep only the checkout-progress tests that pin a contract Drops unit tests that mirror the implementation; the real-git flow, the parser captures, the cross-version Git contract and the create card test remain. * test(worktree): pin the checkout meter's path from git to the create card * test(worktree): merge the narrow-terminal capture's pipe chunks |
||
|
|
0fea878680 |
fix(git): keep merge-tree's throwaway trees out of the real object store (#22356)
* fix(git): keep merge-tree's throwaway trees out of the real object store The PR conflict summary and the squash-merged branch cleanup both run `git merge-tree --write-tree` only to read its output, but every divergent run left unreachable loose objects behind. Run it with a per-call scratch object directory (inside objects/, alternates pointing at the real store) that is deleted afterwards, on native, WSL and SSH relay hosts alike. * fix(git): keep partial-clone downloads and clean up stranded scratch stores - A partial clone fetches missing blobs during merge-tree; those packs landed in the scratch store and were deleted, so every check re-downloaded them. Move fetched packs (index last) into the real store before removing the scratch dir. - Sweep this app's own scratch dirs older than an hour, once per objects dir per process, so a crash or failed delete cannot strand one forever in repos where gc never runs. - Remove with Windows transient-lock retries; the shared core now owns all filesystem work and host adapters only resolve the objects dir. * fix(git): quote Windows alternates paths containing ';' and log scratch cleanup failures * fix(git): sweep a merge-tree scratch dir only when its owner is provably gone Another Orca process on the same repo (a second instance, dev and release builds) could delete a scratch object dir still in use once its mtime passed an hour. Each scratch dir now records its owner (pid, hostname, platform, Linux pid namespace); the sweep removes it only when that owner shares this pid space and has exited, or, with no checkable owner, at Git's own two-week tmp_ expiry. Also: - A fetched pack whose later rename fails is rolled back, so the real store never keeps an index-less pack; a pack already installed is left alone. - WSL Git gets the /mnt/<drive> spelling of a drive-path common dir (Git < 2.31 prints it relative, resolved against the C:\ repo path). - conflict-summary tests no longer create scratch dirs in the cwd. * fix(git): cap every merge-tree scratch dir at two weeks, live-looking owner included A reused pid held by a long-lived process could keep a leftover scratch dir forever in a repo where git gc never runs. The two-week age (git gc's own tmp_ expiry) now removes any scratch dir; a younger one is still swept only when its owner shares this pid space and has exited. * fix(git): sweep merge-tree scratch dirs by age alone, off the check's path - The stale sweep now removes any scratch dir at least two weeks old, Git's own expiry for its objects/tmp_* dirs; no merge-tree runs that long. The owner file and pid-space probe are gone, and with them the cases where a sweeper misjudged another process's pid. - The sweep no longer delays the user's check: it starts without being awaited and can only touch dirs far older than the current run's. - A fetched pack's .keep never moves into the real store, so a transient one from an aborted lazy fetch cannot stop gc from repacking that pack. - Tests: the legacy merge-tree fallback gets the scratch env; the local quarantine's WSL decisions (distro from a UNC path, drvfs spelling, host-Git linked-worktree skip, WSLENV hand-off). * fix(git): let quarantined merge-tree see exactly the object stores Git sees without it The scratch object store must not change what merge-tree can read, or the conflict list and the squash-merged branch cleanup differ from main: - An inherited GIT_ALTERNATE_OBJECT_DIRECTORIES (from Orca's or the SSH relay's environment) was replaced; it is now kept and the real store appended after it, as Git's own temporary object stores do. - An inherited GIT_OBJECT_DIRECTORY is the store Git reads and writes, so the command now runs unquarantined. On the WSL route a Windows value only counts if WSLENV forwards it, and then the quarantine is skipped too. - The real store's own info/alternates entries are listed before it, so an alternates chain is linked at the same depth as without the quarantine (a 6-deep chain no longer hits Git's nesting limit). An alternates file that cannot be read or holds a quoted entry runs unquarantined. * fix(git): run merge-tree unquarantined when the repo's store has its own alternates Mirroring Git's alternates resolution could not match it in every shape: a path back to the real store (mutual alternates, or an inherited alternates value naming the repo itself) still linked its chain one level deeper, and a NUL byte in info/alternates broke the spawn. When objects/info/alternates exists, or cannot be checked, the command now runs exactly as before. Repos cloned with --reference or --shared keep writing throwaway objects, as they always have. Inherited alternates are still appended to, and an inherited GIT_OBJECT_DIRECTORY still skips the quarantine. * refactor(git): drop the stale-folder sweep and pack-move rollback from merge-tree's scratch store A scratch folder left by a crash is a tmp_* dir inside objects/, which Git's own gc prunes after gc.pruneExpire, as it does Git's own leftovers, so Orca's 14-day sweep and its test hooks go. Packs a partial clone fetched into the scratch store are moved in Git's migration order: pack- files only, index last, existing names left in place, stopping at the first failed rename with no rollback. A failed move is logged and never stops the scratch dir's removal or the result. Adds a real-Git test that a partial clone lists its conflicts offline once an earlier check downloaded the contents. Co-Authored-By: Claude <noreply@anthropic.com> * fix(git): quarantine merge-tree only once the host is known to support --write-tree Git before 2.35 cannot prune a leftover scratch folder under objects/tmp_*, so a crash in the moment one exists makes every later `git gc` warn and stops automatic cleanup. On Git before 2.38 merge-tree --write-tree is rejected, yet the first check per host per session still made and removed a folder around it. Branch cleanup and the PR conflict list now use the scratch object store only when that host's capability cache already knows merge-tree --write-tree works. Until then the command runs exactly as it does on main: on a Git that rejects it no folder is ever created, and every Git that does get one is 2.38+, whose gc prunes it. Also reword two comments in the quarantine module. Co-Authored-By: Claude <noreply@anthropic.com> * fix(git): learn merge-tree support from a conflicting branch-cleanup check Branch cleanup quarantines merge-tree only once the host's capability cache knows --write-tree works. A conflicting merge-tree exits 1, so the check threw and support was never recorded; on a host whose only checks conflict (likely over SSH, where the relay has no conflict summary to teach its cache) every check kept writing loose objects into the real store. A failure whose stdout starts with a tree ID proves --write-tree ran (old Git that rejects the option prints nothing there), so it now resolves to the same "not merged" result and lets the cache record support. Every other failure is rethrown unchanged. Also shorten two comments and soften a third. Co-Authored-By: Claude <noreply@anthropic.com> * fix(git): put WSL Git's scratch object store on its command line A WSL login profile that set GIT_ALTERNATE_OBJECT_DIRECTORIES replaced the WSLENV-forwarded value, so a quarantined merge-tree lost the real store and failed where main succeeds. The WSL runner now passes both variables as command-line assignments after the profile, appending to its alternates. * test(git): drop quarantine tests the real-Git and compatibility tests already pin The cold-capability gating, scratch-folder lifecycle and WSL path spelling are covered end to end by merge-tree-object-quarantine-real-git.test.ts, the real-binary compatibility contract (Git 2.25/2.38/2.49) and the localGitObjectsDirectory tests, so the unit copies only mirrored them. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
0d4eb242d7 |
Recover failed-job log retrieval onto main (#29180)
* feat(checks): add optional job log outcome * fix(checks): preserve host capture lifetimes and safe outcomes * fix(checks): retrieve and retry bounded failed-job logs * test(checks): cover queued snapshots and captured error safety * test(checks): verify sanitized host excerpts and paired failures * test(checks): identify measurements and preserve paired cleanup * Use the generated check-log fixture in host retrieval tests * Keep real paired-server shutdown outside the injected stall deadline |
||
|
|
6607c54984 |
perf(ci): cut redundant tests and reuse expensive setup (#28669)
* perf(ci): reduce test overhead and reuse expensive build work * test(ci): keep worker startup outside failure-case deadlines * test: isolate push fixture advisory locks * test(ci): preserve fixed drain retry allowance * Align CI contracts with current triggers and guide content * Stabilize consolidated reveal scenarios and retain behavioral CI contracts * Fix upstream lint blockers while retaining behavior and assertions * Classify global Jira preferences in the WSL migration inventory * Align Jira test fixtures with startup and build safety checks * Include package-test routing dependency in CI checkouts and fixtures * Keep native stream classes intact in transcript dependency tests |
||
|
|
38255300d9 |
fix(worktrees): never delete a folder git no longer tracks (#25075)
* fix(worktrees): a retried or resumed delete only removes the folder it started deleting
A failed worktree delete stays listed for a retry, and an interrupted one is
finished at the next start. Both later deletes force-deleted whatever was at
the recorded path, proven only by "git does not list it and it has no .git",
so an ordinary folder the user put at that path since was deleted with its
files, with no click needed in the startup case (STA-9338).
An accepted removal now records the checkout directory's identity (device,
inode, creation time) in its durable record. A retry, a startup finish, and
the in-slot check before the force-delete refuse any folder at the path that
is not that directory. Startup no longer rebuilds a missing .git for a
registered checkout unless the folder is the recorded one. Records without an
identity (older builds, or no creation time) are only finished through Git's
own checks and never force-deleted.
* fix(worktrees): identify the accepted checkout by inode and a real creation time only
The device number changes when an external volume is attached again, so a
genuine leftover read as a different folder. Windows reports a missing
creation time as a negative number rather than 0, which slipped past the
guard.
* refactor(worktrees): every background removal states the checkout identity it accepted
A new acceptance site that forgot the identity used to compile and silently
write records no retry or resume could ever finish. The resume tests also
assert the identity was read, so their refusals can't pass as unrecorded.
* fix(worktrees): a delete that leaves the user's folder in place also ends the workspace
When Orca decided the folder at a failed or interrupted delete's path was not
the one it started deleting, it dropped the removal record but kept the
workspace's metadata. That metadata still says Orca created a checkout there,
and with no record left, the next Delete of the same workspace (a runtime
Delete from mobile, the CLI or a paired client, or a second desktop Delete on a
row not yet refreshed) went to the cleanup that deletes an Orca-created folder
Git no longer lists, and removed the user's folder after all.
Now, when a removal ends without deleting while Git does not list the path (a
listing ends a failed delete, or a retry, resume or first delete fails with no
leftover to show), the host runs a finished delete's bookkeeping: metadata,
terminal history, caches and change events. The folder and the branch are left
alone, and nothing is ended while Git still lists the path.
* test(worktrees): cover the in-slot folder check when Git cannot remove a registered checkout
Reverting that check left every test green: only the unregistered branch's
check had a test where the folder changes during the delete-slot wait.
* fix(worktrees): a resumed or retried Git delete checks the folder inside the delete slot
The check that the folder at the path is still the one Orca accepted ran
before the watcher gate and the delete-slot wait, which can last minutes behind
other large deletes. A folder restored there meanwhile with a copy of the
checkout's .git file passes Git's own validation, so `git worktree remove
--force` deleted it. The resume and retry now repeat the check inside the slot,
right before Git deletes, as the non-Git branch already did; the refusal skips
the Windows recovery that could otherwise finish the delete itself. A normal
first delete is unchanged.
* test(worktrees): assert the second desktop Delete refuses the user's folder
The handler result is untyped, so the previous catch-all did not typecheck; the
assertion now names the refusal the Delete gets once the workspace has ended.
* test(worktrees): the desktop Delete test's stand-in host ends the workspace without a cast
* fix(worktrees): a listing asks Git before dropping a failed delete, and ends its workspace in the same step
* fix(worktrees): a failed delete Git cannot be asked about stays failed for the next listing to decide
* test(worktrees): the failed-delete record test states that Git still registers the checkout
* fix(worktrees): a restart keeps a failed delete whose checkout is gone, so the first listing ends its workspace
* fix(worktrees): a folder Orca cannot read keeps its failed delete and is never deleted
* fix(worktrees): a failed delete leaves the disk only after its workspace's end is saved
* fix(worktrees): a runtime Delete of a failed delete's row is told when Orca left the folder in place
* test(worktrees): the desktop Delete test's stand-in host returns nothing
* fix(worktrees): a Delete of a failed delete's leftover whose .git Orca cannot read says so
* Revert "fix(worktrees): a runtime Delete of a failed delete's row is told when Orca left the folder in place"
This reverts commit
|
||
|
|
13a75d3939 |
[CI] Reduce redundant test work and import existing helpers directly (#28365)
* [CI] Reduce redundant test work and import existing helpers directly * [CI] Preserve provider guards and pin the mobile browser test core * [CI] Fix CI test setup and verify retained contracts * [CI] Compare native media chunks without boxed byte arrays * [CI] Detect shifted native media reads with position-sensitive bytes |
||
|
|
c745051d7d |
refactor(main): share one caller-signal + timeout composition (#28393)
Behavior-preserving refactor. Eleven sites built the same `signal ? AbortSignal.any([signal, AbortSignal.timeout(ms)]) : AbortSignal.timeout(ms)` inline or behind a local wrapper (Codex `createCodexBackendRequestSignal`, `probeDeadline`, `boundedSignal`). They now call one `abortSignalWithTimeout(signal, timeoutMs)` in src/main/lib/abort-signal-with-timeout.ts: - usage requests: Claude OAuth, Cursor, Grok, OpenCode Go API key, ZCode, Codex backend usage + reset-credit read - local worktree path presence, fork sync, retarget divergence budget, upstream-ref watch resolution The Codex reset-credit redeem, which never had a caller signal, uses plain AbortSignal.timeout. Transports, request options, timeout values, error mapping, and Retry-After handling are unchanged. |
||
|
|
0a1f99fb3a |
fix(wsl): recheck watcher-reported paths with full stat on Windows Git drive checkouts (#27817)
* fix(wsl): recheck watcher-reported paths with full stat on Windows Git drive checkouts Co-Authored-By: Claude <noreply@anthropic.com> * fix(wsl): keep rechecked-dirty drive-checkout paths through watcher overflow and restarts A path the default-stat recheck proved dirty stays a suspect until a recheck proves it clean, so an overflow, interruption, watcher reinstall, or closed watcher no longer drops it back to minimal-only status, where it would silently read as clean. * fix(wsl): stage and discard rechecked drive-checkout paths with full stat Status rechecks watcher-reported paths with core.checkStat=default, but add and restore ran with the injected minimal stat, so a same-size rewrite that kept its mtime showed as modified yet Stage and Discard silently did nothing. Path writes now redo the tracked suspects inside their scope with default stat; everything else stays minimal. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
a7e24766b7 |
fix(wsl): run WSL git with Windows Git line-ending settings on checkouts Windows Git wrote (#27778)
* fix(wsl): run WSL git with Windows Git settings on Windows-authored drive checkouts * fix(wsl): detect Windows Git checkouts from host autocrlf plus a whole-tree sample * fix(wsl): keep WSL-created worktrees on Linux Git defaults and never commit CRLF on an unproven checkout - worktree add / clone write a new checkout whose .git points at /mnt/..., which Windows Git cannot open; injecting autocrlf=true there produced CRLF files later read unflagged. - Writes on a host-autocrlf=true checkout whose sample is unproven run with autocrlf=input, so staging normalizes CRLF without ever writing CRLF to disk. - Writes wait for a stale host answer and re-check checkout identity after joining a sample. * fix(wsl): compare a joined sample with the checkout identity read after it settles |
||
|
|
83056e5edb |
fix(skills): judge official skill updates by ownership, not snapshot matching (#27603)
* fix(skills): judge official skill updates by ownership, not snapshot matching Orca decided whether an installed official skill was pristine by matching its bytes against a shipped list of past snapshots, falling back to the skills CLI's lock hash. Copies installed between releases, CRLF copies, and installs whose lock recorded a sha256 hash all read "doesn't match the official version" and could not be updated (#26841), and older content could be labelled newer and never offered. Now an official skill is Orca's when the skills.sh global lock says it came from stablyai/orca, and it is current only when its listed files equal the bundled manifest (line endings and sidecars ignored); anything else is outdated. Install and update are one pinned `skills add .../tree/v<appVersion>` command, used by the in-app runner, `orca skills update`, and the Settings rails. The update dialog preselects Orca-owned rows and lists same-named skills from elsewhere unchecked. Removes the snapshot registry, release mapping, git-tree identity, lock trust and convergence logic, and the release-cut provenance step. * ci(skills): run the pinned-add round trip on a skills CLI that can fetch a commit * fix(skills): let orca skills update target the shared folder on a bare host Update became the same pinned add as install, so it inherited install's refusal when no agent is detected. That broke the bare-host update the old command guaranteed, and the built-CLI check (update --dry-run, no --agent) on CI runners. The in-app runner already fell back to universal; both now share one detectSkillsCliUpdateAgentKeys rule. Also brings the CLI skills docs off `npx skills update`. * refactor(skills): derive the update selection from the grouped rows The selection hook took the live and the retained inventory, re-derived Orca ownership from installations beside the grouping's own orcaOwned, and kept a separate display count. Groups already carry both eligibility and ownership, so the hook reads them once; the button's existing live-scan gate is the only authorization check. The unselected line is inlined into the headline. * refactor(skills): drop helpers the ownership design made redundant - isOwnerManagedSkillScope only negated into the participation check once plugin caches stopped participating; fold it in. - Eligibility re-checked resolvedPath/physicalIdentity, which observation already turns into 'inaccessible'. - The manifest verifier probed access() before a readFile that throws anyway. - orchestration-install-command.ts only forwarded one constant from the lib module its three importers already import. * i18n(skills): stop naming npx skills update in four skipped-copy strings The es/ja/ko/zh cantUpdateReason and tipDuplicate strings still named `npx skills update`, which Orca no longer runs for its skills. Say 'the update command' instead. * refactor(skills): inline the single-use pinned source URL helper orcaSkillsSourceUrl was exported but only called by the builder beside it. * fix(skills): let an update choice lapse when the install source changes The selection froze every checked name, Orca's preselection included, the moment the user ticked anything. A re-check that then attributed a name to another source kept it checked, replacing it without consent. Store only the user's explicit choices, keyed by name and lock source, and default to ownership otherwise. * fix(skills): label the empty update action 'Update skills', not 'Update 0 skills' The button stays whenever a row is updatable, so unticking every row (or a dialog with only non-Orca copies outdated) doesn't reflow the footer. At zero selected it stays disabled and reads 'Update skills' instead of a zero count. The label lives beside the dialog's other count copy. * fix(skills): let keyboard focus open the Not from Orca tooltip The source of a non-Orca copy is only in the tooltip, and its trigger was a plain span. Make it focusable like Orca's other tooltip-on-text triggers (tabIndex 0, cursor-help, the row primitive's focus ring), as in the agreed mock. * test(skills): hold the round-trip add equal to the shared install builder The CI round trip hardcoded its skills add argv, so it could drift from what Orca runs. A .mjs script can't import the TS builder (extensionless imports), so the argv moves into an exported roundTripAddArgs and a unit test asserts it is the builder's unattended add with the tag swapped for the PR commit. The script runs under the same main guard as the manifest generator. * i18n(skills): translate the skill row's location count and duplicate reason es, ko and zh still carried English for SkillUpdateRow.oneLocation and manyLocations, and es and ko for SkillFreshnessRow.skippedReasonDuplicate. * fix(skills): pin official skill installs to the build commit and skills CLI `skills add .../tree/v<version>` only works for stable and RC builds: hourly, daily and adhoc builds publish elsewhere, so stablyai/orca has no tag for their version and both install and update failed there. Every build is made from a commit that exists in stablyai/orca, so the build now stamps `git rev-parse HEAD` and the one shared builder pins to `tree/<commit>`, which equals the bundled skills by construction. Main, renderer, web and vitest get it as ORCA_BUILD_COMMIT (replacing ORCA_APP_VERSION); the tsc-built CLI reads it from the compiled out/package.json boundary. A missing or malformed commit fails the build or the command with a clear error instead of building a bad URL. The builder also pins the skills CLI (`npx skills@1.7.2`): 1.5.x cannot fetch a commit, and a bare `npx skills` can reuse a stale cache or resolve to another binary (#18687). The CI round-trip script reuses the same constant. * ci(skills): run the round trip on the shared pinned skills CLI version * docs(skills): document the commit-pinned skills add * refactor(skills): keep the pinned skills CLI version in the install builder skills-cli-version.ts existed only so the plain-Node round-trip script could strip-load a .ts file, which needed --disable-warning in CI. The version now lives beside the builder that uses it; the script keeps a literal, and the existing round-trip test already asserts its argv, skills@<version> included, equals the builder's. * refactor(skills): validate the build commit once, where it is resolved resolveBuildSourceCommit already refuses anything but a full hash, and both the define and the CLI's stamped package.json come from it. The builder's second check could only fire as a module-load throw in the renderer's command constants. Also drops the resolver's unused cwd parameter. * test: stamp a fixed build commit instead of running git in the vitest config Tests only interpolate ORCA_BUILD_COMMIT, so they need not read HEAD (or fail outside a git checkout) on every run. * refactor(settings): match only skills add in the Windows npx preflight Every official skill command is now the builder's skills@<version> add; no rail emits skills update any more. * fix(skills): fail an update that leaves a broken skill link behind Eligibility already skips names with a broken link before the run, so one afterwards is the run's own damage; report it instead of claiming success. * ci(release): refuse to tag when the skill manifest is stale The removed --release step implicitly checked the committed manifest against skills/. Keep that guarantee with a verify-only run; it writes nothing. --------- Co-authored-by: Orca Worker <orca-worker@localhost> |
||
|
|
c5224f70d8 |
refactor(providers): delete the SSH git and filesystem providers and relay-only wiring (#27644)
* refactor(providers): delete the SSH git and filesystem providers and relay-only wiring * refactor(ssh): drop relay-protocol exports only the deleted stream readers used --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
a944a37c8c | [CI] Check real I/O progress during worktree deletion (#27620) | ||
|
|
df2f2a72de |
refactor(ipc): filesystem and repo handlers are local or runtime only (#27609)
* refactor(ipc): filesystem and repo handlers are local or runtime only * test(ipc): drop two casts in the rewritten listing and import tests * fix(providers): keep the default error name so IPC callers see the old text --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
3f8d296b8d |
fix(macos): preserve terminal permissions across app updates with a small signed helper (#27021)
* fix(macos): preserve terminal privacy identity across app updates * test(macos): make launchd fixtures portable across host platforms * fix(macos): probe protected contents and close handles on denial * fix(i18n): register terminal permission status labels * fix(macos): fork the daemon when no stable-bundle job can claim the endpoint A copy, signature, plist or deadline failure before bootstrap, a bootstrap launchd refused with the job absent, a job whose daemon exited before answering, and a foreign endpoint all threw out of the launcher. The app then lost its persistent daemon and fell back to local PTYs, where main forks one. These now fall through to the existing fork path, which also adopts a foreign occupant normally instead of as a degraded endpoint. A timed-out bootstrap or a job still running/unreadable stays fatal, so no second daemon races it. * feat(telemetry): split daemon origin events by stable-copy launch method daemon_adopted, daemon_pty_cwd_denied and daemon_pty_cwd_readable gain a closed-enum launch_method (stable-copy / app-fork / unknown), read from the spawner path the daemon recorded, so the update fix is measurable per daemon even when an old forked daemon is adopted across updates. The reliability gate is a test manifest with no runtime switch, so there is no gate state to report; spawner_path_class reports the private copy as 'other' alongside other non-/Applications installs. * refactor(telemetry): report the stable copy as a spawner path class The separate launch_method property duplicated what the recorded spawner path already says. spawner_path_class gains `stable-copy`, classified from the same path under the private runtime root, and launch_method, its enum and classifier are removed from all three .strict() daemon origin events. A deleted copy still reports `missing`. * refactor(settings): gate terminal-host Full Disk Access on macOS once The permissions handler already asks for the terminal-host status only on macOS, so the status function's own platform check and the pane's 'unsupported' branch could never fire. The off-macOS guarantee is now tested at the handler. * fix(macos): keep the fork fallback inside the startup PTY gate The stable launch could run until 55 s after the launch started, so a late "unavailable" verdict left the fork fallback's own 10 s readiness wait to finish after the 60 s gate had already moved the app to local PTYs. The launcher now gives the stable attempt one handoff deadline: the gate minus the fork's readiness timeout and the 5 s lease/adapter allowance (45 s). Copy, signature check and plist conversion share one abort signal that ends early enough for bootstrap and the job's own readiness wait to meet that deadline; every command otherwise keeps a fixed timeout. An unavailable verdict past the deadline is reported instead of forking a daemon that cannot arrive in time. The per-step budget module is gone (its deletion landed in the previous commit by a staging slip). The launch body is now two blocks: preparation (plist, plutil, deadline check) fails as unavailable after removing the copy and job file, and bootstrap runs in its own try/finally that always deletes the job file. Retirement is simpler and claims more orphans: one in-flight collection per process instead of a per-root map and rotating 20-record scan, and the job record (written before copying) no longer carries a bundle name because lsof +D scans the whole runtime directory (measured equal cost, same result on a live copy). A never-submitted copy is retired once its producer has exited and its job is missing, so a bootstrap timeout or a crash mid-copy no longer leaves a copy forever; the open-file check still guards deletion. Collection now starts from the launcher after the attempt. * docs(macos): ship the daemon launch rules, not the investigation The signed investigation harness left the tree in the previous telemetry commit by a staging slip; its results and pinned link stay in the PR. This trims the reference doc to why the copy and launchd exist, the fallback states and the cleanup rule, trims the reliability gate to what its validator needs, and points AGENTS.md at the reference doc next to the Windows relocation entry. * fix(macos): hand off to the fork as soon as the stable job exits A copied daemon refused at exec (for example by Gatekeeper) never answers, so every fresh start paid the full 10 s readiness wait before the fork fallback. The readiness loop now reads the job state every 500 ms and abandons as soon as the job is stopped or missing; a running or unreadable job is still waited out and never forked beside. Also drops the dead forkEntryPath the stable launch never read (its options type now omits it) and un-exports the handoff constant. * fix(macos): keep daemon copies out of Spotlight and unregister retired ones Each runtime copy is a full Orca.app, so Spotlight indexed it as an app and LaunchServices kept a record for it that outlived the copy. A registered copy claims orca: links and Markdown/CSV files, and becomes their default handler whenever /Applications/Orca.app is not registered. New copies go under runtime-*/app.noindex/, which Spotlight skips and which stops the record LaunchServices made while the bundle was being copied. Retirement now runs lsregister -u on every bundle in the copy (both the app.noindex layout and the earlier runtime-*/Orca.app layout) once lsof shows it unused, then deletes it. A running copy stays registered, because Local Network access may be resolved through that record. Collection and stable-copy telemetry match the runtime-* directory, so copies made before this change are still adopted, classified and retired. * docs(macos): record the LaunchServices trade-off and the rollback plan The reference doc now explains why copies sit in a .noindex folder, why only retired copies are unregistered, and how to turn the copy launch off: make launchMacDaemonFromStableBundle return null, and keep the retirement code until no copy-launched daemon can remain, because the old code never deletes copies. A test pins that rollback: with the stable launcher returning null, the launcher forks and still runs the copy collection. * fix(macos): keep daemon copies registered so Local Network can resolve them The `.noindex` folder kept copies out of Spotlight and LaunchServices, but Local Network access is resolved from the signing identifier through LaunchServices to the executable's Mach-O UUID. After an update that changes Electron only the copy carries the running daemon's UUID, so an unregistered copy risks the same Local Network loss main has today. Copies go back to `runtime-*/Orca.app`; retirement still unregisters each copy before deleting it, so deleted copies leave no LaunchServices entry. The remaining trade-off: while `/Applications/Orca.app` is missing, a running registered copy can be picked for `orca:` links and documents. * fix(macos): hide daemon copies from Spotlight and tie the job to Orca Spotlight listed each runtime copy as a second Orca app. New copies go back under runtime-*/app.noindex/Orca.app, which Spotlight skips. Retirement and stable-copy telemetry match the runtime-* directory, so copies in the earlier runtime-*/Orca.app layout still retire and classify the same way. Local Network access identifies the daemon by its code signature and main-executable UUID, not by a LaunchServices record, so an unregistered copy keeps it. The launchd job now sets AssociatedBundleIdentifiers to com.stablyai.orca, which is how a job not installed through SMAppService names the app it belongs to. Copies are never registered explicitly; retirement still runs lsregister -u before deleting each one. * feat(macos): package a signed terminal host helper that runs the daemon on the pinned Node Contents/Helpers/Orca Terminal Host.app carries the pinned Node, the daemon's out/main JS and this slice's node-pty, signed inside-out as com.stablyai.orca. afterPack boots it through one PTY round trip, then fails the build on layout, Info.plist, macOS floor (NODE_RUNTIME_DARWIN_MINIMUM_OS vs Node's minos), architecture or signature drift. * ci(macos): cache and prefetch the pinned Node before packaging Packaging now fetches the darwin Node from nodejs.org, whose download has no retry. Each mac workflow caches it with the electron-builder downloads, keyed on the pin, and warms both darwin targets inside the existing retry wrapper. * feat(macos): launch the daemon from a copy of the terminal host helper, not the whole app The stable launch now clones Contents/Helpers/Orca Terminal Host.app (~124 MiB instead of ~628 MiB) and runs its Node on its daemon entry, without ELECTRON_RUN_AS_NODE. The copy must still match the running app's designated requirement. A missing helper falls back to the fork, and macOS below the helper's Node floor forks without copying. * docs(macos): describe the terminal host helper, its floor and its identity * fix(macos): bound the terminal host boot check by its deadline and always clean up A daemon that stayed silent hung packaging past the 20 s deadline; one that closed the connection left the hello unsettled so Node exited 0 without cleanup; a malformed line threw uncaught. Every connect, hello and PTY wait now races the deadline, close/error/parse failures reject, and the daemon and scratch dir are removed on every path. * ci(macos): cache the pinned Node on its own key with no restore-keys fallback Sharing the electron-builder cache let old Node archives accumulate through restore-keys. Also moves release-mac-build's Electron cache comment back onto its step. * fix(macos): reject a universal slice for the terminal host and pin its name in one test packageMacTerminalHost turned an unknown arch into darwin-undefined; it now throws through the shared mac arch mapping. A test asserts the packager and the desktop agree on the helper bundle and executable names, replacing a Linux/Windows test that could not fail. * fix(macos): pass the launchd adoption lease its options object after the main merge * test(macos): accept a reset as the daemon closing the boot-check connection; keep only passing gate commands * fix(daemon): bump the terminal daemon protocol to v45 so new macOS tabs start in the terminal host helper * fix(macos): ship the daemon's required packages in the terminal host and boot it outside the build tree * style(macos): use a template literal in the terminal host resolution check * fix(macos): derive the terminal host's packages from the daemon's own module graph * fix(macos): treat any non-literal daemon load as computed, follow directory loads, and keep nested dependencies * refactor(macos): move terminal-host telemetry, Settings status and Helper folder strings to a follow-up Keeps this change to the update-safe terminal host itself. The follow-up restores: - the `stable-copy` spawner path class in daemon adoption telemetry; - the per-terminal-host Full Disk Access row in Settings and its daemon RPC; - writing folder usage descriptions into the app's Electron Helpers for the fork fallback. The helper still takes its Desktop, Documents and Downloads descriptions from `macos-folder-usage-descriptions.cjs`, so that constant stays. * fix(i18n): restore en.json to this branch's base, not newer main The previous commit took the file from a newer main, which pulled unrelated strings into this PR. * refactor(macos): publish one terminal-host copy per helper build under a fixed launchd label Name the copy after the helper's CDHash, reuse it when present, and publish a new one by cloning into a staging dir, verifying it once against the app's designated requirement and renaming it into place. One launchd label per profile and protocol replaces per-launch labels, so job.json, producer pids and per-label collection go away. Retirement deletes any other copy lsof shows unused. The stable launcher returns null for every fork fallback and logs its reason. * fix(packaging): copy terminal-host dependencies from where Node resolves them A nested package's own dependencies were never walked, so a hoisted dependency of a nested package was left out of the helper. Resolve each dependency through the node_modules lookup Node performs from its parent and copy it to the same relative place, unless a copied package already carries it. * fix(packaging): fail release and CI mac builds that cannot boot a terminal-host slice An arm64 runner without Rosetta skipped the x64 helper's PTY boot check and shipped the slice untested. Release channels and CI now fail there; local cross-arch builds still skip with a log line. * docs(macos): describe the per-build terminal-host copy and its fallback reasons * docs(macos): keep the codesign command on one line * test(macos): type the launchd test state without an assertion * docs(macos): name what the login-session flag gates in the stable launcher * fix(macos): wait for launchd to drop a booted-out terminal job before reusing its label --------- Co-authored-by: Neil <neil@stably.ai> |
||
|
|
215ee3e625 |
refactor(runtime): runtime git and file callers drop SSH arms (#27501)
Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
ec14775f93 |
refactor(providers): an ssh: route carries no provider and throws one typed error (#27477)
Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
2b4b60a87e |
refactor(relay): delete the SSH relay server, its build assets and tests (#27215)
* wip: delete the SSH relay server and its build, assets and tests * wip: drop main/relay plugin conformance; WSL reader lookup test * fix: drop the emptied relay bench; prune baselines * test: keep the native halves of tests that also covered the relay * chore(ci): drop shard timings for the deleted relay server tests * fix: keep the directory listing budget that darwin terminal names now import --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
b63d39ebe5 |
Keep repository detection off the main registration thread (#26760)
* Keep repository detection off the main IPC thread Share repository discovery decisions across synchronous compatibility callers and the existing asynchronous Git runner. Await discovery in project registration, nested imports, and folder upgrades, and recheck folder ownership after discovery completes. * Make repository detection fixtures work on native Windows * Await asynchronous Git checks in folder upgrade fixtures * Prevent duplicate project registration after async discovery * Check authoritative registration identity before renderer convergence * Narrow repository responsiveness to detection and registration * Keep one local project identity after concurrent registration * Run registration SSH coverage in the existing Docker lane * test(git): cover classifier failure stages and literal path identity * test(git): use the current process host package * test(repos): use current process host in registration fixture |
||
|
|
1912c90141 |
Extract subprocess handling into @orca/process-host (#26963)
Extract subprocess execution and process contracts into @orca/process-host with explicit public entry points, independent checks, and one Node 18 compatible CommonJS runtime. Migrate application consumers and integrate package builds, dependency selection, import enforcement, and runtime resource staging. Preserve daemon startup ownership and profile isolation, and qualify development rebuild recovery and concurrent output publication. Remove unnecessary aliases, test forwarding exports, and a duplicate output scan. Validation: full typechecks, changed-code quality, package and artifact checks, native Windows builds/types plus 306 runtime tests, and independent native reviews. Matched full E2E controls on original main and unchanged reviewed head both pass all 17 applicable tests (nine skips). Earlier intermittent E2E failures are documented in the PR; no assertions were weakened. The final combined-tree audit preserves both branches with no import-boundary violations. |
||
|
|
3e0b828358 |
refactor(main): route git probe and terminal-artifact provider checks through the host router (#27066)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
6aef5c569c |
Phase 3.7: fix SSH/remote failures found by adversarial real-host testing (#26847)
* fix(remote-terminal): keep a reconnected pane's size after an SSH outage (P1-4) (#26625) * fix(remote-terminal): keep a reconnected pane's grid when its stale stream is reaped (P1-4) The host's width floor only handed ownership to viewers that had claimed since subscribing. A reconnecting desktop pane re-subscribes without claiming, so when the heartbeat later reaped the dead socket that owned the floor, the host reclaimed the PTY to its spawn size (120x40) and held the pane there. The owner's same-client replacement stream now inherits the floor, and a pane whose stream was reaped before it reconnected resumes ownership on re-register unless the host or another viewer claimed since. * fix(remote-terminal): host typing during an outage revokes the reaped owner's resume Host input reaches the PTY without a renderer viewport claim once the host has reclaimed its grid, so the orphaned-owner entitlement survived and a passive reconnect resized the host pane back to the remote grid. The host write path now clears that entitlement on driving input (query replies, focus reports and launch writes excluded). * test(pty): stub the runtime's host-input note in write-path handler tests --------- Co-authored-by: m4air <m4air@Mac.localdomain> * fix(remote): an oversized RPC reply fails that request instead of closing the host connection (#26624) Expanding a folder with ~80k entries on a managed SSH host produced a files.readDir reply over the 4 MiB encrypted-reply cap. The channel treated that as a transport fault and closed the whole control socket (1013), and the explorer rendered the failed folder as empty. - The WebSocket RPC transport now answers an over-cap reply with a reply_too_large error for that request id, so the channel never sees it. - files.readDir checks remote listings against the request's content budget and names the entry count. - The explorer shows a subfolder's read error on its expanded row. Finding: HH-2 (Phase 3.7 hostile-host). Co-authored-by: m4air <m4air@Mac.localdomain> * fix(ssh): let a slow host finish installing the pinned Node runtime (HH-3) (#26631) * fix(ssh): let a slow host finish installing the pinned Node runtime The POSIX promote (extract, hash, self-test and publish the ~110 MiB node binary) ran under the 30 s default exec timeout. On slow home storage it timed out, the remote work kept running, and the runtime store lock it held stayed until the 20-minute stale rule, so relay rung A queued behind it past its own deploy bound. Give both platforms the 300 s promote bound and have the host release the store lock itself when the promote exits, with owner-checked releases so a late release never removes a lock someone else took. (HH-3) * fix(ssh): let the host release the store lock after a slow under-lock re-hash The under-lock re-check of a sibling-published runtime hashed the ~110 MiB executable as its own command under the 30 s exec default and outside the host-side lock release. On slow storage it timed out, the client correctly kept the lock, and nothing on the host freed it until the 20-minute stale rule, so later installers ran out their deploy bound. Run the re-hash and the conditional promote as one host command wrapped in the host release, and give every full-runtime hash, including the initial probe, the slow-storage bound. (HH-3) * test(ssh): count the combined under-lock re-probe and promote as one host command --------- Co-authored-by: m4air <m4air@Mac.localdomain> * fix(ssh): shell startup output on stdout no longer breaks SSH connect (HH-1) (#26617) * fix(ssh): fence captured exec stdout so shell rc output cannot break connect sshd runs the user's login shell before Orca's /bin/sh wrapper, so any startup-file stdout (fish/tcsh/zsh/bash echo, or a bare terminal-title escape) was glued onto every captured answer. The home probe read "banner\n/home/u" and connect failed with "Remote home is not a valid path". execCommand now wraps wrapped POSIX commands in a per-call nonce fence and returns only the payload between the markers; the SSH folder browser gets the same fence. The fence is the existing WSL captured-login-shell fence, moved to src/shared/posix-stdout-fence.ts so both transports share it. Finding: HH-1 (phase 3.7 hostile-host). Refs #8324. * fix(ssh): fence the login-shell Node probe and fall back from a noisy SFTP handshake The login-shell 'command -v node' probe ran unwrapped, so output from sshd's shell and the nested login profile became the Node path. It now prints a one-line fence every login shell parses, inside that shell. An external sftp-server is started through the user's shell, so startup output corrupts the SFTP handshake. ssh2 reports that parse failure only before the session is ready, so SshConnection.sftp tags it, and relay install transfers treat it as SFTP unavailable and stream over exec stdin. Finding: HH-1 follow-up (phase 3.7 hostile-host). Refs #8324. * fix(ssh): keep the stdout fence marker out of the command text The runtime-store inventory lists processes with ps, which prints its own script, so a literal marker in the script was read as the fence and the store GC skipped every pass. printf now joins the marker from two halves. --------- Co-authored-by: m4air <m4air@Mac.localdomain> * fix(orcad): finish an abandoned update on connect, and refresh renderers after a main-side re-pair (P1-A, P1-C) (#26635) * fix(orcad): finish an update or rollback its client abandoned, and tell renderers about main-side re-pairs P1-A: a wake that finds the activation fence now runs crash recovery, which takes only a stale, previous-boot or exited-own fence. Recovery of an activation interrupted after the candidate launched resumes it: the candidate commits if it passes the readiness gate, else the activation is undone as before. P1-C: invalidating a still-linked environment's transport restarts its status owner, and a renderer that sees status under a newer pairing revision re-reads the catalog before applying it. * fix(orcad): a resumed update keeps the app version that started it Recovery that commits an abandoned candidate stamped no activeAppVersion, so an older desktop read the host as pre-tracking and downgraded it over state the newer build had migrated. The activate journal now records the initiating app version; recovery stamps it, and a journal too old to carry it is undone instead of committed. * test(runtime-env): answer nothing for the status restart a re-pair now triggers * fix(orcad): recovery leaves running an update server a too-old journal cannot commit A journal without candidateAppVersion was sent to undo, which stopped a healthy candidate that had already migrated state and then refused to restart the incumbent, leaving the host serving nothing. Refuse instead, keeping the fence and the candidate, until the operator accepts Restore. --------- Co-authored-by: m4air <m4air@Mac.localdomain> * fix(terminal): a remote recovery never splices stale history above the new screen (P2-5, P2-7) (#26647) * fix(terminal): a remote recovery never splices stale history above the new screen A pushed remote snapshot (overflow recovery, seq-gap resync, first attach) is sent because the client's stream broke, so the pane's own history ends before that image. Since #26319 a screen-only image kept that history, splicing a stale block directly above the final screen with a silent gap of up to ~270k lines (P2-5), and a first attach to an older host left silent holes (P2-7). - Host: the ACK-overflow recovery serializes up to 1000 history rows. - Client: the seq-gap resync asks for the same 1000 rows. - Client: a pushed snapshot always replaces the pane's scrollback; the keep-local-scrollback path is removed. A screen-only image from an older host now shows only its screen, which is lossy but never misleading. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(terminal): a pushed image replaces the normal buffer over a live alt screen too Over a pane already on the alternate screen, an image that also ended on alt repainted only its alt frame and kept the pane's normal buffer, inferring the two had frozen together. Inside the gap the TUI can exit, the shell print and a second TUI start, so once that TUI exited the pane showed its stale history where the host had the missed output. A pushed image now always rebuilds the normal buffer and history from the host before entering alt itself. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(terminal): bound recovery history by link slack and keep pre-TUI scrollback under screen-only images A recovery that carries history now goes out only when the stream's ACK window has drained; under a sustained flood recoveries stay screen-only and the owed history follows once the client catches up, so snapshot bytes no longer outgrow the client's intake. A pushed image without history (scrollbackRows absent or 0) over a live alt screen repaints only the alt frame again, so a reconnect to an older host no longer wipes the pane's pre-TUI scrollback. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(source-control): Stage All stages every change when the listing hit the 1,000 cap (HH-4) (#26618) * fix(source-control): Stage All stages every change when the listing hit the 1,000 cap The status listing stops at 1,000 rows, but Stage All built its path list from those rows, so it staged 1,000 of 20,000 changes and the next commit was silently partial. When the listing is capped, Stage All (and the Changes header Stage all) now asks the host to stage the whole worktree via an optional scope on git.bulkStage; unresolved conflicts stay unstaged. Older hosts ignore the field and stage the listed paths. Fixes HH-4. * fix(source-control): whole-worktree Stage All needs a host receipt, skips shared links, and reaches mobile - Scoped stage sends no paths and requires a stagedScope receipt, so an older runtime or relay stages nothing and the client reports it instead of a silent partial stage. - Scoped 'all' skips Orca's shared symlinks (configured and really a link), matching the status listing. - The capped-listing banner offers Stage All, reachable when the visible prefix is entirely staged. - Mobile reads didHitLimit and uses the same scoped stage and receipt. * test(source-control): type the Stage All test stubs so lint and the mobile test typecheck pass * fix(source-control): match Shared Paths saved as ./name or name/ against git's spelling The Shared Paths setting keeps entries as typed, but git prints worktree paths without a leading ./ or trailing /. Stage All's conflict path compared the raw spelling to git's untracked list and staged the shared node_modules link, and an exclude pathspec ending in / made git refuse the add after tracked changes were already staged. Status listing had the same mismatch. getSafeRelativePath now returns git's spelling, so every caller gets it, including saved settings. --------- Co-authored-by: m4air <m4air@Mac.localdomain> * fix(ssh): redeploy a managed server another desktop stopped; forget one whose host is gone (P1-D, P1-E) (#26628) * fix(ssh): redeploy a server another desktop stopped; let a dead managed host be forgotten P1-D: a connect whose linked server the host reports as not activated (another desktop stopped it) now goes on to the update check, which redeploys it in place. P1-E: add Forget (managedServer.forget, orca environment forget, Settings row) that runs Stop's local unlink without contacting the host; the verdict stays unverifiable. * test(ssh): type the forget test's hoisted store without an assertion * fix(ssh): never downgrade a stopped host; redeploy it mid-session and retry after a failure A stop now keeps the stopped build's Orca version, so an older desktop's reconnect skips as host-newer instead of starting an older server on a store a newer build migrated. A stop recorded without that version is not guessed at. A stopped host retries the redeploy on each connect instead of the once-per-version failure suppression, and a runtime call that finds the server stopped mid-session redeploys it before the call. * fix(ssh): rebuild the managed forward after redeploying a stopped host A redeploy can bind a different port than the reused forward targets. * fix(ssh): keep stop journals readable by older desktops and recheck version policy under the fence - The stop journal's recordAfter is the core deactivated record older peers compute; the committed record keeps activeAppVersion (which released planners already honour) and rolledBackFrom, rebuilt from recordBefore on recovery. - Auto-update re-applies its version policy to the record read under the host fence, and a census counted against another incumbent is treated as unverifiable. * fix(ssh): file a dispatched stop where older desktops cannot commit it, so their recovery never drops the version guard * fix(ssh): refuse a fresh deploy of an older orcad onto a host a newer desktop stopped The unlinked deploy path never consulted the stopped record's activeAppVersion, so a desktop without a link to the host could activate its older build over state a newer build migrated. Both the auto-update planner and the fresh deploy now apply one downgrade rule under the fence. --------- Co-authored-by: m4air <m4air@Mac.localdomain> * fix(ssh): redeploy a managed server a pre-version Stop left; let a forced deploy run over a newer one (#26766) A stopped activation record without activeAppVersion is what every Stop on main writes, so refusing it stranded those hosts behind an unforceable policy refusal. Only a provably newer host is now refused; a forced fresh deploy runs this version anyway, and the refusal names both versions. Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> * fix(remote-terminal): no keystroke lost, duplicated or split across a silent outage (P1-1, P1-2, P1-3) (#26630) * fix(remote-terminal): no keystroke lost, duplicated or split across a silent outage Remote pane input was fire-and-forget: keys handed to a socket that had silently died were gone, keys still in the 8 ms debounce at detection were cleared, and held keys were discarded when the auto-recovery window ran out. - New negotiated `inputAck` capability: the client names an input session and sequences Input frames (existing frame header `seq`); the host records the last applied sequence per (PTY, session) across connections, writes each sequence once and acks it with new opcode InputAck (18), sent only to clients that advertised the capability. - The pane keeps sent-but-unacked input and replays it to the same terminal on reattach; the host dedupes. Old hosts keep current behavior. - Debounced and claim-queued input at detection is held, not cleared. - Held input survives the 'disconnected' phase; the hold coalesces keys so minutes of typing do not hit the per-entry cap. - Banner copy no longer claims a one-minute retry window. Fixes P1-1, P1-2, P1-3 (phase 3.7 network findings). * fix(remote-terminal): never replay into a restarted runtime; keep Ctrl+C and validating paste in order - Host publishes inputLedgerId with inputAck; the client drops unacknowledged input sent to a different ledger (runtime restart with the same PTY) instead of running it twice. - A replayed duplicate is acked only after the original write settles. - Accepted input (Ctrl+C, Escape, paste) rides the sequenced stream and resolves on the host ack. - Keys typed while held queue behind a paste still in size validation. * fix(remote-terminal): parse subscribed capabilities into a typed shape instead of Reflect.get * fix(remote-terminal): ack only applied input; withdraw writes their caller gave up on - Host ledger records each write's outcome: a refused write is rolled back and never acked, so the client's replay writes it; nothing queued behind it runs or is acked. A write that threw is acked as delivery-unknown, so the client reports it failed and never replays it. - Paste and file-drop timeouts abort their accepted write: a copy still held for an outage is withdrawn, and a journaled copy is left out of the replay. - A batch of sequenced input keeps one endpoint and ledger, so a send that retires the stream mid-batch no longer discards earlier unacked input. * fix(remote-terminal): host writes sequenced input strictly in order and asks the live stream to resend after a refusal - The ledger admits only the next sequence; a refusal or gap answers with an InputAck resend request instead of WriteUnavailable, so the pane keeps its journal and resends on the same stream (no remount, no carrier bookkeeping). - Sequenced input uses the provider's settled write; an unverifiable handoff is delivery-unknown, not refused. - Replays fill given-up slots with empty input so the host can continue; the client remounts only after every resend backoff is refused. - Typing batched ahead of a paste goes out as its own sequences, so cancelling the paste keeps it. * refactor(remote-terminal): deliver sequenced input with standard go-back-N Replace the hand-built "block on refusal" bookkeeping with go-back-N: - Host: per (PTY, input session) the ledger keeps only appliedSeq, the highest contiguous confirmed write. Frames run one at a time in arrival order: a duplicate is re-acked, the next sequence is written and acked on confirmed success, and a refusal or gap is answered with a resend request carrying appliedSeq. No blocked state; every stream is equal. A settlement that proves no byte left is a refusal; one that may have written advances as delivery-unknown and is never retried. - Client: one journal keeps unacked input in order and resends from appliedSeq + 1 on a resend request (with backoff), on a new stream, or when acks stall. Refusals never remount the pane. Withdrawn pastes, stale query replies and over-budget bytes resend as empty slots. - The recovery hold pauses (keeps the rest) when a release is cut short by another outage, and keeps delivering typing after an accepted write a sequenced stream withdrew or could not confirm. - Add a seeded model-based test that drives the real pane transport, multiplexer and journal against the real host dispatcher and ledger through 2,000 random fault schedules. * test(remote-terminal): cover the refusal remount on a stream without sequenced input A sequenced stream now resends refused input on the same stream instead of remounting, so the end-to-end remount check negotiates no inputAck, as an older peer does. * fix(remote-terminal): escalate persistent input refusals, drop input once recovery gives up, and never report a lost unknown ack as applied - The pane falls back to its remount path when the host keeps refusing input past the resend backoff ladder. - Held and journaled input is discarded when auto-recovery gives up, and input typed under the disconnected banner is not held. - The host ledger answers a duplicate for its own sequence and remembers delivery-unknown outcomes; the client settles a write as applied only from its own ack. - The input model now requires a write reported applied to have reached the PTY whole, and covers a refusal that never clears. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Claude <noreply@anthropic.com> * fix(remote-terminal): keep input typed past the auto-recovery window for the same terminal, within a bounded grace (P1-3) (#26832) The last round of #26630 discarded held and journaled input as soon as auto-recovery gave up, and refused input under the disconnected banner. On a real host a 200 s outage lost 102 of 120 keystrokes, although the pane reattached the same terminal on its own seconds later. Input now stays held for the same handle and incarnation while the pane is disconnected, for up to 5 minutes after it gave up. Past that grace it is discarded, and typing into the dead pane is refused, so stale keys never run at an arbitrary later reconnect. The grace is a timestamp checked when input moves, so a latched pane stays free of timers. Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
4d1a3dd6ac |
Detect installed GitHub CLI behind hung launchers (#25183)
* Recover installed forge CLIs after launcher timeouts * Preserve healthy CLI timeouts and add native platform checks * Resolve Windows tree termination outside PATH * Use native absolute paths in the relative PATH fixture * Expect the absolute System32 taskkill in tree-kill tests --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
2137295bb6 |
fix(git): use branch-safe author names as username fallback (#25984)
Co-authored-by: Kyou0203 <kyou12138@gmail.com> |
||
|
|
9e8a3a5c67 |
fix(repos): stop reporting clones that never ran as successful (#23770)
* fix(repos): reuse a saved project only when git shows it's this clone A saved project outlived its folder, so Clone reported success without running git. Fixes #23563 * fix(repos): let a repeat clone of an empty repository reuse its project A clone of a repository with no commits leaves HEAD unborn, which `rev-parse --verify HEAD` rejects exactly like the `refs/heads/.invalid` HEAD a killed clone leaves. The saved-project check read both as "not a finished clone", so cloning a brand-new empty repo a second time — a double-clicked Clone, a re-run `orca project setup-clone`, or #2981's queued retry — fell through to git and died on "destination path already exists and is not an empty directory". Ask the two questions apart when `--verify` fails: `symbolic-ref HEAD` resolves an unborn branch but not a killed clone's HEAD. It is already on the relay's read-only allowlist. Probes also short-circuit once the clone is cancelled, so no probe after Cancel spawns git. * fix(repos): prepare the worktree root after re-cloning a project's folder The branch that re-clones into a saved project's folder returned the project without the two steps every sibling branch runs: the worktree root that vanished with the deleted folder is never re-created, and the authorized-roots cache keeps the answers it cached while the folder was missing. Creating a worktree or reading files in the recovered project then fails on a root that is not there. * fix(ssh): register the root after re-cloning a saved project's folder `addRemoteRepoFromPath` returns an existing project before it notifies `session.registerRoot`, which was unreachable before the saved-project check could re-clone into a project's own path. The relay therefore never learned the folder came back, so sessions under it stayed unregistered. Notify from the clone, mirroring the folder-to-git upgrade beside it. * fix(repos): say which reason refused a clone over a saved project One message covered both "that project holds a different repository" and "Orca has no record of what that project held", which need different things from the user. Name the recorded URL when there is one, and say plainly that there is no record when there isn't — the second is the case a project predating remote-identity enrichment lands in, and the user cannot act on it without being told why. Also drops the `upstream` remote-name check into a named binding: `origin` is the only stored remote that names a project's own repo, because `Repo.upstream` and the GitHub avatar derived from it both name the repo a fork came from. * fix(repos): bound saved-clone probes and prove empty clone completion * fix(repos): keep clone validation host-scoped and locale independent --------- Co-authored-by: Neil <neil@stably.ai> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
8507111d87 |
Document Apple Git ref recovery retirement condition
Name the affected Apple Git-154 (2.39.5) build and document when corruption recovery can be retired while retaining current-Git disambiguation. Comments only; lint, formatting, and review pass. |
||
|
|
1de3aa405f |
Fix truncated Unicode branch names in base ref search
Preserve complete branch selectors when Git truncates or disambiguates short names, and keep slash-named local branches separate from remote-tracking refs through worktree creation and reuse. Keep native/SSH namespace boundaries and mixed-version client capability gates intact. Accept Git-valid dotted components and cover collisions with configured and orphan tracking refs. Adapted from @nishino-tsukasa's #19540. Independently reviewed in seven adversarial rounds; 235 focused tests and 77 affected-Apple-Git tests pass, along with typecheck, quality checks, and the desktop build. Correct the inherited file-explorer cache-scan expectation while retaining its exact constant-work bound. Correct the inherited release-parser compatibility assertion to match the pinned test dependency; all 121 cross-version tests pass locally. Fixes #19515 Co-authored-by: kino <nishinotsukasavirgo@gmail.com> |
||
|
|
ca4e239861 | Remove low-value test inventories and duplicate fuzz oracles (#25791) | ||
|
|
d5bb69d348 |
Cut CI time in store, Git contention and readiness tests (#25147)
* Check store retention boundaries with a faster independent oracle * Replace CI diagnostic sleeps with gated contention and scoped transcript clocks |
||
|
|
e5ba5975df |
Recover working local forge CLIs behind broken PATH launchers
Recover a working local forge CLI when an earlier PATH launcher is broken. Bound executable probes and reuse the verified selection for native operations without replaying authentication or user requests. Fixes #22975 Co-authored-by: Aashish <145881415+aashish254@users.noreply.github.com> |
||
|
|
62451920ed |
fix(git): preserve SSH review context and worktree ownership (#24945)
* fix(git): preserve SSH arguments and guard background writes * test(terminal): settle fish fixture startup readiness * fix(git): preserve bare UNC SSH paths * fix(git): unescape shell operators in Windows SSH paths * test(runtime): settle removal writes before fixture cleanup * test(git): skip optional OpenSSH probe when unavailable * perf(git): skip equal-tip reads and bound relay discovery * test(processes): ratchet the removed relay Git spawn * test(shells): wait for initial zsh output before sending input * Fix SSH review context and recover Git maintenance cleanup safely * Keep relay Git compatibility fixtures outside shared client projects * Fence superseded maintenance and preserve mixed-version session search * test: model Git child termination and search catalogs * test: retain catalog authority over history flags |
||
|
|
a40e5012f5 |
perf(git): reuse queries and stop canceled catalog scans (#24923)
* perf(git): reuse queries and stop canceled catalog scans * refactor(git): check relay filesystem error codes |
||
|
|
328caa2160 |
fix(git): reduce queries and preserve data across execution hosts (#24602)
* fix(git): reduce queries and preserve data across execution hosts * fix(ci): exercise pinned Git and serialize mobile dependency entrypoints * fix(relay): preserve fresh diff retries after hung shared reads * test(git): wait for fetch barrier before canceling preparation * fix(i18n): describe index-preserving discard in every locale * fix(git): retain clone diagnostics and allow WSL policy startup * test(git): refresh default-base and branch-safety fixtures |
||
|
|
b5869eeaae |
fix(worktrees): a worktree delete git fails partway stays listed and can be retried (#23952)
* fix(worktrees): delete removed checkouts in git, not in Orca's file pool Local worktree removal renamed the checkout into a sibling trash root and deleted it in the background with a recursive fs.rm in the main process. That queued one request per entry on libuv's shared 4-thread file pool, so for minutes every other async fs call in the main process (the agent-session store behind chat sends, file explorer reads) waited behind the delete. `git worktree remove` now deletes the checkout inline in git's own process again, so the card stays in its Deleting state for the length of the delete while Orca's file pool stays free. No timeout applies to the call, so a large delete is never killed halfway. If git reports success but the path still exists (Git for Windows leaves junctions and their parent directories in place), the leftover is deleted with the existing removeHostTree; WSL checkouts stay with the distro. Nothing creates trash any more: the scheduling queue, rename/restore helpers and the trash_rename span are gone. The startup sweep stays to drain entries older releases left behind, and now removes each emptied trash root so the obligation ends. * fix(worktrees): let Git delete Windows checkouts with long paths enabled Removal now always runs Git's own recursive delete, and worktree creation checks out with core.longpaths on Windows, so a deep checkout Orca created could fail to delete with "Filename too long" (#6433). The Windows recovery then finishes the delete but keeps the branch. Pass the same command-scoped core.longpaths option to `git worktree remove` so Git can delete what it created. Also point the CI shard timing entry at the renamed real-git removal suite. * fix(worktrees): keep an inherited GIT_ASK_YESNO out of the worktree delete Git for Windows asks $GIT_ASK_YESNO whether to retry when a file stays locked during a recursive delete. Orca's git env inherits the user's environment, so an inherited value would run an arbitrary prompt program in the middle of a removal. Drop it for the removal call only. * perf(worktrees): run worktree deletes under their own limit, outside git admission `git worktree remove` now deletes the whole checkout in Git's own process, which takes 20-35 s on a large tree. It took a general git admission slot at status tier for that whole time, and that cap is as small as two slots on a machine with six or fewer cores, so two deletes blocked every status read. Deletes now skip general admission and queue under their own limit of two per host instead: two concurrent deletes already saturate one disk, and more only slow each other down. Leftover cleanup runs inside the same slot. * fix(worktrees): delete removed checkouts in the background and mark them removing Since the checkout is deleted by `git worktree remove` in Git's own process, a large delete takes 20-35 s. Answering the request only after that made web and mobile (30 s), paired desktop (60/180 s) and the CLI (60 s) report a failure for a delete that was still going, and mobile silently re-showed the row. The request now does everything that can refuse (lock, cleanliness, archive hook, watcher/terminal gate, terminal stop, shared-link unlink), records the removal in an in-memory table on the host and answers `removing: true`. The delete, branch cleanup and metadata purge run after it in the same order as before, and the watcher/terminal gate stays held until they finish. - Listings mark rows in the table `removing` for clients that advertise `worktree.background-removal.v1` (the desktop renderer, paired desktop and web), and leave them out for everyone else (older clients, mobile, the CLI), which already dropped the row when the request answered. - The outcome (removed, with any preserved branch, or the error) rides the existing worktrees-changed event as an optional field, sent after the row has left the table. - A repeat delete while Git runs joins it. A create at the same path or with the same branch is refused with "Cleanup is pending; try again shortly"; create's name search skips the path, so generated names move on. - Nothing is persisted: after a quit or crash Git still lists the checkout and it can be deleted again. WSL checkouts still delete inline. - `orca worktree rm` says the checkout is still being deleted. * fix(worktrees): keep the existing Deleting card until the host's Git finishes The host now answers a local worktree delete on acceptance and deletes in the background. The renderer keeps the existing delete state set until the host publishes how it ended: - The delete that asked waits for the outcome on the worktrees-changed event (local IPC or the paired runtime's client event), then runs the same teardown, preserved-branch toast and card error an inline delete did. If that event is lost to a dropped connection, a listing that shows the row gone after it was marked removing finishes the wait, and one that shows it back without the marker fails it. - Any other renderer (a reload, a paired desktop, web) sets the same delete state from the host's `removing` marker and clears it when the marker goes. A failure the host publishes lands on that card's existing error. - Web advertises `worktree.background-removal.v1` so the host sends it the marker; paired desktop does through the Electron capability list. No new component, style or state: the card reads the delete state it always did. A host that predates this answers when done without `removing`, and the renderer takes that as finished, as before. * test(worktrees): type the removal harness and projection for the node typecheck * fix(worktrees): don't fail a delete retry with an earlier attempt's buffered failure A background removal's outcome that reached this renderer with no waiter (another client's delete, a host-marked card, or one already settled from listings) was buffered for 60 s and consumed by the next delete of the same workspace, so retrying a failed delete failed at once with the old error while the host was deleting. Drop the buffered outcome before sending the request; only an outcome that arrives after it can belong to it. * fix(worktrees): let only a gap in host events settle a background delete from listings Git unlists the checkout before the host deletes the branch, cleans the push target and purges metadata, and the worktree-directory watcher refetches within 250 ms. The renderer read the missing row as a finished delete, so the waiter resolved without the preserved branch (no toast) and a failure in those last steps showed as success; the real outcome was then dropped. The listing fallback exists only for a lost outcome event, so it now applies only after this host's event stream had a gap: a new subscription or a replay after reconnect. * perf(worktrees): let a bulk delete start each same-repo checkout delete once the host accepts the last A bulk delete ran one worktree at a time per repo (#2259, for packed-refs and ref-lock races in branch cleanup). With Git now deleting each checkout for 20-35 s before the request settles, N worktrees in one repo took N times that. The renderer now queues same-repo deletes only until the host accepts each one; a parent still waits for its nested children to finish. The host serializes the branch cleanup step per repo itself, which also covers removals started by different clients. * test(worktrees): pin the host platform in the mocked removal suites so they pass on Windows Removal now passes -c core.longpaths=true on Windows, so the exact-argv assertions and command-keyed mocks never matched there (17 failures on a Windows host). Pin darwin as the add-worktree suites already do, and drive the one Windows-specific case through the same spy. * test(worktrees): type the blocked git remove result instead of a broad object The anti-slop static-analysis gate rejects `object` parameters. * test(worktrees): clear the changed-code quality gate in the removal suites Merge the duplicate node:fs import, build the mock child without a cast, read worktrees:list rows through one typed helper, and give the remaining casts a SAFETY line. * fix(worktrees): record each background delete durably and finish it after a quit or crash A quit mid-delete left git to finish the checkout on its own while the branch delete and metadata purge never ran; a crash left a normal-looking row. Each accepted local removal now writes a record beside the profile state before git starts, clears it on success or failure, and the host runs the same delete again for any record left at startup, re-deriving what remains from git and disk. An orderly quit stops the checkout delete without waiting for it. * test(worktrees): type the interrupted-removal assertions for the node typecheck * fix(worktrees): finish an interrupted delete that already removed the checkout's .git file Quit stops git worktree remove mid-delete, and Git deletes the checkout's .git file wherever it falls in directory order. Git then refuses the checkout ("validation failed ... .git does not exist") on every retry, so the startup finish failed and the row could never be deleted from Orca. A registered checkout this record owns that has lost its .git file now finishes like an unregistered one: leftover files, prune, then the branch. * fix(worktrees): let Git finish an interrupted delete, and never take a different checkout A quit or crash that stops `git worktree remove` after it deleted the checkout's .git file left a registered checkout Git refuses to remove. The previous fix deleted that leftover inside Orca's process, which is the bulk delete this change exists to avoid (and on Windows the leftover can be most of the checkout). The startup finish now rewrites the missing .git file from Git's own admin entry for that path and lets `git worktree remove --force` delete it. `git worktree repair` is not used: it also re-points every other registered path, including a checkout another repository now owns there. Orca deletes the leftover itself only when no admin entry claims the path. The startup finish forces, so it now leaves the path alone when the checkout there is not the one recorded: a registered worktree on a different branch or head, or a `.git` at a path Git already unregistered. The record is dropped and the card shows why. The record write before Git starts is now bounded (2 s, logged when exceeded) so a stalled disk cannot hold the delete, and the outcome is published before the record's clear reaches disk. * test(worktrees): compare worktree paths by value and tear down with Windows lock retries Git prints forward slashes in `git worktree list` on Windows, so the real-Git removal suites never found a joined path there: positive checks failed and negative ones passed without proving anything. They now compare Git's parsed rows by value. Teardown uses the shared retrying removeTree, since Windows can hold the deleted checkout busy for a moment after Git exits. Adds a relative-path worktree case for the .git restore (skipped before Git 2.48). * fix(worktrees): reply to a worktree delete when it has finished, not on a broadcast event A current client's delete request now waits for the host's background delete and gets its real result (removed, a preserved branch, or the error) as the reply, the way it did before the delete moved off the request. A request that arrives while the delete runs joins it and gets the same result. Every other view keeps reading the host's `removing` marker: the row leaving means the delete finished, and the row listed again without the marker shows "The delete did not finish. Try again." on a card that view had marked Deleting. A request whose reply is lost (a timeout or a dropped connection) settles the same way from a fresh listing instead of reporting a failure. Clients without the background-removal capability (mobile, the CLI, older desktops) are still answered on acceptance and have rows under removal left out of their listings. This removes the outcome on worktreesChanged and everything it needed: the renderer's outcome waiters, early-outcome buffer and TTL, per-host event-gap generations, the request pre-registration, and the accept callback bulk delete used. Bulk delete runs same-repo deletes in parallel only on this machine, whose host serializes branch cleanup per repo; SSH and paired hosts stay serialized. * test(worktrees): type the pending-removal host id in the background-removal suite * fix(worktrees): answer a delete request even when a concurrent removal of the same worktree replaced its record The desktop app's removal and the runtime removal (CLI, paired clients) coalesce separately, so both can be accepted for one worktree. The second replaced the first's record, and the first delete then finished without resolving the request waiting on it, leaving the desktop card on Deleting indefinitely. Each delete now settles the request it was started for. * fix(worktrees): run same-repo removal archive hooks and teardown one at a time on the host Local bulk delete now sends same-repo removals in parallel, so their archive hooks, terminal teardown and preflight ran at once; a hook that writes refs can race the repo's ref locks (#2259). The host now serializes each local removal up to acceptance per repo, for every client; Git's checkout delete still runs in parallel under the delete limit. * fix(runtime): keep waiting worktree deletes out of a host's foreground call slots worktree.rm now replies only after Git deletes the checkout (up to minutes), so on paired desktop and web each waiting delete held one of the host's 8 foreground call slots, and a bulk delete queued listing refreshes and every other foreground call behind it. Deletes now run in their own lane with the same bound; the 2-slot background lane stays for status polls. * fix(worktrees): join a same-worktree delete accepted while a removal waited its repo turn The desktop app and the runtime (CLI, paired clients, web) check for a running delete before they queue for the repo's acceptance turn. A delete of the same worktree from the other path, accepted while this one queued, was missed: this request re-ran the archive hook, stopped the terminals again and started a second `git worktree remove` on the directory Git was deleting. The queued acceptance now re-checks and joins the running delete. * fix(worktrees): fence a resumed delete's checkout from startup, and drop rows a listing read before the delete finished A delete a quit or crash interrupted took its terminal and file-watcher gate only when the resume job ran, after the first window was shown; session restore could open a shell or watcher inside the half-deleted checkout first, and on Windows that handle can fail the resumed git delete. Loading the records now fences each recorded path, and the resumed job takes the fence over in the same tick it takes its own gate. A listing that read git's registration before a delete finished, and replied after the removal record cleared, returned the row unmarked, so other views briefly showed "The delete did not finish". Listings now capture the pending removals before reading git and leave out a row whose delete finished successfully since; a row whose delete failed stays listed as before. * test(worktrees): keep git's auto-maintenance out of the real-git removal suite CI's Git 2.55 failed the file-pool test in teardown with ENOTEMPTY on the scratch repo's objects/pack after the test body passed: the 3,000-file commit's detached auto-maintenance was still writing a pack. The scratch repo now disables auto-maintenance and auto-gc. * fix(worktrees): one archive-hook approval covers a same-repo bulk delete again Local same-repo deletes now start together, so each queued its trust prompt with a state snapshot taken before the first prompt was answered; approving the first still showed the same prompt once per remaining worktree. The queued check now reads the store when its turn comes. * fix(worktrees): a delete Git fails partway stays listed with its error; Delete retries it `git worktree remove --force` drops the checkout's registration even when it cannot delete a file (root-owned files, `chflags uchg`, a read-only Windows directory). Orca lists workspaces from Git, so the row vanished after the error, leaving the checkout, the branch and Orca's metadata with no way to retry. - A background delete that fails with the checkout still on disk, unregistered, and still the removed checkout's own leftover keeps its durable removal record with the error (`failure`) instead of clearing it. Every other failure clears it as before. - Local listings (desktop list/list-all/detected, runtime list/ps/detected) add a row for each such record, carrying `removalError`, and for a pending removal whose checkout Git no longer lists (shown as removing). - Delete on that row (desktop IPC and runtime worktree.rm) runs the recorded removal again: terminal teardown, then the leftover, prune, branch and metadata, under the per-host delete limit. - The record ends on a successful retry, when the checkout is gone (listing or startup), when a different checkout takes the path, or on forget-local. Startup never retries a failed record. - The finish's unregistered-path rule accepts a `.git` file naming the admin entry Git removed (the leftover's own) and still refuses any other `.git`. The removal table and listing projection move out of the background removal module into worktree-removal-table.ts and worktree-removal-listing.ts. * fix(renderer): show a failed delete's host error on its card A row the host lists with removalError gets the existing delete-state error (no new element), cleared when the host stops listing it failed. A row this view marked Deleting that comes back failed, and a lost delete reply settled from the listing, report the host's error instead of the generic one. * test(worktrees): type the failed-removal listing and refresh mocks * fix(worktrees): a failed delete's retry never removes a checkout Git registers at the path again The retry replays the recorded choices (force, branch deletion) that were made for the unregistered leftover. If the user removed the leftover and `git worktree add`ed the same branch at the path, the new checkout matched the record's branch and head, so Delete force-removed it with its uncommitted files, skipping the normal delete's cleanliness check. The retry now refuses a registered checkout and lets the record go, so the next Delete takes the normal path. * fix(worktrees): a failed delete's record ends at startup once its repo is removed from Orca * fix(renderer): a failed delete's row offers Remove from Orca * test(worktrees): type the failed-removal IPC test's module mocks without casts * fix(worktrees): Delete picks retry or a normal delete from Git's current listing * fix(worktrees): a failed delete's retry checks the leftover again right before deleting it * fix(worktrees): Remove from Orca reaches paired clients and matches the failed row's own host * fix(worktrees): a failed delete's retry re-lists only its own repo's authorized roots * fix(worktrees): a second Delete joins a retry already running, and the startup finish keeps its last-resort delete * fix(renderer): a failed delete's card says it failed, and Delete keeps the error for its dialog * fix(renderer): a failed delete's dialog shows the host's error, and its card label keeps the full error a hover away * style(worktrees): import the removal result types in one statement * test(worktrees): a runtime listing right after a failed delete shows the failed row, not the cached scan * fix(worktrees): pass the runtime retry's PTY-stop waiver in the shape the waiver invariant pins * fix(worktrees): drop Remove from Orca from failed-delete rows A failed delete stays listed with its error and Delete retries it; the separate forget item, its dialog copy and forget's failed-record clearing are removed. The startup clear for repos no longer in Orca keeps matching the local copy only. * test(worktrees): wait for the dropped record's write before the failed-removal suite tears down |
||
|
|
de77c4b065 |
fix(worktrees): list a folder once when git reports it twice (#24357)
When git lists the same folder twice (a leftover worktree registration that points at the main checkout), Orca's runtime listing turned each line into its own worktree with the same id, so `orca worktree current`, `active` and `branch:` failed with selector_ambiguous, and paired clients saw a duplicate row. The runtime scan now keeps git's first row per folder, the rule the desktop sidebar already uses. Separately, for a bare or separate-git-dir repo added through a linked worktree, the scan no longer relabels the main row with that worktree's folder (it relabels only when the folder's git dir is the common git dir), so the worktree keeps its own row and branch in the CLI and the sidebar. No extra git command runs. Part of #23631: the "Profile state writer command timed out" toast in that issue has a separate cause. |
||
|
|
d6d2795da5 |
chore(worktree): include create timing and spare outcome in the workspace create events (#24483)
* chore(worktree): include create timing and spare outcome in the workspace-created event The workspace_created and workspace_create_failed events gain optional, numbers-and-enums-only fields built from what the create already measured: total and per-phase durations, the prepared-checkout hit/miss and miss reason, the execution host (local/WSL/SSH), a worktree count bucket, how many other creates were in flight, whether the repo has a post-checkout hook (file existence only, probed after the create returns), and for a failure the phase it died in plus elapsed time. No new git process runs; consent and opt-out are unchanged. * fix(worktree): attribute failed_phase by error, label WSL-path repos, skip the hook check with telemetry off - failed_phase now names the outermost timed step the thrown error (or its cause) left, so a caught failure or a concurrent sibling step can no longer be misattributed; the old-relay SSH error keeps its cause so it still reads as git_worktree_add. - execution_host follows the same rule Git routing uses, so a \\wsl.localhost repo reads wsl. - The post-checkout hook check does not read the repo when telemetry is disabled. - Privacy page mentions the miss reason code and the failed step. * test(worktree): pin the old-relay SSH add error to git_worktree_add through its cause * fix(worktree): name the create event field sets for their role, and type the old-relay test's caught error * fix(worktree): send create events from runtime creates and record what the spare checkout did Runtime creates (CLI, agents, phone app, paired clients, orchestration, server automations) reuse prepared checkouts like the app's own creates, but recorded no timing and sent no events. Both entry points now start one shared sender (workspace-create-telemetry.ts), so every create sends exactly one event with the same fields, plus create_entry_point (app | runtime). Spare-checkout fields: - concurrent_preparations: peak prepared-checkout builds and background discards running during the create, excluding the one it used; the window closes before the create's own re-arm starts. - prepared_checkout_claim / prepared_checkout_discard phases, so on a miss git_worktree_add minus the prepared_checkout_* phases is the plain checkout. - prepared_checkout_reset (none | base_moved | retargeted) replaces the retargeted flag; prepared_checkout_origin (prefetch | rearm) on hits. - workspace_create_failed carries the spare outcome and its wait. - repo_index_size_bucket from one stat of .git/index in the existing post-create probe (telemetry on, local/WSL only, 2 s cap). * fix(worktree): add spare build and idle time, the re-arm prefetch origin, and a tracked-file count - prepared_checkout_build_ms / prepared_checkout_idle_ms on hits: from arming the spare to ready, and how long it sat ready before the claim (0 when the create waited). readyAt is recorded in the pool's existing ready handler. - prepared_checkout_origin gains rearm_then_prefetch: an automatic re-arm that the dialog prefetch then asked for too, so rearm means the re-arm alone. - repo_file_count_bucket replaces the index byte size: the entry count from the 12-byte index header, which is the same in every index version; left out for a split or sparse index. - The shared sender never lets a failed send change the create's result or error; it logs instead and still ends the create's concurrency membership. - Tests pin the runtime SSH create's timing hand-off and the throwing-send cases on both entry points. * fix(worktree): leave out the file count under any sparse checkout and time spare builds monotonically - The repo probe also reads .git/config.worktree, where git sparse-checkout --sparse-index writes index.sparse, and omits the file count whenever sparse checkout or a sparse index is on in either file, with Git's boolean spellings. core.hooksPath there is honoured too. - prepared_checkout_build_ms / _idle_ms use performance.now(), like every other duration; the build is timed from its own start (buildStartedAt). - The origin field comment names all three values. * fix(worktree): keep the spare's build time on its first build and count worktrees by lock reason - prepared_checkout_build_ms runs from the first build's start (including any wait for the base fetch it is built on) to its first ready; a later tip refresh no longer restarts it, though it still counts as new preparation work. prepared_checkout_idle_ms runs from the latest ready (build or refresh) to the claim. - The worktree count reads each .git/worktrees entry's locked file and leaves out entries whose lock reason names an Orca preparation, the way the listing does, instead of subtracting this process's spares. That covers spares from other processes, crash leftovers and spares being discarded, and cannot run one low while a spare's admin dir does not exist yet. It has its own 1.5 s cap inside the probe. |
||
|
|
34ae0933e4 |
fix(worktrees): keep creation fast in large repositories (#24346)
* fix(worktrees): remove repeated scans and keep prepared checkouts fresh * fix(worktrees): reclaim unlocked fallback preparations safely * refactor(worktrees): simplify creation ownership and idle maintenance * fix(git): keep ref maintenance armed after an index-only pass An idle attempt that found the pack index due but refs still cooling down returned without rescheduling, so loose refs from the arming fetch waited for the next write instead of the ref cooldown. |
||
|
|
12b8ef8c0b |
fix(worktree): update local main safely, once per branch, alongside the checkout (#23698)
* fix(worktree): retry local main refresh through git lock contention and skip false alarms * fix(worktree): overlap the local main refresh with the checkout and run one refresh per repo at a time * fix(worktree): skip the local base refresh when the create makes that branch itself Creating a workspace named feature-x from origin/feature-x runs `worktree add -b feature-x`, which now overlaps the refresh. The refresh's drift probe could see refs/heads/feature-x missing and its presence probe then see it (the add just wrote it), which reported "not fast-forward" and showed a sticky "Local feature-x was not refreshed" warning. `-b` refuses an existing branch, so there is nothing to refresh in that case: skip it on the local, prepared-checkout and SSH create paths. The SSH overlap tests move to their own file so the existing suite stays under the line limit. * fix(worktree): say plainly what happens after the local base refresh queue wait expires * test(worktree): prove SSH local base refreshes of one repo run one at a time * test(worktree): drop type assertions from the SSH refresh overlap test mocks * fix(worktree): fast-forward local main with one host-owned merge --ff-only per branch Moves the whole local base refresh into one shared routine that runs on the execution host (main process for local and WSL repos, the relay for SSH), so the app no longer keeps a second copy of the checks, queue and retry. A checked-out branch now moves with merge --ff-only (hooks, auto-gc and autostash off) instead of status-then-reset --hard, which silently overwrote an untracked file the new commit adds and could discard an edit or a commit made after the check. A free branch moves with a compare-and-swap update-ref that writes a reflog message. Status reads no longer take index.lock. Creates of one branch share one run plus at most one trailing run; a create waits at most 30 s and never starts a competing mutation. The failure toast is keyed by repo and branch because every create that joined a run reports the same fact. * fix(worktree): fast-forward local main even when the repo requires signed merges With merge.verifySignatures=true, the owner-checkout fast-forward refused an unsigned origin/main tip, so every create warned "Local main was not refreshed" where the old reset moved main. The new workspace is already created from that same unsigned commit, and a branch that is not checked out moves without a signature check, so the refusal protected nothing. Turn the setting off for this one merge, like the hooks, gc and autostash overrides. * fix(worktree): clear git read caches when a shared local main update lands late The update of local main can finish after a create stopped waiting for it, so the shared run now invalidates git read caches itself. The index.lock real-git test also no longer reads the developer's global git config. * fix(worktree): never overwrite an ignored file when fast-forwarding local main A plain `git merge --ff-only` silently replaces an ignored file (for example a local `.env`) at a path the new commit starts tracking. Pass `--no-overwrite-ignore` so git refuses instead and the create reports the checkout as having local changes. Supported on the fast-forward path since well before Git 2.25. Also make the relay test for one-refresh-per-branch hold the first merge until the second request has reached the relay, so it fails without the coalescing. * fix(worktree): keep the local main update a plain fast-forward whatever the user's merge settings say A per-branch mergeOptions such as '-s ours' or '--squash', or pull.twohead=ours, made the update create a merge commit that dropped upstream, or stage upstream without moving main, while reporting success. The command now clears the branch's mergeOptions and passes the strategy and signature choice on the command line, which beats any config. After the move Orca confirms local main is exactly the target before reporting it updated. The exact command also runs in the Git 2.25 compatibility suite. * fix(worktree): make the Git 2.25 fast-forward contract pass in CI and rerun on every change to it The new real-Git contract for the local main fast-forward wrote a post-merge hook into .git/hooks, which does not exist when the repo is created by the uninstalled Git 2.25.5 build CI uses (no templates), so the Git compatibility check failed. Create the directory first. The Git compatibility check also did not run when only the fast-forward module changed, so a later edit to its merge arguments (for example a flag Git 2.25 lacks) would skip the one check that tests them. Add the module to the check's paths. * fix(worktree): answer every create from a local main update toward its own base Creates from different remotes' main (origin/main and upstream/main) shared one queued update per repo and branch, which ran only the latest caller's target: a create could get no result for its own base, or a false "not refreshed" warning computed for another remote's main. The per-branch runner now queues one run per distinct target, still one at a time per branch, and only callers toward the same target share a queued run. Applied in the app and the relay. * test(worktree): record the third create's result in the mixed-remote burst tests and update the toast id rationale * chore(worktree): correct the toast id rationale |
||
|
|
6f2a7d05c9 |
fix(worktrees): let git delete removed checkouts so chat sends never wait behind them (#23837)
* fix(worktrees): delete removed checkouts in git, not in Orca's file pool Local worktree removal renamed the checkout into a sibling trash root and deleted it in the background with a recursive fs.rm in the main process. That queued one request per entry on libuv's shared 4-thread file pool, so for minutes every other async fs call in the main process (the agent-session store behind chat sends, file explorer reads) waited behind the delete. `git worktree remove` now deletes the checkout inline in git's own process again, so the card stays in its Deleting state for the length of the delete while Orca's file pool stays free. No timeout applies to the call, so a large delete is never killed halfway. If git reports success but the path still exists (Git for Windows leaves junctions and their parent directories in place), the leftover is deleted with the existing removeHostTree; WSL checkouts stay with the distro. Nothing creates trash any more: the scheduling queue, rename/restore helpers and the trash_rename span are gone. The startup sweep stays to drain entries older releases left behind, and now removes each emptied trash root so the obligation ends. * fix(worktrees): let Git delete Windows checkouts with long paths enabled Removal now always runs Git's own recursive delete, and worktree creation checks out with core.longpaths on Windows, so a deep checkout Orca created could fail to delete with "Filename too long" (#6433). The Windows recovery then finishes the delete but keeps the branch. Pass the same command-scoped core.longpaths option to `git worktree remove` so Git can delete what it created. Also point the CI shard timing entry at the renamed real-git removal suite. * fix(worktrees): keep an inherited GIT_ASK_YESNO out of the worktree delete Git for Windows asks $GIT_ASK_YESNO whether to retry when a file stays locked during a recursive delete. Orca's git env inherits the user's environment, so an inherited value would run an arbitrary prompt program in the middle of a removal. Drop it for the removal call only. * perf(worktrees): run worktree deletes under their own limit, outside git admission `git worktree remove` now deletes the whole checkout in Git's own process, which takes 20-35 s on a large tree. It took a general git admission slot at status tier for that whole time, and that cap is as small as two slots on a machine with six or fewer cores, so two deletes blocked every status read. Deletes now skip general admission and queue under their own limit of two per host instead: two concurrent deletes already saturate one disk, and more only slow each other down. Leftover cleanup runs inside the same slot. * fix(worktrees): delete removed checkouts in the background and mark them removing Since the checkout is deleted by `git worktree remove` in Git's own process, a large delete takes 20-35 s. Answering the request only after that made web and mobile (30 s), paired desktop (60/180 s) and the CLI (60 s) report a failure for a delete that was still going, and mobile silently re-showed the row. The request now does everything that can refuse (lock, cleanliness, archive hook, watcher/terminal gate, terminal stop, shared-link unlink), records the removal in an in-memory table on the host and answers `removing: true`. The delete, branch cleanup and metadata purge run after it in the same order as before, and the watcher/terminal gate stays held until they finish. - Listings mark rows in the table `removing` for clients that advertise `worktree.background-removal.v1` (the desktop renderer, paired desktop and web), and leave them out for everyone else (older clients, mobile, the CLI), which already dropped the row when the request answered. - The outcome (removed, with any preserved branch, or the error) rides the existing worktrees-changed event as an optional field, sent after the row has left the table. - A repeat delete while Git runs joins it. A create at the same path or with the same branch is refused with "Cleanup is pending; try again shortly"; create's name search skips the path, so generated names move on. - Nothing is persisted: after a quit or crash Git still lists the checkout and it can be deleted again. WSL checkouts still delete inline. - `orca worktree rm` says the checkout is still being deleted. * fix(worktrees): keep the existing Deleting card until the host's Git finishes The host now answers a local worktree delete on acceptance and deletes in the background. The renderer keeps the existing delete state set until the host publishes how it ended: - The delete that asked waits for the outcome on the worktrees-changed event (local IPC or the paired runtime's client event), then runs the same teardown, preserved-branch toast and card error an inline delete did. If that event is lost to a dropped connection, a listing that shows the row gone after it was marked removing finishes the wait, and one that shows it back without the marker fails it. - Any other renderer (a reload, a paired desktop, web) sets the same delete state from the host's `removing` marker and clears it when the marker goes. A failure the host publishes lands on that card's existing error. - Web advertises `worktree.background-removal.v1` so the host sends it the marker; paired desktop does through the Electron capability list. No new component, style or state: the card reads the delete state it always did. A host that predates this answers when done without `removing`, and the renderer takes that as finished, as before. * test(worktrees): type the removal harness and projection for the node typecheck * fix(worktrees): don't fail a delete retry with an earlier attempt's buffered failure A background removal's outcome that reached this renderer with no waiter (another client's delete, a host-marked card, or one already settled from listings) was buffered for 60 s and consumed by the next delete of the same workspace, so retrying a failed delete failed at once with the old error while the host was deleting. Drop the buffered outcome before sending the request; only an outcome that arrives after it can belong to it. * fix(worktrees): let only a gap in host events settle a background delete from listings Git unlists the checkout before the host deletes the branch, cleans the push target and purges metadata, and the worktree-directory watcher refetches within 250 ms. The renderer read the missing row as a finished delete, so the waiter resolved without the preserved branch (no toast) and a failure in those last steps showed as success; the real outcome was then dropped. The listing fallback exists only for a lost outcome event, so it now applies only after this host's event stream had a gap: a new subscription or a replay after reconnect. * perf(worktrees): let a bulk delete start each same-repo checkout delete once the host accepts the last A bulk delete ran one worktree at a time per repo (#2259, for packed-refs and ref-lock races in branch cleanup). With Git now deleting each checkout for 20-35 s before the request settles, N worktrees in one repo took N times that. The renderer now queues same-repo deletes only until the host accepts each one; a parent still waits for its nested children to finish. The host serializes the branch cleanup step per repo itself, which also covers removals started by different clients. * test(worktrees): pin the host platform in the mocked removal suites so they pass on Windows Removal now passes -c core.longpaths=true on Windows, so the exact-argv assertions and command-keyed mocks never matched there (17 failures on a Windows host). Pin darwin as the add-worktree suites already do, and drive the one Windows-specific case through the same spy. * test(worktrees): type the blocked git remove result instead of a broad object The anti-slop static-analysis gate rejects `object` parameters. * test(worktrees): clear the changed-code quality gate in the removal suites Merge the duplicate node:fs import, build the mock child without a cast, read worktrees:list rows through one typed helper, and give the remaining casts a SAFETY line. * fix(worktrees): record each background delete durably and finish it after a quit or crash A quit mid-delete left git to finish the checkout on its own while the branch delete and metadata purge never ran; a crash left a normal-looking row. Each accepted local removal now writes a record beside the profile state before git starts, clears it on success or failure, and the host runs the same delete again for any record left at startup, re-deriving what remains from git and disk. An orderly quit stops the checkout delete without waiting for it. * test(worktrees): type the interrupted-removal assertions for the node typecheck * fix(worktrees): finish an interrupted delete that already removed the checkout's .git file Quit stops git worktree remove mid-delete, and Git deletes the checkout's .git file wherever it falls in directory order. Git then refuses the checkout ("validation failed ... .git does not exist") on every retry, so the startup finish failed and the row could never be deleted from Orca. A registered checkout this record owns that has lost its .git file now finishes like an unregistered one: leftover files, prune, then the branch. * fix(worktrees): let Git finish an interrupted delete, and never take a different checkout A quit or crash that stops `git worktree remove` after it deleted the checkout's .git file left a registered checkout Git refuses to remove. The previous fix deleted that leftover inside Orca's process, which is the bulk delete this change exists to avoid (and on Windows the leftover can be most of the checkout). The startup finish now rewrites the missing .git file from Git's own admin entry for that path and lets `git worktree remove --force` delete it. `git worktree repair` is not used: it also re-points every other registered path, including a checkout another repository now owns there. Orca deletes the leftover itself only when no admin entry claims the path. The startup finish forces, so it now leaves the path alone when the checkout there is not the one recorded: a registered worktree on a different branch or head, or a `.git` at a path Git already unregistered. The record is dropped and the card shows why. The record write before Git starts is now bounded (2 s, logged when exceeded) so a stalled disk cannot hold the delete, and the outcome is published before the record's clear reaches disk. * test(worktrees): compare worktree paths by value and tear down with Windows lock retries Git prints forward slashes in `git worktree list` on Windows, so the real-Git removal suites never found a joined path there: positive checks failed and negative ones passed without proving anything. They now compare Git's parsed rows by value. Teardown uses the shared retrying removeTree, since Windows can hold the deleted checkout busy for a moment after Git exits. Adds a relative-path worktree case for the .git restore (skipped before Git 2.48). * fix(worktrees): reply to a worktree delete when it has finished, not on a broadcast event A current client's delete request now waits for the host's background delete and gets its real result (removed, a preserved branch, or the error) as the reply, the way it did before the delete moved off the request. A request that arrives while the delete runs joins it and gets the same result. Every other view keeps reading the host's `removing` marker: the row leaving means the delete finished, and the row listed again without the marker shows "The delete did not finish. Try again." on a card that view had marked Deleting. A request whose reply is lost (a timeout or a dropped connection) settles the same way from a fresh listing instead of reporting a failure. Clients without the background-removal capability (mobile, the CLI, older desktops) are still answered on acceptance and have rows under removal left out of their listings. This removes the outcome on worktreesChanged and everything it needed: the renderer's outcome waiters, early-outcome buffer and TTL, per-host event-gap generations, the request pre-registration, and the accept callback bulk delete used. Bulk delete runs same-repo deletes in parallel only on this machine, whose host serializes branch cleanup per repo; SSH and paired hosts stay serialized. * test(worktrees): type the pending-removal host id in the background-removal suite * fix(worktrees): answer a delete request even when a concurrent removal of the same worktree replaced its record The desktop app's removal and the runtime removal (CLI, paired clients) coalesce separately, so both can be accepted for one worktree. The second replaced the first's record, and the first delete then finished without resolving the request waiting on it, leaving the desktop card on Deleting indefinitely. Each delete now settles the request it was started for. * fix(worktrees): run same-repo removal archive hooks and teardown one at a time on the host Local bulk delete now sends same-repo removals in parallel, so their archive hooks, terminal teardown and preflight ran at once; a hook that writes refs can race the repo's ref locks (#2259). The host now serializes each local removal up to acceptance per repo, for every client; Git's checkout delete still runs in parallel under the delete limit. * fix(runtime): keep waiting worktree deletes out of a host's foreground call slots worktree.rm now replies only after Git deletes the checkout (up to minutes), so on paired desktop and web each waiting delete held one of the host's 8 foreground call slots, and a bulk delete queued listing refreshes and every other foreground call behind it. Deletes now run in their own lane with the same bound; the 2-slot background lane stays for status polls. * fix(worktrees): join a same-worktree delete accepted while a removal waited its repo turn The desktop app and the runtime (CLI, paired clients, web) check for a running delete before they queue for the repo's acceptance turn. A delete of the same worktree from the other path, accepted while this one queued, was missed: this request re-ran the archive hook, stopped the terminals again and started a second `git worktree remove` on the directory Git was deleting. The queued acceptance now re-checks and joins the running delete. * fix(worktrees): fence a resumed delete's checkout from startup, and drop rows a listing read before the delete finished A delete a quit or crash interrupted took its terminal and file-watcher gate only when the resume job ran, after the first window was shown; session restore could open a shell or watcher inside the half-deleted checkout first, and on Windows that handle can fail the resumed git delete. Loading the records now fences each recorded path, and the resumed job takes the fence over in the same tick it takes its own gate. A listing that read git's registration before a delete finished, and replied after the removal record cleared, returned the row unmarked, so other views briefly showed "The delete did not finish". Listings now capture the pending removals before reading git and leave out a row whose delete finished successfully since; a row whose delete failed stays listed as before. * test(worktrees): keep git's auto-maintenance out of the real-git removal suite CI's Git 2.55 failed the file-pool test in teardown with ENOTEMPTY on the scratch repo's objects/pack after the test body passed: the 3,000-file commit's detached auto-maintenance was still writing a pack. The scratch repo now disables auto-maintenance and auto-gc. * fix(worktrees): one archive-hook approval covers a same-repo bulk delete again Local same-repo deletes now start together, so each queued its trust prompt with a state snapshot taken before the first prompt was answered; approving the first still showed the same prompt once per remaining worktree. The queued check now reads the store when its turn comes. |
||
|
|
a781a602a8 |
test: retire duplicate cases that replay an owner across a re-export or provider shim (#24114)
Resolves 208 candidate pairs where the same case title appears verbatim in two or more
files, produced by a repo-wide scan calibrated against a known positive. 46 case
declarations removed across 32 files, 798 lines gone. No file deleted whole, no
production code touched.
The headline result is the measurement, not the deletions: across the three buckets that
reported in detail, the signal ran roughly 86% false-positive (3/42, 9/42, and the rest).
It has good recall and poor precision, and it reorders a reading queue rather than
replacing one. Calibrating a detector against a known positive proves recall, not
precision.
What the deletions were:
- Duplicate invocation through a re-export shim. `native-chat-tool-summary.ts` is a
ten-line `export {...} from '../../../../shared/native-chat-tool-summary'`, and
`agent-status.ts:161` is `export { isExplicitAgentStatusFresh } from
'./pane-agent-evidence'`. Cases on the shim side were byte-equivalent to the owner's
with no rendering or transport hop.
- Provider-local replays of a shared helper: three `repository-ref` providers that are
each `createRemoteRefProbeCache(parseXRef)` and contribute nothing to transient
handling; two `local-pty` and `daemon/session` tables replaying
`shell-startup-output-scanner`, whose owner additionally checks every split point.
- A reader-side replay of store policy. `runtime-worktree-agent-rows-structured.test.ts`
asserted an attention-to-blocked mapping; the reader contains zero `attention` or
`blocked` tokens and copies `state` through. The mapping lives in
`structuredAgentSessionAgentStatus`. Consistent with
`docs/reference/agent-status-store.md`: readers keep only presentation policy.
- Constructor-only subclass duplication: the shared capability-cache case is covered by
`codex-app-server-capability-cache.test.ts`, whose ten cases include the identical
title plus all four risks `docs/reference/git-compatibility.md` names — first fallback,
later cached call, concurrent probes, per-host isolation.
- A private predicate duplicated at a real boundary, varying only a path passed straight
into the shared predicate.
Why most pairs were KEPT, because the false positives are principled rather than noise:
- Two independent execution hosts. `src/relay/git-handler-*` and `src/main/git/*` are
separate Git implementations that cannot import each other and hold separate capability
caches, exactly as the compatibility doc requires; the repo already ships
`status-branch-line-total-relay-parity.test.ts` to pin the duality deliberately. Neither
side's argv, timeout or cache regression is visible to the other.
- Deliberately duplicated production siblings: Codex vs Claude (different account fields,
different CLIs, different wire protocols), gitea vs bitbucket (`/pulls/42` vs
`/pullrequests/42`), gl-utils vs gh-utils (separate in-flight maps). Same contract
shape, different implementations — an identical title is the correct naming.
- Shared-predicate consumers: one side tests the predicate, the other tests a caller's
wiring to it. A caller that forgot to call the predicate passes the shared test.
In a codebase with intentional provider and host symmetry, identical test titles are
expected, and the signal cannot distinguish "copied" from "parallel by design" because
both produce the same prose. Only reading both bodies separates them.
Verified: 6,968 desktop test files pass; the three modified mobile files pass (39 cases);
`check-reliability-gates.mjs` 140 gates; nothing under
`mobile/src/test-support/rpc-recording/` or `mobile/rpc-foundation/goldens/` touched.
62 local failures across 12 files were each accounted for and none is caused by this
change: `browser-manager-tab-identity`, `browser-manager-viewport-ownership`,
`session-scanner-codex-workers` and `managed-hook-script-refresh` all fail identically in
a pristine `origin/main` worktree; five `mobile-web-app-*-render` tests need Playwright
browsers this machine lacks; `structured-agent-session-restart-ownership` and
`ssh-remote-commands` pass in isolation and fail only under concurrent load.
|
||
|
|
b7209b5ae9 |
perf(git): relist only the repo whose worktrees changed, and stop blocking main on sync git (#23998)
* perf(git): stop blocking main on the open-on-remote git cascade
`getRemoteFileUrl` ran up to 6 sequential `gitExecFileSync` calls on the Electron
main thread — `remote get-url`, then `getDefaultBaseRef`'s `symbolic-ref` plus up
to four `rev-parse --verify` probes — each with its own 15s timeout and no yield
between them.
A complete async twin already existed (`getDefaultBaseRefAsync` ->
`resolveDefaultBaseRefViaExec`, sharing DEFAULT_BASE_REF_PROBES), so the sync
cascade is deleted rather than converted. `getRemoteUrl`, `getRemoteFileUrl` and
`getRemoteCommitUrl` become async; all four downstream callers were already async
(`filesystem-git-url-handlers` inside `ipcMain.handle`, `runtime-git-diff-commands`
async methods) and the provider contract already typed both wrappers
`Promise<string | null>`, so no new async plumbing was needed.
Removes 3 of the 10 `gitExecFileSync` sites and the confusing name collision with
the unrelated async `getDefaultBaseRef` in hosted-review-creation-git-state.
The base-ref regression tests keep their coverage, repointed at the public async
`getBaseRefDefault`.
* perf(git): resolve the repo root in one sync spawn instead of two
getGitRepoRoot ran `rev-parse --is-inside-work-tree` and then `rev-parse
--show-toplevel` as separate blocking spawns. Each sync git call holds the main
thread for up to its whole 15s timeout, so the spawn count is the cost — and this
function is called twice per "Add Project" on a linked worktree, once directly and
once through getLinkedWorktreeMainRepoRoot's self-recursion.
Combined into one invocation. Safe only here: in a bare repo the combined form
exits non-zero, and both that throw and the plain `false` already land on the same
marker-scan fallback. probeGitRepo deliberately does NOT combine — it has to read
`false` cleanly to go on and detect a bare repo, which the combined form's exit 128
would misread as indeterminate.
* perf(git): rebuild only the repos whose authorized roots actually changed
One worktree create called `invalidateAuthorizedRootsCache()`, which dirties every
registered owner. The next authorization-requiring IPC then rebuilt by listing EVERY
repo — and the rebuild never consulted `dirty` when choosing what to list, so `dirty`
gated only whether a rebuild ran, not its scope. At 58 repos that is 58
`git worktree list` spawns, roughly ten seconds of git wall-clock through an
admission budget of four, to rediscover roots one repo changed.
Both halves were needed; scoping the invalidation alone changed nothing.
- `markAuthorizedRootsOwnerDirty` dirties a single owner, reusing the per-owner
primitives `registerWorktreeRootsForRepo` already used. It leaves `baseRevision`
and the per-repo revision map alone — that pair is the global side-effect-token
fence, and bumping it would retire in-flight tokens for untouched repos.
- `rebuildAuthorizedRootsCache(store, onlyDirty)` re-lists only owners that are
dirty, have no listing yet, or still hold recovered roots (those are retired by
comparison against a fresh listing, so skipping them would strand them as
authorized). Only `ensureAuthorizedRootsCache` passes `onlyDirty`; an explicit
rebuild keeps re-listing everything because callers use it to force a refresh —
`filesystem-auth.test.ts` pins that contract.
`invalidateAuthorizedRootsCacheForRepo` wraps the primitive and falls back to the
global form for an unknown owner or a missing store, rather than silently skipping an
invalidation and leaving a stale allowlist. Applied to the worktree-create path.
Changes that can alter the owner SET (store swap, host/WSL re-routing, nested-repo
import, folder->git upgrade) stay global. Removal paths are not converted yet.
The allowlist contents are unchanged and the failure direction is a false denial
rather than a false allow. The relist predicate is split into its own module so it is
testable alone and the cache file stays inside its line budget without a suppression.
* test(perf): measure what git orchestration actually costs the main thread
The existing churn probe (ORCA_MAIN_THREAD_DIAGNOSTICS=1) reported spawn-initiation
cost for git/gh/glab only — its 7 call sites all sit inside git/command-runner — so
it was blind to `spawnProcess`/`runProcess`, the repo's own mandated wrapper, and to
the blocking `execFileSync('ps')` per PTY resize. That understated total churn across
115 main call sites.
- `spawn-observer.ts`: a settable seam, since shared code cannot import src/main.
Unregistered in the daemon/relay/CLI, where it costs one boolean check.
- `spawnProcess` brackets `nodeSpawn` and reports; exec-file-capture's own report is
removed because it routes through runProcess and would double-count.
- `posix-pty-foreground-group` now reports its full blocking duration. Note this
lands on the daemon, not main, whenever the daemon hosts the PTY.
- `ORCA_UNMINIFIED_MAIN=1` build flag, because a minified main bundle cannot
attribute CPU-profile self time to real function names. Defaults unchanged.
- `main-thread-git-cost.spec.ts` + `analyze-main-cpuprofile.mjs`: sweeps concurrency
against real registered repos, captures the churn lines and a V8 CPU profile of
main per phase.
What it found, which is why this is worth keeping: at the width-4 admission ceiling
(~90 git:status/s) main sees ZERO event-loop gaps over 50ms and a worst gap of 23ms,
and is 85% idle. Git orchestration does not stall the main thread. Of the cost it
does incur, spawn-init is 58%, parse 5%, stdout drain 4%.
* test(perf): name the inspector params type the anti-slop gate requires
The broad `object` parameter trips anti-slop(no-object-parameters); the only
Profiler call that passes params sends `{ interval }`.
|
||
|
|
fed1eca486 |
test: stop restating internal tuning constants, keep the ones that are contracts (#23950)
Removes ~74 assertions of the form `expect(SOME_CONSTANT).toBe(<literal>)` where the literal is an internal tuning value — a timeout, retry count, debounce interval, cache TTL, circuit-breaker window, Tailwind class string. Those cannot fail for any reason a user would notice: they fail only when someone deliberately changes the number, and then the test is simply updated. They are copies of the declaration. The same pattern is NOT junk when the exact value is observable outside this process, so those were deliberately kept: - terminal byte contracts: `\r`, `\x03` ETX, Kitty escapes, `\x1b[?1;2c`; - wire and capability values: `agent.launch.v2`, protocol 3 / min-compatible 2, daemon per-feature boundary versions (a daemon survives app updates, so those pin what an old field daemon may be trusted with), relay header tokens; - security invariants: the `127.0.0.1` bind default, an empty iframe `sandbox`; - values external processes read: exit code 78 (EX_CONFIG) and exit code 3 (systemd `RestartPreventExitStatus`), `ORCA_AGENT_SESSION_SPAWN_TOKEN`, `npx skills …` commands users paste, on-disk journal schema versions, the `orca_<hash>` filename prefix the fish sweeper matches; - third-party names: expo-router's `unstable_settings` / `ErrorBoundary`, iOS Safari's 16px zoom threshold. Where a case asserted a relation rather than a literal — `A < B`, a sum of parts, a cap compared against a sibling budget — the relation stays and only the literal went. Test-only changes: no production file is touched and no test file is deleted. |
||
|
|
31012aeb09 |
test: remove assertion-free probes, copied inventories and export-shape checks (#23816)
Second audit wave, targeting three more junk patterns: - assertion-free cases that run code and assert nothing, so they pass no matter what the code does; - inventory literals re-typed from a production declaration, where the only way the assertion can fail is someone editing one of the two copies; - export key-set and export-shape loops (`typeof x === 'function'` over every export) that restate what TypeScript already enforces. Yield is much smaller than wave 1 on purpose: the assertion-free scanner has a high false-positive rate, because many flagged blocks assert through a shared helper or their oracle is "this must not throw". Those were kept. `mobileWebCheckArgs` in `config/scripts/run-mobile-web-app-checks.mjs` is de-exported — after the inventory comparison went away, nothing outside the module read it. |
||
|
|
3976ad4c59 | perf(test): remove obsolete structural snapshots (#23777) | ||
|
|
e13631ee53 |
Prioritize workspace opening over replacement checkout preparation (#23013)
* Prioritize workspace opening over replacement checkout preparation * Preserve Git hook semantics and exercise preparation edge cases |
||
|
|
7ea01279cd |
feat(search): bundle ripgrep for local, WSL, and SSH search (#22396)
* feat(search): bundle ripgrep for local, WSL, and SSH search Ship @vscode/ripgrep-universal's prebuilt rg for all six relay platforms in every desktop artifact. Local and WSL searches spawn the bundled binary and drop the git ls-files / git grep fallbacks; SSH deploys upload the remote's binary once per ripgrep version and the relay prefers it over PATH rg. * fix(search): address bundled ripgrep review findings - Key the SSH ripgrep cache on the binary's content hash; a package bump is the only update step - glibc verifier: read arch tokens below the slice root and accept static ELFs (arm64 release blocker) - Ship ripgrep/PCRE2/musl license notices; bundle rg with orcad - Packaged builds never spawn a bare rg; report fd pressure as transient - SSH: install rg before sweep/GC, size-validate installs, back off instead of disabling on launch failure - Scope Dependabot to @vscode/ripgrep-universal; revert unrelated lockfile churn * chore(search): drop bundled-ripgrep reference doc; assert full packaging layout parity * refactor(search): one entry point for spawning the bundled ripgrep Local Quick Open, Quick Open path search, the Explorer name filter, and runtime text search each repeated the same three steps: resolve the bundled command, spread in the WSL distro, spread in the WSL shell expression. Fold that into spawnBundledRipgrep so one place owns the rule that a bare 'rg' must never reach spawn, and simplify the resolver's command/packaged checks. Restore the AGENTS.md ripgrep rule dropped alongside its reference doc in |
||
|
|
7da9788c83 |
fix(updater): send the gh token and cache the release picker's build list (#21902)
* fix(updater): send the gh token and cache the release picker's build list The dev build picker listed releases through api.github.com with no Authorization header, so it spent GitHub's 60/hour per-IP bucket that every unauthenticated caller on the same network shares, and it refetched on every settings mount and channel click. When that bucket ran dry the picker showed "No builds found" with a rate-limit line even though GitHub was healthy and the user's own token had its full quota. Attach the local `gh auth token` when there is one so the request draws from the user's 5000/hour bucket, fall back to unauthenticated on a rejected token or a spent token bucket, cache the list per channel for five minutes in the main process (the refresh button forces a reload), classify 403 by the rate-limit headers, and say when the limit resets. Fixes #21898 * fix(updater): don't trip breaker for secondary rate limits GitHub sends x-ratelimit-remaining: 0 on both primary and secondary limits. Secondary limits carry Retry-After and shouldn't block all core gh commands — only the primary limit should trip the shared breaker. * Scope gh rate limits to execution environment * Add build list cache hint to release channel settings Inform users that build lists are cached for 5 minutes and they can refresh to check for new builds immediately. This makes the cache behavior visible and explains why a manual refresh is necessary to bypass the cache. |