Merge remote-tracking branch 'origin/worktree/session-format-04-live-assistant-stream' into worktree/session-format-05-v1-v2-chunk-migration

# Conflicts:
#	.agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.md
#	.agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.zh.md
#	.agents/notes/implemented/simplification/2026-08-06-buffer-free-feedback-telemetry.i18n.yaml
#	.agents/notes/implemented/simplification/2026-08-06-buffer-free-feedback-telemetry.md
#	.agents/notes/implemented/simplification/2026-08-06-buffer-free-feedback-telemetry.zh.md
#	apps/web/tests/scaffold.ts
#	docs/config-catalog.i18n.yaml
#	docs/config-catalog.md
#	docs/config-catalog.zh.md
#	packages/api/session-controller/README.i18n.yaml
#	packages/api/session-controller/README.md
#	packages/api/session-controller/README.zh.md
#	packages/api/session-controller/src/client/sessions/assistant-stream.ts
#	packages/core/agent-loop/src/agent.ts
#	packages/core/agent-loop/src/assistant-stream.ts
#	packages/core/agent-loop/tests/loop.spec.ts
#	packages/core/agent/src/runtime-types.ts
#	packages/session/session-telemetry/tests/telemetry.spec.ts
#	packages/test-support/llm-replay/README.i18n.yaml
#	packages/test-support/llm-replay/README.md
#	packages/test-support/llm-replay/README.zh.md
#	packages/test-support/llm-replay/src/alpha-refusal-fixtures.ts
#	packages/test-support/llm-replay/src/index.ts
#	packages/test-support/llm-replay/tests/llm-replay.spec.ts
#	packages/test-support/session-snapshot/README.i18n.yaml
#	packages/test-support/session-snapshot/README.md
#	packages/test-support/session-snapshot/README.zh.md
#	packages/test-support/session-snapshot/src/suite.ts
#	packages/test-support/session-snapshot/tests/suite.spec.ts
#	snapshots/sdk/sdk.snapshot.ts
#	snapshots/session/headless.snapshot.ts
This commit is contained in:
Tianyi Cui
2026-09-03 03:10:05 +08:00
135 changed files with 926 additions and 707 deletions
@@ -101,6 +101,7 @@ export class ClientAssistantStream {
this.durableCursor = Math.max(this.durableCursor, event.seq)
this.transientInGap = 0
if (this.attemptForSettlement(event) !== undefined) {
if (this.pending.has(event.seq)) return { type: 'rebaseline' }
this.pending.set(event.seq, entry)
return undefined
}
@@ -115,6 +116,7 @@ export class ClientAssistantStream {
acceptFrame(frame: SessionAssistantStreamFrame): ClientAssistantStreamResult {
switch (frame.type) {
case 'start':
if (this.activeAttempt !== undefined || this.pending.size > 0) return { type: 'rebaseline' }
this.pending.clear()
this.activeAttempt = {
attemptId: String(frame.attemptId),
@@ -126,9 +128,11 @@ export class ClientAssistantStream {
return undefined
case 'chunk': {
const attempt = this.activeAttempt
if (attempt === undefined
|| attempt.attemptId !== String(frame.attemptId)
|| frame.index !== attempt.nextIndex) return { type: 'rebaseline' }
// A controller mounted after the Host saw this attempt has no start
// frame to reconstruct. Its durable settlement publishes directly;
// ignore the transient suffix until the next known start.
if (attempt === undefined || attempt.attemptId !== String(frame.attemptId)) return undefined
if (frame.index !== attempt.nextIndex) return { type: 'rebaseline' }
attempt.nextIndex += 1
this.transientInGap += 1
return {
@@ -151,10 +155,10 @@ export class ClientAssistantStream {
}
case 'end': {
const attempt = this.activeAttempt
this.activeAttempt = undefined
if (attempt === undefined || attempt.attemptId !== String(frame.attemptId)) {
return { type: 'rebaseline' }
return undefined
}
this.activeAttempt = undefined
if (frame.index !== attempt.nextIndex) return { type: 'rebaseline' }
if (frame.outcome.kind === 'abandoned') {
return this.pending.size === 0 ? undefined : { type: 'rebaseline' }
@@ -162,9 +166,7 @@ export class ClientAssistantStream {
if (this.publishedSeqs.has(frame.outcome.seq)) return undefined
const entry = this.pending.get(frame.outcome.seq)
if (entry === undefined
|| entry.event.type !== frame.outcome.eventType
|| entry.event.data.turn !== attempt.turn
|| entry.event.data.step !== attempt.step) {
|| entry.event.type !== frame.outcome.eventType) {
return { type: 'rebaseline' }
}
this.pending.delete(frame.outcome.seq)
+2 -1
View File
@@ -369,7 +369,8 @@ export class SessionController extends TypertRemoteService {
* Follow one Session log from its opening or resume cursor.
* @param request - durable address and last committed sequence already held by the caller.
* @param signal - cancellation owned by the Remote stream carrier.
* @returns a complete opening snapshot followed by gap-free event frames.
* @returns a complete opening snapshot followed by gap-free durable event
* frames and optional cursorless assistant-stream frames.
*/
@Remote({ mode: 'stream' })
follow(request: SessionFollowRequest, signal: AbortSignal): AsyncIterable<SessionFollowFrame> {
@@ -145,8 +145,6 @@ export class ApiSessionList {
private summarizeCold(header: SessionHeader): SessionSummary {
const projections = this.projectionsFor(header, undefined)
const raced = this.ctx.sessions.get(header.id)
if (raced !== undefined) return this.summaryFor(raced)
const metadata = projections?.values.sessionListMetadata
return {
sessionId: header.id,