perf(history): carry packed assistant chunks

This commit is contained in:
kingwl
2026-08-25 20:10:42 +08:00
committed by imccyu
parent e2a10b141e
commit f2ca913756
32 changed files with 535 additions and 101 deletions
@@ -0,0 +1,99 @@
/** Compact client folding for packed Assistant delta runs in history responses. */
import type { ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows'
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
import type {
SessionEventEntry,
SessionHistoryRecord,
} from '../../types.ts'
/** Resolve one packed member's original timestamp from the row's delta gaps. */
function memberTime(row: ChunkRow, index: number): number {
let time = row.time0
for (let cursor = 0; cursor < index; cursor++) time += row.data.dt[cursor] as number
return time
}
/** Build one coalesced text or reasoning event from a contiguous member slice. */
function textEvent(
row: Extract<ChunkRow, { type: 'text-chunks' | 'reasoning-chunks' }>,
start: number,
text: string,
): SessionEvent<'assistant/chunk'> {
return {
type: 'assistant/chunk',
seq: row.seq0 + start,
time: memberTime(row, start),
data: {
turn: row.data.turn,
step: row.data.step,
chunk: row.type === 'text-chunks'
? { type: 'text-delta', index: row.data.index, text }
: { type: 'reasoning-delta', index: row.data.index, text },
},
}
}
/**
* Coalesce one packed run into the smallest event set that preserves the
* conversation fold's accumulated content, first-token time, and first
* non-whitespace visibility boundary. Exact token boundaries remain available
* in the wire row to consumers that explicitly decode it.
* @param row - one validated packed history record.
* @returns At most two Assistant chunk events for the ordinary UI fold.
*/
export function coalesceHistoryChunkRun(row: ChunkRow): SessionEvent<'assistant/chunk'>[] {
if (row.type === 'tool-call-chunks') {
const firstToken = row.data.name === undefined
? row.data.args.findIndex(fragment => fragment !== '')
: 0
const start = firstToken < 0 ? 0 : firstToken
return [{
type: 'assistant/chunk',
seq: row.seq0 + start,
time: memberTime(row, start),
data: {
turn: row.data.turn,
step: row.data.step,
chunk: {
type: 'tool-call-delta',
index: row.data.index,
id: row.data.id,
...row.data.name === undefined ? {} : { name: row.data.name },
argumentsDelta: row.data.args.join(''),
},
},
}]
}
const texts = row.data.texts
const firstToken = texts.findIndex(text => text !== '')
const tokenStart = firstToken < 0 ? 0 : firstToken
let visibleStart = -1
let accumulated = ''
for (let index = 0; index < texts.length; index++) {
accumulated += texts[index] as string
if (accumulated.trim() !== '') {
visibleStart = index
break
}
}
if (visibleStart > tokenStart) {
return [
textEvent(row, tokenStart, texts.slice(0, visibleStart).join('')),
textEvent(row, visibleStart, texts.slice(visibleStart).join('')),
]
}
return [textEvent(row, tokenStart, texts.join(''))]
}
/**
* Convert history wire records into compact event inputs for the ordinary UI.
* @param records - validated lossless history transport records.
* @returns Ordinary entries unchanged and packed runs coalesced for folding.
*/
export function historyEntries(records: readonly SessionHistoryRecord[]): SessionEventEntry[] {
return records.flatMap(record => 'event' in record
? [record]
: coalesceHistoryChunkRun(record.chunks).map(event => ({ event })))
}
@@ -19,6 +19,7 @@ import type {
SessionPageRequest,
SessionProjectionBaseline,
} from '../types.ts'
import { historyEntries } from './sessions/history-records.ts'
export {
SESSION_SEARCH_RESULT_LIMIT,
@@ -117,7 +118,7 @@ export class SessionEventStream extends RemoteJournalStream<
super(remote, {
name: 'session event stream',
emptyCursor: -1,
entries: page => page.events,
entries: page => historyEntries(page.records),
hasMore: page => page.hasMore,
cursor: entry => entry.event.seq,
compare: (left, right) => left - right,
@@ -144,7 +145,7 @@ export class SessionEventStream extends RemoteJournalStream<
type: 'opened',
cursor: frame.cursor,
page: {
events: frame.events,
records: frame.records,
hasMore: frame.hasMore,
projections: frame.projections,
},
+12 -3
View File
@@ -2,6 +2,7 @@
import type { Context } from '@deepseek-ai/cordis'
import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-session'
import { isChunkRow, packChunkRuns } from '@deepseek-ai/dsh-session/chunk-rows'
import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
import type {} from '@deepseek-ai/dsh-subagent'
@@ -11,6 +12,7 @@ import type {
SessionEventEntry,
SessionFollowRequest,
SessionFollowFrame,
SessionHistoryRecord,
SessionPage,
SessionPageRequest,
SessionProjectionBaseline,
@@ -68,9 +70,9 @@ export class SessionHistoryController {
request.maxMessages ?? DEFAULT_MAX_MESSAGES,
request.throughSeq,
)
const entries = page.events.map(entryFor)
const records = pageRecords(page.events)
return {
events: entries,
records,
hasMore: page.hasMore,
}
}
@@ -128,7 +130,7 @@ export class SessionHistoryController {
type: 'snapshot',
header: source.header,
cursor,
events: page.events.map(entryFor),
records: pageRecords(page.events),
hasMore: page.hasMore,
projections: source.projections === undefined
? { asOfSeq: cursor, values: {} }
@@ -317,3 +319,10 @@ function entryFor(event: SessionEvent): SessionEventEntry {
event: event as unknown as SessionWireEvent,
}
}
/** Encode one bounded logical page without changing its pagination cut. */
function pageRecords(events: readonly SessionEvent[]): SessionHistoryRecord[] {
return packChunkRuns(events).map(record => isChunkRow(record)
? { chunks: record }
: entryFor(record))
}
+11 -2
View File
@@ -6,6 +6,7 @@ import type {
import type { Branded } from '@deepseek-ai/dsh-brand'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows'
import type { JsonValue, SessionHeader, SessionId, SurfaceOp } from '@deepseek-ai/dsh-session/types'
import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types'
import type { JobId } from '@deepseek-ai/dsh-jobs/brand'
@@ -364,6 +365,14 @@ export interface SessionEventEntry {
readonly event: SessionWireEvent
}
/** One lossless run of consecutive Assistant delta events in a history page. */
export interface SessionChunkRun {
readonly chunks: ChunkRow
}
/** One history-page record: a raw event or a packed Assistant delta run. */
export type SessionHistoryRecord = SessionEventEntry | SessionChunkRun
/** Session event wire form; durable readers own recognition of merge-extensible event names. */
export interface SessionWireEvent {
readonly type: string
@@ -392,7 +401,7 @@ export interface SessionFollowRequest {
/** One contiguous backwards page of a Session log. */
export interface SessionPage {
readonly events: readonly SessionEventEntry[]
readonly records: readonly SessionHistoryRecord[]
readonly hasMore: boolean
}
@@ -402,7 +411,7 @@ export type SessionFollowFrame =
readonly type: 'snapshot'
readonly header: SessionHeader
readonly cursor: number
readonly events: readonly SessionEventEntry[]
readonly records: readonly SessionHistoryRecord[]
readonly hasMore: boolean
readonly projections: SessionProjectionBaseline
}
@@ -146,3 +146,14 @@ export function plainTurn(startSeq: number, turn: number, ask: string, answer: s
export function entries(events: readonly SessionEvent[]): { event: SessionEvent }[] {
return events.map(event => ({ event }))
}
/** Build one view-less history response value. */
export function historyValue(events: readonly SessionEvent[], hasMore = false): {
records: { event: SessionEvent }[]
hasMore: boolean
} {
return {
records: entries(events),
hasMore,
}
}
@@ -28,6 +28,7 @@ import {
} from '@deepseek-ai/dsh-api-gateway/client'
import { RpcId } from '@deepseek-ai/dsh-client-connection/client'
import type { SessionRemotes } from '../src/client/sessions/remotes.ts'
import { historyEntries } from '../src/client/sessions/history-records.ts'
const AVAILABLE_STREAM_CONNECTION = {
hostDescription: {
@@ -137,7 +138,7 @@ export class FakeApiClient implements IApiClient {
onFork: (payload: unknown) => Promise<RpcResponse<{ sessionId: SessionId }>> = () => Promise.resolve(ok({ sessionId: 'fk-fork' as SessionId }))
onHistory: (payload: { sessionId: SessionId; throughSeq?: number; beforeSeq?: number; maxMessages?: number })
=> Promise<RpcResponse<SessionPage & { readonly projections?: SessionProjectionBaseline }>> =
() => Promise.resolve(ok({ events: [], hasMore: false }))
() => Promise.resolve(ok({ records: [], hasMore: false }))
onPrompt: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
onAttachment: (payload: unknown) => Promise<RpcResponse<{ attachment: { attachmentId: never; mediaType: 'image/png'; bytes: number; width: number; height: number }; data: string }>> =
@@ -439,7 +440,8 @@ export class FakeApiClient implements IApiClient {
ok: true,
value: {
...result.value,
events: result.value.events.filter(entry => entry.event.seq <= request.throughSeq),
records: historyEntries(result.value.records)
.filter(entry => entry.event.seq <= request.throughSeq),
},
}
}
@@ -467,7 +469,8 @@ export class FakeApiClient implements IApiClient {
)
}
const page = response.result.value
const cursor = this.followCursor ?? page.events.at(-1)?.event.seq ?? -1
const entries = historyEntries(page.records)
const cursor = this.followCursor ?? entries.at(-1)?.event.seq ?? -1
yield {
type: 'snapshot',
header: {
@@ -479,7 +482,7 @@ export class FakeApiClient implements IApiClient {
: {}),
},
cursor,
events: page.events.filter(entry => entry.event.seq <= cursor),
records: entries.filter(entry => entry.event.seq <= cursor),
hasMore: page.hasMore,
projections: page.projections ?? { asOfSeq: cursor, values: {} },
}
@@ -0,0 +1,110 @@
/** Packed history record folding without token-by-token browser expansion. */
import { describe, expect, it } from 'vitest'
import { CallId } from '@deepseek-ai/dsh-llm/brand'
import type { ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows'
import type { SessionHistoryRecord } from '../src/types.ts'
import { coalesceHistoryChunkRun, historyEntries } from '../src/client/sessions/history-records.ts'
describe('coalesceHistoryChunkRun', () => {
it('preserves first-token and first-visible boundaries with at most two text events', () => {
const row: ChunkRow = {
type: 'text-chunks',
seq0: 10,
time0: 100,
data: {
turn: 2,
step: 3,
index: 0,
dt: [1, 2, 3, 4],
texts: ['', ' ', '', 'hello', ' world'],
},
}
const events = coalesceHistoryChunkRun(row)
expect(events).toHaveLength(2)
expect(events.map(event => ({ seq: event.seq, time: event.time, text: event.data.chunk.type === 'text-delta' ? event.data.chunk.text : '' })))
.toEqual([
{ seq: 11, time: 101, text: ' ' },
{ seq: 13, time: 106, text: 'hello world' },
])
expect(events[0]?.time).toBe(101)
expect(events.find(event => event.data.chunk.type === 'text-delta' && event.data.chunk.text.trim() !== '')?.time)
.toBe(106)
})
it('joins visible reasoning members into one event at the first non-empty member', () => {
const row: ChunkRow = {
type: 'reasoning-chunks',
seq0: 4,
time0: 50,
data: { turn: 1, step: 1, index: 2, dt: [5, 7], texts: ['', 'a', 'b'] },
}
const [event] = coalesceHistoryChunkRun(row)
expect(event).toMatchObject({
seq: 5,
time: 55,
data: { chunk: { type: 'reasoning-delta', index: 2, text: 'ab' } },
})
})
it('joins tool arguments while retaining name presence and first-token time', () => {
const named: ChunkRow = {
type: 'tool-call-chunks',
seq0: 20,
time0: 200,
data: {
turn: 2,
step: 4,
index: 1,
id: CallId('call-1'),
name: 'write',
dt: [2, 3],
args: ['', '{"x":', '1}'],
},
}
expect(coalesceHistoryChunkRun(named)).toMatchObject([{
seq: 20,
time: 200,
data: { chunk: { type: 'tool-call-delta', name: 'write', argumentsDelta: '{"x":1}' } },
}])
const unnamed: ChunkRow = {
type: 'tool-call-chunks',
seq0: 20,
time0: 200,
data: {
turn: 2,
step: 4,
index: 1,
id: CallId('call-1'),
dt: [2, 3],
args: ['', '', 'x'],
},
}
const [event] = coalesceHistoryChunkRun(unnamed)
expect(event).toMatchObject({ seq: 22, time: 205, data: { chunk: { argumentsDelta: 'x' } } })
expect(Object.hasOwn(event?.data.chunk ?? {}, 'name')).toBe(false)
})
})
describe('historyEntries', () => {
it('keeps ordinary entries and views while folding a packed run without expansion', () => {
const ordinary = {
event: { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
view: { for: 'call', view: { card: 'generic' } },
} as unknown as SessionHistoryRecord
const packed: SessionHistoryRecord = {
chunks: {
type: 'text-chunks',
seq0: 1,
time0: 2,
data: { turn: 1, step: 1, index: 0, dt: [1, 1, 1], texts: ['a', 'b', 'c', 'd'] },
},
}
const entries = historyEntries([ordinary, packed])
expect(entries).toHaveLength(2)
expect(entries[0]).toBe(ordinary)
expect(entries[1]?.event).toMatchObject({ seq: 1, data: { chunk: { text: 'abcd' } } })
})
})
@@ -759,7 +759,7 @@ describe('connected generation', () => {
it('refreshes query baselines without rebuilding independently resumed Session sources', async () => {
const api = new FakeApiClient()
api.onHistory = () => Promise.resolve(ok({
events: entries(plainTurn(0, 0, 'a', 'b')) as never[],
records: entries(plainTurn(0, 0, 'a', 'b')) as never[],
hasMore: false,
modelSelection: { provider: 'deepseek-official', model: 'deepseek-chat' },
}))
@@ -105,7 +105,7 @@ describe('Session tail-page seeding', () => {
const api = new FakeApiClient()
const session = new Session(SID, api, fakeRemote(api))
api.onHistory = () => Promise.resolve(ok({
events: entries(plainTurn(0, 0, '问', '答')) as never[], hasMore: false,
records: entries(plainTurn(0, 0, '问', '答')) as never[], hasMore: false,
projections: { asOfSeq: 5, values: { 'test/marks': { marks: ['from-baseline'] } } },
} as never))
await session.open()
@@ -116,7 +116,7 @@ describe('Session tail-page seeding', () => {
const api = new FakeApiClient()
const session = new Session(SID, api, fakeRemote(api))
api.onHistory = () => Promise.resolve(ok({
events: entries(plainTurn(0, 0, 'a', 'b')) as never[], hasMore: false,
records: entries(plainTurn(0, 0, 'a', 'b')) as never[], hasMore: false,
projections: { asOfSeq: 5, values: { 'test/marks': { marks: ['baseline'] } } },
} as never))
await session.open()
@@ -128,7 +128,7 @@ describe('Session tail-page seeding', () => {
it('treats a blockless response as no reset: pushed values survive', async () => {
const api = new FakeApiClient()
const session = new Session(SID, api, fakeRemote(api))
api.onHistory = () => Promise.resolve(ok({ events: entries(plainTurn(0, 0, 'a', 'b')) as never[], hasMore: false }))
api.onHistory = () => Promise.resolve(ok({ records: entries(plainTurn(0, 0, 'a', 'b')) as never[], hasMore: false }))
await session.open()
session.projections.apply('test/marks', { marks: ['pushed'] }, 9)
await session.resync()
@@ -390,7 +390,7 @@ describe('cold history recovery view', () => {
maxMessages: 10,
})
if (!history.ok) throw new Error('history failed')
expect(history.value.events.map(entry => entry.event)).toMatchInlineSnapshot(`
expect(history.value.records.map(record => 'event' in record ? record.event : record.chunks)).toMatchInlineSnapshot(`
[
{
"data": {
@@ -564,7 +564,8 @@ describe('subagent ownership fence', () => {
},
throughSeq: 3,
}, new AbortController().signal)
expect(history.events.map(entry => entry.event.type)).toEqual(events.map(event => event.type))
expect(history.records.map(record => 'event' in record ? record.event.type : record.chunks.type))
.toEqual(events.map(event => event.type))
expect(ctx.agents.get(sessionId)).toBeUndefined()
const prompt = await remote.prompt(promptRequest({
@@ -4,10 +4,15 @@ import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'
import { decodeStorageRecord } from '@deepseek-ai/dsh-session/chunk-rows'
import { CallId, 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 { SessionFollowFrame } 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. */
@@ -77,6 +82,13 @@ async function openFollow(
return { [Symbol.asyncIterator]: () => iterator }
}
/** Expand packed page records for assertions over the logical journal. */
function pageEvents(page: SessionPage): SessionWireEvent[] {
return page.records.flatMap(record => 'event' in record
? [record.event]
: decodeStorageRecord(record.chunks))
}
describe('Session history raw journal', () => {
it('follows raw tool events and preserves result metadata without a Tools service', async () => {
const { ctx } = await harness()
@@ -169,7 +181,7 @@ describe('Session history raw journal', () => {
})
expect(response.ok).toBe(true)
if (!response.ok) throw new Error('unreachable')
expect(response.value.events).toEqual([
expect(response.value.records).toEqual([
{ event: start },
{ event: call },
{ event: result },
@@ -210,7 +222,7 @@ describe('Session history raw journal', () => {
maxMessages: 2,
})
if (!response.ok) throw new Error('unreachable')
const page = response.value.events.map(entry => entry.event)
const page = pageEvents(response.value)
// Two append-origin messages fill the page even though a replacement copy of
// the same event type sits in the window: the copy is model-only.
const messages = page.filter(event => event.type === 'user/message' || event.type === 'assistant/message')
@@ -257,7 +269,8 @@ describe('Session history raw journal', () => {
maxMessages: 1,
})
if (!response.ok) throw new Error('unreachable')
expect(response.value.events.map(entry => entry.event.seq)).toEqual([...sources, message.seq])
expect(pageEvents(response.value).map(event => event.seq)).toEqual([...sources, message.seq])
expect(response.value.records.filter(record => 'chunks' in record)).toHaveLength(1)
expect(response.value.hasMore).toBe(true)
} finally {
min.mockRestore()
@@ -148,11 +148,12 @@ describe('session.history projections block', () => {
ctx.sessionProjections.register(lastUserUnit())
seedMessages(session, 3)
const snapshot = await opening(remote(ctx), session.id)
const { events, projections } = snapshot
const { records, projections } = snapshot
expect(projections.asOfSeq).toBe(session.seq - 1)
expect(projections.values['test/last-user']).toEqual({ text: 'm2' })
// asOfSeq IS the window tail: the last served event carries it.
expect(events.at(-1)?.event.seq).toBe(projections.asOfSeq)
const last = records.at(-1)
expect(last !== undefined && 'event' in last ? last.event.seq : last?.chunks.seq0).toBe(projections.asOfSeq)
})
it('returns a complete current replacement cut on each follow generation', async () => {
@@ -162,7 +163,7 @@ describe('session.history projections block', () => {
const snapshot = await opening(remote(ctx), session.id)
expect(snapshot.events.map(entry => entry.event.seq)).toEqual([0, 1])
expect(snapshot.records.map(record => 'event' in record ? record.event.seq : record.chunks.seq0)).toEqual([0, 1])
expect(snapshot.projections.asOfSeq).toBe(1)
expect(snapshot.projections.values).toEqual(
expect.objectContaining({ 'test/last-user': { text: 'm1' } }),
@@ -175,7 +176,7 @@ describe('session.history projections block', () => {
const snapshot = await opening(remote(ctx), session.id)
expect(snapshot.events).toEqual([])
expect(snapshot.records).toEqual([])
expect(snapshot.projections.asOfSeq).toBe(-1)
expect(snapshot.projections.values).toEqual(
expect.objectContaining({ 'test/last-user': null }),
@@ -6,7 +6,7 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
import { Session, type SessionOptions } from '../src/client/sessions/session.ts'
import { FakeApiClient, deferred, err, fakeRemote, ok } from './fake-api.client.ts'
import { entries, ev, plainTurn } from './event-script.client.ts'
import { entries, ev, historyValue, plainTurn } from './event-script.client.ts'
const SID = 'fk-s1' as SessionId
const PARENT = 'fk-parent' as SessionId
@@ -41,8 +41,7 @@ function eventSeqs(session: Session): number[] {
}
function histResponse(events: SessionEvent[], hasMore = false) {
// History returns raw journal envelopes around each event.
return Promise.resolve(ok({ events: entries(events) as never[], hasMore }))
return Promise.resolve(ok(historyValue(events, hasMore)))
}
describe('Session open', () => {
@@ -106,7 +105,7 @@ describe('Session open', () => {
follow(api, ev.user(16, '插进来的')),
]
gate.resolve(ok({
events: entries(page) as never[],
records: entries(page) as never[],
hasMore: false,
modelSelection: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
}))
@@ -227,7 +226,7 @@ describe('paging', () => {
const first = session.loadOlder()
const second = session.loadOlder()
gate.resolve(ok({
events: entries(plainTurn(0, 0, 'a', 'b')) as never[],
records: entries(plainTurn(0, 0, 'a', 'b')) as never[],
hasMore: false,
modelSelection: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
}))
@@ -571,7 +570,7 @@ describe('remaining branches', () => {
api.onHistory = () => histResponse(plainTurn(6, 1, '新', '代'))
const resynced = session.resync()
stale.resolve(ok({
events: entries(plainTurn(0, 0, '旧', '代')) as never[],
records: entries(plainTurn(0, 0, '旧', '代')) as never[],
hasMore: false,
modelSelection: { provider: 'deepseek-official', model: 'stale' },
})) // success, but its generation is gone
@@ -590,7 +589,7 @@ describe('remaining branches', () => {
api.onHistory = () => histResponse(plainTurn(6, 1, 'c', 'd'))
const resynced = session.resync() // bumps the generation
repairPull.resolve(ok({
events: entries(plainTurn(0, 0, '旧', '页')) as never[],
records: entries(plainTurn(0, 0, '旧', '页')) as never[],
hasMore: false,
modelSelection: { provider: 'deepseek-official', model: 'stale' },
})) // repair result: stale, dropped
@@ -617,7 +616,7 @@ describe('remaining branches', () => {
const historyCall = ev.toolCall(6, 1, 'h1', 'bash', '{"cmd":"pwd"}')
const historyResult = ev.toolResult(7, 1, 'h1', 'done')
api.onHistory = () => Promise.resolve(ok({
events: [
records: [
...entries(plainTurn(0, 0, 'a', 'b')),
{ event: historyCall },
{ event: historyResult },
@@ -669,7 +668,7 @@ describe('resync', () => {
])
expect(session.eventSource.getSnapshot()).toBe(oldWindow)
replacement.resolve(ok({
events: entries(plainTurn(10, 2, '终', '页')) as never[],
records: entries(plainTurn(10, 2, '终', '页')) as never[],
hasMore: false,
modelSelection: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
}))
@@ -274,7 +274,7 @@ describe('Agent scope disposal lifecycle', () => {
createdAt: 0,
},
cursor: -1,
events: [],
records: [],
hasMore: false,
projections: { asOfSeq: -1, values: {} },
} as const,
@@ -343,7 +343,7 @@ describe('Agent scope disposal lifecycle', () => {
type: 'snapshot',
header: { version: 0, id: sessionId, createdAt: 0 },
cursor: -1,
events: [],
records: [],
hasMore: false,
projections: { asOfSeq: -1, values: {} },
} as const,
@@ -40,7 +40,7 @@ function entry(seq: number): SessionEventEntry {
}
function page(events: readonly SessionEventEntry[], hasMore = false): SessionPage {
return { events, hasMore }
return { records: events, hasMore }
}
function snapshot(
@@ -56,7 +56,7 @@ function snapshot(
createdAt: 0,
},
cursor,
events,
records: events,
hasMore,
projections: { asOfSeq: cursor, values: {} },
}
@@ -87,7 +87,7 @@ describe('SessionHistoryController', () => {
{ address: { kind: 'session', sessionId: session.id }, throughSeq: 1 },
new AbortController().signal,
)
expect(page.events.map(entry => entry.event.seq)).toEqual([0, 1])
expect(page.records.map(entry => 'event' in entry ? entry.event.seq : entry.chunks.seq0)).toEqual([0, 1])
abort.abort()
expect(await iterator.next()).toMatchObject({ done: true })
@@ -137,7 +137,7 @@ describe('SessionHistoryController', () => {
value: {
type: 'snapshot',
cursor: 2,
events: [{ event: { seq: 0 } }, { event: { seq: 1 } }, { event: { seq: 2 } }],
records: [{ event: { seq: 0 } }, { event: { seq: 1 } }, { event: { seq: 2 } }],
},
})
session.append('turn/end', { turn: 2, reason: { kind: 'completed' } })
@@ -200,7 +200,7 @@ describe('SessionHistoryController', () => {
await expect(opening).resolves.toMatchObject({
done: false,
value: {
type: 'snapshot', cursor: 1, events: [{ event: { seq: 0 } }, { event: { seq: 1 } }],
type: 'snapshot', cursor: 1, records: [{ event: { seq: 0 } }, { event: { seq: 1 } }],
},
})
expect(attached.id).toBe(sessionId)
@@ -301,7 +301,7 @@ describe('SessionHistoryController', () => {
await expect(iterator.next()).resolves.toMatchObject({ done: false, value: { type: 'snapshot', cursor: -1 } })
await expect(transport.page({
address: { kind: 'session', sessionId: session.id }, throughSeq: -1,
}, signal())).resolves.toMatchObject({ events: [], hasMore: false })
}, signal())).resolves.toMatchObject({ records: [], hasMore: false })
abort.abort()
await expect(iterator.next()).resolves.toMatchObject({ done: true })
})
@@ -377,7 +377,7 @@ describe('SessionHistoryController', () => {
await expect(transport.page({
address: { kind: 'subagent', parentSessionId, childSessionId, mode: 'continuable' },
throughSeq: 0,
}, signal)).resolves.toMatchObject({ events: [{ event: { type: 'subagent/descriptor' } }] })
}, signal)).resolves.toMatchObject({ records: [{ event: { type: 'subagent/descriptor' } }] })
await expect(transport.page({
address: {
kind: 'subagent',
@@ -505,7 +505,7 @@ describe('SessionHistoryController', () => {
await expect(ordinaryBench.transport.page({
address: { kind: 'session', sessionId: ordinaryId },
throughSeq: 0,
}, signal())).resolves.toMatchObject({ events: [{ event: { seq: 0 } }] })
}, signal())).resolves.toMatchObject({ records: [{ event: { seq: 0 } }] })
const parentSessionId = SessionId('cold-parent')
const childSessionId = SessionId('cold-child')
@@ -618,12 +618,13 @@ describe('SessionHistoryController', () => {
const page = await transport.page({
address: { kind: 'session', sessionId: session.id }, throughSeq: replacement.seq, maxMessages: 2,
}, signal())
expect(page.events.map(entry => entry.event.seq)).toEqual([3, 4, 5, replacement.seq])
expect(page.records.map(entry => 'event' in entry ? entry.event.seq : entry.chunks.seq0))
.toEqual([3, 4, 5, replacement.seq])
expect(page.hasMore).toBe(true)
const before = await transport.page({
address: { kind: 'session', sessionId: session.id }, throughSeq: replacement.seq, beforeSeq: 3, maxMessages: 1,
}, signal())
expect(before.events.map(entry => entry.event.seq)).toEqual([2])
expect(before.records.map(entry => 'event' in entry ? entry.event.seq : entry.chunks.seq0)).toEqual([2])
})
it('keeps cited source events in the page that owns their appended message', async () => {
@@ -637,7 +638,7 @@ describe('SessionHistoryController', () => {
const page = await transport.page({
address: { kind: 'session', sessionId: session.id }, throughSeq: 1, maxMessages: 1,
}, signal())
expect(page.events.map(entry => entry.event.seq)).toEqual([0, 1])
expect(page.records.map(entry => 'event' in entry ? entry.event.seq : entry.chunks.seq0)).toEqual([0, 1])
expect(page.hasMore).toBe(false)
})