Back to Cherry Studio

IPC Transport

docs/references/ai/ipc-transport.md

2.0.04.2 KB
Original Source

IPC Transport

What it is

IpcChatTransport (src/renderer/services/aiTransport/IpcChatTransport.ts) implements AI SDK's ChatTransport<CherryUIMessage> over Electron IPC. The renderer feeds it into useChat({ id: topicId, transport: ... }). The ChatTransport interface has only two methods — sendMessages / reconnectToStream; the transport relays each over window.api.ai.stream* to Main's AiStreamManager. cancel is not a transport method: it is the cancel callback of the ReadableStream that sendMessages returns (AI SDK invokes it on unmount/disposal), and abort is driven by the request's abortSignal.

useChat({ id: topicId, transport: new IpcChatTransport(defaultBody) })
   │  transport methods
   ├─ sendMessages         → window.api.ai.streamOpen   (Ai_Stream_Open)
   ├─ reconnectToStream    → window.api.ai.streamAttach (Ai_Stream_Attach)
   │  returned-stream / signal callbacks
   ├─ stream cancel()      → window.api.ai.streamDetach (Ai_Stream_Detach)
   └─ request abort signal → window.api.ai.streamAbort  (Ai_Stream_Abort)

Detach ≠ abort. cancel() (e.g. unmount/disposal) calls streamDetach: it drops this subscriber while Main keeps generating and persists the result. Stopping generation is a separate path — the request's abortSignal firing calls streamAbort. Conflating the two would resurrect the v1 "unmount → cancel → upstream abort → lost reply" bug class.

Per-topic chunks arrive via onStreamChunk listeners filtered by topicId.

Triggers

sendMessages distinguishes two triggers:

TriggerWhat it does
submit-messageIncludes userMessageParts (the latest message) so Main persists it
regenerate-messageSends parentAnchorId only; Main re-runs from the existing parent

Cherry's transport never derives continue-conversation from message-state introspection. Approval-driven resumption goes through the explicit Ai_ToolApproval_Respond IPC handled by useToolApprovalBridge.

Dispatch coordinator

streamDispatchCoordinator (src/renderer/services/aiTransport/streamDispatchCoordinator.ts) sits between the transport and the IPC call so the Ai_Stream_Open ack (userMessageId, placeholder ids, executionIds) is observable to callers that need to join optimistic UI bubbles, rather than being thrown away by AI SDK's transport interface.

It does not serialize sends — there is no single-in-flight guard in the coordinator. Concurrency for a topic is arbitrated on the Main side: a chat resubmit to a live topic is persisted and queued as a steer (AiStreamManager.enqueuePendingSteer) — the running turn yields and a continuation answers it — while an agent-session follow-up attaches to the running stream.

Per-execution demux

The chunk stream from Main is keyed by (topicId, executionId). TopicStreamSubscription (src/renderer/services/aiTransport/TopicStreamSubscription.ts) owns the topic-level streamAttach / streamDetach with ref-counted lifecycle and demuxes chunks into per-execution branch ReadableStreams, so multi-model parallel responses render as separate AI SDK messages on the same topic. useExecutionOverlay consumes each branch through readUIMessageStream — the same accumulator Main runs in pipeStreamLoop, so the renderer overlay and the persisted message are structurally identical.

See Execution Overlay for the merge-function symmetry, seed rule, cancellation layering, and lifecycle.

Topic-level subscription

useTopicStreamStatus(topicId) reads topic.stream.statuses.<topicId> from the shared cache. The cache is the cross-window source of truth for:

  • pending / streaming / awaiting-approval / done / error / aborted
  • broadcast-completion anchor ids

classifyTurn(status) decodes the status into the TurnStateFlags predicates the UI consumes (isStreamLive, isTurnActive, isAwaitingApproval, isTerminal).

Where to read more