mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
220165366c2d9594fa0cd5dc3745eef557c2dae0
353
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
4085e1cf60 |
fix(memory): release stale session registries (#21734)
* fix(memory): bound session and lifecycle registries * fix(memory): bound transient filesystem registries * fix(memory): cap path and locale caches * fix(memory): bound runtime recovery registries * fix(memory): bound host mirror gap verdicts * fix(memory): bound shell startup env cache * fix(memory): bound gitlab host context cache * fix(memory): release removed ssh generations * fix(memory): expire cloud refresh replay guards * fix(memory): release retired plugin generations * fix(memory): bound plugin log key retention * fix(memory): bound automation authority generations * fix(memory): bound native chat enrichment cache * fix(memory): bound web session tracking generations * fix(memory): bound codex credential absence paths * fix(memory): bound WSL canonical path cache * fix(memory): bound sparse checkout cache * fix(memory): bound shared directory cache * fix(memory): bound advertised URL scan snapshots * fix(memory): bound automation manager cache * fix(memory): bound web session reorder intents * fix(memory): bound web session focus intents * fix(memory): bound web session handoffs * fix(memory): bound automation dispatch tokens * fix(memory): bound host mirror waiters * fix(memory): bound retained session activity * fix(memory): bound retained session activity * fix(memory): bound web session close intents * fix(memory): bound cloud session cache * fix(memory): bound WSL home cache * fix(memory): bound SSH capability cache * fix(memory): bound trust grant cooldowns * fix(memory): bound WSL auth drain state * fix(memory): bound Linear workspace credential cache * fix(memory): bound local Git capability cache * fix(memory): bound WSL Git environment cache * fix(memory): bound WSL Git environment cache * fix(memory): bound WSL preflight cache * fix(memory): keep hot cache entries warm * fix(memory): preserve generation fences across eviction * fix(memory): close remaining eviction fences * fix(memory): align evicted upstream generations * fix(memory): trim successful capability probes * fix(auth): retain expired refresh replay evidence --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
c34b944136 |
feat(github): bind projects to a specific gh account (#13664)
* feat(github): bind projects to a specific gh account Adds per-project `Repo.ghAccount` so repo-scoped gh calls (create-worktree issue/PR search, work items, hosted-review reads and mutations) run as the bound account via ephemeral child-env token injection instead of the globally active gh login. Multi-account resolution is capability-gated (gh >= 2.40) and fails closed when the bound account or host is unavailable; Project View stays ambient by design. Repository settings gains a section for selecting or clearing a keyring-backed account (shadcn `Select`), with mixed-version "not enforced" handling for older remote runtimes. Attached `-Rhost/owner/repo` forms are covered by the host-drift guard and its tests; es/ja/ko/zh catalogs carry the section's strings. `getLocalProjectGhExecOptions` centralizes the binding lookup so every gh execution path picks it up, including the Electron `hostedReview:*` handlers that previously stayed on the ambient login. `gh auth token` (a keyring read) is exempt from the rate-limit breaker gate so a tripped bucket cannot turn a bound-token resolve into a false "unavailable". The `ghAccount` update field and the two binding RPC methods live in the shared RPC params contract; the generated catalog is regenerated. Fixes #13612 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012B3QEP5iP4WGGEpPLtkHqA * fix(settings): make GitHub account refresh secondary * fix(github): satisfy strict casting quality checks * test(rpc): use runtime fixture for repo binding * fix(github): preserve project account for PR worktree lookups * test(rpc): avoid incomplete runtime settings fixture * fix(i18n): add GitHub account refresh label * fix(i18n): refresh runtime required catalog * fix(windows): preserve mobile patch bytes --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
85d1ffc072 |
fix: accept enterprise managed GitHub owner logins (#20450)
Unify owner validation across project pickers and repository overrides. Preserve EMU usernames in API and auth-status branch-prefix resolution, with regression coverage. Co-authored-by: Neil <neil@stably.ai> |
||
|
|
3e5eb0329a |
feat(cli): orca search over the agent session index (#20514)
* feat(cli): orca search over the agent session index `orca search <query>` calls PR 5's `aiVault.searchSessions` over the CLI's existing runtime RPC, against the host `--environment` / `--pairing-code` selects and no other. `orca search --index-status` calls `aiVault.searchStatus`. It is the proof the contract works with no panel. Every flag maps onto a contract field and nothing else: `--scope`, `--fresh`, `--limit`, `--cursor`, repeatable `--agent` and `--path`, `--since`, `--sort`, `--debug`, `--json`. No fan-out, no merged output, no `--host`. One command rather than a `search status` subcommand: the query is a bare positional, so `orca search status` could not be told apart from searching for the word "status". `--status` is unavailable because `orchestration task-list --status <state>` already owns the name as a valued flag. No new runtime capability. PR 5 decided an explicit `method_not_found` refusal maps to `unavailable/no-service`, so reusing `createSessionSearchClient` gives an old host a plain "this host runs no session search service" answer at exit 0 instead of a raw JSON-RPC error. `CommandSpec.repeatableFlags` scopes repeatability per command, because `--agent` must repeat for search and stay single-valued for `worktree create`. `help.ts` sat exactly at max-lines, so `skills-command-flag-help.ts` becomes `command-scoped-flag-help.ts` carrying both tables at the same call-site size. * refactor(cli): drop the search type assertions main's casting gate now rejects Main gained a `consistent-type-assertions: never` scan in the changed-code gate after this branch was cut, and it reported twelve assertions in the new files. The four in the argument parser were avoidable. `readEnum` now keeps the value `find` returns, which already carries the narrow type, and the agent filter goes through an `isAiVaultAgent` predicate over a `Set<string>` instead of widening the agent tuple. The test now narrows the printed envelope by shape and re-reads the printed result through `AiVaultSearchResponseSchema`, so the JSON assertions are checked rather than claimed, and the flag table is typed so its callback needs no cast. One assertion is left, for the structural fake client, with the SAFETY rationale AGENTS.md requires. * fix(cli): sanitize host strings and scope pre-command repeatable flags Route every host-supplied string the search formatter prints through the escape stripper, and resolve the repeatable-flag set from the command tokens ahead when a flag sits before the command. * refactor(cli): resolve repeatable flag rules once per command * fix(cli): clarify session search availability and SSH scope * feat(cli): hide orca search until the settings toggle ships `orca search` stays dispatchable but leaves every discovery surface: root help, group help, unknown-command suggestions, and `agent-context --json`. `buildAgentContext` did not filter hidden specs, so it also stops leaking the hidden `terminal stop`. |
||
|
|
52b6851b6e |
fix(worktree-create): prioritize creation Git and defer background preparation (#20722)
* fix(worktree-create): run create git commands at interactive tier, defer pool side jobs, bound queue wait by timeout
Creating a worktree on a busy machine stalled for minutes because the create's
own git competed for the same admission budget as everything else.
- The create path never set an admission tier, so it defaulted to 'status' and
could never use the scheduler's headroom slots. It now tags the option objects
that reach git directly: the add, the post-add listing, the base-ref probes and
the prepared-checkout finalize. The speculative warm-up and the SSH path are
unchanged.
- The prepared-pool re-arm is a full `reset --hard`; it ran mid-create and held a
general slot. `consumePreparedWorktreeCreate` now returns it as a thunk the
create runs after the startup terminal is spawned. Stale-preparation
reclamation (`worktree unlock` / `worktree remove`) drops to 'background'.
- A command's timeout only armed once its child spawned, so a saturated queue
could hold a 1s command indefinitely. Admission now takes the same deadline and
raises GitCommandTimeoutError without spawning; a caller abort still reports as
an abort.
The tier is kept off the `{ wslDistro }` routing objects: several callers test
those for emptiness to decide whether a repo has local git routing at all.
* fix(worktree-create): keep a bounded queue wait from reading as an absent base ref
The admission deadline added in the previous commit made every create-path probe's
15s/120s budget cover the queue wait. The default-base and worktree-base probes answer
`false`/`null` for any failure, so a saturated queue reported a repo that has origin/main
as having no default base and the create refused to start. Both probe families now let
`GitCommandTimeoutError` through, and the branch-name resolution loop, the push-target
configuration and the post-add listing run at the create's interactive tier so they reach
the headroom the rest of the create already uses.
Also: the deferred pool re-arm re-checks the pool inside the thunk, since `startPreparation`
replaces a map entry outright and would strand a prefetch's locked checkout with no owner;
the shared worktree scan keys on the tier so an interactive listing cannot inherit a queued
status scan's wait; and the deadline's microtask hop is gone, along with two fake-timer
`vi.waitFor` calls that jumped the clock past a 10ms budget before the grant settled.
* fix(worktree-create): preserve probe fallbacks and defer runtime replenishment
* fix(worktree-create): preserve interactive priority through prepared claims
* fix(worktree-create): prioritize CLI creation and preserve SHA probe timeouts
* fix(worktree-create): scope Git execution policy at creation boundaries
* fix(worktree-create): preserve inconclusive Git probe timeouts
* test(runtime): align creation fixtures with scoped Git execution
* test(native-chat): extract windowing layout fixture to satisfy file limit
* fix(git): restore execution-only timeouts while queued
* refactor(worktree-create): remove unrelated error-handling changes
* chore: narrow review scope and clarify preparation timing
* test(native-chat): restore fixture extraction to fix CI lint
* refactor(git): keep the admission scheduler in its original module
Reverts a move-only extraction. Inlines the single-use command-class
wrapper so the tier-resolution import fits the file's line budget.
* fix(worktrees): re-arm the prepared pool after CLI create launches terminals
The runtime create fired the pool re-arm right after materialization, so its
`reset --hard` competed with the startup agent's first git reads. Return the
thunk to the caller and fire it last, matching the desktop path.
* fix(worktrees): skip a preparation whose checkout is still running
An interactive create that claimed an in-flight preparation awaited a checkout
queued at background, so on a saturated budget it yielded to every arriving
status poller until aging promoted it. The create now misses with not_ready and
does its own add at interactive; the preparation stays armed for the next one.
Also drops the one-field policy object from the Git operation executor.
* fix(worktrees): report repo_mismatch before not_ready when selecting a preparation
The readiness filter ran before the same-repo check, so another repo's
in-flight preparation was labeled not_ready instead of repo_mismatch, hiding
the cap-thrash signal for multi-project users. The hit/miss decision is
unchanged.
* test(runtime): type the worktree-meta stub against WorktreeMeta
Main now rejects bare object parameters, and the merge picked that rule up.
* fix(worktrees): wait on in-flight preparations and re-arm the pool on failed creates
A create landing mid-checkout now claims the in-flight preparation and awaits it, as main
did. The `checkoutFinished` filter and its `not_ready` miss reason made the create skip a
prepared checkout that was seconds from done and pay a full cold add instead; on a 40k-file
repo that turned a 0.2-1.5s create into 2.4-4.3s. The preparation's own git also runs at
`status` again rather than `background`, so awaiting it does not park behind status pollers.
Only the stale reclaim stays `background`, which no create waits on.
The deferred pool re-arm now fires on every path, not just the success path. Main armed the
replacement synchronously inside the consume, so a later failure in include copy, push-target
setup, or terminal startup still left one warming. The thunk stays deferred until after
terminal startup for admission ordering, but a `finally` on the desktop create and matching
failure-path fires on the runtime create restore that guarantee. It fires exactly once.
* refactor(runtime): carry the pool re-arm in one holder
The runtime create used three mechanisms to guarantee the deferred pool re-arm fires: a
catch in the git create, a catch on materialization, and a holder fired in the managed
create's finally. The desktop create already used one holder for the same guarantee.
The holder now threads down through the create args, so the git create arms it at the point
it consumes a prepared checkout and nothing below has to handle the failure case. The thunk
already re-checks the pool before arming, so a single fire point in the outermost finally
covers every failure after the consume. Behavior is unchanged; both flipped failure-path
tests still assert exactly one fire, and each fails without the production change.
|
||
|
|
8edec28a55 |
fix(worktree): keep a WSL checkout case so delete cannot take the twin branch (#20273)
* fix(worktree): let a POSIX path keep its case on a Windows desktop `canonicalWorktreePath` folded case whenever `process.platform` was win32, without asking what the path itself was. A WSL or SSH checkout is spelled `/home/alice/ws/feature` on a Windows desktop too, and ext4 is case-sensitive, so `/home/alice/ws/Feature` and `/home/alice/ws/feature` — two real checkouts on two real branches — collapsed into one row. `removeWorktree` picks the row it is about to remove with that comparison and reads the branch off it. Requesting `/home/alice/ws/feature` removed the right directory (the path rides in argv) and then ran `git branch -d -- Feature`. The same wrong row feeds `assertWorktreeUnlockedForRemoval`, so a locked twin blocks an unlocked delete and an unlocked twin lets a locked one through. Whose filesystem a path names is a property of the path, not of the desktop reading it, so a POSIX-absolute path now takes POSIX rules at any platform and a POSIX/Windows pair is never equal — `win32.resolve` would otherwise give the POSIX path a drive root and manufacture the equality. Windows drive and UNC paths, including WSL UNC aliases, keep folding case as before. Two call sites already carried private copies of this rule (`isSameCommonDirPath`, `ipc/worktree-path-comparison`); this is the same rule at the source. The removal path is the one that never got one. * fix(worktree): keep a WSL checkout's case through the UNC spelling too The first commit gave POSIX-absolute paths POSIX case rules, which is right but does not reach the WSL case it claimed. `listWorktreesStrict` runs every listed path through `translateWorktreePath`, so git-in-the-distro's `/home/alice/ws/Feature` arrives as `\\wsl.localhost\Ubuntu\home\alice\ws\Feature` and the POSIX branch never sees it. The removal suite mocks `translateWslOutputPaths` to identity, which is why the end-to-end test passed without exercising the translation production always applies. Driving the real translator, the original defect survived unchanged: a request naming `...\ws\feature` ran `git worktree remove --force ...\ws\feature` and then `git branch -d -- Feature`. The filesystem behind `\\wsl.localhost` is ext4, so the UNC spelling is case-sensitive for the same reason the Linux spelling is — except where Windows genuinely folds: the `\\wsl$` share alias, the distro name, and a drvfs `/mnt/<letter>` tail, which really is a Windows volume. `foldWslUncPathCaseInsensitiveParts` already draws exactly that line and `git-fetch-head-lock` already depends on it, so this reuses it rather than writing a fourth copy of the rule. Windows drive paths keep folding whole. The end-to-end case now drives the real translator instead of the mock, so the translation cannot go missing again without the test noticing. |
||
|
|
231e805b1e |
fix(lint): enable anti-slop/no-shape-in-symbol-names (#20785)
Flip `anti-slop/no-shape-in-symbol-names` from "off" to "error" and clear
every violation under src, config, tests and mobile.
What the rule bans
------------------
The case-insensitive substring "shape" in any JS/TS identifier: variables,
functions, parameters, types, type parameters, class members, private names,
object-literal keys and JSX identifiers. The one exemption is a statically
accessed member read owned by another value (`zodObject.shape` is fine), so
third-party APIs stay readable without a suppression.
"Shape" names a value's structure rather than its domain role. `UserShape`,
`validateArgShape` and `errorShape` all tell you the symbol is "an object
with some fields" -- which is already what a type says -- while saying
nothing about what the value is for or who owns it. The rule forces the
name to carry the domain instead.
Violations fixed
----------------
689 violations across 109 files at baseline (verified by re-running the
audit against the pre-change tree with the rule set to "error").
Fix pattern
-----------
Rename for the domain role, not the structure:
-type FieldShape = 'list' | 'map' | 'whole'
-const FIELD_SHAPES = { ... } satisfies Record<keyof Observation, FieldShape>
+type FieldEncoding = 'list' | 'map' | 'whole'
+const FIELD_ENCODINGS = { ... } satisfies Record<keyof Observation, FieldEncoding>
-function assertGitPushTargetShape(target: unknown): void
+function assertValidGitPushTarget(target: unknown): void
-function describeReadDirPathShape(p: string): ReadDirPathKind
+function classifyReadDirPath(p: string): ReadDirPathKind
Predicates became statements about the value (`isDeltaShapedProviderFrameKind`
-> `isDeltaProviderFrameKind`, `isDeleteShapedDiscardEntry` ->
`discardDeletesEntryFile`, `isSkillsCliAgentKeyShaped` ->
`isUsableSkillsCliAgentKey`). Type aliases dropped the suffix where the
remaining name was already unambiguous (`GhGraphqlErrorShape` ->
`GhGraphqlError`).
No wire-visible name was renamed: no IPC or RPC channel, stream opcode,
request/response param, persisted field, or i18n key. The `--shape=symlink|copy`
CLI flag read by .github/workflows/skill-update-roundtrip.yml is unchanged --
only the local variable holding it was renamed.
Exemptions
----------
They are file-scoped entries in config/oxlint-anti-slop.json, not inline
`oxlint-disable` comments. An inline directive naming an anti-slop rule reads
back as an UNUSED directive under the root lint scan, which does not load this
plugin -- the changed-code quality gate counts that warning, so the comment form
cannot be used for a rule that lives only in this config.
* src/renderer/src/components/browser-pane/annotate/**:
in the screenshot annotator a "shape" is the drawn geometry -- pen, arrow,
rect, ellipse, highlight. That is a genuine domain noun, and it pervades
every symbol in the module.
* repo-icon.tsx, repo-header-project-actions.tsx, mobile MobileRepoIcon.tsx:
lucide exports the icon component as `Shapes`. The name is theirs, and the
matching REPO_LUCIDE_ICONS key is the persisted icon name shared with the
desktop picker -- renaming it would orphan saved repo icons.
* src/shared/onboarding-state-types.ts, src/shared/constants.ts:
`shapedSidebar` is a persisted onboarding-checklist field and a telemetry
enum member; renaming it would orphan saved state.
* src/shared/rpc-contract/rpc-send-params.ts: matching zod's own literal `shape`
property is what selects the ZodObject branch of the conditional type.
No exemption was added merely to avoid a rename. Eight symbols initially
suppressed as "a cross-module refactor outside this change" were proven to have
zero non-TypeScript references repo-wide and renamed instead.
Zod's `ZodRawShape` needed no exemption at all: `Readonly<Record<string,
z.ZodType>>` is its definition, so repo-update-params.ts and
ui-update-value-tolerance-params.ts spell it out instead. Likewise
telemetry-event-classification.ts now reads `.shape` through an `in` narrowing,
which also retires two pre-existing type assertions; three more assertions the
rename had dragged onto changed lines (two `JSON.parse` sites, one node:sqlite
row read) became annotations and an explicit row mapping.
Verified
--------
* Audit reports zero violations; confirmed the rule genuinely fires by
planting a probe violation.
* node config/scripts/run-typecheck-projects-in-parallel.mjs exits 0.
* Vitest over src/shared, src/main/github/project-view, the annotate module,
the repo-icon components and the Chromium SameSite electron spec: all green.
* All 66 removed "shape" identifiers grepped repo-wide across every file type;
none survive.
* node config/scripts/generate-rpc-params-catalog.mjs --check exits 0.
* node --check on every changed .mjs; oxfmt clean on all changed files.
* `pnpm run check:code-quality:changed` reports 0 findings.
Not machine-verified: the 3 mobile/ files (its Vitest run cannot resolve
`expo/tsconfig.base.json` in this worktree), and the WSL- and Playwright-gated
specs. All are rename- or comment-only hunks, read in full.
|
||
|
|
bfdec26352 |
fix(lint): enable anti-slop/no-object-parameters (#20781)
The rule rejects the broad `object` type on any function input (declarations, expressions, arrows, methods, call/construct signatures, function types), plus local aliases and unions that resolve to `object`. `object` accepts every non-primitive while exposing no properties, so it documents nothing and pushes callers into assertions at the boundary. Fixes all 185 violations across src, config, tests and mobile, and flips the rule from "off" to "error" in config/oxlint-anti-slop.json. Approach: replace each `object` input with the type its owner already has. Most sites took an existing domain type or a type-only import (36 added); 40 new aliases name shapes that had none. Where a value is genuinely only compared by reference, it gets a named identity token instead of a shape -- `Record<string, never>`, the built-in `WeakKey`, or a `unique symbol` brand, matching the branding already used in src/shared. Same treatment for WeakMap and Map key parameters. Two `as unknown as` casts became unnecessary once the parameter carried a real type and were removed; no new casts were added. Suppressions added: none. No `oxlint-disable` for this rule anywhere, and no max-lines disable or per-file bump. Three files sat exactly at their max-lines cap, so the added type imports were made line-neutral rather than suppressed: - src/main/ipc/browser.ts exports the existing guest-registration args type (renamed BrowserGuestArgs) so browser.test.ts reuses it on one line. - pane-scroll.ts takes TerminalScrollIntentTarget through the existing pane-manager-types import via a type-only re-export. - direct-rpc-client.ts drops the identity parameter entirely: the session check moved into the sendProbe callback that owns the token. Verified: anti-slop config reports zero violations over src config tests mobile; run-typecheck-projects-in-parallel exits 0; 144 affected test files pass (1749 tests); oxlint and oxfmt clean on all changed files. Mobile has no runnable test/typecheck target in this worktree (expo is not installed), so its 6 files were typechecked against a standalone config and diffed against the base branch -- error sets are byte-identical, including test files. |
||
|
|
775a932651 |
fix(git): distinguish binary absence from missing cwd on spawn ENOENT (#20798)
* fix(repos): preserve unknown Git availability * fix(git): distinguish binary absence from missing cwd on spawn ENOENT Node reports ENOENT for both a missing git binary and a missing working directory during spawn. The fix checks specifically for spawn syscall, then verifies the cwd exists to disambiguate. This prevents reporting "no Git" when the error is actually a missing working directory. Centralizes probe logic in a reusable function; other failures cause rejection so callers preserve the unknown status instead of collapsing to false. |
||
|
|
99062ed80b |
fix(worktrees): preserve unverifiable disk witness (#20713)
* fix(worktrees): preserve unverifiable disk witness * fix(worktrees): follow gitdir/commondir markers in disk witness The disk witness validates created worktrees by reading the repo's common directory from disk. Previously it only checked for a direct .git directory and returned a status object that conflated different failure modes. Now it properly follows .gitdir and commondir pointer files to locate the true common directory, fixing detection on repos with linked git directories (worktrees, submodules) and WSL scenarios. Error handling is simplified: definitive absence returns undefined, other read failures throw with proper cause chains, eliminating the ambiguous "unverifiable" state that would mask real errors. * fix: validate gitdir marker targets are directories When a .git marker points to a missing or non-directory path, that's unverifiable—not the same as an absent .git file (bare repo). Validate accessibility before reading commondir to catch these errors clearly. |
||
|
|
41e42beab4 |
fix(worktrees): safely remove prunable git-file registrations (#20617)
Preserve checkout files and the named branch when removing a positively attested malformed Git-file registration. Reject file/symlink targets in deferred directory deletion. Verified exact head with 75 focused tests including actual Git malformation, preserved marker/file bytes and branch HEAD. Independent review and complete product CI passed. WSL routing is covered by unit tests; direct SSH fails safely without local recovery. Fixes #17316 |
||
|
|
ee1a0a4e2d |
fix(git): avoid Windows tree kills after the command has exited (#20606)
Validated and independently reviewed OMP integration fix. |
||
|
|
31db2774f8 |
fix(git): skip upstream remote probes when the remote is absent (#18455)
* fix(git): skip upstream remote probes when the remote is absent Issue and PR resolvers listed remotes by probing `git remote get-url upstream` on every poll, including origin-only clones where that remote cannot exist. List remotes once, cache against git config, and skip the probe unless `upstream` is present. * fix(git): avoid stale remote probe cache entries * fix(github): observe origin repository probe failures * fix(github): observe verified origin probe failures * fix(github): skip missing upstream probe for PR lists * test(github): scope the #9171 lazy-resolution guard to default-branch commands The guard asserted that no git command runs for an open PR, using "no git at all" as a proxy for "no default-branch resolution". Remote-name listing is a separate concern, so allow it and keep every other command forbidden; the symbolic-ref/rev-parse resolution this issue is about stays unreachable. --------- Co-authored-by: Neil <neil@stably.ai> |
||
|
|
471463f4ce |
perf(git): normalize tracked discard paths once per operation (#20299)
* perf(git): normalize tracked discard paths once per operation * chore(git): drop the now-dead tracked-pathspec re-export |
||
|
|
490937a043 |
perf(git): classify bulk discard paths once (#20274)
* perf(git): classify bulk discard paths once * style: format bulk discard partition |
||
|
|
afb43ea914 |
perf: avoid duplicate repository directory probes (#20253)
Co-authored-by: Orca Worker <orca-worker@localhost> |
||
|
|
c84007c541 | feat(rpc): generate a shared params catalog from the host registry, gated on parse parity (#19961) |