feat(session)!: embed assistant streams in format v2

This commit is contained in:
Tianyi Cui
2026-09-02 04:00:01 +08:00
parent 0bb7bba015
commit f99b06eaed
387 changed files with 9491 additions and 4625 deletions
@@ -0,0 +1,56 @@
import { describe, expect, it } from 'vitest'
import { LlmAttemptId } from '@deepseek-ai/dsh-llm'
import { SessionAssistantStreamAccumulator } from '../src/assistant-stream.ts'
describe('SessionAssistantStreamAccumulator', () => {
it('replaces stale lifecycles, rejects frame gaps, and caches each baseline', () => {
const accumulator = new SessionAssistantStreamAccumulator()
const empty = accumulator.snapshot()
expect(accumulator.snapshot()).toBe(empty)
accumulator.accept({
type: 'start', attemptId: LlmAttemptId('stale'), revision: 2,
startedTime: 1, turn: 1, step: 1,
})
expect(accumulator.snapshot()).toEqual({ revision: 2 })
accumulator.accept({
type: 'start', attemptId: LlmAttemptId('current'), revision: 1,
startedTime: 2, turn: 2, step: 3,
})
expect(accumulator.snapshot()).toMatchObject({
revision: 1,
activeAttempt: { attemptId: 'current', turn: 2, step: 3, nextIndex: 0, stream: [] },
})
accumulator.accept({
type: 'chunk', attemptId: LlmAttemptId('other'), revision: 2, index: 0,
time: 4, chunk: { type: 'text-delta', index: 0, text: 'lost' },
})
expect(accumulator.snapshot()).toEqual({ revision: 2 })
accumulator.accept({
type: 'start', attemptId: LlmAttemptId('settled'), revision: 3,
startedTime: 3, turn: 2, step: 4,
})
accumulator.accept({
type: 'chunk', attemptId: LlmAttemptId('settled'), revision: 4, index: 0,
time: 5, chunk: { type: 'text-delta', index: 0, text: 'ok' },
})
const active = accumulator.snapshot()
expect(active).toMatchObject({
revision: 4,
activeAttempt: {
attemptId: 'settled', nextIndex: 1,
stream: [{ type: 'text-chunks', time0: 5, index: 0, dt: [], texts: ['ok'] }],
},
})
expect(accumulator.snapshot()).toBe(active)
accumulator.accept({
type: 'end', attemptId: LlmAttemptId('settled'), revision: 5, index: 1,
outcome: { kind: 'abandoned' },
})
expect(accumulator.snapshot()).toEqual({ revision: 5 })
})
})
@@ -178,6 +178,7 @@ describe('Session attachment authorization', () => {
{ ...event('assistant/message', SessionSeq(1), {
turn: 1,
step: 1,
stream: [],
message: createAssistantMessage({
content: [{ type: 'image', attachment: message }],
source: { provider: 'fixture', model: 'fixture' },
@@ -191,10 +192,30 @@ describe('Session attachment authorization', () => {
source: { kind: 'user' },
})],
}),
event('assistant/chunk', SessionSeq(3), {
event('assistant/attempt', SessionSeq(3), {
turn: 1,
step: 1,
chunk: { type: 'block-end', index: 0, block: { type: 'image', attachment: streamed } },
stream: [
{
type: 'chunk',
time: 3,
chunk: { type: 'block-start', index: 0, blockType: 'text' },
},
{
type: 'chunk',
time: 3,
chunk: { type: 'block-end', index: 0, block: { type: 'text', text: '' } },
},
],
}),
event('assistant/attempt', SessionSeq(4), {
turn: 1,
step: 1,
stream: [{
type: 'chunk',
time: 4,
chunk: { type: 'block-end', index: 0, block: { type: 'image', attachment: streamed } },
}],
}),
]
const readImage = vi.fn((ref: ImageAttachmentRef) => Promise.resolve({ ref, data: Uint8Array.of(1) }))
@@ -27,13 +27,18 @@ export const ev = {
}) }),
stepStart: (seq: SessionSeq, turn: number, step = 0): SessionEvent =>
at(seq, { type: 'step/start', data: { turn, step } }),
chunkStart: (seq: SessionSeq, turn: number, step = 0, index = 0): SessionEvent =>
at(seq, { type: 'assistant/chunk', data: { turn, step, chunk: { type: 'block-start', index, blockType: 'text' } } }),
chunkText: (seq: SessionSeq, turn: number, piece: string, step = 0, index = 0): SessionEvent =>
at(seq, { type: 'assistant/chunk', data: { turn, step, chunk: { type: 'text-delta', index, text: piece } } }),
assistant: (seq: SessionSeq, turn: number, body: string, step = 0): SessionEvent =>
at(seq, { type: 'assistant/message', surfaceOp: 'append', data: {
turn, step,
stream: [
{ type: 'chunk', time: 1_700_000_000_000 + seq, chunk: { type: 'block-start', index: 0, blockType: 'text' } },
{ type: 'text-chunks', time0: 1_700_000_000_000 + seq, index: 0, dt: [], texts: [body] },
{
type: 'chunk', time: 1_700_000_000_000 + seq,
chunk: { type: 'block-end', index: 0, block: { type: 'text', text: body } },
},
{ type: 'chunk', time: 1_700_000_000_000 + seq, chunk: { type: 'finish', reason: { kind: 'stop' } } },
],
message: createMessage({
role: 'assistant',
content: text(body),
@@ -161,7 +161,6 @@ export class FakeApiClient {
}
assistantStreamBaseline: SessionAssistantStreamBaseline = {
revision: 0,
attempts: [],
}
workspaceBaseline: Extract<WorkspaceFollowFrame, { type: 'baseline' }>['value'] = {
items: [],
@@ -393,9 +392,10 @@ export class FakeApiClient {
yield {
type: 'snapshot',
header: {
version: 1,
version: 2,
id: sessionId,
createdAt: 0,
isSeeded: false,
...(request.address.kind === 'subagent'
? { origin: 'subagent' as const, parentSession: request.address.parentSessionId }
: {}),
@@ -1,4 +1,4 @@
/** Packed history records become one event-shaped Client value per wire record. */
/** V2 history records become one event-shaped Client value per wire record. */
import { describe, expect, it } from 'vitest'
import { ToolCallId } from '@deepseek-ai/dsh-llm/brand'
@@ -26,53 +26,69 @@ describe('Session history record projection', () => {
expect(historyRecordLastSeq(ordinary)).toBe(7)
})
it('retains one packed text row without copying or reshaping it', () => {
const packed: SessionHistoryRecord = {
type: 'chunks',
it('retains one message with an embedded compact text stream', () => {
const message: SessionHistoryRecord = {
type: 'event',
event: {
type: 'chunkrow/text-chunks',
type: 'assistant/message',
seq: 11,
time: 20,
data: { turn: 1, step: 2, index: 0, dt: [1, 2, 3], texts: ['a', 'b', 'c', 'd'] },
data: {
turn: 1,
step: 2,
message: {
id: 'message-1',
role: 'assistant',
content: [{ type: 'text', text: 'abcd' }],
source: { kind: 'model', provider: 'fixture', model: 'fixture-v2' },
},
stream: [{ type: 'text-chunks', time0: 20, index: 0, dt: [1, 2, 3], texts: ['a', 'b', 'c', 'd'] }],
},
},
}
const [entry] = historyEntries([packed])
if (entry?.type !== 'chunks') throw new Error('expected packed history entry')
const [entry] = historyEntries([message])
if (entry?.type !== 'event') throw new Error('expected v2 history entry')
const { event } = entry
expect(entry).toBe(packed)
expect(event).toBe(packed.event)
expect(historyRecordFirstSeq(packed)).toBe(11)
expect(entry).toBe(message)
expect(event).toBe(message.event)
expect(historyRecordFirstSeq(message)).toBe(11)
expect(event.time).toBe(20)
expect(historyRecordLastSeq(packed)).toBe(14)
expect(historyRecordLastSeq(message)).toBe(11)
})
it('preserves a packed tool-call row and optional-name absence', () => {
const packed: SessionHistoryRecord = {
type: 'chunks',
it('preserves an attempt stream and optional tool-name absence', () => {
const attempt: SessionHistoryRecord = {
type: 'event',
event: {
type: 'chunkrow/tool-call-chunks',
type: 'assistant/attempt',
seq: 20,
time: 200,
data: {
turn: 2,
step: 4,
index: 1,
id: ToolCallId('call-1'),
dt: [2, 3],
args: ['', '{"x":', '1}'],
stream: [{
type: 'tool-call-chunks',
time0: 200,
index: 1,
id: ToolCallId('call-1'),
dt: [2, 3],
args: ['', '{"x":', '1}'],
}],
},
},
}
const [entry] = historyEntries([packed])
if (entry?.type !== 'chunks') throw new Error('expected packed history entry')
const [entry] = historyEntries([attempt])
if (entry?.type !== 'event') throw new Error('expected v2 history entry')
const { event } = entry
if (event.type !== 'chunkrow/tool-call-chunks') throw new Error('expected packed history event')
expect(event).toBe(packed.event)
expect(Object.hasOwn(event.data, 'name')).toBe(false)
expect(historyRecordLastSeq(packed)).toBe(22)
if (event.type !== 'assistant/attempt') throw new Error('expected Assistant attempt event')
expect(event).toBe(attempt.event)
const [record] = event.data.stream
expect(record).toMatchObject({ type: 'tool-call-chunks', id: 'call-1' })
expect(Object.hasOwn(record as object, 'name')).toBe(false)
expect(historyRecordLastSeq(attempt)).toBe(20)
})
})
@@ -3,17 +3,11 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { type Agent, type AssistantStreamFrame } from '@deepseek-ai/dsh-agent'
import SessionStore, { SessionSeq } from '@deepseek-ai/dsh-session'
import { decodeStorageRecord, type ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows'
import SessionStore from '@deepseek-ai/dsh-session'
import { LlmAttemptId, ToolCallId, createMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
import { SessionHistoryController } from '@deepseek-ai/dsh-api-session-controller/src/history.ts'
import type {
ChunkRowEvent,
SessionFollowFrame,
SessionPage,
SessionWireEvent,
} from '@deepseek-ai/dsh-api-session-controller/types'
import type { SessionFollowFrame, SessionPage, SessionWireEvent } from '@deepseek-ai/dsh-api-session-controller/types'
import { createSessionTestRemote, installSessionReadTestServices } from './test-remote.ts'
/** Append a production-shaped human prompt to the session surface. */
@@ -33,6 +27,7 @@ function appendAssistantText(session: Session, text: string, step: number): Sess
content: [{ type: 'text', text }],
source: { kind: 'model', provider: 'p', model: 'm' },
}),
stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: [], texts: [text] }],
}, { surfaceOp: 'append' })
}
@@ -94,25 +89,87 @@ async function disposeFollow(
await ctx.fiber.dispose()
}
/** Expand packed page records for assertions over the logical journal. */
/** Read scalar v2 page records for assertions over the logical journal. */
function pageEvents(page: SessionPage): SessionWireEvent[] {
return page.records.flatMap(record => record.type === 'event'
? [record.event]
: decodeStorageRecord(chunkRow(record.event)).map(event => event as unknown as SessionWireEvent))
}
function chunkRow(event: ChunkRowEvent): ChunkRow {
switch (event.type) {
case 'chunkrow/text-chunks':
return { type: 'text-chunks', seq0: SessionSeq(event.seq), time0: event.time, data: event.data }
case 'chunkrow/reasoning-chunks':
return { type: 'reasoning-chunks', seq0: SessionSeq(event.seq), time0: event.time, data: event.data }
case 'chunkrow/tool-call-chunks':
return { type: 'tool-call-chunks', seq0: SessionSeq(event.seq), time0: event.time, data: event.data }
}
return page.records.map(record => record.event)
}
describe('Session history raw journal', () => {
it('opens an empty opted-in Assistant baseline before any live attempt exists', async () => {
const { ctx } = await harness()
const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } })
const history = new SessionHistoryController(ctx, (observation) => { observation[Symbol.dispose]() })
const abort = new AbortController()
const iterator = history.follow({
address: { kind: 'session', sessionId: session.id },
assistantStream: true,
}, abort.signal)[Symbol.asyncIterator]()
await expect(iterator.next()).resolves.toMatchObject({
done: false,
value: { type: 'snapshot', assistantStream: { revision: 0 } },
})
abort.abort()
await iterator.next()
await ctx.fiber.dispose()
})
it('filters foreign and opening-baseline frames buffered during the source observation', async () => {
const { ctx } = await harness()
const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } })
const agent = { id: session.id, session, status: 'running', ctx } as Agent
const history = new SessionHistoryController(ctx, (observation) => { observation[Symbol.dispose]() })
const originalObserve = ctx.sessionQuery.observeSession.bind(ctx.sessionQuery)
const entered = Promise.withResolvers<undefined>()
const release = Promise.withResolvers<undefined>()
const observe = vi.spyOn(ctx.sessionQuery, 'observeSession').mockImplementation(async (...args) => {
entered.resolve(undefined)
await release.promise
return originalObserve(...args)
})
const abort = new AbortController()
const iterator = history.follow({
address: { kind: 'session', sessionId: session.id },
assistantStream: true,
}, abort.signal)[Symbol.asyncIterator]()
const opening = iterator.next()
await entered.promise
const attemptId = LlmAttemptId('buffered-opening-attempt')
ctx.emit('agent/assistant-stream', {
agent,
frame: { type: 'start', attemptId, revision: 1, startedTime: 1, turn: 1, step: 1 },
})
ctx.emit('agent/assistant-stream', {
agent,
frame: {
type: 'chunk', attemptId, revision: 2, index: 0,
time: 2, chunk: { type: 'text-delta', index: 0, text: 'buffered' },
},
})
const foreign = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } })
ctx.emit('agent/assistant-stream', {
agent: { id: foreign.id, session: foreign, status: 'running', ctx } as Agent,
frame: {
type: 'start', attemptId: LlmAttemptId('foreign-attempt'), revision: 1,
startedTime: 1, turn: 1, step: 1,
},
})
release.resolve(undefined)
await expect(opening).resolves.toMatchObject({
done: false,
value: { type: 'snapshot', assistantStream: { revision: 2 } },
})
const next = iterator.next()
const durable = session.append('turn/start', { turn: 1 })
await expect(next).resolves.toEqual({ done: false, value: { type: 'event', event: durable } })
abort.abort()
await iterator.next()
observe.mockRestore()
await ctx.fiber.dispose()
})
it('opens an opted-in assistant baseline and preserves mixed live FIFO order', async () => {
const { ctx } = await harness()
const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } })
@@ -126,12 +183,10 @@ describe('Session history raw journal', () => {
type: 'start', attemptId, revision: 1, startedTime: 100,
turn: 1, step: 1,
})
const firstChunk = session.append('assistant/chunk', {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'a' },
})
const firstChunk = { type: 'text-delta', index: 0, text: 'a' } as const
emit({
type: 'chunk', attemptId, revision: 2, index: 0,
chunk: firstChunk.data.chunk, legacyChunkSeq: firstChunk.seq,
time: 1, chunk: firstChunk,
})
const abort = new AbortController()
const iterator = history.follow({
@@ -145,35 +200,29 @@ describe('Session history raw journal', () => {
type: 'snapshot',
assistantStream: {
revision: 2,
attempts: [{
activeAttempt: {
attemptId,
startedTime: 100,
turn: 1,
step: 1,
chunks: [firstChunk.data.chunk],
legacyChunkSeqs: [firstChunk.seq],
}],
nextIndex: 1,
stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: [], texts: ['a'] }],
},
},
},
})
const nextChunk = session.append('assistant/chunk', {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'b' },
})
const nextFrame: AssistantStreamFrame = {
type: 'chunk', attemptId, revision: 3, index: 1,
chunk: nextChunk.data.chunk, legacyChunkSeq: nextChunk.seq,
time: 2, chunk: { type: 'text-delta', index: 0, text: 'b' },
}
emit(nextFrame)
const message = appendAssistantText(session, 'ab', 1)
const endFrame: AssistantStreamFrame = {
type: 'end', attemptId, revision: 4, index: 2, outcome: 'committed',
legacyChunkSeqs: [firstChunk.seq, nextChunk.seq],
type: 'end', attemptId, revision: 4, index: 2,
outcome: { kind: 'committed', eventType: 'assistant/message', seq: message.seq },
}
emit(endFrame)
await expect(iterator.next()).resolves.toEqual({
done: false, value: { type: 'event', event: nextChunk },
})
await expect(iterator.next()).resolves.toEqual({
done: false, value: { type: 'assistant-stream', frame: nextFrame },
})
@@ -201,14 +250,12 @@ describe('Session history raw journal', () => {
turn: 1, step: 1,
},
})
const oldChunk = session.append('assistant/chunk', {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'old' },
})
const oldChunk = { type: 'text-delta', index: 0, text: 'old' } as const
ctx.emit('agent/assistant-stream', {
agent,
frame: {
type: 'chunk', attemptId, revision: 2, index: 0,
chunk: oldChunk.data.chunk, legacyChunkSeq: oldChunk.seq,
time: 101, chunk: oldChunk,
},
})
const abort = new AbortController()
@@ -224,14 +271,14 @@ describe('Session history raw journal', () => {
type: 'snapshot',
assistantStream: {
revision: 2,
attempts: [{
activeAttempt: {
attemptId,
startedTime: 100,
turn: 1,
step: 1,
chunks: [oldChunk.data.chunk],
legacyChunkSeqs: [oldChunk.seq],
}],
nextIndex: 1,
stream: [{ type: 'text-chunks', time0: 101, index: 0, dt: [], texts: ['old'] }],
},
},
},
})
@@ -265,14 +312,12 @@ describe('Session history raw journal', () => {
turn: 1, step: 1,
},
})
const chunk = session.append('assistant/chunk', {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'after gap' },
})
const chunk = { type: 'text-delta', index: 0, text: 'after gap' } as const
ctx.emit('agent/assistant-stream', {
agent,
frame: {
type: 'chunk', attemptId, revision: 3, index: 0,
chunk: chunk.data.chunk, legacyChunkSeq: chunk.seq,
time: 101, chunk,
},
})
const abort = new AbortController()
@@ -286,7 +331,7 @@ describe('Session history raw journal', () => {
done: false,
value: {
type: 'snapshot',
assistantStream: { revision: 3, attempts: [] },
assistantStream: { revision: 3 },
},
})
} finally {
@@ -307,14 +352,12 @@ describe('Session history raw journal', () => {
turn: 1, step: 1,
},
})
const chunk = session.append('assistant/chunk', {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'out of order' },
})
const chunk = { type: 'text-delta', index: 0, text: 'out of order' } as const
ctx.emit('agent/assistant-stream', {
agent,
frame: {
type: 'chunk', attemptId, revision: 2, index: 1,
chunk: chunk.data.chunk, legacyChunkSeq: chunk.seq,
time: 101, chunk,
},
})
const abort = new AbortController()
@@ -328,7 +371,7 @@ describe('Session history raw journal', () => {
done: false,
value: {
type: 'snapshot',
assistantStream: { revision: 2, attempts: [] },
assistantStream: { revision: 2 },
},
})
} finally {
@@ -364,7 +407,7 @@ describe('Session history raw journal', () => {
const first = await firstIterator.next()
if (first.done || first.value.type !== 'snapshot') throw new Error('first follow did not open')
const baseline = first.value.assistantStream
expect(baseline).toMatchObject({ revision: 1, attempts: [{ attemptId }] })
expect(baseline).toMatchObject({ revision: 1, activeAttempt: { attemptId } })
const second = await secondIterator.next()
if (second.done || second.value.type !== 'snapshot') throw new Error('second follow did not open')
expect(second.value.assistantStream).toEqual(baseline)
@@ -392,7 +435,7 @@ describe('Session history raw journal', () => {
done: false,
value: {
type: 'snapshot',
assistantStream: { revision: 0, attempts: [] },
assistantStream: { revision: 0 },
},
})
} finally {
@@ -466,7 +509,7 @@ describe('Session history raw journal', () => {
done: false,
value: {
type: 'snapshot',
assistantStream: { revision: 1, attempts: [{ attemptId: frame.attemptId }] },
assistantStream: { revision: 1, activeAttempt: { attemptId: frame.attemptId } },
},
})
@@ -484,74 +527,6 @@ describe('Session history raw journal', () => {
}
})
it('delivers a baseline-framed chunk whose durable event lands after the opening snapshot', async () => {
const { ctx } = await harness()
const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } })
const agent = { id: session.id, session, status: 'running', ctx } as Agent
const history = new SessionHistoryController(ctx, (observation) => { observation[Symbol.dispose]() })
const attemptId = LlmAttemptId('opening-durable-cut-attempt')
ctx.emit('agent/assistant-stream', {
agent,
frame: {
type: 'start', attemptId, revision: 1, startedTime: 100,
turn: 1, step: 1,
},
})
const observationCaptured = Promise.withResolvers<undefined>()
const releaseObservation = Promise.withResolvers<undefined>()
const originalObserve = ctx.sessionQuery.observeSession.bind(ctx.sessionQuery)
const observe = vi.spyOn(ctx.sessionQuery, 'observeSession').mockImplementation(async (sessionId, options) => {
const observation = await originalObserve(sessionId, options)
observationCaptured.resolve(undefined)
await releaseObservation.promise
return observation
})
const abort = new AbortController()
const iterator = history.follow({
address: { kind: 'session', sessionId: session.id },
assistantStream: true,
}, abort.signal)[Symbol.asyncIterator]()
try {
const opening = iterator.next()
await observationCaptured.promise
const chunk = session.append('assistant/chunk', {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'cut-safe' },
})
ctx.emit('agent/assistant-stream', {
agent,
frame: {
type: 'chunk', attemptId, revision: 2, index: 0,
chunk: chunk.data.chunk, legacyChunkSeq: chunk.seq,
},
})
const after = session.append('turn/start', { turn: 2 })
releaseObservation.resolve(undefined)
await expect(opening).resolves.toMatchObject({
done: false,
value: {
type: 'snapshot',
records: [],
assistantStream: {
revision: 2,
attempts: [{ attemptId, legacyChunkSeqs: [chunk.seq] }],
},
},
})
await expect(iterator.next()).resolves.toEqual({
done: false, value: { type: 'event', event: chunk },
})
await expect(iterator.next()).resolves.toEqual({
done: false, value: { type: 'event', event: after },
})
} finally {
releaseObservation.resolve(undefined)
observe.mockRestore()
await disposeFollow(ctx, iterator, abort)
}
})
it('does not release an old-lifecycle frame after the opening baseline resets to revision one', async () => {
const { ctx } = await harness()
const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } })
@@ -582,14 +557,12 @@ describe('Session history raw journal', () => {
try {
const opening = iterator.next()
await observationStarted.promise
const oldChunk = session.append('assistant/chunk', {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'old lifecycle' },
})
const oldChunk = { type: 'text-delta', index: 0, text: 'old lifecycle' } as const
ctx.emit('agent/assistant-stream', {
agent,
frame: {
type: 'chunk', attemptId, revision: 2, index: 0,
chunk: oldChunk.data.chunk, legacyChunkSeq: oldChunk.seq,
time: 101, chunk: oldChunk,
},
})
ctx.emit('agent/assistant-stream', {
@@ -606,7 +579,9 @@ describe('Session history raw journal', () => {
type: 'snapshot',
assistantStream: {
revision: 1,
attempts: [{ attemptId, startedTime: 200, turn: 2, step: 1, chunks: [] }],
activeAttempt: {
attemptId, startedTime: 200, turn: 2, step: 1, nextIndex: 0, stream: [],
},
},
},
})
@@ -649,8 +624,7 @@ describe('Session history raw journal', () => {
ctx.emit('agent/assistant-stream', {
agent,
frame: {
type: 'end', attemptId, revision: 2, index: 0,
outcome: 'aborted', legacyChunkSeqs: [],
type: 'end', attemptId, revision: 2, index: 0, outcome: { kind: 'abandoned' },
},
})
const next = session.append('turn/end', {
@@ -821,25 +795,23 @@ describe('Session history raw journal', () => {
expect(page.map(event => event.seq)).toEqual(page.map((_event, index) => third.seq + index))
})
it('paginates a message with many provenance sources without variadic argument expansion', async () => {
it('paginates a message with a large embedded stream without expanding physical records', async () => {
const { ctx } = await harness()
const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } })
session.append('turn/start', { turn: 1 })
const sources = Array.from({ length: 128 }, () => session.append('assistant/chunk', {
turn: 1,
step: 1,
chunk: { type: 'text-delta', index: 0, text: 'x' },
}).seq)
session.append('step/start', { turn: 1, step: 1 })
const texts = Array.from({ length: 128 }, () => 'x')
const message = session.append('assistant/message', {
turn: 1,
step: 1,
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: 'x'.repeat(sources.length) }],
content: [{ type: 'text', text: 'x'.repeat(texts.length) }],
source: { kind: 'model', provider: 'p', model: 'm' },
}),
}, { surfaceOp: 'append', sourceEventSeqs: sources })
stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: texts.slice(1).map(() => 0), texts }],
}, { surfaceOp: 'append' })
const scalarMin = Math.min
const min = vi.spyOn(Math, 'min').mockImplementation((...values) => {
@@ -853,68 +825,55 @@ describe('Session history raw journal', () => {
maxMessages: 1,
})
if (!response.ok) throw new Error('unreachable')
expect(pageEvents(response.value).map(event => event.seq)).toEqual([...sources, message.seq])
expect(response.value.records.filter(record => record.type === 'chunks')).toHaveLength(1)
expect(pageEvents(response.value).map(event => event.seq)).toEqual([message.seq])
expect(response.value.records).toEqual([{ type: 'event', event: message }])
expect(response.value.hasMore).toBe(true)
} finally {
min.mockRestore()
}
})
it('encodes reasoning and tool-call runs as aligned chunk events', async () => {
it('keeps an earlier declared source on the same message-aligned page', async () => {
const { ctx } = await harness()
const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } })
const source = session.append('request/context', { provider: 'p', model: 'm' })
const laterSource = session.append('request/context', { provider: 'p', model: 'm' })
const message = session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'with source' }], source: { kind: 'user' },
}), { sourceEventSeqs: [source.seq, laterSource.seq], surfaceOp: 'append' })
const response = await remote.page({
address: { kind: 'session', sessionId: session.id },
throughSeq: message.seq,
maxMessages: 1,
})
if (!response.ok) throw new Error('unreachable')
expect(pageEvents(response.value).map(event => event.seq)).toEqual([source.seq, laterSource.seq, message.seq])
expect(response.value.hasMore).toBe(false)
await ctx.fiber.dispose()
})
it('keeps compact reasoning and tool-call runs nested in one attempt event', async () => {
const { ctx } = await harness()
const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } })
const reasoning = [0, 1, 2].map(index => session.append('assistant/chunk', {
turn: 1,
step: 1,
chunk: { type: 'reasoning-delta', index: 0, text: `r${String(index)}` },
}))
const callId = ToolCallId('packed-call')
const toolCall = [0, 1, 2].map(index => session.append('assistant/chunk', {
const attempt = session.append('assistant/attempt', {
turn: 1,
step: 1,
chunk: { type: 'tool-call-delta', index: 1, id: callId, argumentsDelta: `a${String(index)}` },
}))
stream: [
{ type: 'reasoning-chunks', time0: 1, index: 0, dt: [1, 1], texts: ['r0', 'r1', 'r2'] },
{ type: 'tool-call-chunks', time0: 4, index: 1, id: callId, dt: [1, 1], args: ['a0', 'a1', 'a2'] },
],
})
const response = await remote.page({
address: { kind: 'session', sessionId: session.id },
throughSeq: session.seq - 1,
})
if (!response.ok) throw new Error('unreachable')
expect(response.value.records).toEqual([
{
type: 'chunks',
event: {
type: 'chunkrow/reasoning-chunks',
seq: reasoning[0]?.seq,
time: reasoning[0]?.time,
data: {
turn: 1,
step: 1,
index: 0,
dt: reasoning.slice(1).map((event, index) => event.time - (reasoning[index]?.time ?? 0)),
texts: ['r0', 'r1', 'r2'],
},
},
},
{
type: 'chunks',
event: {
type: 'chunkrow/tool-call-chunks',
seq: toolCall[0]?.seq,
time: toolCall[0]?.time,
data: {
turn: 1,
step: 1,
index: 1,
id: callId,
dt: toolCall.slice(1).map((event, index) => event.time - (toolCall[index]?.time ?? 0)),
args: ['a0', 'a1', 'a2'],
},
},
},
])
expect(response.value.records).toEqual([{ type: 'event', event: attempt }])
await ctx.fiber.dispose()
})
@@ -154,14 +154,14 @@ describe('session.history projections block', () => {
const snapshot = await opening(remote(ctx), child.id)
expect(snapshot.header).toEqual({
version: 1,
version: 2,
id: child.id,
createdAt: child.header.createdAt,
cwd: '/workspace',
parentSession: parent.id,
seedLength: inheritedEventCount,
isSeeded: true,
})
expect(snapshot.header).not.toHaveProperty('isSeeded')
expect(snapshot.header).not.toHaveProperty('seedLength')
})
it('tracks pending and used model selections across repeated request headers', async () => {
@@ -99,29 +99,6 @@ describe('Session open', () => {
expect(api.followStarts).toHaveLength(2)
})
it('lands a packed live record in openState=error instead of crashing the stream loop', async () => {
const { api, session } = makeSession()
api.onHistory = () => histResponse(plainTurn(SessionSeq(0), 0, 'a', 'b'))
await session.open()
expect(session.getSnapshot().openState).toBe('open')
// The live tail may carry only events; a packed record breaks that contract.
await api.pushFollow(SID, {
type: 'chunks',
event: {
type: 'chunkrow/text-chunks',
seq: 6,
time: 6,
data: { turn: 1, step: 1, index: 0, texts: ['a'], dt: [] },
},
} as never)
await vi.waitFor(() => { expect(session.getSnapshot().openState).toBe('error') })
expect(session.getSnapshot().openError).toMatchObject({
code: 'gateway/internal', message: 'session live stream emitted a packed history record',
})
})
it('lands a Gateway-marked stream failure in openState=error', async () => {
const { api, session } = makeSession()
api.onHistory = () => Promise.reject(new Error('socket died'))
@@ -134,7 +134,7 @@ describe('search', () => {
})
describe('scope tree', () => {
it('publishes live assistant chunks and durable settlement atomically through one event source', async () => {
it('publishes transient Assistant chunks and the named durable v2 settlement through one event source', async () => {
const b = bench()
await feedList(b, [{ id: 's1' }])
b.svc.open(sid('s1'))
@@ -144,17 +144,10 @@ describe('scope tree', () => {
expect(binding.session.getSnapshot().openState).toBe('open')
})
const attemptId = LlmAttemptId('web-live-attempt')
const durableChunk = {
type: 'event' as const,
event: {
type: 'assistant/chunk', seq: 0, time: 1,
data: { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'live' } },
},
}
const durableMessage = {
type: 'event' as const,
event: {
type: 'assistant/message', seq: 1, time: 2,
type: 'assistant/message', seq: 0, time: 2,
data: {
turn: 1,
step: 1,
@@ -164,8 +157,8 @@ describe('scope tree', () => {
source: { kind: 'model', provider: 'p', model: 'm' },
id: 'message-1',
},
stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: [], texts: ['live'] }],
},
sourceEventSeqs: [0],
surfaceOp: 'append' as const,
},
}
@@ -181,16 +174,12 @@ describe('scope tree', () => {
turn: 1, step: 1,
},
})
await b.api.pushFollow(sid('s1'), durableChunk)
await Promise.resolve()
expect(binding.eventSource.getSnapshot().entries).toEqual([])
await b.api.pushFollow(sid('s1'), {
type: 'assistant-stream',
frame: {
type: 'chunk', attemptId, revision: 2, index: 0,
chunk: durableChunk.event.data.chunk,
legacyChunkSeq: 0,
time: 1,
chunk: { type: 'text-delta', index: 0, text: 'live' },
},
})
await vi.waitFor(() => {
@@ -204,8 +193,7 @@ describe('scope tree', () => {
type: 'assistant-stream',
frame: {
type: 'end', attemptId, revision: 3, index: 1,
outcome: 'committed',
legacyChunkSeqs: [0],
outcome: { kind: 'committed', eventType: 'assistant/message', seq: 0 },
},
})
await vi.waitFor(() => {
@@ -213,8 +201,8 @@ describe('scope tree', () => {
})
expect(publications).toEqual([
['assistant/chunk'],
['assistant/chunk', 'assistant/message'],
['assistant/live-chunk'],
['assistant/live-chunk', 'assistant/message'],
])
dispose()
})
@@ -222,28 +210,15 @@ describe('scope tree', () => {
it('replaces an active assistant baseline on reconnect without duplicate chunks', async () => {
const b = bench()
const attemptId = LlmAttemptId('reconnect-attempt')
const first = {
type: 'event' as const,
event: {
type: 'assistant/chunk', seq: 0, time: 1,
data: { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'a' } },
},
}
const second = {
type: 'event' as const,
event: {
type: 'assistant/chunk', seq: 1, time: 2,
data: { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'b' } },
},
}
let records = [first] as never[]
let records: never[] = []
b.api.onHistory = () => Promise.resolve(ok({ records, hasMore: false }))
b.api.assistantStreamBaseline = {
revision: 2,
attempts: [{
activeAttempt: {
attemptId, startedTime: 1, turn: 1, step: 1,
chunks: [first.event.data.chunk], legacyChunkSeqs: [0],
}],
nextIndex: 1,
stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: [], texts: ['a'] }],
},
}
await feedList(b, [{ id: 's1' }])
b.svc.open(sid('s1'))
@@ -253,14 +228,14 @@ describe('scope tree', () => {
expect(binding.eventSource.getSnapshot().entries).toHaveLength(1)
})
records = [first, second] as never[]
records = []
b.api.assistantStreamBaseline = {
revision: 3,
attempts: [{
activeAttempt: {
attemptId, startedTime: 1, turn: 1, step: 1,
chunks: [first.event.data.chunk, second.event.data.chunk],
legacyChunkSeqs: [0, 1],
}],
nextIndex: 2,
stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: [1], texts: ['a', 'b'] }],
},
}
b.api.failStreams(new RemoteStreamCarrierError('lost'))
await vi.waitFor(() => {
@@ -268,57 +243,20 @@ describe('scope tree', () => {
expect(binding.eventSource.getSnapshot().entries).toHaveLength(2)
})
expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.seq)).toEqual([0, 1])
})
it('publishes a baseline-framed chunk when its durable event follows the opening cut', async () => {
const b = bench()
const attemptId = LlmAttemptId('opening-cut-attempt')
const chunk = {
type: 'event' as const,
event: {
type: 'assistant/chunk', seq: 0, time: 1,
data: { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'late durable' } },
},
}
b.api.onHistory = () => Promise.resolve(ok({ records: [], hasMore: false }))
b.api.followCursor = -1
b.api.assistantStreamBaseline = {
revision: 2,
attempts: [{
attemptId, startedTime: 1, turn: 1, step: 1,
chunks: [chunk.event.data.chunk], legacyChunkSeqs: [0],
}],
}
await feedList(b, [{ id: 's1' }])
b.svc.open(sid('s1'))
const binding = b.svc.binding(sid('s1'))
if (binding === undefined) throw new Error('expected Session binding')
await vi.waitFor(() => {
expect(binding.session.getSnapshot().openState).toBe('open')
})
await b.api.pushFollow(sid('s1'), chunk)
await vi.waitFor(() => {
expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.seq)).toEqual([0])
})
expect(b.api.followStarts.filter(id => id === sid('s1'))).toHaveLength(1)
expect(binding.eventSource.getSnapshot().entries.map(entry => (
entry.event.type === 'assistant/live-chunk' && entry.event.data.chunk.type === 'text-delta'
? entry.event.data.chunk.text
: undefined
))).toEqual(['a', 'b'])
})
it('stages a reconnect-tail assistant settlement behind its exact active attempt', async () => {
const b = bench()
const attemptId = LlmAttemptId('reconnect-settlement-attempt')
const priorChunk = {
type: 'event' as const,
event: {
type: 'assistant/chunk', seq: 0, time: 10,
data: { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'retry ' } },
},
}
const priorMessage = {
type: 'event' as const,
event: {
type: 'assistant/message', seq: 1, time: 11,
type: 'assistant/message', seq: 0, time: 11,
data: {
turn: 1,
step: 1,
@@ -328,22 +266,15 @@ describe('scope tree', () => {
source: { kind: 'model', provider: 'p', model: 'm' },
id: 'prior-attempt-message',
},
stream: [{ type: 'text-chunks', time0: 10, index: 0, dt: [], texts: ['retry '] }],
},
sourceEventSeqs: [0],
surfaceOp: 'append' as const,
},
}
const currentChunk = {
type: 'event' as const,
event: {
type: 'assistant/chunk', seq: 2, time: 20,
data: { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'settled' } },
},
}
const currentMessage = {
type: 'event' as const,
event: {
type: 'assistant/message', seq: 3, time: 21,
type: 'assistant/message', seq: 1, time: 21,
data: {
turn: 1,
step: 1,
@@ -353,25 +284,25 @@ describe('scope tree', () => {
source: { kind: 'model', provider: 'p', model: 'm' },
id: 'current-attempt-message',
},
stream: [{ type: 'text-chunks', time0: 20, index: 0, dt: [], texts: ['settled'] }],
},
sourceEventSeqs: [2],
surfaceOp: 'append' as const,
},
}
b.api.onHistory = () => Promise.resolve(ok({
records: [priorChunk, priorMessage, currentChunk] as never[],
records: [priorMessage, currentMessage] as never[],
hasMore: false,
}))
b.api.assistantStreamBaseline = {
revision: 2,
attempts: [{
activeAttempt: {
attemptId,
startedTime: 20,
turn: 1,
step: 1,
chunks: [currentChunk.event.data.chunk],
legacyChunkSeqs: [2],
}],
nextIndex: 1,
stream: currentMessage.event.data.stream,
},
}
await feedList(b, [{ id: 's1' }])
b.svc.open(sid('s1'))
@@ -381,39 +312,29 @@ describe('scope tree', () => {
expect(binding.session.getSnapshot().openState).toBe('open')
})
expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.seq)).toEqual([0, 1, 2])
expect(binding.eventSource.getSnapshot().entries.at(1)?.event).toBe(priorMessage.event)
expect(binding.eventSource.getSnapshot().entries.at(-1)?.event).toBe(currentChunk.event)
await b.api.pushFollow(sid('s1'), currentMessage)
await Promise.resolve()
expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.seq)).toEqual([0, 1, 2])
expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.type))
.toEqual(['assistant/message', 'assistant/live-chunk'])
expect(binding.eventSource.getSnapshot().entries[0]?.event).toBe(priorMessage.event)
await b.api.pushFollow(sid('s1'), {
type: 'assistant-stream',
frame: {
type: 'end', attemptId, revision: 3, index: 1,
outcome: 'committed', legacyChunkSeqs: [2],
outcome: { kind: 'committed', eventType: 'assistant/message', seq: 1 },
},
})
await vi.waitFor(() => {
expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.seq)).toEqual([0, 1, 2, 3])
expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.type))
.toEqual(['assistant/message', 'assistant/live-chunk', 'assistant/message'])
})
expect(binding.eventSource.getSnapshot().change).toEqual({
kind: 'append', entries: [currentMessage],
})
})
it('replaces an invalid settlement with the authoritative post-end baseline', async () => {
it('rebaselines a reconnect settlement whose end index skips the active tail', async () => {
const b = bench()
const attemptId = LlmAttemptId('reconnect-end-index-attempt')
const chunk = {
type: 'event' as const,
event: {
type: 'assistant/chunk', seq: 0, time: 20,
data: { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'settled' } },
},
}
const message = {
type: 'event' as const,
event: {
@@ -427,51 +348,50 @@ describe('scope tree', () => {
source: { kind: 'model', provider: 'p', model: 'm' },
id: 'current-attempt-message',
},
stream: [{ type: 'text-chunks', time0: 20, index: 0, dt: [], texts: ['settled'] }],
},
sourceEventSeqs: [0],
surfaceOp: 'append' as const,
},
}
let records = [chunk] as never[]
b.api.onHistory = () => Promise.resolve(ok({
records,
records: [message] as never[],
hasMore: false,
}))
b.api.assistantStreamBaseline = {
revision: 2,
attempts: [{
activeAttempt: {
attemptId,
startedTime: 20,
turn: 1,
step: 1,
chunks: [chunk.event.data.chunk],
legacyChunkSeqs: [0],
}],
nextIndex: 1,
stream: message.event.data.stream,
},
}
await feedList(b, [{ id: 's1' }])
b.svc.open(sid('s1'))
const binding = b.svc.binding(sid('s1'))
if (binding === undefined) throw new Error('expected Session binding')
await vi.waitFor(() => {
expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.seq)).toEqual([0])
expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.type))
.toEqual(['assistant/live-chunk'])
})
const openingRevision = binding.eventSource.getSnapshot().revision
await b.api.pushFollow(sid('s1'), message)
await Promise.resolve()
expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.seq)).toEqual([0])
records = [chunk, message] as never[]
b.api.assistantStreamBaseline = { revision: 3, attempts: [] }
await b.api.pushFollow(sid('s1'), {
type: 'assistant-stream',
frame: {
type: 'end', attemptId, revision: 3, index: 0,
outcome: 'committed', legacyChunkSeqs: [0],
outcome: { kind: 'committed', eventType: 'assistant/message', seq: 0 },
},
})
await vi.waitFor(() => {
expect(b.api.followStarts.filter(id => id === sid('s1'))).toHaveLength(2)
expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.seq)).toEqual([0, 1])
expect(b.api.activeFollows(sid('s1'))).toBe(1)
expect(binding.eventSource.getSnapshot().revision).toBeGreaterThan(openingRevision)
})
expect(binding.eventSource.getSnapshot().entries.map(entry => entry.event.type))
.toEqual(['assistant/live-chunk'])
})
it('retains a Host-addressed scope until the first Session baseline owns pruning', async () => {
@@ -610,17 +530,18 @@ describe('Agent scope disposal lifecycle', () => {
value: {
type: 'snapshot',
header: {
version: 1,
version: 2,
id: request.address.kind === 'session'
? request.address.sessionId
: request.address.childSessionId,
createdAt: 0,
isSeeded: false,
},
cursor: -1,
records: [],
hasMore: false,
projections: { asOfSeq: -1, values: {} },
assistantStream: { revision: 0, attempts: [] },
assistantStream: { revision: 0 },
} as const,
})
}
@@ -685,12 +606,12 @@ describe('Agent scope disposal lifecycle', () => {
done: false,
value: {
type: 'snapshot',
header: { version: 1, id: sessionId, createdAt: 0 },
header: { version: 2, id: sessionId, createdAt: 0, isSeeded: false },
cursor: -1,
records: [],
hasMore: false,
projections: { asOfSeq: -1, values: {} },
assistantStream: { revision: 0, attempts: [] },
assistantStream: { revision: 0 },
} as const,
})
}
@@ -1,6 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import {
isRemoteFailure,
RemoteStream,
RemoteStreamCarrierError,
type RemoteStreamOptions,
@@ -42,18 +41,6 @@ function entry(seq: number): SessionEventEntry {
return { type: 'event', event: { type: 'turn/start', seq, time: seq, data: { turn: seq } } }
}
function chunks(seq0: number): SessionHistoryRecord {
return {
type: 'chunks',
event: {
type: 'chunkrow/text-chunks',
seq: seq0,
time: seq0,
data: { turn: 1, step: 1, index: 0, texts: ['a', 'b', 'c'], dt: [1, 1] },
},
}
}
function page(records: readonly SessionHistoryRecord[], hasMore = false): SessionPage {
return { records, hasMore }
}
@@ -62,14 +49,15 @@ function snapshot(
cursor: number,
records: readonly SessionHistoryRecord[],
hasMore = false,
assistantStream: SessionAssistantStreamBaseline = { revision: 0, attempts: [] },
assistantStream: SessionAssistantStreamBaseline = { revision: 0 },
): SessionFollowFrame {
return {
type: 'snapshot',
header: {
version: 1,
version: 2,
id: ADDRESS.kind === 'session' ? ADDRESS.sessionId : ADDRESS.childSessionId,
createdAt: 0,
isSeeded: false,
},
cursor,
records,
@@ -154,18 +142,18 @@ describe('Session Client stream adapters', () => {
const attemptId = LlmAttemptId('transport-attempt')
const baseline: SessionAssistantStreamBaseline = {
revision: 2,
attempts: [{
activeAttempt: {
attemptId,
startedTime: 1,
turn: 1,
step: 1,
chunks: [{ type: 'text-delta', index: 0, text: 'a' }],
legacyChunkSeqs: [0],
}],
nextIndex: 1,
stream: [{ type: 'text-chunks', time0: 0, index: 0, dt: [], texts: ['a'] }],
},
}
const frame: SessionAssistantStreamFrame = {
type: 'chunk', attemptId, revision: 3, index: 1,
chunk: { type: 'text-delta', index: 0, text: 'b' }, legacyChunkSeq: 1,
time: 1, chunk: { type: 'text-delta', index: 0, text: 'b' },
}
const remote = new ScriptedSessionRemote(
[{ frames: [snapshot(0, [entry(0)], false, baseline), assistantFrame(frame)], hold: true }],
@@ -193,9 +181,10 @@ describe('Session Client stream adapters', () => {
frames: [{
type: 'snapshot',
header: {
version: 1,
version: 2,
id: ADDRESS.sessionId,
createdAt: 0,
isSeeded: false,
},
cursor: -1,
records: [],
@@ -249,19 +238,18 @@ describe('Session Client stream adapters', () => {
}
const gap: SessionAssistantStreamFrame = {
type: 'chunk', attemptId, revision: 3, index: 0,
chunk: { type: 'text-delta', index: 0, text: 'lost predecessor' },
legacyChunkSeq: 1,
time: 1, chunk: { type: 'text-delta', index: 0, text: 'lost predecessor' },
}
const replacement: SessionAssistantStreamBaseline = {
revision: 3,
attempts: [{
activeAttempt: {
attemptId,
startedTime: 1,
turn: 1,
step: 1,
chunks: [gap.chunk],
legacyChunkSeqs: [1],
}],
nextIndex: 1,
stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: [], texts: ['lost predecessor'] }],
},
}
const remote = new ScriptedSessionRemote([
{
@@ -297,14 +285,14 @@ describe('Session Client stream adapters', () => {
const attemptId = LlmAttemptId('replacement-lifecycle-attempt')
const previous: SessionAssistantStreamBaseline = {
revision: 2,
attempts: [{
activeAttempt: {
attemptId,
startedTime: 1,
turn: 1,
step: 1,
chunks: [{ type: 'text-delta', index: 0, text: 'old' }],
legacyChunkSeqs: [0],
}],
nextIndex: 1,
stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: [], texts: ['old'] }],
},
}
const replacementStart: SessionAssistantStreamFrame = {
type: 'start', attemptId, revision: 1, startedTime: 2,
@@ -312,14 +300,14 @@ describe('Session Client stream adapters', () => {
}
const replacement: SessionAssistantStreamBaseline = {
revision: 1,
attempts: [{
activeAttempt: {
attemptId,
startedTime: 2,
turn: 2,
step: 1,
chunks: [],
legacyChunkSeqs: [],
}],
nextIndex: 0,
stream: [],
},
}
const remote = new ScriptedSessionRemote([
{
@@ -350,10 +338,9 @@ describe('Session Client stream adapters', () => {
}
})
it('validates a packed logical range before publishing one compact Client entry', async () => {
const row = chunks(1)
it('validates one scalar current-event range before publishing Client entries', async () => {
const remote = new ScriptedSessionRemote(
[{ frames: [snapshot(4, [entry(0), row, entry(4)]), entry(5)], hold: true }],
[{ frames: [snapshot(2, [entry(0), entry(1), entry(2)]), entry(3)], hold: true }],
[],
)
const changes: SessionJournalChange[] = []
@@ -369,34 +356,11 @@ describe('Session Client stream adapters', () => {
type: 'replace',
entries: [
entry(0),
row,
entry(4),
entry(1),
entry(2),
],
})
expect(changes[0]?.type === 'replace' ? changes[0].entries[1] : undefined).toBe(row)
expect(changes[1]).toEqual({ type: 'append', entry: entry(5) })
await stream.dispose()
})
it('rejects a packed record emitted by the live follow path', async () => {
const failed = vi.fn()
const remote = new ScriptedSessionRemote(
[{ frames: [snapshot(-1, []), chunks(0) as SessionFollowFrame], hold: true }],
[],
)
const stream = new SessionEventStream(sessionClient(remote), ADDRESS, {
publish: vi.fn(),
failed,
})
await stream.open({})
await vi.waitFor(() => { expect(failed).toHaveBeenCalledOnce() })
const violation: unknown = failed.mock.calls[0]?.[0]
expect(isRemoteFailure(violation)).toBe(true)
expect(violation).toMatchObject({
code: 'gateway/internal',
message: 'session live stream emitted a packed history record',
})
expect(changes[1]).toEqual({ type: 'append', entry: entry(3) })
await stream.dispose()
})