Commit Graph
10285 Commits
Author SHA1 Message Date
Merge Sim 4108e951e5 Merge commit '6ade5578596e4a1d173f7da6763e35ae9e0e9950' into brennanb2025/claude-sdk-ready-mainsync 2026-09-03 17:42:38 -07:00
Merge Sim 6ade557859 Stop treating an unanswerable create-support probe as a refusal
A worktree is not resolvable for a beat after createWorktree resolves, so a
probe fired immediately after creation fails the RPC with selector_not_found
instead of answering. The catch collapsed that into `supported = false`, so the
composer refused and quietly built a terminal session — the gate never said no,
it was never asked successfully. Elapsed time was the only input that decided
whether a Claude launch went structured.

"Could not answer" and "answered no" are different states and only the second
is a verdict. Retry while the host cannot yet resolve the selector, with a
bounded backoff that covers the measured window with margin, and keep refusing
on the first ask for everything else. Fail-closed is unchanged: a probe that
still cannot be answered when the budget is spent refuses.

The retry is narrowed with the shared error-code matcher, which classifies a
token that transports re-wrap into a longer message without matching prose that
merely mentions it.

Codex never probes, so this race has never been able to refuse a Codex launch —
the race itself is identical for it. Recorded at the early return, because
whoever gives Codex a probe inherits the bug.
2026-09-03 17:41:12 -07:00
Merge Sim c75a3f3ab5 Merge commit 'a3be41978d' into brennanb2025/claude-sdk-ready-mainsync 2026-09-03 17:25:30 -07:00
Merge Sim eea957b097 Merge commit 'd7e2c58549' into brennanb2025/claude-sdk-ready-mainsync 2026-09-03 17:25:30 -07:00
Merge Sim a3be41978d Support structured Claude when accounts are registered but none is selected
Registered-but-deselected Claude accounts were refused, which is behaviourally
identical to having no accounts at all: the auth policy does not strip, ambient
auth is the truth, and the UI names no host identity. A user who deselected
their accounts silently got legacy chat with nothing explaining why.

Nothing selected for the host runtime is two states the settings cannot tell
apart after the fact, because pruneInvalidClaudeRuntimeSelection empties the
host slot and persists null in the second one:

  honest deselection      -> ambient auth, UI names nothing   -> SUPPORTED
  the WSL-only steady state -> ambient auth, UI names the WSL account -> REFUSED

The presence of any WSL-bound account in the list decides. Simplifying this to
"none active -> supported" re-opens the auth-identity misrepresentation, so the
tests fail loudly on exactly that: five of them, across the unit rule and the
createSupport path.
2026-09-03 17:23:15 -07:00
Merge Sim 3b38af0c21 Treat an absent managed-account list as empty, not as unreadable
An empty claudeManagedAccounts array and a missing one are the same answer:
this user has no managed Claude accounts, so nothing claims an identity and
ambient auth is the truth. Refusing on absence strands any profile that simply
never wrote the key, and it disagrees with the auth policy, whose own predicate
takes `(accounts ?? [])` for exactly this reason.

Only settings that cannot be READ stay unknown, and those still refuse — as do
a WSL-bound active account and a selection naming an account the list does not
explain.

The earlier reasoning treated a missing field as settings we failed to parse.
That conflated "not present" with "not readable"; only the second is unknown.
2026-09-03 17:18:20 -07:00
Merge Sim d7e2c58549 fix(claude): let a re-walked descendant become eligible for the forced sweep
A descendant first observed by a capture inside its own birth second could never
be SIGKILLed: `ps lstart` is second-resolution, so that capture cannot rule out
a pid recycled later in the same second, and the merge pinned each retained row
to the boundary of the walk that first saw it. SIGTERM-resistant children forked
in that window were signalled and then never escalated -- they survived close,
quit and restart, reparented to init, and had to be killed by hand.

Advancing that boundary on any later capture would be unsound: a later capture
matching pid, pgid and start-second is exactly what an impostor would also show.
But a capture is not a match -- it is a fresh ppid walk from a root Node pins
through its own handle, so a row it re-derives is proved ours at that instant
without appealing to its start time. Chain the fence from there instead, and
take that walk at the close boundary while the root certainly still lives: the
root may leave inside the grace window, and the post-timeout refresh never runs.

