mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 08:04:05 +00:00
531b620a44bcda42cffadbae59808933aa8a1e11
3429
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
531b620a44 | fix(browser-preview): deny runtime permissions in document previews | ||
|
|
12fb8e75dc | feat(browser-preview): render remote HTML docs locally over an orca-preview scheme (STA-5557) (#16679) | ||
|
|
cda2280d63 |
Show all automations (#16532)
* Add all-host automations with scoped ownership and multi-authority suppo
Enable automations to run on multiple hosts (SSH targets and local) with
owner-fenced mutations, scoped list queries per host, and conflict
resolution. Introduces desktop and runtime authorities as distinct
automation storage owners, with per-host caching, invalidation, and
retry scheduling on the renderer. Captures registration generations for
SSH hosts to survive re-adoption. Adds CLI support for destination
selection and conflict recovery.
* Filter automation create projects by destination host
Only offer projects available on the selected destination, preventing
the mismatches that would fail at submit time. Auto-adjust the project
selection if it becomes unavailable when the destination changes.
* Add runtime storage authority support for automations
- Support both runtime and desktop as automation storage authorities
- Make owner preconditions optional for legacy-client compatibility
- Cache automation list projections to improve performance
- Add per-row repo/worktree resolution for cross-authority collisions
- Extend automation.list RPC to always include owner metadata
* Replace child_process.execFile with runProcess for external automations
- Migrate external-manager to use cross-platform runProcess wrapper per child-process safety policy
- Abstract electron app/ipcMain APIs in orca-runtime via environment accessors
- Install fake app environment in automation tests for consistent setup
- Reorganize imports to use specific module paths (ssh-target-registry, agent-detection, browser-error)
- Remove external-manager from child-process import allowlists (no longer violates direct import)
* Unify desktop automation CRUD onto the local runtime RPC surface
The desktop authority now speaks the same automation.* RPC contract as
remote runtimes, via callRuntimeRpc({kind:'local'}) -> runtime:call ->
the shared RpcDispatcher. The automations:list/listRuns/create/update/
delete/runNow IPC arms, their preload members, and every renderer
desktop-vs-runtime transport fork are retired; the runtime methods are
the single implementation of scoped lists, owner fencing, and change
publication for both transports (mobile clients already exercised them).
The desktop probe scheduler's priority lease survives the move as an
AutomationService hook the IPC registration installs and the runtime
methods take, so Orca's own automation traffic still parks queued
external-manager probes.
External-manager scope arms and dispatch-loop plumbing stay on IPC by
design; automation change events keep their existing channels (renderer
ingestion already converges them by authority).
* Remove automation ghost SSH tombstone scanning
This functionality for synthesizing tombstones for automation-referenced SSH
targets is no longer needed as part of the automation system refactoring.
* Refuse orphan automations at dispatch time, not migration time
Remove migration-time disabling of orphan automations and the `enabledDecidedBy` field. Dispatch now refuses orphans at runtime instead, simplifying state management and UI. Orphans are left unstamped and enabled; dispatch refuses to run them via `resolveAutomationRunTarget`.
* Show all automations in flat table with unified filter menu
- Replace host picker component with comprehensive Filters menu supporting status, last run, agent, and host filters
- Flatten automation list layout to single table instead of host-grouped sections
- Add Host column to display execution host for each automation
- Display active filters as removable pills below toolbar
- Delete unused AutomationHostPicker* components
* Add automation owner fencing and destination validation
- New AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY for owner preconditions; legacy clients get owner metadata snapshotted at RPC boundary for compatibility
- Editor captures and revalidates automation destination before save, preventing silent retargeting if SSH infrastructure changes mid-edit
- SSH target types now isolate renderer-authored fields; generation is server-owned and stripped by IPC handlers
* Route automation recovery actions to the origin host
When an automation action fails due to owner fencing, recovery verbs
("Update server", "Reconnect") must run on the host where the refusal
originated: the row's captured owner for row operations, or the
destination the create dialog captured, not the list's filtered host.
* Remove external manager scope limitation notices
Consolidate create destination eligibility checks with a unified predicate
and fix the bug where desktop repo IDs could be sent to runtime hosts where
they cannot resolve.
* Persist only store-derived automation contexts, not client-perspective o
Store contexts must never be based on client-provided runContext or sourceContext
values—clients speak a different perspective (e.g., 'runtime:<id>' for host IDs
they assign), and persisting those makes the store projection orphan automations
it actually owns. Derived contexts now take precedence in create and update paths,
with explicit null still honored to clear a value. Tests verify this by simulating
drift after storage and confirming that moves re-derive while toggles preserve.
|
||
|
|
44a3ba59e6 |
test(git): isolate worktree-shared-directories git config portably (#16582)
`os.devNull` is `\\.\nul` on win32. Git normalizes it to `//./nul` and rejects it as a config path, so every `git` call in this suite threw in `beforeEach` and 15 of 19 tests failed on Windows. POSIX resolves the same constant to /dev/null, which Git accepts, so CI never saw it. Point GIT_CONFIG_GLOBAL at a real empty file in a private mkdtemp directory, matching how skill-git-tree-identity and skill-windows-workspace already isolate, and use GIT_CONFIG_NOSYSTEM instead of GIT_CONFIG_SYSTEM. Set both on `process.env` rather than only on the suite's `git()` helper. `resolveWorktreeSharedDirectories` runs its own `git check-ignore` through the production runner, and `GitRuntimeOptions` carries no env, so the runner inherits `process.env`. The per-call override never reached the code under test: a host `core.excludesFile` could make a fixture that is not gitignored come back as ignored. Fixes #15409 |
||
|
|
19e9ec695b |
perf(windows): ship the native process table to Windows relay hosts (#16598)
* feat(windows): let a relay host bind the native process table directly The CIM fallback from #16550 answers on relay hosts, but it costs a powershell.exe and ~1.4s per scan where the native reader costs ~57ms. It is a parachute, not the destination. Teach the loader a second source: the desktop app keeps resolving the npm package, and a relay host -- which has none of our node_modules -- binds a bare `windows-process-tree.node` staged beside the bundle. The CIM scan stays as the last resort, so a host with neither is unchanged. Bind the addon directly rather than its package wrapper. lib/index.js adds only a queue over getProcessList, and that queue is the wedge this module already defends against: it latches a module-global requestInProgress with no try/catch. We hold our own single-flight and deadline, so going straight to the addon drops the duplicate. Measured on a Windows 11 SSH host with ~1490 processes, running the relay-externals bundle from the deployed relay directory: no addon staged nativeAvailable=false 1247ms (CIM) addon staged nativeAvailable=true 57ms memory restored Degradation was exercised on that host, not just in fakes: a truncated upload, a text file, and a foreign-arch ELF each fall through to the scan rather than throwing, and restoring a good addon recovers. A file that loads but lacks getProcessList is rejected by shape, because binding to it would reject every read forever where falling through still answers. No artifact is staged yet, so this is inert until the packaging change lands: today every relay takes the same CIM path it does now. * build(relay): ship the Windows process-table addon to relay hosts The CIM scan restored correctness on Windows SSH hosts, but it costs a powershell.exe and ~1.4s per read where the native addon costs ~57ms. It was always the floor, not the destination. The addon cannot be npm-installed on a relay host: it carries a binding.gyp, so npm rebuilds from source and the build wants Spectre-mitigated libraries even where MSVC is already present. The binary inside the published tarball loads, but predates our patch and still caps enumeration at 1024 processes -- on a 1486-process host it returned exactly 1024 rows with the querying process among the missing, which reads as unavailable only under load. No published alternative clears the bar either; the one fork with a working prebuild story still carries the same cap. So build it where a compiler exists and ship the result. The build script refuses unpatched source -- checking the source rather than trusting the install, because the Spectre hunk fails loudly while the 1024 hunk fails silently -- and verifies the PE machine field so a cross-build cannot emit host arch for another target. The artifact is optional: hashed when present so a relay carrying it never shares an immutable directory with one that does not, and never probed, since requiring a file only a Windows build machine can produce would make a correct relay read as MISSING and redeploy forever. Builds on any other OS keep using the scan, unchanged. arm64 cross-compiles from the x64 runner but needs the optional MSVC ARM64 toolset, so it stays best-effort: a runner image without that component should cost arm64 relays the fast path, not fail the release the x64 relay is riding on. ORCA_REQUIRE_RELAY_NATIVE_ADDONS is a per-arch list rather than a flag for exactly that reason. * build(relay): require the arm64 process-table addon too The arm64 cross-compile is no longer unproven. On a Windows x64 machine with the MSVC v143 ARM64 build tools component installed, node-gyp --arch=arm64 produces a genuine ARM64 image: x64 machine=0x8664 152064 bytes arm64 machine=0xaa64 139776 bytes So arm64 stops being best-effort and joins x64 in the required list. It was only best-effort because the component is optional and I had not seen it succeed; a runner image without it now fails the build with MSB8020 naming the missing component, and that step runs before the long packaging step so the failure costs seconds rather than twenty minutes. The env var stays a per-arch list rather than reverting to a flag, so a future arch can land best-effort before being promoted the same way. |
||
|
|
7f034a182f |
docs(windows): correct why the process-tree addon is not installed on relay hosts (#16565)
The note said the package "ships no prebuilds". It does: the published 0.8.0 tarball carries build/Release/windows_process_tree.node, apparently an accidentally published MSVC build directory (.obj and .tlog files ship with it). The conclusion was right and the reason was wrong, so record what was actually measured on a Windows SSH host with 1486 processes. Installing it normally rebuilds from source, because the tarball carries a binding.gyp and npm runs node-gyp regardless of what is already compiled inside. That build fails with MSB8040 (Spectre-mitigated libraries) even on a host that already has MSVC Build Tools 2022 -- the requirement our binding.gyp patch deletes, and patches do not cross SSH. Skipping the build keeps the tarball binary, which loads (it is N-API) but predates the src/process.cc patch and still caps enumeration at 1024. On that host it returned exactly 1024 rows with the querying process among the missing, which the self-presence guard rejects -- so it would work on a quiet machine and fail only under load, the shape of bug that survives testing. Also records the measured cost of the fallback, since the table's 706ms figure is from a 1050-process host and reads as more headroom than there is, and names the fix for the tracked gap: ship our own patched .node as a relay asset, as config/relay-assets already does for node-pty. |
||
|
|
1fafccb26b |
fix(settings): use Workspace Directory for the Create-project default path (#14767) (#16583)
* fix(settings): use Workspace Directory for the Create-project default path `repos:getDefaultCreateProjectParent` hardcoded `join(homedir(), 'orca', 'projects')` and never consulted the settings store, so Settings -> General -> Workspace Directory had no effect on the Location field of "Create new project". Users had to retype the path every time, or fake it with an NTFS junction. Resolve the parent from the store instead, through the same rule the rest of the app uses for a host preference: `host override ?? client default`, i.e. `getEffectiveHostSetting(settings, LOCAL_EXECUTION_HOST_ID, 'defaultWorktreeLocation', settings.workspaceDir)`. This handler only ever answers for the local host, and a local-host override previously could not win either. A seeded value is not a user choice. `workspaceDir` is never blank -- new installs seed it with `~/orca/workspaces` -- so treating any non-blank value as configured would silently relocate every existing user's new projects into the worktree root. Worktrees nest at `<workspaceDir>/<repoName>/<branch>`, so such a project would then host its own worktrees inside its own working tree. Compare against `getDefaultWorkspaceDir(homedir())` (now exported) via `normalizeRuntimePathForComparison`, and keep `~/orca/projects` for blank, whitespace-only, and untouched-default values. Also scope the `~/orca/projects` shorthand in `formatCreateProjectParentSummary` to the fallback path itself. Otherwise a user with Workspace Directory set to `J:\PROJECTS` saw the summary line claim `~/orca/projects` while the field held `J:\PROJECTS`. Fixes #14767 * fix(settings): keep configured orca/projects paths verbatim in the create summary The collapsed Location summary used a tail match on orca/projects, so a configured directory like /data/orca/projects rendered as ~/orca/projects. Scope the shorthand to usual home layouts and pin the lookalike cases. |
||
|
|
5e900b10b3 |
fix(windows): let a build with no job exports still retire a dead agent (#16563)
* fix(windows): let a build with no job exports still retire a dead agent
#16419 (
|
||
|
|
4d2dc0fae5 |
test: pin cross-version browser placement test to explicit baseline (#16554)
* test: use explicit baseline for cross-version browser placement test Pin to v1.4.184 to ensure consistent testing against the release predating client placement. This avoids coupling the legacy-baseline bump to unrelated schema refactors in newer versions. * fix(windows): treat inaccessible processes as alive in tests When checking process state on Windows, EPERM (permission denied) indicates an inaccessible but live process. Only ESRCH (process not found) proves exit. Correct isAlive() to distinguish these cases. Also add windowsHide:true to child process spawns and use explicit SIGKILL when force-killing the host process. |
||
|
|
1c9fb84b77 |
fix(worktree): stop push-target rollback deleting a sibling's remote (#16569)
A push target that reuses an existing Orca-created fork remote inherits ownership of it (`remoteCreated = isRemoteCreatedByKnownWorktree(...)`), so the final worktree to be deleted can remove it. Rollback then reused that same flag to decide whether to undo its own work -- but a reused remote was not added by this call, and a live sibling worktree is still pushing to it. A failed fetch during create therefore deleted a remote another worktree depends on. Track `remoteAddedHere` separately: ownership stays inherited for cleanup, while rollback only removes a remote this call actually added. Both the local and the SSH path had the same bug and are fixed together. Original work by Jinjing (AmethystLiang) in 616d2a4ec8c; split out of that branch so the release fix in #16550 stayed a clean cherry-pick. |
||
|
|
3c5c908451 | fix(automations): scope project refs to destination host (#16552) | ||
|
|
868fc39d32 | fix(worktrees): refresh paired clients after external discovery (#16557) | ||
|
|
e4d95e032d |
fix(windows): restore a CIM fallback for relay hosts with no native binding (#16550)
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
933345d347 |
Clarify upstream divergence stats for rebased branches (#16358)
* Clarify upstream divergence stats for rebased branches When a branch is rebased, it still tracks the pre-rebase upstream while comparing against the new base. Move upstream arrows to the head line to prevent them being confused with compare-base counts. * Show upstream divergence stats independent of compare base Measure HEAD against upstream regardless of compare-base state, so divergence indicators stay visible even when comparison is missing, loading, or failed. Also use cross-platform temp paths in tests. * Show commit counts against compare base, not upstream Upstream divergence (↑/↓ against tracking branch) was confusing for rebased branches — the counts appeared beside the base ref but measured against the upstream branch. Show only the compare base count instead, on the line that names it. * Report branch divergence in both directions Rebased branches are typically ahead AND behind their base; a single count hides this case. Use symmetric range with --left-right --count to capture both directions efficiently, then expose commitsBehind in the UI alongside commitsAhead. * Use semantic names for i18n keys and template variables Rename hash-based translation keys to descriptive identifiers and replace generic value0/value1 placeholders with semantic variable names like `count` and `ref`. Improves code maintainability and makes translation strings self-documenting. |
||
|
|
5479bd9159 |
refactor(task-page): split task page into focused modules (#15163)
* rm unused files * rm unused files * fix(task-page): clean readiness lint findings * Add GitLab IPC timeout wrapper and improve error handling - Extract GitLab timeout logic into reusable `withGitLabIpcTimeout` wrapper to protect all GitLab API calls from hanging indefinitely - Apply timeout protection to all GitLab list and fetch operations - Add error handling for GitHub and Linear issue creation operations - Fix event bubbling in GitHub work item row to prevent nested button clicks from opening detail page - Remove unused `usePRReviewCellState` hook - Consolidate redundant imports * refactor(task-page): extract components and improve provider handling - Add glab timeout handling (30s) to prevent IPC thread blocking - Extract GitHub assignee/review components to dedicated files - Improve GitLab work item row keying (repoId:id) and keyboard event handling - Add context-aware error handling for Jira creation failures - Refactor GitHubAssigneeAvatar to use shared GitHubUserAvatar component * Add timeout support and error handling for GitLab operations - Admission control times out queued work after 30s to prevent indefinite queueing behind saturated operations - Mutation errors now display to users via toast instead of failing silently * Consolidate workspace attachment labeling into unified utility Extract common label-generation logic from GitHub and Linear work-item components into a single getWorktreeAttachmentLabel function, removing duplication across attachment types. * Improve TaskPage accessibility, i18n coverage, and error handling - Add missing aria-labels, roles, and semantic attributes for improved screen reader support - Extract hardcoded UI strings into i18n system with translate() calls - Add error handling and proper abort signal support for async operations - Use locale-aware date formatting throughout - Fix pagination disabled state and reviewer suggestion merging logic - Improve async state management with proper refs and effects - Add Textarea component import for Jira dialog * Improve TaskPage accessibility and i18n key naming - Add DialogTitle/Description with i18n to Linear issue dialog - Use useId to improve aria-labelledby in GitHub selectors - Replace hash-based i18n keys with semantic names - Use Object.hasOwn instead of `in` for safer filter checks - Fix PR review cell to clear input only on success * Add missing dependencies to TaskPage hooks and useCallback/useEffect arr Fixes exhaustive-deps warnings by adding missing setters, refs, and computed values to dependency arrays. Refactors GitHub and Linear issue state handling to compute values from pageData where available, with fallback to local state. Moves imperative ref updates into useEffect to properly track dependencies. * Fix TaskPage ref timing and null repo selection state Treat null newIssueRepoId as a valid selection, and use useLayoutEffect to synchronize the provider context ref before paint rather than after. * Extract Linear issue dialog components and fix popover scroll styling - Consolidate scroll styling: apply popover-scroll-content and scrollbar-sleek classes to PopoverContent wrappers - Remove redundant max-h-60 overflow-y-auto styles from inner picker divs - Fix GitHub new issue repo selection to explicitly target first selected repo on fresh mount - Correct CacheEntry import paths from store/slices/github to store/github/cache-model - Update tests to reference extracted dialog components instead of TaskPage.tsx * Improve GitHub task page i18n and fix issue creation edge cases - Add i18n support to GitHub work item aria-labels (draft PR, PR, issue) - Optimize work item row by extracting repeated source context call - Add safety check to prevent opening detail page when issue URL is missing - Fix dependency reference in detail opener hook - Extend GitLab job trace timeouts (60s backend, 65s frontend) for slow logs * Increase GitLab job trace fetch timeouts Job traces can outlive the runner's 30-second default timeout. Extend fetch operations to allow 60–65 seconds to complete. * Verify sourceContext variable extraction in github row test Update expectations to check that sourceContext is assigned to a variable rather than called inline, matching the refactored component implementation. |
||
|
|
a1ec0479e2 |
fix(windows): revalidate PTY liveness from the job object, not a forked helper (#16419)
* fix(windows): answer console membership from the job object, not a forked helper
node-pty answers "which processes are attached to this pane's console?" by
FORKING a helper, because GetConsoleProcessList must run from a process
attached to that console. Orca asked on a foreground poll, per pane, so each
read spawned a conpty_console_list_agent -- hundreds of hidden processes
exhausting RAM within minutes, respawning as fast as they were killed (#10857).
QueryInformationJobObject has no console-attachment constraint: any process
holding the job handle can ask. Orca already creates that job per PTY, and
listPtyJobProcessIds has exposed it since the W1/W2 work with zero callers.
One syscall, no children.
Semantics the three call sites rely on are preserved: a root-only set still
proves the shell is alone (so a stale agent can be retired), and size > 1 still
proves something is running under it. The single difference is that a
descendant detached from the console stays in the job -- which widens the set,
the conservative direction for every caller.
Also fixes the third call site, which returned { available: false } whenever
membership was unavailable AND a recognized agent existed -- i.e. exactly while
an agent was running. Membership only ever narrowed the candidate list, so an
unavailable answer now leaves it unfiltered instead of failing the whole
resolution.
The no-fork test is asserted through a module-level vi.mock of
node:child_process. A vi.spyOn of a require()'d child_process does not
intercept the module's own import binding: the first version of that test
passed with a fork() deliberately reintroduced.
* fix(windows): keep console attachment for the candidate filter
Readiness review caught that this PR changed two different questions as if they
were one, and the repo's own plan doc had already said so:
"The job is the wrong set here -- it would re-admit precisely the detached
process the filter exists to drop." (windows-wsl-root-cause-plan.html, Use B)
The two uses:
- Use A, `size > 1` at local-pty-provider and the daemon tracker -- "is anything
in this pane besides the shell?". The job answers this, in-process and with no
fork. Unchanged from the previous commit.
- Use B, the candidate filter -- "which of these are ATTACHED TO THIS CONSOLE?".
Its whole job is dropping a descendant that detached, and the job object keeps
those, so answering it from the job makes the filter a no-op in its motivating
case: a detached `Start-Process droid` would be granted byte authority, and a
detached sibling would make an attached agent look ambiguous.
Use B goes back to GetConsoleProcessList, in its own module named for what it
answers, with its fail-closed null restored. That path is not the #10857 storm:
it runs only when a recognized agent candidate already exists, not on every
foreground poll. Bounding it to one pooled supervised helper is the remaining
half, and per the plan doc either half alone takes #10857 from unbounded to one.
My earlier claim that widening membership is "the conservative direction for
every caller" was wrong -- true for Use A, backwards for Use B. The hardware run
did not catch it because I measured a WSL pane, where the superset is harmless,
and never a detached GUI child, which is the divergence.
* fix: restore the coverage and ratchets the module split dropped
Round 2 of review. Two blockers, both from moving the forking code to a new
file without moving what guarded it.
- The child_process import ratchet was RED: windows-console-attached-processes.ts
imports node:child_process and was unlisted, and the old entry was stale. I
never ran that suite -- lint and the providers/daemon tests both pass without
it, which is exactly the gap the ratchet exists to close. Entry repointed;
count unchanged at 159.
- The forking module had ZERO tests. Its 11 assertions -- bounded timeout,
single kill, spawn error, malformed message, helper-pid removal -- were in the
file that now answers a different question, so the module that actually caused
#10857 was shipping untested. Moved with the code.
Also: nothing pinned the round-1 fix itself. No test drove console attachment to
null and asserted the fail-closed result, so re-deleting that branch would have
gone green. Now covered, and verified to fail when the branch is removed.
Cleanups the split left behind: `consoleMembershipUnavailable`/`consoleProcessIds`
renamed to `pane*` where they now hold job membership, the duplicated
`WindowsConptyMembershipDeps` type name, comments still describing the console
on the job path, and eight reliability-gate paths pointing at the moved tests.
* fix(windows): let a superset job answer expire instead of vetoing retirement
Round 3. The job read had reintroduced #9258's bug by a new mechanism.
`size > 1` returned unconditionally, so any pane holding a console-detached
descendant never retired its cached agent. A WSL pane always holds some: the
measurement in this PR's own test recorded job [40980,104068,4888,69908] against
console [69908,40980], i.e. console said "shell alone, retire" while the job said
"three others alive, keep". #9258's third commit describes the identical failure
from the other direction -- a bare shell reading as [helper, shell] "looked like
it still had a child ... the foreground refresh held the exited agent's identity
indefinitely" -- and that is what came back.
It bites because the read branch that serves the cached name across a Windows
shell fallback is deliberately untimed: #9258 made it so on the stated assumption
that "the background refresh authoritatively retires it". Removing the retire
authority left the identity with no bound at all. Second-order: a non-null cache
makes idleNoEvidenceShell false, which pins the refresh at the 1s TTL, so an idle
WSL pane also scanned the process table every second forever.
A TTL on the read would have been the wrong fix -- untimed is deliberate, because
on Windows the fallback name is structurally uninformative. Instead the job answer
is treated as what it is: a SUPERSET of the console, which cannot tell a working
agent from a leftover. Proof of absence retires immediately (size 1, unchanged);
an inconclusive answer ages out at 30s; unverifiable (null) still holds forever
per ssh-execution-boundary.md. Only successful scans that found no agent advance
the clock -- a degraded scan returns before this -- so the fix cannot expire an
agent it simply failed to see.
Also from review:
- Restore the root requirement the forked probe had. Without it a set of one
non-root pid -- shell gone, descendant alive -- read as "shell alone, retire",
inverting the truth.
- Rename to windows-pty-job-membership.ts / readWindowsPtyJobProcessIds. The old
name still said ConPTY console while reading the job, and conflating those two
sets is precisely the bug
|
||
|
|
5a8b4aff7b |
perf(git): only schedule the upstream-ref poll for the repo that has one (#16443)
* perf(git): only schedule the upstream-ref poll for the repo that has one A single global binding means at most one worktree holds a selected upstream ref at a time, so every other repo's 2s poll woke only to stat an empty set. Rebinding is synchronous and in-process, so reacting to it detects a newly selected ref exactly as fast as polling did — the wake-ups were pure waste. Measured at 100 repos with no selected ref: 0.811 -> 0.320 CPU-ms/s idle. * fix(git-watch): re-read ref selection after building the poller A rebind can flip back while the poller is being constructed. The concurrent unbind sees statusRefPolling still null and correctly does nothing, so without re-reading selection the in-flight build adopts a poller for a repo that no longer holds a ref — reinstating the idle wake-ups this change removes. * fix(git-watch): fence concurrent ref-poller starts with a generation token Startup and each rebind could be mid-build at once, and the startup path adopted its poller unconditionally. A rebind that won the slot first was then overwritten without being unsubscribed, stranding that poller's timer and visibility listener for the process lifetime. The generation names which attempt still owns the slot so every loser tears down what it built. Adds a regression test driving the real binding path; it fails if the poller is dropped without unsubscribing. Reported by CodeRabbit on #16443. |
||
|
|
91a500712c |
fix(crash-reporting): see the renderer memory the heap counters never report (#16449)
* fix(crash-reporting): see the renderer memory the heap counters never report Windows renderer crash 36048e26 arrived with 618MB of private renderer memory and a `renderer_memory` breadcrumb reporting a 150MB V8 heap. Both numbers were right: xterm scrollback lives in `Uint32Array` backing stores and glyph atlases live in GPU transfer buffers, and neither is counted by `usedHeapSize`, `mallocedMemory`, or Blink's allocator. That made the report unanalyzable. `renderer_memory_highwater` is the crumb carrying the subsystem census that names what grew, and it is armed on `usedHeapSize / heapSizeLimit`. At 150MB of a 4192MB limit that ratio is 3.6% — nowhere near the 60% mark — so the census never reached a single one of these reports. Measured on Windows (6 worktrees x 4 terminal tabs, 8000 lines each, this app at 4218d505): filling 24 mounted panes moved the renderer working set from 210MB to 656MB while `usedJSHeapSize` stayed at 43MB for the whole run. Sample the renderer's own OS footprint through `process.getProcessMemoryInfo()` (available in the sandboxed preload) and: - report `privateMB`, `residentMB`, and `outsideHeapMB` — the footprint minus everything V8 and Blink admit to holding — on every `renderer_memory` crumb; - arm the highwater census on private-footprint marks (600MB / 1000MB) as well as the heap ratio, so growth outside the JS heap now carries the pane and store census that names it. The footprint read is async, so a sample annotates with the previous read and refreshes in the background: one interval of staleness is irrelevant to a footprint trend, and awaiting it would make every sample reentrant. A shell without the bridge, or a runtime that withholds the read, keeps sampling exactly as before. Retained-breadcrumb keys now distinguish the two threshold ladders; keying only on `thresholdPct` collapsed every footprint crumb onto one slot. crash-diagnostics.ts split at the max-lines budget: memory sampling moves to renderer-memory-sampling.ts and the shared payload shaping to crash-breadcrumb-data.ts. * fix(crash-reporting): retain all renderer memory marks |
||
|
|
76f7e785fc | style(github): trim repo identity cache comments (#16517) | ||
|
|
67f2fc9e7c | perf(github): stop re-spawning the repo-identity probe every 30 seconds (#16450) | ||
|
|
c8567eb16e | fix(sidebar): preserve hidden rows in manual order (#16488) | ||
|
|
efa3b972c2 | fix(native-chat): prevent duplicate mobile prompt echoes (#15656) | ||
|
|
a9781a4118 |
STA-4150: client-hosted remote browser (consolidated) (#15448)
Co-authored-by: Jinwoo-H <jinwoo@stably.ai> |
||
|
|
98bdd653ab |
fix(native-chat): stop the spinner on a not-yet-flushed transcript (#16493)
* fix(native-chat): stop the spinner on a not-yet-flushed transcript A brand-new agent session can take minutes to write its first JSONL line, and one that is never prompted never writes it at all. The host emitted no stream frame until the file resolved, so every native-chat client sat on a bare spinner with the composer enabled but the transcript blank -- forever, in the never-prompted case. The resolve poll now reports the transcript as pending after a short grace, and both host handlers emit a `pending: true` snapshot. It is deliberately not a plain empty snapshot: an empty window sold as a settled read would capture over retained history and unblock consumers that require a trustworthy transcript (the launch-draft adoption would re-offer a prompt the agent may already have taken). Clients render it as the "start a chat" empty state while keeping the read unsettled -- `awaiting-transcript` on mobile, an `awaiting` read phase on desktop, which also stops the seed loop expiring into an error card for a session that is simply new. New optional field only, so older clients ignore it and still stop spinning. * fix(native-chat): negotiate pending transcript frames |
||
|
|
e361da7fb7 |
Deleting skill (#16357)
* Add skill deletion with cross-platform transaction safety Implements end-to-end skill removal with placement enumeration, dependency guards, and transactional recovery. Covers native, WSL, and remote hosts; users can delete canonical directories and alias placements (symlinked directories or files) in a single atomic batch. Includes UI selection flow, preview, confirmation, and results band. Block reasons (bundled, plugin, unowned, stale) gate deletions that would fail or contradict user intent. * Organize IPC handlers into module subdirectories Move register-core-handlers and skill-delete-ipc-handlers into dedicated subdirectories for improved code organization and to reduce the flat structure in src/main/ipc/. * Make skill deletion recovery transactions idempotent Defer journal cleanup until both staging removal and receipt cleanup succeed, leaving the journal in place for startup to retry if either operation fails. This ensures the recovery process is safe to run multiple times without leaving partially-deleted skills. * Consolidate skill-delete files into dedicated module Reorganize skill deletion functionality into a modular structure under `src/main/skills/skill-delete/` with simplified file names. Remove the redundant `skill-delete-` prefix from file names since they now live in the dedicated directory. Update all import paths throughout the codebase to reflect the new structure, including imports from IPC handlers and RPC methods. * Fix broken import paths and add deletion robustness improvements Import paths using `..//'` were invalid and broken. Replace with explicit module names (`skill-discovery-sources`, `skill-install-filesystem`, etc.) to clarify dependencies. - Bind WSL filesystem methods to preserve `this` context - Keep recovery journal when rollback rename fails, so startup can retry - Skip symlink-based tests on Windows where they cannot run - Only treat ENOENT/ENOTDIR as empty directories; propagate other errors - Fix cross-platform path parent calculation to handle drive roots - Replace shared constant with localized string for user-facing message - Use `runProcess` for WSL integration test instead of bare `execFile` * Add batch limit for skill deletion and improve host availability checkin - Limit concurrent deletions to prevent remote host overload - Add retry logic for capability probing to handle transient unavailability - Add reprobe() method to recheck capability after errors or user refresh - Fix status logic: receipt cleanup is best-effort, completion depends only on content removal - Improve error message for unreachable hosts |
||
|
|
4218d5068e |
fix(cli): seed nvm's default version, not the newest install (#16420)
* fix(cli): seed nvm's default version, not the newest install #16314 stopped the login-shell probe inheriting the seeded PATH, but left the seed itself picking the newest installed nvm version. That ordering decides which node a CLI runs under whenever the probe does not land — a timeout, or a login shell whose rc never initializes nvm — and newest is precisely the wrong guess: it is usually the version the user just added and has installed nothing into. That is the root cause reported in #10932. Resolve `alias/default` instead, mirroring nvm: follow the alias chain (`default` -> `lts/*` -> `lts/krypton` -> a version), resolve a partial version like `24` to the highest matching install, and treat `system`/`node`/`stable` as no preference. The chain is bounded and cycle-guarded because nvm's own resolver tracks seen aliases and hand-edited files can point at each other. Ordering is a preference, not a restriction: the remaining versions stay behind the default, so a CLI installed outside it is still reachable. Measured on a real machine with nvm default=24 and a bare v26.7.0 installed: the old resolver seeds v26.7.0/bin (no CLIs), the new one seeds v24.18.0/bin (every CLI). Tests were written first and verified to fail on the three bug cases against main before the fix existed. Also raise the probe budget from 5s to 10s. The old value was never measured against a real profile: a bash -ilc loading nvm, rvm, conda and gcloud takes ~1s idle but 6-7s on a loaded machine, so a cold start under load silently fell back to the seed. Startup does not block on the probe, and the one awaited consumer is agent detection, which is better served by a probe that finishes late than one that gives up early. * fix(cli): reject non-version alias tokens instead of matching v0.x Review finding, and a real bug I introduced. parseVersionSegment coerces every unparseable segment to 0, so an unresolvable default alias — `garbage`, `iojs`, `lts/nonexistent`, any hand-named alias — became [0] and prefix-matched a `v0.12.x` install, or any stray non-version directory. Orca would then seed a decade-old node as the preferred runtime. Real nvm answers N/A for all of them. The `wanted.length === 0` bail could never have caught this: ''.split('.') is [''], never empty. Replaced with a shape check that still admits legitimate numeric prefixes — verified against nvm itself, which resolves `24` to v24.18.0 and `0` to an installed v0.x while answering N/A for the rest. Also corrects two comments that no longer described the code: the seed is no longer "newest install", and the probe budget note claimed startup never blocks on hydration, which is false on packaged Windows where it gates terminal services and git. The traversal-guard comment claimed a containment join() already normalizes away; the real guarantee is that matchNvmVersion can only return an entry of the versions directory. * fix(cli): match nvm's version-token grammar, not just its first character Round-2 review finding, and the same bug one layer down. The previous guard anchored only the first character, but parseInt stops at the first non-digit, so `0x18`, `00` and `0abc` still parsed to [0] and prefix-matched a v0.12.x install — the decade-old-node seed the earlier fix was supposed to close. Reachable: `nvm alias default 0x18` warns that the version does not exist and writes the alias anyway, then resolves it to N/A. Use nvm's actual grammar, leading zeros included — nvm calls `00` and `024` N/A while parseInt reads them as 0 and 24. Verified by executing 17 tokens against a five-version fixture: every one now agrees with nvm, including the legitimate prefixes `0`, `0.12`, `24` and `v24.18.0`. Also drops a dead disjunct (the hop bound already caps the loop, so seen.size can never exceed it) and corrects the log comment in index.ts, which still told the reader a failed probe leaves the newest install in front. It leaves the default version in front now, which is usually survivable but still not what the shell would have resolved. * test(cli): skip the lts/* chain fixture on Windows Round-3 review finding. makeNvmHome materializes each alias as a real file, and the chain case uses nvm's actual `lts/*` alias — `*` is a reserved Win32 filename character, so writeFileSync fails with EINVAL. PR CI runs a Windows allowlist that excludes this file, so the breakage only reaches a Windows developer running the suite locally. Skipped rather than renamed: `lts/*` is the alias nvm really ships, and the assertion pins platform: 'darwin' anyway, so the real name costs no coverage. Matches the skipIf convention already used across src/shared. Also reflows a comment line that a previous edit ran to 143 characters; oxfmt does not reflow comments, so nothing would have caught it. |
||
|
|
822087c8ec |
refactor(git): split runner.ts into focused command-runner modules (#16395)
* refactor(git): split runner.ts into focused command-runner modules * chore(ratchets): repoint child_process and wsl.exe allowlists at the split modules --------- Co-authored-by: Neil <n@example.com> |
||
|
|
1f39c93b01 | refactor(ipc): split ssh.ts into focused modules (#16394) | ||
|
|
fcf55f2d68 |
fix(terminal): stop Orca mangling the OMP/Pi title it writes itself (#16381)
* fix(terminal): collapse identity group in the title churn signature Replaces the ingest-time title rewrite from #16373 with a non-destructive fix at the actual cause. The churn suppressor `isDecorativeAgentTitleFrameChange` keyed on the literal label, so `working:OMP` and `working:Pi` compared unequal and every alternating frame from a wrapped harness committed a store patch. #16373 made the labels agree by rewriting the stored title to the tab's launch owner — but `runtimePaneTitlesByTabId` is also the Windows Shift+Enter byte-encoding input, so normalizing at ingest destroyed evidence other consumers read (fixed separately in #16376). Collapse the identity group inside the signature instead. Which member of a group a frame names is decoration, exactly like the spinner glyph the signature already strips, so frames compare equal without touching what is stored. Suppression now changes only WHETHER a frame commits, never WHAT it says. Also fixes the flap under a multiplexer (#8032): the collapse runs over wrapper segments, so "zsh | ⠋ Pi" and "zsh | ⠙ OMP" compare equal, which the anchored owner-relabel in #16373 never matched. Reverts the store changes from #16373 and drops the helper it added. Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com> * fix(terminal): fold only bare identity frames into the group token A legacy "π - <session> - <cwd>" title is Pi-compatible too, so folding every profile match collapsed two different sessions to the same signature and suppressed the change outright — reintroducing #16093 through the churn signature. Fold only exact bare identity frames, matched per wrapper segment, so semantic session titles keep comparing on their own text. Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com> * docs(terminal): correct the flap diagnosis in the repro header Verified against the OMP source: it emits only π-glyph frames (`DEFAULT_TERMINAL_TITLE = "π"`, title-generator.ts:25), and on an Orca-hosted pane its native titler cedes to Orca's own injected extension, which writes `⠋ π - <session> - <cwd>`. So OMP emits neither "OMP" nor "Pi". Both flap sides are Orca's: "OMP" from driveSyntheticTitleFromHook, "Pi" from normalizeTerminalTitle collapsing our own extension's output to a hardcoded literal. The prior header credited the wrapped harness for frames it never sends, which is the same wrong narrative that produced eight fixes at eight layers. No behavior change. * fix(terminal): stop Orca mangling the OMP/Pi title it writes itself Verified against the OMP source: it emits only π-branded frames (`DEFAULT_TERMINAL_TITLE = "π"`, title-generator.ts:25), and on an Orca-hosted pane its native titler cedes to Orca's OWN injected extension, which writes `π - <session> - <cwd>` / `⠋ π - <session> - <cwd>` at 80ms. So neither flapping string came from OMP. Orca made both: "Pi" — normalizeTerminalTitle collapsing our extension's output to a hardcoded literal, discarding the session name and cwd (#16093) "OMP" — driveSyntheticTitleFromHook injecting over it every 80ms Fixed at the source: - normalizeTerminalTitle canonicalizes only the rotating braille frame and keeps the rest, in both spinner positions and through a multiplexer prefix (#8032). Status still round-trips through normalization. - detectAgentStatusFromTitle reads the π state separator, so `π ! <label>` is permission instead of the blanket idle that hid a blocked agent. - normalizeCompatibleAgentTitleForOwner swaps only the brand for the owner's label, so a pane still reads as its launch owner (#6689, #7633, #9077) without losing the session text. - pi/omp set synthesizeWorkingTitle: false — the agent animates its own working title. Terminal states still synthesize; they carry the pane's agent identity downstream. Reverts the ingest-time title rewrite from #16373, whose normalization of runtimePaneTitlesByTabId also changed Windows Shift+Enter bytes (#16376). Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com> * fix(terminal): match the state separator only in exact profile casing The separator check runs on every title, so `omp - deploy notes` and `pi - refactor the parser` read as an idle agent. The owner rewrite only ever emits the exact profile labels, so dropping case-insensitivity keeps `OMP - tmp` classifying while ordinary prose stops matching. Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com> * test(terminal): pin one real OMP turn to two committed patches Drives 30 working frames as Orca's injected extension emits them plus the idle transition, and asserts what survives the churn gate. Before the fix every frame alternated "⠋ Pi"/"⠋ OMP" and each one committed — ~12 store patches per second on a working tab. Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com> * fix(terminal): carry the permission guard inside the separator reader `-` is both a π state separator and the delimiter in the synthetic permission label, so `OMP - action required` read as idle. It resolved correctly only because detectAgentStatusFromTitle happens to check the synthetic label first — and the separator fn is exported, so a direct caller inherited the bug. Also pins the owner rewrite's fixed-point property, which holds only because getAgentLabel does not tokenize omp/pi, and corrects a comment that overstated how tightly the brand swap is scoped. Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com> * docs(terminal): name the flag the code actually sets The suite header cited `synthesizeTerminalTitle: false`; the profiles set `synthesizeWorkingTitle: false`. The distinction is the whole reason the narrower flag was chosen — terminal-state frames still carry the pane's agent identity downstream — so the wrong name buried the rationale. Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com> --------- Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com> |
||
|
|
33587fb77d | refactor(git): split status.ts into source-control modules (#16393) | ||
|
|
127fa7fae0 |
refactor(ipc): split repos.ts into focused modules (#16392)
* refactor(ipc): split repos.ts into focused modules
* test: point repo notification mocks at the extracted module
* fix(ipc): repoint the child-process allowlists after the repos split
The type-only `import type { ChildProcess }` moved from repos.ts to
repos/repo-clone-lifecycle.ts, so the import-boundary entry follows it and the
windows-console entry (now stale, and that list only shrinks) is dropped.
Fixture-only; the base file had no runtime child_process use at all.
|
||
|
|
e7c5263a93 |
perf(git): make local Git metadata observation event-driven (no scheduler) (#16404)
* perf(git): make local Git metadata observation event-driven Replaces the recurring per-repo metadata scan with native filesystem events on macOS, Linux, and Windows. Polling is retained purely as a fallback. - Narrow @parcel/watcher stream over <common>/worktrees, extended from macOS to Linux and Windows, with the Windows backend pinned explicitly. - New shallow watcher mode over the allowlisted primary metadata leaves. It watches the containing directory rather than each file, so Git's atomic write-and-rename does not orphan the binding. - Selected upstream refs stay on the existing bounded stat poll. Verified on real hosts rather than in principle: - Windows: `git worktree remove` and `git worktree prune` both succeed while the narrow stream holds the directory. The historical concern that an open handle would block prune does not reproduce. - Linux: inotify costs one instance per event loop, not one per watch, so the watch budget is not a constraint. - macOS/Linux/Windows: shallow events survive repeated commit, checkout, config, and pack-refs cycles. Failure handling, each reproduced before being fixed: - fs.watch binds an inode and reports nothing once that inode is replaced, with no error. Directory bindings are re-checked on a bounded cadence and rebound. - A host whose notification path is dead accepts registrations and stays mute forever. Observed on a macOS machine whose fseventsd had grown to ~15GB and saturated a core. A one-shot delivery probe now fails the shallow subscribe on such a host so it falls back to polling instead of showing stale metadata. This change stands alone on main and does not depend on the metadata poll scheduler. * test(git-watch): hold reserved inodes across root replacements Linux returns a released reservation to the free list, so the second replacement could land back on the first replacement's inode and look unchanged to reconciliation. Verified on ext4: releasing yields inodes [N, N+43, N+43] while holding yields [N, N+43, N+44]. macOS never recycles, which is why this only failed on CI. * refactor(git-watch): share one single-flight helper between watcher fallbacks Both fallbacks tracked their in-flight promise with the same self-comparison on settle, duplicated verbatim. Hoisting it removes a subtle invariant that was being hand-maintained in two places. * fix(git-watch): close the silent-staleness paths in primary metadata Two independent reviews converged on the same root cause: nothing bounded how long primary metadata could stay wrong once the shallow watcher stopped reporting. Four distinct paths led there. - A terminal watch error arriving while the status-ref poll was still starting left the repo with status-ref coverage only. handleWatcherError ran its teardown against nulls, then the in-flight poll installed itself, and the fallback guard mistook it for coverage and discarded the fallback. Primary metadata was then never observed again. The guard no longer treats status-ref polling as primary coverage, and startup re-checks watcher liveness after its awaits. - Nothing re-read the six primary files while the watcher was nominally live. A lossy notification path, a dropped batch, or inotify queue overflow raises no error, so the error-driven fallback never fired and the inode rebind sweep does not detect loss. A 15-tick backstop re-stats them, turning permanent staleness into one tick. Measured cost is ~0.2 stats/s/repo against the 3/s the old poll cost. - Reconciliation treated any late-observed entry create as a root replacement, so an ordinary tore down a healthy stream ~30s later and opened a deaf window. It now also requires the root itself to be recreated. - The shallow watcher recorded directory identity from a stat issued after binding, so a replacement in that gap pinned the dead inode's watcher to the new identity and the sweep would never rebind. Identity is now read first, which errs toward a harmless extra rebind. Test helper: replacing the worktrees root frees several inodes at once, so reserving one still let the recreated root reuse its own. It now verifies the inode actually changed. Confirmed on ext4, where three holds were needed. |
||
|
|
d456549c22 |
refactor runtime Git bridges (#16189)
* refactor runtime git bridges * fix(test): avoid computed namespace import access in runtime Git client contract test |
||
|
|
fc0d947983 |
refactor(rate-limits): split Codex and Claude fetchers (#16140)
* refactor(rate-limits): split Codex and Claude fetchers * refactor(rate-limits): restore base error-message defaulting The split moved the 'Unknown error' fallback from inside String() to the call site, which changed behavior for an Error with an empty .message: base surfaced '', head surfaced 'Unknown error'. Restore the base form. |
||
|
|
83ffc0df24 |
refactor Electron facilities modules (#16333)
* refactor oversized Electron facilities * fix interactive process timeout and shortcut repeat guard * chore(child-process): drop stale cli-installer allowlist entry cli-installer.ts now routes privileged spawns through runProcess via cli-privileged-processes.ts, so the shrink-only ratchet flags it as stale. * refactor(child-process): extract the bounded output sink runProcess's timeoutMs opt-out (required to preserve the unbounded osascript admin prompt) pushed run-process.ts past the 300-line cap. Move createOutputSink to its own module rather than add a max-lines bypass, which AGENTS.md forbids. Moved verbatim; no behavior change. |
||
|
|
1cf562deea | refactor: split source control AI modules (#16179) | ||
|
|
6103b78e3c |
fix(crash-reporting): correlate concurrent process deaths instead of blaming the renderer (#15251)
* fix(crash-reporting): correlate concurrent process deaths on a renderer report
Two 1.4.184 reports (a326935a, 1862f316) are renderer "crashed"/-1 crash reports
whose renderer only died alongside a sibling Chromium child that died at the same
instant:
F0BQMB30GJX network.mojom.NetworkService crashed/-1 -21ms -> renderer crashed/-1
F0BRPP8TC0Y audio.mojom.AudioService crashed/-1 -2ms -> renderer crashed/-1
GPU crashed/-1 +180ms
process-gone-classification.ts classifies each event in isolation:
isRecoverableChromiumChildProcess discards the utility/GPU halves as recoverable
churn, and `if (reason !== 'killed') return true` then reports the renderer half as
a genuine renderer crash before any cross-source signal exists. Triage reads
"renderer crashed" for what died with three other processes.
process-gone-sibling-correlation keeps a bounded ring of child deaths, populated
before the suppression early-return so churn-suppressed siblings stay visible, and
matches a renderer death against child deaths sharing its failure signature.
What the timing can and cannot support:
- The window is asymmetric. 1s of lookback (a child that died first can plausibly
have taken the renderer with it), but only 250ms of lookahead: a child dying well
after the renderer is at least as likely to be an effect of it — Chromium tearing
down the dead renderer's channels, or renderer_recovery_reload at +264ms — and a
symmetric window would retro-label a genuine lone crash as collateral.
- crashAttribution is 'concurrent-process-deaths', not a causal claim. The largest
1.4.184 cluster is an external taskkill /T where renderer and children are
co-victims; no sibling caused anything there.
- The verdict is not derived from timing alone. A host with a child looping at the
observed 1459/min drops a death into every window, so crashAttribution is set only
when the nearest sibling is within 250ms and no identity repeats. Looser or
repeating deaths still ship as evidence (siblingProcessDeathCount, signed offsets,
siblingProcessDeathRepeats) with no attribution.
- The signature match buckets `crashed` with `abnormal-exit` and gates on the exit
code only on win32. Both fixtures are win32, where every process in a collateral
pair reports crashed/-1; POSIX surfaces a per-process wait status, so an equality
gate would mean this never fires on macOS or Linux.
The report stays reportable and gains evidence rather than being suppressed
(#14667). Both arrival orders are covered without delaying persistence: a sibling
that dies first is folded into the initial record, a sibling that dies after amends
the record already on disk through attachDetails, the same way the minidump
signature does. Late amends are capped at two per report and skipped when the
rendered evidence is unchanged, so a crash-looping child cannot rewrite the store
during renderer recovery, and a failed amend now leaves a
sibling_attribution_attach_failed breadcrumb instead of vanishing.
Relationship to #12484: it is still OPEN and adds process-tree-kill-window.ts, the
same ring/lookback bookkeeping with a 250ms settle, patching the same recorder
hunks with the opposite policy (suppress the killed/1 renderer report instead of
keeping it). #14667 is test-only — it pinned the keep-the-report policy in tests, it
did not remove a shipped implementation. #12484 has to be closed or rebased out
before this lands.
* chore: remove merge hook formatting drift
|
||
|
|
49acb93e2e |
fix(daemon): let PTY inventory recover from a dead terminal host (#16363)
Worktree removal inventories PTYs through DaemonPtyAdapter.listProcesses. That called ensureConnected bare, so once the terminal-host pipe was dead the removal failed with `connect ENOENT \\?\pipe\orca-terminal-host-...` and stayed broken until the whole app was restarted. spawn already wrapped its work in withDaemonRetry and recovered from exactly this. Inventory did not — so the one path that must not get stuck was the only one that could not heal itself. Both the connect and the listSessions request go inside the retry: a host that dies between them throws the same daemon-gone error, so retrying only the connect would still fail. The reconciliation after the request is deliberately outside it; retrying that would be wrong. Reproduced first, with a real daemon killed mid-test: listProcesses threw DaemonConnectionLostError while a control asserting spawn recovery from the identical kill passed. Both are now regression tests, so the asymmetry cannot come back silently. Co-authored-by: innocarpe <innocarpe@users.noreply.github.com> |
||
|
|
deae7212d9 |
fix(orchestration): derive inject's agent guidance from the recognized-agent roster (#15874)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: vam <a@a.com> |
||
|
|
48e63c015f |
refactor agent config and auth services (#16195)
* refactor: split agent config and auth services * chore: repoint wsl and global-fetch guards at split module paths * fix: restore merge-base Claude CLI error propagation Drop the secret-redaction rewriting added to Claude CLI error paths in the refactor: spawn errors again reject with the original Error (preserving .code/.errno/.syscall/.stack) and command output/auth-status logs are no longer rewritten. |
||
|
|
2b1b094aa8 |
fix(cli): pair every resolved CLI with its runtime, and ratchet it (#16383)
Follow-up to #16365, which paired 8 spawn sites by hand. Hand-pairing is how the class got introduced, so close it structurally instead. cliPath is now required on CodexAppServerInvocation, `null` only for the guest-side wsl.exe launcher where a host path pairs nothing. Optional let a native builder omit it and silently fall back to pairing against a cmd.exe wrapper with no type error. Every production site already passed it; only test fixtures needed updating, which is the type doing its job. Four more sites now pair. codex-state-db-backfill-recovery spawns the same `codex app-server` subcommand #16365 fixed elsewhere. cli/handlers/account was the worst case: addAgentNodePaths prepends the *newest* version-manager bin, which is not necessarily where the CLI being launched lives, so it actively created the mismatch — pairing now runs last so the CLI's own node wins. commit-message-text-generation and skills/skill-update-run spawn resolved binaries with inherited env. cli/handlers/skills had grown its own buildNpxPath: a weaker local copy that prepended unconditionally, ignored the Windows `Path` key, and special-cased a '.' dirname. Deleted in favor of the shared helper, which checks the sibling node actually exists — the behavior change one test had pinned. The ratchet is the point: any file that resolves a CLI and spawns must reference withCliRuntimeOnPath, with a shrink-only allowlist. It caught skill-update-run, which I had missed. Its first draft required a call paren and so let dependency-injected resolvers (`resolveCommand: resolveCodexCommand`) through — verified by removing a pairing and watching it stay green, then widened until it failed. A second assertion fails on a stale allowlist entry so an exemption cannot outlive its reason. external-editor-launch stays allowlisted: it launches a GUI editor, not a Node CLI whose ABI matters. |
||
|
|
20c1a61401 |
fix(orcad): answer host paths honestly and ship the watcher child (#16369)
orcad's AppEnvironment implemented three of seven AppPathNames and returned the userData directory for the rest — including 'exe', where a data directory is not an executable. Every name now has a Node answer: 'appData' is the platform's per-user application-data root, 'logs' lives inside the data root so a headless deployment stays one removable directory, 'downloads' honours XDG_DOWNLOAD_DIR, and 'exe' is the Node binary. getAppPath() is the directory orcad was launched from rather than cwd, so children resolve against the bundle instead of wherever the supervisor happened to be. The watcher child was the load-bearing consequence: resolveWatcherProcessEntryPath probed for the adjacent entry only when !isPackaged, so orcad resolved a desktop out/main path that no deployment has — and build-orcad never emitted the child anyway. isPackaged stays true (consumers read it as "production, not a dev checkout" and it gates HTTPS-only skill downloads); the resolver now asks whether the app root is an asar archive, which is the question it actually meant. The child ships beside orcad.js, and the build forks it to prove it runs. |
||
|
|
c4988fa81c |
refactor workspace lifecycle modules (#16334)
* refactor(workspaces): split lifecycle modules
* preserve workspace cleanup consent contract
* restore workspace delete shortcut hint in context menu view
* restore host-qualified visit recency and viewed-candidate predicate
* test(cleanup): pin the viewed-mark upgrade path and host-qualified visit reads
Two invariants a refactor broke in this PR, both silent:
- viewed marks are persisted, so gating `shouldPreserveCleanupInspection` on any
newer field voids the grace period for every entry written by an older build
- visits are stamped under `${hostId}|${worktreeId}` whenever the host is known
(the normal case, including 'local'), so a bare map[worktreeId] read misses
every modern entry and yields 0, disabling the recent-visible-context blocker
Verified discriminating: reintroducing each bug fails exactly its own test.
|
||
|
|
a7505fd911 |
fix(cli): spawn a version-manager CLI with its own node runtime (#16365)
* fix(cli): spawn a version-manager CLI with its own node runtime resolveCliCommand falls back to scanning every version-manager install when PATH misses, so it can hand back ~/.nvm/versions/node/v20.x/bin/codex while PATH still leads with v22. Nothing paired the binary with the runtime it was installed against, so its `#!/usr/bin/env node` shebang loaded a v20-built native module under a v22 ABI and the agent died on first require (#10932). Reproduced with a real addon rather than asserted: a CLI requiring a cpu-features build for NODE_MODULE_VERSION 115, spawned with v24 leading PATH, fails with ERR_DLOPEN_FAILED and exit 1. With the CLI's own bin directory prepended it runs clean. withCliRuntimeOnPath prepends the resolved command's directory when that directory ships a sibling node, and is a no-op otherwise — so a Homebrew or /usr/local CLI is untouched, and the WSL paths pass a bare `codex`/`claude` that is not absolute and so never matches. Host CLI resolution in the Claude login path is now lazy, keeping the WSL branch from resolving a host binary it never spawns. * fix(cli): split PATH on the delimiter we join with, pair app-server too Readiness review findings, all four addressed. withCliRuntimeOnPath chose its join delimiter from the platform option but split with the host's. Passing platform:'win32' from a posix host turned `C:\Windows;C:\Windows\System32` into `C;\Windows;C;\Windows\System32` — every drive letter torn off at its colon. Latent, since no shipped caller passes platform, but the sole win32 test was written against the corrupted value and asserted one split segment, so it green-lit the shredding. That test's other assertion was vacuous: it seeded only `Path`, so the `PATH` key it asserted absent could never exist. Deleting the whole case-dedupe block left the suite green. It now seeds both keys and asserts the full joined string; removing the block fails it. Nothing covered the wiring, and the argument choice is the easy thing to get silently wrong. Note it only diverges on win32 — on posix getSpawnArgsForWindows returns the CLI itself, so pairing the spawn command is indistinguishable there. The new test drives the win32 branch with a .cmd fixture; pairing spawnCmd or dropping the wrapper both fail it now. codex-trust-grant-host and codex-session-index-heal spawn the same `codex app-server` subcommand through runCodexAppServerSession and were left unpaired. Pair centrally there via a new optional cliPath, since invocation.command may be a cmd.exe wrapper. Pairing tests live in their own file: adding them inline pushed codex-fetcher.test.ts past the 800-line ratchet. * fix(cli): read the Windows path key the child will actually use Round-2 review finding. The read was narrower than the delete: the key was picked from exactly two spellings (`Path`, else `PATH`), while the twin dedupe removed every key whose lowercase form is `path`. A block spelling it `path` or `pATh` therefore had its value deleted without ever being read, handing the child a PATH containing only the CLI's own directory — a strictly worse outcome than not pairing at all. Win32 resolves env names case-insensitively and object order preserves block order, so the entry the child reads is the first case-insensitive match. The repo already encodes that rule in resolvePathEnvKey (src/main/pty/windows-path-segment-merge.ts); src/shared cannot import from src/main, so mirror it locally. Verified by execution across six env shapes: lowercase, mixed-case, Path-only, PATH-only, both twins, and a PATHEXT control that must not be touched. All preserve the original PATH; before the fix the first two lost it entirely. Reverting the selector fails the new test and nothing else. |
||
|
|
e217fdd10f |
build(orcad): gate orcad's own graph, and prove it loads under plain Node (#16368)
* fix(orcad): close the browser-provider gaps The providers landed without enforced coverage, so a regression in either path would have landed silently. - CI: the external-Chromium integration test was gated on ORCA_BROWSER_EXECUTABLE and nothing ever set it, so it skipped forever. It now runs in its own job against the runner's Chrome and FAILS when Chrome is absent rather than skipping, because an unset variable is exactly how it went uncovered. Timeout raised to 120s: a warm run is ~7s but the first launch against an unseeded profile took 30s and hit Vitest's default, and CI is always that cold case. - Electron provider had no test at all. It is the path anyone with the desktop app hits. - Browser unavailability reported one message for four causes, including telling an operator to set a variable they had already set. Fixes a live defect found while covering it: the runtime advertises browser.tabCreate.known-id.v1 unconditionally, so a web client sends a provisional page id for a page that does not exist yet — and the sidecar's generic requestedPageId branch ran require() on it first and threw. Every known-id create against the Electron provider failed. The adoption logic was already there; only the ordering was wrong. Also updates the workflow-parallelism guard, which correctly caught the new job missing from verify's required-check list, and asserts verify actually reads it. * build(orcad): gate orcad's own graph, and prove it loads under plain Node Two gaps the artifact's own comment asked for. The ratchet measured only orca-runtime + runtime-rpc, but orcad imports ipc/pty directly to install the PTY controller, so its graph is strictly larger. The gate could read zero while the shipped artifact regressed. orcad's entry is now a ratchet entry point, and the baseline stays empty with it included. orcad cannot join plain-node-entry-guard — that is a rollup plugin keyed on electron-vite input names, and orcad is an esbuild artifact. But the half that matters here is the guard's smoke-load: scanning the metafile proves no module NAMES electron, not that the graph resolves under plain Node. A dynamic require, a missing native or a top-level throw all pass the scan and fail at runtime. build-orcad now runs the bundle with a bogus flag and requires the argv rejection that only a fully loaded graph can produce. Verified: a bundle that builds but throws on load fails the gate. |
||
|
|
8043b5f705 |
Refactor worktrees IPC into cohesive modules (#16190)
* refactor worktrees IPC into cohesive modules * test update PTY waiver invariant paths * revert unrelated oxfmt reflow from merge commit |
||
|
|
d8807801ff |
Refactor GitHub Project surfaces and mutations (#16166)
* refactor github project modules * restore dropped issue #4756 guard comments |
||
|
|
087b895524 |
refactor(daemon): split oversized PTY services (#16160)
* refactor(daemon): split oversized PTY services * revert(daemon): restore merge-base session listing and canceled-spawn behavior Two behavior changes rode along with the file-splitting refactor: - listLiveTerminalHostSessions dropped sessions with isTerminating, not just dead ones, hiding sessions the merge base still advertised. - spawnAndPublishSession called session.beginTermination() before publishing a canceled spawn into the host map. Both hunks are reverted to the merge base; the refactor is untouched. |
||
|
|
09048c63d4 |
feat(orcad): add headless browser providers (#16193)
* feat(orcad): add headless browser providers * fix(orcad): merge the duplicate runtime-browser type import |
||
|
|
788575e300 | fix(crash-reporting): stop destroying user crash notes (#15252) |