Repository navigation
refactor(native-chat): keep the provider resume handle opaque to shared code - #24991
Conversation
…ed code
Shared structured-chat code parsed each provider's resume handle: Claude's
session id and branch leaf, Codex's thread id, through a 'claude' | 'codex'
union every new agent had to widen. The in-memory handle is now
{ transport, agent, nativeId, providerData? }: shared readers use nativeId,
lease and handle-chain checks compare transport and agent, and only the
Claude adapter reads its leaf (providerData).
Stored and wire forms are unchanged for Claude and Codex. One encoding
module writes their typed shapes and decodes both those and the neutral
shape a new transport uses, which an older build refuses as unreadable
rather than reading as Codex. Key and root strings, which fork seeds,
superseded creations and resume offers persist, stay byte-identical.
The journal's own handle type becomes the journal-row and attach-wire
encoding of the same handle, and the journal identity carries the
neutral handle (null before the provider proves one).
No user-visible change.
…l identity The journal row converter now takes the identity every caller already holds, so a row's handle has one obvious constructor. Tests that wrote the in-memory handle straight into journal rows now build it through that converter, and the processless Claude fixture names a not-yet-proved handle as null.
A typed Claude or Codex handle that also carries the neutral form's transport, agent, native id or provider data named two identities; it was read as Claude or Codex and the next write dropped the other one. Such a row now stays unreadable and is set aside untouched.
Journal-identity fixtures from the timeline branches named the pre-#24991 handle shape; they now use the opaque handle. The ACP acquire keeps its lane in a slot the closures write, and a renderer route test builds its partial store the way its siblings do.
…n suite Main grew the suite to the 800-line limit; the opaque-handle import pushed it over. The two tests built the same identity inline.
Rename the neutral provider handle's providerData to resumeCursor before any row persists the neutral form: it is an adapter-owned resume position (Claude's transcript leaf), never identity. Claude/Codex stored and wire bytes are unchanged; their typed shapes never carried the field. State the stored-form contract (a handle's field set is closed; later per-link data goes on the chain link, which every build preserves) and pin it with a record round-trip test. Document that transport records the id space the native id was minted in, which can differ from the agent's current transport.
Review summary (head
|
main #24576 deleted agent-session-journal-recovery.ts and its two tests; take the deletion (this branch only re-pointed them at the neutral provider handle). Main's new journal and cross-version tests build journal identities with codexProviderHandle(); the older-build open in the newer-chat cross-version test keeps the legacy handle shape it reads.
|
Merged current main into this branch (now
|
|
This run was cancelled 🛑 The workflow was cancelled before completion. Please check the link below for details. |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 🧰 Additional context used📚 Code guidelines (2)No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (206)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review. 📝 WalkthroughWalkthroughThe change replaces provider-specific in-memory handles with a shared handle shape based on transport, agent, native ID, and optional resume cursor. It adds conversions and validation for persisted, wire, and journal forms, then updates record storage, journal rows, runtime logic, and fixtures to use those forms. Claude resume points now use the shared provider-handle transition. Tests cover encoding, decoding, handle identity, invalid forms, and compatibility with older builds. Priority: ⬇️ Low Merge Risk: 🔵 Low · up to This is an internal refactor with no intended user-facing change, and Claude and Codex stored forms are kept identical. The remaining step before merge is to confirm that older app builds can still read upgraded data, since only a simulated cross-version test was run. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to Existing conversation formats and ownership checks appear preserved, and no introduced security defect was established. The change crosses several identity and storage boundaries, however, and incomplete end-to-end coverage leaves limited residual uncertainty. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❓ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 13.79% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 29 functions across 50 files. (156 skipped: 156 over the file limit.) ✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… A4/A5/B1) The conflicts were all "A2's copy of A1" against main's squash of A1. Resolved by re-merging with old-main+A1 as the base, so the result is main plus exactly A2's own change.
…-history Main's #24991 made the provider handle opaque: the rest-test rig's record link and this PR's test journal identities now use codexProviderHandle. Main's journal-row-schema and journal types changed only the handle type's name; the status derivation is unchanged (golden digest passes), so the rules stay at 3. The rest rig's open counter reads the journal's chat and directory through typed narrowing instead of Reflect.get.
…r pointed mail is unread Drops the native-chat-only rule that held back a notice while the agent had not yet read mail it was already pointed at. Also fixes main's stop-note test, which still built a provider handle in the shape #24991 replaced.
…h claudeProviderHandle Main made the provider handle opaque (#24991); build it with the existing helper, as main's own tests now do.
…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.
…LI agent gets, through the same send as your messages (#25078) * feat(orchestration): agent mail to a busy chat waits in the chat's own queue A mail notice for a busy structured chat used to wait in the orchestration lane's own invisible "until the chat is free" gate. It now goes through the queue a person's message uses: sendAgentTurn(..., { delivery: 'queue', source }) makes the host hold it as a draft card the person sees and can Steer or delete, and the queue sends it when the turn ends. - The lane's busy gate (turnRunning / awaitingHuman) is gone; the queue's own hold decides. Its parking now only waits for its own send or card to settle. - A queued card's hand-off (sent under a fresh id) is matched by queuedMessageId, so it stamps the mail once and never sends a second notice. - A card from before an Orca restart is still the lane's: no second card. - A card the person deleted counts as handled for that batch; newer mail notifies again. No stored flag: read off the card and delivered_at. - `orchestration check` waits for the lane to withdraw a card whose mail it read, so a stale notice is never sent; more mail replaces the card. - Each card records who queued it in queued_messages.source_json (versioned, schema-validated): the person, or Orca for agents, with every distinct sender as an orchestration party plus host id, and the mailbox, dispatch, run and message ids. The restart pause holds only the person's cards. - sendAgentTurn answers with a snapshot, not the journal's live submission. * test(orchestration): type the mail fixture as a pointer batch message * fix(orchestration): the queue judges an agent's mail notice as it sends it A mail notice queued in a busy chat could go out stale or twice: it was kept true from outside the queue, by orchestration check withdrawing it, which missed other readers, raced an attempt in flight, lost the card when /clear carried it (a second notice), and moved it to the back when new mail replaced it. An agent's card behind a person's paused card never sent after a restart, so an unattended coordinator stalled. - The drain asks the card's sender, in its own serialized step, whether it still stands: send, restate (count and senders of the mail owed now, written onto the card in the hand-off's transaction), or withdraw as the host. A failing judge sends as written. Send-now restates but never withdraws. Orchestration registers the judge on the host. - check no longer touches any queue (back to main); onMailRead, notifyOrchestrationMailRead and the lane's withdrawals are gone. - One unsettled card per agent message, enforced at insert; new mail is counted when the card sends, never replaces or moves it. - The lane finds its card by what it is (a notice for the mailbox in the session the mailbox reaches now); a decline is a withdrawal a person's operation stamped. /clear moves agent cards as the host. Pointer rows last only while a direct send is in flight and end with the session. - Pauses (restart, Stop, /clear) hold only a person's cards, and a held card never traps an agent's card behind it, keyed on the message kind. - The source is named for the message (agent-session-message-source), one payload shape per message kind, no relative host id. * fix(orchestration): Orca's mail notice waits in the queue out of sight The notice card read as something the person typed and came and went on its own, the transient-state message the queue should not show. It is hidden until B can label who sent it. - The host leaves mail-notice cards out of the published queue, so the list, its count, Steer/Delete/Edit, steer-newest and the paused header never see them, on every client. Keyed on the message kind: D6's task card will be shown, labelled, in B. They still count toward the queue limit (temporary, until B). - A refusal no longer returns a hidden card for the person to act on: the host withdraws it, and the lane points again only after a turn ran since, the rule it already had for a refused notice. - Send-now no longer restates a card: no one can reach a hidden one. - The stored sender drops the pane key, a mailbox credential. - The gate covers an abandoned worker: a mailbox that reaches no session withdraws its notice (tested). * fix(orchestration): a hidden mail notice never waits on the person Hiding the notice left three paths where it waited on someone who cannot see it. - A failed hand-off write put a send_failed hold on it, which only a person's Send or Delete clears: that mailbox's notices stopped for good. The host now withdraws a hidden card instead and hands its mailbox back to the lane through the ordinary redrive, since an idle chat gets no other edge. A write that keeps failing is retried once per edge. - A person's Stop while the agent started on the notice put it back to waiting, where no pause holds it, so it was sent again at once. The host now withdraws it; the lane points again after a turn ran or when newer mail makes it a different notice, as on main. A restart still puts it back. - A notice queued before the person's message went first. One order rule now: the person's cards go in order and never past one waiting on them; a card they cannot see never delays one they can, and goes only when none of theirs may. The drain moves to its own module (structured-agent-session-queued- drain.ts). Comments that still described the notice as visible are corrected. * fix(orchestration): an accepted notice hand-off stamps its mail; a judge that cannot look decides nothing - When the agent opened its mail in the notice's own turn (the normal flow), an open batch left nothing owed, so the lane never marked the mail the accepted hand-off carried as delivered, unlike an accepted direct send. The lane now stamps exactly the ids a handed-off notice carried whenever undelivered mail remains, owed or not. A later conversation is no longer told again about mail already opened. - At quit the host registry is cleared before teardown, so the drain's judge could not resolve the mailbox and withdrew the notice. A judge that cannot read its inputs now defers: the card waits for a step that can look. A mailbox that resolves to another session or none is still withdrawn. - The judge, the owed-batch selection and the notice body move to structured-mail-notice.ts. A failing hand-back of a dropped card is logged. * refactor(orchestration): a chat receives the agent's mail itself, queued like a person's message A structured chat used to get a derived "You have N orchestration messages, run check" notice, which could go stale while it waited in the chat's queue, so the queue judged, restated or withdrew it as it sent. It now gets the mail itself as the turn, through sendAgentTurn with delivery 'queue', the path a person's message takes. - One turn per mailbox batch: the mailbox's unread mail at delivery, in mail order, each message led by "[message from <sender>]", its type, subject, body, payload and reply hint (what check prints). Mail arriving while that card is still in the queue waits for the next card; a card is never edited. - An idle chat takes it at once; a busy one queues it as an ordinary card the person sees and can Steer or delete. Mail is marked read when the chat accepts the turn, so check does not return it again; a deleted card leaves its mail unread for check, and it is not pushed again. - The card stores who it is from (source_json): every distinct sender, and each message's id, run and sender. Kind 'mail-notice' becomes 'mail'. - Removed: the send-time judge (QueuedAgentCardVerdict), structured-mail- notice, structured-pointer-notice-cards, queued-message-restatement, the hidden-card rules, the agent-card exemptions from the restart, Stop and /clear pauses, the dropped-card hand-back, and the drain split. An agent's card now follows the same pauses as a person's. - Terminal agents are unchanged: they keep the typed pointer and check. * fix(orchestration): a chat's check skips mail already queued to it; restarts hold only the person's cards - When a structured chat runs `orchestration check` (consuming, --peek or --wait), mail an agent's card in that chat's own queue still carries (any card not deleted) is left out: it is on its way as a turn, so the agent does not read it twice. Derived from the queued rows at read time; a deleted card no longer carries it, so it comes back. Terminal callers and every other read are unchanged. New batches pass the exclusion to getOrCreateMailboxDelivery; peeks filter it. - A restart's queue pause now holds only the person's cards: an unattended coordinator's agent card sends after an app restart without waiting for a Resume, since the mailbox is the record and reading is marked. Stop and /clear still hold agent cards. One rule, in queuePauseHolding. An agent card queued behind the person's held card still waits behind it: the queue never reorders. - The duplicated direct-mailbox snapshot routing in check-run and check-worker becomes one helper, which keeps check-worker under its line limit. * fix(orchestration): the mailbox is the only record of read mail; a chat's check takes its waiting cards A card held an exclusive claim on its mail that nothing reconciled with the mailbox: queueing it stamped the mail delivered, and a chat's check hid every card that was not deleted. So a chat's own check --wait in one turn could not see a result its waiting card held, a hand-off that ended in doubt or came back "Not sent" stranded its mail, a check racing the card's build read the mail twice, and a card left behind by run-use still sent and marked it read. What a card or send holds is now derived each time from the chat's queue and its sends: an accepted send marks its mail read; a waiting card or an unanswered send holds its mail; everything else unread is pushed again or returned by check. A card the person deleted is recorded as not to be pushed, at the delete. The lane withdraws returned, partly read and moved cards. A chat's consuming check withdraws the waiting cards holding what it reads, one at a time with the lane. The sender is stored on the sent submission too (host-only), so read state survives the card row's prune. A refusal before anything started ends its operation; a restart pause is raised only by the person's cards; an unreadable agent source stays an agent's. * refactor(orchestration): a chat gets the pointer a terminal agent gets, sent through the chat's own queue A structured chat now receives exactly what a terminal agent is typed: "You have N orchestration messages ... run `orca orchestration check`" (formatMessagePointer, same CLI name). It goes through the shared sendAgentTurn with delivery 'queue', the composer's queue-if-active: an idle chat gets it at once, a busy chat's queue holds it as a normal card and sends it when the turn ends. The lane's idle gate is gone; the send decides, as for the person's message. The lane queues no second pointer while one of its cards still waits, read from the queued rows; mail arriving meanwhile, or after the card is sent, is pointed again as the terminal lane points new unread mail. A queued pointer counts as delivered, as an accepted one does. `check` and mail read state are main's: only `check` reads mail. The card records who it is from (queued_messages.source_json, kind 'agent', message 'mail-notice', with its senders) for the next PR to render. A restart's pause is raised by and holds only the person's cards; Stop and /clear still hold an agent's. Removed from the earlier designs: the message-as-turn batching, the check exclusion and taking, the derived claims, lock and reconciliation, the sender on submissions, and their tests. * fix(orchestration): a chat's pointer follows a card that leaves its queue unsent The lane holds new mail while a pointer card waits, and retried it only on the chat's next status change. A card that leaves the queue without a turn after it (the person deletes it while Stop holds it, or its hand-off comes back) made none, so that mail sat unpointed until something unrelated happened. The shared queue wiring now tells its host when an agent's card stops waiting, read only when the queue changed, and the runtime redrives that chat's parked mail. The runtime forwards the new host dep like the others. Tests: that case end to end, and /clear carrying an agent card keeps who it is from. Agent card bodies in tests are the pointer text; the restart pause's header says why an agent card may send. * refactor(orchestration): no special handling for agent notices in the chat queue A structured chat's orchestration notice is now sent through the chat's own send like any message: an idle chat takes it as a turn, a busy chat's queue holds it and sends it at turn end, under the same Stop, restart and /clear pauses as the person's cards. Native chat no longer branches on who a card is from; the card only records it. - Restore main's queue pause logic (no restart exemption for agent cards). - Remove the queue watch that redrove mail when an agent card left the queue, and the host's queued-row read the lane used for it. - The lane reads nothing of the chat's queue. It sends no second notice while mail it already pointed at is unread, read off the mailbox alone. * fix(orchestration): point new mail like a terminal, even while earlier pointed mail is unread Drops the native-chat-only rule that held back a notice while the agent had not yet read mail it was already pointed at. Also fixes main's stop-note test, which still built a provider handle in the shape #24991 replaced. * test(native-chat): keep the queued-message rig fixture under the line limit * fix(test): drop main's duplicate codexProviderHandle import (same line as #25713)
…nstead of shared code naming Claude and Codex (#25076) * refactor(native-chat): keep the provider resume handle opaque to shared code Shared structured-chat code parsed each provider's resume handle: Claude's session id and branch leaf, Codex's thread id, through a 'claude' | 'codex' union every new agent had to widen. The in-memory handle is now { transport, agent, nativeId, providerData? }: shared readers use nativeId, lease and handle-chain checks compare transport and agent, and only the Claude adapter reads its leaf (providerData). Stored and wire forms are unchanged for Claude and Codex. One encoding module writes their typed shapes and decodes both those and the neutral shape a new transport uses, which an older build refuses as unreadable rather than reading as Codex. Key and root strings, which fork seeds, superseded creations and resume offers persist, stay byte-identical. The journal's own handle type becomes the journal-row and attach-wire encoding of the same handle, and the journal identity carries the neutral handle (null before the provider proves one). No user-visible change. * fix(native-chat): derive journal-row provider handles from the journal identity The journal row converter now takes the identity every caller already holds, so a row's handle has one obvious constructor. Tests that wrote the in-memory handle straight into journal rows now build it through that converter, and the processless Claude fixture names a not-yet-proved handle as null. * fix(native-chat): refuse a stored provider handle written in both forms A typed Claude or Codex handle that also carries the neutral form's transport, agent, native id or provider data named two identities; it was read as Claude or Codex and the next write dropped the other one. Such a row now stays unreadable and is set aside untouched. * refactor(native-chat): route structured agents through registered definitions The structured-chat shared layer named Claude and Codex in a closed router type, in option reads, in frame classification, and in renderer label and validation branches, so adding an agent meant editing shared types. Each agent now has a definition (capabilities, resting option rules) owned by its own module; the router is a registry of definition + adapter pairs built at runtime setup, and shared code reads the definition instead of the name. No behavior change for Claude or Codex; no wire or stored shape change. * refactor(native-chat): name the structured agent list once in host types The host types, conversation commands, provider-session ownership and the runtime's create path spelled the structured agent list inline; they now use the one shared type the record and wire already validate against. * fix(native-chat): narrow the record before reading its agent's option rules * refactor(native-chat): make the router's registrations the only agent definition lookup The at-rest option reads looked definitions up in a second, separately populated map, so a registered definition could route and declare capabilities while the static map decided which options a chat at rest accepted. The router now answers `definition(agent)` from its registrations, the at-rest readers take it through the host's adapter, and the built-in definitions are listed only where the runtime composes the router. * test(native-chat): use opaque handle in queued rejection fixture * test(native-chat): share one Codex journal identity in the integration suite Main grew the suite to the 800-line limit; the opaque-handle import pushed it over. The two tests built the same identity inline. * refactor(agent-session): name the handle's adapter state resumeCursor Rename the neutral provider handle's providerData to resumeCursor before any row persists the neutral form: it is an adapter-owned resume position (Claude's transcript leaf), never identity. Claude/Codex stored and wire bytes are unchanged; their typed shapes never carried the field. State the stored-form contract (a handle's field set is closed; later per-link data goes on the chain link, which every build preserves) and pin it with a record round-trip test. Document that transport records the id space the native id was minted in, which can differ from the agent's current transport. * refactor(agent-session): one required agent registry; declarations admit what they claim A StructuredAgentRegistry, built once from the {definition, adapter} registrations, is now a required host dependency and the router routes with it. The adapter interface loses its optional router-only capabilities?() and definition?(); every reader (options at rest, thread goal, rewind, the /compact handover) asks the registry. A live session still narrows rewind through the adapter, and the host combines declared and narrowed in one helper. The registry refuses a registration that declares compact, a thread goal or rewind without the adapter method behind it. /compact is admitted by the declared capability, so an agent that declares compact:false gets the commandRefused fact instead of a thrown error. Also: the cut-turn notice names the agent from the catalog, create-support builds the account home through agentSessionAccountHome, and the turn status text older clients read names the session's agent instead of defaulting to Codex (Claude/Codex text unchanged). * refactor(agent-session): the router applies the declared rewind itself The router holds the registry, so its rewindSupport answers the owner's declared rewind narrowed by the adapter, in one place no reader can bypass. The host-side combining helper is gone; readers ask the adapter they hold. * chore(agent-session): the registry is the one lookup; drop the test-only empty capability record * 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(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.
* fix(native-chat): a child's root exit is reported even during its close, and bookkeeping after it never reads as unproven - Both connections report the root process's exit once, with `expected` set when a close had begun. A close that came back unproven and whose root exits later is finished by the adapter, and its end reaches the host like any other. - A Claude close whose resume-point write fails after the exit was proven, and a Codex close whose terminal row is refused, now end the session and report the failure, instead of keeping a dead child indexed as if its exit were unproven. - A Codex close whose forced tree kill can't prove the descendants gone but saw the root exit reports the descendants and counts the root exit. - Every child exit with an identity, expected or not, is forwarded to the host. * fix(native-chat): the exit ends the child's record; an unfinished stop is the child's own close, which everyone joins - The host keeps no stored "stop still owed" record any more. A stop begins the child's close (`child.close`), which lives on the child and ends with it. A second Stop, the idle reaper, quit, a send and an option/answer/goal/rewind all join that close instead of retrying a separate obligation. - A caller waits on the close only as long as the step deadline; the close itself is never abandoned. A proof that lands after every caller stopped waiting reaches the host as the adapter's report of that exit, which ends the record through the same handler. - Once the exit is proven, draining, settling, the lease release and the adapter's acknowledgement are each attempted and reported on failure; none keeps the child on record. A start, and the handle's close, write a release that failed from this host's proof of that exit, so a failed write never refuses a send. - A start that meets a close still unverifiable is refused with `previousExitUnverifiable`, so the queued message is rejected with a send-again reason; nothing is held and nothing starts beside the old process. - The idle sweep goes back to idle reaping only. - Removes #24333's retry entry points, the wait row and its hold rule, the ask/failure cursors on the stored record, and the stop's own wake. Tests replace the #24333 unproven-stop test: a send joining an unproven close and an in-flight one, a late proof past the caller's bound, a root exiting after its close gave up, a proven exit whose resume-point write and lease release both failed, an unverifiable close rejecting the send and refusing an option change, a surviving descendant, quit and the idle reaper; and Codex's unverifiable, late-exit and joined-close cases. * fix(native-chat): a message refused because the old process's exit is unverifiable says so, and to send again The start failure for a refusal with reason `previousExitUnverifiable` reads "Orca couldn't confirm Claude's previous process ended. Send your message to try again." instead of "Claude couldn't restart." The status-row kind and the refusal reason stay in the shared lists for rows and hosts that still carry them; the catalogs keep one sentence for both. * fix(native-chat): a close's verdict is the root's exit alone, and what follows it is logged - A Claude close resolves as soon as the root's exit is proven: the session ends and its `ended` report goes out then. Saving the resume point runs afterwards and a failure is logged, so a slow or hung write never reads as an unproven exit or keeps a dead child on record. - A root that exits after its close came back unproven finishes that close through the same path as any close, so the session's child work is published as ended (background tasks and subagents no longer stay shown running for a dead agent), and a failure there is logged. - Codex logs a refused final row, and reports a root exit whose forced tree kill could not prove the rest of the tree gone the way Claude does, so the host logs it and blocks nothing. - Both adapters take the host's logger for this bookkeeping. * fix(native-chat): one handler ends every child's exit, and a join waits on the adapter's own close - One exit handler (`structured-agent-session-child-exit`) ends a child's record for an exit expected or not. `expected` only changes what the chat is told: the stop's cause, its end at the stop's ask, the settlement id, and no crash outcome row. The lease release keeps the exit's evidence; the handoff guard, lifecycle barrier, sink release and adapter acknowledgement apply to both. A Claude journal-sink failure ends in the same step as its stop, as Orca's own fault. - Joining a close is asking the adapter, whose close is memoized while it runs and bounded by its own kill escalation; the host keeps no attempt of its own and no 10 s caller bound. An ask after a close came back unproven runs the stop again. - A close's end is stamped where its stop was asked for (a repeated ask moves it), so the closed chat and failed start checks order a message accepted meanwhile after it. - A start refused because the old exit is unverifiable rejects what was queued in the same step. - The end of a close the host asked for no longer waits on the cross-session recovery chain. - The kill no longer waits for the stop event's write; the journal writes rows in order. * fix(native-chat): an exit's lease release lands whatever the length of its reason A crash's reason can carry kilobytes of the provider's stderr, and a lease whose death detail is over 512 characters fails the store's own check. The exit handler cut it, but the release a start or the chat handle's close re-derives did not, so after a crash whose own release failed every message was refused as not resumable until restart. The record's builder now cuts the detail to the record's bound, so no writer can hand it one too long. * fix(claude): a proven close waits at most 2 s for the output it already wrote Once the root's exit is proven, the close still waited for the SDK's output reader to end. Something outside the process tree that holds the output open would keep that close, and every send, Stop and quit joining it, waiting with no bound. The wait is now bounded; past it the close resolves as proven and the open output is logged. * fix(codex): an exit reported inside Orca's close keeps the reason Orca closed it for The connection reports the app-server's exit inside the close that ends it, so that report ended every Codex close and replaced the close's own reason (for example, a provider frame that could not be recorded) with the connection's stderr text in the ended record and the lease's exit evidence. The session now records Orca's close with its reason, and the exit it ends keeps that reason. The test connection reports its exit inside close the way the real one does. * fix(native-chat): quit stops delivery before it drains exit recovery Every exit now wakes delivery, and teardown drained exit recovery before it stopped delivery, so an exit settled in that window could start a fresh agent that teardown then killed. Teardown stops delivery first; queued messages wait for the next launch. * docs(native-chat): the unverifiable-exit refusal no longer names a caller's wait The caller's bounded wait was removed; the comment describes the close as it is now. * fix(native-chat): a stop whose kill did not take is logged, and the next ask kills again When a close's kill leaves the agent's root running, the host now logs it. Tests pin what a later ask does: each connection runs its whole stop again (Codex sends SIGKILL a second time), refuses input meanwhile, and proves the exit once the kill takes. * fix(native-chat): a start refused over the old process says Orca couldn't stop it The host reaches an unverifiable verdict only after its own kill left the agent's root running, on the machine that runs the agent, so the sentence now says that: "Orca couldn't stop {agent}'s previous process." The refusal reason, failure kind and wire shapes are unchanged. The host test also checks the failed kill is logged. * docs(native-chat): an unverifiable close verdict is a root that survived the kill The host's close runs where the agent runs, so lost contact never yields this verdict; the comment no longer says it does. * fix(native-chat): a kill that did not take is reported once, by whoever met it The log added at the close fired beside a Stop's own failure report for the same event. A stop still reports it through its failure; a send or option change refused over it now logs it at the refusal, the only place it is otherwise invisible. * test(native-chat): a second Stop joins a close the first could not prove and retries its kill * fix(native-chat): say a start refused beside an unstopped process plainly The rejection now reads "Couldn't stop {{agent}} from before. Send your message again to try once more." This kind has its own send-again step; every other failure keeps "Send your message to try again." * Move provider process supervision and stream reading out of Codex * Preserve teardown behavior with checked mock types after move * Apply provider launch environment and caller teardown labels * Share managed provider launch, exit observation, and close * Preserve synchronous stdin closure before the grace wait * Keep the stacked Claude adapter within the module size limit * Handle nullable provider stdin and processless fixture identity * Preserve close-time cleanup diagnostics after a late provider exit * Separate provider root and descendant exit observations * Give provider child env one owner and gate Codex contract on the shared reader resolveProviderChildEnv is now the only place that overlays and strips a provider's environment; the spawn spec and the request-scoped Codex session both call it. supervisedPosixLaunch only accepts a launch without env fields, so an override can no longer be silently ignored there. Edits to the shared stream reader or the env rule now run the real-binary Codex contract job. * Give every provider one close result, stderr tail and root-only default - A close reports the root and the descendants with the one verdict vocabulary (live / unverifiable / exited); `tree` is null when the close made no descendant observation, and the fallback teardown's outcome is written into it instead of a side flag a caller could miss. - The managed process drains stderr and keeps the 8 KiB tail, so no provider can forget to drain the pipe. - Root-only providers take the default close policy and completion rule; only Claude overrides them. The one already-exited guard lives in the managed close and is recorded when no earlier close ran. - A failed spawn is never read as an observed root exit. * Report only observed descendant exits from the fallback teardown The fallback teardown answered "accepted", and the close turned that into `tree: 'exited'`, though a Windows tree kill's outcome is never read and an unreadable process table observes nothing. It now returns what it observed: `exited` only when the captured descendants were verified gone (or the kernel reported the group empty), `live` / `unverifiable` as verification found them, and null when it signalled without observing. Codex's process-tree diagnostic fires in exactly the cases it did before. * Give two test fixtures' casts a SAFETY rationale for the changed-code gate * Claim no observation from an empty process group ESRCH from the dedicated-group signal says only that the group is empty: a descendant that left it, or a root that never led it, may still be running. It now reports no observation instead of `exited`. The unreadable-table comment says why that case stays "no observation" for root-only providers. * 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): 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 made the same fix as this branch at a different line; the merge kept both imports.
* Move the turn message ordinals and the turn-row revision to the neutral timeline folder Pure moves so a shared timeline assembler can use them: Codex's message ordinal counter becomes ProviderTurnMessageOrdinals and Claude's turn-row revision becomes the provider-neutral agent-journal turn-row revision. Only names and import paths change. * Admit one provider event's writes as one transition, and let rows be found again after a restart - A sink transition is admitted whole or not at all; its steps run back to back at their turn in the journal's write queue, and each resolver reads the fold with every earlier write landed. A resolver may also say where the row belongs (turn scope, provider reference), and the writer always hears how the transition landed. A resolved lifecycle batch chooses its settlement mutations from the fold at execution. - New optional row field providerItemRef: the provider's own reference for the item a row is, written only where the row's identity cannot spell it (Codex keys messages by their place in the turn and renumbers its item ids on resume). Set by the creating write, kept by revisions, indexed by the journal fold, never read by clients. A downgrade test shows an older host and client render such rows unchanged. - Provider timeline identity schemes (shared legacy arm, Codex) and the join index that resolves a provider item to its row from memory or the fold: ordinals and request incarnations are read back from the rows, so a restart or an evicted entry finds the original row instead of placing a new one. * Recover message ordinals from the journal's highest place, and forget joins read from a replaced epoch A fresh join index continued a turn's messages at the first free place, so a journal holding only a later ordinal (an imported or removed earlier row) had its sequence back-filled. The place is now one past the highest ordinal any row or echoed send holds there, read through a pure scheme reader. The join caches also drop what they read when the journal's epoch is replaced. * Spell the subagent thread's message slot without spreading an identity union * Add a provider timeline grammar and a shared assembler that decides at its turn in the journal Adapters translate their provider's dialect into a small grammar (turns, items, streamed text, requests, context facts, session end/reset); one shared assembler turns it into the journal rows every structured lane writes. Each event is planned as one sink transition. Which row a write lands on, whether a replay writes anything, and every change to what the assembler knows (its ledger) are decided by the transition's resolvers at the event's turn in the journal's write queue, against the fold as it stands then. A forecast (the ledger plus admitted events still queued) only answers apply() at once. So a refused event allocates nothing, a write the journal rejects leaves no trace in memory, and a restart or evicted cache finds the same rows again. Text and full snapshots of one provider item share one row and one lifecycle; reset always flushes text and settles the old session from the journal; the open-work budget is derived from what is actually open. Codex migration contracts compare against the existing Codex translator, including a restart mid-stream and a repeat that outlives the join cache. * Fix the types and the exhaustive event switch CI reported for the assembler * Let the journal decide stream lifetimes, request reuse, named sends and background work A third review found two blockers with the earlier rounds' cause, a remembered interpretation trusted after the journal moved on: - A reused request id was judged by its earlier prompt's settled turn before asking which turn the new one lands in, so a real approval in a later turn was dropped. The target turn now decides: the old turn again is a replay; a different live turn opens the next prompt beside it. - A text stream checked its row's turn only on its first write, and a turn's end released streams by the turn planning expected. Every write now checks the row, a turn's end stops the streams whose rows are in it, and turn status reads the journal first, so another writer's Stop wins. Also: a message boundary drawn by an event the journal held as a replay no longer splits an anonymous message; a send naming a turn not yet open waits for that turn; the budget charges a stream's thread and turn strings and the turn caches are byte-bounded; the open turn ends when the journal shows it settled; a turn's opener is read from the journal's row. Background work is now Orca's existing background-task row instead of a tool call flagged `outlivesTurn` (a flag remembered only in memory, so a restart failed the task). A turn's end never settles that row, so it survives restarts; session end leaves one in flight unverifiable. Three tests that opened a background tool call with `outlivesTurn` now open a background-task row and keep their original expectations about which turn the row stays in. * Type the unbound assembler helper's drain as the void it reports * Bound the rows kept for a continued anonymous message and the stopped streams A row kept for the anonymous stream that may continue it, and the marker that a stopped stream's queued writes write nothing, lived in the live-stream map and were never removed when no stream followed. They now live in their own bounded maps, so the live map holds open streams only. * Read a tool call a stop or the session's end cut short as interrupted, not failed A call still running when its turn was interrupted, when the provider child was seen to exit, or that the provider cancelled now carries `endedAs: 'interrupted'` beside `state: 'failed'`. Every build that predates the field keeps reading the call as the failure it always showed; this build reads both through one shared reader and counts the call apart from failures in the run header, without the error tint on its partial output. The shared assembler, the restart settlement and the Codex lane settle a running call through one rule: only a proven interruption cuts it short, so a turn the provider completed around an unclosed call, or work the host lost track of, still reads failed. * Assert a completed Codex turn leaves its unclosed call plainly failed * Assert an unverifiable restart settlement leaves its running call plainly failed * Let a later death proof correct the calls an unverifiable settle closed A settle with no proof of the child's death closed its running calls as plain failed, so a proof written afterwards corrected the turn to interrupted but could not find the calls. The call now keeps endedAs: 'unverifiable' beside its failed state, and the stale settle revises exactly those calls whose owner the proof names. Older builds still read them as failed. * Render a cut-short call's output on mobile, with and without proof * Find the mobile result box by walking to its View, and type the unverified-ending block * Walk to the mobile result box without a type assertion * Keep the journal store under its line limit after the main merge * Drop the provider item reference, join index and identity schemes from the transition PR Nothing in production reaches the state they defended (an assembler that lost its memory while its child keeps streaming the same turn), and the stored Codex id was positional. The legacy identity scheme moves to the assembler PR with its first caller; the Codex scheme and any persisted reference wait for Codex to move onto the assembler. The Codex ordinal counter goes back to codex/, since no neutral code imports it. * Write a resolved settlement in one transaction through enqueueRows A settlement too large for one row now commits all its rows or none, through the journal's existing all-or-nothing write, instead of a row-by-row writer. Every row is built before any commits, so a settlement naming one item twice is refused before anything is written. * Drop the transition's landing report; keep the turn-row write fire-and-forget Nothing reads which steps of a transition wrote. A failed step fails the sink, leaving the steps before it written; the header says so, and tests cover it plus a settlement whose second row fails inside the transaction. writeAgentJournalTurnRow returns nothing again, as on main. * Run a transition's steps as a prefix; drop the paced flag and resolved options A failed step no longer lets the steps after it write: each step checks, at its own turn in the journal's queue, whether the write handed over just ahead of it completed, using the queue's count of completed write bodies (a promise would report the failure only after the next step ran). The sink fails only once every step has had its turn. The item step's `paced` size bypass and the resolver's replacement `options` are removed; nothing planned uses them. * Rebuild the timeline assembler on one admission-order state The assembler kept a second copy of its state (a forecast beside a ledger), hydrated the open turn from the journal, and recognised replays, all to recover from losing its memory while the provider child kept streaming. That never happens: one assembler lives exactly as long as one child, and a new child is a new assembler in a new generation whose events land after the dead-generation sweep. - One state, changed only when the sink admits an event (minted keys included), so a refused event takes nothing. - Every journal-dependent choice is made when the write runs, by keyed reads: a turn row is written only where none is, a stream checks its row's turn on every write, a running snapshot never lands in a settled turn or relights a settled tool, a request takes the first incarnation the journal holds no row for. - Rows are found by spelling their ids (provider-timeline-rows.ts); no join cache. - The identity scheme (legacy arm only) lives here with its first caller; requests are spelled in their acquisition generation, since JSON-RPC ids restart per process. - Saved history goes in as `input.history` plus ordinary events with the provider's ids, into an empty journal; `session.reset` and every replay rule are gone. - One terminal-body function (`terminalAgentJournalBody`) is shared with the dead-generation settlement. - The test rig's restart now sweeps and starts a new generation, as production does. * Cover new running work in a turn the sweep ended * Run a transition's steps in one queued write that loops over them The steps of one event now share one turn in the journal's write queue: a loop writes each in its own transaction through the row writer's synchronous writeRows (split out of enqueueRows) and stops at the first throw. Prefix semantics and "nothing lands between the steps" now hold by construction, so the completed-write counter on the queue, the step gate and the allSettled barrier are gone; the queue is back to main's bytes. * End a turn another writer settled the way the provider's end does A person's Stop settled the open turn's row without a word to the assembler. The assembler then forgot the turn: its running tools and pending prompts were never settled, the provider's own end and withdrawal were dropped, and the turn's text streams stayed counted against the open budget for the life of the process. Text the provider kept streaming afterwards could land as a message outside the stopped turn. - The open turn the journal shows settled ends first, as one transition, through the same settlement the provider's turn.end plans; its streams stop and their keys drop later text until that turn's end or the next turn opens. - turn.end and request.withdrawn are admitted for a turn or request the journal holds; their settlement writes nothing for rows already settled. - The budget's re-check frees streams whose turn settled. - A settled tool keeps its terminal body against any differing write. - Session-end settlement of lost background work uses the journal's own lostLiveWorkJournalBody instead of a copy. - The rig's window elapses before every read, and restart swaps and disposes the old assembler. * Leave a stopped turn's running tools to the agent's own end When another writer settles the open turn (a person's Stop), the assembler now only stops that turn's text and cancels its pending prompts. Running tool calls stay the agent's: a progress update or completion it reports after the Stop lands as reported, and whatever is still running settles at the agent's turn end for that turn, the next turn's open, or the session's end. An agent's end for an earlier turn while a newer one is open no longer clears the open turn's activity line or ends its anonymous reply. An unnamed end right after a Stop ends the stopped turn instead of being dropped. The test rig's restart no longer writes the dead assembler's window text, matching dispose. * Pin that a stopped turn's running tools hold budget until the agent's end * Type the stopped turn's tool progress update as a tool body * List every event the assembler hands to the decision step The type-aware lint requires an exhaustive switch with no default case. Also retitle a Stop test to say what it asserts. * End a running call as its turn's journal row ends A call still running when its turn ends takes the state of that turn's row: a row another writer settled first (a person's Stop) stands, so its calls read interrupted whatever the provider's later end reports. The no-ending path that settled calls from the Stop row is gone, since a Stop now leaves running calls to the provider. Adds the two Spanish strings. * Settle a stopped turn's running call as its turn row ended after a restart too The restart sweep ended every running call by the death evidence alone, so after a person's Stop with no proof the child died the call read failed under a turn that read interrupted. The sweep and the live dead-generation settlement now ask the same rule the assembler does: a call in a turn already settled ends as that row ended; only a turn still running leaves its calls to the evidence. * Keep the dead-generation settlement under the line cap * 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(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(ratchet): require src/main/provider-process now that it has landed * 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.
…registered agents, behind a negotiated capability (#25159) * refactor(native-chat): keep the provider resume handle opaque to shared code Shared structured-chat code parsed each provider's resume handle: Claude's session id and branch leaf, Codex's thread id, through a 'claude' | 'codex' union every new agent had to widen. The in-memory handle is now { transport, agent, nativeId, providerData? }: shared readers use nativeId, lease and handle-chain checks compare transport and agent, and only the Claude adapter reads its leaf (providerData). Stored and wire forms are unchanged for Claude and Codex. One encoding module writes their typed shapes and decodes both those and the neutral shape a new transport uses, which an older build refuses as unreadable rather than reading as Codex. Key and root strings, which fork seeds, superseded creations and resume offers persist, stay byte-identical. The journal's own handle type becomes the journal-row and attach-wire encoding of the same handle, and the journal identity carries the neutral handle (null before the provider proves one). No user-visible change. * fix(native-chat): derive journal-row provider handles from the journal identity The journal row converter now takes the identity every caller already holds, so a row's handle has one obvious constructor. Tests that wrote the in-memory handle straight into journal rows now build it through that converter, and the processless Claude fixture names a not-yet-proved handle as null. * fix(native-chat): refuse a stored provider handle written in both forms A typed Claude or Codex handle that also carries the neutral form's transport, agent, native id or provider data named two identities; it was read as Claude or Codex and the next write dropped the other one. Such a row now stays unreadable and is set aside untouched. * refactor(native-chat): route structured agents through registered definitions The structured-chat shared layer named Claude and Codex in a closed router type, in option reads, in frame classification, and in renderer label and validation branches, so adding an agent meant editing shared types. Each agent now has a definition (capabilities, resting option rules) owned by its own module; the router is a registry of definition + adapter pairs built at runtime setup, and shared code reads the definition instead of the name. No behavior change for Claude or Codex; no wire or stored shape change. * refactor(native-chat): name the structured agent list once in host types The host types, conversation commands, provider-session ownership and the runtime's create path spelled the structured agent list inline; they now use the one shared type the record and wire already validate against. * fix(native-chat): narrow the record before reading its agent's option rules * refactor(native-chat): make the router's registrations the only agent definition lookup The at-rest option reads looked definitions up in a second, separately populated map, so a registered definition could route and declare capabilities while the static map decided which options a chat at rest accepted. The router now answers `definition(agent)` from its registrations, the at-rest readers take it through the host's adapter, and the built-in definitions are listed only where the runtime composes the router. * feat(native-chat): open the structured-chat wire and stored records to registered agents A host's structured agents are the ones its runtime registered. Records, RPC params, persisted tabs and the model catalog accept any registered agent instead of naming Claude and Codex; each agent's definition declares the transport its handles live in and the variable its account home pins. A new runtime capability, agent-session.structured.registered-agents.v1, advertises that a host accepts and lists its agents (agentSession.agents, with each agent's capability record), and the host withholds any other agent's tabs and restart offers from clients that do not advertise it. * test(native-chat): cover registered agents on the wire, in storage and across versions * refactor(native-chat): let the record store decide which agents' tabs exist * test(native-chat): declare the pilot test agent's storage * test(native-chat): read the old build's saved tabs through a parsed shape * fix(native-chat): act on restart offers only for agents the calling client can show A paired client too old to show an agent's chat was listed only the offers it could show, but dismissing or continuing all reached every offer on the host, and a named continuation answered with the host's whole remaining inventory. The client's audience now goes to the host with every restart operation: only offers it sees are reserved, dismissed or returned. Without an audience (this host's own process, or a client that shows every agent) nothing changes. * test(native-chat): read an agent-registering baseline's storage on its own terms The registered-agents downgrade test assumed its baseline release predates registered agents: it expected the saved-tab parser to erase an unknown agent and called the record reader without the agents list. Once a release with this change becomes the baseline, both break. The expectations now follow what the baseline host advertises, and an agent-registering baseline is handed its own Claude and Codex storage. * refactor(native-chat): derive record-store admission from the runtime's agent registrations Which agents a stored record may name and which agents the router drives came from two lists in the runtime, so a newly registered agent could be routed while its records were set aside. One list of registrations now holds each agent's definition and the factory for its adapter: the store's admitted agents are derived from it before the store opens, and the adapters are built from it once it has. * fix(native-chat): hand the exit drain a promise for every registered agent * fix(native-chat): let the adoption conflict check read any agent's ownership Ownership rows name any registered agent since the stored records opened to them; the adoption check compares by agent, so it takes the same open id. Only Claude and Codex still adopt. * test(native-chat): use opaque handle in queued rejection fixture * test(native-chat): share one Codex journal identity in the integration suite Main grew the suite to the 800-line limit; the opaque-handle import pushed it over. The two tests built the same identity inline. * refactor(agent-session): name the handle's adapter state resumeCursor Rename the neutral provider handle's providerData to resumeCursor before any row persists the neutral form: it is an adapter-owned resume position (Claude's transcript leaf), never identity. Claude/Codex stored and wire bytes are unchanged; their typed shapes never carried the field. State the stored-form contract (a handle's field set is closed; later per-link data goes on the chain link, which every build preserves) and pin it with a record round-trip test. Document that transport records the id space the native id was minted in, which can differ from the agent's current transport. * refactor(agent-session): one required agent registry; declarations admit what they claim A StructuredAgentRegistry, built once from the {definition, adapter} registrations, is now a required host dependency and the router routes with it. The adapter interface loses its optional router-only capabilities?() and definition?(); every reader (options at rest, thread goal, rewind, the /compact handover) asks the registry. A live session still narrows rewind through the adapter, and the host combines declared and narrowed in one helper. The registry refuses a registration that declares compact, a thread goal or rewind without the adapter method behind it. /compact is admitted by the declared capability, so an agent that declares compact:false gets the commandRefused fact instead of a thrown error. Also: the cut-turn notice names the agent from the catalog, create-support builds the account home through agentSessionAccountHome, and the turn status text older clients read names the session's agent instead of defaulting to Codex (Claude/Codex text unchanged). * refactor(agent-session): the router applies the declared rewind itself The router holds the registry, so its rewindSupport answers the owner's declared rewind narrowed by the adapter, in one place no reader can bypass. The host-side combining helper is gone; readers ask the adapter they hold. * test(agent-session): register the agents the merged-in tests now need The adoption replay test builds its host over this build's registered agents instead of an adapter definition method, and the stored-form test passes the stored agents the record guard now requires. * chore(agent-session): the registry is the one lookup; drop the test-only empty capability record * fix(agent-session): keep "dismiss all" restart offers dismissed for the desktop The desktop renderer calls the restart methods as a paired runtime client without the registered-agents capability, so it was handed a Claude/Codex audience and its "dismiss all" took the scoped path: no dismissal fence and no clearing of unwritten teardown witnesses, so a late teardown write could bring a dismissed offer back. The audience is now derived against this host's registry: a client that can show every agent the host registered gets none, and dismisses exactly as before (one fence for every offer). A client that truly cannot show some agent still gets a final dismissal for what it sees: each cleared session is fenced on its own (bounded, superseded by a dismiss-all fence) and only the witnesses of agents it sees are dropped. One predicate now answers which agents a client renders, for both tab projection and restart offers; the Claude capability rule is folded in. * fix(agent-session): a changed agent definition never hides that agent's chats A record was readable only if every handle used the transport its agent's definition declares today, and its account variable was declared by some registered agent. A later build that drives the same agent over another protocol, or under a renamed variable, would have set every existing chat of that agent aside: no tab, no history, no error. Readability now asks only that the record agree with itself: its agent is registered, every handle shares one namespace owned by that agent, and the account variable is a well-formed name. Whether this build can drive it (the chain's transport is the one the agent speaks, and the account variable is that agent's own, since it becomes the child's environment) is decided in performAttach, the one admission every start of every agent passes through, and refused there as hostUnsupported. A record pinning another agent's variable is refused the same way rather than passing because some agent declares it. * refactor(agent-session): each agent's registration says where it runs and which account it pins createSupport, create and the model catalog's account read still chose a location rule and an account resolver by name (Claude, Codex, else none), so a new agent would have needed a third branch beside the registration list that already decides storage, routing and publication. Each runtime registration now carries supportsLocation(location) and resolveAccountHomePath({ launchEnv, location, purpose, workspacePath }), and Claude's and Codex's rules move into their entries unchanged: a read still syncs no home and starts no bridge, a Codex launch still trusts the workspace first, and WSL locations resolve as before. The list exists before the host is built, so none of these reads installs the host, and an agent the list does not hold is answered no without opening the journal. * test(agent-session): drop a duplicate registry key; keep fences out of the capsule state A spread input already carries the registry, so the explicit key was overwritten (TS2783). The session-fence helper now returns only the fence, not the capsule state it was given. * fix(agent-session): a scoped dismiss-all persists no per-session fence The per-session dismissal fence added a forever-persisted capsule field with an arbitrary cap that served no caller: only the local desktop calls the restart methods, and on a host whose agents it all shows it gets no audience and takes the unscoped, fenced dismissal. A scoped dismiss-all (a caller that cannot show some registered agent) keeps that agent's offers, clears only its audience's unwritten witnesses, and writes no fence; a late write from this process is serialized behind it. The field never left this unreleased branch. * fix(agent-session): refuse an attach whose agent is not the session's own The start check judges the record's agent while the router starts the adapter of params.agent, and the attach wire let the two differ. Admission now refuses agent !== provider as requestMalformed, so the agent checked is the agent started. Every host-built attach already sets them equal. * fix(agent-session): offer to start a chat only when the start would accept it The restart offer, a failure's retryable flag and the pre-send check asked only whether the adapter runs the chat's location, while the start also refuses a record this build cannot drive; such a chat was offered Resume and its Retry failed forever. One host predicate, hostCanStartRecord (location support and agentDrivesSession), now answers all four; adapterSupportsRecord stays the reader gate, and an undrivable chat's offer is kept, not retired. * fix(agent-session): the model-catalog probe uses a record's account only if this build drives it A record-scoped catalog read started the agent's lister under the record's account path whatever variable the record pinned. It now uses that path only when agentDrivesSession holds, and otherwise resolves the account as for a read with no record. * refactor(agent-session): the record store admits agent ids; comments say where transport is checked The store only ever asked whether an agent is registered, so its admission list is now the registered ids; the definition is the one home of an agent's transport and account variable. Comments that said the record store checks transport, and one that cited a nonexistent function, now point at agentDrivesSession and the attach admission. The isPersistedAgentSessionRecord(value, agents) call shape and the test fixture the cross-version probe imports are kept. * docs(agent-session): the record store admits the registered agents' ids * 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. * chore(native-chat): keep the record store and recovery capsule under max-lines after the main merge * 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(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. * Keep saved chats readable without provider registration * Keep stored-record compatibility checks independent of registration * Keep saved providers in restart client audiences * test(native-chat): type reveal fixtures without assertions * test(wire): expose known agents in structured host fixture * Supply startability dependency in the new Codex catalog fixture
…nished while it was hidden (#24305) * fix(native-chat): a chat you return to drops background tasks that finished while it was hidden A background command that finished while its chat pane was hidden stayed in the strip as running, its timer counting for hours, and Stop failed with "The background task wasn't stopped." The pane stops listening while hidden, so it missed the "no tasks" update; once the idle sweep stopped the agent, the host reopened the pane's subscription without any roster at all, and the client reads a missing roster as "unchanged". The roster now rides each subscriber's frames the way the slash-command list and the message queue already do. The subscriber registry reads it from the host's child records on a subscriber's first frame and on every snapshot and reset, and re-sends it to each subscriber whose last copy differs when the records change. The channel always answers: a conversation with no running child is "none" whether or not an agent holds it. Ordinary journal batches never read it. This also fixes the reverse case: a snapshot or reset sent to a live pane carried no roster, which cleared a still-running task from the strip until the roster next changed. The client treats a resumed subscription's first batch as stating the roster, so a pane reconnecting to an older host that still omits it drops its stale copy too. Fixes #24227 * refactor(native-chat): require the background-task roster read and drop the unused close observers The host's delivery layer now requires every hook it is built with, so production wiring cannot leave out the roster read (an opening frame without it reads as "no tasks" to current clients). The conversations map's close observers lost their only caller in this PR and are removed. The reducer test comment states the old-host omission rule precisely. * refactor(native-chat): keep structured-agent-session-host.ts within the line limit Main brought the host file to the 300-line lint limit, and this PR's roster wiring adds one line. Name the lease reconciler's type by its factory, as the neighbouring fields do. * test(native-chat): build the roster test's Claude provider handle with claudeProviderHandle Main made the provider handle opaque (#24991); build it with the existing helper, as main's own tests now do.
…otocol (#25225) * Leave a stopped turn's running tools to the agent's own end When another writer settles the open turn (a person's Stop), the assembler now only stops that turn's text and cancels its pending prompts. Running tool calls stay the agent's: a progress update or completion it reports after the Stop lands as reported, and whatever is still running settles at the agent's turn end for that turn, the next turn's open, or the session's end. An agent's end for an earlier turn while a newer one is open no longer clears the open turn's activity line or ends its anonymous reply. An unnamed end right after a Stop ends the stopped turn instead of being dropped. The test rig's restart no longer writes the dead assembler's window text, matching dispose. * Pin that a stopped turn's running tools hold budget until the agent's end * Type the stopped turn's tool progress update as a tool body * List every event the assembler hands to the decision step The type-aware lint requires an exhaustive switch with no default case. Also retitle a Stop test to say what it asserts. * End a running call as its turn's journal row ends A call still running when its turn ends takes the state of that turn's row: a row another writer settled first (a person's Stop) stands, so its calls read interrupted whatever the provider's later end reports. The no-ending path that settled calls from the Stop row is gone, since a Stop now leaves running calls to the provider. Adds the two Spanish strings. * Say why a Grok turn failed, and keep task rows in Grok's own words A failed Grok turn ended with no reason on screen: the translator dropped every copy of Grok's message. The failed turn now gets one status row in Orca's existing "provider did not accept this message" words with Grok's reason, read from whichever copy arrives first (the given-up retry, the turn's end, the prompt's completion notice, or the prompt's error answer); later copies only fill a reason the row still lacks. A running background command no longer reads "Background task <id> started": a task's summary is mapped only once it has settled. A monitor stays a monitor when the agent reads its output: a frame that names no kind keeps the known one, and a "[monitor" command is a monitor. A prompt's turn is marked started, so a late frame for an ended prompt neither reopens it nor becomes the active turn. A tool's turn is held in one place at a time. * Read a monitor from Grok's exact output prefix * Word a failed Grok turn in Grok's own text, not as a refused message A turn that started and then failed was told "The provider did not accept this message", Orca's sentence for a message refused before its turn. The row now reads as a Codex turn-ending error does: an error status row with the provider's own words. With no words, the dialect names the failure ("Grok ended this turn with an error." / "Grok usage limit reached."), else the agent's display name does. * Settle a stopped turn's running call as its turn row ended after a restart too The restart sweep ended every running call by the death evidence alone, so after a person's Stop with no proof the child died the call read failed under a turn that read interrupted. The sweep and the live dead-generation settlement now ask the same rule the assembler does: a call in a turn already settled ends as that row ended; only a turn still running leaves its calls to the evidence. * Keep the dead-generation settlement under the line cap * Register the ACP schema verify step in the PR preflight phase test * refactor(agent-session): one required agent registry; declarations admit what they claim A StructuredAgentRegistry, built once from the {definition, adapter} registrations, is now a required host dependency and the router routes with it. The adapter interface loses its optional router-only capabilities?() and definition?(); every reader (options at rest, thread goal, rewind, the /compact handover) asks the registry. A live session still narrows rewind through the adapter, and the host combines declared and narrowed in one helper. The registry refuses a registration that declares compact, a thread goal or rewind without the adapter method behind it. /compact is admitted by the declared capability, so an agent that declares compact:false gets the commandRefused fact instead of a thrown error. Also: the cut-turn notice names the agent from the catalog, create-support builds the account home through agentSessionAccountHome, and the turn status text older clients read names the session's agent instead of defaulting to Codex (Claude/Codex text unchanged). * Read ACP permissions, session events and prompt errors through the protocol client's own types The translator now reads a permission request with the client's lenient reader, a session update with its session-event reader, and takes only the agent's own error answer as a failed prompt's reason, so an Orca-side error never reads as the provider's words. Tests cover protocol values newer than this build. * refactor(agent-session): the router applies the declared rewind itself The router holds the registry, so its rewindSupport answers the owner's declared rewind narrowed by the adapter, in one place no reader can bypass. The host-side combining helper is gone; readers ask the adapter they hold. * test(agent-session): register the agents the merged-in tests now need The adoption replay test builds its host over this build's registered agents instead of an adapter definition method, and the stored-form test passes the stored agents the record guard now requires. * chore(agent-session): the registry is the one lookup; drop the test-only empty capability record * fix(agent-session): keep "dismiss all" restart offers dismissed for the desktop The desktop renderer calls the restart methods as a paired runtime client without the registered-agents capability, so it was handed a Claude/Codex audience and its "dismiss all" took the scoped path: no dismissal fence and no clearing of unwritten teardown witnesses, so a late teardown write could bring a dismissed offer back. The audience is now derived against this host's registry: a client that can show every agent the host registered gets none, and dismisses exactly as before (one fence for every offer). A client that truly cannot show some agent still gets a final dismissal for what it sees: each cleared session is fenced on its own (bounded, superseded by a dismiss-all fence) and only the witnesses of agents it sees are dropped. One predicate now answers which agents a client renders, for both tab projection and restart offers; the Claude capability rule is folded in. * fix(agent-session): a changed agent definition never hides that agent's chats A record was readable only if every handle used the transport its agent's definition declares today, and its account variable was declared by some registered agent. A later build that drives the same agent over another protocol, or under a renamed variable, would have set every existing chat of that agent aside: no tab, no history, no error. Readability now asks only that the record agree with itself: its agent is registered, every handle shares one namespace owned by that agent, and the account variable is a well-formed name. Whether this build can drive it (the chain's transport is the one the agent speaks, and the account variable is that agent's own, since it becomes the child's environment) is decided in performAttach, the one admission every start of every agent passes through, and refused there as hostUnsupported. A record pinning another agent's variable is refused the same way rather than passing because some agent declares it. * refactor(agent-session): each agent's registration says where it runs and which account it pins createSupport, create and the model catalog's account read still chose a location rule and an account resolver by name (Claude, Codex, else none), so a new agent would have needed a third branch beside the registration list that already decides storage, routing and publication. Each runtime registration now carries supportsLocation(location) and resolveAccountHomePath({ launchEnv, location, purpose, workspacePath }), and Claude's and Codex's rules move into their entries unchanged: a read still syncs no home and starts no bridge, a Codex launch still trusts the workspace first, and WSL locations resolve as before. The list exists before the host is built, so none of these reads installs the host, and an agent the list does not hold is answered no without opening the journal. * test(agent-session): drop a duplicate registry key; keep fences out of the capsule state A spread input already carries the registry, so the explicit key was overwritten (TS2783). The session-fence helper now returns only the fence, not the capsule state it was given. * fix(agent-session): a scoped dismiss-all persists no per-session fence The per-session dismissal fence added a forever-persisted capsule field with an arbitrary cap that served no caller: only the local desktop calls the restart methods, and on a host whose agents it all shows it gets no audience and takes the unscoped, fenced dismissal. A scoped dismiss-all (a caller that cannot show some registered agent) keeps that agent's offers, clears only its audience's unwritten witnesses, and writes no fence; a late write from this process is serialized behind it. The field never left this unreleased branch. * fix(agent-session): refuse an attach whose agent is not the session's own The start check judges the record's agent while the router starts the adapter of params.agent, and the attach wire let the two differ. Admission now refuses agent !== provider as requestMalformed, so the agent checked is the agent started. Every host-built attach already sets them equal. * fix(agent-session): offer to start a chat only when the start would accept it The restart offer, a failure's retryable flag and the pre-send check asked only whether the adapter runs the chat's location, while the start also refuses a record this build cannot drive; such a chat was offered Resume and its Retry failed forever. One host predicate, hostCanStartRecord (location support and agentDrivesSession), now answers all four; adapterSupportsRecord stays the reader gate, and an undrivable chat's offer is kept, not retired. * fix(agent-session): the model-catalog probe uses a record's account only if this build drives it A record-scoped catalog read started the agent's lister under the record's account path whatever variable the record pinned. It now uses that path only when agentDrivesSession holds, and otherwise resolves the account as for a read with no record. * refactor(agent-session): the record store admits agent ids; comments say where transport is checked The store only ever asked whether an agent is registered, so its admission list is now the registered ids; the definition is the one home of an agent's transport and account variable. Comments that said the record store checks transport, and one that cited a nonexistent function, now point at agentDrivesSession and the attach admission. The isPersistedAgentSessionRecord(value, agents) call shape and the test fixture the cross-version probe imports are kept. * docs(agent-session): the record store admits the registered agents' ids * 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. * 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. * 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. * 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 0ef6d21. 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. * 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. * 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. * 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). * fix(acp): cancel a running turn before a close, dispose or quit ends the agent Closing a tab, disposing a session or quitting while an ACP agent's turn ran killed the process without asking the agent to cancel first. A requested close now does what a Stop does: withdraw the agent's open requests and held steers, send session/cancel once, and wait for the turn to end, bounded by the Stop's grace (4 s), before closing the process. A close that follows a Stop sends no second cancel and waits only what is left of that Stop's grace. An idle close, a lost connection and a sink-failure force close are unchanged. The cancel-and-wait moves to acp-structured-stop.ts, shared by Stop and close. * test(codex): pass resolveLaunchArgs in the stopped-send-order test so typecheck passes Main's typecheck fails here too: #25721 made resolveLaunchArgs required and this test (#25051) predates it. Same line, same place as the open main fix (#25977), so merging main after it lands is a no-op. * test(native-chat): run the close-aborts-start test on the Node runtime, as its SQLite journal fixture requires * refactor(native-chat): the Stop control reads the outbox itself, so the chat hook stays within its line limit * Keep the session host under the line cap after main's two new delegates Pass the lifetime's conversation opener to reveal directly; it is already passed unbound to the mutation context.
* 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 0ef6d21. 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
* End a running call as its turn's journal row ends
A call still running when its turn ends takes the state of that turn's
row: a row another writer settled first (a person's Stop) stands, so its
calls read interrupted whatever the provider's later end reports. The
no-ending path that settled calls from the Stop row is gone, since a Stop
now leaves running calls to the provider. Adds the two Spanish strings.
* Say why a Grok turn failed, and keep task rows in Grok's own words
A failed Grok turn ended with no reason on screen: the translator dropped
every copy of Grok's message. The failed turn now gets one status row in
Orca's existing "provider did not accept this message" words with Grok's
reason, read from whichever copy arrives first (the given-up retry, the
turn's end, the prompt's completion notice, or the prompt's error answer);
later copies only fill a reason the row still lacks.
A running background command no longer reads "Background task <id>
started": a task's summary is mapped only once it has settled. A monitor
stays a monitor when the agent reads its output: a frame that names no
kind keeps the known one, and a "[monitor" command is a monitor.
A prompt's turn is marked started, so a late frame for an ended prompt
neither reopens it nor becomes the active turn. A tool's turn is held in
one place at a time.
* Read a monitor from Grok's exact output prefix
* Word a failed Grok turn in Grok's own text, not as a refused message
A turn that started and then failed was told "The provider did not accept
this message", Orca's sentence for a message refused before its turn. The
row now reads as a Codex turn-ending error does: an error status row with the
provider's own words. With no words, the dialect names the failure ("Grok
ended this turn with an error." / "Grok usage limit reached."), else the
agent's display name does.
* Settle a stopped turn's running call as its turn row ended after a restart too
The restart sweep ended every running call by the death evidence alone, so after
a person's Stop with no proof the child died the call read failed under a turn
that read interrupted. The sweep and the live dead-generation settlement now ask
the same rule the assembler does: a call in a turn already settled ends as that
row ended; only a turn still running leaves its calls to the evidence.
* Keep the dead-generation settlement under the line cap
* Register the ACP schema verify step in the PR preflight phase test
* refactor(agent-session): one required agent registry; declarations admit what they claim
A StructuredAgentRegistry, built once from the {definition, adapter}
registrations, is now a required host dependency and the router routes with
it. The adapter interface loses its optional router-only capabilities?() and
definition?(); every reader (options at rest, thread goal, rewind, the
/compact handover) asks the registry. A live session still narrows rewind
through the adapter, and the host combines declared and narrowed in one
helper. The registry refuses a registration that declares compact, a thread
goal or rewind without the adapter method behind it.
/compact is admitted by the declared capability, so an agent that declares
compact:false gets the commandRefused fact instead of a thrown error.
Also: the cut-turn notice names the agent from the catalog, create-support
builds the account home through agentSessionAccountHome, and the turn
status text older clients read names the session's agent instead of
defaulting to Codex (Claude/Codex text unchanged).
* Read ACP permissions, session events and prompt errors through the protocol client's own types
The translator now reads a permission request with the client's lenient reader, a session update
with its session-event reader, and takes only the agent's own error answer as a failed prompt's
reason, so an Orca-side error never reads as the provider's words. Tests cover protocol values
newer than this build.
* refactor(agent-session): the router applies the declared rewind itself
The router holds the registry, so its rewindSupport answers the owner's
declared rewind narrowed by the adapter, in one place no reader can bypass.
The host-side combining helper is gone; readers ask the adapter they hold.
* test(agent-session): register the agents the merged-in tests now need
The adoption replay test builds its host over this build's registered agents
instead of an adapter definition method, and the stored-form test passes the
stored agents the record guard now requires.
* chore(agent-session): the registry is the one lookup; drop the test-only empty capability record
* fix(agent-session): keep "dismiss all" restart offers dismissed for the desktop
The desktop renderer calls the restart methods as a paired runtime client
without the registered-agents capability, so it was handed a Claude/Codex
audience and its "dismiss all" took the scoped path: no dismissal fence and
no clearing of unwritten teardown witnesses, so a late teardown write could
bring a dismissed offer back.
The audience is now derived against this host's registry: a client that can
show every agent the host registered gets none, and dismisses exactly as
before (one fence for every offer). A client that truly cannot show some
agent still gets a final dismissal for what it sees: each cleared session is
fenced on its own (bounded, superseded by a dismiss-all fence) and only the
witnesses of agents it sees are dropped.
One predicate now answers which agents a client renders, for both tab
projection and restart offers; the Claude capability rule is folded in.
* fix(agent-session): a changed agent definition never hides that agent's chats
A record was readable only if every handle used the transport its agent's
definition declares today, and its account variable was declared by some
registered agent. A later build that drives the same agent over another
protocol, or under a renamed variable, would have set every existing chat of
that agent aside: no tab, no history, no error.
Readability now asks only that the record agree with itself: its agent is
registered, every handle shares one namespace owned by that agent, and the
account variable is a well-formed name. Whether this build can drive it (the
chain's transport is the one the agent speaks, and the account variable is
that agent's own, since it becomes the child's environment) is decided in
performAttach, the one admission every start of every agent passes through,
and refused there as hostUnsupported. A record pinning another agent's
variable is refused the same way rather than passing because some agent
declares it.
* refactor(agent-session): each agent's registration says where it runs and which account it pins
createSupport, create and the model catalog's account read still chose a
location rule and an account resolver by name (Claude, Codex, else none),
so a new agent would have needed a third branch beside the registration
list that already decides storage, routing and publication.
Each runtime registration now carries supportsLocation(location) and
resolveAccountHomePath({ launchEnv, location, purpose, workspacePath }),
and Claude's and Codex's rules move into their entries unchanged: a read
still syncs no home and starts no bridge, a Codex launch still trusts the
workspace first, and WSL locations resolve as before. The list exists
before the host is built, so none of these reads installs the host, and an
agent the list does not hold is answered no without opening the journal.
* test(agent-session): drop a duplicate registry key; keep fences out of the capsule state
A spread input already carries the registry, so the explicit key was
overwritten (TS2783). The session-fence helper now returns only the fence,
not the capsule state it was given.
* fix(agent-session): a scoped dismiss-all persists no per-session fence
The per-session dismissal fence added a forever-persisted capsule field
with an arbitrary cap that served no caller: only the local desktop calls
the restart methods, and on a host whose agents it all shows it gets no
audience and takes the unscoped, fenced dismissal. A scoped dismiss-all
(a caller that cannot show some registered agent) keeps that agent's
offers, clears only its audience's unwritten witnesses, and writes no
fence; a late write from this process is serialized behind it. The field
never left this unreleased branch.
* fix(agent-session): refuse an attach whose agent is not the session's own
The start check judges the record's agent while the router starts the adapter of params.agent, and the attach wire let the two differ. Admission now refuses agent !== provider as requestMalformed, so the agent checked is the agent started. Every host-built attach already sets them equal.
* fix(agent-session): offer to start a chat only when the start would accept it
The restart offer, a failure's retryable flag and the pre-send check asked only whether the adapter runs the chat's location, while the start also refuses a record this build cannot drive; such a chat was offered Resume and its Retry failed forever. One host predicate, hostCanStartRecord (location support and agentDrivesSession), now answers all four; adapterSupportsRecord stays the reader gate, and an undrivable chat's offer is kept, not retired.
* fix(agent-session): the model-catalog probe uses a record's account only if this build drives it
A record-scoped catalog read started the agent's lister under the record's account path whatever variable the record pinned. It now uses that path only when agentDrivesSession holds, and otherwise resolves the account as for a read with no record.
* refactor(agent-session): the record store admits agent ids; comments say where transport is checked
The store only ever asked whether an agent is registered, so its admission list is now the registered ids; the definition is the one home of an agent's transport and account variable. Comments that said the record store checks transport, and one that cited a nonexistent function, now point at agentDrivesSession and the attach admission. The isPersistedAgentSessionRecord(value, agents) call shape and the test fixture the cross-version probe imports are kept.
* docs(agent-session): the record store admits the registered agents' ids
* 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.
* 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.
* 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.
* feat(native-chat): add inactive Pi RPC transport foundation
* 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.
* 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.
* feat(native-chat): add bounded RPC reading control
* 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 0ef6d21. 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).
* Add Pi structured chat over its native RPC mode
* Use shared provider spawn identity and base environment
* Complete Pi startup ownership and prompt delivery safeguards
* Use generic failure copy for Pi RPC sessions
* Admit finalized Pi output before session eviction
* Apply Pi client capability checks to all chat routes
* Keep older Pi versions on terminal chat
* Use supervised process support checks for Pi launches
* Complete the Pi location-test adapter fixture
* Retain the settled-work extraction in the main merge
* Use the settled-work module in the new reasoning sweep test
* Test that Pi prompt alerts skip clients that cannot read Pi
Main added prompt alerts to the turn-completion stream; the merge extended Pi's audience filter to them.
* Pin why a timed-out write closes the agent connection only once it is being written
A line still waiting in the queue is dropped and only its request fails, as before. A line already handed to the stream cannot be taken back and would hold every later write, a cancel included, so that timeout closes the connection; tests now cover both for the shared queue and for ACP.
* Type the prompt-alert fixture as the wire's prompt attention
Its host id is a branded type; the literal only checks under the wire type, as the completion fixture beside it does.
* Run the Pi launch-resolution test in the Node runtime project
It opens a real SQLite record store, which main's new boundary check now requires to run on Node.
* Pin that a Pi chat reopened after a restart resumes its stored session file
A fresh record store reading the journal back from disk must hand the launch the session file the chat proved before the restart, and the spawn must carry it as --session.
* Show an explicitly empty text answer as "Empty answer", not as unreadable
Live QA: answering a Pi input dialog with nothing (which Pi accepts) left the resolved card reading "Selected answer unavailable", the wording for an answer Orca cannot read. The receipt joined the answer's labels and text and turned an empty result into "no answer". It now keeps an answer whose text is present but empty (or whitespace) as an empty answer, shown as "Empty answer"; an answer naming neither text nor a known option still reads as unavailable.
Also pins that the editor dialog submits the text the user edited, not its prefill.
* refactor(pi): check and resolve Pi's binary through the shared version probe and command resolver
Pi's own `--version` runner and PATH lookup are gone. Create support and
every launch now resolve the binary through resolveStructuredAgentCommand,
so the Pi Command setting picks the binary both check and spawn, and an
unrunnable Command is refused at launch with agentCommandNotRunnable as for
the other agents. The version check runs through probeAgentCliVersion with
the same rule as before: stable 1.x of the current package; the older 0.x
package keeps the terminal chat. Pi's launch reads the shared per-agent
environment overlay and Command settings instead of Pi-only runtime deps.
* feat(pi): run stable Pi from 0.84.0 in the structured chat, not only 1.x

ELI5
Every structured native chat keeps a bookmark that says which conversation inside the agent it continues (Claude calls it a session, Codex calls it a thread). Until now, Orca's shared code knew the exact layout of that bookmark for Claude and for Codex, so adding any new agent meant teaching a dozen shared files a third layout. This PR turns the bookmark into a generic id plus a sealed note that only the agent's own code opens. Nothing changes for anyone using the app today; it is groundwork so the next agents (starting with Grok over the Agent Client Protocol) plug in without touching shared code.
What Changed
The problem. The "provider handle" (the durable pointer from an Orca chat to the provider's own conversation) was a typed union in shared code:
{ provider: 'claude', sessionId, leafUuid } | { provider: 'codex', threadId }. Shared readers picked fields by provider: ownership lists, orchestration mail addressing, history results, the record validator, the lease and handle-chain checks, and the journal identity. Claude'sleafUuid(which transcript entry a resume continues from, a branch cursor only Claude has) was interpreted in shared runtime code. A new transport would have had to add a union arm and a branch at every one of those sites.User-facing change: none. Claude and Codex chats create, resume, fork, rewind and recover exactly as before, and everything stored on disk or sent over the wire for them is byte-for-byte the same.
The mechanism.
src/shared/agent-session-provider-handle.ts):{ transport, agent, nativeId, resumeCursor? }.transportis an open vocabulary for the protocol whose id spacenativeIdlives in ('claude-sdk','codex-app-server', later'acp').agentis the Orca agent whose binary resumes the conversation. One transport serves many agents, so the transport alone does not scope an id.nativeIdis the provider's own conversation id (Claude's session id, Codex's thread id). Shared code reads only this.resumeCursoris resume state only that provider's adapter reads (where a resume continues from), never identity. For Claude it is the leaf uuid; Codex has none.transportrecords the id space the native id was created in. If an agent later changes transport, its old chats must become not resumable on that build, never unreadable records.nativeIdonly: ownership listing, mail-address lookup, the history result's provider session id, the rewind target check, journal recovery, and adopted-transcript import.agentSessionProviderHandleBelongsTo) and never read the handle's data. The chain rules (created / adopted / resumed / forked, unsaved-creation supersession, fence ordering) stay on the link and are unchanged.reviseAgentSessionClaudeResumePointbecame the genericreviseAgentSessionProviderResumePoint, which takes a whole replacement handle and only refuses one that names another conversation. The Claude runtime adapter builds that handle from its leaf. Claude launch resolution and the Claude history window read the leaf throughclaudeProviderHandleLeafUuid.src/shared/agent-session-provider-handle-encoding.ts):transport,agent,nativeIdorresumeCursorfields names two identities, so it is refused rather than read as Claude/Codex (which would silently drop the other identity on the next write). The whole record is then set aside as unreadable and kept byte for byte. Unrelated extra fields on a typed handle are still tolerated on read (and dropped on the next write).provider === 'claude'/'codex'and refuse it, so it is set aside as unreadable (never rewritten, every mutation refused) instead of being read as Codex. The "unknown providers must not impersonate Codex" guard keeps its intent.claude:["sid","leaf"],claude:"sid",codex:"tid").src/shared/agent-session-record-stored-form.ts) wraps the existing lease normalization. Rows are decoded on load and encoded on every write: the store's row diff and the one-time records-file import.normalizeLegacyHandoffRecordis replaced bydecodePersistedAgentSessionRecord.AgentSessionProviderHandle({ kind: 'claude' | 'codex' | 'opaque', … }) is renamedAgentSessionJournalProviderHandleand documented as what it always was in practice: the encoding journal rows record and the attach wire carries. Every journal row builds it the same way, from the journal identity, through one function (agentSessionJournalProviderHandle(identity)): the identity's handle in row form, orpendingbefore one is proved. Tests build row handles through that same function instead of writing the in-memory handle into a row. The journal identity handed to adapters now carries the neutral handle, ornullbefore the provider has proved one (today this is the{ kind: 'opaque', value: 'pending' }placeholder, which journal rows still record byte-for-byte). The reservedopaquearm gets its intended use: a non-Claude/Codex handle is recorded in journal rows as{ kind: 'opaque', agent, value: nativeId }.Census of every place a handle is persisted or sent (checked before changing anything):
agent_session_records.record_json(chat journal database)providerHandleChain[].handleagent-sessions.jsonrecords file (read once by the version-4 import)forkedFromKey,supersedesKeyproviderHandleRoot(persisted across quit/relaunch)epochandsubmissionrows (providerHandle){ kind }handle; write-only, readers only check it is an objectopaquearmagentSession.attach/ensureparams (providerHandle, strict zodclaude/codex), host-internaladopt.providerHandle{ kind }handle; the attach fingerprint covers it as sentproviderSession.id, orchestration mail lookup by provider idnativeIdisAgentSessionHandleProvider/AgentSessionHandleProviderNothing an older client or host decodes changes shape, so no capability gate is needed.
Overlap with #24862. Both PRs touch
src/shared/agent-session-record.ts,structured-claude-runtime-adapter.ts,codex-structured-session-acquire.tsand seven test files or fixtures. A trial merge (git merge-tree) of this branch with #24862's head shows no conflict in any of them; the only conflicts in that trial come from main changes #24862 has not merged yet (i18n catalogs, and a test file it deletes that main has since edited). Its only new handle literal is an attach-wire one, which this PR keeps valid. Whichever lands second should re-run its tests.Why
The common pattern keeps the provider's resume handle generic in shared code: a native id that shared code reads, plus provider-owned data it never parses. Orca's typed union was the outlier, and it is why every new agent touched shared types.
Alternatives considered:
provider === 'codex'again. With the neutral type, the provider fields do not exist outside the encoding module.Differences from the common pattern
{ kind }shape for Claude and Codex (temporary). The wire follow-up (feat(native-chat): open structured chat's wire and stored records to registered agents, behind a negotiated capability #25159) widens what clients and hosts exchange behind a new advertised capability. Journal rows are write-only and need no change.record.provideris still'claude' | 'codex', and the account-home variable is still a closed pair (temporary). refactor(native-chat): structured agents declare their capabilities instead of shared code naming Claude and Codex #25076 gives each of them one owner; feat(native-chat): open structured chat's wire and stored records to registered agents, behind a negotiated capability #25159 opens the stored agent list behind a negotiated capability and replaces the one function that derives a record's handle namespace fromrecord.provider(agentSessionProviderHandleNamespace). The account-home variable opens with the first protocol agent's launch description.Linked Issue
None (maintainer foundation PR for structured native chat beyond Claude/Codex).
Visual Proof
N/A: no UI or behavior change. The handle is internal state, and stored and wire bytes are unchanged for Claude and Codex.
CI status (October 5)
Merged current main (clean). On head
4178e5a0385every CI job passes, including typecheck, cross-version wire compatibility and packaging, except unit shards 1/5 and 4/5. Those two fail on main's own locale tests, which #25418 broke by adding translations the tests expect to be missing (NativeChatSupportedAgents.test.tsx,source-control-discard-localization.test.ts). This PR touches neither those tests nor any locale catalog. The aggregateverifyjob fails only because of those shards.Testing
New tests (
src/shared/agent-session-record-stored-form.test.ts,src/shared/agent-session-provider-handle.test.ts):forkedFromKeyand a superseding creation'ssupersedesKey) validate, decode to the neutral handle, and encode back byte for byte.acp/grokwith provider data) round-trips, is refused by older builds' rules (it does not read as Codex), and has no attach-wire form.{ kind }objects as before (includingpending).nativeIdalone. A same-fence resume that only moved the resume cursor is recorded, not elided. A chain never accepts a link from another transport or agent with the same id.Run:
vitest run --config config/vitest.config.tson every test file that changed or imports a changed module (480 files, 4780 tests): all pass exceptclaude-agent-sdk-contract-pins.test.ts. That test reads the installed SDK'spackage.jsonfrom the sharednode_modulesof my checkout (0.3.251 against the pinned 0.3.284). This PR touches neither, so it is a stale install on my machine, not this change. Also run: the changed-code quality gate (check-changed-code-quality.mjs, passes), oxlint and oxfmt on every changed file (clean).What I verified / didn't
providerHandleChain), and that older builds set an unreadable record aside without rewriting it (by readingagent-session-record-rows.ts; I did not run an older build).git merge-tree).tests/e2e/cross-version-wire/agent-session-stop-event-downgrade.unit.test.tspasses. It opens the same journal with an older build's code, which now gets its identity in the typed form it expects. The other changedtests/e2eunit tests pass too.Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
{ provider: 'codex', threadId }→codexProviderHandle(...)). The substantive change is in the two new shared modules and about 20 non-test files.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)