mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
perf(history): carry packed assistant chunks
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user