fix(compact): enforce replay provenance
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/compaction.md
|
||||
compaction.md: b17dfc03c62c0453c45ca2810ef90bdce9ae9ac7
|
||||
compaction.zh.md: b904cdd3ad5bf63e968fc22fccb32c5220ab8763
|
||||
compaction.md: f1df5b83bd43136af60988dabd9dc68fe32467a2
|
||||
compaction.zh.md: 52540250d466f81f51cf7c681bb6f1436e29a12d
|
||||
@@ -13,7 +13,7 @@ Compaction extends [`SessionEventMap`](session.md) with three event types via de
|
||||
| Event | Payload | Role |
|
||||
|---|---|---|
|
||||
| `compact/start` | `{ turn }` | acquires the log-recorded lock; a number identifies the open automatic turn, while `null` identifies a standalone manual attempt |
|
||||
| `compact/summary` | `{ summary, rawOutput?, llmStreamCall?, shadowedRange, shadowedSeqs, shadowedTokenCount, provider, model, maxTokens?, usage? }` | provenance: the safe summary projection, optional complete provider output and usage, an `llmStreamCall: true` marker when producing the result consumed exactly one call through this context's `ctx.llm.stream()`, the shadowed surface-boundary pair (`start`/`end` seqs — a position span, not a numeric interval), the shadowed seqs in surface order, the estimated token count, and the summarize call's envelope (`provider`, `model`, plus its generation cap when one applied) — logged so the one-shot request is reconstructable from log + code (the reconstructability Agent Note); `rawOutput` alone does not identify the call path |
|
||||
| `compact/summary` | `{ summary, rawOutput?, llmStreamCall?, shadowedRange, shadowedSeqs, shadowedTokenCount, provider, model, maxTokens?, usage? }` | provenance: the safe summary projection, optional complete provider output and usage, an `llmStreamCall: true` marker when producing the result consumed exactly one call through this context's `ctx.llm.stream()` (which requires complete `rawOutput`), the shadowed surface-boundary pair (`start`/`end` seqs — a position span, not a numeric interval), the shadowed seqs in surface order, the estimated token count, and the summarize call's envelope (`provider`, `model`, plus its generation cap when one applied) — logged so the one-shot request is reconstructable from log + code (the reconstructability Agent Note); unmarked `rawOutput` does not identify the call path |
|
||||
| `compact/end` | `{ turn, error? }` | releases the lock with the same numeric-or-null owner (`error` records an unsuccessful attempt) |
|
||||
|
||||
The lock brackets the **whole** operation: `compact/start` is appended first, then summarization, the `compact/summary` provenance record, and the `user/message` replacement all land, and only then `compact/end`. Releasing the lock last turns a crash mid-operation into a detectable orphaned lock (a `compact/start` with no matching `compact/end`) rather than a `compact/end` that falsely claims compaction finished.
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
| 事件 | 载荷 | 作用 |
|
||||
|---|---|---|
|
||||
| `compact/start` | `{ turn }` | 获取日志记录的锁;数字标识打开的自动轮次,`null` 标识独立手动尝试 |
|
||||
| `compact/summary` | `{ summary, rawOutput?, llmStreamCall?, shadowedRange, shadowedSeqs, shadowedTokenCount, provider, model, maxTokens?, usage? }` | provenance:安全摘要投影、可选的完整 provider 输出与 usage、生成结果时恰好通过此上下文的 `ctx.llm.stream()` 发起一次调用所带的 `llmStreamCall: true` 标记、被遮蔽的 surface 边界对(`start`/`end` seq——位置跨度,而非数值区间)、按 surface 顺序排列的被遮蔽 seq、估算 token 数,以及摘要调用的 envelope(`provider`、`model`,若有生成上限则还包括该上限)——写入日志后,该一次性请求可由日志 + 代码重建(见可重建性 Agent Note);单有 `rawOutput` 并不能判定调用路径 |
|
||||
| `compact/summary` | `{ summary, rawOutput?, llmStreamCall?, shadowedRange, shadowedSeqs, shadowedTokenCount, provider, model, maxTokens?, usage? }` | provenance:安全摘要投影、可选的完整 provider 输出与 usage、生成结果时恰好通过此上下文的 `ctx.llm.stream()` 发起一次调用所带的 `llmStreamCall: true` 标记(此时必须提供完整的 `rawOutput`)、被遮蔽的 surface 边界对(`start`/`end` seq——位置跨度,而非数值区间)、按 surface 顺序排列的被遮蔽 seq、估算 token 数,以及摘要调用的 envelope(`provider`、`model`,若有生成上限则还包括该上限)——写入日志后,该一次性请求可由日志 + 代码重建(见可重建性 Agent Note);未带标记的 `rawOutput` 并不能判定调用路径 |
|
||||
| `compact/end` | `{ turn, error? }` | 使用相同的数字或 `null` 归属值释放锁(`error` 记录失败尝试) |
|
||||
|
||||
锁括住**整个**操作:先追加 `compact/start`,然后执行摘要生成、写入 `compact/summary` 来源记录与 `user/message` 替换,最后才追加 `compact/end`。最后释放锁意味着操作中途崩溃会表现为可检测的遗留锁(有 `compact/start` 而无匹配的 `compact/end`),而非一个虚假声称压缩已完成的 `compact/end`。
|
||||
|
||||
+16
-13
@@ -237,7 +237,7 @@ Source: [`packages/ui/commands/src/index.ts:139`](../packages/ui/commands/src/in
|
||||
'compact/end': { turn: number | null; error?: string }
|
||||
```
|
||||
|
||||
Source: [`packages/compact/compact/src/types.ts:62`](../packages/compact/compact/src/types.ts)
|
||||
Source: [`packages/compact/compact/src/types.ts:65`](../packages/compact/compact/src/types.ts)
|
||||
|
||||
#### `compact/prune` — log-only
|
||||
|
||||
@@ -261,7 +261,7 @@ Source: [`packages/compact/compact/src/types.ts:62`](../packages/compact/compact
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/compact/compact/src/types.ts:72`](../packages/compact/compact/src/types.ts)
|
||||
Source: [`packages/compact/compact/src/types.ts:75`](../packages/compact/compact/src/types.ts)
|
||||
|
||||
#### `compact/start` — log-only
|
||||
|
||||
@@ -290,16 +290,6 @@ Source: [`packages/compact/compact/src/types.ts:19`](../packages/compact/compact
|
||||
*/
|
||||
'compact/summary': {
|
||||
summary: ContentBlock[]
|
||||
/**
|
||||
* Complete provider output before the backend's safe summary projection;
|
||||
* this alone does not identify the call path.
|
||||
*/
|
||||
rawOutput?: ContentBlock[]
|
||||
/**
|
||||
* Present only when producing the summary consumed exactly one call
|
||||
* through this context's `ctx.llm.stream()`.
|
||||
*/
|
||||
llmStreamCall?: true
|
||||
shadowedRange: { start: number; end: number }
|
||||
shadowedSeqs: number[]
|
||||
shadowedTokenCount: number
|
||||
@@ -316,7 +306,20 @@ Source: [`packages/compact/compact/src/types.ts:19`](../packages/compact/compact
|
||||
maxTokens?: number
|
||||
/** Provider-reported token usage for the summarization request, when emitted. */
|
||||
usage?: TokenUsage
|
||||
}
|
||||
} & (
|
||||
| {
|
||||
/** Complete provider output before the backend's safe summary projection. */
|
||||
rawOutput: ContentBlock[]
|
||||
/** Identifies exactly one call through this context's `ctx.llm.stream()`. */
|
||||
llmStreamCall: true
|
||||
}
|
||||
| {
|
||||
/** Optional complete output from an unmarked template, remote, or other summarizer. */
|
||||
rawOutput?: ContentBlock[]
|
||||
/** An unmarked summary does not identify a call through this context's LLM seam. */
|
||||
llmStreamCall?: never
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Types: [ContentBlock](core-data-structures/core.md) · [TokenUsage](core-data-structures/llm-streaming.md)
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/compact/compact-basic/README.md
|
||||
README.md: d1c1dfb509ae0750e1237532a829a35de5084c5e
|
||||
README.zh.md: a78887daad04d534ffed9cbd0357b76bdd908f5e
|
||||
README.md: 4241899788998a744801bb0406a15ecd70af6401
|
||||
README.zh.md: c1df4afaa1837a3b689da80cc1b49df43f60e513
|
||||
@@ -21,7 +21,7 @@ This backend owns the compaction policy:
|
||||
- **Overflow recovery** — provider-confirmed overflow needs no capacity metadata: it bypasses normal pressure and retention, prunes, then attempts one maximal balanced head reduction while leaving the newest indivisible unit. Retry is authorized whenever `surface.replaceGeneration` advances, including when pruning lands before later summary work throws. No replacement, an exhausted target-specific cap, cancellation, or an unknown/noncanonical error preserves the original provider failure.
|
||||
- **Failure handling** — a live unmatched `compact/start` is the durable lock. An unmatched marker before a newer `session/end-seed` is stale evidence from a prior lifecycle and does not block; one after that boundary reports `busy`. Summary and changed-span failures close with an error and leave the conversation surface untouched, though the attempt remains in the log. A failed close deliberately leaves a blocking orphan. Operational pressure failures warn and continue, while overflow-recovery failure preserves the original provider error only when no earlier replacement advanced the surface. Cancellation remains authoritative after cleanup and durability.
|
||||
|
||||
The protected `summarize()` method is the sole subclass hook. A template- or remote-summarizer subclass can override it while pressure, retention, provenance, shrink validation, and shadowed-token accounting stay on `ctx.tokenMeter`. The hook returns the safe summary plus the complete provider output, call envelope, and usage when available (`{ summary, rawOutput?, llmStreamCall?, provider, model, maxTokens?, usage? }`); `llmStreamCall: true` means producing that result consumed exactly one call through this context's `ctx.llm.stream()`, while `rawOutput` alone does not identify the call path. The transaction preserves those fields on `compact/summary`.
|
||||
The protected `summarize()` method is the sole subclass hook. A template- or remote-summarizer subclass can override it while pressure, retention, provenance, shrink validation, and shadowed-token accounting stay on `ctx.tokenMeter`. The hook returns the safe summary plus the complete provider output, call envelope, and usage when available (`{ summary, rawOutput?, llmStreamCall?, provider, model, maxTokens?, usage? }`); `llmStreamCall: true` means producing that result consumed exactly one call through this context's `ctx.llm.stream()` and requires complete `rawOutput`, while unmarked `rawOutput` does not identify the call path. The transaction preserves those fields on `compact/summary`.
|
||||
|
||||
## Config (`BasicCompactConfig`)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
- **溢出恢复**:提供方已确认的溢出不需容量元数据。它会绕过常规压力与保留,执行剪枝,再尝试一次最大平衡头部缩减,并留下最新不可分单元。只要 `surface.replaceGeneration` 前进,就允许重试,包括剪枝在后续摘要工作抛出异常前已落地的情况。如果没有替换、目标特定上限已耗尽、已取消,或遇到未知/非规范错误,则保留原始提供方失败。
|
||||
- **失败处理**:活动的未匹配 `compact/start` 是持久锁。位于较新 `session/end-seed` 之前的未匹配标记,是先前生命周期留下的陈旧证据,不会阻塞;位于该边界之后的标记报告 `busy`。摘要和 span 变更失败会以错误闭合,并保持会话表层不变,但日志中仍保留该尝试。闭合失败会有意留下阻塞性的未匹配标记。压力检查中的运行故障会发出警告并继续;只有此前没有替换推进表层时,溢出恢复失败才保留原始提供方错误。完成清理与持久化后,取消仍具有最终决定权。
|
||||
|
||||
受保护的 `summarize()` 方法是唯一的子类钩子。基于模板或远程摘要器的子类可以覆盖该方法,同时压力、保留、溯源、缩减验证与已遮蔽 token 计量仍由 `ctx.tokenMeter` 负责。钩子返回安全摘要,以及完整提供方输出、调用 envelope 和可用时的 usage(`{ summary, rawOutput?, llmStreamCall?, provider, model, maxTokens?, usage? }`);`llmStreamCall: true` 表示生成该结果时恰好通过此上下文的 `ctx.llm.stream()` 发起了一次调用,而单有 `rawOutput` 并不能判定调用路径。事务会在 `compact/summary` 上保留这些字段。
|
||||
受保护的 `summarize()` 方法是唯一的子类钩子。基于模板或远程摘要器的子类可以覆盖该方法,同时压力、保留、溯源、缩减验证与已遮蔽 token 计量仍由 `ctx.tokenMeter` 负责。钩子返回安全摘要,以及完整提供方输出、调用 envelope 和可用时的 usage(`{ summary, rawOutput?, llmStreamCall?, provider, model, maxTokens?, usage? }`);`llmStreamCall: true` 表示生成该结果时恰好通过此上下文的 `ctx.llm.stream()` 发起了一次调用,且必须提供完整的 `rawOutput`;未带标记的 `rawOutput` 并不能判定调用路径。事务会在 `compact/summary` 上保留这些字段。
|
||||
|
||||
## 配置(`BasicCompactConfig`)
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ interface PreparedCompaction extends SurfaceSelection {
|
||||
readonly input: SummarizationInput
|
||||
}
|
||||
|
||||
interface SummarizedCompaction extends PreparedCompaction, SummaryResult {
|
||||
type SummarizedCompaction = PreparedCompaction & SummaryResult & {
|
||||
readonly checkpointMessage: UserMessage
|
||||
}
|
||||
|
||||
@@ -415,18 +415,18 @@ function commitCompactionBody(
|
||||
shadowedSeqs,
|
||||
shadowedTokenCount,
|
||||
summary,
|
||||
rawOutput,
|
||||
llmStreamCall,
|
||||
provider,
|
||||
model,
|
||||
maxTokens,
|
||||
usage,
|
||||
checkpointMessage,
|
||||
} = summarized
|
||||
const callProvenance = summarized.llmStreamCall === true
|
||||
? { rawOutput: summarized.rawOutput, llmStreamCall: true as const }
|
||||
: summarized.rawOutput === undefined ? {} : { rawOutput: summarized.rawOutput }
|
||||
const summaryEvent = session.append('compact/summary', {
|
||||
summary,
|
||||
...rawOutput === undefined ? {} : { rawOutput },
|
||||
...llmStreamCall === undefined ? {} : { llmStreamCall },
|
||||
...callProvenance,
|
||||
shadowedRange: { start, end },
|
||||
shadowedSeqs: [...shadowedSeqs],
|
||||
shadowedTokenCount,
|
||||
|
||||
@@ -85,24 +85,27 @@ export interface SummarizationInput {
|
||||
}
|
||||
|
||||
/** Safe summary content plus the exact auxiliary call envelope recorded in provenance. */
|
||||
export interface SummaryResult {
|
||||
export type SummaryResult = {
|
||||
summary: ContentBlock[]
|
||||
/**
|
||||
* Complete provider output before the text-only summary projection; this
|
||||
* alone does not identify the call path.
|
||||
*/
|
||||
rawOutput?: ContentBlock[]
|
||||
/**
|
||||
* Present only when producing the summary consumed exactly one call through
|
||||
* this context's `ctx.llm.stream()`.
|
||||
*/
|
||||
llmStreamCall?: true
|
||||
provider: string
|
||||
model: string
|
||||
maxTokens?: number
|
||||
/** Provider-reported usage for this summarization request. */
|
||||
usage?: TokenUsage
|
||||
}
|
||||
} & (
|
||||
| {
|
||||
/** Complete provider output before the text-only summary projection. */
|
||||
rawOutput: ContentBlock[]
|
||||
/** Identifies exactly one call through this context's `ctx.llm.stream()`. */
|
||||
llmStreamCall: true
|
||||
}
|
||||
| {
|
||||
/** Optional complete output from an unmarked template, remote, or other summarizer. */
|
||||
rawOutput?: ContentBlock[]
|
||||
/** An unmarked result does not identify a call through this context's LLM seam. */
|
||||
llmStreamCall?: never
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Run the default cache-reusing `ctx.llm.stream()` summarization call: replay
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, expectTypeOf, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import BasicCompactService from '@deepseek-ai/dsh-compact-basic'
|
||||
import type { BasicCompactConfig } from '@deepseek-ai/dsh-compact-basic'
|
||||
import { selectCompactableRange } from '@deepseek-ai/dsh-compact-basic/src/region.ts'
|
||||
import type { SummarizationInput } from '@deepseek-ai/dsh-compact-basic/src/summarizer.ts'
|
||||
import type { SummarizationInput, SummaryResult } from '@deepseek-ai/dsh-compact-basic/src/summarizer.ts'
|
||||
import { toolPairingBalancedAfter, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compact'
|
||||
import {
|
||||
resolveCompactSpec,
|
||||
@@ -1166,6 +1166,15 @@ async function summarizerHarness(
|
||||
}
|
||||
|
||||
describe('default one-shot summarizer', () => {
|
||||
it('requires complete raw output when a subclass marks one local LLM stream call', () => {
|
||||
expectTypeOf<{
|
||||
summary: ContentBlock[]
|
||||
llmStreamCall: true
|
||||
provider: string
|
||||
model: string
|
||||
}>().not.toExtend<SummaryResult>()
|
||||
})
|
||||
|
||||
it('uses configured model/default cap, forwards cancellation, and keeps only safe text', async () => {
|
||||
const { adapter, compact } = await summarizerHarness([
|
||||
{ type: 'reasoning', text: 'private' },
|
||||
|
||||
@@ -28,16 +28,6 @@ declare module '@deepseek-ai/dsh-session' {
|
||||
*/
|
||||
'compact/summary': {
|
||||
summary: ContentBlock[]
|
||||
/**
|
||||
* Complete provider output before the backend's safe summary projection;
|
||||
* this alone does not identify the call path.
|
||||
*/
|
||||
rawOutput?: ContentBlock[]
|
||||
/**
|
||||
* Present only when producing the summary consumed exactly one call
|
||||
* through this context's `ctx.llm.stream()`.
|
||||
*/
|
||||
llmStreamCall?: true
|
||||
shadowedRange: { start: number; end: number }
|
||||
shadowedSeqs: number[]
|
||||
shadowedTokenCount: number
|
||||
@@ -54,7 +44,20 @@ declare module '@deepseek-ai/dsh-session' {
|
||||
maxTokens?: number
|
||||
/** Provider-reported token usage for the summarization request, when emitted. */
|
||||
usage?: TokenUsage
|
||||
}
|
||||
} & (
|
||||
| {
|
||||
/** Complete provider output before the backend's safe summary projection. */
|
||||
rawOutput: ContentBlock[]
|
||||
/** Identifies exactly one call through this context's `ctx.llm.stream()`. */
|
||||
llmStreamCall: true
|
||||
}
|
||||
| {
|
||||
/** Optional complete output from an unmarked template, remote, or other summarizer. */
|
||||
rawOutput?: ContentBlock[]
|
||||
/** An unmarked summary does not identify a call through this context's LLM seam. */
|
||||
llmStreamCall?: never
|
||||
}
|
||||
)
|
||||
/**
|
||||
* Marks the end of a compaction — log-only, releases the lock. Its owner
|
||||
* matches `compact/start`; `error` records an unsuccessful attempt.
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user