mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 16:03:53 +00:00
788575e300cb24d2bee7ecd7bb014f05304efe88
3380
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
788575e300 | fix(crash-reporting): stop destroying user crash notes (#15252) | ||
|
|
b6a24ecac1 | refactor: split GitHub backend modules (#16194) | ||
|
|
9bcaf09869 | refactor loading store into cohesive domains (#16192) | ||
|
|
b516300b8c | refactor agent hook listener modules (#16187) | ||
|
|
07b13c8468 | refactor git repository host boundaries (#16177) | ||
|
|
1c436a8084 | refactor(rpc): split terminal methods into cohesive modules (#16188) | ||
|
|
4371aaf722 | refactor provider clients into domain modules (#16168) | ||
|
|
5c116b6ec2 |
fix(startup): stop the PATH seed pinning nvm to its newest install (#16314)
* fix(startup): stop the PATH seed pinning nvm to its newest install patchPackagedProcessPath prepends the newest nvm version dir to process.env.PATH, then hydrateShellPath probes the login shell with that same env. nvm's startup `use` honors whatever node is already on PATH instead of the user's `default` alias, so the probe returns a PATH pinned to the newest install and every terminal pane inherits it. A user whose newest nvm node is a bare install then loses every global CLI (codex, claude, gemini, vercel...) inside Orca while they still resolve in Ghostty/Terminal, which start from the bare GUI PATH and fall through to `default`. Probe with the PATH the process launched with. Windows already gets this through WindowsShellPathOwnership. * test(startup): pin the platform in the probe-env test shellProbeEnv short-circuits on win32, so the POSIX-only assertion failed for anyone running the suite on Windows. Matches the convention in hydrate-shell-path.windows.test.ts. Also narrows the win32 exemption comment: WindowsShellPathOwnership snapshots its baseline after the seeds land, so it does not unwind them. The exemption holds because Windows keys PATH as `Path` and no Windows seed pins a node version. * fix(startup): give win32 the same probe insulation, keyed by Path The previous commit exempted win32 on the stated grounds that no Windows seed pins a node version. That is wrong: getVersionManagerDirectories calls getNvmVersionDirectories on every platform, so a Git Bash user whose nvm uses the POSIX ~/.nvm/versions/node layout gets the newest version dir seeded on Windows too, and the -ilc Git Bash probe inherits it. Record the PATH key alongside the value and overwrite that entry in place, so Windows never carries both `Path` and `PATH` — which was the only real reason to skip win32. * fix(startup): snapshot the launch PATH at module load, log probe failures Three loose ends from the review, folded in rather than deferred. The probe's clean PATH was handed over by an explicit recordLaunchPath call from the seeding site, so the invariant lived across three files and a refactor that moved the seed call would silently re-pin nvm. Snapshot PATH during module init instead: that runs while the import graph is evaluated, strictly before any statement in main's body, so it cannot observe the seeds and there is no call ordering left to break. All seven importers are static, so no lazy import can defeat it. A test asserts the probe ignores later process.env mutation, and fails if the live read is reintroduced. A failed startup probe leaves the seeded newest-nvm dir in front and said nothing, so the population whose rc files blow the 5s budget hit the original symptom with no diagnosable trace. Log the failureReason. The probe is an interactive login shell, so rc files that exec into a multiplexer or start a heavy prompt can outrun that budget with no way to opt out. Set ORCA_SHELL_PATH_PROBE=1 so they can take a fast path. * fix(startup): drop the other-cased PATH key from the Windows probe env Caught by running the suite on a real Windows machine, not a mocked platform. The spread of process.env is a plain, case-sensitive object, while Windows resolves env names case-insensitively. Writing the captured `Path` back onto it left the seeded value still live under `PATH`, so the probe shell could read either one — the exact duplicate-key hazard the win32 branch was supposed to prevent. Drop any other-cased variant of the key before writing. * fix(startup): preserve launch PATH across app restarts |
||
|
|
0bfd3808f6 |
test(pty): pin the renderer-liveness guard STA-2373 relies on (STA-5373) (#16345)
* test(pty): pin the renderer-liveness guard STA-2373 relies on (STA-5373) STA-5373 reported that #15927 dropped the `webContents.isDestroyed()` half of the guard #10065 added for STA-2373, and that the app therefore dies when a daemon death fans out to every pane. The guard is present on main: #15172 restored it at both senders when it split the PTY monolith, and a per-file count across v1.4.188..HEAD shows only relocation (pty.ts:4 -> write-input.ts:2 + bind-listeners.ts:2). The reported 4 -> 0 was scoped to `src/main/ipc/pty.ts`, now a 39-line barrel. No production fix is needed. What was real is that nothing pinned the guard — it survived #15927 only because #15172 happened to re-expand it — and the shared PTY test fake made that invisible: its `webContents` had no `isDestroyed` at all, so both guards passed vacuously in every suite. That is also why they are written defensively as `typeof ... === 'function' && ...`. - Give the fake an `isDestroyed` mock, re-stubbed to `false` each beforeEach rather than left `undefined`, so "alive" is stated rather than accidental. - Cover both senders red/green: the daemon-death fan-out (bind-listeners.ts:37) and the per-write reporter (write-input.ts:57). Verified red against a window-only guard and green with the real one. The per-write case needs a chunked write. A single-chunk write is already fenced by `isPtyWriteEventFromMainWindow`, which rejects the event once the WebContents is gone; only the multi-chunk path yields a macrotask mid-write, letting the renderer die after the sender check passes. That is the sole route to this sender with a dead WebContents, and what makes its own guard load-bearing. Also deletes `src/main/ipc/pty-renderer-surface.ts`: zero importers repo-wide, and its `isRendererGone` is exactly the weakened predicate. Its comment claims the headless path "now passes null", but register-headless-runtime.ts:26 still fakes `{ isDestroyed: () => true }` — #15172 rolled that back too. Adopting it would reintroduce STA-5373 for real. Verified: 17 tests across the two pty write suites; full src/main/ipc run 3156 pass (4 pre-existing @parcel/watcher failures in filesystem-watcher-real and worktree-base-directory-poller, unrelated and failing identically on a pristine tree); pnpm typecheck clean; oxlint clean. * docs(pty): correct headless-path comments the #15172 rebase left stale #15927 made `registerPtyHandlers` accept `BrowserWindow | null` so the headless path could pass null instead of faking a window. #15172 reverted that signature while splitting the PTY monolith, but the comments describing it survived, so three of them now document an API that does not exist. - orcad-entry.ts: the module docstring claimed orcad installs its controller via `registerPtyHandlers(null, …)`. It uses `registerHeadlessPtyRuntime`, and null is not accepted. It also claimed desktop surfaces are "declared rather than faked" — true except for the renderer window, which is still faked. - register-headless-runtime.ts: record why the fake is safe rather than leaving `isDestroyed: () => true` looking arbitrary. It is load-bearing: every renderer-liveness guard reads it and skips, so no send is attempted. Also gives the fake a `webContents.isDestroyed`. The real guards check both, and a missing method reads as "alive" — the same gap this PR fixes in the test fake. No behavior change: the window-level check already short-circuits. Verified: 45 tests across the liveness-guard, startup-barrier, management and kill/exit suites; pnpm typecheck clean; oxlint clean. |
||
|
|
1a7934c54f |
Fix deleted remote worktree reappearing due to host ID mismatch (#16039)
* Fix deleted remote worktree reappearing due to host ID mismatch Paired clients and servers may use different spellings for execution hosts (e.g., client 'runtime:env-1' vs server 'local'). Resolve these spellings before worktree.rm and worktree.forceDeleteBranch to prevent failures and orphaned worktrees. * Validate runtime kind before comparing environment IDs Ensure parsed host IDs are actually runtime environments before accessing their environmentId property. Fixes incorrect host ID matching during worktree cleanup that caused deleted remote worktrees to reappear. * Use hostId directly when same-ID surviving host exists When a surviving host has the same ID as the deleted worktree's original host, use the hostId directly instead of qualifying it through the runtime call host. This prevents worktrees from reappearing due to host ID mismatch. --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
50438041a8 | fix(rate-limits): safely surface Codex RPC exit reasons (#16023) | ||
|
|
31562c5b27 |
fix(windows): attach interactive login children to console input
Verified on native Windows awin at the exact PR head with Electron CDP/Playwright: the Claude sign-in console is visible, cancellation after console launch restores Add Account state, and the login process/PID/temp cleanup completes. |
||
|
|
7e7947665c | fix(relay): gate relay control work on proven broker liveness (#16021) | ||
|
|
fba910f2ea | fix(crash-reporting): scope renderer crash evidence (#16313) | ||
|
|
723158a519 |
feat(workspace-cleanup): blockers become labels, not refusals (#16282)
* wip(workspace-cleanup): PR 3 blockers-become-labels, recovered from a dead worker Uncommitted work recovered from a worker that died with 'Agent process stop was requested but never confirmed'. Committed as-is to preserve it; NOT verified yet. * Fix workspace cleanup review regressions * fix(workspace-cleanup): drop filter chips for the safety fields this PR removes The cleanup dialog crashed on every open. My merge of #15300 brought in the applied-filter chips, which read `safety.tiers` and `safety.selectableOnly` -- the exact fields this PR deletes. Electron QA caught it; the chip derivation runs on open, so it threw before anything rendered. Removed the two chip branches, their formatter entries, and the now-dead 'tier' chip-kind label. The test that swept every chip keeps its breadth by using `safety.dismissed`, which survives. Worth noting what did not catch this: typecheck flagged only the test file, not the source, because the QA agent had already patched the source locally without committing. A merge that compiles can still remove a field a caller reads at runtime, and only opening the dialog proved it. |
||
|
|
beb1d45198 |
test(windows): run PTY IPC suites in Windows lane
Merged after clean CI, Windows PTY IPC validation, and readiness review. |
||
|
|
3bc13f7b8c |
Split monolithic PTY IPC module into organized submodules (#15172)
* 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. |
||
|
|
ec4687c434 |
feat(agents): distinguish Claude background monitoring (takes over #14205) (#16201)
* feat(agents): distinguish Claude background monitoring Adds an optional `workingMode: 'monitoring'` discriminator for a Claude session whose lead turn finished but which still has background shell tasks or session crons registered. The wire state stays `working`, so older peers that never read the field keep rendering Working. (cherry picked from commit |
||
|
|
1921ba2250 |
fix(agent-status): clear the pane when a Claude compact finishes (STA-2915, STA-4613) (#15202)
* 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 |
||
|
|
08f5570a43 |
Fix mail pointer repoint scheduler tests with pending count getter (#16292)
Add pendingCount getter to MailPointerRepointScheduler to expose the number of handles awaiting repoint. Replace flaky vi.getTimerCount() assertions with direct scheduler state checks. |
||
|
|
43461fca46 |
fix(ai-vault): replace scanner internals with actionable panel copy (#16229)
* fix(ai-vault): replace scanner internals with actionable panel copy Agent Session History painted the scanner's supervision errors verbatim: "AI Vault service restart circuit is open." and "AI Vault service timed out after 130000ms." Neither tells a user what happened or what to do. Add a shared mapper that rewrites the supervision family into copy tied to an action, passing anything unrecognized through so scanner-authored messages (host name, remote path, cap) keep their own wording. It also strips Electron's `Error invoking remote method` wrapper, which this path never handled. Applied at both surfaces the panel paints: the local leg's scan-issue row and the thrown-rejection banner. Humanizing in main covers remote clients too; the raw text moves to the main log. Also make "Refresh to try again" true. The relay path lets a forced refresh reopen the restart circuit, but on the local path `force` stopped at the cache layer and never reached the supervisor, so the refresh button was inert for the full 60s fault window. The client file sat at 299/300 lines, so extract the child-listener and init-frame wiring into `attachAiVaultServiceChild`, next to the ready-waiter and retirement helpers it belongs with, rather than bumping the max-lines ceiling. * fix(ai-vault): humanize runtime scanner errors * fix(ai-vault): preserve runtime error metadata * fix(ai-vault): normalize wrapped scan errors * fix(ai-vault): preserve non-scanner relay errors * fix(ai-vault): make forced retries cancel backoff |
||
|
|
7e76bb3aec |
Fix rebase race by fetching to private ref before rebasing (#15990)
* Fix rebase race by fetching to private ref before rebasing
`git pull --rebase` is vulnerable to concurrent fetches modifying remote-tracking refs during execution. Fetch to a temporary private ref (refs/orca/rebase/*) first, then rebase from that stable ref to avoid the race condition.
* Fix rebase race by fetching to private ref with timeout
Concurrent fetches can interfere with remote-tracking refs between
fetch and rebase. Use a unique private ref and 60-second timeout to
isolate each rebase operation and prevent hangs on stalled remotes.
Extract gitPullRebaseFromBase to a dedicated module.
* fix rebase race by fetching to private ref with timeouts
Concurrent fetches can replace FETCH_HEAD and remote-tracking refs between
fetch and rebase, causing the rebase to fail. Fetch to a temporary private
ref instead, use --no-write-fetch-head when available (Git 2.29+), and
serialize FETCH_HEAD access for older versions. Add process termination
barriers to ensure proper cleanup and extend timeouts for SSH operations.
* Fix rebase race by fetching to both private and tracking refs
Concurrent fetches between source and rebase can replace remote-tracking refs,
causing rebases to use stale bases. Now fetch to both a private ref and the
remote-tracking ref simultaneously, ensuring the tracking ref stays current.
Also improves process termination for WSL guests with process-group tracking,
fixes process-tree termination timeouts on POSIX, and serializes FETCH_HEAD
operations for linked worktrees through their shared Git directory.
* Add WSL setsid --wait probe and barrier termination timeout
Probe for `setsid --wait` support and fall back to unwrapped execution for BusyBox compatibility. Add a deadline for process termination barriers to prevent hanging when tree termination cannot be verified. Update tests for cross-platform compatibility.
* Add wsl-process-group-termination to WSL invocation allowlist
* Serialize per-worktree git mutations to fix rebase race
Introduce operation locking for each worktree to prevent concurrent
mutations (like rebase) from interfering with each other. Ensures
rebasing a linked worktree doesn't affect the source worktree state.
Add SIGKILL fallback if process termination barriers cannot verify
tree termination.
* Serialize pull and fastForward operations per-worktree
- Extract generic git operation lock to reuse locking pattern
- Refactor existing locks to use the generic implementation
- Apply per-worktree serialization to pull and fastForward to prevent races
* Route WSL group termination through runWslProcess
|
||
|
|
aa32871a61 |
Improve cmd j ranking with recency (#16281)
* Track container-only tokens and tab focus for cmd+j ranking Previously ranked by whether any container-only matches existed (boolean); now counts tokens matching only containers for finer-grained ranking. Tab focus recency is now tracked explicitly so recent refocuses rank above stale worktree activity. Preserves worktree grouping by input order while applying focused-group MRU within each block. * fix(cmd-j): preserve duplicate recent tab occurrences * fix(cmd-j): preserve host scope during worktree purge * fix(cmd-j): scope repo purge for exact-id host twins * fix(cmd-j): scope ssh visit recency to local to survive restarts Boot hydration loads only local + runtime:* partitions, so routing ssh-qualified recency to ssh partitions strands it across restarts. - Keep ssh-qualified visit timestamps in local partition - Route runtime-qualified keys to their partition - Remove groupId from recent tab occurrence base (unstable on regroup) - Collapse bare and host-qualified timestamps, preserving max - Simplify repo pruning host-match logic - Add robustness: optional chaining, helper function * Scope focused tab recency by worktree to fix Cmd+J ranking Tab ids can be duplicated across worktrees; scoping recency keys to per-worktree prevents one worktree's MRU position from overwriting another's in Cmd+J. Scope worktree order blocks to (hostId, worktreeId) to keep same-id worktrees on different hosts separate. Also fix recency preservation during partial identity migrations and prune orphaned host keys on removal. |
||
|
|
94f231737d |
fix(agent-status): retire panes whose agent process is gone (STA-4612) (#15212)
* fix(agent-status): retire panes whose agent process is gone (STA-4612) Agent status can hold `working` on a pane where no work is outstanding, and nothing closes the gap. A pane's Claude state is a join of a lead turn and three latches — the subagent roster, the background-task gate and the session-cron gate — and each is set by a hook and cleared only by another hook. Claude Code emits no terminating hook on `/exit`, `/clear`, Ctrl+C, crash, SIGKILL or terminal close, so every one of those latches is a claim with no owner and no expiry. The join is also materialised at ingest time and persisted, so a stale `working` survives restart and blocks hibernation, which requires `done`. Registering `SessionEnd` is not the fix: it covers roughly a third of exit paths (measured on 2.1.231/2.1.233; upstream anthropics/claude-code#17885 and #6428 are both closed as not planned). Nor is a TTL — `AGENT_STATUS_STALE_AFTER_MS` only decays the sidebar dot at read time while the stored row stays non-terminal. So the backstop is built from evidence Orca already owns. A session id that changes means the conversation was replaced. On the first hook of the new session — whatever that hook is — the previous session's own claims are void: its session crons and its one-shot subagents. Deliberately not voided: the background-task gate (a background shell is an OS process that survives `/clear`, and the previous inventory is positive evidence it was running), and `confirmedTeammate` rows (persistent in-process teammates a lead swap cannot end). The lead record is left to the incoming event's own fold. A certified process exit retires the pane. Orca already does this on every attributable PTY exit — `clearProviderPtyState` resolves the pane key and calls `clearPaneState` — but that resolution depends on the spawn-time `ptyPaneKey` mapping, which a restored or reattached PTY may never rebuild. Those panes keep their row and latches for good. `onPtyExit` knows the keys teardown could not resolve, so it reconciles them from its own records. The certificate is `exitCode >= 0 || hostExitConfirmed || providerExitObserved`: a synthetic `-1` from a failed stop is not a death (the PTY can have survived it), while a real exit can also report `-1`, so neither the code nor the SSH surface predicate is sufficient alone. `providerExitObserved` is additive and separate from `hostExitConfirmed`, which also drives the liveness verdict and the SSH surface decision. A confirmed shell foreground is the `/exit` case: the agent died, the shell lived. That already dropped the row, but through `agentStatus:drop`, which by its own contract preserves a live pane's caches — so every latch survived and the next event resolved the pane back to `working`. It now routes through the reconciler instead, gated on a per-pane accepted-status generation rather than row identity: the confirming process read can take seconds, and `updatedAt` cannot order two writes inside one millisecond (the store deliberately admits equal timestamps). Cold start generalises the same way. The startup sweep required a restored subagent roster, so a stranded lead row, background-task gate or cron gate — the shapes with no child event left to reap them — were never candidates. Hibernation needs no change: with the above, those rows become genuinely `done` and the lockout resolves through the front door. A `restoredUnconfirmed` bypass in the planner would let it reclaim the heap of an agent that may be working. Not included: folding `background_tasks` from a child-attributed `SubagentStop`. Writing its test surfaced #11838's deliberate assertion that child inventories are not authoritative for lead-owned background work, and the listener says the same — "background_tasks is trusted only where unambiguous". An empty list on a `SubagentStop` does not prove the lead's shell ended, so the fold would have cleared a gate on evidence that establishes nothing. STA-4119's live-side question — whether a genuinely live background shell should hold the lead row after the lead turn ends — is untouched. This change extends gate-clearing to zero new triggers. * fix(agent-status): make the confirmed-shell reconcile survive its own drop The /exit leg never fired. `settleDeferredCommandFinishedStatusDrop` runs the paired drop before the reconcile, and `dropAgentStatus` cleared the per-pane accepted-status counter the reconcile's guard then read — so the guard compared a live anchor against a zeroed counter and skipped itself on every pane that had a status row, which is every pane worth reconciling. The existing test passed only because it used a pane with no row, where the drop early-returns and both sides read 0. Stop keying the guard on a counter a sibling teardown path can reset: the ordinal is now stamped on the row itself, derived from the row it replaces, so there is no side table to clear and a batched burst lands the same ordinals as the equivalent sequential writes. A removed row means "nothing reported", which is exactly what the paired drop leaves behind. Also: - Keep the `providerSessionOnly` resume identity that the paired dismissal mints when the shell outlived the agent; a certified PTY exit still takes it, since there is no pane left to resume into. - De-vacuum two guard tests. The confirmed-teammate pin never anchored a session owner, so the void it claimed to survive never ran; the unavailable-inspection pin asserted before the confirm ladder settled. Both now fail when their guard is removed. - Derive `hasLiveClaimsForPaneKey` from a predicate that lives beside `clearPaneCacheState`, so a new latch cannot be added to the teardown and silently missed by the claim check. - Drop the unreachable compact-`trigger` clauses; SessionStart is the whole guard. - Cover the connectionId arm of the exit certificate, where a provider-observed death and a preserved SSH surface are deliberately independent. * fix(agent-status): keep agent-status-types under its line cap main already sits exactly at the 300-line max-lines cap for this file, so the single `acceptedStatusSeq` field this branch adds pushed it to 301 once main's observation facet merged in. Declared the field as a mixin beside the observation facet instead. Both are per-write facets mixed into `AgentStatusEntry` rather than fields a reporter supplies, so they belong together — and the capped file loses a line rather than gaining one, since it already imports from that module. No lint suppression. * fix(agent-status): collapse the entry facets into one intersection The previous attempt still tripped max-lines: two mixins on one intersection wrap across two lines under oxfmt, so removing the field line bought nothing. Expose a single AgentStatusRowFacets that already includes the observation facet, so the entry intersects one short name on one line. The payload keeps intersecting the observation facet alone — it must not carry the renderer-local ordinal. Verified by formatting first and then linting, which is the order that catches this. * fix(agent-status): retire resume authority with dead panes |
||
|
|
8d08d0078d |
fix(codex): stop an unreadable legacy hooks.json from clearing managed trust (#16147)
`cleanupLegacySystemManagedHooks` reads `~/.codex/hooks.json` and, when it finds
no hooks, removes Orca's managed trust entries from the system config.toml and
deletes that home's grant-ledger record.
Since the STA-4823 read classification landed, `readHooksJsonWithRaw` reports a
genuine absence as `{ raw: null, config: {} }` and a failed read as
`{ raw: null, config: null }`. Both still fell into the same branch, so a read
that merely failed discarded hook approvals the user had already given — and the
ledger record that would have let a later pass notice.
Only the definitive-absence answer may reach the removal now.
Found while reviewing #15417 and not covered by it: that PR fixed the classifier
and this is a consumer of it that still collapsed the two answers.
The sibling `assertHooksJsonGeneration` guard in codex-real-home-hook-install.ts
has the same `existsSync ? read : null` shape and is deliberately NOT changed
here. Measured, a permission denial leaves existsSync true and throws from the
read, so that path already fails closed; a guard there could not be made to fail
in a test and would be unprovable code.
|
||
|
|
a70fe63993 |
fix(workspace-cleanup): read git for a pinned workspace before deleting it (#15370)
`shouldReadWorkspaceCleanupGitEvidence` refused to read git whenever `blockers` included 'pinned' -- in a clause `forceGitCheck` could not override. So the confirm-time forced read, which exists precisely to decide whether a removal needs force, never ran for pinned rows. This is reachable today, not only after the planned verdict removal. 'pinned' is not a queue blocker (queue blockers are main-worktree, folder-repo, dismissed), so a pinned idle workspace is hand-selectable from the row checkbox right now. Its git evidence stays `clean: null, checkedAt: null`, and `shouldForceWorkspaceCleanupRemoval` returns true whenever git is unknown -- so it force-deletes with no evidence ever obtained. Moved 'pinned' into the cost-skip clause so `forceGitCheck` overrides it: broad scans still skip it, targeted preflights do not. main-worktree and folder-repo stay unconditional because both are refused before removal, so reading git for them is pure cost. Broad-scan performance is unchanged, and there is now a test pinning that: it passes before and after. The preflight test fails against the unfixed source. |
||
|
|
7b9529da22 |
Add keyboard shortcut for workspace deletion (#16271)
* Add keyboard shortcut for workspace deletion Default Mod+Shift+Backspace (⌘⇧⌫ on Mac) lets users delete the hovered worktree or folder workspace immediately. The shortcut targets the sidebar hover state rather than requiring focus, and avoids terminal pane D-based split shortcuts on all platforms. Co-authored-by: Brennan Benson <brennankbenson@gmail.com> * Omit delete shortcut from disabled Delete Worktree for primary checkout - Remove shortcut badge from the disabled "Delete Worktree" action when it cannot be executed - Only show shortcut in multi-context delete actions where the command is available - Extract host identity parsing into reusable helper function to prevent inline string manipulation - Fix folder workspace deletion to use correct host-qualified identity comparison * Document host extraction safety for destructive worktree ops Unqualified identities must stay undefined rather than defaulting to 'local'. Destructive operations depend on correct host identification. Added tests and JSDoc to clarify this safety-critical behavior. * fix test --------- Co-authored-by: Brennan Benson <brennankbenson@gmail.com> |
||
|
|
c618ec7393 | test(reliability): protect recent P0 regression invariants (#16163) | ||
|
|
d9aa09cc8c |
fix(runtime): retire terminal snapshot probes before waiter timeout (#16149)
* refactor(renderer): use terminal paste sanitizer directly * fix(runtime): order visible probe timeout settlement * fix(runtime): skip unorderable visible probes |
||
|
|
6989c92b16 | fix(skills): recover upload admission lifecycle (#16057) | ||
|
|
0753f0a8dc | fix(crash-reporting): stamp React #185 boundary attribution as unreliable (#16153) | ||
|
|
0926c25854 | refactor: reuse canonical regex escaping (#16150) | ||
|
|
a7a4f5d987 | style(secrets): trim backend probe comments (#16136) | ||
|
|
ab3b1d07cd |
reland(opencode): session continuity without the command-finished deferral (STA-4557) (#15350)
* reland(opencode): session continuity without the command-finished deferral (STA-4557) Relands #14866 (reverted in #14943) minus its `orca-runtime.ts` change, which is what caused the revert. ## Why the original runtime change was wrong `retirePtyAgentLaunchAuthorityAfterCommandFinished` deferred launch-authority retirement behind an async foreground read, on the premise that OpenCode emits `command-finished` while still in the foreground. Raw PTY capture disproves it: OpenCode emits no OSC 133 of its own, and Orca's shell wrappers emit exactly one `133;D` per pane — at OpenCode's exit — under both zsh and bash. The event being deferred past only ever fires at exit, which is exactly when authority should be retired. Both call sites stay on the synchronous `retirePtyAgentLaunchAuthority`. ## Why the deferral was unsafe `confirmPtyAgentExit` uses the same async-foreground pattern four lines away, but its early return means "don't record an exit" — conservative. The deferral copied that shape into a site where the early return means "don't revoke a secret". Same code, inverted consequence: every guard failed open, so a stale or racing read silently kept a finished session's authority alive, and the pane's persisted `launchTokenHash` was never scrubbed — so it rehydrated as `restored` authority after an app restart. ## Why the deferral's guards could not have worked `ORCA_AGENT_LAUNCH_TOKEN` lives in the PTY environment, so every process started in that shell inherits it — both sessions in a reused pane post the same token. A pane-lifetime bearer secret cannot be a session identity baseline, by construction, and `incarnationId` tracks the PTY, not the agent. The only field that separates sessions is the provider `sessionID`. ## What lands - Status/session-boundary work from #14866: opencode emits `SessionStart` for root sessions (mimo-code does not), launch-token fencing, and `SessionStart` as an opencode turn boundary. - The two `server.ts` fixes from #14941: re-fence a still-authorized pane on a tokened `SessionStart`, and restore mimo-code's explicit-prompt restart boundary (mimo emits no `SessionStart`, so opencode-only stranded its panes). #14941's re-poll hunk is dropped along with the code it patched. - Five regression tests in `opencode-finished-session-authority.test.ts`. They pass here and all five go red if the deferral is re-added. * chore: drop incidental reformatting of files unrelated to this PR |
||
|
|
9814813b2e | refactor(skills): centralize observation matching (#16134) | ||
|
|
7a72f341f7 |
Split pty-connection.ts into focused modules under 400 lines (#15166)
* refactor: split pty-connection.ts under 400 lines * rm design doc * refactor(pty-connection): extract reattach payload handlers as factories - Replace bindApplyReattachPayload with createReattachPayloadHandlers factory that returns handlers instead of mutating session directly, enabling better composability and testing - Extract waitForUserInitiatedSshConnect as standalone function for reuse across deferred session attach flows - Create ReattachPayloadSession type to document and isolate required session capabilities - Add test coverage for overlapping reattach payload attempts - Clean up comments to remove redundant prefixes (session.pane → pane, session.transport → transport) * fix(pty-connection): correct sequencing and state bugs in spawn and reat - Fix terminal tail slice to take prefix instead of suffix, preserving escape sequence markers needed by next scan - Clear pending pane serializer when direct SSH retry PTY is unclaimed - Initialize interrupt status baseline to undefined so first input advances sequence counter - Bump reattach generation only after confirming current attempt owns the stream, preventing superseded results from canceling in-flight prepaint * fix(pty-connection): correct sequencing and state bugs in spawn and reat - Fix terminal tail slice to take prefix instead of suffix, preserving escape sequence markers needed by next scan - Clear pending pane serializer when direct SSH retry PTY is unclaimed - Initialize interrupt status baseline to undefined so first input advances sequence counter - Bump reattach generation only after confirming current attempt owns the stream, preventing superseded results from canceling in-flight prepaint * fix(test): increase poll iterations to prevent Node 26 test leakage Increase event loop turns from 40 to 200 in the timer settlement loop. Node 26's libuv poll phase can briefly starve when concurrent workers transform tests, causing cleanup to leak into the next test. The higher iteration count ensures async operations complete before returning. * fix(foreground-output-budgets): use >= for budget window boundary check At the exact window boundary, the budget should roll over. Change the comparison from > to >= so the window resets when now equals windowStart + FOREGROUND_BUDGET_WINDOW_MS, not just after. Add tests to verify budget rejection and rollover behavior. * refactor(pty-connection): add status observations and routing improvemen - Track agent status observations with origin and transition metadata - Separate interactive redraw input timing from general terminal input - Restore pane authority on bind and reattach - Refine routing trust and confirmation state handling - Invoke queued startup callbacks when PTY is bound - Resolve Windows shell overrides with user settings * refactor: extract resolveLaunchAgentCandidate helper Consolidate duplicated launch-agent resolution logic into a shared helper to prevent future divergence between paneExpectsLaunchAgent and resolveExpectedLaunchTuiAgent. * refactor(pty-connection): use model snapshot for direct SSH reconnects Direct SSH reconnects now restore from the full SSH model snapshot (complete scrollback) when dimensions are compatible, instead of the bounded relay tail. Falls back gracefully when incompatible or alternate-screen was exited. * refactor(pty): retry unverifiable SSH reattaches via preserved bindings Preserve deferred SSH session IDs longer when they serve as the only retry binding, allowing the system to attempt recovery through direct SSH retries or PTY remounts when reattach fails in an unverifiable way. Simplify reconnect model restoration by removing the conditional model snapshot probe and using relay replay directly. * test: poll terminal readiness in expectSingleOwningPty Retry the terminal list assertion with polling to account for timing delays in PTY state reporting from the runtime. |
||
|
|
677718c4a5 |
fix(codex): stop rebuilding shared Codex state from a read that failed (STA-4823) (#15417)
* fix(codex): stop rebuilding shared Codex state from a read that failed (STA-4823) Six shared files were rebuilt, erased or reported healthy after a read that had only failed. Batch A of the STA-4606 split: every one of these is reachable and testable on the host lane, so none of them wait on the WSL work. - `config-toml-trust.ts` upsertHookTrustEntries: `existsSync` reported a locked config.toml as absent, so the base content became '' and the upsert rewrote the file from the trust entries alone — a trust-only stub, with the user's model, provider, MCP servers, approvals and comments gone. It refuses now; every hook-service caller already turns that into "trust entries could not be written. Run /hooks in Codex to approve." - `codex-trust-grant-ledger.ts`: an unreadable ledger degraded to empty and the next write persisted a file holding only the home being written, dropping every other home's grants. The write paths refuse; the read path still degrades, and a corrupt ledger is still rebuilt. - `codex-pane-account-registry.ts`: an unreadable registry erased every pane's attribution AND cached that erasure, so it survived the file recovering. The failure is no longer cached, and both write sites refuse rather than persist a registry derived from an empty stand-in. - `hooks-json-read.ts`: the read arm already separated "no hooks" from "could not read", but the `existsSync` arm in front of it returned a valid empty config for a file that could not be opened. One read now classifies both. - `config-settings-baseline.ts`: absent, unparseable and unreadable all collapsed into `null`, so the snapshot rebuilt a baseline it could not read — recording an in-Codex edit as Orca's own write, after which promotion skips it forever. - `config-sync-stall.ts`: an unreadable runtime config read as absent and the status reported `synced` while the mirror was refusing. It reports `managed-home-unavailable`, the existing reason for exactly this, rather than borrowing a source-side one and blaming the wrong path. Absent and malformed still rebuild throughout — resetting corrupt state is the intent, and conflating it with unreadable would wedge a user on a broken file. * fix(codex): close shared state read-denial gaps * fix(codex): recover oversized settings baselines * fix(codex): name the stalled managed config * fix(codex): preserve hooks after failed source reads * test(codex): correct what the denyExistence rig actually models MEASURED on both platforms: a file-permission denial leaves existsSync TRUE and fails only the content read — chmod 000 gives EACCES on macOS, icacls /deny (R) gives EPERM errno -4048 on Windows, with stat/lstat succeeding in both. The rig's docblock claimed this mode modelled that denial. It does not. What it models is the UNC / \\wsl$ transport, where an unreachable distro reports errno UNKNOWN at every level and existsSync folds it to false. The distinction decides what the D29 guard is worth: under a permission denial the pre-fix code already failed safe, because existsSync was true so it took the read branch and threw. Only a transport that lies about existence reaches the rebuild-from-empty path. No behaviour change; the comment was wrong, not the code. * test(codex): exercise live baseline read denial * fix(codex): retry pane attribution writes * fix(codex): retry reconciliation registry writes * fix(codex): report unreadable sync baselines |
||
|
|
d1127b9939 |
fix(agent-hooks): revive a retired pane on each provider's own new-turn event (#15344)
* fix(agent-hooks): revive a retired pane on each provider's own new-turn event The un-retirement gate matched two raw event-name literals, UserPromptSubmit and SessionStart. Only 5 of 18 hook sources name their turn boundary that way, so for the rest a reused pane stayed rowless forever: the user starts a new turn and no row ever appears in the sidebar or dashboard. Measured, not estimated — 10 providers fail the new test on main: gemini (BeforeAgent), antigravity (PreInvocation), amp (agent.start), cursor (beforeSubmitPrompt), pi/omp/prime-agent (before_agent_start), grok (user_prompt_submit), copilot (sessionStart, seen raw), hermes (pre_llm_call). The gate was also wrong in the other direction: opencode has no turn boundary at all, yet a literal SessionStart revived its pane. Both directions are covered. The correct per-provider classifier, isNewTurnEvent, was already imported into this file and already used 160 lines below — #14706 added that call specifically so consumers would stop re-deriving boundaries from literals, and left this one. Keeps the literal check when the remote envelope omits source: that field is optional, an older relay does not send it, and requiring it would have left every source-less remote pane retired forever. * fix(agent-hooks): tell an absent source apart from an unrecognized one Review of the first commit found that `isAgentHookSource(envelope.source) ? envelope.source : undefined` collapses two different wire conditions into one: an older relay that omits `source`, and a NEWER host relaying a provider this build does not know. They need opposite answers. Case two is the normal upgrade order — hosts and clients update independently — and its boundary event will not be UserPromptSubmit/SessionStart, because 13 of the 18 providers we already ship are named something else. So the legacy-literal fallback stranded that provider's panes permanently: the exact defect this branch fixes, silently reintroduced for traffic our own wire doctrine calls normal. Pass the raw wire value so the gate can distinguish them, and fail OPEN on an unrecognized provider. The costs are asymmetric: a stranded pane is invisible, permanent, and has no user-facing recovery, while a spurious revive produces a row that decays after AGENT_STATUS_STALE_AFTER_MS. Also from review: - Cover the source-less branches, which had no coverage at all — including a test pinning what the legacy shim CANNOT do, so nobody later widens the literal list to "fix" it. - Use the source as agentType rather than always 'claude'; 15 of 16 rows were describing a state that cannot occur. - Reword the opencode comment: no plugin in that family emits these literals, so this closes a hole rather than removing behavior. Checked and not changed: the local HTTP path 404s an unresolvable source, so the fallback is unreachable there and masks nothing. * test(agent-hooks): stop pinning opencode's fence behavior on a synthetic event The negative case asserted that an opencode pane stays retired after a literal SessionStart. That rests on a false premise: origin/main's opencode plugin never emits SessionStart at all — zero occurrences in opencode/hook-service.ts, and the shared family source is what mimo-code uses too. So the assertion pinned an event no plugin sends, and it would have become actively wrong the moment a pending change gives opencode a real SessionStart, silently re-breaking the rowless reused-pane case that change exists to fix. Assert against mimo-code and command-code instead. Both genuinely have no boundary event in any planned state, so the case tests what it claims to. * fix(agent-hooks): reject malformed relay sources at retired fence Only a non-empty unknown string can identify a future provider. Keep null, blank, numeric, and object source values behind the retired-pane fence instead of treating malformed wire data as a new turn. |
||
|
|
e50cc309c3 |
fix(runtime): prevent restored workers from appearing idle while busy (#15998)
* fix(runtime): classify tui-idle from the visible screen only The adopted-PTY tui-idle probe added in #15569 read the provider snapshot as `scrollbackAnsi + data`, and the Codex readiness classifier matches the startup banner. For a daemon-hosted adopted worker — where the retained tail stays empty forever — every wait re-probed and could resolve `satisfied: true` off banner history while Codex was actively working, turning a loud timeout into a silent false ready. - probe now requests and parses the visible grid, never scrollback - retirement of a timed-out provider acquisition is checked before the re-acquire branch, so a wider row request can no longer resurrect a hung provider - probe builds its result before clearing the poll interval, so a stale handle cannot leave the waiter with neither poll nor probe Fixture follow-ups from the same review: - resume legs pin the captured `launchConfig.agentCommand` to the fake instead of bare `codex`, which resolved the machine's real Codex off PATH - the command override is quoted for the Windows shell the runtime will actually use, and specs pin that shell alongside the override - fake agents acknowledge a bare submit after a short grace, so an unbracketed delivery path fails with a diagnosable ACK instead of a suite timeout Refs STA-4907, STA-4885 * test: assert tui-idle probes serialize visible grid only - Verify idle timeout probes exclude scrollback from serialization - Add test case for Git Bash shell path quoting with apostrophes - Simplify verbose test helper comments * test: improve fake agent paste protocol validation Refactor paste end detection to properly track both begin and end markers, validate bracketed paste protocol (RFC 2544) through chronological event sequencing, and emit correct error messages for protocol violations. This ensures reliable detection of when pastes complete even when delivered across multiple chunks, and correctly distinguishes between bracketed and unbracketed paste modes. * fix(runtime): reject provider snapshots when live output advances Provider snapshots become stale when live output is received after the snapshot is requested. Reject snapshots where the current output sequence exceeds the snapshot sequence, preventing callers from consuming outdated terminal state. Add tests verifying stale frame rejection. |
||
|
|
c3a1694b1d |
perf(preflight): read the WSL mount table once, and make launch agree with detection (#16053)
* perf(preflight): read the WSL mount table once per shell, not once per CLI
The prelude is embedded in the lookup script, and the caller wraps that in
`for cmd in <every agent>`, so the unconditional assignment forked awk once per
probed CLI -- 36 of them inside the distro against a 10s detection budget. The
comment claimed it was read once outside the loop; it was not.
`${x+set}` rather than `[ -n ... ]`: a host with no Windows mounts yields the
empty string, which must still count as read.
Pinned by counting real awk forks through /bin/sh with a stub on PATH, because
nothing covered this expression at all -- a wrong-field mutation shipped green.
Verified to bind: the unconditional form counts 4 for 4 commands.
* fix(wsl): make launch resolve the same binary detection reported
Agent detection skips Windows mounts during the PATH walk; the Codex WSL
command builder and the WSL branch of isCommandOnPath did not. So Orca could
report the guest codex as installed and then launch the Windows one sitting
ahead of it on PATH, or disagree with itself between preflight and detection
about the same distro.
Both now pass the same option.
Verified on a real Windows host against a real WSL2 distro, with a Windows
binary planted ahead of a guest one on PATH:
plain `command -v orcaprobe` -> /mnt/c/Users/neil/orca-agree/orcaprobe
this lookup -> /home/neil/.orca-agree/bin/orcaprobe
That host reports /mnt/c as 9p, which the mount expression matches, so the
fstype list is confirmed against hardware rather than fixtures.
* test(preflight): prove the memoised mount list applies past the first command
Counting awk forks with a stub that reports no mounts cannot see what the
hoist trades correctness for. A mutant that empties `_orca_win_mounts` inside
the walk keeps the fork count at 1 and keeps every existing test green, while
every agent after the first stops skipping /mnt.
This runs two commands behind a stubbed Windows mount and asserts both resolve
to the guest binary. Verified against that exact mutant.
Credit: review counsel.
|
||
|
|
e26f849682 |
fix(wsl): budget the whole command line, not just the script (#16032)
* fix(wsl): budget the whole command line, not just the script The argv/stdin threshold measured `script.length`, but the cap applies to the finished command line -- which also carries `PATH=<login PATH>` and `HOME=`. A login PATH is itself a few KB. That produced a perverse band: with a long enough PATH, a 7,999-char hook was placed on argv and CreateProcess refused it, while the SAME hook at 8,001 chars flipped to stdin and ran. Size decided how a hook behaved, in the wrong direction, and the failure looked like "your setup hook failed" with nothing pointing at length. Now the argv form is built, measured, and only used if the whole line fits; otherwise the script goes to stdin as before. The count over-estimates slightly (it charges quoting for every argument) because over-counting is the safe direction for a cap. The regression test uses a 7,000-char script -- deliberately under any script-only threshold -- with a 27KB PATH, and asserts it lands on stdin. My first attempt used 7,999 + `echo `, which is 8,004 and flipped under the old rule too, so it passed either way and proved nothing. Credit: Grok. * fix(wsl): charge quoting and measure the line that is actually spawned Two under-counts the review found in the estimator I added. The doc comment claimed it over-counts. It did not: libuv escapes every `"` and doubles a backslash run before a quote, so a quote-dense script costs more than its length. And `wsl.exe` plus `-d <distro> --exec` are prepended AFTER the measurement, so ~45 characters of the budget were never counted. Together those put a quote-heavy ~26KB script on argv and over the real 32767 ceiling -- where the old script-only rule would have sent it to stdin and it would have run. A narrower band than the one this PR removes, but the same shape of bug, so worth closing before merge rather than after. Now charges one character per `"` or backslash and measures the full spawn line. New test: 26,000 quote characters must land on stdin; verified to fail with the quoting charge removed. |
||
|
|
92315c4178 |
fix(preflight): do not count a Windows binary reached through interop as a WSL install (#16028)
* fix(preflight): do not count a Windows binary reached through interop as a WSL install WSL appends the Windows PATH to the guest PATH by default, so on a distro with no guest `claude`, `command -v claude` resolves to `/mnt/c/Users/me/.../claude.exe`. That path is POSIX-absolute, so the existing absolute-path check accepted it and preflight reported the agent as installed in the distro. That is worse than reporting it absent. Absent tells the user to install it; a false positive launches a Windows executable inside a Linux session, where it sees Windows paths, no guest $HOME and none of the distro's config -- and the failure surfaces later, somewhere less obvious. Rejects `/mnt/<drive>/` and any `.exe`, case-insensitively. A genuine guest install is unaffected. * fix(preflight): skip Windows mounts during the PATH walk, not after it The review caught this and it is the more important half of the fix. Rejecting the interop path in TypeScript happens after the guest walk has already stopped on it: the lookup breaks at the first executable, and the version-manager fallback dirs are APPENDED, so they sit behind the Windows entries WSL appends. A user with claude in nvm AND on the Windows PATH therefore went from a false positive to "not installed" -- the exact #9725 population the fallback dirs exist to serve. Worse than the bug being fixed. The lookup now takes `skipWindowsMountDirs` and skips those PATH components mid-walk, so the guest binary behind the shadow is still found. Matched by mount metadata from /proc/mounts (drvfs/9p/virtiofs), not by a `/mnt` name: the automount root is configurable, and `/mnt` is an ordinary directory on a Linux box. That also closes the custom-root hole the reviewers found in the name-based predicate. The TypeScript check stays as a secondary net for a mount the guest does not report, with a comment saying why it must never be the thing that decides. Proven with a real /bin/sh: a Windows `claude` ahead of an nvm `claude` on PATH now resolves to the nvm one. Credit: review counsel, and community PR #12794 (spfcraze), which proposed this shape first. * fix(preflight): let the mount table be the only word on what is a Windows path The name-based check could veto a path the walk had deliberately kept. /mnt/d is a perfectly ordinary Linux mount, so a guest binary there was resolved correctly by the walk and then discarded by its name -- the #9725 false negative, reintroduced by the belt-and-braces net I added "just in case". And if awk were missing, the name rule became the only rule, which is precisely the failure it was supposed to backstop. The walk skips components the guest itself reports as drvfs/9p/virtiofs. That is authoritative. Without a mount table we now degrade to main's behaviour (the old false positive) rather than inventing a new false negative. Net: one predicate, three fixtures and an import deleted. |
||
|
|
7a739c6bf5 |
fix(secrets): probe the Linux-only storage backend defensively (#16046)
`safeStorage.getSelectedStorageBackend` is `@platform linux`, so it is genuinely undefined on macOS and Windows — confirmed against the installed Electron 43, where it reads `undefined` on darwin and `function` on Linux. The shipped code called it behind a `process.platform === 'linux'` check, so it never threw, but the guard was the only thing standing between that call and a startup TypeError. The platform check now lives with the probe, alongside a typeof check and a try/catch, and an unreadable or unknown backend reports no gap — claiming one we cannot prove would be its own kind of lie. The gap this closes is in the tests, not just the code: every suite here mocks safeStorage with the method present, so the suite could stay green while the shipped app threw. The new case deletes the member from the live mock rather than re-mocking, because the module already holds that object and a later vi.doMock is inert — the first version of this test passed against the unguarded code, which is the failure mode it exists to catch. Verified against the expression currently on main: three cases fail. |
||
|
|
1375c57b16 |
fix(secrets): report the protection gap on change, not on every launch (#16044)
The gap warning fired every startup with no way to stop it. It usually needs a keyring installed and unlocked to fix, so repeating it every launch is nagging the user cannot act on and will learn to ignore. It now reports when the answer changes: once when the gap starts being true, again if it becomes true for a different reason, and once when it is fixed — because silence after a "your secrets are not protected" warning would leave the user assuming that is still the case. State lives beside the profile data file, which is why the call moved out of the port bootstrap: that state has nowhere to live until the profile exists. A corrupt state file re-reports rather than trusting it, and a failed write logs instead of failing startup, since re-reporting next launch is the safe direction. ORCA_ALWAYS_REPORT_SECRET_PROTECTION=1 forces a re-report for support without disturbing the stored state. |
||
|
|
202d74a8a4 |
fix(git): enable Windows long paths for worktree creation (local, sparse, and SSH hosts) (#15866)
Co-authored-by: hwantage <hwantagexsw2@gmail.com> |
||
|
|
af2e825626 |
fix(worktree): stop warning about a stale local base branch that does not exist yet (#15331) (#15871)
Co-authored-by: vam <a@a.com> |
||
|
|
8d1557c225 |
fix(skills): discover Hermes home skills (#15862)
Co-authored-by: kriptoburak <kriptoburak@users.noreply.github.com> |
||
|
|
838f5bfb75 |
fix(secrets): tell Linux users when their secrets are only obfuscated (#16033)
On Linux with no keyring, Electron falls back to the `basic_text` backend, which "encrypts" with a hardcoded password. `isEncryptionAvailable()` returns true for it, so Orca reported those secrets as sealed. They are not. The obvious fix — returning false for basic_text — is wrong and would have been a credential regression: `decryptWithStatus()` skips decryption entirely when encryption is unavailable, so every already-stored secret would read back empty. Sealing genuinely works on basic_text and must keep working. So capability and trust are now separate questions. `isEncryptionAvailable()` still answers "can this host seal and unseal", and `describeProtectionGap()` (renamed from `describeUnavailable`) answers "is my data actually protected", covering both no-sealing and weak-sealing. That method had no production caller — the port documented a promise nothing kept. `reportSecretProtectionGap()` now reads it at startup. A user-visible surface is follow-up; this at least stops the silence. Adds a bootstrap wiring guard over all nine host port installs. The no-op defaults are correct for a renderer-less host and silently wrong for the desktop, and a dropped or reordered install fails no existing test. Verified in both directions: it fails when an install is removed, and when one moves after the runtime is constructed. |
||
|
|
36dfcc3955 |
fix(terminal): arm the restore baseline only on a snapshot that painted content (#16009)
The restored-snapshot baseline permanently drops every delivery chunk at or below the snapshot's seq, on the model's claim that those bytes are already painted. reconcileChunkAgainstRestoredSnapshot recovers when the baseline under-reports (a gap ahead re-restores; a rawLength mismatch re-restores) but has no path for a baseline that over-reports: those chunks return drop-duplicate forever, and an idle shell never re-sends them. Gate arming on whether the snapshot painted printable cells. A snapshot claiming seq > 0 while painting nothing cannot be the rendering of the output it claims to cover, so the claim is disproven and the redelivery is the only remaining copy. Also pins disposeHeadlessTerminal's two-part write ordering, which was previously unpinned and silently reversible. Refs STA-5179 |
||
|
|
03fcfdfb92 |
feat(orcad): boot the Orca runtime on plain Node (#15968)
* refactor(host): resolve the app root through the port in fork-reachable modules
`parcel-watcher-entry-path.ts` and `session-scanner-service-entry-path.ts` read the
app root via `require('electron').app` inside a try/catch that already returns null
when Electron is absent. They were therefore correct under plain Node at runtime and
only failed the *static* text check — which is real, not pedantic: the comment in
`ports/port-scan-command-client.ts:19` records that the plain-node-entry-guard fails
on that literal text, try/catch or not.
`hasAppEnvironment() ? getAppEnvironment() : null` gives the identical "no app root
here" answer without the text. That restores `hasAppEnvironment`, which an earlier
commit in this stack deleted as unused — it now has the caller it was waiting for.
Ratchet baseline 27 → 25.
Verified: 74 files / 458 tests; `pnpm typecheck` clean; `oxlint` clean.
* feat(orcad): boot the Orca runtime on plain Node
Closes the last two Electron couplings and makes `orcad` a working artifact:
a 4.43 MB Node bundle that boots, pairs, registers a repo, creates a real git
worktree and round-trips a PTY — with zero `require("electron")`.
Ratchet 2 -> 0, so `config/runtime-electron-baseline.txt` is now empty and its
test asserts exactly that: any reachable electron import is a regression.
- speech: inject the service factories, so importing ModelManager for its type
no longer drags Electron's streaming net.request into the graph
- filesystem-watcher: add a WorktreeWatcherRemoval port. Every entry in those
maps arrives through an ipcMain handler carrying a renderer sender, so a host
with no renderer has nothing to close, restore or forget — the inert default
is what the desktop code does against empty maps, not a stub hiding work
- user-data-path / profile-storage-paths: resolve userData through
AppEnvironment. These surfaced only once orcad pulled the store in
Both host ports now anchor to a realm-global symbol. `vi.resetModules()` gives
the re-imported graph a fresh module copy, so a binding installed before the
reset silently read back as uninstalled.
The acceptance smoke drives both hosts through one code path (`--target
orcad|electron`) and seeds its own git repo, so it is hermetic and asserts the
same contract of each. Wired into PR CI.
* test(smoke): remove the seeded workspace container, not just the worktree
* test(smoke): surface the server's stderr when it dies before ready
* fix(smoke): build node-pty for Node before booting orcad in CI
* fix(smoke): drive the CLI built from this checkout, not one on PATH
* docs(ratchet): say the baseline must stay empty, not merely shrink
* build(orcad): externalize only the native modules actually in the graph
|