Repository navigation
feat(native-chat): Grok as a structured chat over the Agent Client Protocol - #25225
Conversation
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.
…imeline-assembler
…e' into brennanb2025/acp-a2-agent-registry # Conflicts: # src/renderer/src/lib/structured-agent-session-provisional-tab.ts
The type-aware lint requires an exhaustive switch with no default case. Also retitle a Stop test to say what it asserts.
…embler' into brennanb2025/acp-d2-rebase2
…embler' into brennanb2025/acp-c5-tool-interrupted
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.
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.
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.
…start 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.
…mit 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).
…to brennanb2025/acp-d2-rebase2
…otocol 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.
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.
…ry' into brennanb2025/acp-a3-wire # Conflicts: # src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts # src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts # src/main/native-chat/agent-session-wire/structured-agent-session-host-types.ts # src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts # src/main/runtime/structured-agent-session-runtime.ts
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.
The PR was conflicting with main, so CI could not run. - structured-agent-session-agent-start.ts: kept D3's `aborted` and main's `argumentProblem` (#25721) on the failed start outcome. - en.json: kept D3's `sessionNotRestored` and main's saved-Arguments strings. - structured-agent-session-delivery-loop.ts: main's growth plus D3's aborted-start guard went over the 300-line budget; named the refused-start type once so refusedStart's signature fits on fewer lines.
# Conflicts: # src/renderer/src/i18n/locales/es.json # src/renderer/src/i18n/locales/fr.json # src/renderer/src/i18n/locales/ja.json # src/renderer/src/i18n/locales/ko.json # src/renderer/src/i18n/locales/zh.json # src/shared/agent-session-failure-copy.ts
…e, as its SQLite journal fixture requires
# Conflicts: # src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
…he chat hook stays within its line limit
Review summary (review coordinator) — head
|
Conflicts: agent-launch.ts (main moved intent resolution to its own module; kept D3's callerRendersLaunchedChat), its test (gemini stays a terminal: viewMode 'terminal'), the capabilities test imports, and NativeChatStructuredSession.tsx (main's queue hold/resume now flow through D3's composer-transport hook).
📝 WalkthroughWalkthroughThis change adds an ACP structured-session adapter and registers Grok as an ACP agent. It adds session acquisition, launch resolution, request and turn handling, options, restore behavior, and process lifecycle management. Host-registered agents now inform structured-chat launch routing and renderer capabilities. The host can abort pending acquisitions and revise settlement records when it observes a child exit. The renderer also uses agent capability data for image input, option catalogs, and Stop behavior. Launch argument matching now supports multi-token permission-bypass flags. Priority: ➖ Normal Merge Risk: 🔵 Low · up to Closing or stopping Grok can be delayed while launch environment probing completes, and a rare process-exit timing can leave internal turn tracking unresolved. These are localized lifecycle risks, but should be considered before relying on prompt cleanup. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 94 functions across 65 files. (7 skipped: 7 unsupported.)
✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/renderer/src/runtime/host-structured-agents.ts (1)
103-108: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDiscard a host-agent reply when its host is no longer retained.
The
.thenhandler writesentries.set(executionHostId, ...)without checking if the host is still paired.retainHostStructuredAgentscan run while the read is in flight and drop the host. The late reply then adds the unpaired host back. For a paired host this does not cause a wrong route:readHostStructuredAgentscompares runtime IDs, and the stale entry is pruned on the next status sync. The impact is a small, self-recovering cache leak. Change this only if the cache must stay exact.Based on learnings: re-check the captured generation after an await before you write results.
Source: Learnings
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
d0834f72-f3f8-4224-9abd-bad3e59838e9
📒 Files selected for processing (168)
config/scripts/vitest-sqlite-runtime-files.mjssrc/main/acp/acp-dialects/acp-dialect.tssrc/main/acp/acp-dialects/grok-dialect.tssrc/main/acp/acp-dialects/grok-requests.tssrc/main/acp/acp-launch-specs.tssrc/main/acp/acp-prompt-turns.tssrc/main/acp/acp-session-reopen-failure.tssrc/main/acp/acp-structured-acquire.tssrc/main/acp/acp-structured-adapter.test-support.tssrc/main/acp/acp-structured-agent-definitions.tssrc/main/acp/acp-structured-auth.test.tssrc/main/acp/acp-structured-broken-stream.test.tssrc/main/acp/acp-structured-connection.tssrc/main/acp/acp-structured-electron-free.test.tssrc/main/acp/acp-structured-fixture-replay.test-support.tssrc/main/acp/acp-structured-host-crash.test.tssrc/main/acp/acp-structured-host-lifecycle.test.tssrc/main/acp/acp-structured-host-option-hang.test.tssrc/main/acp/acp-structured-host-restore-failed.test.tssrc/main/acp/acp-structured-host-start-aborts.test.tssrc/main/acp/acp-structured-host-stop.test.tssrc/main/acp/acp-structured-host.test-support.tssrc/main/acp/acp-structured-lane.tssrc/main/acp/acp-structured-launch-resolution.test.tssrc/main/acp/acp-structured-launch-resolution.tssrc/main/acp/acp-structured-options.tssrc/main/acp/acp-structured-prompts.tssrc/main/acp/acp-structured-request-admission.test.tssrc/main/acp/acp-structured-session-adapter-deps.tssrc/main/acp/acp-structured-session-adapter-lifecycle.test.tssrc/main/acp/acp-structured-session-adapter-recordings.test.tssrc/main/acp/acp-structured-session-adapter-resume.test.tssrc/main/acp/acp-structured-session-adapter-teardown.test.tssrc/main/acp/acp-structured-session-adapter.test.tssrc/main/acp/acp-structured-session-adapter.tssrc/main/acp/acp-structured-session.tssrc/main/acp/acp-structured-starts.tssrc/main/acp/acp-structured-steer.test.tssrc/main/acp/acp-structured-stop.tssrc/main/acp/acp-structured-turns.tssrc/main/acp/acp-timeline-dialect.test.tssrc/main/acp/acp-timeline-recordings.test.tssrc/main/acp/acp-timeline-recovery.test.tssrc/main/acp/acp-timeline-requests.tssrc/main/acp/acp-timeline-translator.tssrc/main/agent-launch/agent-launch-executor.test.tssrc/main/agent-launch/agent-launch-executor.tssrc/main/agent-launch/agent-launch-mode.tssrc/main/agent-launch/agent-launch-surface-factories.tssrc/main/claude/claude-stream-json-connection-close.test.tssrc/main/codex/codex-app-server-connection-exit-order.test.tssrc/main/ipc/desktop-renderer-runtime-capabilities.test.tssrc/main/ipc/desktop-renderer-runtime-capabilities.tssrc/main/native-chat/agent-session-timeline/provider-timeline-identity.tssrc/main/native-chat/agent-session-wire/structured-agent-session-acquire-aborts.tssrc/main/native-chat/agent-session-wire/structured-agent-session-acquisition.tssrc/main/native-chat/agent-session-wire/structured-agent-session-adapter.tssrc/main/native-chat/agent-session-wire/structured-agent-session-agent-start.tssrc/main/native-chat/agent-session-wire/structured-agent-session-attach-flow.tssrc/main/native-chat/agent-session-wire/structured-agent-session-attach-orchestration.tssrc/main/native-chat/agent-session-wire/structured-agent-session-child-exit.tssrc/main/native-chat/agent-session-wire/structured-agent-session-close-aborts-start.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-codex-stop-row.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-conversation-lifetime.tssrc/main/native-chat/agent-session-wire/structured-agent-session-dead-generation-settlement.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-dead-generation-settlement.tssrc/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.tssrc/main/native-chat/agent-session-wire/structured-agent-session-forget-status.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host.tssrc/main/native-chat/agent-session-wire/structured-agent-session-mutation-admits-now.tssrc/main/native-chat/agent-session-wire/structured-agent-session-mutation-context.tssrc/main/native-chat/agent-session-wire/structured-agent-session-provider-child-record.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-reasoning-sweep.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.tssrc/main/native-chat/agent-session-wire/structured-agent-session-startup-failure-exit.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-stop-aborts-start.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-turns-options.tssrc/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-unfinished-work.tssrc/main/provider-process/provider-spawned-process-identity.tssrc/main/provider-process/supervised-provider-child-location.tssrc/main/runtime/orca-runtime-get-worktree-ps.tssrc/main/runtime/orca-runtime-structured-agent-registration-seam.test.tssrc/main/runtime/orchestration/send-agent-turn-boundary.test.tssrc/main/runtime/rpc/methods/agent-launch-caller-renders.test.tssrc/main/runtime/rpc/methods/agent-launch.test.tssrc/main/runtime/rpc/methods/agent-launch.tssrc/main/runtime/rpc/methods/orchestration-worker-start-mode.tssrc/main/runtime/rpc/methods/orchestration-worker-start-receipt-wording.test.tssrc/main/runtime/rpc/methods/orchestration-worker-start-registered-agent.test.tssrc/main/runtime/rpc/methods/structured-agent-session-restart-dismiss-fence.test.tssrc/main/runtime/rpc/methods/structured-agent-session-restart-resume.test.tssrc/main/runtime/rpc/methods/structured-agent-session-restart-unregistered-agent.test.tssrc/main/runtime/structured-agent-account-home.tssrc/main/runtime/structured-agent-runtime-registrations-acp.test.tssrc/main/runtime/structured-agent-runtime-registrations.tssrc/main/runtime/structured-agent-session-runtime.tssrc/main/runtime/structured-agent-shell-environment.tssrc/renderer/src/app-shell/use-app-shell-services.tssrc/renderer/src/components/native-chat-resume-on-restart-grouping.tssrc/renderer/src/components/native-chat/NativeChatComposer.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.tsxsrc/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.tsxsrc/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.test.tsxsrc/renderer/src/components/native-chat/agent-session-failure-words-text.tssrc/renderer/src/components/native-chat/native-chat-composer-types.tssrc/renderer/src/components/native-chat/structured-agent-registered-agent-surface.test.tssrc/renderer/src/components/native-chat/structured-agent-session-seed-catalog.tssrc/renderer/src/components/native-chat/structured-agent-session-stop-control.tssrc/renderer/src/components/native-chat/structured-agent-session-tabs.tssrc/renderer/src/components/native-chat/use-host-model-catalog-upgrade.tssrc/renderer/src/components/native-chat/use-native-chat-composer-attachments.tssrc/renderer/src/components/native-chat/use-native-chat-composer-image-input.test.tsxsrc/renderer/src/components/native-chat/use-native-chat-resolved-path-attachments.tssrc/renderer/src/components/native-chat/use-native-chat-structured-composer-transport.tssrc/renderer/src/components/native-chat/use-structured-agent-session-option-state.tssrc/renderer/src/components/native-chat/use-structured-agent-session-options.tssrc/renderer/src/components/native-chat/use-structured-agent-session-provisional.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.tssrc/renderer/src/components/settings/ExperimentalPane.test.tsxsrc/renderer/src/components/settings/NativeChatExperimentalSetting.tsxsrc/renderer/src/components/tab-bar/TabBarItemRow.tsxsrc/renderer/src/components/terminal/initial-terminal-structured-launch.test.tsxsrc/renderer/src/components/use-terminal-watcher-effects.tssrc/renderer/src/i18n/en-runtime-required.jsonsrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/renderer/src/lib/agent-launch-route-input.tssrc/renderer/src/lib/agent-launch-route-registered-agents.test.tssrc/renderer/src/lib/agent-launch-routing.tssrc/renderer/src/lib/agent-session-launch-plan.test.tssrc/renderer/src/lib/agent-session-launch-plan.tssrc/renderer/src/lib/launch-structured-agent-session.tssrc/renderer/src/lib/structured-agent-launch-settlement.tssrc/renderer/src/lib/structured-agent-session-host-admission.tssrc/renderer/src/lib/structured-agent-session-idle-empty-chat.tssrc/renderer/src/lib/structured-agent-session-launch-draft.tssrc/renderer/src/lib/structured-agent-session-launch-persistence.test.tssrc/renderer/src/lib/structured-agent-session-launch-persistence.tssrc/renderer/src/lib/structured-agent-session-launch-registered-agent.test.tssrc/renderer/src/lib/structured-agent-session-launch-registry.tssrc/renderer/src/lib/structured-agent-session-launch-status.tssrc/renderer/src/lib/structured-agent-session-launch.tssrc/renderer/src/lib/structured-agent-session-paired-admission.tssrc/renderer/src/lib/structured-agent-session-provisional-tab.tssrc/renderer/src/lib/worktree-creation-flow-execute.tssrc/renderer/src/lib/worktree-creation-structured-session.tssrc/renderer/src/runtime/host-structured-agents-sync.tssrc/renderer/src/runtime/host-structured-agents.test.tssrc/renderer/src/runtime/host-structured-agents.tssrc/renderer/src/runtime/use-host-structured-agent.tssrc/shared/agent-session-failure-copy.tssrc/shared/agent-session-failure-words.tssrc/shared/agent-session-failure.tssrc/shared/agent-session-refusal-notice.test.tssrc/shared/electron-remote-runtime-client-capabilities.tssrc/shared/structured-agent-session-create.tssrc/shared/structured-agent-session-mutation.tssrc/shared/tui-agent-launch-defaults.test.tssrc/shared/tui-agent-launch-defaults.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.
| await input.beforeDispatch?.() | ||
| session.turns.dispatch({ | ||
| clientMessageId: input.clientMessageId, | ||
| prompt, | ||
| requestedAt: input.requestedAt ?? this.now() | ||
| }) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Check the session again after beforeDispatch resolves.
dispatch reads the live session before it awaits input.beforeDispatch. The connection can break, or the child can exit, during that await. In that case, connectionLost or finish has already run closeAcpSessionJournal, which calls session.turns.end().
After that, session.turns.dispatch adds the send to unsettled and calls start:
lane.applydoes nothing, because the lane is disposed.connection.promptrejects withAcpConnectionClosedError.- The rejection handler returns without settling, because it expects the session's end to settle the send. That end has already run.
Result: the adapter returns { state: 'admitted' } for a message that never reached Grok, and no later settlement arrives. The send stays unconfirmed in the journal.
To fix this, re-check that the session is still live after the await. If it is not, reject the send as never sent.
🐛 Proposed fix
await input.beforeDispatch?.()
+ if (this.sessions.get(input.sessionId) !== session || session.journalClosed !== null) {
+ // The child ended while the send was prepared: the message never left Orca.
+ return this.rejected(session, 'providerExited')
+ }
session.turns.dispatch({📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await input.beforeDispatch?.() | |
| session.turns.dispatch({ | |
| clientMessageId: input.clientMessageId, | |
| prompt, | |
| requestedAt: input.requestedAt ?? this.now() | |
| }) | |
| await input.beforeDispatch?.() | |
| if (this.sessions.get(input.sessionId) !== session || session.journalClosed !== null) { | |
| // The child ended while the send was prepared: the message never left Orca. | |
| return this.rejected(session, 'providerExited') | |
| } | |
| session.turns.dispatch({ | |
| clientMessageId: input.clientMessageId, | |
| prompt, | |
| requestedAt: input.requestedAt ?? this.now() | |
| }) |
# Conflicts: # src/main/native-chat/agent-session-wire/structured-agent-session-child-exit.ts # src/main/native-chat/agent-session-wire/structured-agent-session-dead-generation-settlement.ts
Pass the lifetime's conversation opener to reveal directly; it is already passed unbound to the mutation context.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟡 Minor · Make launch resolution abortable. · acp-structured-acquire.ts:79-92
src/main/acp/acp-structured-acquire.ts:79-92
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winMake launch resolution abortable.
acquireAcpStructuredSessionawaitsdeps.resolveLaunchbefore it tracks a connection. The production resolver awaits the login-shell environment, whose probe can take 5 seconds and can run a second 5-second fallback probe. Close, Stop, and quit abort only the tracked connection, so they cannot settle this wait. Host teardown can therefore wait several seconds for the attach to settle.Thread
attempt.signalinto launch resolution and race the environment and workspace awaits against it. Return the existing pre-spawn cancellation error when the signal aborts.
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
702b71fe-318d-4627-887d-47fad10da60b
📒 Files selected for processing (13)
src/main/native-chat/agent-session-wire/structured-agent-session-adapter.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host.tssrc/renderer/src/components/native-chat/agent-session-failure-words-text.tssrc/renderer/src/i18n/en-runtime-required.jsonsrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/shared/agent-session-failure-copy.tssrc/shared/agent-session-failure-words.tssrc/shared/agent-session-failure.ts
🚧 Files skipped from review as they are similar to previous changes (8)
- src/renderer/src/i18n/en-runtime-required.json
- src/shared/agent-session-failure-copy.ts
- src/renderer/src/i18n/locales/zh.json
- src/renderer/src/i18n/locales/es.json
- src/renderer/src/i18n/locales/ja.json
- src/renderer/src/i18n/locales/fr.json
- src/renderer/src/i18n/locales/ko.json
- src/renderer/src/i18n/locales/en.json
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.
Main's #25225 moved the composer transport into use-native-chat-structured-composer-transport.ts and Stop into structured-agent-session-stop-control.ts, both built on the saved outbox this branch removes. Both now read the in-memory sender: the transport sends through sendThroughLaunch (keeps image connectionIds, sendOut while starting), and Stop before the chat is published gives the launch's unsent text back to the composer (takeBackStructuredLaunchPrompts) and asks nothing of the host, as main's rule does for its outbox.
Stacked on
Nothing any more. Every PR this one was stacked on has been squash-merged into main: #24991, #25076, #25159 (registered-agents wire and storage), #24989, #24988, #25204 (shared provider process lifecycle), #25072, #25141, #25064 (shared timeline writer), #25181 (a stopped tool shows as interrupted), #24990 (ACP protocol client), #25090 (ACP → timeline translation and Grok's dialect), #25747 (record a fresh session after a failed reopen) and #25810 (the ACP connection owns the agent's process). This branch last merged main at
86d03ff9085. Where main's squash equals a branch head this branch had already merged (#25159, #25181, #25810, #25090), the merge was computed against main's tree with those heads as extra parents and committed as an ordinary merge of main, so every base file equals main's copy. What remains in the diff is this PR's own: Grok's registration, the ACP adapter, the client wiring, and the few shared changes listed below.ELI5
Today Grok's native chat in Orca is a terminal running Grok, with Orca reading Grok's transcript file and typing into the terminal. Claude and Codex instead have a structured chat: Orca talks to the agent's own protocol, so replies stream, tool calls are real rows, approvals are real cards, and Stop actually stops. This PR gives Grok that structured chat. Orca now speaks the Agent Client Protocol (ACP, the open JSON-RPC protocol
grok agent stdioimplements), and Grok is the first, and only, agent on it. How Orca starts, stops, reopens and talks to Grok follows the common pattern for ACP agents; where Orca does something different, the list below says so and why.What Changed
The problem. The PRs under this one built the parts: an ACP protocol client whose connection owns the agent's process (#24990, #25810), a translator from ACP traffic to Orca's chat timeline with Grok's protocol extensions (#25090), a shared timeline writer (#25064), a shared process lifecycle (#25204), a record that can hold a fresh session after a failed reopen (#25747), and a wire that can carry agents other than Claude and Codex (#25159). Nothing joined them. No agent was registered, the desktop never asked a host which agents it runs, and
agent.launch, the account-home lookup and a dozen renderer gates still only knew Claude and Codex.What you'll see. All of this is behind the existing setting Settings → Chat UI → Use updated structured native chat, the same switch Claude and Codex structured chats use, with the same fallbacks. With it off, Grok keeps the terminal-backed chat it has today, unchanged.
/commands come from Grok. On SSH or WSL, Grok keeps the terminal chat.XAI_API_KEYin Grok's own environment there, else the sign-in Grok cached on that machine. With neither, the chat shows the existing "Grok is not signed in… Sign in first." start failure. For a chat on a paired runtime, that runtime's environment and Grok sign-in are used, never this computer's. No new sign-in screen.agent.launchfrom a client that can show Grok chats (a desktop window on this version) starts a structured Grok chat only when that setting is on; with it off it starts a Grok terminal, as for Claude and Codex. Starting Grok from a phone still opens a Grok terminal, because phones cannot show Grok chats yet. A phone shows the existing "Update to view" row for a Grok chat started elsewhere.orca orchestration worker-start --agent grokopens a terminal Grok worker, as today, whatever the setting (structured workers only take Claude and Codex yet; see Differences, temporary).session/load). It replays the conversation, and Orca throws the replay away except the context-usage reading, because Orca's own history already holds the chat. The chat looks the same as before it closed. A long chat may take a little longer to reopen, because Grok replays it.[cli] use_leaderin Grok's config, off by default), Grok's chat process connects to Grok's shared leader process. Stop and Close then end Orca's connection to Grok, not that shared leader. With leader mode off (the default) nothing changes.Mechanism.
src/main/acp/acp-launch-specs.ts), one row:grok agent [--always-approve] stdio, no extra environment.--always-approveis added only when the Agent Permissions setting gives Grok full access; in that mode Orca also answers any permission request with the agent'sallow_onceoption. The account home isGROK_HOME(else~/.grok), resolved on the runtime's own machine; the binary is searched on PATH, then in<GROK_HOME>/bin. The spec also carries Grok's sign-in rule:xai.api_keywhenXAI_API_KEYis set in the environment Grok is launched with and Grok offers that method, elsecached_tokenwhen offered, else none.createAcpAgentConnection). The adapter builds it before the handshake and records it at once, so a Close, Stop or quit during a start reaches it. It spawns through the shared process lifecycle on the machine that runs the chat, with the chat's launch environment and working directory. Grok's stdout ending is not treated as exit; the connection reports Grok's exit only when the process is seen to exit, and reports a broken protocol (onClose) only while the process may still run. The adapter pauses and resumes reading through it when the journal is backed up, and Stop's end of the process isconnection.close()after the host's 4-second grace. A start whose process Orca can't prove gone keeps that same connection, and the next start or quit closes it again; Orca never spawns a second Grok meanwhile.AcpStructuredSessionAdapter(src/main/acp/acp-structured-*.ts) implements the same adapter contract Claude and Codex do:acquire):initializewith the client file system and terminals off, thensession/newfor a new chat. A chat that already has a Grok session reopens it withsession/load(an agent that can only resume getssession/resume). Inside that call Orca marks every frame Grok sends as replay before the translator reads it, so only context usage is kept and options and commands are still taken. If the reopen fails, the adapter startssession/newand records it as a fresh session that replaces the old one (feat(native-chat): record fresh sessions after failed restoration #25747'sreplaceslink, reasonrestore-failed), then writes the one warning row. The row's id names the forgotten session. Every start that succeeds also writes the row for any forgotten session in the chat's session list whose row the chat's history lacks, so a row an attach failure, quit or crash dropped is written by the next start. Nothing is stored for it: the session list and the history already say whether the row is owed. A history that can't be read whole writes nothing. Exceptions: a session this chat created that Grok reports not found, on which the chat's history holds no turn, is replaced through the existing "replace an unsaved creation" link with no row (the history is read only at that failure, from the chat's own journal, with no stored flag; a turn on that session, or a history that can't be read whole, takes the normal path above); a sign-in failure, or a start already being closed, still fails the start. When Grok reports it needs to sign in, the adapter names the method the launch spec's rule picks and the protocol client signs in and retries once, for new and reopened sessions alike. The handshake has no time bound. A start that fails because the connection closed waits (up to the Stop's 4 seconds, or until a Close or Stop) for the process's exit before it is classified, so the failure carries Grok's own last words.AbortControllerfirst and hands its signal to the acquire (StructuredAgentSessionAcquireInput.signal). A close, a Stop that names no turn and that admission would run now, and quit abort it from outside the chat's queue. The abort closes the connection: the process is stopped and every request Grok left unanswered fails at once, even before the exit is proven, as a kill does in the common pattern. An aborted start fails nothing: what the Stop or close withdrew is already settled, and a message accepted after it gets its own start.session/prompt. For an agent whose dialect echoes a prompt identity (Grok's does), Orca's prompt id goes in_meta.promptId, which the translator keys the turn on; other ACP agents get a plain prompt. Grok's first event for the turn, or its answer, settles the send as accepted. Grok's own error answer before the turn starts rejects the send with Grok's reason; after, it ends that turn as failed. Any other failure of the prompt (an answer Orca can't read, one too large) also ends the turn as failed and settles the send. Only a closed connection leaves the send running, for the connection-loss path below to settle.session/cancelonce for that prompt, then sends the message as the next prompt once Grok answers. The adapter owns the "once": further steers before Grok answers send no second cancel, a cancel that could not be written lets the next steer ask again, and the mark clears when that prompt settles. The cancel is one notification with no time bound and never closes the connection. Steers wait in the adapter only for that answer; the last one runs, a Stop withdraws a waiting steer, and an exit rejects it as never sent. A send while Grok runs a turn it began itself goes as a prompt at once, with no cancel, and Grok queues it behind that turn; a second send then steers.stopEndsSessionis true for every ACP agent, as for Claude. Stop withdraws Grok's open requests, sends its ownsession/cancel(without waiting on that write) and asks Grok to end its turn; the host waits for that (4 s from the cancel), then closes the connection, which ends the process. A Stop naming a turn that has ended is declined only while another turn is live, which keeps the session. In the gap before a follow-up's turn opens it is taken, as Claude's adapter does.session/cancelonce (none if a Stop already sent one), waits for the turn to end up to the same 4 s grace, then ends the process. An idle close ends it at once; a lost connection or a journal failure ends it without asking. The cancel-and-wait lives inacp-structured-stop.ts, shared by Stop and close.cancelledbefore the full-access answer.x.ai/exit_plan_mode) is answered at once, asking no one: the plan text goes to the chat's existing plan row (theplan-documentstatus row Codex plans and ACP plan updates use), and Grok is answeredabandonedwith feedback to stop and wait for the person. Dialects gainsettleRequestfor requests like this one.configOptions, and a pick goes throughsession/set_config_option. A pick waits at most 30 seconds, as Claude's and Codex's do, and a close, Stop or quit ends the wait at once. Saved picks are re-applied at start./commands come fromavailable_commands_update, including ones Grok sends while it reloads.onClose: Grok's input broke, or a frame Orca can't accept), the journal closes, the running turn reads unconfirmed, the connection is closed, and the host's settlement revises that turn to interrupted when the exit is proven.structured-agent-runtime-registrations.ts): Grok is one more entry. It declares rewind, compact and goals off, context usage on, images off,steering: 'queue'(a steer is held behind a cancel, the common pattern's own meaning of that word), andapprovalEnforcement: 'orca'(Orca answers what Grok asks; Grok's own settings decide when it asks). The registration resolves its own account home and opens its connection withcreateAcpAgentConnection.agent.launchand orchestration worker-start all readexperimentalStructuredNativeChatthroughprefersStructuredNativeChatByDefault/structuredAgentLaunchSupported(shared/structured-native-chat-launch-route.ts).agent.launchmakes a chat for an agent other than Claude or Codex only when the calling client can show it (clientRendersStructuredAgent); the host's own callers (CLI, orchestration) carry no capability list. Worker-start decides with no registered agents beyond Claude and Codex, because the structured worker factory creates only those two.agentSession.agentsper host (local and paired) into one cache stamped with each host's runtime id, only once structured chat is in use, and advertisesagent-session.structured.registered-agents.v1with that reader. The renderer's Claude/Codex gates on launch plan, provisional tab, persistence, tab bar, pane overlay, worktree creation and terminal seeding now trust the structured route.tuiAgentArgsBypassPermissions), which is what makes the full-access setting apply to Grok. Stop shows for a message waiting on a new chat that is not published yet, and takes it back.Why
Differences from the common pattern
_meta.promptIdand_meta.requestId) onsession/prompt, only to agents whose dialect declares they echo it (Grok); the common pattern sends a plain prompt. Orca needs it to join each reply to the turn Orca opened for that send, and to keep Grok's own wake-up turns apart from the person's sends. Other ACP agents get a plain prompt.fs.readTextFile/writeTextFile: false). Common implementations differ here; off adds no new trust surface, and Grok edits files with its own tools either way.approvalEnforcement: 'orca'). Nothing in Orca reads that field yet: Orca has no per-chat approval policy for every agent, so full access is applied from the launch's own setting.orca orchestration worker-start --agent grokopens a terminal Grok worker even with the setting on, because structured workers only take Claude and Codex. Follow-up: structured workers for registered agents.agentSession.close, but the aborted create then comes back as a refusal and the window would show "Chat could not be started" with Retry, a failure notice for the person's own Stop. Follow-up: the host reports a start aborted by the person's own Stop as stopped, not failed, and the launch flow lets the next send start the chat again.grok login) is declared on the launch spec, but the existing "not signed in" words don't name it. Follow-up: carry the launch spec's sign-in command into those words.holdsLiveProviderProcess) yet. Without a start time Grok falls back as before this PR's merge. Follow-up: give the ACP adapterholdsLiveProviderProcess, then drop ACP's start-time requirement.session/load, discarding the replay except context usage.cancelled, with no card. Questions follow the same rule (the common pattern has no Grok question support at all; its request gate is the permission one).Linked Issue
N/A (internal stack: native chat for more agents).
Visual Proof
Live QA round 6 with a real Grok (1.0.46, auto-update off) on a separate Mac, driven only through the hidden renderer, with the app's HOME, Codex and Claude folders isolated and the machine's real config files hashed after every step (nothing changed but Grok's own sign-in refresh). Every cell that ran passed. Cells ran on three heads as main moved:
940e05a5c42,7a712ac2a4candb081e2ddf49; later merges only brought in main and line-limit moves.940e05a5c42940e05a5c42session/load, no duplicate history940e05a5c42940e05a5c42940e05a5c42940e05a5c42940e05a5c427a712ac2a4c7a712ac2a4c7a712ac2a4cworker-start --agent grokopens a terminal Grok worker7a712ac2a4cb081e2ddf49b081e2ddf49b081e2ddf49b081e2ddf49session/cancel, Grok gone in 1.2 sb081e2ddf49b081e2ddf49b081e2ddf49Testing
Tests (explicit files, fake agents only). The adapter contract against a scripted ACP agent and against Grok's recorded sessions; the real host with a scripted Grok for Stop, steer, reopen, the reopen fallback, crash and start-abort paths; the launch spec, launch resolution and registration; the client's registered-agent route and its setting gate. The scripted agent now sits behind the same connection surface as #25810's (stdout ending is not exit; the exit closes the protocol with the agent's last words), and one test runs a real Node process through the real connection. Added or changed in this round, each removed in turn to see its tests fail:
_meta(1 fails).Local run (this machine's shared install is older than the branch; I may not reinstall): 952 explicit test files on the repository configuration before main's newest test-runner change: 921 passed; the other 30 need
stream-json, which that install lacks — 29 of them pass through a configuration that stubs it (the one real failure was main's new restart test, adapted here), and 2 whole-tree ratchet tests hit the same missing module. After main's Vitest-on-Bun change (#25840) the repository configuration needs a package this install lacks, so the last merges were tested with the previous configuration from a scratch folder: all 36 ACP test files (297 tests), the renderer native-chat files (61 files, 522), the settings, capability and merge-touched suites. This round: all 36 ACP test files plus timeline identity and the runtime registration and session-runtime suites (42 files, 338 tests), and after merging main5cf3585b78dthe same plus the held-send, accept-then-deliver, attach-retry and restart-ownership host suites (46 files, 415 tests), all passing. oxlint, oxfmt and the changed-code quality gate pass. Localization checks pass.Typecheck: not run locally (this machine's install can't run it safely); CI's typecheck is the authority. Its first run on this round found 3 errors in one test (main's
structuredAgentsReadBynow takes agent ids); fixed.CI: At
27c63719b2a(after merging main86d03ff9085): every check passed except unit shard 2 of 5, and the PR Checksverifyjob, which failed only because shard 2 did. Passed: static analysis and typecheck, unit shards 1, 3, 4 and 5, relay integration, cross-version wire compatibility, package (macOS and Windows), Mobile Checks, unit selection evidence and the test LoC check (PR Checks, Mobile Checks). Shard 2 is not a test failure: on three runs here (b081e2ddf49twice and27c63719b2a), GitHub stopped its runner partway through ("The runner has received a shutdown signal"), with no failing test in its log. Another open PR that has main's new unit-test scheduling (#25967) but none of this branch's code fails shard 2 the same way, so this comes from main. The 25 test files this branch adds or changes that no finished shard ran all pass locally (302 tests). Since the last green run (7a712ac2a4c), merging main changed the PR's own diff in four places. (1) A failed agent start carries both this branch'sabortedflag and main's saved-Arguments problem (#25721). (2) The failure wording and its five translations keep main's retry-count strings (#25818) beside this branch's "couldn't reopen its earlier session" string. (3) The composer's send moved into this branch's composer-transport hook, so main's sending changes (#24514: a send that was queued reports it, and sending scrolls to the newest message) now live in that hook. (4) Line limits: the delivery loop names its refused-start type once, and Stop's control now reads the unsent messages itself, so the chat hook stays under its limit. This branch's test for a close during a start now runs on the Node runtime, as main's new check (#25967) requires of every test that opens the SQLite journal. A one-line test fix the branch had carried for main is gone: main's own copy (#25977) replaced it. Earlier merges (up to main81a1968bc65) changed the PR's own diff in four places. (1) After main's Stop rework (#24369), this branch's Stop module adds only its own case: before the host publishes a new chat, Stop takes back a message that hasn't been sent yet. Every other Stop goes through main's hook, so the chat reads "Stopping…" as on main. (2) Codex's process-identity and location files are main's again: #25718 made Codex's start-time read best-effort, which relies on a lease-renewal path the ACP adapter doesn't implement yet. The shared helpers this branch added now serve only the ACP adapter, which keeps requiring a readable process start time, as before. (3) The session host and Codex's connection test stayed within their line limits: a namespace import in the host, and this branch's stdout-before-exit test moved intocodex-app-server-connection-exit-order.test.ts. (4) Merge-only resolutions: the composer uses main's owner worktree, and main's floating-workspace comment and Settings copy are kept beside this branch's agent-neutral wording.What I verified / didn't
agent.launch, orchestration worker-start and the renderer route read the same setting; the common pattern's behavior for each item, read first-hand.worker-start, and close, quit and close-after-Stop during a reply.b081e2ddf49) were not re-run live.QA plan for live Grok (run on an allowed machine with an isolated HOME and GROK_HOME)
Set
GROK_DISABLE_AUTOUPDATER=1in the rig's own environment so QA never updates the machine's real Grok (Orca does not set it). Use a forcing hook for permission prompts.sleep 20; echo done > marker.txt, Stop after the shell starts: turn interrupted, nomarker.txt, Grok's process gone within ~4 s, the next prompt reloads (session/loadin the protocol log).session/cancels (the first message is sent to Grok and cancelled at once), and the last one runs.session/loadin the log.initializestays starting past 60 s; Stop, Close and quit each end it at once; a send after a Stop during a restart is delivered with no start-failure row.XAI_API_KEYin the rig's Grok environment, and separately with only a cached sign-in, the chat starts (protocol log:authenticatewithxai.api_key/cached_tokenif Grok asks); with neither, the existing "not signed in" failure.--always-approvewithout it (psthe argv).agent.launchopen a Grok terminal.worker-start --agent grokwith the setting on opens a terminal Grok worker.GROK_HOME), reopen: the chat continues, one warning row "Grok couldn't reopen its earlier session…", earlier messages still on screen, the next reopen loads the fresh session with no second row. Run it once on a chat's first reopen after one completed exchange (that case used to be silent), and once on a chat closed before any message (no row).AI Disclosure
Review
Self-reviewed, then reviewed in five rounds and audited behavior by behavior against the common pattern, then two final reviews. The first (readiness checklist, the reopen fallback, the body) found the worker-start regression, the hung turn on an unreadable answer and the prompt-identity extension sent to every agent. A reference audit found a chat's first reopen after real exchanges could forget silently. The second final review found three wrong sentences in this description (two quick messages, questions during a steer, the last-merged main) and the warning row lost when an attach fails after the fresh session is saved; it also asked for a ruling on questions during a turn Grok began itself (now declined). All are fixed here. Live QA rounds 1–4 ran on a separate machine with a real Grok; round 4 passed every required cell at an earlier head. Live QA round 6 passed every cell it ran (see Visual Proof). A later review found that Close, quit and dispose during a reply ended Grok without cancelling its turn first; fixed (see What Changed).
Agent skill upstream boundary
Notes
Local runtime and paired runtimes only; SSH/WSL stay blocked as today. Cursor and user-defined ACP agents are not built. The terminal-backed Grok chat is unchanged. No new dependencies; no lockfile change. New persisted shape: Grok records use the neutral handle
{transport: 'acp', agent: 'grok', nativeId}andGROK_HOME; after a failed reopen a Grok record's session chain also holds acreatedlink withreplaces: {key, reason: 'restore-failed', replacedAt}(#25747's shape, on main; Claude and Codex never write it), and the warning row is a journal status row whose id names the forgotten session. Claude/Codex records are byte-for-byte unchanged. Rollback: after a downgrade to a version without this PR, a saved Grok chat tab loses its agent (that version's tab schema does not know Grok); Claude and Codex tabs are unaffected.Checklist