fix(session): harden embedded assistant streams

This commit is contained in:
Tianyi Cui
2026-09-02 04:00:33 +08:00
parent f99b06eaed
commit 7e5b72ceee
68 changed files with 573 additions and 251 deletions
@@ -163,6 +163,20 @@ export class MutableSessionEventSource implements SessionEventSource {
})
}
/**
* Replace one attempt's transient rows with its committed durable settlement.
* @param attemptId - process-local attempt whose live rows are now redundant.
* @param entry - durable settlement committed for that attempt.
*/
settleAssistant(attemptId: string, entry: SessionLiveEventEntry): void {
const entries = materialize(this.window).filter(candidate => (
candidate.type !== 'transient' || String(candidate.event.data.attemptId) !== attemptId
))
entries.push(entry)
this.window = leaf(entries)
this.publish(this.snapshot.hasMore, { kind: 'replace', entries })
}
private publish(
hasMore: boolean,
change: SessionEventChange,
@@ -14,7 +14,7 @@ import type {
interface ActiveAttempt {
readonly attemptId: string
readonly startedTime: number
readonly startedAfterSeq: number
readonly turn: number
readonly step: number
nextIndex: number
@@ -23,6 +23,11 @@ interface ActiveAttempt {
/** One Web publication decision from the assistant stream fold. */
export type ClientAssistantStreamResult =
| { readonly type: 'publish'; readonly entry: SessionLiveEventEntry }
| {
readonly type: 'settlement'
readonly attemptId: string
readonly entry: SessionLiveEventEntry
}
| { readonly type: 'transient'; readonly entry: SessionTransientEventEntry }
| { readonly type: 'rebaseline' }
| undefined
@@ -52,7 +57,7 @@ export class ClientAssistantStream {
if (opening !== undefined) {
this.activeAttempt = {
attemptId: String(opening.attemptId),
startedTime: opening.startedTime,
startedAfterSeq: opening.startedAfterSeq,
turn: opening.turn,
step: opening.step,
nextIndex: opening.nextIndex,
@@ -120,7 +125,7 @@ export class ClientAssistantStream {
this.pending.clear()
this.activeAttempt = {
attemptId: String(frame.attemptId),
startedTime: frame.startedTime,
startedAfterSeq: frame.startedAfterSeq,
turn: frame.turn,
step: frame.step,
nextIndex: 0,
@@ -170,7 +175,8 @@ export class ClientAssistantStream {
return { type: 'rebaseline' }
}
this.pending.delete(frame.outcome.seq)
return this.publish(entry)
this.publishedSeqs.add(entry.event.seq)
return { type: 'settlement', attemptId: attempt.attemptId, entry }
}
}
}
@@ -182,7 +188,7 @@ export class ClientAssistantStream {
if (attempt === undefined
|| (event.type !== 'assistant/message' && event.type !== 'assistant/attempt')
|| (event.type === 'assistant/message' && event.surfaceOp !== 'append')
|| event.time < attempt.startedTime
|| event.seq <= attempt.startedAfterSeq
|| attempt.turn !== event.data.turn
|| attempt.step !== event.data.step) return undefined
return attempt
@@ -668,6 +668,12 @@ export class Session implements SessionFace {
})
return
}
if (result?.type === 'settlement') {
this.eventSource.settleAssistant(result.attemptId, result.entry)
this.observeSubmissionEvent(result.entry.event)
this.notifier.markDirty()
return
}
if (result?.type === 'publish' && this.appendLive(result.entry)) {
this.notifier.markDirty()
} else if (result?.type === 'transient') {