* test(repro): #19735 resumes a published mirrored pane before its handle lands * fix(runtime): park a mirrored pane's resume until its PTY handle lands Mirror hydration means the host's tab rows arrived, not that a given pane's liveness is decidable: the PTY handle lands one relay round trip later. On that frame the pane read as not-live and the sweep resumed a session the host was still running, producing a duplicate resume tab. An empty handle map for a published row is unverifiable, never exited. Park the pane on a per-pane wait with three bounded exits, each replaying the sweep: its own handle lands, the row is retracted, or a deadline expires. The deadline decides resume rather than an indefinite hold, and is scoped to the connection generation so a reconnect re-arms it. Closes #19735 * fix(runtime): bound the handle-gap expiry map to the current connection * fix(runtime): void a handle-gap verdict the reconnect made stale The per-pane park bounds itself with one deadline per connection, but the waiter never recorded WHICH connection it was armed on. A wait armed on generation 0 that fires after a reconnect stamps its expiry against the current generation, so hasHostMirrorHandleWaitExpired agrees, the mirror lookup returns null, and the pane is resumed after 1ms on a connection that has had no chance to publish the handle. That is #19735's fork with an extra step, reached through the guard that exists to prevent it. The module's own doc comment claims the opposite -- "a reconnect bumps the connection generation and arms a fresh wait" -- and that is true only for a wait which had ALREADY expired, which is precisely the case the existing test covered. The test and the comment agreed with each other and both were wrong about the live case. The waiter now carries the generation it was armed on and records no verdict when the generation has moved; the replay re-parks through the existing machinery and the new connection gets its own full budget. Still bounded per connection generation, which is what was documented all along. Also pins the three sibling attacks on the same window: two panes in one environment where only one handle lands, a handle published by a foreign environment, and an environment tearing its rows down mid-park (which leaves no waiter and no scheduled timer). The test file now leads with how to assert on this module at all, because the obvious shape cannot fail. "Did the waiter release" is not an observable here -- a waiter released for the wrong reason is re-parked by the replayed sweep, so the store reads identically one tick later, and a mutation releasing every waiter on any tab's handle survived twelve assertions written that way. What a spurious release costs is the deadline, so the assertions advance the clock and require the pane to decide on the ORIGINAL schedule. * fix(terminal): a live pane owns its transcript in any workspace The resume dedup was scoped to the record's own workspace on both terms -- the entry's tab had to be in worktreeTabIds AND entry.worktreeId had to match -- and additionally required entry.state !== 'done'. A record whose peer pane has finished a turn and still holds a live PTY therefore matched nothing, and the sweep launched a second agent onto a transcript the peer is still writing. Cross-workspace, it matched nothing even while the peer was mid-turn. The two ids really do drift. canonicalizeTerminalSessionWorktreeId re-keys tabsByWorktree, tabGroups, tabGroupLayouts, activeTabIdByWorktree and activeGroupIdByWorktree onto the canonical worktree id, and does NOT re-key sleepingAgentSessionsByPaneKey, whose records carry worktreeId inside them. So adopting an orphaned terminal is a direct producer of a record naming one workspace while its pane and status row name another. Split into two arms rather than widening the existing condition. The new arm carries no workspace scope but demands hard evidence: a provider session id names one transcript, so a pane whose exact PTY is live right now already owns it wherever that pane sits, and no workspace boundary makes a live PTY less live. The scoped arm keeps its scope and its state !== 'done' term, because a status row with no live PTY is a claim about the past and must not reach across workspaces. Relationship to #19736: that PR fixes the SAME-workspace half of this in the same function, by relaxing only the status term. This arm covers that cell too -- measured both ways on this branch, which does not carry #19736: its thirty `checks exact live ownership before resuming` cases all pass with this change alone, and ten of them fail without it. So this supersedes #19736 rather than sitting beside it, and #19736's one-line `export` of stablePaneHasLivePty is carried here because this arm needs it. If #19736 lands first this becomes a pure widening and its tests should be kept. Both cells are pinned here either way. * fix(runtime): isolate one pane's replay from the handle-gap drain One store write releases every due pane, and the drain runs synchronously inside a zustand subscriber. `waiter.run()` was unguarded, so a single pane's replay reached two things it has no business touching: - the throw escapes out of `useAppStore.setState`, meaning the mirror apply that published the PTY handle throws at its own call site; - every pane queued behind the thrower is stranded — waiter still parked, deadline still armed — and then decides on a connection whose evidence landed long ago. The deadline path fans out the same way, so a throwing replay also escaped the timer callback. Reachable: `resumeSleepingAgentSessionsForWorktree` reaches `state.createTab` with no guard of its own. The panes in a drain are strangers to each other and to the frame that released them; none of them should be able to see another's failure. The new tests live in their own file because host-mirror-handle-gap-resume.test.ts drives the waiter through the real resume sweep and so cannot choose what a replay DOES. Note for anyone extending that file: per its header, "did the waiter release" is not an observable here — a spurious release is re-parked immediately and reads identically one tick later. These tests assert on timer count and on the deadline instead. Also records two findings next to the code, so they are not rediscovered: `expiredGenerationByPane` is never pruned for a removed environment (bounded and inert, since removal advances the generation, but it does not drain — and a DIFFERENT leak in that same map is being fixed concurrently, so reconcile rather than patch around it); and sustained reconnect churn holding a pane parked indefinitely is CORRECT, not the latch-that-never-releases defect, because under churn liveness genuinely is unverifiable and ssh-execution-boundary.md forbids resolving that to `exited`. It has the shape of the defect and will eventually be "fixed" by someone who does not know that. Mutation: dropping the guard kills exactly the three new assertions and leaves all twelve existing waiter tests passing. * fix(runtime): drain a removed environment's handle-gap verdicts on teardown `expiredGenerationByPane` is pruned only by rules that run when a verdict is RECORDED — the stale-generation sweep here, and the tab-death sweep added separately (8f16641130, env-scoped inc0e44238ea). An environment that is REMOVED records nothing ever again, so neither rule can reach its rows and they survive for the life of the session. Two orphan classes on one map; neither prune subsumes the other, because both are driven by a recording. Severity is a leak, not a correctness bug, and the commit pins WHY so nobody re-derives it: removing an environment advances its connection generation, so a stranded verdict can never match again even if the id returns. That test exists to stop the generation advance being "optimised" away later, since it is the only thing making the stranded row inert. Hung off `clearWebSessionTabsTrackingForEnvironment` because that is the only caller that fires for an environment that is going away. Clears VERDICTS ONLY. Parked waiters deliberately survive, matching `clearHostSessionMirrorHydration`: a re-pair or effect restart replaces the connection's evidence, it does not cancel the recovery this client still owes the pane. A waiter left behind is bounded by its own deadline and replays its sweep exactly as it would have. Clearing them here would silently drop a parked resume that nothing else replays. A measurement worth recording, because it argued me out of a change I was about to make: on the unfixed map the per-expiry rescan is super-linear — 500/1000/ 2000/4000 sequential expiries cost 7.2/15.3/51.8/173.1 ms, doubling ratios converging on ~3.35 against 4.0 for quadratic. That looked like a case for reshaping the map to `Map<env, {generation, Set<tabId>}>`. It is not: the quadratic is a property of the LEAK, not of the scan. Once the tab-death prune holds the map at roughly one entry per environment the scan is over ~1 entry, and a counting probe on the fixed map (summing `map.size` across N expiries, which IS the iteration count and needs no clock) gives exactly N-1 — linear, and 2000x fewer iterations than quadratic at N=4000. The flat prefix loop used here is the established pattern in this subsystem and needs no restructure. Two methodology traps this cost, recorded for the next person measuring in this repo: `vi.useFakeTimers()` fakes `process.hrtime` and `performance.now` as well, so a timing harness reports the advanced deadline rather than work done — fake only the timer surface under test. And expiring N panes in one burst measures the fake-timer harness clearing N timers, not product code; 1000 panes "cost" ~1s that way and almost none of it was ours. Mutations: a clear that drops nothing kills exactly the two assertions that claim it drains, and correctly leaves the waiter-survival and generation-advance tests passing. An UNSCOPED clear kills the same two, via their sibling- environment half. * fix(runtime): reconcile three branches' handle-gap verdict rules into one loop Three agents changed `recordExpiredWait` on three branches and each verified only their own. This is the union, resolved into the agreed shape and proved on one tree. The rules are NOT alternatives — they have different safety properties, and flattening them to one scope is wrong in both directions. Both wrong shapes were independently written before this was reconciled, so the comments say why. GENERATION rule, per key across EVERY environment (adv2-skew's class). `hasHostMirrorHandleWaitExpired` compares a row against its own environment's CURRENT generation, so a row whose generation has moved can never return true for anybody; retiring it cannot cost a reader a verdict, whoever owns it. Scoped to the recording environment, an environment that reconnects and then goes quiet strands its rows forever. TAB-DEATH rule, recording environment ONLY (my class). Row absence is transient where a generation is not: a sibling mid-republish has no rows for a frame and would lose a verdict its pane still needs — reproduced before it was narrowed. Teardown drain (adv2-races' class) is unchanged and orthogonal: it is the only trigger that fires for a REMOVED environment, whose rows no rule above reaches because such an environment records no further verdict. Right predicate, wrong trigger. The union suite proves all four orphan classes simultaneously, plus the two properties none of the three rules may break: the verdict stays sticky enough to break the park/expire/replay loop, and no rule evicts a verdict a live pane still needs. It uses three environments throughout, because with two at one generation the candidate rules are indistinguishable and the naive fix survives. THE FOURTH CLASS IS UNOWNED AND ASSERTED AS A HAZARD. A retracted tab id that is republished inherits the old pane's verdict and skips its own wait. Unlike every other gap on this map it is NOT conservative: the others drop a verdict and re-park, holding longer, while this one retains a verdict and resumes on a handle that has not landed — the #19735 direction. No rule reaches it: the tab-death predicate stops matching once the id is republished, the teardown drain fires on environment teardown rather than tab retraction, and no waiter exists to observe the retraction because a pane holding a verdict never parks. Closing it needs a fourth trigger, on row retraction. The suite pins the current behaviour so it cannot be quietly forgotten. Union finding, recorded rather than merged: adv2-skew's `docs(relay): the live-broker wait budget does not bound the call` (6b029820cc) is SKIPPED here. It documents the unbounded wait, and adv2-concurrency-fixes (1673716c6d) fixed exactly that by extracting the loop into relay-live-broker-wait.ts. The doc and its test pin behaviour the union no longer has. This is the kind of interaction neither branch could see alone. * fix(test): repair the teardown suite the union broke Cherry-picked from46ad377cebwith the relay half dropped: that commit also repaired relay-concurrency-policy-flip-mid-mint.test.ts, which does not exist on this PR and belongs with the relay cluster's own branch. The handle-gap half is what this PR needs. Neither break was visible on its own branch -- both only appear once the verdict rules compose. * fix(runtime): a handle-gap verdict answers for its pane, not for the tab id Folds adv2-skew'se8cac056d7into the reconciled union. Closes the fourth orphan class, the only one that was not conservative: a retracted tab id republished as a different pane inherited the old pane's verdict and skipped its own wait — the #19735 direction rather than a longer hold. It needs no fourth trigger, which is why it composes with the three drains rather than competing with them. Every trigger those rules own fires downstream of the moment this hazard needs. The verdict instead carries the environment-minted PTY binding its pane held AT PARK TIME, and only answers for a pane that still holds it: a republished pane binds a newly minted PTY and serves its own wait, while a genuine reattach to the same PTY inherits, which is correct — the verdict follows the PTY, not the id. A transient rowless frame touches neither, so the read-time check is safe where a retraction-triggered prune would not have been. TWO MEASUREMENTS, both requested rather than assumed. 1. The record-then-release ordering is load-bearing and IS pinned. `recordExpiredWait` reads the waiter's park-time binding, so it must run before `releaseWaiter` deletes the entry. Swapping the two statements fails three cases, so the capture is not correct merely by accident of statement order. 2. The `''` fallback is a MATCH VALUE, not a null: two panes that both hold no environment-minted PTY compare equal and inherit, which is the same hazard in a narrower window. Measured unreachable through the production park path rather than assumed — the only route in is `kind: 'handle'`, which `findUnhydratedHostMirrorForPane` reports only when `tabHoldsEnvironmentPtyBinding` finds a binding, reading the SAME map through the SAME predicate as `paneBindingFor`. It now refuses to answer anyway. That coupling is two functions in two files with nothing enforcing it, refusing costs only a re-park, and the direction is conservative. THE REFUSAL IS WHAT FOUND THE REAL BUG. With `''` matching, any fixture that omits `terminalLayoutsByTabId` records `''`, compares `'' === ''`, and passes while the pane-identity check is entirely inert. Making it refuse turned that silence into four failures across host-mirror-handle-gap-drain and -teardown, whose fixtures seed no layout binding at all. Both now bind per environment — one shared environment id filters every other environment's pane back to `''` and restores the no-op. Mutation-tested on the merged tree: ignoring the binding fails case D and the mid-wait case; re-reading at expiry fails the mid-wait case and nothing else; letting `''` match fails the empty-binding case; widening the tab-death rule across environments still fails the live-verdict case, so pane identity does not weaken the scoping the sweep was reconciled around. Also fixes a real-clock race this branch introduced: the revoke-window test read `Date.now()` separately from `enqueue`'s own stamp, and under load the drift ate into the window. It now anchors the injected clock to the item's `createdAt`. * docs(runtime): the two guards on the park-time binding are not redundant Recording a reconciliation result that existed only in a review thread, and correcting it in the process — measuring the claim changed it. The claim under review was that the `?? ''` fallback in `recordExpiredWait` is unreachable by two independent guards, either sufficient alone: the caller's generation gate (a missing waiter fails `undefined === number`) and the record-before-release ordering. That is not what the code does. Measured, by removing each in turn: - ordering removed, generation gate kept: the gate does NOT carry it. With the waiter already deleted, the gate is false on every expiry, so nothing is ever recorded — five failures, and the door is shut by breaking the mechanism rather than by refusing ''. - generation gate removed, ordering kept: 736 files green, one failure, and it is `does not let a wait armed on the previous connection decide the new one` in host-mirror-handle-gap-resume.test.ts — a different property entirely. So the ordering alone makes `''` unreachable, and the generation gate is not a second guard on it at all: it pins reconnect-void. Both are load-bearing, for different reasons, which is a stronger argument against removing either than redundancy would have been — redundancy invites deleting one. Worth writing in the file because the two sit three lines apart and read as belt and braces on the same thing. The `''` comment next to them already exists because an unexplained guard on an unreachable value gets deleted as dead code in a year; a guard that looks redundant is deleted sooner. No behaviour change. One comment, corrected against measurement rather than against the thread it came from. * fix(runtime): a published handle retires the verdict it answered The fourth eviction trigger on `expiredGenerationByPane`, and the reason it is not redundant with the three already there or with the two other agents' guards on this same map. A verdict records that a pane's 15s handle-gap wait ran out. Nothing retires it when that pane subsequently publishes its handle, so the NEXT gap on that pane gets no wait at all — #19735 with the bounded wait removed rather than merely shortened. Measured on the reconciled union tree (1b621b6b13) plus the outage guard: the verdict still answered `true` after the handle landed, and the second gap resumed with zero panes parked. Why none of the existing rules reach it, each checked rather than assumed: - superseded generation: #19647 in this same stack stops recording `status: null` for an unreachable host, so the generation no longer moves across an outage on one runtime. - dead tab row: the row stays published throughout. It is the HANDLE that comes and goes — that is the definition of the gap. - removed environment: the environment is still here. - read-time pane identity (adv2-skew,cdafc90d8f): the pane keeps the same layout binding across the gap BY DESIGN, and the union suite pins that a genuine reattach to the same PTY must inherit. That check discriminates a different pane behind one tab id; this one discriminates a later gap on the same pane. - contact lost (adv3-journeys,2da662424b): no outage is involved; this is a healthy connection where the host was simply slow once. Composition proven by mutation on the union tree, four disjoint kills: dropping this drain kills 2 tests and only mine; dropping the re-park worktree kills 1 and only mine; dropping the contact guard kills 1 and only theirs; forcing the contact guard always-true kills 16 across every suite. No mutation kills another agent's test, so these are three guards on three holes, not three on one. Also carries `worktreeId` across a re-park: adopting an orphaned terminal re-keys `tabsByWorktree` without re-keying the record, so a live wait kept releasing on retraction evidence about the workspace it was no longer about. The park-time `paneBinding` deliberately does not move with it — that is the pane's identity, this is only where its rows are filed. * docs(runtime): the reused-tab-id class is closed at read time, not still open The `ExpiredHandleGapVerdict` docstring told the next reader that a retracted tab id republished under the same id still inherits its predecessor's verdict, and that closing it "needs a fourth trigger, on row retraction". The test it names as its own pin says the opposite: class D in host-mirror-handle-gap-verdict-union.test.ts asserts the verdict does not answer, and explains it is closed at READ time rather than by any prune. Provenance, since two sources disagreeing is what made this expensive: the paragraph was last written in46ad377ceband the read-time pane-identity check landed one commit later incdafc90d8f(adv2-skew). The prose predates its own fix by a single commit and was never updated. Confirmed by mutation rather than by reading: dropping `verdict.paneBinding === paneBindingFor(...)` fails exactly "handles all four orphan classes simultaneously", which is the class-D assertion, so the read-time check is what closes it. Rewritten to say what the code does, keeping the part that was always true — why no trigger could have reached that class — and keeping the distinction the new PUBLISHED HANDLE drain needs: read-time identity separates two panes behind one tab id, the drain separates two gaps on one pane. The drain does not close class D and must not be read as closing it. Also records why this block specifically keeps going stale: several agents change this map in parallel, the invariants move faster than the prose, and when the two disagree the test file is the one that ran. * test(runtime): pin replay containment on the deadline path too Cherry-picked fromaa98edf35a(nwparker/adv3-failure-fixes) with its implementation hunk dropped: a second agent found the same throwing-replay hole independently, and `15f34014153` already closed it on this branch with an equivalent guard. Applying both would have been a double-apply, and the two spellings of the log line would have shipped side by side. The tests are worth keeping regardless. The first duplicates coverage15f3401415already has; the second does not -- it drives the throw from the DEADLINE path rather than the store-write path, which is a separate call into releaseWaiter and was unpinned. Spies retargeted from console.error to console.warn, the channel the guard that actually shipped writes to, so the suite silences what the code emits. * fix(runtime): isolate one worktree's replay from the mirror-hydration drain The same fan-out hazard as the handle-gap drain, one module up. Settling an environment drains every worktree parked on it in a single loop, called from the frame apply, with `waiter.run()` unguarded. One replay that throws strands every waiter queued behind it and surfaces in the caller applying the frame. Found by looking for the sibling of a defect rather than by a separate interleaving: both modules park a `run` callback and drain N of them from one event, so both have the same blast radius. Kept as its own commit because the two modules route independently. Mutation: dropping the guard kills exactly the one new assertion. * test(runtime): pin sleeping-agent resume on a failed SSH target The terminal-state floor in workspace-terminal-host-authority.ts has three consumers: initial-terminal seeding, the startup terminal watcher, and sleeping-agent resume. Seeding is covered end to end by worktree-agent-activation-seam.test.ts. Resume was covered only at the predicate, so nothing failed if the floor stopped reaching it — and the floor's own comment says the cost of losing it is a failed target left terminal-less with unresumable agents for the rest of the app session. Pins the resume half directly: an SSH git worktree on a target whose sync terminated in offline/error with an empty hydrated set resumes its sleeping agent. Two controls keep the floor from widening into "resume whenever we are unsure" — an in-flight 'pulling' sync and no sync status at all both stay unverifiable and resume nothing. Verified by mutation: emptying TERMINATED_WITHOUT_ANSWER_PHASES fails exactly the two floor assertions and leaves both controls passing. Routes independently of the two fixes on this branch: the floor predates this stack (#16750), and this only closes a coverage gap in it. * test(runtime): pin the store subscription the reconciled loop can leak The retention suite that `reconcile three branches' handle-gap verdict rules into one loop` replaced carried an assertion the split suites did not: the store subscription is held for exactly as long as something needs it. Measured before writing it, because half of it was already covered: RETAIN direction -- drop the verdict term from `stopStoreSubscriptionIfIdle` so a verdict with no waiter behind it loses the subscription its drain needs: already caught, 2 failures in host-mirror-handle-gap-landed-handle.test.ts. RELEASE direction -- never release the subscription at all: caught by NOTHING. That mutation passes all 272 tests across the 33 other handle-gap and session-tabs suites. A leaked subscription rescans every parked pane on every store write for the life of the session and nothing notices. So this is for the release direction. The retain cases ride along because both halves of one invariant belong in one file, not because they were missing. That term is also precisely what the reconcile moved -- it now counts verdicts as well as waiters -- so it is the part of this map most likely to drift again. Asserted with a spy on useAppStore.subscribe rather than a new test-only export: whether the module is subscribed is already observable at the store boundary, and the production surface should not grow just to say so. * fix(lint): carry SAFETY rationales for the handle-gap fixtures main tightened typescript/consistent-type-assertions to assertionStyle: never after this branch was written. The gate only ran here once the rebase put the casting config at the merge base, so these sites are new to it, not new to the branch. The store seeds are genuinely partial -- dropping the casts does not typecheck -- so each carries its rationale. * fix(runtime): release a handle-gap pane once per store write `releaseDueWaiters` snapshotted the due KEYS and then re-looked-up each one. A replay earlier in the loop writes to the store — the sweep reaches `createTab` and `clearSleepingAgentSession` — and zustand notifies re-entrantly with no queue, so the nested pass can release and re-park a pane still queued in the outer loop. The outer `releaseWaiter(key)` then found the re-park, cleared its brand-new deadline and replayed it a second time off one store write, handing that pane another full budget. That is the extension `parkUntilHostMirrorHandleLands` already refuses to grant a re-park, arriving through a different door. The direction is conservative (hold longer, never resume early), which is why no outcome assertion could see it; only the replay count separates the two implementations. Snapshot the waiter alongside its key and release only while the map still holds that same waiter. Also folds host-mirror-handle-gap-replay-containment.test.ts into the drain suite, since the guard it pins is the one this commit extends. It was the same fix imported twice: its deadline case is a strict subset of the drain suite's, its store-write case differs only by also asserting that later store listeners still run, and one mutation — rethrowing from the replay catch — killed all five cases across both files. Its fixture also seeded no layout bindings and used tab ids `isWebTerminalSurfaceTabId` rejects, so those panes could not have reached the park path it claimed to exercise. The unique assertion moves across; the file goes. Killed by `releases a pane once per store write even when an earlier replay re-enters the drain`: 2 replay calls instead of 1 without the identity guard. * test(runtime): retire the handle-gap assertions that could not fail `returns to baseline under churn across all three drains` asserted nothing. It ran 300 expiries and then cleared every environment's verdicts by name before counting, so the map was empty by construction — deleting the whole prune loop in `recordExpiredWait` left the test green. It now asserts the bound BEFORE the teardown clear: 300 expiries must leave exactly one live verdict per environment. It also binds each round's pane to the environment recording it; the old fixture filed every binding under env-a, so two rounds in three stored the empty match value the read-time check refuses, and that much of the churn was synthetic. Renamed: there are four drains, not three. Two comments described outcomes their assertions do not produce. `c1` reads false on the read-time generation gate alone, whether or not a drain ever swept it — the count below is the only assertion that distinguishes retired from stranded. And the discriminator in `never evicts a live pane verdict` is env-b, whose row goes absent while env-a records; env-c is a control that holds under every candidate rule. Two more fixtures modelled states the mirror apply cannot produce, both leaning on `ptyIdsByTabId[tab]` holding a PTY id no leaf of that tab is bound to. It builds one from the other (web-session-tabs-sync/terminal-build.ts), so they can never disagree. The producible shape is a SPLIT tab whose sibling surface went `ready` first, which is what both cases now seed — and which makes the residual they were quietly standing in for visible instead: the decidability gate above this wait is tab-granular while everything below it is leaf-aware, so a sibling handle ends the wait for a surface still `pending-handle`. Recorded at the gate in host-mirrored-pane-liveness.ts, pinned by name, and left open here: it needs the per-surface status the host already publishes and the client drops on apply. Also states what the unscoped live-PTY arm trades — a finished agent whose shell is still up releases its record and will not auto-resume — because it reads as a regression and is not one. And replaces the subscription-lifetime header's unreproducible "272 tests across 33 suites" with the measured 326 across 37. * fix(runtime): re-judge a handle-gap waiter the drain's own replay moved The identity guard added one commit ago catches only half of how the drain's snapshot goes stale. It proves the map entry was not REPLACED; it cannot prove the verdict still holds, because `parkUntilHostMirrorHandleLands` re-parks a still-parked pane by MUTATING the waiter in place. `worktreeId` moves with `run` — that is what adopting an orphaned terminal does — and object identity survives it. So a waiter the snapshot judged retracted, because its tab was absent from the worktree it was filed under, can be re-filed by an earlier replay in the same loop and then released on evidence about a workspace it is no longer about. That is the defect the `existing.worktreeId` assignment exists to prevent, reached through the drain instead. Neither guard covers the other: re-judging alone still replays a re-park twice (it was just made due, so it re-judges due), and identity alone misses the mutation. Both, in that order. A waiter that is no longer due simply stays parked — bounded by its own deadline and re-judged on the next store write, so declining costs at most one frame of latency. Killed by `does not release on retraction evidence a mid-drain re-park has already made stale`; the identity half is still killed by `replays a pane once per store write even when an earlier replay re-enters the drain`. 29 mutations across these modules, no survivors. Corrects three claims made in the two preceding commits, each wrong in a way a future reader would have acted on: - the duplicate release does NOT extend the pane's budget. `releaseWaiter` deletes the waiter before calling `run`, so the re-park takes the `!existing` branch and arms a full deadline either way; a second release clears and re-arms at the same instant. What it costs is running an entire worktree resume sweep twice off one frame. The test is renamed to say so. - `retainPendingTerminalBindings` carries a `pending-handle` surface's prior binding forward, so the split-tab residual cannot arise from a bound leaf going pending — it needs a leaf that was NEVER bound, which means a cold start or re-pair with no layout to retain from. The fixture staged the impossible history; it now seeds the producible first frame, where the point sharpens: no wait is armed at all, which the test now asserts directly. - `clearSleepingAgentSession` cannot re-enter the drain; the subscription's slice guard drops that write. Only `createTab` can. Also: the churn assertion pins that the prune loop runs at all, not which rule prunes — with one live tab per round either rule alone still reads 3. Says so, and points at the case that does isolate the generation rule. * test(runtime): stage the handle-gap adoption the way a sweep can reach it The case added one commit ago pinned the right guard through a call sequence production cannot make. A replay is `resumeSleepingAgentSessionsForWorktree` closed over ONE worktree, so it re-parks only under that worktree — but the fixture had the first pane's replay re-park the second under a DIFFERENT one. That is the same fault the previous commit corrected in the resume fixture, made one file over. The reachable route: the second tab has already been re-keyed onto the canonical worktree id while its sleeping record still names the old one, so the first pane's sweep legitimately owns it and re-parks it there, mutating the live waiter in place. The waiter's snapshot verdict — "retracted", because the tab is absent from the id it was filed under — is stale by the time the loop reaches it. Restaged that way, with both waiters and the re-park inside one worktree. Also drops the second global store read. Guard TWO now re-judges against the same `state` the drain was notified with, because the two provably cannot differ here: a re-park only happens when `findUnhydratedHostMirrorForPane` finds the row already filed under the sweeping worktree, which is a row this frame carries. `useAppStore.getState()` was an unpinnable degree of freedom — swapping it for `state` left every test green — and it contradicted the drain's own claim to judge one snapshot. And corrects two more claims: guard ordering is a cost preference, not a correctness requirement (either order works; identity first is just cheaper), and "no wait is armed here" in the split-tab fixture is not caused by the cold start — it is the tab-granular gate reading the sibling's handle, which is the residual itself. 29 mutations across these modules, no survivors. Each guard is killed by exactly one case and they do not overlap: `replays a pane once per store write even when an earlier replay re-enters the drain` for identity, `does not release on retraction evidence a mid-drain adoption has already made stale` for re-judging. * fix(runtime): judge a re-parked handle-gap waiter on the live store Reverts the `state` read the previous commit put in guard TWO, and says why the difference is deliberately unpinnable rather than leaving the next reader to "simplify" it back. The previous commit swapped `useAppStore.getState()` for the subscriber's `state` because a reviewer noted the swap left every test green. That was optimising for mutation-killability over the property the module exists to protect. The two reads do agree on every sequence the sweep can produce — a replay's only store write is `createTab`, which appends a freshly minted tab id, so it can neither make an absent tab id present nor touch `ptyIdsByTabId` — which is exactly why no test separates them. But they are not interchangeable: `state` is the staler of the two, and its failure direction is to RELEASE a pane whose row has come back. That is resolving unverifiable to exited, which is #19735. Holding on evidence that might be stale costs one frame; acting on it forks a transcript. Take the fresher read, and record in the comment that no test can fail on this and why that is not a reason to change it. Both guards remain killed by exactly one case each and they do not overlap: `replays a pane once per store write even when an earlier replay re-enters the drain` for identity, `does not release on retraction evidence a mid-drain adoption has already made stale` for re-judging. Verified while confirming the previous commit's test premises against production, both of which hold: a sweep that parks every record writes nothing to the store (resume-sleeping-agent-session.ts takes `continue` on the park branch), and `workspace-session-worktree-id.ts` moves `tabsByWorktree` onto the canonical id while leaving `sleepingAgentSessionsByPaneKey` naming the old one — the stale `worktreeId` the adoption case depends on.
Orca
中文 · 日本語 · 한국어 · Español · Français · Português
The AI Orchestrator for 100x builders.
Run Codex, ClaudeCode, OpenCode or Pi side-by-side — each in its own worktree, tracked in one place.
Download Orca
Features
Also in the box:
- Quick open — Search across worktrees, files, agents, commands, and repo context without leaving your flow.
- Account switcher & usage tracking — See Claude and Codex usage and rate-limit resets, and hot-swap accounts without re-logging in.
- Rich repo previews — Preview Markdown, images, PDFs, and repo docs in the workspace.
- Computer Use — Let agents operate desktop apps and visible UI when a workflow needs real interaction.
- Notifications and unread state — Know when an agent finishes or needs attention, then mark threads unread to come back later.
- And many, many more — we ship daily, so this list is perpetually behind. The changelog is the real feature list.
Supported Agents
Works with any CLI agent — if it runs in a terminal, it runs in Orca.
Claude Code
Codex
Grok
Cursor
GitHub Copilot
OpenCode
MiMo Code
Amp
OpenClaude
Antigravity
Pi
oh-my-pi
Hermes Agent
Devin
Goose
Auggie
Autohand Code
Charm
Cline
Codebuff
Command Code
Continue
Droid
Kilocode
Kimi
Kiro
Mistral Vibe
Qwen Code
Rovo Dev
+ any CLI agent
Install
Desktop — macOS, Windows, Linux
- Download from onOrca.dev
- Or grab a build directly: macOS Apple Silicon · macOS Intel · Windows (.exe) · Linux AppImage · All builds
- Running
orca serveon a headless Linux server? See the headless Linux server guide.
Or via a package manager:
# macOS (Homebrew)
brew install --cask stablyai/orca/orca
# Arch Linux (AUR) — or stably-orca-git to build from source
yay -S stably-orca-bin
Mobile Companion — iOS, Android
Pair with your desktop app to monitor and steer your agents from your phone.
- iOS: Download on the App Store or join TestFlight
- Android: Download APK 0.0.48 · Install guide
Community & Support
-
Discord: Join the community on Discord.
-
Twitter / X: Follow @orca_build for updates and announcements.
-
WeChat: Scan to join the Orca community WeChat group 8. Group 8 may be full; if so, scan the Group 9 QR code instead.
-
Feedback & Ideas: We ship fast. Missing something? Request a new feature.
-
Privacy: See the privacy & telemetry docs for what anonymous usage data Orca collects and how to opt out.
-
Show Support: Star this repo to follow along with our daily ships.
Developing
Want to contribute or run locally? See our CONTRIBUTING.md guide.
The relay that pairs the mobile app with a desktop host is also in this repository under
cloud/, with a separate pnpm workspace and setup guide.
Signed Builds
Windows code signing sponored/provided by SignPath.io, certificate by SignPath Foundation.
License
Orca is free and open source under the MIT License.












