fix(compact): enforce replay provenance

This commit is contained in:
Tianyi Cui
2026-08-08 14:45:23 +08:00
parent 8dcfe5c406
commit 37e4e1585b
13 changed files with 107 additions and 76 deletions

View File

@@ -1,7 +1,7 @@
/**
* Keyless snapshot-test LLM replay. It derives one model-call script per
* recorded session from `assistant/chunk` events and durable compaction
* summaries, then binds fresh live sessions to parent/child scripts by
* recorded session from `assistant/chunk` events and explicitly marked local
* compaction calls, then binds fresh live sessions to parent/child scripts by
* first-call order. Throw and hang cases require an explicit override because
* a session log cannot reconstruct them alone.
* @module @deepseek-ai/dsh-llm-replay
@@ -14,6 +14,7 @@ import type {} from '@deepseek-ai/dsh-compact'
import { decodeStorageRecord } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import type {
ContentBlock,
GenerateOptions,
LlmModelInfo,
LlmProviderInfo,
@@ -21,14 +22,15 @@ import type {
ResolvedRetryPolicy,
RetryPolicyConfig,
StreamChunk,
TokenUsage,
} from '@deepseek-ai/dsh-llm'
import { LlmAdapter, LlmError, assertNever, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
/**
* One recorded model call. `throw` may replay prefix chunks before failing;
* `hang` models cancellation. Chunk entries derive from ordinary model streams
* and complete compaction outputs in JSONL; the other variants come from an
* override sidecar.
* `hang` models cancellation. Derived chunk entries come from ordinary model
* streams and complete outputs of explicitly marked local compaction calls;
* an override sidecar can supply any variant.
*/
export type ReplayEntry =
| { kind: 'chunks'; chunks: StreamChunk[] }
@@ -205,16 +207,23 @@ export function deriveReplayScript(events: SessionEvent[]): ReplayEntry[] {
close(currentKey, current)
currentKey = undefined
current = []
if (event.data.llmStreamCall === true) {
if (event.data.rawOutput === undefined) {
// JSONL decoding crosses an untyped durable boundary, so retain its wider
// shape even though current in-process producers enforce this correlation.
const persisted: {
readonly llmStreamCall?: true
readonly rawOutput?: ContentBlock[]
readonly usage?: TokenUsage
} = event.data
if (persisted.llmStreamCall === true) {
if (persisted.rawOutput === undefined) {
throw new Error('llm-replay: compact/summary marks an LLM stream call without rawOutput')
}
const chunks: StreamChunk[] = []
for (const [index, block] of event.data.rawOutput.entries()) {
for (const [index, block] of persisted.rawOutput.entries()) {
chunks.push({ type: 'block-start', index, blockType: block.type })
chunks.push({ type: 'block-end', index, block })
}
if (event.data.usage !== undefined) chunks.push({ type: 'usage', usage: event.data.usage })
if (persisted.usage !== undefined) chunks.push({ type: 'usage', usage: persisted.usage })
chunks.push({ type: 'finish', reason: { kind: 'stop' } })
script.push({ kind: 'chunks', chunks })
}

View File

@@ -259,23 +259,27 @@ describe('deriveReplayScript', () => {
expect(deriveReplayScript([event])).toEqual([])
})
it('rejects a marked compact LLM call without its complete output', () => {
const event: SessionEvent<'compact/summary'> = {
type: 'compact/summary',
seq: 1,
time: 0,
data: {
summary: [{ type: 'text', text: 'incomplete provenance' }],
llmStreamCall: true,
shadowedRange: { start: 1, end: 1 },
shadowedSeqs: [1],
shadowedTokenCount: 20,
provider: 'mock',
model: 'mock',
},
}
it('rejects a persisted marked compact LLM call without its complete output', () => {
const [event] = parseSessionLog([
JSON.stringify({ type: 'session', version: 0, id: 'invalid-compact', createdAt: 0 }),
JSON.stringify({
type: 'compact/summary',
seq: 1,
time: 0,
data: {
summary: [{ type: 'text', text: 'incomplete provenance' }],
llmStreamCall: true,
shadowedRange: { start: 1, end: 1 },
shadowedSeqs: [1],
shadowedTokenCount: 20,
provider: 'mock',
model: 'mock',
},
}),
].join('\n'))
expect(() => deriveReplayScript([event])).toThrow(/LLM stream call without rawOutput/)
expect(() => deriveReplayScript(event === undefined ? [] : [event]))
.toThrow(/LLM stream call without rawOutput/)
})
it('derives a compact/summary stream when usage is unavailable', () => {