mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
eb545aaa59be6ca812eaaa0a55421d78f0acaa3e
7278
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
eb545aaa59 |
fix(worktree): collapse duplicate "Local Mac" run targets in the host picker (#10472)
* fix(worktree): collapse duplicate "Local Mac" run targets in the host picker A linked worktree added as its own project projects a second ready host setup on the same project+host, so the run-target picker rendered N identical "Local Mac" rows differing only by path. Only the first was reachable — resolveWorkspaceCreationTarget takes the first project+host match — so the extras pointed at paths that may no longer exist. - Dedupe ready setup options by host in the picker (display fix for profiles that already hold duplicates). - Canonicalize a stale draft's setup id to the setup the picker shows, so the displayed path is the path the workspace is created in. - Reject a linked worktree at repos:add when its main checkout is already tracked, preventing new duplicates. * fix(worktree): only dedupe a linked worktree against a git main checkout Review follow-up: the repos:add guard matched any tracked repo on the main checkout path, including a folder-kind record. A folder repo does not project onto the same project as the git worktree, so matching it would suppress a legitimate add without deduping anything. |
||
|
|
56d3e2cb2e |
fix(editor): preserve Markdown focus handoffs (#10618)
* fix(editor): preserve Markdown focus handoffs * fix(editor): scope focus requests to panes via viewStateId When opening a file to focus it, tag the pending request with the pane's viewStateId. This prevents split siblings from claiming each other's requests and stops later remounts from stealing focus. Both Monaco and rich-markdown editors now retire requests on mount. |
||
|
|
97175ed92b |
fix(diff): keep scroll restore armed through layout shifts (#10615)
* fix(diff): keep scroll restore armed through layout shifts * test: add scroll-restore convergence and user-scroll disarm cases Verify that a converging restore withstands layout shifts and continues retrying, while unmarked user scroll disarms the restore attempt. Stabilize marks and offset objects across renders to preserve the bookkeeping state that guards restoration retries. |
||
|
|
e564603d54 |
Observe daemon health failures and fix e2e test races (#10595)
- Add E2E_FORCE_DAEMON_HEALTH_UNREACHABLE env to simulate failed health checks - Log when replacing a failed daemon, but stay silent on cold starts - Simplify daemon-slow-health-check-preservation: use forced-unreachable health instead of SIGSTOP/SIGCONT - Add --no-sandbox flag to electron launch args for Ubuntu CI - Support extraEnv option in restart session launches |
||
|
|
9eff3728a3 | Update README downloads badge | ||
|
|
505967eba0 |
fix(runtime): report the effective ask timeout so a clamped wait isn't misreported (#10550)
The 30-min clamp was silent: the ask result carried no timeout figure, so
the CLI printed the value the caller *sent*. A worker passing
--timeout-ms
|
||
|
|
b31e9bb03d |
fix(worktree): bound the .worktreeinclude copy so a huge include can't freeze workspace creation (#10540)
* fix(worktree): bound the .worktreeinclude copy so a huge include can't freeze creation `.worktreeinclude` copying was bounded in entry count (1000) but unbounded in bytes and files, and awaited inline during worktree creation. A repo listing `node_modules` froze creation for minutes behind the create dialog on Linux and Windows, where the fallback is a full `fs.cp` (macOS gets a cheap APFS clone). Measure each copy-mode source against a cumulative budget (2 GB / 50k files) before the first byte is written, and refuse the entries that bust it. Refused entries ride the existing `CreateWorktreeResult.warning` channel so a workspace never silently comes up missing its included files. Pre-measurement rather than mid-copy abort: `fs.cp` ignores its `signal` option, so a started copy cannot be cancelled and would strand a partial tree. Refusing up front means there is no partial state to clean up. * fix(worktree): don't charge bytes for copy-on-write clones, and bound the sizing walk Two defects in the copy budget, both found by review: - The byte limit was applied on macOS, where the copy is an APFS clonefile. Measured: a 2.7 GB tree clones in 22 ms and consumes no disk. Refusing it on a 2 GB byte ceiling denied work that was already free — a regression on the one platform this bound was never meant to touch. Bytes are now charged only when a byte-for-byte copy will actually run; the volume probe that decides this is the same cached df+diskutil pair the clone runs, and writes nothing, so the "refuse before the first byte" invariant holds. The entry limit still applies everywhere: inodes are real work even on the clone path. - A refused entry consumed no budget, so a `.worktreeinclude` listing many over-budget directories paid a fresh full-limit walk for each one — up to 1000 x 50,000 lstat calls, re-creating the stall this bounds. The walk is now charged against its own ceiling whatever the verdict. Also documents that `admit()` must be awaited sequentially (CodeRabbit). * fix(worktree): give the sizing walk headroom so one huge entry can't starve the rest The walk ceiling added in the previous commit was seeded with maxEntries, the same number the entry limit uses. Sizing an entry that busts the file-count limit walks maxEntries + 1, driving the ceiling negative, so every later `.worktreeinclude` entry was refused without being measured at all. That regressed the common case: a repo listing `node_modules` plus `.env` used to get `.env`; it silently got nothing. Reproduced, and now covered by a test that fails when the headroom is removed. The walk now gets 5x the entry budget, so total sizing work stays bounded (<=250k lstat per materialization, vs the 1000 x 50k this ceiling exists to prevent) while ordinary lists never reach it. Entries refused because earlier ones exhausted the walk report a distinct 'sizing' reason, so the warning stops quoting size limits at a 4-byte file that was never measured. * fix(worktree): bill a failed clone's bytes, and blame the right ceiling Two follow-on defects from the copy-on-write fix: - A predicted APFS clone that then failed mid-copy (EPERM, ENOSPC) fell through to a real `fs.cp` whose bytes were never charged, because the entry had been admitted on the premise that cloning is free. That reopened the unbounded copy on macOS. The measured size is already known, so the fallback now bills it and refuses if it no longer fits, reporting the entry as skipped instead of silently copying gigabytes. A clone that was never viable (ApfsCloneUnavailableError) was already charged as a real copy, so that path keeps falling back as before. - The walk ceiling is also applied inside the measurement via min(remainingEntries, remainingWalk), and when the walk term bound, the refusal was still reported as 'entries' — telling the user a 3-file directory busted a 4-file limit. It now attributes to whichever ceiling actually bound. Also fixes the singular warning text, which said "entry X was not copied ... copying them would exceed ... Copy them in manually". * fix(worktree): flag a partial clone leftover, cap the warning, cover two branches - A clone that fails partway only removes an *empty* reservation, so leftovers can survive at the target. Reporting that entry as simply "not copied" sent the user to copy it in manually, straight into a half-populated directory. Those skips now carry mayBePartial and the warning says to check the path first. Cleaning up the leftovers stays the deferred follow-up it already was. - The warning enumerated every skipped path. `.worktreeinclude` allows 1000 entries and all of them can be skipped, so it now names five and counts the rest — an unbounded string is a poor look in a PR about bounds. - Two load-bearing branches had no test, both proven by surviving mutants: the `bytesAreCopied` short-circuit (reachable when a wedged df/diskutil makes the volume probe answer "no clone", so bytes are charged up front and must not be billed twice), and chargeBytes actually consuming budget for later entries. * fix(worktree): only flag directory clones as partial, and cap that list too - mayBePartial was set for every refused clone fallback, but only a *directory* clone can leave anything behind: the file path clones into a temp name and publishes with link(2), so a failure leaves nothing at the target. Sending the user to inspect a path that does not exist is its own small lie. - The partial-copy sentence sliced to five names without the "and N more" that the other sentence appends, so entries past the fifth were surfaced nowhere. Both sentences now share one nameList helper. |
||
|
|
159057c5d4 |
test(git): cover the false-positive class the header fix also removes (#10547)
The old anchored regex matched neither branch on a `[section "sub"]key = value` line, so the parser never left `[core]` and credited the next indented line to it — reporting sparse for a worktree git says is not. Fails on the pre-fix parser (returns true where git reports unset). |
||
|
|
baf25785e0 |
fix(tasks): keep repos with a pending remote-identity probe in the picker (#10527)
* fix(tasks): keep repos with a pending remote-identity probe in the picker Task-repo eligibility filtered on `hasProjectRemoteIdentity`, which is populated by a background `git remote -v` probe. When the probe could not reach git — an SSH-hosted repo whose connection is not up yet, a cold launch — the repo silently vanished from the Tasks picker and stayed hidden for the full 5-minute negative-cache TTL, even after the host came back. GitHub repos were largely shielded because a persisted `upstream` satisfies the identity projection through a different route; GitLab and other providers depend on the probe. Distinguish unknown from settled instead of hiding both: - `probeGitRemoteIdentity` reports `resolved` / `no-remote` (git answered, no usable remote) / `unavailable` (never reached git). - Enrichment persists `gitRemoteIdentity: null` only on `no-remote`, mirroring the existing `upstream: null` "not a fork" marker. An unreachable host leaves the identity undefined. - Persistence keeps the explicit `null` instead of dropping it. - `getTaskEligibleRepos` keeps a repo whose identity is still pending; folders and settled remote-less repos stay filtered out. * test(tasks): cover the SSH probe exec paths for remote-identity status Addresses CodeRabbit review: the unavailable-on-error case only exercised the local git runner. Adds a connected-provider whose exec rejects, and an SSH repo git answered for with no remotes. * test(tasks): pin that a settled no-remote repo still resolves once it gains a remote Three independent reviewers flagged that the candidate filter's `!repo.gitRemoteIdentity` looks like an oversight next to the new null marker. Tightening it to `=== undefined` would silently stop detecting a remote added after the marker landed. Document that the re-probe is deliberate and pin the behavior with a test. |
||
|
|
713fa40505 |
fix(runtime): reserve long-poll headroom so orchestration.ask can't starve terminal.wait (#10529)
* fix(runtime): reserve long-poll headroom so orchestration.ask can't starve waits orchestration.ask joined the long-poll set, which also opted it into the single server-wide activeLongPolls counter. Because ask blocks on a reply for its full timeout (600 s default, previously unbounded via a caller timeoutMs), 16 asking workers could hold every slot and shed terminal.wait and check --wait with runtime_busy for every other client — mobile, web, CLI, SSH and relay all share this runtime. Meter ask as its own long-poll class with a sub-cap of half the budget, and clamp the caller-supplied timeoutMs at 30 min. The keepalive and abort-signal wiring that motivated the original change is unchanged. * test(runtime): cover the ask sub-cap and counter release on the WebSocket path The admission fence is shared by both transports but only the Unix-socket path was exercised, so a WS-only regression in admitLongPoll/releaseLongPoll would have shipped silently. Drives handleWebSocketMessage with a 'runtime' scoped device (orchestration.ask is absent from the mobile allowlist) and asserts the overflow ask is shed without burning a reserved slot, that check --wait still gets the other half, and that both counters return to zero when the socket closes. |
||
|
|
9d02782969 |
fix(git): read core.sparseCheckout the way git does (#10537)
* fix(git): read core.sparseCheckout the way git does Sparse-checkout detection parsed git config line-by-line and only accepted a section header alone on its line, so git's legal same-line form `[core] sparseCheckout = true` matched neither branch and was silently skipped: a genuinely sparse worktree lost its badge and partial-checkout warning. It also read `config.worktree` unconditionally, although git honors that file only while extensions.worktreeConfig is on, so a stale worktree config could override the repo's real setting. Headers are now consumed left-to-right off each line (further headers and one assignment may follow), and config.worktree is read only behind the extension gate. Every new expectation was confirmed against real `git config --get`. * test(git): correct what git actually does with a trailing-junk config value Git does not reject `[core] sparseCheckout = true bogus = false` outright: it parses the line and takes the whole tail as one value (`git config --list` reports `core.sparsecheckout=true bogus = false`), then fails only the boolean coercion. The expectation is unchanged; the comment now matches the binary. |
||
|
|
fc513233cb |
fix(release-cut): gate an explicit RC against its own series (#10525)
* fix(release-cut): gate an explicit RC against its own series
semver_gt compares through strip_pre(), so the explicit-version override
only ever checked the stable line: 1.4.156-rc.0 read as 1.4.156, cleared
a 1.4.155 stable, and republished an RC below what clients already run.
Anchor a prerelease request on highest_rc_for_base -- the same rc history
the kind path uses -- so the override can only advance the series.
Two sibling gaps in the same block:
- version_suffix was silently dropped when version was set, because the
append lives in the kind branch the override skips.
- the shape regex rejected X.Y.Z-rc.N.suffix, so a suffixed RC the rc
path can produce could never be re-cut explicitly.
* fix(release-cut): close both ends of the rc-number range the gate compares
The new explicit-rc gate compares with `[[ -le ]]`, i.e. bash machine-width
integers, and the author closed only the low end. Past INTMAX bash saturates,
so `version=1.4.156-rc.99999999999999999999` reads as "above the published
rc.3" and the gate falls open — then the tag it cuts pins
highest_rc_for_base at 1e20 for that base forever, and every later cut wraps
to a lower rc the fleet never updates to. Bound the rc number to nine digits.
Also reject leading zeros on an all-digit prerelease identifier. `npm version`
renormalizes rc.4.01 to rc.4.1 while the tag step keeps the literal input, so
the shipped package.json version and its own release tag name different
releases. The explicit path's embedded identifier now goes through the same
validator the kind path uses instead of only the shape regex.
* fix(release-cut): stop the refusal pointing minor/major RCs at the wrong series
kind=rc derives its base from bump(latest_stable, patch), so the remedy the
refusal suggested only works when the requested base *is* that next patch. A
1.5.0-rc.N series exists only because this override created it, so an operator
resuming a stuck 1.5.0-rc.2 was told to dispatch kind=rc, which would have cut
an unrelated 1.4.156-rc.4. Spell the condition out and give the fallback that
does work for a non-patch base.
Also correct the mechanism in the comment I added in
|
||
|
|
7d47e9e1d8 |
fix(window): stop viewport reflow from moving screen geometry (#10543)
* feat(diagnostics): record OS suspend/resume so sleep gaps aren't read as freezes Renderer timers stop across OS sleep, so a multi-hour `renderer_memory` heartbeat gap looks identical to a wedged renderer. That ambiguity sent the uber-crash investigation down a deadlock path that the telemetry later disproved -- and a healthy machine's own trace shows a 427-minute mid-session gap with reason=interval, so the gap alone proves nothing either way. powerMonitor 'resume' was already wired for renderer wake recovery but left no breadcrumb. Stamp suspend and report the measured span on resume so the next freeze report can be told apart from a laptop lid. * fix(diagnostics): only record sleeps long enough to hide a heartbeat Adversarial review flagged that the first cut would flood the 30-entry breadcrumb ring and evict the crash evidence it exists to explain. Measured on 7 days of pmset history: 70 user-visible sleep cycles, worst 60-min burst of 7. Median span is 2 SECONDS -- only ~24% run past 60s, so most of that traffic could never explain a gap anyway. Cycles are counted Sleep -> next FULL Wake, since powerMonitor's resume maps to NSWorkspaceDidWake, which does not fire for dark wake. Drop the suspend breadcrumb (suspend now only stamps a timestamp) and emit a single `system_slept` on resume, gated at 60s. That cuts 70 cycles to 17 over the same week (worst burst 3) while still catching every sleep long enough to swallow a 60s renderer heartbeat. * test(diagnostics): assert resume listeners detach by identity The off mock deleted by event name alone, so teardown detaching a different closure than the one registered still passed -- a leak of the real powerMonitor listener would have gone unnoticed. For 'suspend' that leak has no other observable effect through the public API. Co-authored-by: Orca <help@stably.ai> * fix(diagnostics): span from the first suspend across dark wake powerMonitor 'resume' maps to NSWorkspaceDidWake, which does not fire for dark wake, so macOS can deliver suspend -> suspend -> resume. Overwriting the stamp reported only the trailing segment, and when that segment fell under the 60s gate a 90-minute sleep recorded nothing at all -- leaving the gap looking like the unexplained freeze this is meant to rule out. Co-authored-by: Orca <help@stably.ai> * docs(diagnostics): correct the threshold rationale to match measurement The comment claimed maintenance sleeps would flood the ring. Re-measuring pmset over 6 days (78 sleeps, 29 full wakes, 51 dark wakes) shows they resolve as DarkWake, which never fires 'resume', so they never recorded a breadcrumb at all. Real rate is 29 breadcrumbs / 6 days, worst 60-minute burst 4 against a 30-entry ring. The gate's actual job is narrower: skip sleeps shorter than the 60s heartbeat, which cannot open a gap to explain. Co-authored-by: Orca <help@stably.ai> * fix(window): stop viewport reflow from moving screen geometry Blink's ScreenMetricsEmulator::Apply checks screen_size and view_position before the desktop/mobile branch, so screenPosition:'desktop' does not make them inert -- despite Electron documenting both as mobile-only. Passing the content size and 0,0 overrode screen.width/availWidth and the window origin for the whole 32ms hold, so a browser context menu opened mid-reflow would translate against 0,0 and land in the wrong place (BrowserPane.tsx:3167). Empty screenSize means 'no override', and an omitted viewPosition stays nullopt in Electron's converter, so the real position survives. Only the scale factor moves now, which is what the reflow actually needs. Also record a breadcrumb when the restore exhausts its attempt budget: the renderer is left at the wrong scale factor until some later reveal fixes it, and that was previously silent. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
5faf3b2ea0 |
fix(i18n): keep the macOS "Local Network" wording searchable in every locale (#10536)
Settings search indexed the macOS privacy-toggle name as English-only aliases on the LAN keyword, so a Chinese user typing the term macOS System Settings actually shows them (本地网络) got no hit — zh's catalog value had been changed to 局域网 (LAN). Split the two wordings onto their own catalog keys so each locale carries both: 87620e6416 = LAN, fa3239cd42 = Local Network (its true content hash). Localized values come from the repo's own LAN title translations and from macOS 26's SecurityPrivacyExtension Localizable.loctable (LOCAL_NETWORK), so nothing is invented. ja/ko/es already matched macOS and are unchanged apart from gaining the LAN key. |
||
|
|
9bc640addb |
fix(terminal): make primary-selection paste suppression single-shot (#10526)
* fix(terminal): make primary-selection paste suppression single-shot Middle-clicking in the terminal armed a 750ms window that swallowed every native paste event, not just Chromium's one follow-up — so a real Ctrl+V inside that window was silently dropped on Linux. Consume the deadline on first use (`shouldSuppress*` -> `consume*`) so the arm owes exactly one event; 750ms stays as that event's expiry bound. * test(terminal): cover the paste event xterm actually forwards to the PTY Review follow-ups on the single-shot suppression change; no source change. The new real-module file only synthesized `beforeinput`. An Electron probe (Chromium 150) showed `paste` fires first, is cancelable, and cancelling it at document capture suppresses `beforeinput` entirely — and xterm registers `handlePasteEvent` for `paste` only, never `beforeinput`. So `paste` is the sole event that can double-write the PTY, and it was the one event the end-to-end file did not exercise. Add it; it fails with the fix reverted. Consuming mutates, so `isTerminalNativePasteTarget(...) && consume()` is now load-bearing: swapped operands would burn the arm on an unrelated paste and let the real follow-up double-paste. The guarding test asserted only `defaultPrevented`, which survives the swap — assert `consume` is never reached instead. Rename the mocked-file case that claimed to prove single-shot. With the module mocked its sequence is dictated by the mock; it pins that the hook re-asks per event rather than caching, so name it that. * test(terminal): name the beforeinput dispatcher for the event it dispatches Round-2 review nit on my own round-1 change: once `dispatchClipboardPaste` existed alongside it, a helper named `dispatchPaste` that dispatches `beforeinput` inverted the reader's expectation. Match the sibling file's `dispatchNativePasteBeforeInput` convention. |
||
|
|
70b4d12067 |
perf(worktree): park the git-common existence poll while the window is hidden (#10528)
startGitCommonNarrowWatch was the only watch entry point that never received WorktreePollerWindowVisibility, so its `worktrees/` existence poll kept stat'ing every repo without a linked worktree forever in the background (0.5 stat/sec/repo at the 2s default). Its siblings — the primary-metadata snapshot poller and the non-darwin git-common polling — already park. Threads visibility through and matches the snapshot poller's park/re-arm pattern: the poll stops on the first hidden tick, and re-checks immediately on onWindowBecameVisible so a worktrees dir created while hidden still upgrades to the native stream and emits its create event. The visibility listener is dropped in the dispose path. darwin-only: the narrow watch is the `platform === 'darwin'` branch. |
||
|
|
f8b9b5c508 |
feat(diagnostics): record OS suspend/resume so sleep gaps aren't read as freezes (#10530)
* feat(diagnostics): record OS suspend/resume so sleep gaps aren't read as freezes Renderer timers stop across OS sleep, so a multi-hour `renderer_memory` heartbeat gap looks identical to a wedged renderer. That ambiguity sent the uber-crash investigation down a deadlock path that the telemetry later disproved -- and a healthy machine's own trace shows a 427-minute mid-session gap with reason=interval, so the gap alone proves nothing either way. powerMonitor 'resume' was already wired for renderer wake recovery but left no breadcrumb. Stamp suspend and report the measured span on resume so the next freeze report can be told apart from a laptop lid. * fix(diagnostics): only record sleeps long enough to hide a heartbeat Adversarial review flagged that the first cut would flood the 30-entry breadcrumb ring and evict the crash evidence it exists to explain. Measured on 7 days of pmset history: 70 user-visible sleep cycles, worst 60-min burst of 7. Median span is 2 SECONDS -- only ~24% run past 60s, so most of that traffic could never explain a gap anyway. Cycles are counted Sleep -> next FULL Wake, since powerMonitor's resume maps to NSWorkspaceDidWake, which does not fire for dark wake. Drop the suspend breadcrumb (suspend now only stamps a timestamp) and emit a single `system_slept` on resume, gated at 60s. That cuts 70 cycles to 17 over the same week (worst burst 3) while still catching every sleep long enough to swallow a 60s renderer heartbeat. * test(diagnostics): assert resume listeners detach by identity The off mock deleted by event name alone, so teardown detaching a different closure than the one registered still passed -- a leak of the real powerMonitor listener would have gone unnoticed. For 'suspend' that leak has no other observable effect through the public API. Co-authored-by: Orca <help@stably.ai> * fix(diagnostics): span from the first suspend across dark wake powerMonitor 'resume' maps to NSWorkspaceDidWake, which does not fire for dark wake, so macOS can deliver suspend -> suspend -> resume. Overwriting the stamp reported only the trailing segment, and when that segment fell under the 60s gate a 90-minute sleep recorded nothing at all -- leaving the gap looking like the unexplained freeze this is meant to rule out. Co-authored-by: Orca <help@stably.ai> * docs(diagnostics): correct the threshold rationale to match measurement The comment claimed maintenance sleeps would flood the ring. Re-measuring pmset over 6 days (78 sleeps, 29 full wakes, 51 dark wakes) shows they resolve as DarkWake, which never fires 'resume', so they never recorded a breadcrumb at all. Real rate is 29 breadcrumbs / 6 days, worst 60-minute burst 4 against a 30-entry ring. The gate's actual job is narrower: skip sleeps shorter than the 60s heartbeat, which cannot open a gap to explain. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
ba434a4a93 |
fix(sidebar): stop a worktree drag preview from closing the Agent Dashboard (#10531)
The companion-board mutual-exclusion Effect keyed on `workspaceBoardRenderedOpen`, which is `workspaceBoardOpen || workspaceBoardDragPreviewOpen`. WorktreeList sets the drag preview at the start of every card drag — including a pure reorder within a group that never opens the board — so dragging any card closed the Agent Dashboard drawer, and cancelling the drag never restored it. Key the Effect on `workspaceBoardOpen` so only the user actually opening the board evicts the dashboard. The reciprocal Effect is unchanged. |
||
|
|
d8e0f112c6 | perf(terminal): avoid repeated pending drain snapshots (#10163) | ||
|
|
e1eca7f311 |
fix(agent-hooks): install OpenCode status plugin into the WSL guest so status works over WSL (#10328)
* fix(agent-hooks): install OpenCode status plugin into the WSL guest so status works over WSL
OpenCode reports agent status via a JS plugin dropped into OPENCODE_CONFIG_DIR
(unlike Claude/Codex, which use managed hooks.json scripts). Over the WSL
runtime that plugin was never materialized inside the guest and
OPENCODE_CONFIG_DIR never crossed into the guest, so OpenCode status never
reached Orca's sidebar (the workspace stayed green). Codex already worked; this
was OpenCode-specific.
Mirror the SSH plugin-overlay path for WSL:
- Guest relay registers AGENT_HOOK_INSTALL_PLUGINS_METHOD, byte-caps the source,
and materializes an OpenCode config overlay via PluginOverlayManager (the same
electron-free path the SSH relay uses), returning overlayDirs.opencode.
- Host manager ships the plugin source over the existing stdio channel after
installers run (and on mid-session reinstall) and records the guest overlay
dir; -32601 / CONNECTION_LOST / DISPOSED are swallowed like ssh-relay-session.
- PTY env points OPENCODE_CONFIG_DIR/ORCA_OPENCODE_CONFIG_DIR at the guest
overlay; until the relay reports it (first spawn / older guest bundle) it drops
those vars rather than crossing the Windows overlay path into WSL — so
in-guest OpenCode falls back to its own config (pre-fix behavior, no
regression).
- WSLENV passes OPENCODE_CONFIG_DIR/ORCA_OPENCODE_CONFIG_DIR through (/u for
guest paths).
SSH is untouched: the same JSON-RPC constant is reused and the guest response
merely gains an optional overlayDirs field the SSH host ignores.
Runtime repro: native opencode launched in a WSL Orca terminal had
ORCA_AGENT_HOOK_PORT/ORCA_PANE_KEY but no OPENCODE_CONFIG_DIR and no Orca plugin
in ~/.config/opencode, so agentStatusByPaneKey stayed empty.
* fix(agent-hooks): stop the WSL OpenCode overlay leaking Windows paths and churning under running agents
Review fixes on top of the WSL OpenCode plugin install:
- Never cross a Windows OPENCODE_CONFIG_DIR into the guest. The /p flag was
not a defensive default but WSLENV's translate-and-deliver flag, and
buildWslRelaySpawnEnv spreads process.env while the daemon merge resurrects
keys buildPtyHostEnv only deleted -- so a Windows value reached the guest as
/mnt/c/... and was adopted as its OpenCode config root. Register the two
vars only when the value is already a guest POSIX path.
- Make guest materialization idempotent. The overlay id is instance-scoped,
and the host re-ships on every reinstall (60s after connect, and on later
pane spawns), so materializeOpenCode's remove-and-rebuild wiped the config
root under running agents and raced panes spawning against the path just
handed to them. Rebuild only when the shipped source changed or the overlay
went missing.
- Mirror the guest's default ~/.config/opencode (honouring XDG_CONFIG_HOME)
when no explicit dir is discoverable, so pointing OPENCODE_CONFIG_DIR at the
overlay no longer silently drops the user's models/agents/skills/mcp.
- Carry opencodeOverlayDir across relay relaunch; it is instance-keyed and on
the distro's persistent filesystem, so dropping it only blanked status on
panes spawned mid-relaunch.
* fix(agent-hooks): stop advertising a WSL OpenCode overlay the guest failed to rebuild
Round-2 review fixes:
- materializeOpenCode wipes before rebuilding, and every failure path after the
wipe returns null leaving the dir present but plugin-less. The host treated
that null the same as "no handler / teardown" and silently kept the previous
value, so a pane could be pointed at an empty config root -- worse than the
documented fallback of dropping the var. requestGuestOpenCodeOverlayDir now
distinguishes 'none' (guest answered, no dir) from 'unavailable', and the
manager clears the recorded dir on 'none'.
- The handler cache keyed only on plugin source, so a ~/.config/opencode created
after the relay connected was never mirrored for the relay's lifetime. Key on
the resolved source dir too, and validate the cache by the plugin file rather
than the directory -- the directory is exactly what a failed rebuild leaves
behind, so checking it alone made the bad state stick.
* fix(agent-hooks): don't mirror the XDG default OpenCode config into the WSL overlay
OPENCODE_CONFIG_DIR is APPENDED to OpenCode's config-dir list, not a
replacement for it. Verified against the shipped binary: the list is built as
[Path.config, ...project .opencode dirs, ...OPENCODE_CONFIG_DIR ? [it] : []],
and Path.config is derived independently from XDG_CONFIG_HOME/$HOME/.config.
So ~/.config/opencode is read whether or not Orca overrides the var, and the
earlier fallback that mirrored it into the overlay made OpenCode load the
user's config -- and their plugins -- twice. Resolve only an explicitly-set
dir, which is the one case that genuinely leaves the list when Orca overwrites
the variable. This also restores parity with the SSH and local paths.
* docs(agent-hooks): correct the WSL install-plugins cache comment and test framing
The per-call source-dir re-resolution comment still described the XDG default
branch that
|
||
|
|
f009500677 | ci(release-cut): always show resolved commit, branch, and tag in summary (#10482) | ||
|
|
d9aa919e3c |
perf(terminal): drop the JSON structural pre-scan from the history read path (#10499)
* perf(terminal): drop the JSON structural pre-scan from the history read path readTerminalHistoryJson/Async walked every character of checkpoint.json in interpreted JS before handing the same string to native JSON.parse. On a 26.8MB checkpoint that scan measured 63-330ms — 2.7-12x the JSON.parse it guards, and 57% of the whole read path — all of it synchronous main-thread work. readTerminalHistoryJsonAsync exists so cold-restore reads do not block the main thread; running the scan inline right after the async read defeated its own stated purpose. The scan also had a correctness cost: TERMINAL_HISTORY_JSON_MAX_STRUCTURAL_TOKENS was 1_000_000, and oscLinks is unbounded at ~10 structural tokens per link, so roughly 100k OSC-8 hyperlinks tripped the assert, history-reader swallowed the throw to a null checkpoint, and the terminal restored blank — the same user-visible loss #10479 just fixed for the byte cap. checkpoint.json and meta.json are our own SerializeAddon output, not untrusted input: the byte cap still bounds the read, and a corrupt file fails JSON.parse into the same catch. The shared helper stays for the call sites that do handle untrusted JSON. * test(terminal): pin iterative JSON.parse for the dropped nesting-depth cap The retired pre-scan enforced two limits; the new tests only covered the structural-token half. Dropping the 128-level nesting cap is safe solely because V8 parses JSON iteratively — depth costs heap, not stack — so a deeply nested checkpoint parses instead of overflowing. Verified: 10M-deep arrays and objects parse without throwing on V8 14.6. That property is an engine guarantee this code now silently depends on, and a recursive parser would abort the daemon outright rather than throw into the callers' catch. The test fails on main with "JSON nesting exceeds 128 levels" and costs 4ms. * docs(terminal): drop the rot-prone benchmark figure from the reader comment Keeps the durable rationale for skipping the pre-scan (self-authored input, byte cap still bounds the read, corrupt files land in the callers' existing catch) and moves the "~57% of the read path" measurement to the PR body, where it cannot go stale against a later change to this path. Addresses the CodeRabbit comment-length nitpick against the repo's one-line-if-possible comment guideline. |
||
|
|
a0971e0f9b |
test(terminal): pin checkpoint-only cold restore of a large checkpoint (#10500)
* test(terminal): pin checkpoint-only cold restore of a large checkpoint Triaging a report that checkpoint-only cold restore renders blank at every checkpoint size found no v1.4.156 regression: an A/B of v1.4.155 against origin/main returned byte-identical ColdRestoreInfo at 1.01, 5.56, 11.30 and 20.61 MiB, all 300 marker lines intact on both refs. Blankness tracks the meta.endedAt eligibility gate, not size — a cleanly ended session refuses to cold-restore at any size, which is by design and unchanged between the refs. What the triage did surface is a coverage gap. #10179's 16MiB checkpoint read cap sat under what the unbounded writer emits, so a large checkpoint threw, was swallowed to checkpoint=null, and the pane reopened empty; #10479 raised the cap but nothing pinned the round trip it had broken. history-reader-memory covers the bounded reader at its limit, not writer→reader recovery. Adds that round trip through the real HistoryManager writer and HistoryReader over a header-only log, and pins the endedAt gate that has now been mistaken for a size regression twice. Fails at the pre-#10479 cap with the exact production symptom (detectColdRestore returns null). * test(terminal): fail loudly when writeSync is unavailable writeLargeScrollback ignored writeSync's boolean, which is false when xterm's private _core.writeSync goes away. The size assertions catch that in the large-checkpoint test (416 bytes vs 16MiB), but the endedAt gate is size-independent, so that test silently passed on an empty snapshot — pinning the gate over no scrollback at all. * test(terminal): cut large-checkpoint fixture peak RSS from 1.2GiB to 800MiB The filler colored per line, not per cell as its comment claimed, so the serialized seed only tracked plain-text size and needed 26k buffer rows to clear 16MiB. The xterm buffer costs rows x cols, which made this single file raise the whole src/main/daemon/ suite's peak RSS 5.2x (241MiB -> 1244MiB) — a real OOM risk on CI right after #10299 bounded readers for that reason. Carrying the bytes in SGR runs instead of rows reaches a larger seed from 5.5k rows: suite peak RSS 1244MiB -> 793MiB, and the margins improve too (seed 25.16MiB = 1.57x the threshold vs 1.19x before, checkpoint.json 46.1MiB = 2.88x vs 1.20x). Also makes the fixture match its own comment and be more representative of real colored agent output. Re-verified all three mutations still fail: cap at 16MiB -> "expected null not to be null"; endedAt gate deleted -> test 2 fails; writeSync unavailable -> both fail. * test(terminal): tighten timeout, reuse prod dir-name helper, dispose first Review follow-ups, all test-only: - Import getHistorySessionDirName instead of hand-rolling encodeURIComponent. Equivalent today, but that helper exists to absorb encoding changes, so the hand-rolled copy would silently diverge from the writer it is checking. - Index FILLER_ROWS by its own length so growing the array cannot leave rows unused. - 300_000ms -> 60_000ms. The file runs in ~2.8s and vitest's own default is 30s; a 5-minute ceiling turns a hung regression into a stalled job rather than a failure. 60s keeps Windows headroom. - Take the snapshot, dispose, then checkpoint, so a throwing checkpoint() cannot leak the buffer. Note this is memory-neutral, not a saving: peak RSS is set at getSnapshot(), where the buffer and the serialized strings coexist, and measured ~800MiB either way. Mutations re-verified: cap at 16MiB -> "expected null not to be null"; endedAt gate deleted -> test 2 fails; writeSync unavailable -> both fail. |
||
|
|
be0212e278 | Update README downloads badge | ||
|
|
7a01910f20 |
fix(skills): advance the release ledger at the cut so shipped revisions freeze (#10483)
* fix(skills): advance the release ledger at the cut so shipped revisions freeze #10340 made the released-skill registry a function of the committed ledger instead of a git tag walk, and #10460 reverted the cut step that advances that ledger because it violated the #9119 contract (a version-only cut must not regenerate or stage the content-addressed skill artifacts). Both were right; the result is a ledger that never advances. generate-skill-bundle-manifest.mjs:390 derives releasedCount solely from release-mapping.json and :461 assigns a changed skill releaseRevision = releasedCount + 1, while :518 protects only committedReleasedCounts[name] — so index releasedCount is unprotected. A tag ships that tail revision, nothing records it, and the next skill change rebuilds the same revision number over different bytes. Installs carrying the shipped digest then match no snapshot and degrade to unrecognized, which cannot be updated. Restore the advance in a form the #9119 contract can keep enforcing: --release now verifies that current-manifest.json and snapshot-registry.json already match the ref being tagged, appends the mapping row, and writes only release-mapping.json. The cut stages just that file, so it still cannot move a content-addressed artifact — the failure #9119 guarded against — and now fails loudly instead of recording a revision the tag does not ship. The contract test is narrowed to match: it asserts the cut runs --release (never --write) and stages exactly package.json and release-mapping.json. * test(release-cut): close the staging bypasses the narrowed gate left open The narrowed contract test anchored its `git add` scan to line start and only inspected staged paths, so three ways to reintroduce #9119 stayed green: a `git add` chained after `&&`, a write that never calls `git add` at all, and `pnpm run generate:skill-bundle-manifest` — the package.json alias for `--write`, which the hyphenated ban never matched. That last one also passed the pre-#10460 assertions, so it was never covered. Drop the anchor, require every `resources/skills` mention in the step to be exactly what is staged, and ban the alias and `commit -a`. Comments are stripped first so prose cannot trip a ban. Verified each bypass fails and the real workflow passes. * fix(release-cut): make the new provenance failure actionable to an operator Verifying the content-addressed artifacts is the only new way the cut can block, and it fails inside a step named "Bump package.json and tag" with a lint-shaped message. That names the files and the command but not the two things the operator needs: the regeneration has to land on main, and the cut is safe to re-run afterwards. Say so. Also pin down why assertReleasedHistoryPreserved takes the pre-append mapping. It pairs with artifacts.releasedSnapshotCounts, which seeding fixed before the row existed; handing it the post-append mapping makes every cut throw "Released snapshot history is incomplete", which points at tag fetching rather than the real cause. Nothing enforces the pairing. * test(release-cut): gate the whole cut job, not just the bump step Round-2 review defeated the previous gate twice, both proved by running the full contract file green with #9119 reintroduced. Every step in the cut job shares one workspace and one index, but the contract test only inspected `Bump package.json and tag`. A step inserted earlier could run --write and `git add resources/skills`, and the bump step's own commit swept it into the version commit and the tag. Assert job-wide instead: only the bump step may name the directory, and no step may regenerate under either the flag or its package.json alias. That lives in the generator suite because the contract file is at its max-lines cap. Two regexes were also evadable. The mention scan required a trailing slash, so a path held in a variable was invisible; it now matches the directory itself. The `commit -a` ban matched nothing at all — `commit\s` ate the only separator, so `-a`, `-am`, and `--all` all survived while only a trailing `-a` was caught. `--allow-empty` stays allowed. * fix(release-cut): assert the index, not the workflow text, before committing Round-3 review defeated the job-wide grep three ways, each proved by running both test files green with #9119 reintroduced into the tagged commit: an `env:` block holding `--write` and `resources/skills`, a composite action whose steps the workflow never spells out, and plain shell concatenation (`root=resources; leaf=skills`). Grepping shell source for path literals is inherently evadable, and the previous fix only relocated round-2's variable-indirection hole one step over. Move the invariant to where it cannot be dodged: immediately before committing, the cut diffs its own index and refuses anything that is not package.json or the release-mapping row. That does not care which step staged what, or how the path was spelled. The workflow grep stays as a cheap tripwire for literal spellings, now paired with a positive assertion that the index guard exists and precedes the commit — indirection cannot hide a missing guard. Mention matching dedupes and trims quotes, since the guard names the row a second time. * fix(release-cut): match the staged-path allowlist literally `grep -vx` treats its patterns as regexes, so the `.` in `package.json` matched any character: a staged `packageXjson` or a `resources/skills/release-mappingXjson` was silently accepted by the index guard. Verified both slip through `-vx` and are caught by `-vxF`. Exercised the guard against a legitimate cut, an empty index, a staged content-addressed artifact, paths containing a space and a non-ASCII character (git quotes the latter, so it fails closed), and a staged deletion. Only the two allowed paths pass. * test(release-cut): assert the index guard aborts, not just that it exists The positive assertion pinned the guard's shape and its position before the commit, but not its effect: replacing `exit 1` with `:` left both test files green while the cut logged the error and shipped the artifact anyway. That is the same failure this whole gate keeps having — asserting the shape of a defense rather than what it does. Pin the abort too. Verified the neutered guard now fails the suite. * test(release-cut): scope the abort check and catch clustered commit flags Two holes in the guards this PR added, both in the same shape-not-effect class the previous commit was meant to close. The abort assertion's lazy match was not scoped to the guard's own block, so it could borrow an `exit 1` from any later `if ... fi` in the step. Degrading the guard to a warning while adding a plausible HEAD precondition left every test green. Stop the match at the guard's `fi`. The `commit -a` ban only matched when `a` led the flag cluster, so `-vam`, `-va`, `-qam` and `-sam` all survived. That matters more than it looks: `commit -a` stages at commit time, after the index guard has already inspected a clean index, so it is the one way to defeat that guard. Match `a` anywhere in a short-flag cluster; `--allow-empty` and `--amend` stay allowed. Verified both mutants now fail. * fix(release-cut): validate the commit, not the index, before tagging The index guard asserted the wrong thing. `git commit` has a family of forms that commit the working tree rather than the index — `-a`, `-i`, `--only`, and a bare pathspec — so a rogue earlier step could leave regenerated artifacts unstaged and any of those forms would carry them into the tagged commit while the guard saw a clean index and passed. Reproduced end to end: `git commit -i resources` put current-manifest.json and snapshot-registry.json in the tag with all gates green, and `--only resources` additionally dropped package.json from the tag. Banning those flags one by one is the same enumeration game the earlier rounds kept losing. Assert the outcome instead: after committing and before tagging, diff-tree HEAD and refuse anything that is not package.json or the release-mapping row. That is indifferent to which step staged what and to how the commit was spelled. Verified the whole family is now blocked (-i, --only, -a, -am, -vam, pathspec, and an alias expanding to `commit -i`), that a stock commit and an --allow-empty re-cut still pass, and that deleting, neutering, un-anchoring, or relocating the guard each fails the suite. * fix(release-cut): make the commit guard fail closed on a merge commit Plain `git diff-tree` prints nothing for a merge commit, so the guard would have passed silently instead of failing closed — the one direction that matters on a release path. `-m --first-parent` reports the diff against the first parent; verified byte-identical output for an ordinary commit and still empty for the `--allow-empty` re-cut, so nothing else changes. Not reachable today (nothing in the cut job creates a merge, and npm version has no lifecycle hooks defined), but the failure mode is a guard that looks like it ran. Pin the flags in the assertion too, so neither dropping -m nor slipping in a `--diff-filter` can weaken it without failing the suite. |
||
|
|
9ae8f340ae |
fix(cli): explain SIGABRT serve exits instead of naming the signal (#10464)
* fix(cli): explain SIGABRT serve exits instead of naming the signal (#10461) `orca serve` reported only "Orca serve exited via SIGABRT", which sent a P0 investigation down a code-signature path while a diagnostic crash report sat unread on disk. On darwin + SIGABRT the signal-exit path now names the macOS application-startup abort, its usual sandbox/SSH/CI causes, and points at ~/Library/Logs/DiagnosticReports/Orca-*.ips via the existing nextSteps channel. Other platforms and signals get a clear message with no invented cause. * fix(cli): stop asserting the SIGABRT exit happened at startup * fix(cli): stop steering macOS SIGABRT users away from SSH serve |
||
|
|
12fa5ff79e |
fix(mobile): heal an orphaned native-chat image paste across screen unmounts (#10480)
* fix(mobile): heal an orphaned native-chat image paste across screen unmounts The stale-input marker lived in a per-screen `useRef`, but the condition it tracks — a bracketed image paste sitting unsubmitted on the agent's composer line — lives on the host and outlives the screen. Backing out of a session and returning remounted the hook with an empty Set, so the next message submitted on top of the orphaned paste and the agent received `<image path><text>`. Move the marker to a module-level store keyed by terminal handle, and consult and consume it from every write path that can submit the composer: the image hook's text-only send, the controller send (which the chat overlay's question card reaches directly, bypassing the image hook), and the ask-answer send. Permission choices and the Escape cancel deliberately do NOT heal: they are `enter: false` keys for an active overlay that swallows the clear, so healing there would consume the marker without clearing the line and leave the next real message corrupted. Desktop scopes its Ctrl+U the same way. * fix(mobile): stop the ask heal from burning the marker on selector answers The heal ran on every ask answer, but Claude's and Codex's selector shapes cannot submit the composer: a single-select answer is a bare option digit and every stepping group is written `enter: false` (the host coerces it), so the clear is swallowed by the live overlay while the host still acks the write. That consumed the one-shot marker and left the orphaned paste to corrupt the next real message — the same failure this PR exists to fix, through a new door that main did not have. Scope the heal to the pasted-label shape, which does commit the composer. Desktop splits it the same way: use-native-chat-interactive-send.ts routes only the non-stepping answer through the clearing sender and never pre-clears sendNativeChatAskAnswer. Also pin the three deliberate skips (selector answer, permission choice, Escape cancel) with tests, so the PR's central design argument is an invariant rather than a comment, and guard the failed-heal toast with the generation check every other error surface in answerAsk already uses. |
||
|
|
6c03ecf8e1 |
fix(terminal): stop cold restore dropping all scrollback on large checkpoints (#10479)
* fix(terminal): stop cold restore dropping all scrollback on large checkpoints The checkpoint read cap shipped without its write bound. history-reader.ts reads checkpoint.json through a 16MiB cap that throws past the limit, and the catch swallows it to checkpoint=null; history-manager.ts still writes the checkpoint with an unbounded JSON.stringify. Every fallback then collapses (stale-generation log, unlinked legacy scrollback.bin), so the terminal reopens empty with nothing surfaced. Raise the read cap to cover the largest checkpoint the writer can legitimately emit, derived from the scrollback policy's 50k-row max preset so it cannot drift back under the writer. A bound is kept so a corrupt file still cannot OOM the main process. Bounding the writer instead would not recover the scrollback: the stringify throw lands in handleWriteError, which adds the session to disabledSessions and permanently stops history recording for it. * fix(terminal): anchor checkpoint read cap to its own reasoning The cap was derived as 2 * LEGACY_TERMINAL_SCROLLBACK_BYTES_100_MB, but that constant is a legacy byte-preset setting value with no other consumer, and the 50k-row bucket it was attributed to has no upper byte bound. Same value, stated without the false policy linkage. * fix(terminal): assert the checkpoint byte cap, correct its rationale The retained oversized-checkpoint test passed with the byte guard removed entirely — 200MB of NUL fails JSON.parse, so detectColdRestore returned null either way. Assert the bounded reader directly, as the amplification test does. Also: a 50k-row max preset of ordinary text measures ~14MB serialized, not 'far below' by an unbounded margin — per-cell-colored output can still exceed the cap, which is what a writer-side snapshot trim has to fix. |
||
|
|
6592c01592 |
fix(daemon): keep agent-completion detection alive on pre-v27 daemons (#10478)
* fix(daemon): keep agent-completion detection alive on pre-v27 daemons DaemonPtyAdapter.inspectProcess() threw terminal_liveness_unavailable when the connected daemon predated protocol v27. The intended provider-level fallback only fires when a provider lacks inspectProcess, so for the daemon adapter the throw propagated: agent-completion-coordinator swallowed it into consecutiveInspectionErrors and retried forever, killing process-exit completions and pending-title validation. Daemons intentionally survive app updates, so updating in place with agent terminals open routes those PTYs to a legacy adapter and permanently disables agent-finished notifications until the terminal is recreated. Compose the inspection client-side from getForegroundProcess, which v26 fully supports. No new wire traffic and no new daemon capability. * test(daemon): pin null-foreground semantics on the pre-v27 inspect fallback The legacy composition had no coverage for a null foreground, which is the one daemon response shape whose semantics diverge from v27: there inspectProcess goes through getAliveSession() and throws for a vanished session, while getForegroundProcess is deliberately null-not-throw. It is also the only shape that reaches a user-visible completion, so reading it as idle is a deliberate choice that should not change silently. |
||
|
|
2653794c82 |
fix(terminal): verify Windows PTY root identity before taskkill /T /F (#10484)
* fix(terminal): verify Windows PTY root identity before taskkill /T /F killWithDescendantSweep guarded its Windows tree kill with ownsRoot() alone, which is JS state only. node-pty's ConPTY exit watcher closes the last shell handle before it queues the JS exit callback, so Windows can recycle the PID while the session map still looks live — force-killing an unrelated process and its whole descendant tree. Walk the recycled PID's ancestry back to this process before taskkill: skip the sweep when the root is gone or resolves to a stranger, and keep the sweep when identity is unknown so #10004 orphan cleanup still runs. Also gate the local provider's ownsRoot on observed physical exit. * fix(terminal): dedupe the Windows root-identity scan, drop dead exit gate Review fixes on the PID-identity guard. The probe read the process table through a new uncached export, bypassing the reader that worktree teardown depends on: worktree-teardown.ts fans out 32-wide inside a 10s deadline, so a delete forked 32 powershell cold-starts (the churn windows-foreground-process-rows.ts:25-32 warns about, #6288/#6667). getFreshSnapshot() already guarantees a scan that starts after the request -- the exact property the bypass existed for -- and coalesces concurrent callers, so use it. Measured on the new test: 32 scans -> 1. The PhysicalExitTracker.hasExited gate could never fire. markExited() is only reached at local-pty-provider.ts:985/:1431, and both are followed synchronously by clearPtyState(), which deletes the ptyProcesses entry -- so ownsRoot's map check is already false whenever hasExited is true. Reverting it broke no test. Drop it and the shared getter it added; the identity probe already covers every ownsRoot caller from inside killWithDescendantSweep. Also point the Windows terminal-restart E2E job at the files that own this behavior, so a change to the new Windows-only module runs the one job that executes on a real Windows host. * docs(terminal): state what the Windows root probe actually proves The probe checks subtree membership, not root identity: a recycle that lands on another Orca descendant (another pane's shell, an agent CLI, a git.exe we spawned) still reads `own`, and that is not remote during teardown when Orca is itself allocating pids. It bounds the blast radius rather than closing the class. Say so at the type and at classifyWindowsTreeKillTarget, and name what a real close would need (a CreationDate baseline -- the analogue of the POSIX lstart check already used here -- or an inherited handle / Job Object). Also note why our own pid must classify `foreign`. * ci(windows): trigger the terminal-restart E2E on the shared snapshot reader The Windows root-identity probe now reads through getFreshSnapshot, so an edit to that module changes Windows teardown behavior without touching any path the job already watches. * test(terminal): guard the teardown probe against a reintroduced scan bypass The existing volume guard covers queryWindowsProcessRowsFresh directly, but the identity-probe cases all inject readRows, so nothing exercised the DEFAULT reader wiring -- a bypass reintroduced inside windows-pty-root-identity would have gone unnoticed. Drive verifyWindowsTreeKillTarget 32-wide through the real reader and assert one scan. Verified it fails at 32 when the bypass is put back. |
||
|
|
879aad7dd6 |
oom(foundation): bound shared readers/limits + add BoundedMap primitive (#10299)
* oom(01): A1-shared-readers — reintroduce #10179 subset Files: 18 applied, 0 deleted (from |
||
|
|
cafa958e70 |
fix(ui): stop the reveal reflow from dismissing popovers (#10487)
* fix(ui): stop the reveal reflow from dismissing popovers The main process reflows the renderer on every reveal, resume, and restore. On macOS 26 that nudges the emulated device scale factor, and Chromium fires a real window resize for it even though innerWidth/innerHeight are identical. Anything bound directly to resize treated that as a user resize. The selection copy menu and the markdown link bubble both dismiss on resize unconditionally, so they closed on their own whenever the window was revealed or restored. Gate both on an actual dimension change. Only fixes the macOS 26 path: the pre-Tahoe repaint jiggles the native frame by a real pixel, so the renderer sees a genuine dimension change and no change-detection guard can — or should — filter it. Co-authored-by: Orca <help@stably.ai> * test(ui): cover the reveal-reflow guard at the component level Pins both halves of the wiring: a same-size resize must not dismiss, a real one still must. Mutation-checked — reverting to a bare resize listener, or dropping the listener entirely, each fails the suite. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
46942783c4 |
fix(window): reflow via scale factor so terminals stop re-gridding (#10485)
The +1px emulated viewport changed the CSS box, so a terminal sitting one pixel under an xterm row boundary gained a row. The pane fit observer's two-frame stability check reads that transient grid as stable well inside the 32ms hold, forwards a real PTY resize, then reverses it on restore — two SIGWINCHes per reveal, measured on 3/54 window heights (~1/cellHeight). Nudging the device scale factor instead re-runs layout with byte-identical CSS geometry. Same reflow, 0/54 SIGWINCH, no WebGL atlas rebuild or context loss, and webview guests stop seeing spurious native resizes too. Co-authored-by: Orca <help@stably.ai> |
||
|
|
81bfb3c396 |
oom(02/29): bound shared image/media/PDF memory limits (#10295)
* oom(01): A1-shared-readers — reintroduce #10179 subset Files: 18 applied, 0 deleted (from |
||
|
|
c419aadb19 |
oom(01): A1-shared-readers — reintroduce #10179 subset (#10294)
Files: 18 applied, 0 deleted (from
|
||
|
|
c468e3f8b8 |
fix(macos): close both macOS 26 main-thread deadlock doors and restore the reveal reflow (#10473)
* fix(window): restore the macOS 26 reflow without touching the native frame #10253 stopped the main-thread deadlock by skipping the repaint size nudge on macOS 26, but invalidate() repaints without reflowing, so the h-dvh root kept a stale viewport height and the status bar stayed clipped off-screen (STA-2383) on every Tahoe reveal, restore and wake. Drive the reflow through device emulation instead: a +1px emulated viewport, reverted a frame later, makes the renderer recompute layout without any NSWindow mutation, so the FrontBoardServices re-entrancy that wedged the main thread for 109 minutes is still never triggered. Verified against real Electron 43.1.0 on macOS 26.3.1 (Darwin 25.3.0): the renderer sees the resize and relayouts, the native frame is untouched, the viewport and devicePixelRatio restore exactly across zoom levels, overlapping calls collapse to one cycle, and a 1px delta never crosses a terminal cell boundary so no pane reports new geometry (no SIGWINCH to running shells). Also close two gaps in the surrounding code: - the pre-Tahoe size jiggle now clears its WeakSet latch in a finally block, so a throwing setSize can no longer suppress every later repaint for that window - cover powerMonitor 'resume' under the Tahoe guard, the other AppKit dispatch context implicated in the freeze * fix(tray): keep NSStatusItem scene updates off the AppKit callout stack The main-window repaint was only one of the two doors into the macOS 26 FrontBoardServices deadlock. Showing or restoring the window calls setTrayAttention(false) straight from the window event handler, and tray.setImage/setToolTip drive an NSStatusItem scene update — the same re-entrant scene mutation from inside AppKit's own dispatch, matching the stackshot in openai/codex#23695. Defer the native mutation to a fresh event-loop turn so the callout frame is vacated first. The attention flag itself still flips synchronously: rapid show/hide would otherwise mis-dedupe against a value that had not landed yet. Bursts collapse to a single repaint, and because the deferred pass reads current module state rather than a captured value, a coalesced schedule can never apply a stale icon. applyTrayImage already no-ops on a destroyed tray, so a repaint still queued when the tray goes away is harmless. * fix(window): reflow maximized and fullscreen windows on macOS 26 too The maximized/fullscreen bail-out predates the Tahoe path and exists only to keep the size nudge from resizing a window out of those states. Emulation never touches the frame, so that guard was suppressing the reflow for no reason — and a maximized window strands its dvh layout exactly like a normal one. Run the Tahoe branch before the guard. Verified on macOS 26.3.1 that the emulated viewport reflows a maximized and a fullscreen window while leaving both states intact. * fix(window): retry the viewport restore instead of stranding the renderer If disableDeviceEmulation threw while the webContents was still alive, the previous code swallowed the error and cleared the latch anyway, leaving the renderer pinned at the emulated 1px-taller viewport for the rest of the window's life — and letting the next reveal stack a fresh cycle on top of it. Retry the restore on a bounded schedule and hold the latch while a retry is pending. A destroyed webContents still short-circuits, since the emulated viewport dies with it, and the attempt budget keeps a permanently failing restore from pinning the latch forever. |
||
|
|
b4718dd05c |
fix(terminal): resume hibernated agents that reattach with no payload (#9648)
* fix(terminal): resume hibernated agents that reattach with no payload When agent hibernation is on, a stopped (done) agent's PTY is killed and a passive sleeping record is kept; returning to the worktree relies on the pane reattaching on remount. On the daemon path (Windows/local worktrees) the daemon can reattach the hibernation-killed session as already-live (isReattach, isNew:false) and return no snapshot/replay/coldRestore — and, being a reattach rather than a fresh spawn, it silently drops the --resume command passed on connect. The renderer adopted that empty session, leaving a blank terminal with nothing running and the sidebar history still pointing at the dead tab (the sleeping record never cleared). A reopened pane that owns a resumable slept session must always be re-driven with its resume command, never left as a bare empty attach. handleReattachResult now discards a contentless isReattach for a pane with a hibernation record and re-drives the prepared resume. This excludes the healthy cases: a fresh session the daemon created (isReattach falsy — it already ran the command) and a live reattach (carries a snapshot/replay). Forward the isReattach signal the transport was dropping so the two cases are distinguishable. Adds a deterministic regression test that fails without the guard and passes with it. * fix(terminal): preserve provider ownership on resume * chore(skills): refresh generated skill bundle manifests Regenerate the skill bundle artifacts against the full release-tag set so the freshness verify check passes. Append-only additions for the newer release tags; no released snapshot history is rewritten. |
||
|
|
7e1d7a825b | release: v1.4.156-rc.1 v1.4.156-rc.1 | ||
|
|
c3526cc19d |
feat(codex): surface a stalled config sync instead of failing silently (#10449)
* feat(codex): surface a stalled config sync instead of failing silently Why: the mirror keeps serving the last synced settings when ~/.codex/config.toml is missing, blank, or unreadable. That is the right call for data safety, but it is invisible — a downed WSL distro or an unhydrated cloud-synced home leaves "Orca ignores my config edits" with no log line and no UI to diagnose. Status is derived on demand from the same predicates the mirror uses, so the two cannot disagree. The stall is logged once per episode rather than on every launch and quota poll, and the Codex account section names the file and what to do. * fix(codex): latch an unreadable source and stop over-claiming recovery An unreadable source throws out of the mirror, so reporting only on the success path left that stall latch-less: it logged the raw failure on every launch and quota poll while its reason never reached the surfaced status. Report from the catch path too. The clear message also claimed the source was "readable again", which is false when the stall ended because the runtime config was removed rather than because the source came back. Restoring console.warn now happens in afterEach — an inline mockRestore is skipped by a failing assertion, and the leaked spy made every later case in the block fail spuriously. * fix(codex): latch the stall promotion hits first, and scope it to the host Review round 1 findings: - The unreadable-source latch still never fired in the steady state. Once a baseline exists, promotion reads the source before the mirror does, so it throws first and `!promotionPlan` returned before any reporting — logging a reasonless failure every launch and quota poll, which is exactly what the previous commit claimed to fix. Report from that branch too. The test only passed because its fixture had no baseline; it now seeds one first and fails without the fix. - The banner named the host's ~/.codex while a WSL or per-account runtime was selected, whose real source is a different file entirely. Gate it to the host scope, matching how the sign-in warning is already gated. - Three new translate keys were missing from the locale catalogs, failing the localization gate in `pnpm lint`. - The registrar mock was never asserted, so deleting the registration left the suite green. - `codexConfigSyncStatus` hung off the `agentHooks` namespace despite having nothing to do with agent hooks; moved to its own `codexConfigSync.status` while it is still a four-file change. * fix(codex): report sync health for the home the selection actually mirrors Review round 2: - The status resolved the shared runtime home, but the system default now runs Codex directly against ~/.codex and managed accounts get their own home. So a stalled per-account mirror showed no banner at all, while a stale shared home could warn about a config the active lane never reads. Resolve the mirrored home from the current selection, and report synced when the lane has no mirror to fall behind. - The round-1 report on the promotion failure path could clear the latch on a pass where no mirror ran, claiming a recovery that never happened and silencing every later pass. Only ever latch a stall there; leave clearing to the path that actually mirrored. * fix(codex): refetch sync status when the active Codex account changes Review round 3: - Resolving the status per selection made the fetch account-dependent, but the effect was not keyed on the active account. Switching accounts left the banner describing the previous one — and switching INTO a stalled account showed nothing at all, which is the silence this change exists to remove. - Pin the home resolution itself: it had no direct test, and its shared-home path was a hand-copied literal that could drift from the real helper and silence the banner with every other test still green. - Narrow the handler's dependency to the one method it calls, which also drops an `as unknown as` cast from its test. - Skip the chmod-based test on Windows, where a read-only directory does not block writes so the scenario cannot be constructed; matches the convention already used in config-settings-promotion.test.ts. * chore(codex): restore the handler docstring and isolate the resolver suite Round 4 returned clean; these are its two non-blocking nits. Narrowing the handler param left its JSDoc stranded above the new type, so the function had no hover doc. The resolver suite also read the developer's real CODEX_HOME and shell rc, so anyone exporting one would see it fail locally. |
||
|
|
9acb685816 |
fix(ssh): re-arm remote watches that die without a reconnect (#10470)
* fix(ssh): re-arm remote file watches when the provider reconnects Remote file changes stopped being detected over SSH until the file was reopened. The watch pipeline itself was fine — nothing ever re-established the subscription after the transport it was made on went away. Two paths left an editor tab permanently stale: - A reconnect kills the relay's watch registrations, and the previous provider's unwatch handle belongs to the dead transport. - A connect slower than the 60s retry window made installRemoteWatcher give up for good; first deploy to a new host far exceeds that. Neither recovered, because installRemoteWatcher is only reachable from the fs:watchWorktree handler, the retry timer, and the removal-restore path, and the renderer only issues a watch for newly added targets. Reopening the file just re-read it — the watcher stayed dead. Give the layer that owns the transport the job of re-arming: registerSshFilesystemProvider now notifies subscribers, which covers both establish and reconnect since registerProviders runs on both. The watcher keeps the intent to watch in a registry that outlives any single connection, and on registration drops the stale entry (installRemoteWatcher treats an existing entry as installed and would otherwise hand back a watcher that can never fire), reinstalls, and emits overflow so consumers resync the gap. Intent is dropped on unwatch and sender destroy so a closed tab is never resurrected. Verified over SSH to a Rocky Linux 10 host: after the reconnect that previously killed it, a remote append lands in the editor in ~2s with a live remote watcher process, and a second edit in ~1.5s. * fix(ssh): drop watch intent when a remote worktree is removed A removed worktree kept its entry in the intent registry, so a reconnect landing before the renderer's unwatch would re-watch a deleted path — 60s of retries against the host and then a bogus overflow. Also covers two reinstall cases: several senders on one connection must collapse onto a single relay watch (and all of them resync), and a destroyed renderer must not be reinstalled. * fix(ssh): resync when a reconnect's watch only lands on a retry The reinstall emitted the overflow only for listeners whose first install returned 'installed'. If that attempt failed — relay-watcher.js still spawning on the fresh transport, a transient fs.watch rejection — the 1s retry restored the watch but never signalled the gap, so everything that changed while the transport was down stayed invisible: the STA-2525 symptom reappearing inside the fix for it. Thread the resync intent through the retry record so the overflow lands when the retry does. All pre-existing callers default to false, so the watch/terminal-error/restore paths are unchanged. * test(ssh): cover the resync merge when a fresh watch claims the retry slot The `resyncOnInstall ||=` merge was uncovered: removing it left every existing test green. It is load-bearing — if a second renderer joins the reinstall's failing install and reaches the retry slot first with resync=false, the whole chain stays false and the retry restores the watch without ever signalling the gap. New sibling file rather than an append: filesystem-watcher.test.ts is ~10 counted lines from the 800-line lint cap, and AGENTS.md forbids a max-lines disable. * fix(ssh): re-arm a remote watch that died without a reconnect The 60s fast-retry window gave up with a single overflow and no further trigger — provider registration is the only re-arm, and a watch killed by remote OOM/inotify exhaustion leaves the SSH link perfectly healthy. Back off from 1min to a 30min ceiling instead, and stand down entirely when the provider is gone so registration owns that case. * fix(ssh): re-arm the remote file-explorer watch on reconnect SshFilesystemProvider.dispose() stops each watch registration without invoking its terminal callbacks, so a dropped transport left the runtime file-explorer watch silently dead: the lease's restart only runs from the failed-removal path, and the renderer's runtime subscription stays open because it rides a different link. Reinstall on the connection's next provider registration and emit overflow so clients resync. |
||
|
|
a903304ad4 | Update README downloads badge | ||
|
|
b9a7c00df5 |
fix(editor): read out-of-worktree SSH paths without a stamped target id (#9743) (#10455)
* fix(editor): read out-of-worktree SSH paths without a stamped target id (#9743) Route external absolute-path tabs by their resolved SSH connection instead of the externalSshTargetId stamp, which only the terminal-link open path sets. The stamp keeps its fail-closed role when present. * fix(editor): keep client-local live-tail log tabs off the worktree SSH host AI Vault "View Log" tabs are opened client-local by construction (readOnly + liveTail, no runtime env), so inferring the external SSH owner from the worktree connection made them read the remote host instead of the granted client path. Restrict that inference to non-live-tail tabs; an explicit externalSshTargetId stamp still routes remotely. |
||
|
|
fb5ced64a7 |
fix(ssh): re-arm remote file watches when the provider reconnects (#10445)
* fix(ssh): re-arm remote file watches when the provider reconnects Remote file changes stopped being detected over SSH until the file was reopened. The watch pipeline itself was fine — nothing ever re-established the subscription after the transport it was made on went away. Two paths left an editor tab permanently stale: - A reconnect kills the relay's watch registrations, and the previous provider's unwatch handle belongs to the dead transport. - A connect slower than the 60s retry window made installRemoteWatcher give up for good; first deploy to a new host far exceeds that. Neither recovered, because installRemoteWatcher is only reachable from the fs:watchWorktree handler, the retry timer, and the removal-restore path, and the renderer only issues a watch for newly added targets. Reopening the file just re-read it — the watcher stayed dead. Give the layer that owns the transport the job of re-arming: registerSshFilesystemProvider now notifies subscribers, which covers both establish and reconnect since registerProviders runs on both. The watcher keeps the intent to watch in a registry that outlives any single connection, and on registration drops the stale entry (installRemoteWatcher treats an existing entry as installed and would otherwise hand back a watcher that can never fire), reinstalls, and emits overflow so consumers resync the gap. Intent is dropped on unwatch and sender destroy so a closed tab is never resurrected. Verified over SSH to a Rocky Linux 10 host: after the reconnect that previously killed it, a remote append lands in the editor in ~2s with a live remote watcher process, and a second edit in ~1.5s. * fix(ssh): drop watch intent when a remote worktree is removed A removed worktree kept its entry in the intent registry, so a reconnect landing before the renderer's unwatch would re-watch a deleted path — 60s of retries against the host and then a bogus overflow. Also covers two reinstall cases: several senders on one connection must collapse onto a single relay watch (and all of them resync), and a destroyed renderer must not be reinstalled. * fix(ssh): resync when a reconnect's watch only lands on a retry The reinstall emitted the overflow only for listeners whose first install returned 'installed'. If that attempt failed — relay-watcher.js still spawning on the fresh transport, a transient fs.watch rejection — the 1s retry restored the watch but never signalled the gap, so everything that changed while the transport was down stayed invisible: the STA-2525 symptom reappearing inside the fix for it. Thread the resync intent through the retry record so the overflow lands when the retry does. All pre-existing callers default to false, so the watch/terminal-error/restore paths are unchanged. * test(ssh): cover the resync merge when a fresh watch claims the retry slot The `resyncOnInstall ||=` merge was uncovered: removing it left every existing test green. It is load-bearing — if a second renderer joins the reinstall's failing install and reaches the retry slot first with resync=false, the whole chain stays false and the retry restores the watch without ever signalling the gap. New sibling file rather than an append: filesystem-watcher.test.ts is ~10 counted lines from the 800-line lint cap, and AGENTS.md forbids a max-lines disable. |
||
|
|
49e32ff2b4 |
fix: prevent UI freeze from dual-modal race in action sheets (#10432)
Add closeBeforePress flag to Rename, Browser, and Refresh actions to defer modal opening until the action sheet closes. Eliminates the race condition that caused the mobile app to freeze when opening these modals. |
||
|
|
d5340fd191 |
ci(release-cut): restore the skill-independent version commit (#10460)
#10340 added a ledger-advance step to the release cut, which violates the contract test #9119 added: the cut must not run generate-skill-bundle-manifest or stage resources/skills. Both PRs were green on their own branches and only conflicted once merged, so nothing failed until main had both — main and every open PR have been red since. Revert the two workflow lines. The script's --release implementation stays: it is correct and harmless when unused, and the root fix in #10340 — verify no longer walking git tags — does not depend on the cut step. This leaves #10340 semantically incomplete and that must not be dropped. With the registry seeded from the committed ledger instead of a tag walk, nothing advances the ledger at cut, so each new skill change re-uses the same unreleased tail revision for different bytes; older installs then match no known snapshot and degrade to unrecognized, which reads in the UI as a skill that needs attention and cannot be updated. Follow-up is to reintroduce the advance narrowly — stage only resources/skills/release-mapping.json and narrow the assertion to forbid mutating the content-addressed artifacts while permitting the provenance row. |
||
|
|
1aaf049a4d |
fix(rate-limits): keep Codex PTY reset text for weekly-only plans (#8643)
* fix(rate-limits): keep Codex PTY reset text for weekly-only plans The PTY /status fallback parses '5h limit' and 'Weekly limit' lines by label, but the extracted reset text was only ever attached to the session window. Codex plans without a 5h session bucket (e.g. current Pro) produce a weekly-only parse, so the reset time the CLI printed was silently dropped. Fall back to the weekly window when no session window exists. * review: parse Codex PTY reset text per window into resetsAt * review: make Codex PTY status fallback work on codex >=0.145 * review: harden PTY status parse against model-scoped rows and styled output * fix(rate-limits): strip private PTY control sequences --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
e133d93b7b |
fix(skills): stop promising a skill update the command cannot deliver (#10436)
* fix(skills): stop promising a skill update the command cannot deliver An "Update available" badge could never clear: pressing Update ran `npx skills update <name> --global`, which reported "All global skills are up to date" and wrote nothing, while the badge stayed on. Freshness marked a name updatable whenever ANY placement was outdated, including a standalone duplicate in an agent home. The global command only converges the canonical copy and its symlink aliases, so a stale duplicate kept the badge lit with no command that could clear it. - Count only reliably-convergent placements toward the update promise, so a stale duplicate no longer advertises an update that cannot land. - Give the duplicate chip a real skipped-reason instead of falling through to the generic sentence, naming the copy and how to resolve it. - Surface the existing freshness review dialog from the setup rails via a Details link, so a blocked or duplicate copy is explainable where the user actually sees the badge. Covers orchestration, Computer Use, Ephemeral VMs, Linear, the CLI section, the floating orchestration modal, the Browser Use card, and the Mobile Emulator row. * fix(skills): say when a skill copy needs attention instead of reading as all-clear A skill with an out-of-date copy the update command cannot reach rendered as a green "Installed" pill. That is honest about the main copy but reads as all-clear, so real drift in an agent home stayed invisible — the user had no reason to suspect there was anything to click. - Add a needs-attention display status (amber) for a placement that is not current but has no eligible update: stale duplicates, edited copies, read-only, inaccessible, and broken or external links. Presence-only stays green, and an unscanned inventory stays quiet so nothing flashes amber on launch. - State the reason inline on the setup rails, so the cause is readable without opening the review dialog; Details still opens the full per-location list. - Extract the skipped-reason sentence into its own module so the rails and the dialog share one source and can never drift apart. - Carry the state through the settings sidebar badge so the nav and the card cannot disagree. * fix(skills): give the inline skill warning something to point at The shared reason sentences are deictic — "this copy", "the copy here" — because they were written for the review dialog, where the location rows they describe sit directly beneath them. On the setup rails there are no rows, so "this" referred to nothing and the sentence read as if it were about the skill itself. Name the offending paths above the sentence on the rails, so the referent is present before the wording that depends on it. Every copy sharing the blocking reason is listed, not just the first, so resolving one does not leave the badge unexplained. The dialog keeps its existing wording and rows unchanged. * fix(skills): stop withholding the update over copies it cannot reach Eligibility is now decided purely over the placements the global command actually converges — the canonical copy and its symlink aliases. A project skill, plugin cache, standalone duplicate, or unreadable copy in another agent's home used to withhold the update from the whole name. `skills update --global` provably never writes any of them, so that refused work the command could have done over a copy that was never at stake. A blocked *convergent* copy still withholds it: that is the placement the command writes to, and overwriting it is the real data-loss case. Also drop the inline reason from the setup rails. The reasons are written to sit beside the location rows they describe, so on a card they had nothing to point at and could only ever name one cause. The rails now mark Details with a warning icon when a copy needs the user's own hands, and the dialog does the explaining with every location and cause it knows about. * fix(skills): make the skill Details affordance read as a control The review link rendered as bare text, so nothing but hover said it could be clicked — the badge told the user something was wrong and then gave them no visible way in. Use the ghost variant so it carries a hover/focus background and a real hit target instead of a zero-padding text run, and add a chevron so it reads as a control at rest. The chevron points right, not down: this opens the review dialog, while a down chevron already means the in-place expander inside that dialog. |
||
|
|
b600e25fa1 |
feat(worktrees): support project-level .worktreeinclude (literal paths) for copying gitignored files into worktrees (#9791)
* feat(worktrees): copy project-level .worktreeinclude paths into new worktrees Read .worktreeinclude at the repo root (gitignore syntax) and copy matching gitignored paths from the primary checkout into each newly created local worktree, so .env and other local config carry over with zero per-user setup. - Literal patterns resolve by direct stat; globs match against ls-files --others --ignored --exclude-standard --directory (collapsed dirs keep huge repos fast); every candidate is re-verified with check-ignore so tracked or unignored files are never copied. - Copy semantics, never symlink: APFS clone-copy on macOS, real copy elsewhere, so each worktree owns its files (unlike repo.symlinkPaths, which it merges with rather than replaces). - Failures never block worktree creation. - Remote (SSH) creation skips it, same as symlinkPaths. - Split APFS clone helpers into worktree-apfs-clone.ts (max-lines). Closes #7549 * fix(worktrees): harden worktree include copying * fix(worktrees): support nested includes on Git 2.25 * fix(worktrees): bound include copy costs * fix(worktrees): close include correctness and perf gaps * fix(worktrees): preserve included copy semantics * fix(worktrees): harden include resolution * fix(worktrees): preserve bounded include resolution * fix(worktrees): bound include filesystem resolution * fix(worktrees): harden include matching * fix(worktrees): tighten include matching and scan bounds * fix(types): use concrete filesystem stat types * fix(worktrees): harden included path materialization * perf(worktrees): stop include parsing at resolver budgets * chore(skills): refresh bundled skill manifests * refactor(worktrees): reduce .worktreeinclude to focused literal-only scope The reviewed implementation grew well past the ticket (#7549), which asks for a size-M feature that reuses existing worktree machinery. Trim back to the minimal change that solves the reported problem safely: - Resolver now supports literal files and directories only. Glob/negation lines are skipped with a warning (documented follow-up), which removes the entire user-controlled-regex ReDoS surface, the CPU/byte budgets, the git enumeration scan, and the case-sensitivity engine. The filesystem + git check-ignore handle existence and case for free. - Copy layer folded back into worktree-symlinks.ts (link/copy modes share one loop); dropped worktree-path-copy.ts, worktree-target-safety.ts, the descendant-dedup/realpath/target-parent machinery, and the per-materialization APFS filesystem cache. Kept the df/diskutil probe timeout. - Reverted unrelated changes: check-ignored-paths timeout param and the git-binary-compatibility enumeration tests. Net: -1903/+172 across the include+copy code. Behavior for the ticket's cases (.env, .env.local, .vscode/, node_modules, config/secrets.json) is unchanged; gitignored-only + copy-not-symlink semantics preserved. Closes #7549 * fix(worktrees): dereference symlinked .worktreeinclude entries + cache APFS volume probe Two issues found by review + perf audit of the copy path: - Correctness (HIGH): a listed entry that is itself a gitignored symlink was copied AS a symlink (fs.cp dereference:false), and the darwin APFS branch was skipped for all symlink sources. Editing the worktree's copy then wrote through to the shared/primary target — inverting copy-mode's 'each worktree owns its files' guarantee, and escaping the worktree entirely if the link pointed outside it. Now resolve realpath for a top-level symlink in copy mode so we copy content; nested symlinks inside a copied dir stay as-is (cp -R semantics). - Perf: assertSameApfsVolume ran df+diskutil per copied path (4 subprocesses each), so an N-entry include spawned ~4N short-lived processes on the macOS create hot path, all re-probing one volume. Add a per-materialization device-keyed cache: one probe per distinct volume (4N -> ~4). Tests: symlinked-file and symlinked-dir dereference regressions (no leak to primary); APFS volume probed once regardless of copied-path count. |
||
|
|
4dcb68f8fb | release: v1.4.156-rc.0 v1.4.156-rc.0 | ||
|
|
506b75c768 | Update README downloads badge |