Commit Graph
10881 Commits
Author SHA1 Message Date
Jinwoo Hong b0070e3720 refactor(mobile): migrate settings reads to RpcOperation (#20499)
* refactor(mobile): migrate settings reads to RpcOperation

Replay the settings slice on the landed RPC foundation after rebasing onto main.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): refresh task parity snapshots after main rebase

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): correct rebased declaration parity hash

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): account for main task declaration

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): preserve raw RPC rejection timing

Return the transport promise directly and interpret replies separately so sibling Promise.all rejection order cannot change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): refresh parity hashes after timing fix

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): use operation interpreter after raw request

* test(mobile): refresh settings migration parity hashes

Refresh hook and statement parity hashes for the two task declarations whose settings reads now use RpcOperation request and interpretation.

Changed declarations:
- useMobileTasksRuntimeHydration: settings.get replaced by settingsRead request/interpret.
- useMobileTasksWorkspaceCreateActions: settings.get response handling replaced by settingsRead request/interpret.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-13 17:53:58 -04:00
Jinwoo Hong 1f7655f3e3 feat(ai-vault-search): public session search contract and transports (#20277)
* feat(ai-vault-search): define public contract and service seam

* feat(ai-vault-search): add IPC runtime relay and web transports

* fix(ai-vault-search): register search IPC at the core handler site

ai-vault.ts was two lines over the 300-line max-lines limit; the search
handlers belong with the other register*Handlers calls anyway.

* fix(ai-vault-search): withhold degraded-root paths from relay status

Status carried local filesystem paths over the relay while hits redact
theirs. redactStatusForTransport applies the same policy at the same
boundary: relay callers keep each root's reason and the array length as
the count, so the type only makes root optional.

* fix(ai-vault-search): close diagnostic path leak and remove test casts

* feat(ai-vault-search): carry an execution host id and per-host outcomes on hits

* feat(ai-vault-search): route desktop search by execution host scope, including runtimes

* feat(preload): accept an execution host scope on session search

* feat(web): answer only for the paired runtime on session search

* docs(ai-vault-search): describe execution-host routing and the all-hosts merge

* test(ai-vault-search): cover every host scope, the all-hosts merge and wire compat

* fix(ai-vault-search): resume every host mid-page so a merged page never drops a hit

* fix(ai-vault-search): decode the merged cursor with a schema instead of casts

CI's type-aware audit refuses type assertions; a zod record validates the
per-host entries and yields the typed map without one.

* refactor(ai-vault-search): defer cross-host merged search
2026-09-13 17:53:50 -04:00
Brennan BensonandMerge Sim 974af8c0fb fix(worktrees): retire the chat tab of a chat with no child when its workspace goes (#19970)
* fix(worktrees): retire the chat tab of a chat with no child when its workspace goes

Deleting a workspace left a chat tab behind for every structured session that
had no attached provider child at the time, and that tab came back at the next
launch pointing at a workspace that no longer exists.

A provider child is scoped to a VISIBLE pane, not to a tab: the hold that keeps
one is `enabled: isVisible && isWorktreeActive`, and dropping the last hold
evicts the child after the release grace. So the sweep's liveness predicate
selected only "the chat that is the visible pane in the active workspace, or was
moments ago" — which means deleting a workspace from the sidebar while a
different one is active left every chat in the target invisible to the sweep,
and the `live.length === 0` early return did nothing at all.

The durable reference is `visibleSessionIds` in the agent-session record store.
Both purges a removal already performs miss it: the renderer drops
`unifiedTabsByWorktree` and the main process drops the workspace metadata, and
neither touches that index. Startup replays it, restores the session from it and
republishes the tab. Worktree ids are path-derived, so a later workspace created
at the same path inherits the old chat.

Splits the two concerns the sweep conflated in one list. Liveness still decides
what to CLOSE and what to refuse over, unchanged. Membership — the same fenced
record filter minus the liveness clause — decides what to RETIRE, and covers
exactly the complement of the close list so each session's tab is handled once.

Retirement runs from `killAllProcessesForWorktree`, past every point that can
refuse, not from the structured sweep itself: that sweep is joined BEFORE the
unstopped-PTY verdict so a structured refusal can outrank a terminal one, and a
tab retired there would still be ahead of a gate that can refuse the whole
removal — leaving the workspace in place with its chats gone.

* fix(worktrees): retire tabs across all teardown outcomes

* test(worktrees): type teardown fixtures

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-13 14:49:47 -07:00
Brennan BensonandMerge Sim cf20e089d2 fix(native-chat): wait for the runtime capability probe before resolving the creation launch route (#19819)
* fix(native-chat): wait for the runtime capability probe before resolving the launch route

A worktree created before the renderer's hydration-gated capability refresh
runs read the local capability set as null, which
resolveStructuredNativeChatSupport treats as a blocker, silently degrading
structured native chat to the legacy terminal-backed route. Creation submits
now await ensureLocalRuntimeCapabilities(), which probes the local runtime
when no answer has landed yet, so the route resolves on an actual answer.

Fixes #19154

* fix(native-chat): await the capability probe in the work-item direct launch route too

prepareDirectWorkItemAgentLaunch is the fourth creation-flow route owner and
already async; a pre-hydration submit-after-ready launch (fix-checks) read the
unprobed cache as unsupported and silently degraded to legacy. Draft-delivery
launches were unaffected (draft-prompt blocks structured before the capability
check). Same shape as the three creation-submit sites.

* fix(native-chat): keep the capability probe starting synchronously

The broken-bridge hardening wrapped the probe in Promise.resolve().then(...),
which deferred window.api.runtime.getStatus() by a microtask. The session-tabs
restore deliberately overlaps its inventory RPC with this refresh and relies on
the probe already being in flight when refresh returns, so the deferral broke it.

The bridge call is synchronous again; a synchronous throw becomes a rejection
instead, which is what the wrapper was actually for.

* fix(native-chat): hydrate local runtime capabilities at renderer boot

The capability cache's only writer was `useLocalStructuredSessionTabsSync`,
gated on workspaceSessionReady + terminalStartupRestorationReady + the
experimental flag. Every `resolveAgentLaunchRoute` reader treats an
unanswered cache as "unsupported", so the answer arriving seconds late is
what produces the bare-terminal create in #19154 — awaiting the probe at a
route decision guards four call sites but leaves the window open for the
three readers that are synchronous and cannot await.

Start the probe from the renderer boot chain, ungated, so the answer is
cached before any launch route is resolved. The per-call-site awaits stay
as the backstop for the residual window and for re-probing after a failed
probe.

Also: hoist the full-creation probe above its cancel gate so the gate stays
adjacent to createWorktree; pin the retry-after-failure, concurrent-ensure
and missing-bridge contracts; drop a stale microtask tick and correct two
comments that no longer described the code.

* test(native-chat): pin the cancel gate around the capability probe

The probe added an await to two composer creation paths. Full creation had
no gate between the route decision and createWorktree, so the earlier
revision opened a window where a dismissed composer still created a
worktree; the hoist that closed it was unpinned. Quick creation already
gated immediately before runBackgroundWorktreeCreation, so its inline
await is safe — pin that too, since nothing asserted it.

Both tests fail against origin/main (no probe) and the full-creation one
fails against the pre-hoist revision.

* fix(native-chat): close the folder-create cancel window the probe opened

The probe added the first `await` inside `submitFolderWorkspaceCreate`. On
`main` that function ran straight through to `createFolderWorkspace` with no
suspension of its own, so its caller's `isSubmissionCancelled()` gate and the
create call sat in the same turn. With the probe inline, a composer dismissed
while the probe is in flight still creates the folder workspace and launches
an agent — the same defect the full-creation hoist fixed on the git path.

Resolve capabilities in `folder-submit-orchestration` above its existing gate
and hand them down, so the create path's prefix is synchronous again. The
parameter stays optional: a caller without a cancel gate keeps the probe.

Both new tests fail against `origin/main` and against this branch's previous
head; the cancel-window one still fails with its probe-pending assertion
removed, so it pins the create, not just the probe.

* refactor(native-chat): require pre-resolved capabilities on the folder create path

The cancel-window fix in f492064432 left its invariant -- a caller that gates
on cancellation must resolve capabilities above its gate -- enforced only by a
comment, because `hostCapabilities` stayed optional with an inline probe as the
fallback. A future caller that owns a cancel gate and forgets the parameter
would silently reopen the window twice fixed already, and nothing would catch
it: the caller census test pins `resolveAgentLaunchRoute` callers, not this
function's, and `exactOptionalPropertyTypes` is off so even an explicit
`undefined` is legal.

Make it required and drop the now-unreachable inline probe. The sole
production caller already passes it, so runtime behaviour is unchanged: the
old ternary never evaluated its `await` when a value was supplied.

`null` keeps its meaning -- probed, genuinely unknown -- and still degrades to
the legacy route; only absence becomes impossible. The launch-route test that
covered the removed probe is replaced by one pinning that `null` contract with
the cache and the bridge both holding the structured capability, so only the
handed-in value can produce the legacy outcome. The cases in the sibling suite
are not about the route, so they go through one typed wrapper that supplies the
unknown answer rather than repeating it 21 times.

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-13 14:26:27 -07:00
Brennan BensonandMerge Sim ca2356c194 feat(native-chat): decide a restart-stranded send against provider history (#20139)
* feat(native-chat): decide a restart-stranded send against provider history

`markPendingSubmissionsUnknown` flips every surviving `pending` submission
to `unknown` on attach and stops there. The module written to finish the job
describes the intended two-step in its own header -- "Every surviving
`pending` becomes `unknown` and is then matched against provider history" --
and only the first step ever shipped. `reconcileSubmissions` has been
imported by exactly one test file and nothing else.

So a message stranded by a dead child or a host restart had no recourse but
retyping: Retry correctly refuses to redeliver something that may already be
with the model, the outbox entry drops, and a transient error line is all
that remains. This wires the second step, so those are decided on evidence
instead of refused.

Caller placement is the design decision, because where it runs determines
what a consistent history boundary can mean. It runs in `attachJournal`,
immediately after the sweep: attach happens after the record store's CAS
hands this host the lease and before a provider child starts, so nothing can
append to provider history while it is read, and the window stays valid
until the resume consumes it. The three other settlement sites can all be
overtaken by a newly started child before the read is acted on.

The history source is the Claude project JSONL for the handle chain's
provider session id -- definitionally what a resume replays, which is what
makes absence meaningful. Boundary consistency reuses
`proveClaudeTranscriptBranchFromJsonl` rather than inventing a check:
a fork, a compacted log and a truncated tail each already throw there, and
each maps onto `boundaryConsistent: false`. A null leaf uuid is also false,
because there is no anchor to prove a start from.

Two guards were needed that the reconciler cannot enforce itself, because
Claude echoes no client message id and only the fingerprint pass can fire:

- A transcript records a pasted image as base64, and the block decoder drops
  it silently for want of a url or path. Such a record would enter the
  window advertising a text-only fingerprint, where an unrelated text-only
  submission with identical text could claim it. The window now inspects raw
  content parts before decoding and excludes any record a part would be
  dropped from.
- A submission carrying an image-ref path can never match a transcript that
  keeps only base64. Without a guard it matches nothing by construction
  rather than by absence and falls straight through to `not_delivered`, and
  a Retry would then redeliver an image already sent. Only text-only bodies
  are handed to the reconciler.

Both guards fail a named test when removed.

Limits, stated rather than implied. The exact-match tier needs the provider
to echo our id, which Codex does and Claude does not, so Claude resolves by
fingerprint alone -- and two identical prompts deliberately reach
`ambiguous_match` instead of guessing. Repeated one-word prompts therefore
stay unknown by construction. This decides what it can prove and refuses the
rest, which is the intended contract, not a shortfall in the wiring.

Found while doing this and not fixed here: the block decoder silently
dropping base64 images has a blast radius beyond reconciliation and deserves
its own change.

* fix(native-chat): harden restart history reconciliation

* fix(native-chat): keep Claude adapter within lint budget

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-13 13:47:33 -07:00
Brennan BensonandMerge Sim 8999a00281 refactor(native-chat): give each structured dispatch state exactly one meaning (#20133)
* refactor(native-chat): give each structured dispatch state exactly one meaning

`unknown` meant five different things. Only one of them was genuine
ambiguity.

A transport write that the provider's input pump never took is provably
undelivered -- which is what `rejected` already means. It was recorded as
`unknown` anyway, and a one-entry allowlist then existed solely to teach
Retry that this particular `unknown` was safe to re-deliver.

Collapsing that case into `rejected` deletes the allowlist and turns a
predicate into an invariant: Retry never re-delivers an `unknown`, with no
exception to reason about. The four states now each assert one thing --
`pending` written and awaiting, `accepted` the provider has it, `rejected`
provably did not happen, `unknown` genuinely cannot tell.

A fail-closed guard is the right default here because the asymmetry is
severe: refusing a legitimate retry costs the user a retype, while allowing
an illegitimate one sends the model a second copy of their message.

Also fixed, found while auditing every reader of `rejected`:

- The renderer printed `submission.reason` verbatim, so a broken pipe put
  the internal token `provider_write_failed: broken pipe` on screen in
  destructive red. The journal reason is unchanged -- it is the durable
  evidence and the transport-versus-content discriminator -- but the screen
  now gets copy that names the cause and says the message is safe to
  resend. Content rejections still show the provider's own words.
- The fallback copy "Message was not accepted" read as a content refusal.
  A null reason now yields "Message was not sent.", which asserts only what
  every rejection shares.
- A refused worker-start preamble threw a plain Error out of the dispatch
  path. It now throws `OrchestrationError('dispatch_preamble_undelivered')`
  so a coordinator can tell "we could not send it" from "we sent it and
  something else broke" without parsing prose. Retain/discard behaviour is
  unchanged; only the verdict's legibility improves.

Two behaviours improve as a consequence rather than by design: a provably
undelivered message no longer blocks conversation commands, and no longer
leaves the session reading as "working" in chat and in every session list.

Not addressed here, and named rather than implied: a message left `unknown`
by a dead child or a host restart still has no recourse but retyping. The
restart reconciler that would decide those on evidence is written and has
never had a production caller. Parking the refused entry instead would
reintroduce the head-of-queue wedge removed in #19863, so it is not an
option.

Note for whoever edits `journal-reducer.ts` next: it sits at 297 of its 300
counted lines. The next statement added there needs a split, not a shave.

* fix(native-chat): close two gaps review found in the rejection taxonomy

Both are narrow and both were real.

A journal written before a refused write became `rejected` still holds that
submission as `unknown` with the transport marker. The predicate this change
replaced excluded exactly that shape from provider-echo matching; the
state-only check that replaced it does not, so on replay such a row could
claim the echo of a later, genuinely delivered send of the same text and
attach the delivery to the wrong message. Fail-closed still prevented any
re-delivery, so nothing duplicated — but the wrong submission was credited.
Replay now excludes the legacy shape too.

And the content-versus-transport split had a third case neither side covers:
a local capacity refusal is neither the provider explaining itself nor a
frame that failed to leave. It fell through to the verbatim branch, so
`claude structured dispatch queue is full` reached the screen — the same
class of leak this change set out to fix, one reason short of being caught.
Internal reasons now get copy; only a provider's own words are shown as
written.

Each is pinned by a test that fails with its guard reverted and passes with
it restored.

* fix(native-chat): preserve dispatch refusal across clients

* fix(native-chat): rotate immediately rejected retries

* docs(native-chat): correct rejection taxonomy reference

* docs(native-chat): align mobile retry comment

* docs(native-chat): clarify unknown replay semantics

* fix(native-chat): keep a mobile send's operation id when delivery is unknown

Mobile released the retained operation id whenever a send came back
`unknown`, so the user's next send of the same text went out under a fresh
id. A fresh id has no ledger row, so the host treats it as a first delivery
and dispatches it -- even though `unknown` is the one answer that says the
provider may already have the message. That is the duplicate this branch
exists to remove, reintroduced on the client that has no outbox.

Which case that was matters. Mobile only ever sees `unknown` from ack-loss
(`isRpcDeliveryUnknown`: "the host may have processed it and only the ack
was lost"), because the mapper reported every `ok` result as `accepted`
without reading `dispatchState`. So the rotation fired exclusively where
delivery was ambiguous and never where it was provably refused, which is
the inverse of the rule this branch establishes.

Retaining the id is what makes a retry safe, and it costs no liveness:
`performSend` answers a second request under a recorded id from the journal
and never puts it back on the wire, so a reused id delivers when nothing
landed and replays when something did. Rotating can only ever add a second
copy. The retention stays bounded by the host's admission window, which
`retainStructuredSessionOperationId` already enforces.

`retryUnknown` goes with it: the host ignores it for delivery, and all it
does is skip the cached answer to re-read the same row.

Keeping the id exposes what the rotation was hiding, so fix that too: a
replayed `unknown` comes back `ok`, and mobile called it `accepted` and
cleared the composer as if the message had landed. `dispatchState` now
decides, in one pure function:

  accepted/pending  sent, and the id is spent
  rejected          provably did not happen and terminal in the reducer, so
                    reusing the id could only replay that rejection: spent,
                    and the next attempt is a first delivery under a new id
  unknown           keeps its id

Reading `dispatchState` at all is a pre-existing defect, fixed here because
the false "sent" cannot be removed without it, and scoped to the send path.
`mutate`'s rotation for prompt/option/cancel plans is untouched. The
rejection copy is the desktop's notice, so an internal reason
(`provider_write_failed: ...`) still never reaches a person.

Tests: the hook test that was flipped to assert a rotated id now pins the
opposite -- one id across an ack-loss and two `unknown` replays, each
reported `unknown` rather than `accepted`. The send fixture grew the durable
submission row a real host returns; without it every send test asserted
against a shape that cannot express the bug.

* fix(native-chat): enforce fail-closed structured send replay

* fix(native-chat): align retry and mobile RPC contracts

* fix(native-chat): keep transient admissions retryable

* test(tab-bar): expand nested create menu in harness

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-13 13:46:57 -07:00
Jinwoo Hong c6548b98f4 test(scripts): widen the Windows shim ratchet to catch package bin spawns (#20285)
* Widen Windows shim ratchet to detect package bin spawns

Follow local program expressions into node_modules/.bin while preserving the existing literal check, roots, and allow-list. Document static-analysis limits and cover unsafe and resolver-based invocations.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(scripts): fold dot segments before matching node_modules/.bin

The predicate joins call arguments textually, so a literal '..' segment hid a
path that resolves into node_modules/.bin at runtime. Folds '.' and '..' (and
Windows separators) first. A '..' that genuinely escapes .bin still does not
match.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* style(scripts): use .at(-1) in the dot-segment fold

oxlint's prefer-at rule; the repo-wide lint gate is an error, not a warning.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-13 16:08:56 -04:00
Jinwoo Hong bc5e67606f test(rpc): add a compile-time params catalog parity gate (#20281)
* Add compile-time RPC params catalog parity gate

Check each registered handler against its catalog params type in both directions, with explicit exceptions for the three uncatalogued schemas.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(rpc): keep the params generator off its own output

The parity gate imports the generated catalog for types, and it lives under
RPC_DIR, which indexableModules() scans for shared imports. That re-added
OUTPUT_PATH after line 46 removed it, so the generator bundled and require()d
the committed catalog. A catalog referencing a renamed or deleted shared export
then crashed regeneration — in exactly the state that requires regenerating.

Reproduced before and after: with a dangling reference injected into the
catalog, `generate:rpc-params-catalog` threw; it now rewrites the file.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-13 16:08:53 -04:00
Jinwoo Hong fdf16fff70 fix(sidebar): show agent activity before workspace activation (#20398)
* fix(sidebar): observe agent titles before workspace activation

Reuse parked terminal watchers for eligible live tabs in never-mounted workspaces, with initial title catch-up and capability-driven admission. Preserve existing watcher cleanup and avoid allocating watcher sets for empty workspaces.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix: reconcile background watchers when remote coverage changes

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test: type-check terminal watcher fixtures without casts

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-13 15:47:22 -04:00
Jinwoo Hong a1d135e233 fix(ai-vault): re-read a transcript rewritten to its previous size (#20261) 2026-09-13 11:08:12 -07:00
Jinwoo Hong 131d5ab07e fix(mobile): reuse current workspace on notification taps (#20310)
* fix(mobile): reuse the current workspace on notification taps

* revert(mobile): restore notification setting hint
2026-09-13 13:36:12 -04:00
fe4237cd41 fix(agent-hooks): let the provider, not a keystroke, end these turns (#20149)
Escape is ambiguous at the source for Claude, OMP, Pi and Prime Agent: the same
key closes an overlay and cancels a turn, and which one it meant is focus state
only the TUI holds. Nothing downstream can recover it, so for these agents a
plain Escape is never evidence a turn ended — the provider's own hook decides.
Ctrl+C is untouched, and no other agent type changes.

The renderer skips the round-trip and main re-checks the same rule, so a stale
or direct inference request cannot route around it. A navigation Escape does not
clear a Ctrl+C already waiting to settle: Escape is not a retraction.

Fixes #13547
Fixes #9208

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-09-13 00:01:33 -07:00
github-actions[bot] 9a12ccd19d Update README downloads badge 2026-09-13 06:41:10 +00:00
e9065ddd16 fix(runtime): rank tui-idle evidence instead of inferring idle from silence (#20155)
`terminal wait --for tui-idle` returned satisfied in ~0s while an agent was
mid-turn. The shared title detector defaults a name-only agent title to `idle`
so the sidebar can clear a stale spinner, and the wait accepted that stored
value as completion.

Rank the evidence instead. An explicit idle marker in the agent's own title or
a known ready prompt settles the wait; a fresh first-party OSC 9999 status
saying working/blocked/waiting vetoes it; a name-only title is a last resort
that settles only once the stream has also gone quiet. The rank is derived at
read time from `lastOscTitle` rather than stamped onto the record, because
`syncWindowGraph` rebuilds leaves from an explicit field list and would drop a
bespoke provenance field on any renderer publish.

Two things the ranking alone gets wrong are handled here too. A quiet non-shell
foreground process no longer proves idle on a pane where Orca launched a known
agent — that is an agent still booting, and resolving on it is what let
`dispatch --inject` lose the prompt (#9976). And the idle poll re-reads the live
leaf each tick, because a record captured at registration stops advancing and
its frozen `lastOutputAt` makes the quiescence gate pass while the pane streams.

The demotion is scoped to agents that go on to announce rest explicitly. Grok,
Copilot, Aider, Mimo, agy and OpenCode emit their name and nothing more at rest:
a real idle Grok pane repaints its banner about four times a second forever, so
demanding quiescence from it left no settle signal at all and the wait ran to
timeout.

The design is Brennan Benson's, from #14642, which won a cross-review against
#6012, #6555 and this branch's earlier approach; it is ported here only because
that branch shares no git history with main and cannot be merged. Neil's #6555
first drew the explicit-vs-ambiguous line the ranking rests on, and Revofusion's
#6012 first identified that a single title sample cannot prove completion.

Fixes #6011

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Revofusion <syed@moonai.org>
2026-09-12 23:16:59 -07:00
Neil 8759b25e07 fix(automations): isolate the scheduler tick and refuse oversized cron steps (#20152)
Two defects that change nothing about when an existing schedule fires.

#16303: evaluateDueRuns awaited each row with no catch, so one unreadable schedule
skipped every later due automation in that tick. Each row is isolated now; a poison
record writes one folded skipped_unavailable run explaining itself and the tick
continues. A renderer send that throws is closed out as dispatch_failed rather than
mislabelled as an unreadable schedule.

#15895: step validation only checked integer >= 1, so a step wider than its field
degraded silently to a single value and still passed validation. Oversized steps are
refused at input time only, bounded by the count of distinct values a field holds, so
day of week rejects */8 while */7 stays legal.

Runtime parsing stays lenient so rows saved before the gate keep running the cadence
they have. isValidAutomationSchedule now answers only 'acceptable as new input'; a new
isRunnableAutomationSchedule answers 'can Orca still run this', and the editor uses it
so a legacy row opens intact and can be renamed without re-authoring a schedule that is
still firing.

Verified: 34/34 corpus expressions fire identically to main.

Fixes #16303
Fixes #15895
2026-09-12 22:56:23 -07:00
599e669375 fix(skills): evict removed runtime discovery cache (#11489)
* fix(skills): evict removed runtime discovery cache

* fix(skills): retire removed runtime cache entries using pending scan identity

* fix: rescan mounted skill consumers when a runtime re-pairs under the same id

- Fold the pairing revision into useActiveSkillDiscoveryRuntimeTarget's
  selector so a same-id re-pair yields a new runtime target and every
  mounted useInstalledAgentSkillNames effect re-runs instead of holding
  the retired peer's installed list after the module cache is evicted.
- Reset hook-local result/loading state on runtime target identity change,
  which also bumps the refresh generation so an in-flight scan issued to
  the retired peer can no longer commit its result into React state.
- Add mounted-hook regression tests covering the re-pair rescan and the
  in-flight stale-scan fence.

* fix(skills): reset discovery state via render-adjusted state, not a ref write

React Doctor flagged the render-phase write to stateResetInputRef. React can
discard a render after the write, in which case the next render sees "already
reset" and keeps painting the previous target's skill list until a rescan.
Track the reset inputs in useState and adjust it during render instead, which
React replays safely.

Also drop the `as never` / `as GlobalSettings` casts from the tests this PR
added, since main now enforces consistent-type-assertions on changed lines.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-12 22:38:29 -07:00
6c1d95b0da perf(tooling): reuse directory entry types in source scans (#20212)
* perf(tooling): reuse directory entry types in source scans

* fix(source-scan): stat DT_UNKNOWN dirents so untyped directories are still walked

`readdirSync(..., { withFileTypes: true })` can hand back a Dirent whose
type the filesystem did not report. For that entry every predicate is
false, so the readdir-type fast path treated a real directory as a file
and silently dropped its subtree from every ratchet guard. Fall back to
`statSync` whenever the entry is neither conclusively a file nor a
directory, keeping the no-stat fast path for ordinary entries.

Also make the two readdir-order assertions in the walk test
order-independent; `scanSourceTree` returns raw readdir order, which
differs on tmpfs.

* test(source-scan): unit-test the stat fallback via an extracted helper

The fabricated-Dirent readdir mock could not satisfy both gates at once:
vi.mocked(readdirSync) resolves to Node's Dirent<NonSharedBuffer> overload, so
the mock needed a type assertion, and #19462's casting gate rejects new ones on
changed lines. Removing the cast then failed tsc.

Extract directoryEntryNeedsStat and test it directly with a structural probe.
No mock, no cast, no top-level await, and the DT_UNKNOWN case is pinned:
removing the fallback fails 'stats an entry whose type readdir could not report'.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-12 22:38:25 -07:00
Neil f2b6434fe6 perf(ai-vault): bound per-row bookkeeping in unlimited session scans (#20291)
* perf: deduplicate unlimited vault scans once

* perf: release discarded vault aliases during unlimited scans

* fix: bound per-session bookkeeping in unlimited vault scans

- Drop the per-session alias-key string, wrapper object and positions array
  the accumulator retained for every parsed row; index winning positions by
  the row's own sessionId instead (~430 B -> ~45 B per session at 50k rows).
- Add a --expose-gc retention test asserting a 50k mostly-unique load-all
  corpus stays under 128 B of bookkeeping per session while matching
  dedupeCodexSessionsBySessionId exactly.

* perf(ai-vault): bound per-row bookkeeping in CodexSessionCollection

Key winners by the row's own sessionId string so an unlimited scan retains no
alias-key string per live row (301 -> ~115 B/row measured over 50k rows), and
split into a per-alias-key map only for the rare id that spans several hosts,
namespaces, or rollout names, so admission stays O(1). Fold the PR's
CodexSessionAccumulator into the collection main already routes every scan
through, and rerun its scanner-level tests against that single class.
2026-09-12 22:10:30 -07:00
Neil e86cba888b build: reduce native dependency installs to the host platform (#20420)
* Reduce native dependency installs to the host platform

* Remove install policy documentation

* Guard cross-arch packaging and scope release installs to the runner

electron-builder only logs a warning for a missing extraResources source,
so a host-only install silently shipped a foreign-arch slice without its
natives — `pnpm build:mac` on Apple Silicon produced an x64 DMG with no
sherpa-onnx-darwin-x64 and no @parcel/watcher-darwin-x64. The previous
beforePack hook covered only win32.

- Add assertPackagedNativeVariantsInstalled, an arch-aware check over the
  target's sherpa-onnx, @parcel/watcher, and (on Windows) node-gyp addons.
  beforePack now runs it for every platform, with remedies split: another
  architecture comes from install:release, the os:win32 addons need a
  Windows host.
- Drop --os from the release installs. Every packaging job already runs on
  a runner whose OS matches its target, so only the macOS lanes need extra
  breadth, and only on CPU for their x64+arm64 config. Windows and Linux
  packaging return to a plain host-only install.
- Add --frozen-lockfile to install:release so a bare run cannot rewrite
  the lockfile.
- Restore the install policy reference doc and the CONTRIBUTING note, plus
  the rationale comments dropped from the runtime contract test.
- Gate the packaging-closure assertions on whether the Windows addons are
  installed rather than on the host OS, so a cross-arch install exercises
  them off Windows too.
- Make the workflow contract test read `run:` steps as well as retry-action
  commands, and enforce host-only scoping on the non-macOS packaging lanes.
- Remove the unreferenced install measurement script; its numbers live in
  the policy doc.

* Track the install policy doc and index it from AGENTS.md

docs/** is ignored behind a per-file allow-list, so the new reference doc
was only committed via git add -f and future edits would be skipped. Add
it to the allow-list and give it an AGENTS.md entry like every other
tracked reference doc, so the host-only install rule is discoverable
before someone packages a second architecture.

* Route Windows-lane removals through the retrying helper

Adding these four specs to the PR Windows lane pulled them into the
windows-lane-tree-removal-boundary ratchet, which failed on 20 raw
recursive removals. On Windows a bare rmSync races a handle the OS has
not released, throwing EPERM after the assertions already passed and
reporting a green test as a lane failure.

* Adapt the packaging guard to the vendored Windows registry addon

main vendored windows-native-registry as the workspace package
@orca/windows-registry (#20438). A workspace link resolves on every
host, so including it in the installed-Windows-addons checks proved
nothing. @vscode/windows-process-tree is the only os: win32 npm addon
left, so it alone decides whether the win32 resource plan resolves.
2026-09-12 21:25:03 -07:00
Neil df375cdd8a perf(mobile): reuse Linear issue ordering when grouping list and board (#20314) 2026-09-12 21:19:00 -07:00
392583caba perf: skip WSL discovery when filtering native-only paths (#20266)
* perf: skip WSL discovery when filtering native-only paths

* fix: skip the AI Vault running-distro probe on WSL-less hosts

- getAiVaultWslHomeDirs, the sibling in the same Promise.all as the
  native-path filter, still spawned wsl.exe unconditionally on win32;
  gate it on the cached installed-distro list so a host with no distro
  performs no probe when only native Codex homes are configured.
- Hosts with a distro installed keep probing from that sibling, so the
  running-distro last-known-good cache is still warmed by the listing
  and a later probe outage falls back to the observed list, not [].
- Add a test against the real wsl module asserting zero wsl.exe spawns
  across the whole listing Promise.all, plus the warmed-cache fallback.

* fix(ai-vault): gate WSL home discovery on the cached distro list, not a probe

`listWslDistrosAsync()` resolves `[]` when the `wsl.exe` probe is rejected, so a
transient failure made `getAiVaultWslHomeDirs()` conclude "no WSL distros" and
skip discovery. That narrowed the allowed-roots set `ai-vault-delete` and
`ai-vault-subagent-list` validate against, wrongly rejecting WSL-hosted paths.

Gate on `hasCachedWslDistros()` / `getCachedWslDistros()` instead: a pure cache
read that only skips discovery once a successful probe has reported zero user
distros. It also never probes, so the AI Vault listing cannot be the first to
cache `[]` and flip a configured distro to "missing" in runtime resolution.

* test(ai-vault): drop the type assertion tripping the casting gate

check-changed-code-quality runs config/oxlint-code-quality-casting.json with
assertionStyle:'never' over changed lines, and `args as string[]` in the new
wsl-probe spy failed it. Narrow through Array.isArray instead, which is also
honest about execFile's argv being optional.

cached-session-list-wsl-probe + cached-session-list: 9/9 pass; tc:node clean;
changed-code quality gate passes.

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
Co-authored-by: Neil <neil@stably.ai>
2026-09-12 21:18:50 -07:00
81c3d188a4 build(macos): parallelize native helpers with complete cancellation (#19651)
* build(macos): run native module builds concurrently

* fix(build): terminate sibling native builds when one fails

Address coderabbit review: concurrent builds kept writing native
artifacts after a sibling reported failure. Track spawned children,
kill remaining siblings on first nonzero exit, and forward SIGINT/
SIGTERM to all children.

* fix(build): process-group teardown and prefixed output for parallel native builds

Address second coderabbit round:
- Detached process groups + negative-pid kill so SIGTERM reaches swift/
  swiftc descendants, not just the direct pnpm child (they could keep
  writing artifacts after fail-fast)
- Signal handlers preserve the received signal (SIGINT no longer becomes
  SIGTERM for children) and are removed before re-raising, so the parent
  actually dies instead of looping through terminateAll
- runPnpmScript settles only on close, never on error alone, so
  Promise.all cannot exit while children are still running
- Per-module output prefixes ([computer]/[keyboard-layout]/[notification-
  status]) match what the PR description always claimed; interleaved
  swiftc errors are now attributable
- Windows path untouched (early return before any of this runs)

execa/p-limit were considered and rejected: no new runtime deps for a
build script, and detached process groups give strictly stronger cleanup
than execa's direct-child kill.

* fix(build): memoized handler removal and external-vs-sibling signal split

Second-round coderabbit findings on 24392a0:
- Registration now uses the memoized handlerFor() instances so
  removeListener actually removes them (inline arrows were never
  registered, so the parent looped through terminateAll and hung)
- externalSignal is set only by the parent's own signal handlers; a
  sibling's fail-fast SIGTERM no longer masquerades as an external
  signal, so settle() resolves Promise.all with the failing module's
  exit code instead of leaving top-level await unsettled (exit 13)
- Also fixes a TDZ crash: handlerFor() was invoked at registration time
  before the signalHandlers const initialized

Verified: sibling fail-fast resolves failer=7 with no survivors;
external SIGINT kills children then the parent exits 130; real
concurrent macOS build green.

* Wait for native build cancellation before exiting

* Clean up native builds when output streams fail

* fix: bound native build waits, forward SIGHUP, honour output backpressure

- Bound the per-child close wait: two seconds after a child exits, reap
  its process group and destroy its pipes so a descendant that inherited
  stdout/stderr cannot hang `pnpm build:native` forever.
- Handle SIGHUP alongside SIGINT/SIGTERM so a terminal hangup reaches the
  detached compiler sessions instead of orphaning them.
- Pause a compiler's output stream when the launcher's stdout/stderr
  reports backpressure and resume on drain, so prefixed output no longer
  buffers without bound.
- Run build-native-for-platform.test.mjs in the computer-e2e
  mac-native-owner-smoke PR job and trigger that workflow on launcher
  changes; the tests are darwin-only and no other PR job runs on macOS.
- Report the first failing child's status: re-raise its signal, or use
  its exit code instead of Math.max over cancelled siblings.

* fix(native-build): keep output when reap timer overlaps backpressure; fail on ignored re-raised signal

The descendant reap timer started on every child 'exit' and fired even when
'close' was late only because the launcher paused the pipe for its own stdout
backpressure, destroying pipes with compiler output still queued. Arm the
countdown only while the pipes are actually draining: clear it on 'pause' and
re-arm on 'resume' after exit. Write the reap notice to stderr since stdout
is the stream that may be blocked.

Re-raising a child's fatal signal is a no-op when Node ignores it (SIGPIPE),
so set a non-zero exit code first; a failed build no longer exits 0.

Tests: stall the launcher's stdout consumer past the reap timeout and assert
every kernel-accepted compiler line still arrives; kill the computer build
with SIGPIPE and assert the launcher exits 1.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-12 21:18:40 -07:00
Neil bd0f8826ea fix(ci): match the truncated windows-process-tree virtual store dir (#20447)
On Windows, pnpm shortens the virtual store directory to
@vscode+windows-process-tre_<hash>, cutting into the package name before
the @, so the @vscode+windows-process-tree@* glob matched nothing and the
addon recompiled on every Windows job. node-pty escapes this because its
truncation lands after node-pty@, which the glob still matches.

Widening the prefix to @vscode+windows-process-tre* matches both the full
name kept on macOS/Linux and the truncated Windows one.
2026-09-12 21:11:39 -07:00
fc81355fe1 perf: accelerate cancellable remote transcript line scanning (#20351)
* perf: search remote transcript newlines directly

* fix: bound newline search by the yield window so cancellation stays observable

A newline-free segment jumped straight to the next line break, skipping the
character-count yield and its abort checks. Cap each jump at the yield window
and yield there so a large single-line transcript still stops promptly.

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
Co-authored-by: Neil <neil@stably.ai>
2026-09-12 21:05:50 -07:00
Neil 411843f633 fix(ci): cache the vendored addon where node-gyp actually writes it (#20445)
The workspace link means pnpm never creates a .pnpm/@orca+windows-registry@*
entry, so all four native-cache blocks globbed a path that cannot exist and
the addon was recompiled on every Windows job.

Also hardens the addon itself: RegEnumValueW reports a byte count and the
registry does not enforce whole WCHARs for string types, so an odd count let
Napi's auto-length scan run past the value; and a value named __proto__ would
reassign the result object's prototype instead of becoming an entry.
2026-09-12 20:45:42 -07:00
Jinjing 182cd4c2f7 Add code quality lint for type assertions (#19462)
* Add casting code quality lint scan

Enforce type assertion style by adding a new oxlint scan with `typescript/consistent-type-assertions` rule. Requires using `as const`, type annotations, or `satisfies` instead of raw type casts, with documented `SAFETY:` exceptions for unavoidable cases.

* fix minor issue
2026-09-12 20:43:36 -07:00
Neil 62ae09d947 chore(deps): pin serve-sim to an exact version (#20446)
serve-sim ships platform binaries that are bundled into the app, and it
publishes frequently in the 0.1.x range, so a routine install could change
them. The resolved version is unchanged at 0.1.40; only the range is
narrowed, so upgrades become a deliberate edit.
2026-09-12 20:41:02 -07:00
Neil 7e9ade7c74 perf(mobile): reuse Linear grouping between list and board (#20431)
* perf(mobile): reuse Linear grouping between list and board

* test(mobile): realign parity oracle and ratchet with current main

Rebasing onto main surfaced two breakages that the earlier ratchet-only fix
could not have caught, because it was computed against a base main had already
superseded:

- The parity oracle called compareLinearIssues, which #20249 deleted in favour
  of sortLinearIssues. Rewrote the oracle to use sortLinearIssues, matching what
  the production memo now calls, and dropped the stale mock override.
- Regenerated EXPECTED_SCREEN_HOOKS and EXPECTED_STATEMENTS from an observed run
  on the rebased tree. Arity assertions (350 hooks, 417 statements) unchanged.

mobile/src/tasks: 37 files, 295 tests pass.
2026-09-12 20:36:07 -07:00
Neil 2162e31f80 test(relay): differential coverage for the single-pass host-data owner lookup (#20426)
The production change (single insertion-order scan of the session inventory,
reused by the unfenced leg) landed in #20219. This carries the regression
coverage for it: a 1,000-session differential suite that counts iterator visits
and pendingConns.has probes against the pre-change two-find oracle, ordering
under duplicate connection IDs, and attach-ownership tests on the client-accept
path. Folds host-session-owner-scan.test.ts into that suite.
2026-09-12 20:35:58 -07:00
e0e79f1ccd fix(resource-manager): show saved folder workspace names and groups (#20324)
* fix(resource-manager): resolve folder workspace names and groups

* fix: recover local folder PTY attribution after restart

* fix(resource-manager): keep ambiguous-id rows and open folder rows

Ambiguity filtering removed both rows of a workspace-id collision from
worktreeById, so step 3 of the merge dropped browser-only rows for any
id present on two execution hosts. Carry ambiguity as a separate
MergeContext signal that gates only folder host/name attribution; the
existence check and the old repo-level host default are unchanged.

Folder-workspace rows rendered as enabled buttons but navigateToWorktree
resolved only worktrees, so clicks were a silent no-op. Route folder
keys through activateAndRevealWorkspace, which owns host selection and
path-status gating.

* test(resource-manager): repair the merge-call ratchet anchor

The ambiguous-id fix added `ambiguousWorktreeIds` after `worktreeById` in the
mergeSnapshotAndSessions call, so the parity test's end anchor no longer matched:
indexOf returned -1 and slice(start, -1) silently widened the scan to the rest of
the file. The test still passed but stopped pinning the merge call site.

Verified: removing `...resourceSessionBindings` now fails the test again.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-12 20:35:48 -07:00
Neil 5127d1eb3b refactor(windows): vendor the registry addon as @orca/windows-registry (#20438)
* refactor(windows): vendor the registry addon as @orca/windows-registry

windows-native-registry@3.2.2 was last published in 2023 by a single
maintainer. Orca called two of its exports, both read-only, so the whole
dependency is replaced by a local N-API addon under native/.

The vendored addon is read-only by construction: setValue, createKey and
deleteKey are gone, so RegDeleteTreeW no longer ships in the app. Two
upstream defects are also fixed rather than carried over — the name/data
scratch buffers were file-scope statics that concurrent reads would
scribble over, and createKey/deleteKey called .c_str() on a temporary.

Build wiring keeps the existing shape: still an optionalDependency gated
to win32, still excluded from pnpm's allowBuilds so only Orca's own
Windows rebuild runs node-gyp for it, still copied into the packaged
resources. The CI native caches now key on the vendored sources so an
addon.cc edit cannot restore a stale .node.

* test(windows): check the vendored registry addon against reg.exe

The addon is vendored source, so no upstream release proves it still
decodes values the way Orca's PATH readers expect. reg.exe is the only
independent oracle on the box.

* ci(windows): register the registry addon test on the Windows runner

A Windows-gated file self-skips on ubuntu, so without both registrations
it reports success while running on no machine at all.

* fix(build): link the registry addon as a workspace package, not file:

As a `file:` dependency pnpm re-resolved and re-linked the package on
every install, including `--frozen-lockfile` (measured: "added 1" on a
repeat no-op install). That virtual-store churn ran concurrently with
node-gyp reading the same tree and cost @vscode/windows-process-tree its
binding.gyp mid-rebuild, failing package (windows) whenever the native
cache hit and only that module needed building. The linux packaging job
hit the same race from the other side, as a pnpm staging move failure.

A workspace link resolves once and leaves the store alone; repeat
installs are now 55ms no-ops. native/windows-registry is listed
explicitly so `packages:` still does not auto-discover mobile/.

* fix(build): stop tracking node-gyp output for the vendored addon

The build/ tree is generated per host and ABI; the committed copy was
macOS-specific gyp scaffolding from a local build and would have shipped
stale Makefiles to every checkout.

* chore: ignore the vendored addon's node-gyp bin output too

node-gyp also emits bin/<platform>-<abi>/ beside build/; both are per-host
generated output that must never be committed.
2026-09-12 20:10:49 -07:00
Neil 471463f4ce perf(git): normalize tracked discard paths once per operation (#20299)
* perf(git): normalize tracked discard paths once per operation

* chore(git): drop the now-dead tracked-pathspec re-export
2026-09-12 20:05:54 -07:00
Neil 42a2c6510d perf(mobile): release consumed terminal write-queue slots (#20430) 2026-09-12 20:04:32 -07:00
Neil 91143cf7af perf(git): reject non-HEAD refs before sorting remotes (#20429) 2026-09-12 20:04:22 -07:00
Neil 480fc20a7c perf(skills): test agent ownership without building a deduped list (#20428) 2026-09-12 20:04:12 -07:00
Neil 101cdc45f8 perf(relay): sort latency samples once per percentile pair (#20427) 2026-09-12 20:04:01 -07:00
OrcaWinandm4air 90b02cba60 fix(updater): open background check errors from the status bar (#20270)
* fix(updater): open background check errors from the status bar

* docs(updater): describe error disclosure initialization

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-09-12 19:56:46 -07:00
OrcaWinandm4air 0a44b29741 fix(tabs): end drag gestures when the window loses focus (#20323)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-09-12 19:56:29 -07:00
Neil 2d1bd1eb48 perf: bound whitespace normalization for tool previews (#20332) 2026-09-12 19:40:31 -07:00
Neil 7b0701aefa chore: remove 20.7 MiB of duplicate and unused media (#20416)
* chore: remove duplicate and unused documentation media

* chore: guard README local links and refresh tile-01 vendor metadata

- Add config/scripts/check-readme-local-links.mjs: every local src/srcset/href
  in README.md and docs/readme/*.md must resolve to a tracked file. Runs in the
  ungated root_directory_guard job so docs-only diffs (which skip static_analysis)
  still catch a deleted docs-site or feature-wall asset the README embeds.
- Refresh tile-01.recorded-at.json to what vendor-feature-wall-assets.mjs now
  emits for the tab-split source path.
- Drop the pr-19217 evidence prose that cited the removed screenshots.

* fix: accept single-quoted attributes in README local link check

The parser only matched double-quoted src/srcset/href, so <img src='missing.gif'>
was skipped and the guard passed a README that GitHub renders with a broken image.
Regression test fails without the parser change.
2026-09-12 19:40:18 -07:00
Neil fccc887037 perf: skip impossible inline HTML comment matches during encoding (#20293) 2026-09-12 19:40:10 -07:00
Neil 7a440b1c85 perf(mobile): skip successful duplicate connection log saves (#20252) 2026-09-12 19:39:55 -07:00
35a5259ccd perf(android): queue fragmented scrcpy video packets (#20230)
* perf(android): queue fragmented scrcpy video packets

* fix(android): release consumed scrcpy chunk storage

* fix(android): bound queued scrcpy fragment count

- Coalesce pending video fragments once more than MAX_PENDING_CHUNKS
  (1024) are queued, so a large frame delivered in tiny socket chunks
  cannot retain millions of Buffer objects below the 16 MiB byte guard.
- Add a regression test feeding a 256 KiB frame one byte at a time and
  asserting the retained fragment count stays bounded.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-12 19:39:40 -07:00
Neil fee47fdb09 fix(chat): bound journal replay memory by live history (#20247)
* fix(chat): stream journal replay without retaining obsolete revisions

* fix: page journal replay reads so no SQLite snapshot outlives its statement

- iterateJournalEpochRows fetches one completed LIMIT statement per page
  instead of a lazily consumed .iterate() cursor, so reduction never runs
  inside an open read snapshot and a WAL checkpoint can pass mid-replay.
  Regression test: a checkpoint issued from inside the reducer is not busy.
- The retention test now asserts the applyJournalRow spy observed every
  row, so the 8 MiB bound cannot pass vacuously if the spy stops
  intercepting.
- Reliability gate manifest records the new assertion and the paged
  read design.
2026-09-12 19:39:30 -07:00
659f204fec perf: avoid rescanning partial Windows desktop responses (#20235)
* perf: avoid rescanning partial Windows desktop responses

* fix: own the retained serve-channel tail and pin the no-newline invariant

- Copy the retained partial line via ownRetainedString after each drain so
  a 13+ char tail no longer pins the whole drained response as a V8
  SlicedString (measured 23 MB -> 22 KB for 32 pending tails behind 1 Mi
  lines); regression test with --expose-gc.
- Add a test asserting the retained buffer never contains a newline after
  a drain, which the chunk-only fast path depends on.
- Locate the first delimiter with decoded.indexOf offset by the retained
  length instead of rescanning the whole accumulated buffer; test pins
  that no indexOf runs over more than the new chunk.

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
Co-authored-by: Neil <neil@stably.ai>
2026-09-12 19:39:20 -07:00
Neil de7558f095 fix(editor): eliminate multi-second Markdown blank-run scans (#20231)
* fix(editor): keep Markdown blank-run scans linear

* test(editor): guard rich Markdown blank-run performance
2026-09-12 19:39:10 -07:00
b9263eaaf6 perf: reuse pending filesystem watcher debounce timers (#20264)
* perf: reuse pending filesystem watcher debounce timers

* fix: cancel watcher batches after terminal errors

* fix: null the cleared batch timer on last-listener unsubscribe

`Timeout.refresh()` is a no-op on a handle already passed to
`clearTimeout`. `unsubscribeLocalWatcher` cleared `root.batch.timer`
without nulling it, so a re-subscribe inside the teardown grace window
reused the root with a dead handle and `scheduleLocalBatchFlush` never
re-armed — fs change events for that root stopped reaching the renderer.

- Null `root.batch.timer` after clearing it in the unsubscribe path.
- Add a real-timer regression test covering unsubscribe + re-subscribe
  within the grace window; it fails on the previous PR head.

---------

Co-authored-by: Orca Worker <orca-worker@localhost>
Co-authored-by: Neil <neil@stably.ai>
2026-09-12 19:39:01 -07:00
Neil 8641b3af09 perf: remove repeated sibling scans from cyclic agent lineage cleanup (#20302) 2026-09-12 18:49:34 -07:00
OrcaWinandm4air 25a1259d28 perf(ai-vault): count recent sessions for scan cutoffs (#20301)
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-09-12 18:36:57 -07:00
Neil 25d6cd75eb perf(cmd-j): scan phrase word boundaries monotonically (#20282) 2026-09-12 18:36:47 -07:00