A row absent from the later walk still keeps its earlier boundary, and a row no
walk has ever re-derived in a later second is still never escalated.
2026-09-03 17:10:43 -07:00
Merge Sim d4bc9448a9 fix(claude): keep command queue bookkeeping out of the transcript
Claude Code 2.1.258 emits a `command_lifecycle` frame for every uuid-stamped
command it starts, completes or cancels. The frame carries a command uuid and a
state and no content, and the CLI keeps it out of its own transcript -- but it
is absent from the SDK's SDKMessage union and so from Orca's frame catalogue,
where an uncatalogued kind defaults to a substantive row. Every structured turn
therefore painted raw JSON rows into the user-visible transcript.

Catalogue it and disposition it as status chrome. The unknown-kind default stays
`timeline-substantive`: a kind we have never seen is likelier to carry content
than to be chrome, and a visible row we can catalogue later beats content we
silently dropped. A lifecycle state that reads as a failure still surfaces,
because the payload error check in `classifyProviderFrame` outranks the
catalogue.
2026-09-03 16:31:54 -07:00
Merge Sim 33f2293467 Merge commit '8cd63706e245f1450b5c5b31afb28f7fd75e1d2a' into brennanb2025/claude-sdk-ready-mainsync 2026-09-03 15:28:56 -07:00
Merge Sim 8cd63706e2 Pin the absent-vs-empty distinction in the managed-account gate
An empty claudeManagedAccounts array is a real answer: the user has no managed
accounts, nothing claims an identity, and the ambient path is legitimate. A
readable settings object with no such field is settings we failed to parse —
the same unknown as unreadable — so it refuses.

The two are one character apart in the code and the difference is invisible
without the reasoning, so record it at the branch and pin both sides. The test
fails under the obvious "consistency fix" of treating a missing field as empty.
2026-09-03 15:21:34 -07:00
Merge Sim 8a897b9911 Merge commit '6b038e114bd90f8e863ecdde56cb550dc50839a7' into brennanb2025/claude-sdk-ready-mainsync
# Conflicts:
#	src/main/runtime/structured-agent-session-runtime.ts
2026-09-03 15:20:27 -07:00
Merge Sim 585bf17a1f Derive the gate test's auth policy from the settings under test
A hardcoded stripAuthEnv asserts a gate/policy pairing production cannot
produce, and false additionally lets launch.env inherit the runner's real
process.env. Derive via claudeStructuredAuthPolicyForSettings instead: the
gate settings type is the same Pick the policy takes, and both resolve the
account through getSelectedClaudeAccountIdForTarget.
2026-09-03 15:18:32 -07:00
Merge Sim 6b038e114b Move the structured Claude gate out of the @ts-nocheck runtime files
Both call sites of the managed-account gate sat in files whose first line is
`// @ts-nocheck`, so neither was typechecked: three arguments to a one-argument
function plus an undeclared identifier compiled clean. New auth-identity
decision logic had no compiler behind it.

Move the verdict into a checked module that takes the two facts the runtime
owns — the adapter's answer and a settings getter — and decides. The runtime
class now only forwards. Move the gate reader's construction into the checked
installer too, so the nocheck file passes a plain settings closure and never
names a gate symbol.

Every reference to the gate predicate and its reader now lives in a checked
file, so the ablation that used to pass silently is a compile error at both the
create-support and reacquire sites.

Removing the file-level @ts-nocheck is a separate, larger job and is not
attempted here.
2026-09-03 15:14:24 -07:00
Merge Sim eba728d1d6 Merge commit '1d7a3938a1' into brennanb2025/claude-sdk-ready-mainsync
# Conflicts:
#	src/main/claude/claude-structured-launch-resolution.ts
#	src/main/runtime/orca-runtime-get-worktree-ps.ts
#	src/main/runtime/structured-agent-session-runtime.ts
#	src/main/runtime/structured-claude-runtime-adapter.ts
2026-09-03 14:38:21 -07:00
Merge Sim be45b351a8 Merge commit 'a632ea3353' into brennanb2025/claude-sdk-ready-mainsync 2026-09-03 14:34:07 -07:00
Merge Sim 1d7a3938a1 Run the managed-account gate on every Claude acquisition, not just create
createSupport gates the create path, but a session's account state can change
while it lives. A reacquire after an unexpected child exit re-resolves the
launch and re-derives auth, with nothing re-checking the gate — so a session
created while supported could come back up in the refused shape. With the strip
predicate keyed on there being an active non-WSL account, the WSL-only user's
normalized steady state (accounts exist, none active) does not strip, and that
reacquire reaches the child with ambient auth while the UI names the account.

