Commit Graph
41 Commits
Author SHA1 Message Date
OrcaWinandOrcaWin 7d6603aa17 [comment audit] Complete repository-wide comment cleanup (#33249)
* [comment audit] Align legacy failure-read counts with cached display

* [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci]

* [comment audit] Condense shared generation, path and preview comments [skip ci]

* [comment audit] Condense host, file and Git contract rationale [skip ci]

* [comment audit] Tighten Git lifecycle and provider contract comments [skip ci]

* [comment audit] Clarify shared host authority and launch isolation [skip ci]

* [comment audit] simplify local access mobile and native chat contracts [skip ci]

* [comment audit] simplify native chat presentation and state contracts [skip ci]

* [comment audit] simplify native runtime and provider contracts [skip ci]

* [comment audit] simplify migration and orchestration contracts [skip ci]

* [comment audit] simplify plugin trust and process capture rationale [skip ci]

* [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci]

* [comment audit] clarify remote recovery and launch wire rationale [skip ci]

* [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci]

* [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci]

* [comment audit] clarify sidebar, skill and SSH invariants [skip ci]

* [comment audit] condense structured chat delivery and timing rationale [skip ci]

* [comment audit] condense terminal replay and telemetry invariants [skip ci]

* [comment audit] clarify launch permission and terminal delivery rationale [skip ci]

* [comment audit] clarify workspace ownership and layout contracts [skip ci]

* [comment audit] wait for fitted and painted SSH terminal links

* [comment audit] clarify layout persistence and session ownership [skip ci]

* [comment audit] clarify host routing and workspace lifecycle contracts [skip ci]

* [comment audit] clarify startup persistence and app lifetime comments [skip ci]

* [comment audit] clarify recovery and activity boundaries [skip ci]

* [comment audit] condense activity and artifact invariants [skip ci]

* [comment audit] condense automation authority contracts [skip ci]

* [comment audit] simplify automation scheduling and history contracts [skip ci]

* [comment audit] clarify browser chrome and annotation lifecycles [skip ci]

* [comment audit] clarify browser routing and retained guest ownership [skip ci]

* [comment audit] clarify browser recovery and document ownership comments [skip ci]

* [comment audit] restore hosted runners required by native IME fixtures

* [comment audit] shorten preview, palette and dashboard invariants [skip ci]

* [comment audit] clarify terminal preview, dashboard and retention comments [skip ci]

* [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci]

* [comment audit] shorten rich-text and combined-diff ownership comments [skip ci]

* [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci]

* [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci]

* [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci]

* [comment audit] clarify rich Markdown source and selection safeguards [skip ci]

* [comment audit] clarify editor loading and emulator stream lifetimes [skip ci]

* [comment audit] trim emulator and feature tour narration [skip ci]

* [comment audit] clarify floating workspace and GitHub cache ownership [skip ci]

* [comment audit] trim provider controls and Linear filter explanations [skip ci]

* [comment audit] clarify pairing and chat resume lifetimes [skip ci]

* [comment audit] clarify chat delivery and transcript lifetimes [skip ci]

* [comment audit] clarify session options and host-owned status [skip ci]

* [comment audit] clarify draft durability and live-status precedence [skip ci]

* [comment audit] Trim chat send and model ownership explanations [skip ci]

* [comment audit] Shorten transcript ownership and send recovery comments [skip ci]

* [comment audit] Simplify native chat composer and transcript hook comments [skip ci]

* [comment audit] Shorten native chat scroll and structured session explanations [skip ci]

* [comment audit] Trim workspace picker and onboarding commentary [skip ci]

* [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci]

* [comment audit] Trim sidebar and session history explanations [skip ci]

* [comment audit] Condense session resume and review checks comments [skip ci]

* [comment audit] Shorten checks state and file explorer refresh comments [skip ci]

* [comment audit] Trim plugin bridge and source-control action rationale [skip ci]

* [comment audit] Trim source-control listing and review rationale [skip ci]

* [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci]

* [comment audit] Condense Settings account and appearance rationale [skip ci]

* [comment audit] Trim pairing and repository Settings explanations [skip ci]

* [comment audit] Condense Settings search SSH and terminal safeguards [skip ci]

* [comment audit] Shorten Settings voice and provider ownership comments [skip ci]

* [comment audit] Condense Settings draft persistence and host-state rationale [skip ci]

* [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci]

* [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci]

* [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci]

* [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci]

* [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci]

* [comment audit] Prune focus-read baseline after comment cleanup

* [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci]

* [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci]

* [comment audit] Clarify viewport geometry and skill update safeguards [skip ci]

* [comment audit] Tighten skill inventory, usage and host status comments [skip ci]

* [comment audit] Clarify usage ownership and resource kill safeguards [skip ci]

* [comment audit] Tighten tab selection, rename and search contracts [skip ci]

* [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci]

* [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci]

* [comment audit] Tighten provider loading and terminal recovery comments [skip ci]

* [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci]

* [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci]

* [comment audit] Tighten PTY startup and replay explanations [skip ci]

* [comment audit] Clarify PTY identity and delivery ownership [skip ci]

* [comment audit] Shorten remote reconnect and input replay rationale [skip ci]

* [comment audit] Condense terminal parking clipboard and file link comments [skip ci]

* [comment audit] Clarify IME ownership and trim trace history [skip ci]

* [comment audit] Shorten pane recovery and parked watcher explanations [skip ci]

* [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci]

* [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci]

* [comment audit] Trim renderer lifecycle and menu rationale [skip ci]

* [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci]

* [comment audit] Clarify renderer hook evidence and source ownership [skip ci]

* [comment audit] Tighten IPC ownership and reconnect explanations [skip ci]

* [comment audit] Trim renderer focus, paste and attention comments [skip ci]

* [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci]

* [comment audit] Trim agent launch, resume and ownership commentary [skip ci]

* [comment audit] Shorten account, browser and editor comments [skip ci]

* [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci]

* [comment audit] Condense launch recovery and editor tokenization comments [skip ci]

* [comment audit] Condense workspace selection and terminal pane rationale [skip ci]

* [comment audit] Condense terminal fitting and rendering recovery comments [skip ci]

* [comment audit] Condense terminal input and host routing explanations [skip ci]

* [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci]

* [comment audit] Condense structured launch and terminal link explanations [skip ci]

* [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci]

* [comment audit] Condense session persistence and workspace activation rationale [skip ci]

* [comment audit] Condense workspace routing and browser ownership rationale [skip ci]

* [comment audit] Condense mirror recovery and layout authority rationale [skip ci]

* [comment audit] Condense runtime transport and file safety rationale [skip ci]

* [comment audit] Condense runtime graph and structured session rationale [skip ci]

* [comment audit] Condense browser creation and remote tab recovery rationale [skip ci]

* [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci]

* [comment audit] Condense frame, startup and provider cache rationale [skip ci]

* [comment audit] Condense host twin and agent lifecycle rationale [skip ci]

* [comment audit] Condense browser authority and editor cleanup rationale [skip ci]

* [comment audit] Condense editor buffer identity and review cache rationale [skip ci]

* [comment audit] Condense provider lifetime and repository ownership rationale [skip ci]

* [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci]

* [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci]

* [comment audit] Condense cleanup ownership and listing admission rationale [skip ci]

* [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci]

* [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci]

* [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci]

* [comment audit] Condense cloud rollout and assignment safety rationale [skip ci]

* [comment audit] Condense relay session and database recovery rationale [skip ci]

* [comment audit] Condense relay placement and protocol compatibility rationale [skip ci]

* [comment audit] Condense build and packaging constraints [skip ci]

* [comment audit] Condense CI gates and validation isolation rationale [skip ci]

* [comment audit] Condense catalog identity and browser validation rationale [skip ci]

* [comment audit] Condense mobile browser and frame-budget rationale [skip ci]

* [comment audit] Shorten mobile route and browser test explanations [skip ci]

* [comment audit] Condense editor and session seam test rationale [skip ci]

* [comment audit] Trim session and terminal closure history [skip ci]

* [comment audit] Shorten terminal browser and census explanations [skip ci]

* [comment audit] Preserve root-only E2E diagnostic baseline

* [comment audit] Condense mobile seam and HTML preview test rationale [skip ci]

* [comment audit] Condense preview instrumentation and render harness rationale [skip ci]

* [comment audit] Condense bundle contracts and native-build constraints [skip ci]

* [comment audit] Shorten native packaging and CI routing rationale [skip ci]

* [comment audit] Condense release tooling and benchmark constraints [skip ci]

* [comment audit] Condense packaging and platform gate rationale [skip ci]

* [comment audit] Shorten mobile route and history rationale [skip ci]

* [comment audit] Condense mobile browser and component rationale [skip ci]

* [comment audit] Shorten drawer and workspace-form rationale [skip ci]

* [comment audit] Trim mobile review and editor-document explanations [skip ci]

* [comment audit] Condense editor lifecycle and Markdown explanations [skip ci]

* [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci]

* [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci]

* [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci]

* [comment audit] Condense bridge stream, native request and audio rationale [skip ci]

* [comment audit] Reduce bridge schema and page-client commentary [skip ci]

* [comment audit] Tighten mobile page bridge contracts [skip ci]

* [comment audit] Clarify mobile cache and session constraints [skip ci]

* [comment audit] Condense mobile route and lifecycle rationale [skip ci]

* [comment audit] Tighten mobile shell hook timing rationale [skip ci]

* [comment audit] Clarify route handoff and notification compatibility [skip ci]

* [comment audit] Clarify push registration and dictation cleanup constraints [skip ci]

* [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci]

* [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci]

* [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci]

* [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci]

* [comment audit] Tighten stream replay and queued session ownership rationale [skip ci]

* [comment audit] Condense mobile composer and reply-schema constraints [skip ci]

* [comment audit] Clarify mobile transcript recovery and session ownership [skip ci]

* [comment audit] Clarify mobile queue and source-control compatibility [skip ci]

* [comment audit] Condense storage and task launch constraints [skip ci]

* [comment audit] Clarify task schemas and terminal document ownership [skip ci]

* [comment audit] Tighten terminal input and viewport rationale [skip ci]

* [comment audit] Shorten terminal and RPC recorder contracts [skip ci]

* [comment audit] Condense recorder engine and connection contracts [skip ci]

* [comment audit] Shorten host credential and relay recovery comments [skip ci]

* [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci]

* [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci]

* [comment audit] Condense catalog and relay entry contracts [skip ci]

* [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci]

* [comment audit] Condense WSL relay and E2E regression rationale [skip ci]

* [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci]

* [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci]

* [comment audit] Shorten layout oracle and regression explanations [skip ci]

* [comment audit] Condense paired recovery and ownership regression contracts [skip ci]

* [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci]

* [comment audit] Condense terminal rendering and IME regression rationale [skip ci]

* [comment audit] Shorten terminal recovery and benchmark contracts [skip ci]

* [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci]

* [comment audit] Condense workflow routing and release rationale [skip ci]

* [comment audit] Condense mobile security and installer contracts [skip ci]

* [comment audit] Shorten native rationale and stylesheet workarounds [skip ci]

* [comment audit] Shorten Windows visibility probe rationale [skip ci]

* [comment audit] Condense embedded release and signing rationale [skip ci]

* [comment audit] Retain current main test behavior in final reconciliation [skip ci]

* [comment audit] Complete final comment review across current source inventory

* [comment audit] Handle consolidated audit reports before final CI

* [comment audit] Preserve credential storage census detail [skip ci]

* [comment audit] Scale broad changed E2E selections with verified timing shards

* [comment audit] Align E2E assertions with browser close and settled session metadata

* [comment audit] Match existing E2Es to current resource and windowing contracts

* [comment audit] Observe the shared server outage notice in reconnect coverage

* [comment audit] Make existing renderer probes measure settled tab and PTY work

* [comment audit] Align palette E2E selector with task search

* [comment audit] Refresh remaining palette search selectors

* [comment audit] Keep selector correction within selected audit specs

* [comment audit] Await emoji fixture settings inside renderer evaluation

* [comment audit] Cancel fixture fit retries between terminal tests

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-25 04:05:46 -07:00
Neil b16a71c7fb ci: reuse Windows hook builds and shorten test setup and shutdown (#29439)
* ci: remove Windows build and persistence test overhead

* ci: collect unit phase diagnostics during PR checks

* test(ci): seed hook reuse fixture from the canonical prebuild

The hook reuse regression first built into a missing private --output, so it
always reached Cargo (74s warm on Windows). Build through the default output
instead, which reuses a warm canonical prebuild, then copy its executable and
fingerprint into the private fixture root.

* fix(lint): clear inherited max-lines and broad object tab parameter after main merge

Combine the duplicate shared/execution-host imports in runtime-host-services
(301 -> 300 lines) and type the overlap test's tab mapper with the
RuntimeMobileSessionClientTab owner type instead of object.

* fix(ci): align inherited runtime and workflow contracts

* test: establish changed runtime probe signatures deterministically

* test: await asynchronous daemon profile cleanup

* test: shut down owned daemons before deleting profiles

* test: clean up oracle daemons during final app close

* test: recognize verified Linux zombie exit during daemon teardown

* fix: preserve folder SSH ownership when project groups conflict
2026-10-16 02:52:22 -07:00
09aff1b94d perf(usage): keep per-source usage caches off the main thread (#26980)
* perf(usage): keep per-source usage caches off the main thread

The usage stores kept every provider's per-source scan cache
(processedFiles / processedDatabases) inside the one JSON file main
loads at startup. On a large Claude history that file is 56 MB, 43 MB
of it per-source records only a scan reads, so main paid a sync
read+parse at launch, a structured clone of the whole cache to and
from the scan worker on every scan, and a pretty-printed rewrite
after every scan.

The usage scan worker now owns those records in a sidecar
(`<cache>-sources.json`): it reads it before a scan, writes it after
(compact, durable rename), and returns only sessions and daily
aggregates. Main keeps and writes only that report, compact. A cache
still carrying inline records is split on the worker the first time
it is loaded (above 8 MB); smaller ones parse on main and rescan cold.
Sync IPC getters and automation lookups wait for the load.

Synthetic 56 MB cache, same machine, interleaved runs:
- first launch: 108 ms sync parse -> 18 ms main busy, 4 ms longest block
- later launches: ~69 ms -> ~7 ms
- forced scan, longest main-thread block: 89 ms -> 11 ms
  (real built app, headless: 84-94 ms -> 11-13 ms)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(usage): preserve cache attribution and legacy warm scans

* fix(usage): reclaim cache temps after worker retirement

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
2026-10-09 17:19:29 -07:00
Brennan Benson a551aa5fe6 OpenCode structured chat over the Agent Client Protocol (#25845)
* refactor(native-chat): Grok's registration declares where it runs; ACP no longer borrows Codex's location rule

The rule a self-supervised agent child runs under (this machine, no WSL, Windows only with process
start-time proof) is its own module that Codex and the ACP adapter both use. Grok's registration
takes the full account-home resolver signature, and D3's tests build hosts with the agent registry.

* fix(native-chat): Grok follows the ACP runtime's request contract and the managed process's close

A request the agent or a Stop cancels is answered with the agent's own cancelled reply by the code that
owns it (the runtime no longer answers a silent handler), so a Stop needs no separate decline pass. A
permission answer still being saved when the agent stopped waiting is reported unconfirmed, since the
protocol already answered it cancelled. Cancelling the agent's own turn is the plain cancel. Request
rows are matched under their generation-scoped ids. A refusal's reason comes from the dialect's wording
path. The child drops its own stderr tail and close policy for the managed process's, and a close
whose process tree was not proven gone is reported as the adapter contract asks.

* fix(native-chat): a Grok chat Orca already holds resumes without writing what Grok replays

A chat with a saved Grok session reattaches with session/resume where the agent offers it, else
session/load. Either way the call runs inside the translator's load window, so what Grok sends while
it reattaches (its saved exchange, a task the dead process left running, ended by the restart) opens
no turn and writes no row; only context usage reads on. A reply an Orca or Grok crash cut short is no
longer completed from Grok's saved history: it reads like a Claude or Codex chat's, with the existing
notice. The attach window also closes after a failed attach, and a created session that session/resume
reports missing is replaced like one session/load reports missing.

The replay reconciliation is removed: the lane no longer reads the journal, and D3's replayed-input
grammar test and completed-turn check in the assembler go with it.

* refactor(native-chat): a failed Grok reattach needs no window close of its own; its lane is replaced

* test(native-chat): D3's merged tests use the shipped declarations and the launch options main requires

* fix(native-chat): typecheck fallout of the base merges; any agent's empty chat is reusable

Main's idle-empty-chat lookup and launch join now take any registered agent, as the rest of the
launch path does. The refusal check moved into the prompt turns and the prompt-block conversion beside
the turns that send it, keeping both files in their line limit.

* fix(native-chat): a Grok Stop ends the process once Grok settles its turn; the next send resumes

Grok's session/cancel ends only the running turn: work it already moved to the background keeps
running and can begin a turn of its own after the person pressed Stop. Stop is now a session
boundary, as it is for Claude: the cancel answers open requests and lets Grok end the turn, the host
waits a bounded grace for that, then ends the process; the next send relaunches and resumes.
The adapter's own bounded close of a turn Grok began is gone. Its named-turn check stays: the host
ends the session unless the provider declines a Stop naming a turn that has since ended.

* test(native-chat): a Grok Stop ends the process only after Grok answered the cancel

* fix(native-chat): Steer on a Grok card cancels the running prompt, then sends it

A send that reached Grok while a prompt ran was held in the adapter until that turn ended: Steer
on a queued card took the card out of the host's editable queue and meant 'send after this turn'.
It now cancels the running prompt (session/cancel; the session stays) and sends as the next prompt
once Grok answers the cancel, as the common pattern does; a steer behind another cancels it in
turn, so the last one runs. The adapter holds a send only while that cancel lands, so its general
held-send queue and its holdsDispatch report are gone (every send it holds has its turn open in
the journal). An older client's mid-turn send takes the same path. capabilities.steering is
unchanged and still unread.

* refactor(native-chat): a close or Stop cancels a start through the acquire's own abort signal

The host owns the acquire it runs, so it now owns its cancellation: each attach's acquire gets an
AbortSignal, aborted from outside the session's queue by a close and by a Stop admitted now (the
same admission rule as before). The optional abandonStart adapter hook, the router's fan-out to
every adapter and the ACP adapter's session-keyed start map are gone; the ACP adapter keeps an
unkeyed set of starts only so quit can prove their children gone, and keeps a failed start's
unproven child until its exit is proven.
The hook also let a later close ask that child again. The host now does that from state it holds:
a close of a chat with no live child whose record still names an owner process with no death
evidence asks the adapter to release it. The answer is not recorded as proof (the lease probe
does that), so an owner pid an earlier Orca left is never killed or marked gone. Claude and Codex
ignore the signal and hold no such child; their release is a no-op (tested).

* fix(native-chat): a Grok crash that closes stdout before its exit still ends with Grok's last words

On macOS and Linux the agent's stdout ends before its exit is observed, with or without the
supervisor's EOF forwarding, so the connection's loss closed the journal first and its error text
became the session's ended reason, dropping Grok's stderr. The reason is now read at the proven
exit: the agent's last words when it left any, else why the connection closed. The failure already
carried them. Comments that assumed the exit comes first, that early frames past the cap refuse the
start, and that dispatch re-checks image support are corrected.

* fix(native-chat): nothing Grok sends while a held chat reattaches is written, marked as replay or not

The reattach window relied on the dialect's replay verdict, and Grok's frames read as live unless
they carry isReplay, so an unmarked chat frame during session/resume opened a turn that never
ended. D3 now marks every frame inside the window as replay before the translator reads it, so the
translator keeps only context usage whatever the agent marked; options and commands are still
adopted. The translator's load semantics are unchanged.

* test(native-chat): a Stop after a resume finds no turn an unmarked old reply opened

* test(native-chat): a resumed Grok chat keeps its last context reading; the resume refreshes only the window

* test(native-chat): a Grok background task a Stop ended reads as stopped reporting

* refactor(native-chat): quit's stop of each start answers through one promise kind

* fix(native-chat): quit aborts every start the host has in flight before draining attaches

A Grok that never answered its handshake held quit until the start's own 60 s bound, past the
20 s quit deadline. The host's teardown now aborts each in-flight acquire (and any the drain
still begins), so the adapter's own quit controller and its map of starts are gone: a start
has one canceller, the host's signal.

* fix(native-chat): a Grok start's abort stops reaching its child once the start has returned

The listener stayed on the host's signal until the attach finished committing, so a Close in that
window killed the now-live child behind the host's back and it read as Grok crashing. The start
now detaches it when it ends; a later Close goes through the session's own stop.

* fix(native-chat): a close or Stop during any attach phase stops the start before it launches

The attach began its abort controller only after reconciling leases, resolving recovery and
probing the previous owner, so a close or admitted Stop in those phases reached nothing and Grok
launched anyway. The controller now begins first, and the acquisition checks it before asking the
adapter to start.

* test(native-chat): a close during the attach's owner probe asks no adapter to start

Also renames the close test after the hook it no longer exercises.

* test(native-chat): a close's re-ask closes a Claude or Codex child a failed cleanup left

The re-ask is not a no-op for them: when the adapter still holds the child its cleanup could not
prove gone, the close stops it again as a requested close, and Claude persists the handle of the
conversation it ran so the next send resumes it. Corrects the tests' and comment's wording; the
close awaits the re-ask, bounded by each adapter's kill ladder.

* fix(native-chat): Steer during a turn Grok began itself cancels it and sends once it ends

A send while Grok ran a turn of its own (a background task waking it) went straight to Grok, which
queued it behind that turn where Orca could no longer withdraw it, while Stop treated the same turn
as the running reply. The send now waits as a steer, the turn is cancelled once, and the message
goes when the turn ends; a Stop withdraws it and an exit rejects it as never sent.

* test(native-chat): a steer whose cancel Grok never answers ends Grok and is rejected as never sent

Pins the bounded steer cancel kept from the runtime: past the bound the connection closes, the
running reply reads unverifiable, Grok's end reads as its exit, and the waiting steer is rejected
as never sent.

* fix(native-chat): a Grok crash stays a crash when a stop lands before its exit is proven

After the connection broke and the close could not prove Grok's exit, any later stop Orca asked
for (the next start, a Stop, a Close) marked the child as closed by Orca, so the crash read as a
requested close and Grok's last words were dropped; a send meanwhile was recorded unconfirmed.
The connection loss now decides the cause, and a send on that session is rejected as never sent.

* test(native-chat): fixtures this PR's registered Grok and desktop capability made stale

CI's unit shards failed on tests outside the PR's own lists. Each encodes something this PR changes
on purpose: Grok is now a registered agent (the seam test's unregistered agent is now Cursor); the
desktop now advertises registered agents (the restart-offer tests' older client drops that
capability explicitly); the attach context carries the start's abort controllers (the forget-status
double gains them); and the ACP real-host test rig sends to the host directly (listed beside the
other real-host rig in the send ratchet).

* fix(native-chat): a start quit stops is not the queued message's start failure

With quit now aborting a start it would have waited for, the delivery step recorded the aborted
start as the message's failure ("couldn't restart"). After quit has stopped delivery, the step
leaves the message to quit, which settles it as a close does ("The chat closed before this message
was sent."). The test that pinned quit waiting for that start and stopping its child now pins that
nothing is launched behind quit.

* fix(native-chat): a message sent after a Stop or close aborted a start gets its own start

A start the host aborts (an admitted Stop, a close, or quit) returned its refusal to the delivery
loop, which then rejected whatever was queued at that moment with "couldn't restart", including a
message the user sent after the Stop. The attach now reports that the host aborted it, and the loop
re-derives from the journal instead: what the Stop or close withdrew is already settled, a message
accepted since gets a start of its own, and quit's next step stops the loop. This replaces the
quit-only carve-out with the same rule for every abort and every agent.

* test(native-chat): the message sent after an aborted start is answered, so no settlement outlives the test

* fix(native-chat): a Grok model pick Grok never answers no longer holds Stop or Close

The pick runs on the session's queue. It now registers in the host's out-of-queue
abort registry beside a start, so a close, an admitted Stop or quit abandons it, and
the ACP adapter bounds it at 30 s like Claude and Codex. A late answer is still adopted.

* fix(agent-launch): a phone's launch opens a terminal for an agent whose chat it cannot show

agent.launch now reads the caller's capabilities by the rule tabs and restart offers
use (clientRendersStructuredAgent). A phone without registered-agents.v1 gets Grok as
a terminal again, as on main; the host's own callers and desktop clients are unchanged.

* fix(acp): strip every agent hook variable from the ACP child, from the shared list

ACP_CHILD_ENV_TO_DELETE was a second copy of the hook runtime keys that missed
ORCA_AGENT_HOOK_TRANSPORT; it now spreads AGENT_HOOK_RUNTIME_ENV_KEYS beside the pane
identity keys.

* refactor(native-chat): the mutation context carries the provider-wait registry itself

Keeps the host file within its line limit; one field instead of two closures over it.

* fix(agent-launch): agent.launch.v2 still vouches for Claude and Codex chats

The caller rule from the previous commit also turned Claude and Codex into terminals
for a client advertising only agent.launch.v2, whose contract says it opens a chat
(mobile retry-authority tests). Only an agent beyond those two now needs the client to
read it (clientRendersStructuredAgent); the test fixtures go back to what they were.

* refactor(native-chat): drop saved-history adoption from the timeline assembler

The common pattern discards the history a provider replays while loading a
session, so the assembler has no use for an input.history event.

* refactor(acp): drop session/load history adoption from the translator

The common pattern discards the history an agent replays during session/load,
keeping only what it says about the context window. Remove the adoption path
(acp-history-adoption.ts, the adopt option, and the historical background-task
liveness rewrite it fed) so load replay is always dropped except usage.

* refactor(native-chat): a pending input is only Orca's send now

Review follow-up to the adoption removal: drop the comment naming the
provider's saved message, and make requestedAt required since every pending
input comes from input.accepted.

* test(acp): keep the task-result status table on live frames

Review follow-up to the adoption removal: the result-status mapping was only
tested through adopted history, so run the same table on live frames, and
cover an unmarked task notice during a load being dropped.

* test(acp): a frame helper for a shell command Grok is running

* fix(acp): a Grok crash settles through the host's provider-exit batch, scoped to the turn it ended

A Grok crash ended the journal unverifiable before the adapter reported the exit, so the host's
provider-exit settlement found no running turn and wrote nothing: the adapter's failure (with
Grok's last words) never reached the journal, and a later stale-session pass wrote a bare,
thread-scoped cut-short row, so the partial reply was not folded as Claude's and Codex's are.

At a proven exit the ACP lane now ends its running turn interrupted at the exit instant, as the
host's exit contract expects of a child's own translator (Codex's does the same). When Grok's
stdout closed first (every POSIX crash), the turn is unverifiable only until the exit is proven:
the host's provider-exit settlement now takes the exit as proof naming the child's fence and
revises what that child left unverifiable in the same batch, with the turn-scoped row and the
adapter's failure. Claude and Codex write no unverifiable turn of a live child except a command
whose hand-off is in doubt; that turn is now revised at the exit instead of at the next open.

* test(acp): a crash seen first leaves the host no Grok turn to revise

* refactor(native-chat): what a gone generation left unfinished gets its own module

The settlement file passed 300 lines with the exit-proof revision. The unfinished-work reads
(capture, interrupted-by-the-exit, in-progress) are their own concept and move out unchanged,
apart from the exit proof they now take.

* refactor(native-chat): a watched exit revises what its child left unverifiable without reading Stop marks

An exit's own instant is the turn's end, so the revision needs only each row's fence: the
settlement's journal type gains itemFence alone, and the host test fakes say so.

* test(native-chat): drop the duplicate itemFence on the fake that already had one

* test(claude, codex): an exit whose stdout ended first still reports as it always did

The provider supervisor now ends Orca's stdout when the agent's ends, so on every crash EOF
arrives before the exit is seen. Claude's and Codex's connections report nothing at EOF and
report the exit, with its usual reason, once it is seen.

* fix(acp): reopen a chat with session/load, as the common pattern does

An agent that offers both now reloads its session instead of resuming it; the
reattach window still discards what it replays except context usage.

* fix(acp): drop the 60 s handshake bound; an abort fails the start's waits at once

Neither common design bounds an ACP handshake: Close, Stop and quit end a start
that never answers. The abort now also closes the connection, as a kill there
does, so the start settles even before the child's exit is proven. The
host-stopped start refusal only this bound produced goes with it; the idle
sweep keeps its words.

* fix(acp): a Stop naming an ended turn follows Claude's rule

It still stops nothing while another turn is live, but in the gap before a
follow-up's turn opens, which no client can name, it now stops what is in
flight and the session ends, as a Claude Stop does.

* fix(native-chat): a close no longer re-asks a failed start's unproven child

Neither common design retries that stop at Close, and Orca's Claude contract
re-asks only at the next start and at quit. The ACP adapter keeps the child
until its exit is proven and asks it again there, as Claude does.

* fix(acp): a message sent during a turn the agent began itself goes at once

Both common designs send it straight to the agent with no cancel; only Orca's
own running prompt is steered (cancelled, then re-prompted).

* test(native-chat): dismiss-all through a remote client's audience keeps a newer Orca's offer

Uses an audience production sends (one that cannot show every agent), per review.

* fix(acp): launch Grok as `grok agent stdio`, without the update and leader flags

The common pattern passes neither --no-auto-update, --no-leader nor
GROK_DISABLE_AUTOUPDATER; full access still adds --always-approve.

* fix(acp): an agent that ends its stdout, or answers unreadably, is not a lost connection

As in the common pattern, only a broken stdin (or Orca's own close) ends the
agent; one that closed its output but can still be written to stays until a
Stop, a close or its exit. The provider supervisor goes back to its base
content, so Claude and Codex no longer get the forwarded stdout end either.

* fix(native-chat): a person's close joining a failed one still binds the turn its child end cuts

On main every close of the chat writes its own Stop and settle. Here a later close joins the
first and writes no row, and the first's settle closed when its kill failed, so a turn that opened
in between and was cut by the next close read as failed. A person's close joining a person's close
whose Stop opened a settle now reopens that settle until its attempt is done.

Tests: a close whose kill failed still closes its settle; a turn opened between a failed close and
the next reads as the person's cancellation (each fails without its half of the fix).

* test(native-chat): Grok opens as a chat only behind the structured-chat setting

agent.launch and orchestration worker-start read the same setting as the
renderer route; pin both states for Grok on each. The setting's description no
longer names only Codex and Claude, in every catalog.

* docs(acp): generic ACP comments say what holds for every agent, not Grok

Stop ends the session for every ACP agent, as in the common pattern; the
adoption hook comment goes (adoption is not planned); a failed start's child is
retried at the next start or quit.

* test(claude, codex): type the EOF-before-exit test's streams; the supervisor no longer forwards EOF

The Claude test wrote to the child's stdout and stderr through their Readable
type, which the node typecheck rejects; it now holds its own PassThrough
streams. The comments no longer credit the reverted supervisor change.

* feat(acp): a steer's cancel asks once and never ends the agent

The runtime had one cancel: send session/cancel, wait at most 10 s for Orca's prompt to settle,
then close the connection, which ends the agent. A steer used it too, so a slow agent lost its
process just because the person added a message. requestSteerCancel() now sends session/cancel
once per prompt, cancels the agent's open requests and answers later permissions cancelled, and
never bounds or closes: the prompt's own reply ends it and the steer's prompt follows. cancel()
stays the Stop: bounded, then close. A Stop after a steer still bounds and closes. Both cancel
paths move into acp-prompt-cancel.ts over one cancel channel.

* chore(native-chat): keep the record store and recovery capsule under max-lines after the main merge

* fix(acp): a repeated steer shares the cancel in flight; say what the caller owns

Per review: a second steer before the first write lands returns that write instead of resolving
early. The steer's JSDoc says the wait for the prompt's reply is unbounded and that a prompt that
fails instead must not take the steer until the caller rebuilds the session; the Stop's says a
prompt that settles in time leaves the agent for the Stop's owner to end. The steer test now gives
the runtime a handler that would allow: the open permission's signal aborts and the late one never
reaches it.

* fix(native-chat): drop the stopDelivery the A3 merge doubled

* fix(acp): a steer's cancel asks Grok once and never ends it

A steer now uses D1's notify-only cancel. Two messages sent during a reply Grok began itself
cut that reply, as the common pattern does, and then both run; before, the queued first
message could not answer the bounded cancel and Orca ended Grok although Grok answered.
A Stop keeps the bounded cancel and its 4 s grace.

* fix(acp): a permission Grok asks with no prompt of Orca's running is declined

During a turn Grok began itself nobody asked it to act, so the request is answered
cancelled at once instead of opening a card that waits, as the common pattern does.

* fix(acp): a Grok that dies while starting is reported with its own last words

A dying process's stdout ends before its exit is seen, so the start failed as a closed
connection and Grok's stderr was lost. A start whose connection closed now waits, bounded by
the Stop grace (or a Close/Stop), for the exit before it is told.

* test: a Stop after a steer sends its own cancel; drop the import the A3 merge doubled

* test(native-chat): main's Stop-note test builds its turn context with the agent registry

* test(claude): say why the close test's fake child cast is safe

* test(native-chat): build the Stop-opened-turn test's identity and turn context the current way

The test (#25056) landed before the opaque provider handle (#24991), so main still built
the old {kind, threadId} handle; the turn context also needs this branch's agent registry.

* test(native-chat): build the Stop-opened-turn test's identity with the opaque handle

The test (#25056) landed before the opaque provider handle (#24991), so main still built
the old {kind, threadId} handle.

* test(ratchet): require src/main/provider-process now that it has landed

* test(native-chat): keep main's opaque-handle import in the Stop-opened-turn test

Main's #25706 and this branch both added the import at different lines; the merge kept both.

* test(native-chat): keep main's opaque-handle import in the Stop-opened-turn test

Main's #25706 made the same fix as this branch at a different line; the merge kept both imports.

* feat(native-chat): record a fresh provider conversation that replaced one the agent could not restore

A chat whose saved conversation the agent cannot reopen can now continue in a
fresh one: the handle chain records the new conversation as a creation that
replaces the lost one (which, why, and when), keeping every earlier link.

Rows keep a shape older builds read: the stored chain starts at the latest
replacement and carries the earlier links inside it.

* test(native-chat): build this stack's journal identities with main's opaque provider handle

Main's #24991 replaced the {kind, ...} handle with {transport, agent, nativeId}; three test
files from this stack still wrote the old shape. Same lines the downstream ACP branch uses.

* docs(acp): every reattach drops the agent's replay, not only for a chat the journal holds

* refactor(native-chat): store a replaced conversation flat; refuse it where older builds read the row

Older builds only read Claude and Codex records, so the nested stored form
protected rows no replacement can reach while adding a cap mismatch after a
downgrade. Store the chain as held, refuse a replacement in a Claude or Codex
chain until one has a stored shape older builds read, and refuse a supersession
key on a replacement that names no creation in the chain.

* refactor(native-chat): read hosts' structured agents from the app-shell services

Main grew the startup hydration hook to its line limit; the host agents sync is an app-lifetime subscription like the structured session tabs sync beside it, so it moves there.

* Use current provider handles in transition tests

* Use current provider handles in timeline fixtures

* test(native-chat): prove replacement rows survive downgrade and re-upgrade

* Require the ACP directory in the runtime import check

* test(ratchet): require src/main/acp now that this PR lands it

* feat(acp): a saved session the agent cannot reopen continues in a new one, with one warning row

When session/load (or session/resume) of a saved ACP session fails, the chat starts a new session and records it as a creation that replaces the lost one (#25747's 'replaces' link), and writes one warning row that the agent no longer remembers the earlier messages. A created session the agent reports missing is still superseded silently; a signed-out agent or a start that is over (Close, Stop, a lost agent) still fails the start.

* chore(acp): rewrap the acquire header comment

* test(acp): a start closed while the agent reopens fails without opening or announcing a new session

* Let ACP connections own their supervised agent process

* Preserve ACP cleanup evidence and isolate exit observers

* Expose ACP cleanup observations and type the permission fixture

* refactor(native-chat): the registered-agents capability lives in its own module

Main's growth put protocol-version.ts one counted line over its 300-line limit once the capability
was added; like main's other per-feature capabilities, it now has its own module, and importers read
it from there.

* refactor(acp): one connection owns the Grok process and its protocol

D3 now opens each ACP agent through createAcpAgentConnection (ACP-ALIGN #25810): one object spawns the
process on the execution host, owns its stdio and protocol, and reports its proven exit. It is built and
tracked before the handshake, so a start's abort (Close, Stop, quit) still reaches it, and a failed start
keeps that same connection for the next close to retry rather than spawning another process.

Deleted: the spawnAcpStructuredChild wrapper and its test, the raw-stream runtime assembly, the caller's
exit -> runtime.close wiring, the stdout-EOF heuristic (the connection no longer treats stdout EOF as
exit), and the 10 s steer/Stop cancel bound with requestSteerCancel. Reader control maps to
pauseReading/resumeReading; a close is connection.close after the host's existing 4 s Stop grace.

The adapter owns what the protocol no longer does: one session/cancel per running prompt however many
steers arrive (cleared with that send's settlement, retried after a failed write), and a Stop or steer
answers every open agent request the person has not already answered with the agent's own cancelled
reply. An answer already being saved when the Stop lands is sent.

Tests: blocked cancel write never holds Stop's grace, two quick steers send one cancel, a failed cancel
write is retried, a real process exiting while a child holds its stdout ends the session, and the
existing start-abort, retention, crash, connection-loss and reload-failure suites on the new rig.

* Add chat-owned OpenCode server transport foundation

* Combine server fixture type imports for CI lint

* fix(acp): Grok signs in on its own machine with its API key or cached sign-in

When Grok reports that it needs authentication, Orca now names a sign-in method on the machine Grok runs
on, read from the same environment Grok was launched with: xai.api_key when XAI_API_KEY is set there and
Grok offers that method, else cached_token when Grok offers it, else none and the chat keeps the existing
not-signed-in refusal. The rule lives in Grok's launch spec; the adapter applies any agent's rule for new
and reopened sessions through the protocol client's caller-named method (authenticate, then retry once).
No new sign-in UI; interactive methods are never chosen.

* Bound request serialization and correct transport fixture types

* fix(acp): the adapter decides which of Grok's requests reach the person

The turn owner now admits every agent request, permission or question, from its own turn state: a
request reaches the person only while Orca's prompt runs and no steer or Stop is cutting it short (a
question may also come from a turn Grok began itself, until a Stop). Anything else gets the agent's
own cancelled reply and opens no card, so a question arriving after Stop or during a steer never
appears. A steer, like a Stop, withdraws the requests already open; an answer already being saved is
still sent. The protocol client's abort-on-cancel path is no longer used: after the connection
change its request signal aborts only when the connection closes.

* fix(acp): a plan Grok proposes shows as a plan, with no approval card

When Grok leaves plan mode it asks the client to approve its plan (x.ai/exit_plan_mode). Orca showed a
blocking 'Approve plan / Request changes' card for it; the common pattern has no such gate. Now the
plan goes into the chat's existing Plan row (the plan-document status row Codex and ACP plan updates
already use) and the request is answered at once with 'abandoned' plus feedback telling Grok to stop and
wait for the person's feedback or a request to implement it in a later turn, so nothing is approved on
the person's behalf. Dialects gain settleRequest for requests answered without asking anyone.

* fix(orchestration): worker-start opens a Grok worker in a terminal, as before

With the structured chat setting on, worker-start decided 'structured' for Grok and then the structured
worker factory (Claude and Codex only) refused it, so the start failed; main opened a terminal Grok
worker. Worker-start now decides with no registered agents beyond Claude and Codex, so Grok gets a
terminal worker as before. agent.launch and the app's own launches still open Grok as a structured
chat. Temporary until structured workers take registered agents.

* Bound OpenCode event consumers and isolate chat caller identity

* fix(acp): a prompt answer Orca can't read ends the turn instead of hanging it

A session/prompt rejection that was not the agent's own error answer (an answer that fails Orca's
schema, or one too large to read) left the turn running: the next message became a steer with nothing
to cancel and was never sent or settled, and Stop waited its full grace. As in the common pattern, any
prompt failure now ends the turn as failed (a failed-turn row without words, since none are the
agent's) and settles the send, so the next message goes. Only a closed connection keeps the send
running, for the connection-loss path to settle.

* fix(acp): send Grok's prompt-identity extension only to agents that echo it

session/prompt carried _meta {promptId, requestId} for every ACP agent, though only Grok's dialect
echoes it (injectedPromptIdentity). Now only an agent whose dialect declares it gets the extension;
other ACP agents get a plain prompt.

* refactor(native-chat): the registered-agents capability lives in protocol-version again, as on main

This reverts 0ef6d21815. That commit moved the capability to its own module only because main's
protocol-version.ts was then one counted line over its limit; main now defines it there itself within
the limit, and main's new restart test imports it from there. Main's test also reads the desktop's
capability list as an older client; on this branch the desktop advertises registered agents, so its
older client is that list without this one capability.

* test(acp): read the sign-in method with a schema, not a type assertion

* refactor(native-chat): composer transport and Stop control in their own modules

Main's rewind change (#19338) brought NativeChatStructuredSession.tsx and use-structured-agent-session.ts
to their line limits, leaving no room for this branch's image-acceptance and unpublished-Stop lines.
The composer's transport (sends, commands, options, image acceptance) moves to
use-native-chat-structured-composer-transport.ts, and whether Stop shows and what it does moves to
structured-agent-session-stop-control.ts. Behavior is unchanged; the runtime cast on the composer's
'local' | 'remote' is now a typed return.

* test(native-chat): read registered agents by id, as main's structuredAgentsReadBy now takes

Main's A3 squash changed structuredAgentsReadBy to take agent ids; this branch's test still passed
{ agent } objects (CI typecheck TS2322).

* fix(acp): a first reopen warns when Grok forgets a chat that exchanged turns

A Grok session the chat created was treated as one Grok never saved, so
when Grok reported it missing on the chat's first reopen, Orca swapped in
a fresh session silently, even after completed exchanges: the person saw
the old messages while Grok had forgotten them.

The launch now counts a created session as never saved only when the
chat's journal, read at the failed reopen, holds no turn of that Grok
session; anything else, an unreadable journal included, takes the normal
path: the fresh session is recorded as replacing the old one and the one
warning row is written.

* Add setting-gated OpenCode server native chat integration

* Use checked native event ordinals in timeline identities

* Validate nested account tags and preserve bounded restore failures

* Keep native history restoration within its session module

* Check native protocol records and support registered-agent drafts

* fix(acp): a start writes the warning row an earlier attach failure dropped

The row saying Grok forgot the chat was written only into the attach's
deferred sink, while the fresh session's link was saved earlier. An attach
failure, quit or crash in between dropped the row forever.

Every start now derives the owed rows: each conversation the chain says was
lost to a failed restore gets its row unless the chat's journal already holds
it. The row now names the lost conversation rather than the fresh session, so
a row an unused replacement wrote still counts after Grok supersedes it.

* fix(acp): a question during a turn the agent began itself gets its cancelled reply

A question or other card-opening request the agent sends while no prompt of
Orca's runs (a turn it began itself, as when a background task wakes it) now
gets the agent's own cancelled reply and opens no card, the same rule
permissions already follow there. Nobody is waiting on that turn. A plan the
agent shares in it is still shown.

* Update launch and status tests for registered structured agents

* test(native-chat): import the unfinished-work capture from the module that owns it

Main's reasoning sweep test (#19221) imported it from the dead-generation settlement, which
this branch split it out of.

* Run OpenCode structured chat over ACP

OpenCode joins the ACP lane as two launch rows (opencode and opencode2 run
'acp'), with a dialect that reads command output and exit codes from
OpenCode's metadata and labels its project-wide 'always' grant honestly.
The launch forces OPENCODE_CLIENT=acp and turns OpenCode's question tool
off, strips Orca's terminal status overlay from the child, and pins the
chat's OpenCode account as a tagged locator (managed profile, or data and
state directories). Image prompts go as ACP image blocks when the agent
advertises them, and restart recovery reads OpenCode's own store for
presence only.

* OpenCode 2 over ACP only for its default account

OpenCode 2's ACP command runs inside the user's own background service,
which the chat's environment never reaches, so a managed profile cannot be
honoured: while one is selected a new OpenCode 2 chat opens in the terminal,
and a record pinned to one never launches.

* Test OpenCode over ACP against its recorded traffic

Seven scrubbed recordings of opencode acp (1.18.31 and 2.0.14) replay through
the adapter: an approved shell command with its exit code and the
project-wide always label, deny on both majors, the 1.x subagent approval
that never arrives (Stop still ends the turn), the 2.x subagent approval
answered on the chat's session, the stray 1.x file write, and a provider
error shown in OpenCode's own words. The replay now plays recorded error
answers too.

* Name the normalized tool update for what it is

* OpenCode over ACP: 1.x only, honest grants, sound recovery

Review round 1:
- OpenCode 2 stays on its terminal-backed chat: its acp command runs in the
  user's own background service, which a chat's environment and account pin
  don't reach, and a declined local launch has no terminal fallback yet.
- The model-pick launch change is out of this PR (it could leave a worktree
  behind when the host later chose the terminal).
- 'Always allow' reads 'Allow for this chat': OpenCode 1.x keeps it in the
  chat's own process.
- A file write OpenCode repeats to a client with no file system leaves no
  row.
- Images are bounded by the encoded prompt line, not just raw bytes.
- Restart recovery: each accepted send claims its own stored copy first, and
  queued cards never trigger the read.
- A send whose child ended during the image read settles as not sent.
- An account Orca cannot pin (inline auth, relative directories) is refused
  as unsupported; default XDG directories the user never set stay unset; the
  retired shared plugin directory and data-account variables never reach the
  child; newer locator fields are tolerated.

* Round 2: recovery reads the chat's own database, honest grant label

- Restart recovery resolves OpenCode's database under the chat's own home,
  not Orca's, now that default data directories stay unset.
- 'Always allow' reads 'Allow until OpenCode restarts': Stop, quitting and an
  idle chat's stopped agent all end it.
- Only a message with images is refused for passing one protocol line, so a
  long text message never reads as an image problem.

* refactor(native-chat): keep the session host within its line budget after main's Stop work

Main's #25949 left the host at exactly its 300-line budget, and this branch's acquire-abort wiring
adds one line. The reveal module now comes in as a namespace import, as the host already does for
its other helper modules, and the earlier reorder of two type imports is undone.

* test(codex): move the stdout-before-exit test into its own file

Main's connection test file is at its 800-line test budget, and this branch's exit-order test
pushed it over (CI lint, max-lines).

* Keep the record's path bound, which the launch directory now uses

* Narrow the refusal in the prompt content test instead of casting it

* Type the replayed error answer as the protocol's error

* Read the replayed message through the replay's own schema

* Match recovered OpenCode messages through main's send fingerprint

Main gave every stored send one fingerprint helper (it drops the sender field); restart recovery now hashes stored messages through it instead of rebuilding the hash by hand.

* Keep an `opencode` that is 2.x on the terminal chat

Homebrew's `opencode` is 2.x now, so the binary, not only the agent name, decides whether OpenCode's acp runs in-process. An ACP launch spec can name the --version releases it runs on; OpenCode takes stable 1.x from 1.18.31. createSupport asks the binary a launch would spawn, with the environment a launch starts from, so a desktop launch of 2.x opens the terminal chat; every launch asks again before spawning, so a binary swapped after create never starts. The --version probe is shared and bounded, and a version it cannot read counts as unsupported.

* Type the mocked login-shell environment instead of asserting it

* Run the OpenCode the Command setting names, and say why a version check refused

The structured OpenCode and Grok chats resolved a bare `opencode`/`grok` from PATH while detection and the terminal chat honoured the per-agent Command setting, so a user who pointed Orca at a specific binary got another one (and, with the version gate, another version) in the structured chat. resolveStructuredAgentCommand now takes any agent id and the stock binary name; the ACP launch and createSupport's version check both resolve through it, so they ask about and spawn the same file. An unrunnable Command is admitted at create and refused at launch with agentCommandNotRunnable, as Claude and Codex do, instead of silently opening a terminal.

The --version probe now logs why it refused (exit code, timeout, oversized or versionless output, unsupported release), with the binary's own first words, so a refusal on a real host can be diagnosed. A real-probe test drives createSupport and the launch against stand-in binaries: per-agent PATH, Command setting, 2.x first on PATH, and an unrunnable Command.

* Say why a new OpenCode tab opened a terminal, and ask the host again for its agents

Live QA run 5 opened the terminal-backed OpenCode chat with the private 1.18.31 set as the Command and on the per-agent PATH, and Orca's main log had no version-check line. The host never decided: a local launch routes to the structured chat only once the renderer has learned the host's registered agents, and that list was read once at startup, so a read that failed (or was skipped while the local runtime's capabilities were unknown) kept OpenCode on the terminal for the whole session. A launch that finds the list unlearned now asks the host again, so the next launch opens the chat.

Every point that sends a launch to a terminal now says so, naming the check and never a value: the renderer's route decision ([agent-launch-route], in the renderer console), createSupport's refusing check ([structured-create-support]), a version check that passed as well as one that refused ([agent-cli-version]), an OpenCode account that cannot be pinned ([opencode-account]), and a host launch the default asked to be a chat ([agent-launch]). createSupport's composition moves out of the unchecked runtime class into structured-agent-launch-support.ts.

Tests reproduce run 5: the host's create check with HOME and every XDG directory in the rig, the Command and per-agent PATH naming a stand-in 1.18.31, and the account pinned to the rig's XDG directories; and the renderer route with those settings, before and after the host's list is learned (ablation: without the re-ask the route stays on the terminal chat).

* Type the mocked login-shell environment in the OpenCode probe tests instead of asserting it

* Pass the OpenCode Command and environment through the route test's store helper, which types its settings

* Open OpenCode's chat on the first new tab, and learn the host's agents when they become available

The renderer routes an OpenCode tab to the structured chat only once it has learned the host's registered agents, and that list was read once at startup with nothing to read it again, so a read that came too early or failed sent every OpenCode tab to the terminal chat.

- agentSession.agents answers from the registrations the host is built from, as createSupport already does, instead of installing the host first: the answer is fixed for this build, so it no longer waits on, or fails with, opening the chat journal.
- The local list is read again when the local runtime's capabilities land, so a startup read made while they were unknown is redone on the event that makes it possible.
- A new tab whose chat route waits only on that list asks the host for it and opens nothing until it answers or 3 seconds pass (the local admission bound), then decides: the first launch opens the chat. A host that does not publish its agents is never waited on.

* Type the new-tab route request as a launch request, which carries the delivery callback

* Declare that agentSession.agents answers from the registrations in the cross-version manifest

Older builds read the list from the installed host, newer ones from the registrations it is built from; the manifest now asks only that Codex is listed, which holds for both.

* Wait for this computer's runtime too before routing a new tab during a slow startup

Field QA: soon after a slow startup, a new tab opened the terminal chat because the local runtime had not reported its capabilities yet, which routes straight to the terminal; the bounded wait covered only an unlearned agent list. A new tab whose chat route waits only on either answer now asks for it and opens nothing until it comes or 3 seconds pass, then decides; the [agent-launch-route] line still names the blocker if it falls back. Tests that never meant to exercise startup now start with the runtime's answer known.

* Wait for the probe that lands the runtime capabilities, not one that failed at startup

When the startup probe fails (a rig whose terminal daemon failed to start), asking again answers null at once, so the new-tab wait ended early and the tab opened the terminal chat. The wait now subscribes to the event that delivers the capabilities, probes once, and stops at the 3 s cap; it applies to both routes, so OpenCode (whose blocker is the unlearned agent list) and Claude (unknown capabilities) both wait for it. Tests run the real capability module with a failed startup probe: Claude and OpenCode open their chat when a later probe lands within the wait, and the terminal chat after the cap otherwise.

* Answer an OpenCode account Orca cannot pin at create support, not as a failed chat

Live QA with inline OpenCode credentials (OPENCODE_AUTH_CONTENT) passed the version check at create support and was refused only at create, so the window showed a failed "OpenCode Chat" with Retry where a declined launch opens the terminal chat. Create support now resolves the account a new chat would pin through the same read-only resolver the model catalog and create use, and answers no, logging the account-pin check, when the host refuses to pin it; any other failure is left for create to state. Create's own refusal stays as the backstop.

* fix(opencode): start the chat with the user's OpenCode environment as-is

An OpenCode chat with no managed profile no longer records data/state
folders or a database choice, no longer refuses inline credentials or
relative paths, and launches with the user's environment unchanged.
Drops the create-support account-pin probe that only those refusals used.

* test(cross-version): build each method's params in their own file, under the line limit
2026-10-07 16:26:27 -07:00
53f9ea7839 fix(opencode): keep Go credentials private and resolve backend keys (#24615)
Preserve the complete credential storage, migration, IPC, Settings and rate-limit refresh change alongside standalone GLM plans, current-main database diagnostics and the reviewed unknown-backend environment correction. Keep native discovery cancellation third and selected environment fourth.

Original-topic-commit: 7903f1cddb
Original-topic-commit: 588117b5cf
Original-topic-commit: dedd4f8c86
Original-topic-commit: 6645dae104
Original-topic-commit: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-from: a25b80c02c1af7830b0e6a65e72d965b3ad98276
Restacked-onto: b032867021

Co-authored-by: kespineira <kespineira@users.noreply.github.com>
Co-authored-by: kevimux <kevimux@users.noreply.github.com>
Reported-by: pullfrog
Reviewed-full-source: 849fe093073f4c1606bd65d79a0c725d955d0d1f
Native-helper-source: 80dbe23237


Reviewed-full-current-source: 36acb57d44adb3d378c0289c8c15f7da0fda214c
2026-10-02 23:21:27 -07:00
Neilandgriffinmartin f1463683ee fix(usage): include OpenCode cached input in totals
Normalize OpenCode cache reads and writes consistently across usage views, persistence and telemetry.

Continues #20187.

Co-authored-by: griffinmartin <griffinmartin@users.noreply.github.com>
2026-10-02 20:41:18 -07:00
Jinwoo Hong a1da632eb9 fix(opencode-go): don't fall back to OPENCODE_API_KEY when the credential database is unreadable (#24605)
* fix(opencode-go): stop before OPENCODE_API_KEY when the credential database is unreadable

An unreadable OpenCode credential database read as 'no key', so the Go key
resolver fell through to OPENCODE_API_KEY, which OpenCode shares with its
Zen provider and can show usage for the wrong key. The database read now
reports unreadable separately; with an env key set the resolver stops, and
the usage fetch uses a configured cookie or shows a readable error.

* fix(opencode-go): treat a denied credential-database listing as unreadable, not missing
2026-10-02 19:00:27 -04:00
Neil a781a602a8 test: retire duplicate cases that replay an owner across a re-export or provider shim (#24114)
Resolves 208 candidate pairs where the same case title appears verbatim in two or more
files, produced by a repo-wide scan calibrated against a known positive. 46 case
declarations removed across 32 files, 798 lines gone. No file deleted whole, no
production code touched.

The headline result is the measurement, not the deletions: across the three buckets that
reported in detail, the signal ran roughly 86% false-positive (3/42, 9/42, and the rest).
It has good recall and poor precision, and it reorders a reading queue rather than
replacing one. Calibrating a detector against a known positive proves recall, not
precision.

What the deletions were:

- Duplicate invocation through a re-export shim. `native-chat-tool-summary.ts` is a
  ten-line `export {...} from '../../../../shared/native-chat-tool-summary'`, and
  `agent-status.ts:161` is `export { isExplicitAgentStatusFresh } from
  './pane-agent-evidence'`. Cases on the shim side were byte-equivalent to the owner's
  with no rendering or transport hop.
- Provider-local replays of a shared helper: three `repository-ref` providers that are
  each `createRemoteRefProbeCache(parseXRef)` and contribute nothing to transient
  handling; two `local-pty` and `daemon/session` tables replaying
  `shell-startup-output-scanner`, whose owner additionally checks every split point.
- A reader-side replay of store policy. `runtime-worktree-agent-rows-structured.test.ts`
  asserted an attention-to-blocked mapping; the reader contains zero `attention` or
  `blocked` tokens and copies `state` through. The mapping lives in
  `structuredAgentSessionAgentStatus`. Consistent with
  `docs/reference/agent-status-store.md`: readers keep only presentation policy.
- Constructor-only subclass duplication: the shared capability-cache case is covered by
  `codex-app-server-capability-cache.test.ts`, whose ten cases include the identical
  title plus all four risks `docs/reference/git-compatibility.md` names — first fallback,
  later cached call, concurrent probes, per-host isolation.
- A private predicate duplicated at a real boundary, varying only a path passed straight
  into the shared predicate.

Why most pairs were KEPT, because the false positives are principled rather than noise:

- Two independent execution hosts. `src/relay/git-handler-*` and `src/main/git/*` are
  separate Git implementations that cannot import each other and hold separate capability
  caches, exactly as the compatibility doc requires; the repo already ships
  `status-branch-line-total-relay-parity.test.ts` to pin the duality deliberately. Neither
  side's argv, timeout or cache regression is visible to the other.
- Deliberately duplicated production siblings: Codex vs Claude (different account fields,
  different CLIs, different wire protocols), gitea vs bitbucket (`/pulls/42` vs
  `/pullrequests/42`), gl-utils vs gh-utils (separate in-flight maps). Same contract
  shape, different implementations — an identical title is the correct naming.
- Shared-predicate consumers: one side tests the predicate, the other tests a caller's
  wiring to it. A caller that forgot to call the predicate passes the shared test.

In a codebase with intentional provider and host symmetry, identical test titles are
expected, and the signal cannot distinguish "copied" from "parallel by design" because
both produce the same prose. Only reading both bodies separates them.

Verified: 6,968 desktop test files pass; the three modified mobile files pass (39 cases);
`check-reliability-gates.mjs` 140 gates; nothing under
`mobile/src/test-support/rpc-recording/` or `mobile/rpc-foundation/goldens/` touched.

62 local failures across 12 files were each accounted for and none is caused by this
change: `browser-manager-tab-identity`, `browser-manager-viewport-ownership`,
`session-scanner-codex-workers` and `managed-hook-script-refresh` all fail identically in
a pristine `origin/main` worktree; five `mobile-web-app-*-render` tests need Playwright
browsers this machine lacks; `structured-agent-session-restart-ownership` and
`ssh-remote-commands` pass in isolation and fail only under concurrent load.
2026-09-30 03:30:23 -07:00
Mr. ZengandNeil ef987e42d7 feat(analytics): persist local usage session identities (#18759)
Co-authored-by: Neil <neil@stably.ai>
2026-09-27 14:29:19 -07:00
Neil d4386763d5 fix(opencode-usage): merge a migrated session's two rows per column (#22550)
A session that lived through OpenCode 2's V1 import has a row in both
`session` and `session_v2`, and neither is complete. #22391 resolved the
pair by ranking whole rows on one number — total token count, ties to
`session_v2` — which let that number decide everything else on the row.

Three consequences, each reproduced against that PR's own fixtures:

- A recorded cost could be zeroed. `session_v2` wins on tokens while
  carrying `cost = 0`, and row parsing maps a zero cost to `null`, so a
  legacy row's $12.50 disappeared. Cost is re-derived by the same lossy
  reduce as the tokens, but only the tokens were guarded.
- The token comparison decided metadata. A legacy row with more tokens
  supplied a stale pre-migration directory, and a legacy row with a NULL
  model erased the model `session_v2` had — 23 of 234 shared ids on a
  real migrated database have a model only on the v2 side.
- Winner-takes-all is per row, so a legacy row holding the input tokens
  and a v2 row holding the cache reads reported one of them as zero.

Metadata now comes from the generation OpenCode still writes, with older
generations filling only its NULLs; usage columns take a per-column MAX.
Both rows aggregate the same assistant messages, and the import can only
drop messages, never invent them, so each column's MAX is a tighter lower
bound on the truth than either row and can never exceed it.

The relation stays exactly one row per id — the highest-priority
generation holding it — every column stays `columnExists`-guarded with a
SQL fallback, and a database with a single session table builds the same
SQL it did before.

Cache schema version 4 -> 5 so existing caches rescan.
2026-09-23 17:39:26 -07:00
Neil ee1c522070 fix(opencode-usage): read OpenCode 2 session_v2 token totals (#22391)
* fix(opencode-usage): read OpenCode 2 session_v2 token totals

OpenCode 2 copies every v1 `session` row into a new `session_v2` table and
then writes only there, so every OpenCode 2 session was invisible to the
usage scanner, which only knew `session`. Reading both tables unfiltered
would double-count the migrated rows, so the scan now builds one
deduplicated session relation where `session_v2` owns any id it shares with
the legacy table, and joins the message-level fallbacks against it too.

Bumps the usage cache schema version so existing caches rescan.

Measured against the real local opencode.db (OpenCode v2.0.12, 234 legacy
`session` rows / 252 `session_v2` rows, 18 of them v2-only):

  migrated db   before: 206 sessions, 367,419,745 tokens, $79.4704
                after:  219 sessions, 374,305,364 tokens, $79.4817
  fresh v2 db   before:   0 sessions,           0 tokens, $0
  (legacy table after:  219 sessions, 374,305,364 tokens, $79.4817
   emptied)

Fixes #15841

* fix(opencode-usage): resolve a migrated session to its fuller row

Review found two ways the fixed `session_v2`-wins precedence loses usage.

A `session_v2` without the token columns scores 0 through the source's
fallback literals, but still excluded the legacy row for every shared id, so
a token-bearing legacy `session` paired with a token-less `session_v2`
returned nothing at all — worse than before this branch. And upstream's
importer recomputes v2 totals from decoded messages, so a session whose
messages fail to decode lands *below* its frozen legacy copy, which the
"legacy is frozen, v2 is newer" rationale did not account for.

Both collapse into one rule: per id, the row with the greater token total
wins, and a tie goes to the higher-priority table. A faithful migration is a
tie, so it still resolves to `session_v2`; a v2 row that lost data no longer
erases the legacy record. This also makes `hasSessionUsageColumns`'s `some`
correct rather than merely lenient — a table without the columns can never
outrank a sibling that has them.

Measured on the real local opencode.db: zero shared ids have a legacy row
that beats its `session_v2` counterpart, so the rule is a no-op on healthy
data and only engages on the degraded shapes above.

Both new tests fail on the previous commit and pass here.
2026-09-22 21:40:49 -07:00
Neil 87a22db25a fix(opencode): include cache read and write usage totals (#21886)
* fix(opencode): include cache read and write usage totals

* fix(opencode): satisfy aggregate query safety checks

* chore(i18n): refresh runtime English catalog
2026-09-20 21:02:24 -07:00
NeilandXiro The Dev ee354a35d7 feat(agents): add OpenCode 2 beta support (#21418)
* feat(agents): add OpenCode 2 beta support

Co-authored-by: Xiro The Dev <lethanhtrung.trungle@gmail.com>

* fix(opencode2): support current plugin lifecycle and session storage

* fix(opencode2): preserve lifecycle ordering and full session capture

* test(opencode2): cover setup event bridge

* test(opencode2): cover setup event bridge

* test(browser): satisfy anti-slop naming check

* test(opencode2): cover live form lifecycle

* fix(relay): preserve OMP config directory selection

* test(opencode2): avoid assertions in bridge fixture

* fix(rebase): retain OMP resume and fresh launch behavior

* test: align upstream OMP resume expectations

* test(opencode2): verify rejected form closes waiting state

---------

Co-authored-by: Xiro The Dev <lethanhtrung.trungle@gmail.com>
2026-09-19 17:49:03 -07:00
Jinwoo Hong c5733e812a fix(opencode-usage): count cache-read tokens (#21522) 2026-09-18 17:07:22 -04:00
Jinwoo Hong 631b51f508 perf(usage): run the Claude/Codex/OpenCode usage scans on a worker thread (#21114)
* perf(codex-usage): resume rollout scans at the last parsed byte

Codex rollout files are append-only and grow all day, but any append
changed both mtime and size, so `canReuse` discarded the cached entry and
the scanner re-read the whole file from byte 0 on the Electron main
process. On one real corpus that was 6.59 GB re-read per cycle across
26.63 GB / 21,110 files.

Each parsed file now persists a resume point: the offset just past the
last newline-terminated line, the parse context at that offset (session
id, cwd, model, running totals), a sha256 of the 4 KiB before it, and the
file's dev:ino. A grown file resumes there and merges the appended
rollup into the cached one; anything unproven falls back to a full
reparse — truncation, an in-place rewrite, rotation, a counted tail with
no trailing newline, a legacy copied-session suffix offset, or a file
that must reclaim deferred fork claims. Resume never depends on mtime
equality, so a coarse-mtime filesystem cannot hide an append.

Fixture: a 75,737-byte rollout with a 758-byte append re-read 76,495
bytes before and 8,950 after (the append plus two bounded 4 KiB boundary
windows).

Also bounds the automation-attribution force predicate for both Codex and
Claude: it keyed on `lastScanError`, so a persistently failing scan forced
a fresh full rescan on every single lookup. It now keys on the most recent
scan attempt, which is one forced scan per run regardless of outcome.

* perf(usage): run the Claude/Codex/OpenCode usage scans on a worker thread

The three first-party usage scans walk whole rollout and transcript corpora
and read OpenCode's SQLite synchronously, all on the Electron main process.
They rarely produce a long stall — the JSONL reader streams, so it yields to
the loop between chunks — but they pin the main-process event loop at ~95%
utilization for the scan's whole duration, which is what every IPC message,
timer and window event then queues behind.

Move that work to one lazily-spawned, unref'd worker thread shared by all
three providers, following the OpenCode SQLite scanner precedent (#8864).
Measured on a synthetic 4,000-rollout corpus (25.8 MB cache): a cold scan
drops from 2,147 ms of main-thread time to 31 ms, and a steady-state
incremental scan from 165 ms to 64 ms.

The worker is stateless and the cache crosses the boundary both ways. That
costs ~64 ms of structured clone at this corpus size, against 2,147 ms saved
on the cold path, and it keeps the persisted cache the single source of
truth — a worker-owned copy would need an invalidation protocol and a second
resident copy of the same multi-MB array.

Failure is closed, never a silent empty result: a worker that cannot spawn,
times out, or crash-loops rejects, and the store records the scan error and
keeps the previous projection.

Two clients already carried the same FIFO/timeout/crash-cap machinery, so
extract it once as WorkerThreadRequestQueue (with the packaged entry-path
resolver as worker-thread-entry-path) and move all three onto it, rather
than adding a third copy. Their existing tests pass unchanged.

The oracle is event-loop utilization on the calling thread, not a stopwatch:
usage-scan-worker-event-loop.test.ts runs the same scan both ways and asserts
the worker leg leaves the caller idle while the main-thread leg does not, so
CI load moves both legs together (#18788).

* test(usage): compare the two scan arms instead of two fixed thresholds

The event-loop oracle claimed to be self-calibrating — its header said "the
ratio is self-calibrating, so CI load moves both legs together (#18788)
instead of tipping a fixed millisecond threshold." It computed no ratio. Two
separate `it()` blocks each asserted an absolute threshold against its own
arm, run separately, so load moved them independently. The comment described
a test nobody wrote, and the flake it promised was impossible is the one that
landed: `activeRatio > 0.8` on the calling-thread arm measured 0.764 on an
ubuntu runner.

Fixing the comment is not enough, because the fraction is the wrong quantity.
CPU contention drags the calling-thread arm's active/wall fraction *down*
toward the worker's, since the loop parks waiting on a contended libuv pool.
A 4-vCPU Linux container measured that arm at 0.175-0.756 across twenty runs,
idle and loaded — never once above 0.8. Active *milliseconds* move the other
way: contention stretches the caller's JS time far more than it stretches the
worker arm's fixed post-and-deserialize cost, so the gap widens under load.

Merge the two arms into one case over one corpus and assert the worker arm
costs the caller under a fifth of the inline arm's active milliseconds. Same
twenty Linux runs: 10.9x-83.6x, passing throughout. Keep the presence
preconditions on both arms — an arm that silently scanned nothing satisfies
the comparison trivially — and extend them to the calling-thread arm, which
previously checked only file and session counts.

* fix(ports): name the dropped command when the probe queue is full

The shared-queue extraction turned `Port scan command queue is full; dropped
${command}.` into a constant string, because `describeFull` was given no way
to see the request. Pile-up is per-probe, so the name is the only thing in
that log that identifies which of lsof/ps/netstat was shed.

Pass the rejected request to `describeFull` and restore the name. The request
is built before the cap check so it exists to be named; the id it burns is a
correlation token, so a gap costs nothing.

The existing overflow test asserted only the error class, which is why the
regression escaped a 29-test suite. It now dispatches the overflow under a
different command than the accepted ones and asserts the message text, so a
message that names the wrong request fails too.

Also add a direct WorkerThreadRequestQueue test. Three subsystems share the
queue and each client test only sees the parts its own protocol exercises,
with `queueCap` reachable from port-scan alone. Covers one-at-a-time FIFO
dispatch, the deadline starting at dispatch rather than enqueue, the
consecutive-death cap, and both points where that count clears.

And record the child-process hazard at the usage worker entry. `terminate()`
reaps nothing the thread spawned, and OpenCode discovery reaches a fork
today: `wslGated*` forks the WSL transcript sidecar for a `\\wsl$\...` path,
which a Windows `OPENCODE_DB` or `XDG_DATA_HOME` can be. One scan through
that entry with a UNC `OPENCODE_DB` forked a sidecar that outlived
`terminate()`.

* test(ai-vault): assert the OpenCode worker messages exactly, not by fragment

Checked every message string in the two clients the shared-queue extraction
rewrote against origin/main. Only the port-scan queue-full one regressed
(fixed in the previous commit); the OpenCode SQLite client's four messages
render identically, the remaining source diffs being renames — `error.message`
to `lastError`, `call.timeoutMs` and `CALL_DEADLINE_MS` to `timeoutMs`.
`session-scanner-worker-client.ts` was not touched by the extraction.

But its suite could not have caught it either. `/timed out/`, `/exited with
code/` and a bare `rejects.toThrow()` all still match a message that has lost
its interpolated value, which is the same blind spot that let the port-scan
regression through. Assert the rendered text instead: the timeout names its
deadline, the exit names its code, and the crash-loop drain still carries the
text of the fault that killed the run.

* fix(usage): correct the worker entry's child-process note

The previous note said `worker.terminate()` leaves a forked sidecar orphaned.
It does not, and the reproduction that appeared to show it used a stub sidecar
missing the `process.on('disconnect', () => process.exit(0))` the real entry
has. With a faithful one: the sidecar lives exactly as long as the thread and
is gone within 2s of `terminate()`, because tearing the thread down closes the
IPC channel it owned. Two worker lifecycles forked two sidecars and leaked
neither, and the pre-worker main-thread path reaps its sidecar the same way,
on host exit.

What is true and worth recording: a fork is reachable from this bundle at all,
which is easy to miss; it survives only as long as the channel does; and the
sidecar is now re-forked per worker lifecycle instead of pooled for the app's
life. State those, and warn that a future child which does not exit on channel
close would not get the same free cleanup.

* fix(usage): kill a wedged scan worker on no progress, not on wall clock

`USAGE_SCAN_TIMEOUT_MS` was a 10-minute deadline on the whole scan. A cold
scan of a real history is legitimately minutes — 637 s measured on a 30 GB
corpus with 300 worktrees before the per-cwd memo, ~51 s after — so a
larger corpus or a slower disk crosses it. Crossing it killed the worker,
recorded a scan error and left the cache unadvanced, so the next refresh
started cold and died at the same point, forever.

The deadline is now a no-progress window. The worker posts a file counter
as it walks the corpus (`UsageScanWorkerProgress`, rate-limited to one
message a second), and `WorkerThreadRequestQueue` re-arms the active
call's timer on each one via the new optional `isProgress`. Clients that
do not pass it keep the plain wall-clock deadline. `MAX_CONSECUTIVE_DEATHS`
and idle teardown are unchanged.

* refactor(usage): report scan progress as a file count, not one call per file

Claude's scanner walks batches, so a per-file callback made it loop just
to bump a counter.
2026-09-16 23:03:37 -04:00
Jinwoo Hong 2c2d068b26 perf(usage): resolve each cwd's worktree once per scan (#21130)
* perf(usage): resolve each cwd's worktree once per scan

Codex and OpenCode attribution ran the worktree containment search for every
parsed event, so a cold scan cost events x worktrees. On 745 MB of real
rollouts (~20k events) that is 1.2s with 0 worktrees, 5.0s with 100, 12.8s
with 300 and 39.8s with 1000; a full corpus with hundreds of remembered
worktrees is where the STA-7724 reparse burned minutes of main-thread CPU.

A scan holds only a few hundred distinct cwds, so both scanners now build one
memoized resolver per scan and thread it through parsing instead of passing the
worktree list to every event.

* refactor(usage): make the worktree resolver own canonicalization

`createUsageWorktreeResolver` now takes raw worktree refs and canonicalizes
them itself, so each scanner has one entry point and neither keeps a private
`buildWorktreesWithCanonicalPaths` or `canonicalizePath`. The resolver unit
test counts comparisons through the same `areWorktreePathsEqual` mock the
scanner-level test uses instead of a property getter.
2026-09-16 22:30:55 -04:00
1a925ed2b9 perf: select highest usage totals without full sorting (#19490)
* perf: select highest usage totals without full sorting

* test(usage): pin first-inserted tie-break contract for highestUsageKey

Document why the strict > and the NaN sort fallback are load-bearing, and
cover the tie/re-set ordering the replaced stable sort guaranteed.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-09-07 23:28:53 -07:00
Neil 2dfaa676d8 chore: update oxlint and oxfmt (#17150) 2026-08-29 14:13:35 -07:00
OrcaWinandOrcaWin 4ec6bbf588 Kill hung WSL transcript filesystem operations via child process with route quarantine (#15381)
* fix(native-chat): kill hung WSL operations via child process

Stalled UNC file operations hold libuv permits even after the gate
timeout expires, blocking Chat tab recovery. Two stalled operations
fill both permits and freeze all WSL access until restart.

Fork file I/O for UNC paths into a separate child process. On deadline
expiry, kill the process to force the hung syscall to exit. This frees
the permit for the affected tab's next read. Temporarily quarantine the
stalled route to avoid retry storms.

* chore: drop internal review artifact from the repo root

* fix(native-chat): harden the WSL transcript fs sidecar

Review follow-ups on the sidecar isolation change:

- Only the deadline may abort running gate work. The sole waiter's
  same-duration timeout fired first, killed healthy children on caller
  abandonment, and settled the task before the deadline could quarantine
  a stalled route - leaving the back-off dead for every dedupe:false op.
- Resolve the fork entry from out/main/chunks too: the resolver compiles
  into a shared chunk, and the scanner service child has no
  process.resourcesPath, so packaged WSL vault scans threw entry-not-found
  (masked as an empty tree).
- Allowlist the fork env instead of spreading process.env; ambient
  NODE_OPTIONS would halt or --require code into every child.
- Wrap transport faults (spawn failure, child death) in
  WslTranscriptFsError('unavailable') so discovery reports them as scan
  issues instead of misreading them as missing paths or empty trees.
- Gate the vitest in-process fallback on the vitest worker global so a
  leaked VITEST=true cannot revert production to in-process UNC syscalls.
- Reap idle sidecar processes after 60s instead of holding them for the
  app session.
- Split 'open' into its own protocol union member so the reusable-call
  Exclude actually strips it from the pooled-process API.
- Guard kill('SIGKILL') against the teardown race where an exiting child
  emits an unlistened 'error', and dispatch reads by handle kind before
  path spelling.

* fix(native-chat): probe stalled WSL routes instead of a fixed quarantine

Remaining review follow-ups:

- Escalating route quarantine: first strike lifts after 5s so a distro
  that was cold-booting when its op hit the deadline recovers on the
  next poll (~35s total instead of ~90s); repeat stalls double the
  back-off toward the prior 2x-timeout cap, and any settle the deadline
  did not force clears the strikes. Queued same-route tasks fail fast
  at quarantine instead of stranding one waiter deadline per file in
  sequential scans.
- Single request implementation: the vitest in-process fallback now runs
  the child's own dispatcher (WslTranscriptFsProcessOperations + decode),
  so unit suites exercise exactly what the forked process executes and
  the per-call-site fallback closures are gone. Dirent fixtures gained
  the full kind-flag set the serializer reads.
- Dropped the production-dead per-route close queue; UNC FileHandles
  (test fallback only) mirror the process-handle close contract.
- Error class, messages, and factories move to wsl-transcript-fs-error
  (re-exported from the gate) to keep the gate under the lines budget.

* fix(native-chat): harden WSL transcript fs with route quarantine strike

Extract quarantine logic into a dedicated module with strike decay: stalls older
than 5 minutes restart from base back-off, and concurrent-lane timeouts count as
one incident. Allow joining live in-flight tasks on quarantined routes (they cost
no new I/O). Preserve quarantine across transport faults (child death). Handle
file shrinking during tail reads by detecting short reads and returning empty.
Defer file closes that arrive mid-read instead of refusing, preventing slot
leaks. Separate process slot and boundary-finding concerns into focused modules.

* fix(native-chat): enforce route quarantine windows and isolate lanes per

A late result arriving after the deadline was incorrectly lifting the route
quarantine, allowing subsequent work to start before the back-off period
expired. Now late results are correctly recognized as stale and never cut
the quarantine short.

Process work is now isolated per (route, priority) lane so a scan stall
cannot block exact reads on the same distro. Each lane gets its own client
and process pool; late results and handle faults stay scoped to their lane.

Tests now fake performance.now() alongside timers (the quarantine clock
depends on it) and wait for the full back-off window to expire rather than
advancing by 0. Gate state is reset between test cases since late releases
never lift the quarantine.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-18 16:57:35 -07:00
Neil 97b71c2285 refactor(usage): split AI-usage scanners and stores under the max-lines budget (#14668)
The three usage scanners and their stores, plus the renderer usage-overview
model, each carried a file-level `eslint-disable max-lines` and had grown to
338-769 counted lines against a 300-line budget. AGENTS.md calls for splitting
rather than suppressing, and config/max-lines-baseline.txt is a shrink-only
ratchet, so this removes all seven suppressions and prunes their entries
(341 -> 334).

Each file is cut along the seams it already had -- and that several of the
suppression comments named out loud: filesystem discovery / record parsing /
attribution / aggregation for the scanners, and pricing policy / scope filters /
rollups / session rows / automation attribution for the stores.

Pure move, no behavior change. Code is relocated verbatim; the only edits are
import plumbing and, where a private class method became a free function, the
mechanical `this.state` -> `state` parameter threading. Every converted call
site passes `this.state` at call time and the automation path takes a live
`getState: () => this.state` getter, so no state is snapshotted. No barrel
exports: each new module owns real logic and importers point at the owner.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(remaining failures are pre-existing load flakes in untouched files, each green
when re-run serially), no import cycles among the 64 affected modules, and a
statement-level diff of every split confirms the moves are verbatim.
2026-08-15 18:33:33 -07:00
Jinjing b65e2175cd Fix WSL transcript stream cleanup and route-level isolation (#14243)
Parsers now properly tear down gated transcript streams in finally blocks,
even when throwing mid-parse, so stalled gate deadlines do not leak file
handles into later scans. Handle close queues are isolated per WSL route
so a stuck distro cannot strand closes on healthy ones. Route key logic is
extracted to a shared module used by both the gate's admission and the
close queue's serialization.
2026-08-13 01:03:03 -07:00
JinjingandOrca 2c28b6c92c Gate WSL transcript filesystem I/O to prevent stalls (STA-4049) (#14203)
* fix(ai-vault): gate post-resolution WSL transcript I/O (STA-4049)

PR #14090 admitted only path *resolution* through the WSL transcript
filesystem gate. Every byte read afterwards from the resulting
\\wsl.localhost\... UNC path ran raw, so a distro that answers the first
access() and then stalls hung Native Chat at "loading" and AI Vault at
"scanning" with no timeout and no error.

Route that I/O through a new wsl-transcript-fs-access accessor, which is
a verbatim node:fs passthrough off UNC and an admitted, deadlined task on
it. open/positional-read opt out of coalescing (dedupe: false): joiners
would share one FileHandle or one caller's buffer.

Refusals now surface as the existing retryable message rather than
notFound, per-root scan failures are contained to an AiVaultScanIssue,
and the memoized Codex/Kimi indexes evict on refusal so a stall cannot
pin "no titles"/"no cwd" until the index changes.

* fix(ai-vault): stop caching WSL gate refusals as results (STA-4049)

Code review 1 P1 fixes on top of the transcript gate:

- transcript-read-cache: never store a gate refusal. The refusal leaves the
  file's mtime untouched, so the cached error would have been served to every
  later call until the transcript itself changed.
- kimi/grok/opencode parsers: rethrow WslTranscriptFsError instead of folding it
  into "no session"/"no transcript", so the session parse cache cannot store a
  null or partial answer under an unchanged mtime. Ordinary missing/half-written
  files stay contained.
- opencode-usage scanner: gate the data-directory readdir and the absolute
  OPENCODE_DB stat. The AI Vault's primary OpenCode source reaches them
  transitively, which is why the direct-import guard never saw them.
- gated stat/lstat: accept an AbortSignal, matching gated open/read, so a
  cancelled watch install or title probe detaches immediately instead of holding
  a waiter to its deadline.
- gated open: close a FileHandle whose syscall lands after the last waiter gave
  up, and close handles off UNC verbatim (awaited, failures surfaced).

Co-authored-by: Orca <help@stably.ai>

* fix(native-chat): decode gated chunks incrementally and cancel drain I/O (STA-4049)

Addresses the CR2 blockers.

UTF-8 chunk-boundary corruption: the UNC branch yielded raw 1 MiB Buffer
slices that `decodeTranscriptStream` decoded independently, so any multibyte
codepoint straddling a boundary became U+FFFD on both sides — corrupting the
JSONL line and shifting `consumedBytes` (which seeds fallback message ids).
`gatedChunks` now holds a StringDecoder when `encoding` is set, and
`decodeTranscriptStream` holds one for the Buffer path, matching what
`createReadStream`'s decoder already did off UNC.

Watcher teardown: `installTranscriptWatcher` owns an AbortController that
`unsubscribe()` aborts, threaded through every gated call on the drain path.
Waiters now detach at teardown instead of holding to the 30s deadline, and
the gate's aborted-signal pre-check stops an in-flight drain from admitting
new tasks after close.

Rovo `session_context.json`: `readJsonObjectIfExists` rethrows
WslTranscriptFsError so `parseSessionCandidate` records a scan issue, instead
of caching an un-enriched session under an unchanged mtime that never re-reads.

Primary OpenCode source: `listOpenCodeDatabases` takes an optional refusal
reporter so a refused `OPENCODE_DB`/`XDG_DATA_HOME` surfaces an
AiVaultScanIssue, matching `listOpenCodeDatabasesInDirectory`.

`boundaryFingerprint` moved to its own module to keep the watcher engine
under the max-lines cap.

* refactor(native-chat): consolidate transcript I/O and remove fallback te

- Move boundaryFingerprint from its own module to transcript-file-version.ts
- Extract runPathOperation helper to eliminate duplicate UNC path routing
- Remove tests for fallback behaviors when transcripts are unavailable or incomplete
- Clean up implementation comments and verbose test documentation

* consolidate scan issues and gate session scanner I/O (STA-4049)

Both local and remote session scans hit stalled WSL distros identically:
one failed probe per discovered path. Unifying issue recording and gate
refusal handling prevents duplication and ensures consistent behavior.

- Gate all file operations (stat, readdir, read, open) through WSL
  stall detection instead of scattered or missing gates
- Serve cached transcripts when stat stalls; distinguish gate refusals
  from missing files
- Serialize UNC close operations to prevent thread pool exhaustion
- Incremental chunk decoding in streams handles codepoint boundaries
  correctly

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-13 00:54:19 -07:00
Neil 65e2b5b598 refactor(usage): share provider store lifecycle (#13558) 2026-08-10 20:38:31 -07:00
Neil 07bd574294 refactor(usage): share the Codex/OpenCode scan fold behind a provider contract (#12082)
* refactor(usage): share the session/daily fold between Codex and OpenCode

The Codex and OpenCode scanners each carried their own byte-identical copy of
the ~325-line aggregation pipeline (createEmptySession, the three breakdown
folds, finalizeSessions, mergeSessions, mergeDailyAggregates). Two copies means
a token-accounting fix — a bucket that double-counts, a merge that drops a
breakdown row — lands in one provider and silently not the other. The copies had
already started to drift in comments only; the next drift would have been in
arithmetic.

The providers differ in exactly one dimension: the extra metric folded alongside
the token counters (Codex `hasInferredPricing`, OpenCode `estimatedCostUsd`).
That is now injected as an empty/fromEvent/fold triple, so the shared code stays
generic without collapsing the two record schemas into a nullable union. The
clone strategy stays per-provider (`cloneSessionForMerge` vs `structuredClone`)
rather than being unified on the assumption that the difference is accidental.

`usage-provider-contract.ts` is the seam a plugin-contributed usage source will
implement. It is deliberately generic over each provider's record types: Claude
bills per turn while Codex/OpenCode bill per event, and `cachedInput` is a subset
of `input` for the latter but a peer bucket for Claude, so a single normalized
record would push nullable handling onto every consumer.

No behavior change. Emitted objects are byte-identical, including key insertion
order — verified by diffing JSON.stringify of the scan output before and after
across mixed models, mixed locations, an inferred-pricing flip, and null vs
non-null cost. Persisted field names and schemaVersion are untouched, so caches
do not invalidate.

* refactor(usage): make the provider contract load-bearing and dedupe worktree refs

Follow-up to the aggregation extraction, addressing three review points.

`UsageProvider`/`UsageScanResult` were declaration-only, which is the same
speculative-interface problem #12077 just deleted 8,900 lines of. They are now
implemented by both real providers via `satisfies`, so the seam is typechecked
against actual scan functions rather than asserted. The blocker was that codex
returns `processedFiles` and opencode returns `processedDatabases`; rather than
rename persisted-adjacent fields, the source key is a type parameter, so each
provider keeps its own on-disk name and the contract still binds. Verified the
constraint bites: swapping the key to 'processedSources' fails typecheck.

`schemaVersion` is part of provider identity in the contract, so each provider's
SCHEMA_VERSION constant (with its cache-invalidation rationale) moves into the
provider module and the store imports it. Values are unchanged (codex 5,
opencode 2) and the stores compare them exactly as before, so no cache
invalidates. This also keeps store -> provider -> scanner acyclic.

`UsageWorktreeRef` collided with the existing export in usage-worktree-metadata
(3 fields, no repoId). Two different exported types under one name in src/main
is worse than the duplication being removed, so the scan-input type is now
`UsageScanWorktreeRef`; usage-worktree-metadata is untouched.

`createWorktreeRefs` was triplicated. Codex, OpenCode, and Claude copies are
byte-identical apart from the return type name (verified by diff), and all three
ref types have the same four fields, so one shared copy replaces all three. This
is the only change to claude-usage/.

No behavior change: same functions, same arguments, same call order. The store
tests' `./scanner` mock still intercepts scanning because the provider captures
the mocked binding; their now-inert `createWorktreeRefs` mock key is dropped so
it does not read as still mocking something.
2026-08-02 02:32:29 -07:00
Jinjing cbe8635f46 fix(worktrees): prevent deletion from blocking Orca (#11233)
* fix(worktrees): prevent deletion from blocking Orca

* test(worktrees): loosen async history-delete event-loop bound for CI

The main-thread safety check failed on a loaded runner when a single
timer gap hit ~48ms under the prior 30ms threshold. Keep the bound well
below a recursive sync-rm stall without treating CI jitter as a block.

* test(worktrees): measure history-delete critical path, not timer gaps

setInterval gaps during async rm of thousands of files still flake under
CI scheduling. deleteWorktreeHistoryDir is sync and must only rename, so
assert that critical-path wall time stays well below a recursive walk.

* fix(worktrees): prevent deletion from blocking Orca

Add timeout-based draining of watcher closes so SSH round-trip delays don't
indefinitely block the worktree removal path. Also: order durable temp-file
sweeps ahead of writes to reclaim orphans before accumulation, skip own-process
temps to avoid deleting live writes, swallow persistence errors so disk failures
don't cascade to query callers, and measure history-deletion progress by loop
turns rather than timer gaps to detect blocking on CI runners.

* fix(worktrees): prevent deletion from blocking Orca

Worktree deletion can now proceed even if filesystem watchers or history cleanup operations hang, preventing Orca from freezing. Changes:

- Fence install slots with tokens instead of counters so removals can abandon wedged installs without corrupting later removals
- Timeout-bound watcher unsubscribe operations with a shared drain budget
- Move JSON serialization of large usage caches from queue-time to write-time to avoid blocking main thread
- Async tombstone + schedule history tree deletion instead of blocking recursive rmSync during GC, preventing main-thread stalls ~10s after startup

* Extract usage cache writer into reusable durable snapshot class

Consolidates serialized durable-write and generation-veto logic from
three usage stores into UsageCacheSnapshotWriter. Eliminates duplication,
centralizes multi-MB JSON serialization on the main thread via write-queue
serialization, and vetoes superseded snapshots to avoid wasted rewrites.

* fix(worktrees): prevent deletion from blocking Orca

Worktree deletion used to recursively delete large session trees (hundreds
of MB) on the critical path, stalling the event loop. Instead, rename trees
into a `.pending-delete` tombstone queue and reclaim them asynchronously
off the removal's critical path.

Extracted host tree removal into a reusable helper (`removeHostTree`) that
centralizes Windows retry logic. Added usage-cache flush on quit to prevent
data loss when scans complete right before shutdown. Improved watcher
removal deadline management with reserved tail slices for the final
unsubscribe, and added retry logic for tombstone removals that fail once
under transient Windows locking.

* fix(history): retry failed session tree removals

Tombstoned session trees whose removal fails transiently (e.g., EBUSY
under Windows AV) are now re-queued in-process with bounded exponential
backoff instead of sitting until the next HistoryManager construction.
Prevents a single stuck tree from blocking the entire Orca process.
2026-07-29 18:21:26 -07:00
c6c6c71196 fix(opencode): use cross-platform data directory (#10362)
* fix(opencode): use cross-platform data directory

* fix(opencode): honor in-memory database override

* fix(opencode): harden database discovery coverage

* test(opencode): reproduce Windows session discovery

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 19:58:49 -07:00
Neil 8b154d686c perf(runtime): remove timer clamps from cooperative yields (#10908)
* perf(runtime): remove timer clamps from cooperative yields

Renderer paste and input loops can schedule more than a thousand zero-delay timer yields for a maximum-size payload. Chromium clamps nested timers to 4ms, adding seconds of idle wall time.

Use MessageChannel tasks in renderer runtimes and setImmediate in Node while retaining a timer fallback for tests and unsupported environments.

* fix(runtime): preserve pacing and release yield callbacks

Adversarial review found that concurrent producers could retain resolved callbacks until global quiescence. Route renderer yields by token and delete each resolver before resuming its producer.

Keep timer pacing in terminal paste and accepted-write loops where SSH and local PTYs do not provide drain acknowledgement. Use the shared scheduler for the OpenCode scanner.
2026-07-27 12:21:14 -07:00
NeilandOrca aab112933e Revert "fix(memory): bound OOM-prone accumulators (#10179)" (#10255)
Co-authored-by: Orca <help@stably.ai>
2026-07-23 18:35:31 -07:00
Neil 8f40ddf328 fix(memory): bound OOM-prone accumulators (#10179) 2026-07-23 06:22:56 -07:00
Jinjing bcd16e56fd Skip reparsing zero-owner backup databases on live db change (#8043)
Backups that own no sessions have nothing to reclaim, so reparsing
them on every live-db update was wasted work; only reparse siblings
that still hold claimable session ownership.
2026-07-10 00:14:01 -07:00
JinjingandOrca 4908a03671 fix: dedupe fork-copied usage history across Claude, Codex, and OpenCode scanners (#8023)
* fix: dedupe fork-copied usage history across Claude, Codex, and OpenCode scanners

Coding-agent CLIs copy transcript/rollout history into new files on
resume/fork, and the usage scanners deduped per-file only (or not at
all), so copied history was re-counted once per descendant file
(issue #8006: 38.6B tokens / $72,981 reported vs ~2.3B real).

- claude-usage: cross-file turn ownership keyed on message.id:requestId;
  per-file ownedDedupeKeys persisted; deterministic sorted-path claim
  order; schema v3 -> v4 so inflated caches rebuild.
- codex-usage: cross-file token_count event ownership keyed on the raw
  record identity (sessionId + timestamp + token tuples); fixes both the
  copied-prefix re-count and the total-only branch that re-counted the
  entire cumulative session per descendant rollout; legacy
  .orca-session-copies skip-bytes bridge unchanged; schema v3 -> v4.
- opencode-usage: each sessionId is counted from exactly one database;
  the canonical opencode.db claims ahead of stale sibling copies
  (opencode-backup.db etc.) so backups no longer double totals, while
  backup-only sessions are still counted; schema v1 -> v2.

Regression tests cover fork-copied files counted once (including the
Codex total-only variant), duplicated OpenCode databases, and dedupe
stability across cached incremental rescans.

Co-authored-by: Orca <help@stably.ai>

* Fix cross-tool usage double-counting for fork/resume-copied history

- Widen Claude dedupe keys with message-id and uuid fallbacks so forks
  missing requestId still dedupe correctly, and drop Codex's sessionId
  from event keys since fork/resume rewrites session_meta.id while
  copying identical token_count records.
- Track hasDeferredClaims per cached file/database across Claude, Codex,
  and OpenCode scanners so that when an owning file is deleted, only
  files that deferred a claim need reparsing to reclaim those turns
  instead of rescanning the entire corpus.
- Let OpenCode's live opencode.db reclaim sessions from a stale backup
  claim once it reappears, avoiding a frozen stale snapshot.
- Bump schema versions to invalidate caches built with the old,
  narrower ownership keys (#8006, #8013 follow-up).

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-09 22:36:53 -07:00
NeilandOrca e33b2006f4 Remove stale max-lines lint disables from files under the limit (#7548)
110 files carried an eslint/oxlint-disable max-lines directive but are
already under the default max-lines budget (300 .ts / 400 .tsx / 600 .mjs
/ 800 test), so the suppression is dead. Removing it restores real
max-lines coverage on these files with zero behavior change.

Each removed directive had max-lines as its only rule; verified via a
full oxlint run (0 max-lines violations, 0 new errors). Diff is pure
deletions (200 lines, 0 additions) — no code touched.

Co-authored-by: Orca <help@stably.ai>
2026-07-06 02:12:32 -07:00
NeilandOrca 46646d7ff1 chore(lint): upgrade oxlint to 1.71 + enable 7 new rules (autofixed backlog) (#6841)
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules

Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day
minimum-release-age supply-chain guard; nothing here needs it). The
bump is a no-op on the existing config.

Enable 3 error rules (backlog autofixed to zero in this commit) and
4 warn rules (surface signal without gating CI):

error (autofixed, behavior-preserving):
- unicorn/prefer-node-protocol        (~1531 sites: bare builtin -> node:)
- typescript/no-import-type-side-effects (~36: all-inline-type -> import type)
- unicorn/no-array-reverse            (19: copy-then-reverse -> toReversed)

warn (real signal, current fires are test-only/correct):
- unicorn/no-array-fill-with-reference-type  (aliasing footgun guard)
- typescript/no-unsafe-function-type         (bans bare Function type)
- unicorn/prefer-array-flat-map              (map().flat() -> flatMap())
- unicorn/prefer-regexp-test                 (.match() in bool ctx -> .test())

mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix
ran from root and covered mobile/ too.

Verification (all green): oxlint 0 errors (root+mobile+aux configs),
oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed,
builds (electron-vite + web + cli) succeed. node: rewrites confirmed to
skip embedded SSH/CLI string payloads (AST-only); all toReversed sites
verified to operate on fresh copies or write-once locals.

* chore(lint): bump mobile oxlint to 1.71 so inherited rules parse

mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which
lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since
mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile &&
oxlint' failed to parse the new rule. Bump mobile to match root (1.71).

Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit
pass, vitest 978 passed / 0 failed.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-06-29 22:38:29 -07:00
03fd869b92 feat(ai-vault): support OpenCode SQLite session storage (#5925)
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: brennanb2025 <brennankbenson@gmail.com>
2026-06-23 11:18:17 -07:00
Neil 6e77c02fc6 fix: update Electron and revert UTF-8 write mitigations (#4598) 2026-06-03 16:45:01 -07:00
Neilandthiagomsoares 996b69dce0 Harden large app state UTF-8 writes (#4587)
* fix: write stats file in chunks to avoid Electron UTF-8 abort

orca-stats.json gains an event on every agent start/stop. After about a
month of use mine had grown to ~3.6k events / ~608 KB, and the app started
hard-crashing a few seconds after every launch (SIGTRAP, no catchable JS
stack):

    Assertion failed: (length + 1) <= (capacity())
    node::MaybeStackBuffer<char>::SetLengthAndZeroTerminate <- node::Utf8Value

The crash is in StatsCollector.writeToDiskSync(), which saves the whole
file in one writeFileSync(JSON.stringify(data)). Electron 42.3.2's bundled
Node aborts when encoding a string that large to UTF-8 in a single write;
stock Node 24 handles the same file fine and the data is well-formed, so
it's an Electron/Node encoding limit, not bad data. The save runs on a
debounce after agent_start, which restored agents fire on launch -- so it
crashed right after opening.

Write the JSON in 64 KB slices through one fd instead (never splitting a
surrogate pair), and lower MAX_EVENTS 10k -> 1k so the file can't grow back
this large. Lifetime aggregates are unaffected.

Verified by reproducing the abort standalone with the real 608 KB file
under ELECTRON_RUN_AS_NODE, confirming the chunked writer round-trips it
byte-for-byte with no crash, and running a patched build that loads the
file without crashing. The underlying encode abort is an Electron/Node bug
to report upstream.

* fix: harden stats JSON writes

* fix: chunk app state UTF-8 writes

* fix: stabilize status and terminal polling

---------

Co-authored-by: thiagomsoares <5190162+thiagomsoares@users.noreply.github.com>
2026-06-03 14:58:28 -07:00
Jinwoo HongandOrca 530bfe6c39 Speed up cached usage analytics loads (#4528)
Co-authored-by: Orca <help@stably.ai>
2026-06-02 18:10:31 -07:00
Neil 0856df806f perf: cap usage worktree canonicalization (#4263) 2026-05-31 10:11:51 -07:00
Neil 1009ac9083 chore: update Electron to 42 (#3919) 2026-05-30 13:26:48 -07:00
07b8d014bd Fix usage attribution for dotdot-prefixed child paths (#3651)
* fix: attribute dotdot-prefixed usage paths

* Add usage attribution boundary tests

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-05-30 12:46:55 -07:00
Jinwoo HongandOrca bce3ef1776 Add OpenCode usage analytics (#1986)
Co-authored-by: Orca <help@stably.ai>
2026-05-15 16:33:15 -07:00