Repository navigation
Native chat: new and resumed ZCode sessions as live structured chats - #27230
star-nebula wants to merge 22 commits into
Conversation
…e SQLite reader ZCode's CLI persists an OpenCode-v1-shaped history in its own SQLite store (~/.zcode/cli/db/db.sqlite). The Agent Session History scan already lists those sessions, but opening one in the native chat found nothing: the chat transcript path had no ZCode awareness. Resolve a zcode transcript against the ZCode database (native home, then WSL homes) before any OpenCode install, tag every read (signal, page, subscribe) with the agent, and apply the same hidden-transcript filter the scan uses so ZCode's hidden bookkeeping rows stay out of chat renders.
ZCode has no structured-session host adapter, so an Agent Session History row could not reach a chat at all: the resume paths only knew how to adopt a conversation into a host-registered structured session. Rows whose agent reads its own store (zcode today) now open as a read-only chat tab. The tab reuses the agent-session content type with the session id as its entity, and AiVaultSessionHistoryChatLayer paints it with the same live-session hook and message list the chat overlay uses — polling the ZCode SQLite store through nativeChat:subscribe, no PTY involved. There is no composer: nothing sends into a conversation the CLI may still be holding. The eligibility gates answer from the history-chat branch alone — no host adoption, capability, or workspace matching — while keeping the remote and empty-content refusals, and reopening focuses the existing tab instead of duplicating it.
…d pass agent to full-read discovery Address two CodeRabbit majors on stablyai#27131: - zcode discovery awaited getAiVaultWslHomeDirs() before probing the usable native database, so slow WSL discovery could eat the whole 5s deadline and fail an available native transcript; probe native first as the OpenCode branch does. - readOpenCodeNativeChatTranscriptFull passed the agent selector to page reads but not resolveDbPath, so zcode full reads discovered against OpenCode stores: zcode-only sessions reported 'Transcript unavailable' and a colliding session id could select the wrong conversation.
…ver protocol A ZCode session can now run as a live structured chat, the same way Codex and Claude do. A new `src/main/zcode/` adapter family spawns one `zcode app-server --stdio` child per session over the ZCode NDJSON protocol: create a conversation (or resume the one the session record proved), subscribe to its event stream, translate `session/event` notifications into journal rows, and answer the child's reverse-RPC permission prompts. Sending a message is `session/send`; cancelling a turn is `session/stop`, the one call that bypasses the request queue. Registration adds ZCode to the agent-agnostic structured-session registry and to the provider-handle providers, so the existing provisional-tab, resume, and routing machinery adopts it without renderer changes. Verified against a real ZCode CLI build: create -> subscribe -> send -> streamed events -> resume with a full conversation snapshot.
… chats With the ZCode adapter in place, the entry points that decide what a ZCode session opens as now route to the live structured chat. Session creation treats a ZCode conversation as adoptable (its transcript lives in the provider's SQLite store, and the resume handshake re-seeds the journal from the provider's snapshot, so no file import is needed), and the ownership, history-adoption, and worker-authority lookups accept ZCode through the shared provider check instead of hardcoded agent names. The renderer's read-only history layer learns the split: a tab the host adopted as a structured chat carries a `structured-agent-session-` id and belongs to the structured overlay, while the vault row's own tab id stays the read-only path for hosts that have not adopted the session.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ➖ Normal Merge Risk: 🔵 Low · up to
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
src/renderer/src/components/right-sidebar/ai-vault-session-history-chat-open.ts (1)
14-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUpdate the stale comment about the ZCode structured-session adapter.
The comment says "ZCode has no structured-session host adapter". This PR adds that adapter.
canOpenAiVaultSessionHistoryChatnow uses the read-only tab only for rows with nostructuredSession. The comment misstates the routing rule. Reword the comment to say that the read-only tab handles rows that no host has adopted.
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
9f4ee907-0ab9-46b2-8550-ff36c9028d11
📒 Files selected for processing (50)
src/main/ai-vault/session-scanner-opencode-sqlite-dispatch.tssrc/main/ai-vault/session-scanner-opencode-sqlite-worker-protocol.tssrc/main/ai-vault/session-scanner-opencode-sqlite-worker-spawn.tssrc/main/ai-vault/structured-session-ownership.tssrc/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.tssrc/main/native-chat/structured-agent-session-history-adoption.tssrc/main/native-chat/transcript-opencode-database.test.tssrc/main/native-chat/transcript-opencode-database.tssrc/main/native-chat/transcript-opencode-sqlite-query.tssrc/main/native-chat/transcript-opencode-subscribe.tssrc/main/native-chat/transcript-opencode.test.tssrc/main/native-chat/transcript-opencode.tssrc/main/native-chat/transcript-read-cache.tssrc/main/native-chat/transcript-reader.tssrc/main/native-chat/transcript-tail-reader.tssrc/main/native-chat/transcript-watch.tssrc/main/runtime/structured-agent-runtime-registrations-acp.test.tssrc/main/runtime/structured-agent-runtime-registrations.tssrc/main/runtime/structured-agent-session-create-adoption.tssrc/main/runtime/structured-agent-session-runtime.tssrc/main/runtime/structured-worker-authority.tssrc/main/zcode/zcode-app-server-connection-types.tssrc/main/zcode/zcode-app-server-connection.test.tssrc/main/zcode/zcode-app-server-connection.tssrc/main/zcode/zcode-app-server-record-dispatch.tssrc/main/zcode/zcode-app-server-request-error.tssrc/main/zcode/zcode-protocol-events.tssrc/main/zcode/zcode-runtime-registration.tssrc/main/zcode/zcode-structured-acquire.tssrc/main/zcode/zcode-structured-adapter-fixture.tssrc/main/zcode/zcode-structured-agent-definition.tssrc/main/zcode/zcode-structured-child-environment.tssrc/main/zcode/zcode-structured-journal-payload-readers.tssrc/main/zcode/zcode-structured-journal-translation.tssrc/main/zcode/zcode-structured-launch-resolution.tssrc/main/zcode/zcode-structured-session-adapter.test.tssrc/main/zcode/zcode-structured-session-adapter.tssrc/main/zcode/zcode-structured-session-state.tssrc/renderer/src/components/WorkspacePaneOverlayLayers.tsxsrc/renderer/src/components/native-chat/AiVaultSessionHistoryChatLayer.tsxsrc/renderer/src/components/native-chat/AiVaultSessionHistoryChatView.tsxsrc/renderer/src/components/right-sidebar/ai-vault-session-history-chat-open.tssrc/renderer/src/components/right-sidebar/ai-vault-session-history-chat.tssrc/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.tssrc/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-workspace.tssrc/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat.tssrc/shared/agent-session-provider-handle-encoding.tssrc/shared/agent-session-provider-handle.tssrc/shared/native-chat-agent-support.test.tssrc/shared/native-chat-agent-support.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
…, and bind backpressure Address the CodeRabbit majors and minors on stablyai#27230: - Prompts the child asks after acquire no longer strand: the live prompt map is shared with the session, and each permission request also lands a durable pending-approval journal row so a second client can answer it. Previously answerPrompt always threw AgentSessionPromptUnavailableError and the child waited forever. - Snapshot and upsert messages file their id inside info on some builds; read it there before dropping the message, so a resumed chat keeps its history. - onSpawned now reports the identity the acquisition carries: the start time is read once and shared, so the owner check cannot see a null start time against a proved one and throw agent_session_ownership_unknown. - The sink's reading control binds to this child's stream, as the Codex acquire does, so hard-watermark backpressure pauses the reader instead of dropping rows; every close path unbinds. - session/send carries the session's model option. - The read-only history chat opens in the row's target worktree rather than whichever workspace is active.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/main/zcode/zcode-structured-session-adapter.test.ts (1)
165-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert the restored snapshot message.
The test calls
adapter.acquire(acquireInput())without an event sink, so the translator is not created. It then checks only thesession/resumerequest. Bind a sink and assert that the journal receivesmessage:m1with textearlier.Suggested test assertion
const adapter = adapterFor(zcode, [], { resolveLaunch: async () => ({ command: 'zcode', args: ['app-server', '--stdio'], @@ workspacePath: '/work/repo' }) }) + const rows: { identity: unknown; body: unknown }[] = [] + const sink = { + appendItem: (_identity: unknown, body: unknown) => { + rows.push({ identity: _identity, body }) + }, + publish: () => {} + } - await adapter.acquire(acquireInput()) + await adapter.acquire( + acquireInput({ events: sink as unknown as StructuredAgentSessionEventSink }) + ) - // The resumed conversation seeded its history: the session answer over the - // journal names the snapshot rows, whatever shape carried the id. - expect(zcode.connections[0]!.requests[0]!.method).toBe('session/resume') + expect(rows).toContainEqual({ + identity: expect.objectContaining({ recordId: 'message:m1' }), + body: expect.objectContaining({ + blocks: [{ type: 'text', text: 'earlier' }] + }) + })
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
b4a1a00a-2346-463d-9c7b-2064d08cc813
📒 Files selected for processing (8)
src/main/zcode/zcode-structured-acquire.tssrc/main/zcode/zcode-structured-adapter-fixture.tssrc/main/zcode/zcode-structured-journal-translation.tssrc/main/zcode/zcode-structured-session-adapter.test.tssrc/main/zcode/zcode-structured-session-adapter.tssrc/main/zcode/zcode-structured-session-state.tssrc/renderer/src/components/right-sidebar/ai-vault-session-history-chat-open.tssrc/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts
- src/main/zcode/zcode-structured-acquire.ts
- src/main/zcode/zcode-structured-session-state.ts
- src/main/zcode/zcode-structured-session-adapter.ts
- src/main/zcode/zcode-structured-journal-translation.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
…ry chat Tab state is per-worktree: createUnifiedTab and activateTab never move activeWorktreeId, so a row whose conversation belongs to another worktree built the tab out of view. Activate the row's workspace first through the resume workspace helper; a failed activation keeps the tab unopened instead of landing it in the wrong workspace.
…g deltas This ZCode build never upserts whole assistant messages: a reply reaches Orca only as model.streaming deltas (text_delta / reasoning_delta, keyed by assistantMessageId, with the delta fields nested one payload level down), so the journal translator's message.upserted path never fired and finished turns showed the user's message and nothing else. Accumulate the deltas in a stream translator (satellite module, shared delta coalescer), rewrite the running assistant/reasoning row as the stream grows, close every open stream completed at turn end, and let a whole-message upsert or snapshot restore override the stale stream copy. Identity and append plumbing move to zcode-structured-journal-sink to keep the translator under the file-size cap.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
7f2eeaf0-dc53-48ff-a44f-f71828b6dff3
📒 Files selected for processing (5)
src/main/zcode/zcode-protocol-events.tssrc/main/zcode/zcode-structured-journal-sink.tssrc/main/zcode/zcode-structured-journal-translation.test.tssrc/main/zcode/zcode-structured-journal-translation.tssrc/main/zcode/zcode-structured-stream-translation.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
…Chat The Resume in New Chat action short-circuited every unadopted ZCode row into the read-only history tab, so old conversations never reached the structured adoption path the host already implements. The eligibility gate now emits a route: an unadopted local row whose database the app-server child can open adopts into a live chat, while a row stored inside WSL keeps the read-only tab. The main Resume action keeps the claude/codex semantics unchanged. The launch-target notify helpers move to a satellite module to hold the file under the line cap.
|
Pushed a follow-up commit (27a6f1a) that closes the main gap left by the base PR: old, never-adopted ZCode rows used to open as the read-only transcript from "Resume in New Chat", never reaching the structured adoption path this PR implements. What changed:
Housekeeping: two launch-target notify helpers moved into a satellite module ( Verification: adapter suite 16 tests, routing suite 24 tests, parity + menu suites green; tsc node/web/cli clean; oxlint + oxfmt clean. |
A completed rewrite the sink refuses used to be lost: the coalesced stream was no longer dirty, so the coalescer timer never retried it and the journal kept a running row for a finished turn. Pending completions are now retained and retried on the coalescing window until the sink admits them.
Adopts upstream's structured-agent abstractions rather than replaying our older shapes: ZCode's launch now composes through composeStructuredLaunch with a ZCode launch part, the sealed child env supplies identity and spawn token, and transcript adoption plus Session History ownership are registration data (ZCODE_CLI_CONVERSATIONS). ZCode declares its model catalog unavailable because the app-server lists models only inside a session. Conflicts resolved toward upstream in the runtime registrations, session create adoption, structured-session ownership, and the OpenCode transcript reader; the ZCode journal admission type moved to the sink to break an import cycle.
Upstream's native-chat design-token lint rejects arbitrary font sizes on changed lines. The history chat header used a fixed 11px, which also meant it ignored the user's chat font setting; text-2xs is the same size at root and scales with it.
…ly sends Upstream added the folder-workspace parity case before this branch began attaching the resume route to the request, so its expectation stayed short one field the sibling-worktree case already pins.
… row-action extraction Conflicts resolved toward upstream's newer abstractions rather than our own: - resume-in-chat now asks `adoptsCliConversations(agent)`, the shared list upstream added for agents that can take over a CLI conversation. 'zcode' joins that list: the ZCode adapter registers a transcript import, and upstream's registration test asserts the two sets are equal. - the ZCode two-lane decision (structured adoption vs. the read-only history tab) survives as the `route` the eligibility gate returns, carried through the row action and the tab menu. - `AiVaultVirtualRow` takes upstream's `resolveAiVaultSessionRowChatActions`, with `resumeInNewChatRoute` added to its result. Two oversized files split instead of given a max-lines disable, per AGENTS.md: `orcad-entry.ts` (upstream grew it to 301 counted lines over budget, so a merge commit touching it cannot pass the pre-commit lint) sheds its Node host adapters into `orcad-host-adapters.ts`, and the structured-session runtime sheds its attention delivery wiring.
…code adoption Adopt upstream's orcad-host-adapters extraction (drop our duplicate). Keep ZCode in the CLI-conversation adoption map under upstream's new row-agent shape. Port upstream's agent-aware preparation-failure notification into the extracted ai-vault-session-launch-target-notify module the merged call sites use.
…versation import Upstream reworked StructuredAgentTranscriptImport to pass AgentSessionAccountHome instead of raw path strings. ZCode's single-directory import now goes through the same directoryTranscriptImport adapter Claude/Codex/Grok/OMP use. Behavior unchanged: one candidate (the chat's own home), transcript resolved only if its SQLite store exists.
…ting Upstream already routes zcode through the OpenCode reader branch (opencode || opencode2 || zcode), so the branch this PR added below it could never run. Keep its rationale as a comment on the branch that does. Behavior unchanged: zcode still resolves to the OpenCode-v1 SQLite transcript reader.
…ed-chat # Conflicts: # src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts
|
Merged current Resolution notes for reviewers:
Local verification on the merge commit: all four typecheck projects clean (16.7k + 14.9k + 2.9k + 31 files), the 10 test files this branch touches pass (115 tests), oxlint clean on changed files. The upstream PR workflow has not run for this commit yet (fork PRs wait for a maintainer to approve the run); |
…ed-chat # Conflicts: # src/main/runtime/structured-agent-runtime-registrations-acp.test.ts # src/renderer/src/components/right-sidebar/ai-vault-session-surface-switch.ts
…hild Upstream split the provider contract into acquire (spawn only) and open (create/resume plus the handle), so the zcode adapter now publishes the child with no provider session id and mints the link at open time.
ELI5
Before, opening a past ZCode conversation in Orca showed a frozen, read-only transcript. Starting a new ZCode session looked like a terminal, not a chat. This PR makes ZCode conversations work like the other agents' chats: a new session opens as a real chat you can type into, and an old session can be reopened as a chat and continued from where it left off.
What Changed
Before: ZCode history chats were read-only transcripts (from #27131), and a new ZCode session fell back to the terminal-style view. The native chat's structured-session system knew about pi, Codex, and Claude — but not ZCode. "Resume in New Chat" on an old ZCode row always opened the read-only transcript, because the row had never been adopted as a structured session.
After: When the Experimental Chat UI setting is on, a ZCode session opens as a live chat. You type, the assistant answers with streamed text and tool calls, you can stop a turn, and you can approve or deny tool permissions in the chat. Opening a past ZCode conversation resumes it in the same live chat, with the full history shown. "Resume in New Chat" on an old ZCode row now opens a live chat too — except when the conversation's database lives inside WSL, which a local app-server child cannot open; those keep the read-only transcript.
Mechanism: a new
src/main/zcode/adapter family drives onezcode app-server --stdiochild per session over ZCode's NDJSON protocol. Acquiring a session spawns the child; opening it then creates or resumes the conversation, subscribes to its event stream, seeds the journal from the snapshot the provider returns, and mints the provider handle. That acquire/open split is upstream's current provider contract, so the adapter publishes a spawned child with no session id and refuses dispatch until the open lands.session/senddispatches a message,session/stopcancels a turn, and the child's reverse-RPC permission requests surface as chat approvals. ZCode registers in the agent-agnostic structured-session registry and joins the provider-handle providers, so the existing provisional-tab, resume, and routing machinery adopts it with no renderer changes. The read-only history layer now distinguishes an adopted structured tab (by itsstructured-agent-session-tab-id prefix) from a vault row's read-only tab, so the two layers never paint the same tab. For routing, the resume-in-chat eligibility gate now emits a route (structuredorhistory) alongside its verdict: an unadopted local ZCode row routes to structured adoption, a WSL-stored or remote row routes to the read-only tab, and an already-adopted row reopens its own chat. The route travels through the surface-switch targets and the tab right-click menu into the launch action, so the row menu and the tab menu always offer the same thing. The main Resume action is unchanged and keeps the claude/codex semantics: an unadopted row opens the CLI terminal, an adopted row opens its chat.Why
ZCode's app-server protocol gives a whole-conversation snapshot on create and resume, so the journal can be seeded from the provider itself — no transcript-file import path was needed, unlike Codex/Claude adoption. Building a regular adapter on the existing registry was much smaller than any parallel ZCode-specific chat path, and it inherits the journal-first durability, provisional tabs, and recovery behavior the other agents already have. For the resume routing, reusing the eligibility gate as the single decision point (rather than adding a second zcode-specific check in the launch action) means the row menu and the tab menu cannot drift apart, and the WSL-storage refusal lives in one place next to the other storage refusals.
Linked Issue
Follow-up to PR #27131 (read-only ZCode history chats); no separate issue.
Visual Proof
N/A for automated proof here; the behavior change is visible as: with Experimental Chat UI on, a new ZCode session renders as a chat with streamed replies, a history row opens as a resumable live chat instead of a frozen transcript, and "Resume in New Chat" on an old row opens that live chat rather than a read-only one. (Real-machine verification steps under Testing.)
Testing
6cd2550e61(2026-10-22) and adapted to the split provider contract (acquirespawns the child;opencreates or resumes the conversation and answers the handle). Two merge overlaps resolved by taking upstream's abstraction and keeping the ZCode additions.zcode-app-server-connection.test.ts, 6 tests over a realnode -eNDJSON child), adapter behavior (zcode-structured-session-adapter.test.ts, 17 tests: spawn-then-open, create/resume, refusal, timeout, cancel, close, exit, sink binding, permission routing), plus existing suites updated.ai-vault-session-resume-in-chat.test.ts, 24 tests): an unadopted local ZCode row routes to structured, a WSL-stored one to the read-only tab (with and without a structured route available), an adopted one reports already-structured, and a WSL-stored non-ZCode row keeps the remote refusal. Parity tests pin that the tab right-click menu passes the same route as the row menu.session/requestRuntimePreferencesreverse request) → subscribe → send → 14 streamed events → resume returning the full conversation snapshot.src/main/zcode150 passed / 7 skipped. The wider local set (agent-session-wire,acp,claude,runtime/structured-agent-*,zcode) still reports host-specific failures on this machine — POSIX PATH expectations,C:\vsC:/temp paths, and Antigravity sign-in capture — none in the files this PR touches. The full unit suite is not a meaningful local gate on Windows, so it is left to CI's Linux runners rather than reported as green here.supportsLocation= supervised local child), same as Codex, so SSH/WSL hosts keep their existing behavior; remote rows never enter the structured route (covered by theremoterefusal tests).AI Disclosure
Developed with assistance from ZCode (GLM); all changes reviewed and tested by the author.
Review
Agent skill upstream boundary
Notes
Security: the child runs with the workspace as cwd; permission requests from the child are gated through the existing chat approval flow, and the answers are limited to the decision words the protocol defines. Cross-platform: process spawning goes through the shared managed-provider-process layer (windowsHide, no shell); no paths or shortcuts are hardcoded; the ZCode command resolves through the shared CLI resolver with the
ZCODE_HOMEaccount home honored. WSL-stored conversations are detected by their UNC transcript path and routed to the read-only tab because the app-server child runs on the host OS. Remote SSH: out of scope by design (local child only), and mixed-version peers are unaffected because nothing changes on the wire between client and host.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)