Gate at resolveLaunch, the one choke point every acquisition passes through,
refusing with the pre-spawn error the caller already handles. Same predicate as
create-time, now sharing one settings reader so the two cannot drift.

Claude only; Codex resolves its account on a different path and is untouched.

The runtime class that wires this does not typecheck its own `this` calls — a
missing hookup compiles clean — so the wiring is pinned behaviourally rather
than trusted to the compiler.
2026-09-03 14:30:46 -07:00
Merge Sim a632ea3353 Round-3 review fixes: N-1 empty-value regression, N-2 gate leak window, N-4 lost history
N-1: my presence-based conflict predicate refused a terminal launch that works
today. 'ANTHROPIC_API_KEY=' is how a user blanks a variable and the settings
pipeline preserves that empty value; an empty override cannot beat the pinned
account and the strip removes the name anyway. Back to truthiness for the value,
keeping the win32 case folding.
N-2: enter the live-auth gate only after the exit/close handlers that release it,
so no throw in between can leave an entry nothing reconciles.
N-4: the Claude transcript resolver searches config-dir-then-default and de-dupes,
matching the Codex sibling in the same file, so adopting CLAUDE_CONFIG_DIR no
longer hides history written before it.
2026-09-03 14:24:48 -07:00
Merge Sim 488b56d62f Read the structured Claude account gate through the auth policy's accessor
The gate resolved the active account from the account-service snapshot's
runtime map; the auth policy resolves it with
getSelectedClaudeAccountIdForTarget(settings, { runtime: 'host' }). Those are
two sources and two resolution rules, and they disagree on a legacy settings
blob that carries the selection only in the flat activeClaudeManagedAccountId:
the accessor falls through to it, a direct read of the runtime map does not. The
gate would then refuse a launch the policy would have run under host-1 — and in
the mirror case a session could be admitted under a policy computed from a
different account than the gate approved.

Read the same settings through the same accessor so agreement is structural
rather than coincidental, and drop the controller accessor that existed only to
reach the snapshot.

No behaviour change for any state both already agreed on; Codex is untouched.
2026-09-03 14:17:48 -07:00
Merge Sim 746c952749 Merge commit '4e287f54868ad33ff46fc4401f9cd4925a1c8de8' into brennanb2025/claude-sdk-ready-mainsync 2026-09-03 14:09:18 -07:00
Merge Sim 4e287f5486 Do not offer structured Claude under a WSL-only managed account
Structured Claude launches against the ambient Claude config, which the account
service keeps in sync with the selected HOST account. A WSL-bound managed
account lives inside the distro and is never synced there, so on Windows a
structured session would authenticate as whatever the ambient identity happens
to be while the UI names the WSL account — the user is told one identity and
given another.

That was unreachable only because nothing offered structured Claude on win32.
Enabling it makes it reachable, so gate it here rather than patching the auth
layer: refuse the structured path when the active managed Claude account is
WSL-bound, and let the terminal-backed path — which resolves the account per
runtime — handle that account shape.

The answer rides the agentSession.createSupport seam the renderer already
consumes, so no new capability and no renderer knowledge of account internals.
A create the host declines becomes the definitive refusal the launch fallback
already turns into a legacy native chat tab, with no error toast.

Unknown answers refuse. An install with no managed accounts claims no identity
and is fine, but an active selection that cannot be resolved — or account state
that cannot be read at all — is not evidence that the ambient identity is right.

