* 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 aee07c24aa reverted. Same for
windows-console-foreground.ts, which guards a job read now.
- Gate the two files that had no coverage: the job read and the retire path.
* fix(windows): bound the provider's job short-circuit too
The previous commit fixed the daemon retire path and left the identical bug in
the local provider, which I found while asking the reviewer to check for it.
local-pty-provider.ts returned the cached agent early on `size > 1` and that
early return skips the scan at the bottom of getForegroundProcess -- the ONLY
code that can delete ptyLastRecognizedForeground. So on a WSL pane, whose job
always holds console-detached plumbing, the short-circuit was permanent and the
identity could never be cleared. Same failure, second location, and the daemon
fix did nothing for it because this path never calls retireStaleForegroundIdentity.
The cache was a bare Map<id, name> with no timestamp, so bounding it needs one.
Added ptyLastRecognizedForegroundAt, stamped only when the recognized name
actually changes, and paired with every existing delete including pane teardown
so the new map cannot outlive the old one.
The 30s threshold now lives in windows-cached-agent-revalidation.ts rather than
being duplicated: that module already answers "can we revalidate this cached
agent without a scan", and the max age is the other half of that question.
Also renamed two tests that still said "ConPTY console presence" while driving a
job read. Re-conflating those two sets by name is how this PR got its first two
review rounds wrong.
* fix(windows): stamp the provider cache on every confirmation, not on change
My own previous commit was wrong, and wrong in the direction #9258 exists to
prevent. Review caught it; the test in this commit reproduces it first.
I stamped ptyLastRecognizedForegroundAt only when the recognized name CHANGED.
That makes the value the time of first recognition, so the age measures how long
the agent has been running rather than how long since we last confirmed it. For
a live agent recognized as the same name every cycle the stamp never moved, the
age crossed 30s and stayed there, and the short-circuit died permanently.
Two consequences, the second serious:
- every getForegroundProcess call on a >30s-old agent pane ran the whole-table
scan, defeating the exact optimization the branch exists for;
- with the short-circuit off, one available-but-agentless snapshot was enough to
delete a LIVE agent's identity, because paneMembershipUnavailable is false in
this state so the degraded-scan substitution does not engage. That is the false
"agent done" this code's own comment warns about.
The daemon path was already right -- it re-stamps refreshedAt on every positive
recognition -- so the same constant meant two different things in the two files.
Now both mean "time since we last saw the agent", which turns the bound from
"disable the short-circuit after 30s" into "force one revalidating scan every
30s": ~16-31ms per pane per 30s via the native process table.
Test asserts the scan count stops incrementing after the revalidation, and fails
against the stamp-on-change form.
Also correct the shared docstring, which had dropped the invariant the whole
design rests on, and stop calling this a WSL bug: the trigger is a persistent
console-detached job member plus a fallback that reads as a shell. wsl.exe is
not in SHELL_NAMES, so a plain WSL pane does not even reach this code -- WSL is
just where it was measured.
* refactor(windows): shrink the job-membership path
Elegance pass. No behaviour change -- all three mutation checks still bind
(restoring the size>1 veto, stamping only on name change, dropping the root
requirement each turn their tests red).
- windows-pty-job-membership.ts 54 -> 31 lines. A deps object carrying one
optional function became a defaulted parameter, the accumulate loop became a
filter, and the docblock lost two thirds of its bulk.
It also lost a claim that was simply false: it said a widened set "is the
conservative direction for every caller: it keeps a live agent rather than
retiring it early". For the retire caller, never retiring IS the failure --
that is the bug this stack just fixed, still being described as a feature
three commits later.
- One local `identityOlderThan(ms)` in the tracker replaces two hand-rolled
`Date.now() - refreshedAt` comparisons, one of which I had added.
- The provider's two parallel maps collapse into one Map<id, {name, at}>.
Parallel maps meant every delete site had to remember its sibling, in three
places; the reviewer flagged the leak risk and I fixed it by pairing them,
which leaves the hazard for the next person. One map removes the class.
Comments trimmed to the load-bearing sentence throughout, per AGENTS.md.
* fix(windows): preserve foreground cache age evidence
* fix(windows): anchor cached agent identity to the pid that proved it
The job short-circuit and retirement veto only knew 'something besides the
shell is alive', so a detached leftover pinned a dead agent's name for the
30s age bound, and 30s of incomplete-but-successful scans could retire a
live one. The scan already knows which row proved the name: carry that pid
through the resolution, and judge the cache against the job with it --
membership of a known pid in a complete, inescapable job list is proof of
life (restamp, never expire), and its absence is proof of exit (retire now,
leftovers notwithstanding). Unanchored identities keep the age-bound
superset behavior.
* fix(windows): anchor the reported process, and let a scan refute a recycled pid
Review findings on the pid anchor:
1. The anchor followed the LEAF that proved a collapsed name: 'omp' reported,
pi's pid stored. Pi exiting or restarting under a live OMP then read as the
wrapper's exit -- retiring the identity before a scan that (degraded) may
miss OMP, a false 'agent done'. resolveOuterWrapperForegroundIdentity now
carries the pid of the process the name belongs to.
2. A bare numeric pid can be recycled inside the pane's job, making membership
falsely confirm a dead identity indefinitely. Command lines are immutable,
so a scan row holding the anchor pid without recognizing as an agent proves
a different process: the resolution reports it (anchorPidForeign) and both
consumers retire immediately. A query-denied row (command falls back to the
image name) stays inconclusive -- never grounds to drop a live agent.
* fix(windows): find a recycled anchor pid in the full table, not the ppid walk
A squatter that inherited the pane job from a leftover whose creator then
exited is orphaned out of the shell-rooted descendant projection, so the
foreign-anchor refutation never saw its row. Pluck the anchor pid's row from
the same whole-table snapshot instead; a job member holding the pid is in the
table even when no ppid chain reaches it.
* fix(windows): survive an agent restart, and refute a squatter by name
Two review findings on the exit verdicts:
1. 'exited' deleted the cache before the scan, so an agent restarting under a
new pid plus a degraded scan at that instant reported the shell -- a false
'agent done'. Only the shell standing alone is decisive now; an anchor
leaving a job that still has members downgrades to unanchored, age-bounded
evidence and lets the scan decide. The daemon tracker keeps immediate
retirement: its verdict path only runs after an available scan already
found no agent.
2. The foreign-anchor refutation treated any recognized row as 'ours'. A pid
recycled by a DIFFERENT agent now compares against the cached name the
anchor is supposed to prove.
* 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
* 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>
* 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.
* Refactor terminal coordination modules
* preserve terminal completion and stale-connect guards
* restore pre-spawn E2E barrier and stale-connect check order in ipc-pty-connect
* restore merge-base title-working replay and stamped-tail delete semantics
* fix(terminal): merge the duplicated shortcut-matching import
Two adjacent imports of the same module tripped oxlint's
no-duplicate-imports under --deny-warnings. Import-only; no behavior change.
* 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.
* 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.
* refactor(editor): split editor and watch surfaces
* fix(editor): revert behavior changes smuggled into the surface split
Restore merge-base React keys in IpynbCellOutputs: the content-identity keys
JSON.stringify'd every output value, including raw base64 image payloads, on
every keystroke.
Collapse the duplicated lazy() declarations into editor-lazy-views so each
viewer keeps a single React.lazy identity across the extracted surfaces.
A diagnostic's span often reaches past the block a split moved — most commonly
to a hook dependency array, which legitimately grows when closure variables
become props. Requiring every line of the span to match contiguously reported
the moved body as new.
The block must still start at the same line in the base and appear in order,
and >=90% of it must be present. Genuinely new code shares neither the anchor
nor the ordering.
* 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.
* refactor(renderer): split composer state
* fix(renderer): satisfy composer static analysis
* test(renderer): migrate composer boundary contracts
* fix(composer): restore project group reset effect
Revert read-side mask back to the merge-base state clear so a momentarily
unavailable host permanently drops the folder group instead of silently
retargeting Create when the host reappears.
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.
* 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.
A file-splitting refactor makes every line of the new module an added line, so
pre-existing lint debt in code that merely moved starts failing the gate. The
only way to satisfy it is to edit the moved code, which is what a
behavior-preserving refactor must not do. Exempt a diagnostic when its
highlighted lines already existed verbatim and contiguous in the base revision.
* 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.
* build(windows): drop the packaged node-pty prebuild that can silently replace the patch
node-pty's loader tries build/Release, then build/Debug, then
prebuilds/<platform>-<arch>, and swallows every failure in between. Windows
packaging ships both the source build and the prebuild, and only the source
build carries Orca's job-object exports (listJobProcessIds, terminateJob,
assignCurrentProcessToJob).
So an ABI mismatch, a truncated file, or an AV quarantine of
build/Release/conpty.node degrades the shipped app to the UNPATCHED prebuild:
PTY teardown silently falls back to guessing by PID ancestry, with no error
anywhere. That is the failure mode that made #16059 hard to see -- an install
that looks fine and quietly cannot own a PTY tree.
Removing the fallback turns a silent downgrade into a loud load failure.
Scoped narrowly: only win32, and only when the source build is actually
present, so a build that legitimately has no build/Release keeps something
loadable. macOS and Linux prebuilds are untouched -- they have no patched
export to lose.
Refs #16059.
* fix: delete only the stale conpty fallback, not the whole prebuilds tree
Review caught a P0 in the first version of this change, and it was the same
defect the PR exists to prevent, pointed at a different target.
Orca's own patch removes the `conpty_console_list` and winpty `pty` gyp
targets, so a Windows source build emits conpty.node and nothing else.
conpty_console_list.node, pty.node, winpty.dll and winpty-agent.exe therefore
exist ONLY in prebuilds/. Deleting the tree removed them:
- the forked console-list agent throws at require, and its caller resolves null
with silent: true, so console-membership probing dies with no log anywhere --
a new silent degradation, in a PR whose thesis is "make it loud";
- node-pty still selects winpty below Windows build 18309, so PTY spawn would
fail outright on Server 2019 / Win10 LTSC 2019.
Now removes only prebuilds/win32-<arch>/conpty{.node,.pdb}, and only when
electronArch matches the host arch -- a cross-arch package copies the host's
build/Release, so its presence does not mean it matches the target, and
deleting the target-arch prebuild would remove the only loadable binary.
The old fixture wrote just conpty.node, so it could not see any of this. It now
seeds a realistic prebuilds directory, and four tests assert each sibling
survives; all four fail against the broad delete.
Credit: review counsel.
* 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.
* refactor Linear workspace surfaces
* refactor(linear): restore merge-base behavior in split modules
The Linear surface split smuggled in three behavior changes; revert them
so the refactor is a pure move.
- detail-state: drop 'project' from EDITED_LINEAR_ISSUE_FIELDS. List
issues never carry `project` (only getIssue maps it), so preserving it
across hydration permanently blanked the hydrated project whenever an
edit landed while linearGetIssue was in flight.
- detail-state: handleProjectChanged no longer sets hasEditedRef.
- project-selector: remove the mountedRef/requestId guards around the
global patchLinearIssue write and the success/error toasts.
- sub-issues: remove the added isComposing guard on the title Enter key.
The detail-state test asserted the smuggled project-preservation; updated
to assert hydration owns `project`.
* refactor settings maintenance modules
* revert behavior changes smuggled into settings split
- hoist isAdvancedOpen state back into RepositoryHooksSection so it survives
SearchableSetting unmount during settings search
- drop isComposing guards absent from the merge-base AgentsPane handlers
- restore merge-base JSX for the 'when one exists.' fragment (no separator)
* refactor feature wall animated visuals
* fix(feature-wall): restore merge-base render behavior in split visuals
- Hoist workbench reduced-motion state to module constants so cursorTarget
identity is stable and the cursor layout effect stops re-firing per render.
- Render one frame component and branch on the state source so toggling
reducedMotion re-renders the storyboard instead of remounting its DOM.
* rm unused files
* remove unused files
* Refactor PTY IPC and add host environment paths
- Split PTY handlers out of inline baseline checks
- Rename local PTY shell provider for clarity
- Pass userDataPath and resourcesPath to host environment
* Establish PTY daemon identity before first await in spawn flow
Move identity setup, session ID minting, and hidden delivery state to
the beginning of preflight, ensuring these complete synchronously
before any awaited operations. Defer async operations like folder
workspace validation; add liveness tracking for SSH provider failures.
Refactor pane spawn reservation to prevent concurrent spawns from
creating duplicate providers.
* Add incarnationId tracking throughout PTY exit lifecycle
Track PTY incarnation IDs in exit messages sent to renderer, and add cause tracking for exit events. This enables proper lifecycle state management when PTYs can be respawned or have multiple concurrent instances. Also adds deadline support to process listing operations and stop-request tracking for better shutdown observability.
* Use fake timers in SFTP namespace tests for deterministic abort handling
Tests now use `vi.useFakeTimers()` to control time during abort scenarios,
advancing timers explicitly instead of waiting on real async delays. Ensures
more reliable test execution without flakiness from timing-dependent behavior.
* Fix PTY spawn lifecycle: handle concurrent races and cleanup abandoned a
Properly release Agent Teams leader handles when spawns are abandoned or fail,
restore provisional PTY sizes on reattachment, and settle concurrent spawn races
for the same pane. Add validation guards for destroyed renderers and improve
handler re-registration to reset delivery state before bridging a new window.
* Move PTY cleanup to localized error boundaries
Restore provisional PTY size when build-options fails and guard pre-allocated handle registration. This ensures cleanup happens at the point of error, not deferred to the general catch block.
* Replace Promise.resolve() with vi.waitFor in PTY claim test
Wait explicitly for the providerSpawn call to be made using vi.waitFor()
instead of relying on event-loop yielding. This makes the test more
deterministic and reduces flakiness from timing assumptions.
* Redact PTY IDs in pending data drop diagnostics
Prevent workspace paths embedded in session IDs from leaking through
diagnostic logs by using redactPtyIdForDiagnostics.
* Mark PTY exit events as observed by provider
Exit handlers now receive `providerExitObserved: true` to
distinguish definitive provider-witnessed exits from inferred
state changes. Preserves optional exit cause when present.
* Add defensive input validation to PTY IPC handlers
Validate that IPC arguments are present and the correct type before
passing them to handler logic. Uses optional chaining and type checks
to safely handle malformed requests from the renderer process.
* Replace direct Electron imports with PTY host bindings
Abstract app, ipcMain, and powerMonitor access through getter functions
to support multiple host environments and improve testability.
* Defend against transient PTY setup failures with state cleanup
Host-env setup failures now trigger cleanup of runtime-allocated PTY state. Cached PTY geometry is preserved after transient reattach failures but cleared when the provider reports the PTY exited before the spawn reply—preventing stale geometry from corrupting future operations. Error handling now distinguishes expired SSH sessions and early-exit conditions to preserve geometry appropriately.
* fix(agent-status): clear the pane when a Claude compact finishes (STA-2915, STA-4613)
A manual /compact ends at an idle prompt without emitting Stop, so nothing in the
compact window could ever clear the pane. A worktree that entered the compact
`working` stayed `working` until the 30-minute stale sweep -- and the summarizer's
start-less SubagentStop kept republishing the row, resetting that clock each time.
The correlation added by #12332 was supposed to own this, but it could never run:
PreCompact and PostCompact were never added to CLAUDE_EVENTS, so they were never
registered with Claude. compactTrigger was always undefined, and the transition
guard, the ownership cache, the relay wire field and the ingest branch were all
unreachable. Five test files exercised the logic by injecting events past the
registration boundary, so the suite stayed green over code that could not execute.
Register PostCompact -- and deliberately NOT PreCompact. Measured on Claude Code
2.1.227, a successful manual compact emits PreCompact, a start-less SubagentStop,
SessionStart(source=compact), then PostCompact; an ABORTED compact ("Not enough
messages to compact") emits PreCompact ALONE. Mapping PreCompact to `working`
would strand the pane on every aborted compact, which is the bug being fixed, so
the abort guard is structural: Orca never subscribes to the pre-validation event.
PostCompact carries its own trigger, so no anchor is needed to tell manual from
auto and the correlation machinery is deleted rather than repaired. Manual becomes
a `done` with sessionBoundary set -- a finished compact is a session-shaped
boundary, not a completed turn, so completion notifications, unread counts and
automation-run evidence stay out of it. Auto claims nothing: it runs inside a turn
that resumes and emits its own Stop.
The source-blind early return that dropped compact events for EVERY provider
before its normalizer ran is narrowed to Claude, so it keeps failing closed on a
malformed payload without pre-empting other providers.
Ownership is kept where the deleted guard had it: a valid provider prompt id is
required, a completion clears a row but never creates one (a retired pane must not
be resurrected), and a hydrated row is matched on provider session only -- it
carries the previous session's connectionId, and older rows carry no session at
all, so a strict check would reject the restart case this fixes. A consumed
prompt id keeps relay duplicates from refreshing the row.
Mixed versions: no new wire field and no new opcode. An older relay normalizes
with its own shipped mapping and forwards the event, so ingest drops `auto`
envelopes and stamps the boundary on `manual` ones; its replay strips the trigger
entirely, so payload state stands in for it while ownership is still enforced. The
relay now caches a completion with its compact identity removed, so a client that
was offline during the compact still receives the clearing row on reconnect.
Tests go red before this change and green after: 6 of 12 in the new
registration-gated suite and 5 of 8 in the relay/ingest suite. The harness delivers
only events present in CLAUDE_EVENTS, so a fix that is never registered cannot
pass -- the failure mode that let the original correlation ship unreachable.
* test(agent-status): restate the compact reliability gate around the new invariant
The gate pinned a test file this change deletes, so the manifest check failed.
Repointing the path alone would have left the gate describing an invariant that
no longer exists: it required a manual PostCompact to match its exact PreCompact
generation, and PreCompact is no longer consumed at all.
Restate it. The invariant is now that PreCompact never moves a pane, that only a
manual PostCompact marks done and does so as a session boundary, that a
completion clears an existing row but never creates one, and that a relay
predating the contract has its automatic envelopes dropped and its trigger-
stripped replays classified by payload state under the same ownership checks.
Evidence runs are the real ones: the 105-test suite from this branch, and the
Claude Code 2.1.227 PTY capture that measured PreCompact arriving alone on an
aborted compact.
* fix(agent-status): clear the restart-stuck pane a compact was meant to clear
Review found the completion did not clear the pane STA-2915 actually reports, and
that republishing it was a strict regression.
- A manual completion now retires a subagent that exists only as a disk snapshot:
a /compact only completes at an idle prompt, so a restored child is proof of
nothing. Live evidence -- a child observed in this runtime, an unclassifiable
running background task, a registered session cron -- still holds the pane.
- A completion that cannot clear now publishes nothing instead of restating the
row, which was stripping restoredUnconfirmed off a hydrated row and restarting
the staleness clock for work the compact never observed.
- The relay defers compact ownership to the client that owns pane identity, so a
cold relay cache can no longer swallow the one event that clears a remote pane.
- claudeConsumedCompactPromptIdByPaneKey joins all three pane-scoped teardown
routes, and an auto compact no longer spends the pane's consumed-compact slot.
- The promptless completion keeps the summarized turn's label with or without a
trigger on the envelope.
Tests: the two restart cases now deliver the completion while the hydrated row is
still cached, so they exercise the restored-row branch instead of passing through
the strict one; the triggerless working replay is asserted from a FINISHED pane so
it can fail. Reverting the four source files turns 12 of 21 registration-gated and
8 of 12 relay/ingest tests red, and 18 of 18 targeted mutations are caught.
* fix(agent-hooks): preserve compact identity across relay replay
* docs(reliability): describe compact replay ownership