Repository navigation
[Refactor] ChatStreamManager: the Main process owns the live execution registry #140
Copy link
Copy link
Closed
Labels
area:foundationModule boundaries, architecture, tech debtModule boundaries, architecture, tech debtrefactorInternal restructuring, no behavior changeInternal restructuring, no behavior change
Description
Part of #138.
Second step of the epic. #139 puts the outcome on the message; this puts the running turn somewhere that can be asked about.
The problem with what is there now
const activeStreams = new Map<string, AbortController>()That map is the entire live state of every in-flight turn. It cannot answer any question the features on the roadmap need:
- Is this turn still streaming, and since when? (needed for a timeout)
- How much text has it produced so far? (needed to persist a partial turn without the renderer's copy)
- What does the renderer get when it opens the notebook mid-turn? (needed for attach/replay)
- Which turn is this chunk for, and is it in order? (needed before the protocol change)
What this introduces
A Main-process registry keyed by execution id, holding one object per running turn:
interface ChatExecution {
readonly id: string
readonly messageId: string
readonly sessionId: string
readonly notebookId: string
status: ChatExecutionStatus // pending → streaming → terminal (once)
outcome?: ChatExecutionOutcome
readonly abort: AbortController
readonly startedAt: number
seq: number // monotonic per execution
message: AccumulatedMessage // the final message, assembled here
buffer: StreamEvent[] // bounded; for attach while streaming
}- The only place a terminal transition happens is this object, and it accepts one outcome ([Refactor] Chat execution semantics: one terminal outcome per turn, persisted #139's rule, now enforced at the registry rather than at the call site).
abort(reason)recordsabortedbefore cancelling upstream, so the outcome is decided by the stopper rather than inferred from a rejection arriving later.- The registry owns subscriber bookkeeping: a renderer that attaches mid-turn gets the current accumulated message and subsequent events; a renderer that goes away does not cancel the turn by itself (that becomes a policy, not an accident of
WebContentsListenerlifetime). - Persistence is driven from here, not from a callback closure inside an IPC handler.
Acceptance
- Every turn in flight is present exactly once in the registry, with a status
(seqmoved to [Refactor] Stream protocol: AiService returns a chunk stream, and the hand-made done chunk is deleted #141: a counter nobody reads is structure without a consumer, and the envelope that carries it lands there) - A turn reaches exactly one terminal status; tests assert it cannot be flipped afterwards
-
abortsetsabortedand the later upstream rejection does not change it - A second turn in the same notebook does not affect the first turn's outcome or content
- Registry entries are removed on terminal status, and nothing leaks after a failed turn
- The renderer can attach to a running execution and receive its current state — moved to [Refactor] Stream protocol: AiService returns a chunk stream, and the hand-made done chunk is deleted #141, where the envelope gives attach a consumer and a shape
Not here
- No protocol change:
chatHandlersstill forwards the existing chunk shapes. The registry is introduced underneath it, so this PR is revertible without touching the transcript. - No timeouts or retry policy — the registry makes them possible, [Feat] Stream reliability: idle timeout, Continue vs Retry, output ceiling configuration #143 decides them.
Closed by #145 (ChatStreamManager), which moved the lifecycle and its tests into
src/main/services/chat/ and left the renderer-facing events unchanged.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area:foundationModule boundaries, architecture, tech debtModule boundaries, architecture, tech debtrefactorInternal restructuring, no behavior changeInternal restructuring, no behavior change
Activity
fix(chat): settle a turn once, so an abort is not a completion
fix(chat): settle a turn once, so an abort is not a completion (#144)
refactor(chat): move the turn lifecycle into a Main-process ChatStrea…
refactor(chat): carry the SDK's own events to the renderer, and let t…
feat(chat): render an answer's state, and keep the part that arrived
feat(chat): render an answer's state, and keep the part that arrived (#…