Claude only. Codex resolves its account through a different path and its
createSupport answer is untouched, as is every Codex routing decision.
2026-09-03 14:06:54 -07:00
Merge Sim ece915e4b1 Merge branch 'brennanb2025/claude-sdk-auth-parity' into brennanb2025/claude-sdk-ready-mainsync 2026-09-03 13:53:24 -07:00
Merge Sim 2a3bf2af5d Make the live-auth gate tests independent of sibling connection teardown order 2026-09-03 13:49:58 -07:00
Merge Sim 7d78675a06 P2-1/P2-2/P3: close the teardown window, join the live-auth gate, align the refusal
P2-1: a switch beginning inside the acquire teardown left a dead chat and no
replacement. Past that point the launch waits the swap out and refuses only if it
never settles; the entry guard still refuses outright, because nothing is torn down
there yet.
P2-2: structured children now hold the same OAuth-refresh gate a Claude PTY does,
so a managed refresh cannot rotate the token out from under a live turn.
P3: the refusal now matches the strip it guards (case-folded on win32, presence not
truthiness), and the dead structured-to-TUI builder states its auth policy instead
of silently signing a system-auth user out.
2026-09-03 13:43:59 -07:00
Merge Sim 67790e7b02 P2-3: mobile's default Claude transcript root must follow CLAUDE_CONFIG_DIR
session-file-resolver's default ignored the variable the pinned account home
follows, so a CLAUDE_CONFIG_DIR launch wrote one tree and mobile read another. The
Task-4 test now resolves with no root override (mobile's own call) and checks the
answer against the root the CLI itself reports, instead of mirroring the code under
test's own expression.
2026-09-03 13:32:21 -07:00
Merge Sim 747374b58f P1-1: make the structured Claude auth policy required and testable
The optional dep plus a {stripAuthEnv:false} fallback meant a dropped wiring
under-stripped silently. Required at all three hops, asserted at install time for
the @ts-nocheck caller, and the settings-to-policy mapping is now a named tested
function.
2026-09-03 13:29:44 -07:00
Merge Sim 891a1e72d6 Merge branch 'brennanb2025/claude-sdk-enable-platforms' into brennanb2025/claude-sdk-ready-mainsync 2026-09-03 13:16:57 -07:00
Merge Sim eb1880d095 Merge branch 'brennanb2025/claude-sdk-p1-fix-300f' into brennanb2025/claude-sdk-ready-mainsync 2026-09-03 13:16:44 -07:00
Merge Sim 9eb3aed2b3 Reach the Claude structured chat lane from the desktop UI
The main process has had a complete, correctly gated Claude Agent SDK lane for
a while, but no renderer ever asked for it: the launch route accepted only
`codex`, and the create path was typed `agent: 'codex'` end to end.

Widen both to the structured provider union that already exists
(`AgentSessionHandleProvider`), and generalize the codex-named create path
instead of adding a Claude twin beside it. The pending-launch registry is now
keyed by agent as well as workspace — a shared key handed a second caller the
first agent's intent, so a Claude and a Codex launch in one worktree collided.

Windows, per agent. Codex's client-side win32 refusal is deliberate and settled
elsewhere, so it stays exactly as it was. Claude's answer is no longer guessed
from the client's platform: a structured session fences its provider child on
that child's process start time, and only the executing host knows whether it
can read one. `agentSession.createSupport` already answers precisely that, per
agent, and had no renderer caller — so the Claude create path asks it before
creating and turns a "no", or a probe it cannot get answered, into the
definitive refusal the launch fallback already handles. Fail closed either way.

That refusal mapping also closes a real gap: the host reports an unsupported
location by throwing `structured_agent_session_unsupported`, which reaches the
client as a transport rejection rather than a refusal envelope, so
`StructuredAgentSessionCreateRefusalError` never fired. The launch would retry
the create, strand itself in `visibilityUnknown`, run no legacy fallback, and
show an error toast.

Close a fail-open hole while Claude and win32 become reachable: `create` with a
client-supplied location, and `ensure`, both skip the worktree-resolving support
check. They now ask the executing host the same question directly, so a host
that cannot fence a provider child no longer creates one on a client's say-so.

Also deletes `structured-agent-session-provider-routing.ts`, a duplicate of
`structured-agent-session-provider-support.ts` with no importers.

WSL, SSH and paired hosts, floating workspaces, draft prompt delivery, explicit
TUI customization and initial session options all keep refusing; folder
workspaces keep working.
2026-09-03 12:51:21 -07:00
Merge Sim 73fb77a7e7 Match the Claude structured launch to the terminal path's managed-account auth rules
The SDK path stripped ambient Anthropic auth unconditionally, let an explicit
agentDefaultEnv override beat a pinned managed account, and had no account-switch
guard. Reuse the terminal preflight's own predicate and messages so both transports
strip, refuse, and report identically, and cover the CLI transcript location that
mobile native chat depends on.
2026-09-03 12:04:30 -07:00
Merge Sim 7bc4dfdc1e docs(claude): rewrap the root-kill ordering comment 2026-09-03 11:54:32 -07:00
Merge Sim b88adb94ca fix(claude): restore the root SIGKILL fallback the identity gate took away
The direct root kill goes through the handle Node owns, not through a pid:
libuv drops that handle in the same turn it reaps, so the signal either
reaches the process Orca spawned or reaches nothing at all. Gating it on a
process-table probe therefore bought no safety and cost the tree its only
fallback whenever the probe declined -- a first capture landing in the fork's
own second, a recycled descendant pid voiding the snapshot, or a process table
that could not be read on either platform.

Identity verification stays where a bare pid is genuinely addressed: Windows
`taskkill /T /F`, and the descendant sweep's own revalidation before it signals.

Also stops a declined root probe from collapsing an observed `live` or `exited`
descendant verdict into `unverifiable`, and stops a successful taskkill from
reporting `unverifiable` because a later probe found the root correctly dead.
2026-09-03 11:50:53 -07:00
Merge Sim cb213e13c6 Merge remote-tracking branch 'origin/main' into brennanb2025/claude-sdk-ready-mainsync 2026-09-03 11:24:44 -07:00
Jinjing 0f22e1e905 Fix table header transparency with opaque background (#18499)
Replace the translucent bg-muted/25 with an opaque color-mix blend
(40% muted on background) to ensure scrolled rows don't show through
the sticky header. Add test coverage for header styling and layout.
2026-09-03 11:16:40 -07:00
Merge Sim 300f0ec872 fix(claude): fence same-second root identity reuse 2026-09-03 07:37:54 -07:00
Merge Sim 871f95667f fix(claude): gate root cleanup on process identity 2026-09-03 06:57:43 -07:00
Merge Sim e81c05f75d fix(claude): fail closed on recycled child identities 2026-09-03 06:23:05 -07:00
Merge Sim ef694fa47b fix(claude): close races with fresh child snapshots 2026-09-03 05:53:51 -07:00
Merge Sim fea7d710c9 fix: close Claude sessions safely 2026-09-03 05:21:45 -07:00
Merge Sim 54deb637b1 fix: harden Claude SDK transport session cleanup 2026-09-03 04:47:31 -07:00
Jinwoo Hong 67e22345da fix(cloud): stop passing manage_artifact_dns to the relay root (#18442)
The relay root does not declare it (it belongs to the private apps root), and
Terraform rejects an undeclared -var, so the first public Deploy Relay Staging
run failed at the C4 image bind.
2026-09-03 07:18:29 -04:00
Jinwoo Hong 3de1b9d058 fix(cloud): stop asking setup-node to cache the pnpm store in the relay workflows (#18432)
setup-node's cache: pnpm runs 'pnpm store path' from the repository root,
where packageManager pins pnpm 12; the shim it downloads fails to execute on
the runner, so the step dies before auth. Cloud Verify never used the cache
and passes; the six relay workflows that copied it from orca-cloud (root
pnpm 10 there) now match.
2026-09-03 07:05:21 -04:00
Jinwoo Hong 3eec77c11a chore(cloud): add the relay fence broker, ops console, Terraform root, scripts, and 24 cloud-* workflows (#18413)
Phase 6 of the relay split: the relay's deploy/operate surface moves under cloud/ with 24 cloud-* workflows gated on ORCA_CLOUD_OPERATIONS_ENABLED, the Cloud SQL rollout lease action, the relay Terraform root (dual-accept identities for both repositories), scripts, docs, CODEOWNERS, and a terraform validate job in Cloud Verify.
2026-09-03 06:55:14 -04:00
Neil 4cc0b8de61 perf(hot-paths): delete allocation-only work in sort, explorer, monaco, rpc, snapshots (#18372)
* perf(hot-paths): delete allocation-only work in sort, explorer, monaco, rpc, snapshots

* fix(perf): revert snapshot revision fast-path — same revision can carry a new session

* perf(hot-paths): drop the unproven rpc buffer rewrite, dedupe the equality helpers

- Revert the unix-socket chunk-carry change. Its comment claimed it avoided
  O(n^2) rescans, but chunks is reset to [remainder] every data event, so the
  join plus the tail byteLength is two passes where the old code did one;
  benchmarks showed no win. It also moved consumed-frame bookkeeping out of the
  closure, so a synchronous throw from the handler would re-dispatch frames.
- project-host-compatibility: fold the two byte-identical array comparators
  into one generic arraysEqualByJson.
- smart-attention: drop the leftover byTab.size === 0 branch that returned the
  same value as the line after it.
2026-09-03 03:20:59 -07:00
Neil f70f580627 perf(renderer): park hibernation and panel-watchdog work behind a hidden window (#18373)
* perf(renderer): stop five timers from ticking behind a hidden window

* perf(renderer): park hibernation and panel-watchdog work behind a hidden window

Narrowed from five timers to two, and made both correct:

- Gate on getWindowParkVisible(), not raw document.visibilityState. macOS can
  wedge visibilityState at 'hidden' with no further visibilitychange, which
  would park these for the rest of the session.
- Add a real becoming-visible pass via subscribeWindowParkVisibility, so resume
  does not wait out the remaining interval. Unsubscribed on stop.

Dropped the other three gates:
- terminal-delivery-watchdog: it is the recovery lane for the byte-drop bug the
  stale-visibility latch exists for; parking it costs a frozen terminal.
- crash-diagnostics: the dashboard-popout surface is normally occluded, so it
  would sample once at startup and never again.
- use-contextual-tour: attempts only increments past the gate, so a hidden
  window turned a self-clearing 20-attempt interval into a permanent one.

Tests stub visibilityState and the stale latch; both watchdog cases are RED
against the previous raw-visibilityState implementation.
2026-09-03 02:47:48 -07:00
Merge Sim d56809dd99 Merge remote-tracking branch 'origin/main' into brennanb2025/claude-sdk-main-sync-final 2026-09-03 02:43:29 -07:00
Neil 10b5ac8c90 perf(renderer): narrow vault and editor subscriptions off the every-write path (#18374)
* perf(renderer): narrow vault and editor subscriptions off the every-write path

* fix(perf): revert EditorPanel narrowing — downstream hooks need the full openFiles list

* perf(renderer): make the vault session-id cache resettable between tests

Every production writer replaces agentStatusByPaneKey, but test fixtures
commonly mutate it in place, which would keep serving the key cached for that
identity. Fold the WeakMap into the existing reset hook.
2026-09-03 02:41:03 -07:00
Neil 7b530f1eb5 fix(crash-reporting): record Orca-initiated tree kills so a killed renderer is decidable (#18367)
* fix(windows): refuse tree-kills of Orca's own Chromium pids and record the rest

G2 is 20 field reports that share only a symptom. It is at least four
fingerprints: ~15 Windows `reason=killed exitCode=1`, 3 POSIX SIGKILL under
memory pressure (G4-oom), 2 duplicate reports of one macOS V8 Proxy Resolver
SIGKILL, and 1 `0x80000003` install-dir ACL crash (G1; #17740 ships in
v1.4.196 only, not 1.4.195). Nothing here claims to fix all of them.

Two changes:

1. Behaviour. `classifyWindowsTreeKillTarget` returns `own` for any direct
   child of the main process — which our renderer, GPU and network-service
   utility all are — so PTY teardown could `taskkill /T /F` Orca's own UI
   (#10680). Both that classifier and `terminateWindowsProcessTree` now refuse
   any pid Electron is currently accounting for in `getAppMetrics()`.

2. Diagnosis. An Orca-issued kill and an external one are byte-identical in
   every field the crash report records today, so the cluster is undecidable.
   Every main-process force-kill choke point now records a durable
   `self_tree_kill` breadcrumb, and `process_gone` reports carry
   `selfInitiatedTreeKills` naming the pid and its offset from the death.
   A refused kill records `self_tree_kill_refused_own_chromium`, which is
   falsifiable: if it ever shows up in the field, we were the killer.

* fix(crash-reporting): coalesce self-kill breadcrumbs and scope the discriminator

Round-1 review remediation. Three blocking findings, all accepted.

1. Breadcrumb flood (accepted). recordSelfInitiatedTreeKill wrote an
   uncoalesced durable crumb from two routine teardown paths, and the
   reviewer reproduced 12 terminal closes x 3 process groups completely
   evicting the 30-slot ring — including this PR's own refusal crumb — plus a
   forced writeSync per killed group. It now uses the existing
   recordCoalescedDurableCrashBreadcrumb (5s window for pid-addressed
   taskkills, 60s for routine group/job teardown), so a burst costs one ring
   slot and one flush. The refusal crumb is coalesced per victim pid, so a
   retry loop cannot flood while a distinct pid always gets its own crumb.
   Regression test replays the reviewer's exact 12x3 reproduction and asserts
   the refusal crumb and a pre-existing gpu_process_crashed both survive.

2. Undifferentiated count (accepted). posix-process-group and win-pty-job are
   structurally incapable of reaching a Chromium process, and scope was absent
   from the persisted string. Scope is now in every entry
   (`<scope>/<site>/pid<N> +Nms`), and the count is split:
   selfInitiatedTreeKillCount now counts only pid-addressed taskkills — the
   kills that can land on a recycled pid that is now our renderer — with
   pty-scoped sweeps in selfInitiatedGroupKillCount. The list is renamed
   selfInitiatedKills because it carries both, and sorts pid-addressed kills
   first so truncation never drops the discriminating ones for teardown noise.
   The reviewer's repro (routine macOS terminal close + unrelated exit-133
   crash) now yields selfInitiatedTreeKillCount undefined.

3. Recording gaps and a false comment (accepted). New
   admitSelfInitiatedTreeKill gate: it refuses own-Chromium pids and records
   the rest, and all three main-process taskkill families now go through it —
   terminateWindowsProcessTree plus codex-accounts/service.ts and
   claude-accounts (which keep their own spawn lifetimes). The false "single
   taskkill choke point" comment is gone. The runProcess choke point the
   investigation asked for is instrumented via a
   setProcessTreeKillObserver seam in src/shared/child-process — shared code
   runs in the CLI and relay so it cannot import the main breadcrumb store —
   registered in main preflight. The codex app-server POSIX group teardowns
   and the claude POSIX branch record too. The module doc no longer claims
   absence is discriminating: it enumerates what is instrumented and names the
   direct process.kill(-pid) sites that are not.

Non-blocking, also fixed:
- Breadcrumb calls moved out of the try blocks whose catch is the ESRCH
  contract (posix-pty-process-groups, codex teardown, claude POSIX), so a
  throw from the diagnostic path can never be reported as a failed kill.
- Detail truncation now bounds the first entry too, matching its comment.
- own-chromium-tree-kill-refusal.test.ts renamed to
  own-chromium-tree-kill-guard.test.ts, colocated with the module it tests.

Not changed, with reasons:
- Date.now() vs performance.now(): kept. Offsets are computed against
  goneAt = Date.now() in process-gone-recorder; a monotonic clock here would
  make the offsets meaningless. The reviewer verified this and agreed it is
  not a defect.
- app.getAppMetrics() per force-kill remains unbenchmarked. It reads
  in-process browser state rather than enumerating the OS process table, and a
  TTL cache would let a recycled pid slip past the refusal, so it stays
  uncached.
- The ~15 remaining direct process.kill(-pid) sites (browser routes,
  notebooks, automation prechecks, ephemeral VM recipes) are not instrumented.
  Rather than claim coverage this PR does not have, the module doc names them.

claude-command-process.ts crossed the 300-line cap, so terminateClaudeProcess
moved to claude-login-process-termination.ts. No max-lines suppression added.

* fix(crash-reporting): scope the self-kill guard to its real host topology

Round-2 review findings on the own-Chromium tree-kill guard.

BLOCKING 1 — "the own-Chromium refusal is a no-op in the process that issues
the pty-descendant-sweep taskkill". Correct on the mechanism, wrong on the
consequence; REBUTTED in part and documented in full.

Confirmed: the only non-test `setAppEnvironment` installs are
main-process-preflight.ts:177 (Electron) and orcad-entry.ts:84 (Node, whose
`getAppMetrics()` is `[]`); daemon-init-fresh-import.ts is a test harness. So
in the standalone daemon `readOrcaChromiumProcessPids()` is empty and
`admitSelfInitiatedTreeKill` always admits.

But that is not a live hazard. `killWithDescendantSweep` reaches
`terminateWindowsProcessTree` only when `verifyWindowsTreeKillTarget` returns
`own`, and that walks ancestry back to `deps.ownerPid ?? process.pid` — the
KILLING process's pid. In the daemon that is the daemon's pid. Orca's Chromium
processes are children of Electron main, a sibling of the daemon, so their
chain never reaches it: hop 0 lands on main, and within MAX_ANCESTOR_HOPS the
walk dead-ends and returns `foreign`. The reviewer's probe passes
`ownerPid: 1000` with the renderer as a direct child of 1000 — that is the
Electron-main topology, where the AppEnvironment IS installed and the guard DOES
fire, not the daemon's. On an orcad/SSH host there is no Chromium on the box at
all, so `[]` is accurate rather than degraded.

Locked in as tests rather than prose (own-chromium-tree-kill-guard.test.ts):
a renderer classifies `foreign` from a daemon ownerPid with an empty pid set,
and `own` from main's ownerPid with an empty set — the falsifiable pair showing
the pid set is load-bearing in main and nowhere else. Documented the host
coverage in orca-chromium-process-pids.ts and own-chromium-tree-kill-guard.ts.

One genuine hole the finding exposes: `signalProcessTree`'s `taskkillTree` is a
fourth pid-addressed taskkill family (non-blocking item 2), it runs in the
daemon/relay/CLI where the guard cannot run, and it guarded only on
`!child.pid`. Reusing the predicate the codex login teardown already uses, the
win32 branch now refuses a reaped child and falls back to `killRoot` — the same
shape as the existing `!child.pid` branch. That closes the reaped-then-recycled
pid path in every host.

BLOCKING 2 — module doc overstates coverage. Rewritten: the ring is per-process
and its only reader lives in Electron main, so a count on a `render-process-gone`
covers main-issued kills only. Sites are now split into main-only, main-and-
other-hosts (runProcess choke point, POSIX PTY group sweep, Windows Job Object —
which record into a ring nothing reads when they run in the daemon or relay),
and never-instrumented, with the note that a daemon/relay omission is a
diagnostics gap, not a missed suspect, per the topology argument above.

BLOCKING 3 — the three out-of-main instrumentation sites were untested. Added
regression coverage: the runProcess seam on both branches plus the reaped-child
refusal (process-tree-termination.test.ts), the group sweep recording only
groups it actually signalled and skipping an ESRCH group
(posix-pty-process-groups.test.ts), and the Job Object recording the shell pid
only on `terminated` (windows-pty-job.test.ts). Verified red: reverting the
three production files to origin/main fails 7 of the new tests.

BLOCKING 4 — the Windows evidence validates a single-process model. Accepted.
The main2.js arms exercise `pty-descendant-sweep` inside one Electron process;
that models the in-process/degraded daemon and the local PTY provider, not the
standalone daemon. Arm C's "the 449351d6 shape is not producible with the guard"
holds for main-issued kills only. In the daemon the shape is blocked one layer
earlier, by the ancestry check, which the arms do not exercise.

NON-BLOCKING taken: `recordSelfInitiatedTreeKill` moved outside the native
`terminateJob` try in windows-pty-job.ts, so a diagnostics throw can no longer
downgrade a real termination to `unavailable` and escalate callers to a broader
kill; covered by a test. The "all three families" parenthetical is gone with the
doc rewrite. `pnpm build:relay` run: exit 0, all seven targets built.

NON-BLOCKING declined: codex-accounts/service.ts records before the spawn
because a refusal must prevent the spawn — the crumb means "we were about to
kill this pid", which is the artifact worth having; the existing comment already
says so. `app.getAppMetrics()` perf is unbenchmarked and unchanged by this round.

Verification: pnpm tc clean; oxlint clean on touched paths;
check:code-quality:changed 0 new findings; oxfmt applied. 730 tests pass across
shared/child-process, main/crash-reporting, main/pty, main/windows and the guard
and descendant-sweep suites. The 4 failures in providers/git/codex-integration
reproduce on HEAD without these changes.

* fix(crash-reporting): keep the reaped-pid skip from flipping the termination barrier

The win32 hasExited short-circuit correctly avoids taskkill on a pid Windows
may have reissued, but it resolved `true` — verified tree termination. A
taskkill against a reaped pid already resolved `false`, and run-process turns
`true` into barrierTerminationVerified + terminationReporter.report(), which
releases the git admission grant on root exit instead of on `close`. That
admits the next git command while a descendant holding the inherited pipes is
still writing the repo. Resolve `false` so the skip changes only which process
we refuse to signal, not what the barrier claims.
2026-09-03 02:40:58 -07:00
Neil 3a32e084dd perf(renderer): index diff comments, skip no-op hydration, drop duplicate normalizes (#18375)
* perf(renderer): index diff comments, skip no-op hydration, drop duplicate normalizes

* fix(perf): keep tree-path stability hook render-pure for react-doctor

* fix(perf): publish the returned array from the tree-path stability hook

The ref was written with the raw input but read in render to pick the return
value, so it trailed one commit and a wave of content-equal arrays flipped
identity every render — re-firing the uncancellable full-tree git check-ignore
it exists to prevent. Publish `stable` instead, keyed on `[stable]`.

Also drops the hydrateOverrides no-op skip: notifyChange is not a bare wakeup
(it drives getPanesNeedingOverrideFit -> safeFit and the remote viewport
re-claim), and the branch never fires in production anyway.
2026-09-03 02:37:36 -07:00
Merge Sim cf5904476e Merge branch 'main' of https://github.com/stablyai/orca into brennanb2025/claude-sdk-latest-main-sync 2026-09-03 02:31:40 -07:00
Merge Sim f7ddc0e489 chore: remove tracked review artifact 2026-09-03 02:18:51 -07:00