压缩与补丁文本:压的时候也要命中缓存
pi 上下文满了就另起一次摘要调用:换一段专用的 system prompt,把要压的对话转成一段 <conversation> 文本塞进 user 消息1——和刚发出去的那次请求开头对不上,前缀缓存一个 token 也用不上。dsh 把摘要也当成对话的延续来发。下面按一次压缩的顺序讲:什么时候压、先裁再摘、怎么摘、怎么拼回、拼回后补什么,最后是几段特殊时刻的补丁文本。
效果都来自同一次实跑:dsh 自己的 agent 循环、压缩插件、read 工具和 DeepSeek 适配器,模型 deepseek-flash,任务是读 dsh 压缩包的源码讲给用户听,工作区里放了一份三行的 AGENTS.md。为了几轮就能触发,压缩阈值从默认的 678,464 token 调到 15,000,其余照 base 组合包的配置。
扣掉输出预留和余量,678,464 token 才压
问题:DeepSeek 的窗口是 100 万 token,但一次请求要给回复预留 256,000——按窗口的 80% 算阈值,看着没满,请求已经塞不下回复。
阈值取两者里小的:窗口 × 0.8,和窗口减输出预留、再减 65,536 的余量。保留不压的最近一段按(窗口 − 输出预留)× 16% 算。每一步开头(agent/pre-step)量一次:
const thresholdTokens = Math.floor(Math.min(
contextWindow * policy.thresholdRatio,
pressureBudgetTokens,
))
const retainTokens = policy.retainTokens === undefined
? Math.floor(messageBudgetTokens * policy.retainRatio)
: policy.retainTokens
用 dsh 的 resolveConfig → resolveTargetPolicy → resolveCompactSpec 算产品默认和实跑配置:
contextWindow = 1000000, reservedCompletionTokens = 256000
# 产品默认(base 组合包不给 compaction-basic 配置)
thresholdRatio=0.8 headroomTokens=65536 retainRatio=0.16 maxTokens=65536
thresholdTokens=678464 retainTokens=119040 compactionRetries=1 maxOverflowRetries=1
# run.ts 的演示配置
thresholdRatio=0.015 headroomTokens=65536 retainTokens=3000 maxTokens=65536
thresholdTokens=15000 retainTokens=3000 compactionRetries=1 maxOverflowRetries=1
DeepSeek 默认值下 80% 那条不起作用,卡住的是扣完预留和余量的 678,464。另有两个入口不看阈值:provider 报上下文超长时,裁剪后把保留长度设成 0 压一次再重试;用户打 /compact 手动压。超长重试这一支没跑,只贴模版:
if (trigger === 'context-overflow') {
if (prune !== undefined) {
prune.pruneSession(agent.session)
measurement = meter.measure(agent.session)
}
const range = selectCompactableRange(agent.session, measurement, 0)
if (range === null) return null
return this.compactRegion(range.start, range.end, agent, signal)
}
先裁大工具结果,裁完够了就不摘要
问题:上下文常常是被几个大工具结果撑满的——一次 read 读进来一万多字符。为这个调一次模型写摘要,又贵又丢细节。
压力一到,先跑不调模型的裁剪:超过 8192 字符的工具结果留头 4096、尾 1024,中间换成一行标记。每次裁剪是一个单节点 replace,原结果留在日志里。裁完再量,低于阈值就不摘要:
/** Fixed marker substituted for every removed middle span. */
export const PRUNE_MARKER = '\n\n[... tool result middle pruned ...]\n\n'
/** Low-friction defaults for coding-agent tool output. */
export const DEFAULTS: ResolvedConfig = deepFreeze({
thresholdChars: 8192,
headChars: 4096,
tailChars: 1024,
})
if (measurement.totalTokens < spec.thresholdTokens) return null
// Once pressure qualifies, land the model-free pass before choosing a
// summary range, then remeasure through the singleton replay fold.
if (prune !== undefined) {
prune.pruneSession(agent.session)
measurement = meter.measure(agent.session)
}
if (measurement.totalTokens < spec.thresholdTokens) return null
实跑第二轮读完 config.ts,压力第一次过线。三个大结果各裁到 5159 字符,没有摘要:
26 tool/call read {"file_path": "packages/compaction/compaction-basic/src/config.ts"}
27 tool/result 16084 字符
28 step/end
29 compaction/prune shadowedSeqs=[13] shadowedTokenCount=2609
30 tool/result replace 13,5159 字符
31 compaction/prune shadowedSeqs=[14] shadowedTokenCount=6664
32 tool/result replace 14,5159 字符
33 compaction/prune shadowedSeqs=[27] shadowedTokenCount=4029
34 tool/result replace 27,5159 字符
35 step/start
36 request/header
(seq 29–36 之间没有 compaction/start:裁完已低于阈值,这一步不摘要)
seq 13 原文 10403 字符 → seq 30 替换后 5159 字符(头 4096 + 标记 + 尾 1024)
surfaceOp: {"op": "replace", "startSeq": 13, "endSeq": 13} sourceEventSeqs: [13]
……(前面略)
to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.'
72:
73: /**
74: * The replayed conversation sur
[... tool result middle pruned ...]
or | undefined {
198: switch (finish.kind) {
199: case 'error':
200: case 'aborted': {
201: return new LlmError(finish.failure.message, finish.failure.code, finish.failure)
202:
……(后面略)
刚读进来的文件也会被裁:config.ts(seq 27)还没给模型看过就成了头尾。模型看到标记,自己带 offset 重读了一遍:
seq 37 reasoning:The middle was pruned — I need the resolveRetention and the policy resolution function that computes thresholdTokens/retainTokens from contextWindow and output reservation. Let me read lines ~100-300.
seq 38 tool/call:read {"file_path": "packages/compaction/compaction-basic/src/config.ts", "offset": 100, "limit": 200}
摘要指令放在最后一条 user 消息,前缀原样回放
问题:dsh 的决策记录2写过这个案例:摘要器自带一段 system prompt,后接拍平成字符串的历史,「每次压缩都要为整段回放的历史付出两次完整的提示词处理成本」——恰好发生在对话最大的时候。
摘要请求原样回放上一个请求的 system、tools 和被压的那段消息,只在末尾追加一条 user:压缩指令。tools 也带上——摘要用不到工具,可去掉它们,后面每个 token 都会错位:
function buildSummarizationInput(
session: Session,
shadowedSeqs: readonly SessionSeq[],
): SummarizationInput {
const header = session.requestHeader()
// shadowedSeqs are current surface seqs, so the surface has a node 0.
// oxlint-disable-next-line typescript/no-non-null-assertion
const head = systemHead(session, session.surface.nodes[0]!)
const system = head === undefined ? null : session.deriveEventMessage(head)
const regionMessages = shadowedSeqs
// shadowedSeqs are current surface seqs, so each is a valid log index.
// Existing Session history read; migration deferred.
// oxlint-disable-next-line typescript/no-non-null-assertion, typescript/no-deprecated
.map(seq => session.deriveEventMessage(session.eventAt(seq)!))
.filter((message): message is Message => message !== null)
return {
...header?.tools === undefined ? {} : { tools: header.tools },
messages: system === null ? regionMessages : [system, ...regionMessages],
}
}
const assembler = new BlockAssembler()
const messages: RequestMessage[] = [
...input.messages,
deepFreeze({
role: 'user',
content: [{ type: 'text', text: COMPACTION_INSTRUCTION }],
}),
]
const options: GenerateOptions = {
provider: target.provider,
model: target.model,
messages,
toolHistory: agent.session.toolHistory(),
...input.tools === undefined ? {} : { tools: [...input.tools] },
maxTokens: config.maxTokens,
sessionId: agent.session.id,
purpose: 'compaction',
...signal === undefined ? {} : { signal },
}
指令要 8 节固定的 Markdown,写英文工程短句,路径、命令、报错原样保留,不许提压缩这件事,碰到旧检查点要合并、不要照抄:
const COMPACTION_INSTRUCTION = [
'You are now acting as a compaction engine for this AI coding assistant. Condense the conversation ABOVE into a structured checkpoint that lets another model resume the work with no loss of essential context.',
'',
'Output EXACTLY the Markdown structure below: keep every section, in order. Use terse bullets, not prose paragraphs. Write "(none)" for an empty section — never drop a section.',
'',
'## Primary Request and Intent',
"- [the user's original and evolving goals; quote verbatim where the exact wording matters]",
'',
'## Key Technical Concepts',
'- [technologies, frameworks, patterns, and conventions in play]',
'',
'## Files and Code',
'- [exact path: why it matters, key changes or snippets]',
'',
'## Errors and Fixes',
'- [error: how it was resolved, plus any related user feedback]',
'',
'## Pending Jobs',
'- [explicitly requested work not yet completed]',
'',
'## Current Work',
'- [precisely what was in progress at this checkpoint]',
'',
'## Next Step',
'- [the single next action, directly in line with the most recent request, or "(none)"]',
'',
'## Critical Context',
'- [decisions and their rationale, constraints, user preferences, open questions, data needed to continue]',
'',
'Rules:',
'- Write concise English engineering prose. Preserve exact file paths, commands, error strings, identifiers, numeric values, function signatures, and syntax fragments.',
'- Capture user feedback and explicit instructions faithfully, especially corrections.',
'- Do NOT mention this summarization request or that the context was compacted.',
'- Output only the checkpoint text: do not call any tool or take any other action.',
`- If the conversation already contains a ${SUMMARY_OPEN_TAG} block, it is a PRIOR checkpoint. Do not copy it forward verbatim: preserve still-true facts, drop stale ones, and merge newer information into a single consolidated summary under the same structure.`,
].join('\n')
强制英文有决策记录3的理由:检查点是之后每次请求都带着的前缀,多语言对话里压缩器若沿用对话语言,这种语言会跟着一次次压缩留下来,带偏模型的推理文体。
实跑第 9 次调用是第一次摘要。和第 8 次(上一个对话请求)逐字节比:
上一个对话请求(第 8 次调用):15 条消息
压缩请求(第 9 次调用):13 条消息
system 逐字节相同:True
tools 逐字节相同:True(1 个工具)
thinking / output_config 相同:True
前 12 条消息逐字节相同;第 13 条(最后一条)的前 2 个块也相同 = True,只在末尾多了一块
压缩请求最后一条 user 消息的内容块:
- tool_result
- tool_result
- text: You are now acting as a compaction engine for this AI coding assistant. Condense the conversation ABOVE into a…
整次实跑每次调用的用量;末行是对照组:同一段对话按 pi 的格式另起一次摘要请求:
# 用途 消息数 新输入 命中缓存 输出
1 conversation 1 234 384 99
2 conversation 3 9688 640 1366
3 conversation 5 231 11520 56
4 conversation 7 4799 1536 137
5 conversation 9 2476 6400 1061
6 conversation 11 141 9856 96
7 conversation 13 3186 9984 2902
8 conversation 15 6923 7296 56
9 compaction 13 530 11136 5663
10 conversation 5 8251 256 136
11 conversation 7 2160 8576 82
12 conversation 9 1844 10752 3450
13 compaction 9 2722 9472 4197
14 conversation 3 8041 256 777
15 conversation 5 1384 8960 414
对照 pi 格式摘要 1 5791 0 2359
第 9 次调用 11,666 token 里有 11,136 命中缓存,新算的 530 主要是末尾那段指令;对照组 5,791 token 全部重算。代价落在压完之后:第 10 次请求只剩开头 256 token 命中——被压的那段换成了检查点,后面全是新内容,缓存要从这里重新攒。
检查点换下被压的那段,旧事件留在日志里
问题:摘要是模型写的,不一定靠得住:可能被 token 上限截断,可能比原文还长,也可能根本没写摘要。拼回去的那段话,模型还得分得清是背景还是新指令。
摘要只收文本块:被截断、或包上框之后不比被压的那段短,整次拒收。收下的包上固定引言和 <compacted-summary> 标签:
/** Framing that makes the replacement user message established context. */
const CHECKPOINT_PREAMBLE =
'This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.'
const framedSummaryTokenCount = dependencies.meter.estimateMessage(checkpointMessage)
if (framedSummaryTokenCount >= prepared.shadowedRouteTokenCount) {
throw new Error(
`summary is not smaller than the shadowed content (${framedSummaryTokenCount} estimated framed tokens >= ${prepared.shadowedRouteTokenCount})`,
)
}
落地是两条事件:一条只留档的 compaction/summary,记摘要、模型原始输出、用量和被遮住的 seq;一条 user 消息 replace 掉被压的那段,引用全部来源。被压的范围从第 0 个 system 节点之后开始,system 永远不压:
const summaryEvent = session.append('compaction/summary', {
compactionId: startEvent.data.compactionId,
...startEvent.data.sourceCommandId === undefined
? {}
: { sourceCommandId: startEvent.data.sourceCommandId },
summary,
...callRecord,
shadowedRange: { start, end },
shadowedSeqs: [...shadowedSeqs],
shadowedTokenCount,
provider,
model,
...maxTokens === undefined ? {} : { maxTokens },
...usage === undefined ? {} : { usage },
})
session.append('user/message', checkpointMessage, {
surfaceOp: { op: 'replace', startSeq: start, endSeq: end },
sourceEventSeqs: [startEvent.seq, summaryEvent.seq, ...shadowedSeqs],
})
用 dsh 的 foldSurface 折叠实跑日志,看压缩前后和下一次请求前的模型视图:22 个节点压成 6 个,日志一条没少:
# 压缩前(seq 75):日志 76 条事件,模型视图 22 个节点
4 system system-prompt You are an AI agent powered by DeepSeek Harness. You are a coding agent
5 user user Read packages/compaction/compaction-basic/src/summarizer.ts and packages
6 user agent-instructions <system-reminder> The following workspace instructions may be relevant t
7 user runtime-context Current runtime context. This snapshot supersedes earlier runtime-contex
10 assistant model [reasoning] I'll read both files. [tool-call read {"file_path": "package
30 tool tool <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/sr
32 tool tool <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/sr
17 assistant model [reasoning] Both files read. Here's how a compaction request is built an
24 user user Now read packages/compaction/compaction-basic/src/config.ts. With a 1,00
25 assistant model [reasoning] [tool-call read {"file_path": "packages/compaction/compactio
34 tool tool <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/sr
37 assistant model [reasoning] The middle of the file was pruned — I need the resolution ar
64 tool tool <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/sr
42 assistant model [reasoning] Defaults confirmed: `thresholdRatio` 0.8 (`config.ts:20`), `
49 user user Read packages/compaction/compaction-tool-result-pruner/src/index.ts and
50 assistant model [reasoning] [tool-call read {"file_path": "packages/compaction/compactio
66 tool tool <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-tool-res
54 tool tool <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-tool-res
57 assistant model [reasoning] ## Ordering relative to summarization Neither file schedules
68 user user Read packages/compaction/compaction-basic/src/index.ts. Describe the con
70 assistant model [reasoning] [tool-call read {"file_path": "packages/compaction/compactio
75 tool tool <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/sr
# 刚压完(seq 79):日志 80 条事件,模型视图 6 个节点
4 system system-prompt You are an AI agent powered by DeepSeek Harness. You are a coding agent
78 user compact-checkpoint This is an automatically generated checkpoint condensing an earlier span
57 assistant model [reasoning] ## Ordering relative to summarization Neither file schedules
68 user user Read packages/compaction/compaction-basic/src/index.ts. Describe the con
70 assistant model [reasoning] [tool-call read {"file_path": "packages/compaction/compactio
75 tool tool <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/sr
# 下一次请求前(seq 83):日志 84 条事件,模型视图 8 个节点
4 system system-prompt You are an AI agent powered by DeepSeek Harness. You are a coding agent
78 user compact-checkpoint This is an automatically generated checkpoint condensing an earlier span
57 assistant model [reasoning] ## Ordering relative to summarization Neither file schedules
68 user user Read packages/compaction/compaction-basic/src/index.ts. Describe the con
70 assistant model [reasoning] [tool-call read {"file_path": "packages/compaction/compactio
75 tool tool <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/sr
81 user agent-instructions <system-reminder> The following workspace instructions may be relevant t
82 user runtime-context Current runtime context. This snapshot supersedes earlier runtime-contex
检查点开头(全文见文末):
seq 78 user/message source=compact-checkpoint surfaceOp={"op": "replace", "startSeq": 5, "endSeq": 54}
This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.
<compacted-summary>## Primary Request and Intent
- Three sequential read-and-explain tasks over `compaction-basic` and `compaction-tool-result-pruner` packages, established by these verbatim asks:
- "Read packages/compaction/compaction-basic/src/summarizer.ts and packages/compaction/compaction-basic/src/region.ts. Explain in five bullets how a compaction request is assembled and how its result lands back in the session."
- "Now read packages/compaction/compaction-basic/src/config.ts. With a 1,000,000-token context window, a 256,000-token output reservation and default settings, what are thresholdTokens and retainTokens? Show the arithmetic."
- "Read packages/compaction/compaction-tool-result-pruner/src/index.ts and packages/compaction/compaction-tool-result-pruner/src/config.ts. When does the pruner run relative to summarization, and what exactly does a pruned tool result look like?"
- Deliverable format: explanatory prose/bullets with `path:line` citations, not code changes.
- Tasks 1 and 2 were answered; task 3 was in progress.
第 5 轮又压了一次,被压的范围从上一份检查点开始。新检查点把四个问题并成一份,没有嵌套旧的 <compacted-summary>。
拒收也真碰到过:第一次实跑阈值设得更低(8,000),模型在摘要请求里没写摘要,又调了两次 read——摘要请求带着 tools,模型就可能去调。程序只认文本,这次压缩记一条带错误的结束事件,本轮照常往下走,下一步再压才成:
第 4 次调用(压缩)的返回:thinking, tool_use read, tool_use read;stop_reason=tool_use
seq 38 compaction/start
seq 39 compaction/end error: summarization produced no text summary content
seq 40 step/start ← 本轮照常往下走
被压掉的 AGENTS.md 和运行时上下文,下一步补发
问题:AGENTS.md、工作目录、权限这些,是作为 user 消息排在对话开头的(拼法见拼 prompt),压缩会把它们一起压进检查点。检查点里只剩模型的转述,「工作区只读」这条规矩写不写进去全看模型。
这几样各自按当前的模型视图对账。运行时上下文记着自己最后一条的 seq,一被 replace 遮住就清空记录,下一步重发:
ctx.on('session/event', (subject, event) => {
if (subject !== session) return
if (event.type === 'user/message' && isOwned(event.data)) {
this.retained = { seq: event.seq, text: textOf(event.data) }
} else if (this.retained
&& isReplacementSurfaceEvent(event)
&& event.sourceEventSeqs?.includes(this.retained.seq) === true) {
this.retained = null
}
})
}
/**
* Create an uncommitted snapshot only when the retained value differs.
* @param current - fully rendered dynamic context.
* @param sections - named contributions that formed the current snapshot.
* @returns a candidate user message, or `undefined` when no update is needed.
*/
project(current: string, sections: readonly ContextSnapshotSection[]): UserMessage | undefined {
if (this.retained === undefined && current.length === 0) return
const snapshot = current.length === 0 ? CLEARED : current
if (this.retained?.text === snapshot) return
return createUserMessage({
content: [{ type: 'text', text: snapshot }],
// The cleared marker has no contributions left to attribute.
source: sections.length === 0
? { kind: SOURCE }
: { kind: SOURCE, form: 'snapshot', sections },
})
}
AGENTS.md 每步从模型视图末尾往前找基线,找不到就重读文件重发;skill 目录也是这个做法:
function visibleBaselineSource(
agent: Agent,
authorityMessages: readonly UserMessage[],
): AgentInstructionSource | undefined {
for (const message of authorityMessages.toReversed()) {
if (message.source.kind === 'agent-instructions' && message.source.baseline === true) {
return message.source
}
}
for (const seq of agent.session.surface.nodes.toReversed()) {
// oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
const event = agent.session.eventAt(seq)
if (event?.type === 'user/message'
&& event.data.source.kind === 'agent-instructions'
&& event.data.source.baseline === true) return event.data.source
}
return undefined
}
实跑压完的下一步,两条都补回来了,正文和最初一字不差:
agent-instructions: seq 6 被检查点 seq 78 遮住 = True;seq 81 重发,正文与 seq 6 相同 = True
runtime-context: seq 7 被检查点 seq 78 遮住 = True;seq 82 重发,正文与 seq 7 相同 = True
重发的 runtime-context 正文:
Current runtime context. This snapshot supersedes earlier runtime-context snapshots.
Current working directory: "/tmp/dsh-lab/compaction/ws".
补发只能接在末尾——日志只追加,模型视图也只在末尾长:上一节「下一次请求前」那段视图里,seq 81、82 排在最后。
特殊时刻的补丁文本
压缩之外,harness 还在几个时刻往对话里塞固定文本。取消和崩溃后的补齐见会话日志,大输出落盘、bash 的退出码和沙箱标记见工具与安全边界,这里讲剩下三种。
问题:模型卡在一个调用上,一遍遍发同样的参数。
同一个 agent 连续发出同一个调用(参数按键排序后比较),第 3 次附一句轻提醒,第 5、8 次换成点名工具和参数的详细版。提醒作为 user 消息跟在工具结果后面,不拦调用;用户一插话就清零:
const GENTLE_REMINDER =
'You are repeating the exact same tool call with identical arguments. '
+ 'Carefully analyze the previous result before calling again: if the task is '
+ 'not complete, try a different approach or different arguments instead of '
+ 'repeating the call.'
/** The detailed later-threshold reminder naming the tool, the run length, and the canonical arguments. */
function detailedReminder(toolName: string, count: number, canonicalArguments: string): string {
return 'Repeated tool call detected:\n'
+ `- tool: ${toolName}\n`
+ `- consecutive_calls: ${count}\n`
+ `- arguments: ${canonicalArguments}\n`
+ 'The repeated calls are not making progress. Do not call this tool with '
+ 'these exact arguments again. Inspect the latest result and choose a '
+ 'different action, different arguments, or finish the task if enough '
+ 'evidence has been gathered.'
}
工具声明了 timeoutMs,超时的结果换成一句报错:
function toolTimeoutResult(timeoutMs: number): ToolExecutionResult {
const message = `tool call timed out after ${timeoutMs}ms`
return {
content: [{ type: 'text', text: `Error: ${message}` }],
isError: true,
error: { message, info: { name: 'ToolTimeoutError', code: TOOL_TIMEOUT } },
}
}
两样用真的 agent 循环跑,模型换成仓库测试里的脚本模型(构造的):连发 8 次同一个 grep,再调一个声明 50ms 超时、实际要跑 1 秒的工具:
--- seq 9 tool/call #1 grep "{\"pattern\":\"TODO\",\"path\":\"src\"}"
seq 10 tool/result: No matches.
--- seq 14 tool/call #2 grep "{\"pattern\":\"TODO\",\"path\":\"src\"}"
seq 15 tool/result: No matches.
--- seq 19 tool/call #3 grep "{\"pattern\":\"TODO\",\"path\":\"src\"}"
seq 20 tool/result: No matches.
seq 25 user/message (repeat-tool-reminder):
You are repeating the exact same tool call with identical arguments. Carefully analyze the previous result before calling again: if the task is not complete, try a different approach or different arguments instead of repeating the call.
--- seq 27 tool/call #4 grep "{\"pattern\":\"TODO\",\"path\":\"src\"}"
seq 28 tool/result: No matches.
--- seq 32 tool/call #5 grep "{\"pattern\":\"TODO\",\"path\":\"src\"}"
seq 33 tool/result: No matches.
seq 38 user/message (repeat-tool-reminder):
Repeated tool call detected:
- tool: grep
- consecutive_calls: 5
- arguments: {"path":"src","pattern":"TODO"}
The repeated calls are not making progress. Do not call this tool with these exact arguments again. Inspect the latest result and choose a different action, different arguments, or finish the task if enough evidence has been gathered.
--- seq 40 tool/call #6 grep "{\"pattern\":\"TODO\",\"path\":\"src\"}"
seq 41 tool/result: No matches.
--- seq 45 tool/call #7 grep "{\"pattern\":\"TODO\",\"path\":\"src\"}"
seq 46 tool/result: No matches.
--- seq 50 tool/call #8 grep "{\"pattern\":\"TODO\",\"path\":\"src\"}"
seq 51 tool/result: No matches.
seq 56 user/message (repeat-tool-reminder):
Repeated tool call detected:
- tool: grep
- consecutive_calls: 8
- arguments: {"path":"src","pattern":"TODO"}
The repeated calls are not making progress. Do not call this tool with these exact arguments again. Inspect the latest result and choose a different action, different arguments, or finish the task if enough evidence has been gathered.
--- seq 58 tool/call #9 slow_check "{}"
seq 59 tool/result: Error: tool call timed out after 50ms
图片也有两种占位:模型只收文本,或者图片超了请求预算、最早的几张被换下。喂一个构造的图片引用:
# 纯文本模型收到一张图(textOnlyImageText)
[image omitted because this model accepts text only; attachment sha256:9f2c41d0]
# 图片预算超限、最早的图被替下(offloadedImageText,没有本地副本时)
[image omitted to fit request image limits; "screenshot.png" (sha256:9f2c41d07ab35e18c4f0e3b2a1d6c7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4). No local normalized image path is available; ask the user to attach it again if needed.]
占位里带着附件的哈希;有本地副本时还会写上路径,模型能自己用工具去读。
谁管什么
- 程序:什么时候压(阈值、每步开头量)、先裁后摘、裁多少、摘要请求怎么拼、拒收不合格的摘要、检查点放哪、system 不压、被压掉的上下文补发、重复调用和超时的补丁文本
- 模型:检查点写什么;看到裁剪标记后要不要重读
- 用户:
/compact手动压;部署方按模型配阈值、保留长度和摘要用哪个模型——摘要换成别的模型,前缀缓存就用不上了
实跑原件
脚本和实验设置写在维护记录里。整份会话日志(流式原件已略去)在 assets/sim/compaction/out/session-events.jsonl。
第一份检查点全文(seq 78)
seq 78 user/message source=compact-checkpoint surfaceOp={"op": "replace", "startSeq": 5, "endSeq": 54}
This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.
<compacted-summary>## Primary Request and Intent
- Three sequential read-and-explain tasks over `compaction-basic` and `compaction-tool-result-pruner` packages, established by these verbatim asks:
- "Read packages/compaction/compaction-basic/src/summarizer.ts and packages/compaction/compaction-basic/src/region.ts. Explain in five bullets how a compaction request is assembled and how its result lands back in the session."
- "Now read packages/compaction/compaction-basic/src/config.ts. With a 1,000,000-token context window, a 256,000-token output reservation and default settings, what are thresholdTokens and retainTokens? Show the arithmetic."
- "Read packages/compaction/compaction-tool-result-pruner/src/index.ts and packages/compaction/compaction-tool-result-pruner/src/config.ts. When does the pruner run relative to summarization, and what exactly does a pruned tool result look like?"
- Deliverable format: explanatory prose/bullets with `path:line` citations, not code changes.
- Tasks 1 and 2 were answered; task 3 was in progress.
## Key Technical Concepts
- TypeScript monorepo; every package lives under `packages/<group>/<name>/src`.
- Read-only checkout at `/tmp/dsh-lab/compaction/ws`; no edits or writes permitted.
- Workspace instruction: cite `path:line` for every claim.
- Cordis service/dependency-injection framework: `Context`, `Service`, `static inject`, `declare module '@deepseek-ai/cordis'` merges.
- Schemastery config validation: `z.object({...}).default(...)`, `z.number().step(1).min(...)`.
- Event-sourced session model: `Session.append`, `SessionSeq`, `SessionEvent`, surface nodes, and `surfaceOp: { op: 'replace', startSeq, endSeq }` shadowing instead of deletion (replay-safe).
- Token accounting: `TokenMeter`/`TokenMeasurement`, heuristic `estimateMessage` vs route-priced totals, shadow-price events.
- LLM streaming: `BlockAssembler`, `FinishReason`, `LlmError`, `RequestMessage`, `ToolSchema`, `TokenUsage`.
- Unicode correctness: code points via `Array.from(text).length` (never splits surrogate pairs); grapheme clusters may still split.
## Files and Code
- `packages/compaction/compaction-basic/src/summarizer.ts` (221 lines) — one-shot summarization, LLM request construction, checkpoint framing.
- `SUMMARY_OPEN_TAG = '<compacted-summary>'` / `SUMMARY_CLOSE_TAG = '</compacted-summary>'` (22-23).
- `COMPACTION_INSTRUCTION` (32-67) is appended as the **final user message**, not a separate summarizer system prompt, so the call is a byte-prefix of the last routed request and reuses provider KV cache (25-31). Requires exact Markdown structure: `## Primary Request and Intent`, `## Key Technical Concepts`, `## Files and Code`, `## Errors and Fixes`, `## Pending Jobs`, `## Current Work`, `## Next Step`, `## Critical Context`; empty section writes `"(none)"`.
- `CHECKPOINT_PREAMBLE` (70-71) makes the replacement message read as established background.
- Target resolution order: config → last routed header → agent options (`summarizer.ts:127-142`).
- `ctx.llm.stream()` with `toolHistory`, `purpose: 'compaction'`, `sessionId`, `maxTokens`, `signal` (152-163).
- Chunks feed `BlockAssembler` (163); terminal `error`/`aborted` → `LlmError`, `max-tokens` → hard `Error` with `code = 'MAX_TOKENS'` (197-211).
- Empty summary throws (167-171); `summaryText` rejects images with `LlmError('compaction summary cannot contain image output', 'UNSUPPORTED_CONTENT')` and keeps only `type: 'text'` blocks (214-220).
- `SummaryResult` = summarized text + raw output + `llmStreamCall: true` + provider usage (87-107, 172-180); `frameSummary` wraps preamble + tags (188-194).
- `packages/compaction/compaction-basic/src/region.ts` (600 lines) — surface-retention selection and the log-recorded compaction transaction shared by automatic open-turn and manual idle-session paths.
- `selectCompactableRange` (117-155): reserve `retainTokens` tail budget, walk back, then forward to first balanced boundary; node 0 `system/message` excluded (131); tool-pairing balance via `toolPairingBalancedAfter`/`toolPairingBalancedBefore` (143-147).
- `validateSurfaceRegion` (337-358): both seqs exist on surface, ordered, balanced on both sides.
- Owner from open turn or `null` for manual; `compaction/start` with fresh `CompactionId` appended before any `await` — the durable marker is the lock, re-checked by `assertCompactionInactive` (184-210, 308-320).
- `prepareCompaction` prices selected nodes, detects surface drift, captures heuristic + route-priced totals (361-384); `buildSummarizationInput` replays head system message + tool schemas + shadowed region in surface order (544-563).
- `summarizeCompaction` tags the checkpoint `UserMessage` with `compactCheckpointSource`, refuses it unless the framed estimate is strictly smaller than the span's route-priced total, and retries once via `recover` (396-422).
- `commitCompactionBody` appends `compaction/summary` then a `user/message` with `surfaceOp: { op: 'replace', startSeq, endSeq }` and `sourceEventSeqs` (491-509); bracket closes with `compaction/end`; `completeCompaction` attaches `endSeq` (237-239, 525-530); failed close leaves an unmatched start detectable (240-251).
- `packages/compaction/compaction-basic/src/config.ts` (362 lines) — load-time validation and routed-model policy resolution.
- `DEFAULT_THRESHOLD_RATIO = 0.8` (20); `DEFAULT_RETAIN_RATIO = 0.16` (23); `headroomTokens` default `65_536` (75); `maxTokens` defaults to `headroomTokens` (76); `compactionRetries`/`maxOverflowRetries` default 1 (105-106); `auto` default true (108).
- `resolveTargetPolicy` merges exact provider/model override over defaults (118-139); `retainRatio` and `retainTokens` are mutually exclusive (292-294).
- `resolveCompactSpec` (153-216): `messageBudgetTokens = contextWindow - reservedCompletionTokens` (172); `pressureBudgetTokens = messageBudgetTokens - headroomTokens` (181); `thresholdTokens = floor(min(contextWindow * thresholdRatio, pressureBudgetTokens))` (191-194); ratio retention `floor(messageBudgetTokens * retainRatio)` (195-197); `retainTokens < thresholdTokens` guard (198-204); `deepFreeze` result (205-216).
- `packages/compaction/compaction-tool-result-pruner/src/index.ts` (185 lines) — "Replay-safe, model-free tool-result pruning service."
- `ToolResultPruner extends Service`, `static inject = ['tokenMeter']` (44-47) — the meter prices each shadowed node for its logged shadow-price event.
- Config schema `thresholdChars` / `headChars` / `tailChars` (49-53); constructor resolves immutable `config` (58-61).
- `measureContent(blocks)` sums code points across text blocks only (68-74).
- `pruneContent(blocks)` returns `null` when `totalChars <= config.thresholdChars` (85); sets `removedStart = headChars`, `removedEnd = totalChars - tailChars` (87-88); pushes non-text blocks through unchanged to preserve rich-block order (94-97); slices by `Array.from(block.text)` (99); inserts the marker once via `markerInserted` flag (91, 99+). JSDoc: code-point slicing cannot split surrogate pairs, but grapheme clusters may still split (76-82).
- Per pruned result: appends `compaction/prune` with `shadowedRange: { start: seq, end: seq }`, `shadowedSeqs: [seq]`, `shadowedTokenCount: this.ctx.tokenMeter.estimateMessage(original)` (160-164), then appends a new `tool/result` with `{ ...event.data, message }` and `surfaceOp: { op: 'replace', startSeq: seq, endSeq: seq }`, `sourceEventSeqs: [seq]` (165-171).
- Records `{ originalSeq, replacementSeq, callId, charsBefore, charsAfter }` (172-178); returns `{ pruned, charsRemoved }` (179-181). Default export `ToolResultPruner` (185).
- `packages/compaction/compaction-tool-result-pruner/src/config.ts` (77 lines).
- `PRUNE_MARKER = '\n\n[... tool result middle pruned ...]\n\n'` (7) — 39 code points by computation.
- `DEFAULTS = { thresholdChars: 8192, headChars: 4096, tailChars: 1024 }`, `deepFreeze` (10-14).
- `codePointLength(text) = Array.from(text).length` (27-29).
- `resolveConfig` rejects unknown keys listing allowed set (37-44); validates `thresholdChars` positive int, `headChars`/`tailChars` non-negative ints (51-53, 67-77).
- Guard: `headChars + codePointLength(PRUNE_MARKER) + tailChars <= thresholdChars`, else throws "must be at most thresholdChars" (55-63); returns `deepFreeze(structuredClone(resolved))` (64).
## Errors and Fixes
- No code errors or user corrections occurred. All three requests were answered without pushback.
- Self-corrected trap in task 2: the 80% ratio does not bind — `678,464 < 800,000`, so the headroom-capped budget is the real `thresholdTokens`. Also noted retention scales off `messageBudgetTokens` (744,000), not the raw 1,000,000 window.
- Tool-output truncation, not a code error: middle segments of read results were pruned by the tool (e.g. `summarizer.ts` lines 73-197, `region.ts` lines 92-575, `config.ts` lines 181-286). Recovered the needed ranges by re-reading with `offset`/`limit`. The pruner's `index.ts` middle (lines ~102-158) was never recovered.
## Pending Jobs
- Task 3 answer is incomplete: it was not delivered before this point.
- Unknown from current evidence: the exact hook that invokes the pruner relative to summarization, and thus its precise ordering (before/after the summarizer call, and within which turn boundary). The trigger site was in the unread middle of `index.ts`.
- Not yet verified: whether pruning runs on the same shadowed span that summarization later prices, and how `compaction/prune` events interact with `selectCompactableRange`'s tail-retention decisions.
- Detailed multi-text-block slicing semantics (how head/middle/tail are distributed across several text blocks) in `index.ts:99-130`.
## Current Work
- Just completed reading `packages/compaction/compaction-tool-result-pruner/src/index.ts` and `.../config.ts` in parallel for task 3.
- Confirmed halves: config defaults/validation/marker, and the session-writing half of `pruneContent` (shadow-price event + shadowed `tool/result` replacement + result bookkeeping).
- Task 2 answer already delivered with arithmetic: `messageBudgetTokens = 1,000,000 - 256,000 = 744,000`; `pressureBudgetTokens = 744,000 - 65,536 = 678,464`; `thresholdTokens = floor(min(800,000, 678,464)) = 678,464`; `retainTokens = floor(744,000 × 0.16) = 119,040`. Valid since `119,040 < 678,464`.
## Next Step
- Answer task 3: re-read `packages/compaction/compaction-tool-result-pruner/src/index.ts` around lines 100-160 (and check what consumes/emits `compaction/prune` or calls `pruneContent`) to pin the pruning trigger relative to summarization, then state precisely when the pruner runs and what a pruned tool result contains.
## Critical Context
- Citation discipline is mandatory: every claim needs `path:line`; do not answer from memory of these files.
- Read-only checkout — never attempt edits; verification is by reading, not running.
- Architectural split, both already established: `compaction-basic` is model-based (one LLM call, `purpose: 'compaction'`, KV-cache-prefix-preserving replay, fail-closed on `max-tokens`), while `compaction-tool-result-pruner` is deterministic and model-free (character budgets, no LLM, replay-safe substring replacement). The pruner's cheaper character-level surgery is the natural counterpart to summarization's expensive semantic rewrite, which is why its ordering relative to summarization is the crux of the pending question.
- Pruned-result shape, confirmed portion: the original `tool/result` is not deleted but shadowed by a replacement `tool/result` event whose `data` is the original spread with only `message` swapped, aliased via `sourceEventSeqs: [seq]` — so `message.source.callId` and tool pairing survive intact and replay is stable. Content becomes retained head (default 4096 code points) + `PRUNE_MARKER` (39 code points) + retained tail (default 1024) with all non-text blocks preserved in original order. Default expansion is `4096 + 39 + 1024 = 5159`, comfortably under `thresholdChars: 8192`. The 8192 threshold is why typical coding-agent tool output passes untouched (`pruneContent` returns `null`).
- Pruning is measurement-coupled: it is only sound because `tokenMeter` is injected (`index.ts:46-47`) and logs `shadowedTokenCount` via `estimateMessage(original)` at `index.ts:160-164`, letting pure consumers subtract the shadowed node's heuristic price without per-node state (`index.ts:157-159`). Note this is a heuristic estimate, whereas `compaction-basic`'s shrink comparison uses route-priced totals (`region.ts:46-47`, `region.ts:417-422`) — a real unit mismatch to keep in mind if the two are ever compared.
- Pruned entries keep `charsBefore`/`charsAfter` per item and an aggregate `charsRemoved`, which are character counts, not tokens — again distinct from token accounting elsewhere.
- Token constants already established for `compaction-basic` defaults: `0.8`, `0.16`, `65_536` headroom, `maxTokens` defaulting to headroom; `retainTokens` and `retainRatio` mutually exclusive, so supplying both is a load error.</compacted-summary>
第二份检查点全文(seq 104,合并了第一份)
seq 104 user/message source=compact-checkpoint surfaceOp={"op": "replace", "startSeq": 78, "endSeq": 91}
This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.
<compacted-summary>## Primary Request and Intent
- Four sequential read-and-explain tasks over the `compaction-basic` and `compaction-tool-result-pruner` packages, established by these verbatim asks:
- "Read packages/compaction/compaction-basic/src/summarizer.ts and packages/compaction/compaction-basic/src/region.ts. Explain in five bullets how a compaction request is assembled and how its result lands back in the session."
- "Now read packages/compaction/compaction-basic/src/config.ts. With a 1,000,000-token context window, a 256,000-token output reservation and default settings, what are thresholdTokens and retainTokens? Show the arithmetic."
- "Read packages/compaction/compaction-tool-result-pruner/src/index.ts and packages/compaction/compaction-tool-result-pruner/src/config.ts. When does the pruner run relative to summarization, and what exactly does a pruned tool result look like?"
- "Read packages/compaction/compaction-basic/src/index.ts. Describe the context-overflow recovery path step by step."
- Deliverable format: explanatory prose/bullets with `path:line` citations, not code changes.
- Tasks 1–3 answered; task 4 is the active request and has not yet been answered.
## Key Technical Concepts
- TypeScript monorepo; every package lives under `packages/<group>/<name>/src`.
- Read-only checkout at `/tmp/dsh-lab/compaction/ws`; no edits or writes permitted.
- Workspace instruction: cite `path:line` for every claim.
- Cordis service/dependency-injection framework: `Context`, `Service`, `static inject`, `ctx.get()`, `ctx.on()`, `ctx.waterfall()`, `declare module` merges.
- Schemastery config validation: `z.object({...})`, `z.number().step(1).min(...)`, `.required()`.
- Event-sourced session model: `Session.append`, `SessionSeq`, `SessionEvent`, surface nodes, `surfaceOp: { op: 'replace', startSeq, endSeq }` shadowing instead of deletion (replay-safe).
- Token accounting: `TokenMeter`/`measure`, `estimateMessage`, route-priced totals, shadow-price events; a "singleton replay fold" remeasure.
- Compaction triggers: `CompactionTrigger` = `'pressure' | 'context-overflow'`; `assertNever` exhaustiveness guard.
- `CONTEXT_WINDOW_EXCEEDED_CODE` imported from `@deepseek-ai/dsh-llm` (`index.ts:12`).
- Automatic recovery registered as agent hooks: `agent/pre-step`, `agent/status`, `session/event`.
## Files and Code
- `packages/compaction/compaction-basic/src/summarizer.ts` (221 lines) — one-shot summarization LLM request + checkpoint framing.
- `SUMMARY_OPEN_TAG`/`SUMMARY_CLOSE_TAG` (22-23); `COMPACTION_INSTRUCTION` (32-67) appended as final **user** message so the call byte-prefixes the last routed request and reuses provider KV cache (25-31); exact Markdown sections incl. `## Primary Request and Intent`, `## Key Technical Concepts`, `## Files and Code`, `## Errors and Fixes`, `## Pending Jobs`, `## Current Work`, `## Next Step`, `## Critical Context`; empty section = `"(none)"`.
- `CHECKPOINT_PREAMBLE` (70-71); target resolution config → last routed header → agent options (127-142); `ctx.llm.stream()` with `toolHistory`, `purpose: 'compaction'`, `sessionId`, `maxTokens`, `signal` (152-163); `BlockAssembler` (163).
- Terminal `error`/`aborted` → `LlmError`; `max-tokens` → hard `Error` with `code = 'MAX_TOKENS'` (197-211); empty summary throws (167-171); `summaryText` rejects images via `LlmError('compaction summary cannot contain image output', 'UNSUPPORTED_CONTENT')` (214-220).
- `SummaryResult` = text + raw output + `llmStreamCall: true` + provider usage (87-107, 172-180); `frameSummary` wraps preamble + tags (188-194).
- `packages/compaction/compaction-basic/src/region.ts` (600 lines) — surface-retention selection and the log-recorded compaction transaction.
- `selectCompactableRange` (117-155): reserve `retainTokens` tail budget, walk back, forward to first balanced boundary; node 0 `system/message` excluded (131); tool-pairing balance via `toolPairingBalancedAfter`/`toolPairingBalancedBefore` (143-147).
- `validateSurfaceRegion` (337-358); owner from open turn or `null` for manual; `compaction/start` with fresh `CompactionId` appended before any `await` — the durable marker is the lock (184-210), re-checked by `assertCompactionInactive` (308-320) and `assertNoActiveCompaction` (exported, used at `index.ts:305`).
- `prepareCompaction` prices nodes, detects surface drift, captures heuristic + route-priced totals (361-384); `buildSummarizationInput` replays head system message + tool schemas + shadowed region in surface order (544-563).
- `summarizeCompaction` tags checkpoint `UserMessage` with `compactCheckpointSource`, requires strictly smaller framed estimate vs span's route-priced total, retries once via `recover` (396-422).
- `commitCompactionBody` appends `compaction/summary` then `user/message` with `surfaceOp: { op: 'replace', startSeq, endSeq }` and `sourceEventSeqs` (491-509); bracket closes with `compaction/end`; `completeCompaction` attaches `endSeq` (237-239, 525-530).
- Stability modes `'whole-surface'`/`'selected-span'` with `SurfaceChangedError` (`region.ts:56-59`, `region.ts:77`).
- `packages/compaction/compaction-basic/src/config.ts` (362 lines) — load-time validation and routed-model policy resolution.
- `DEFAULT_THRESHOLD_RATIO = 0.8` (20); `DEFAULT_RETAIN_RATIO = 0.16` (23); `headroomTokens` default `65_536` (75); `maxTokens` defaults to headroom (76); `compactionRetries`/`maxOverflowRetries` default 1 (105-106); `auto` default true (108).
- `resolveTargetPolicy` merges exact provider/model override over defaults (118-139); `retainRatio`/`retainTokens` mutually exclusive (292-294).
- `resolveCompactSpec` (153-216): `messageBudgetTokens = contextWindow - reservedCompletionTokens` (172); `pressureBudgetTokens = messageBudgetTokens - headroomTokens` (181); `thresholdTokens = floor(min(contextWindow * thresholdRatio, pressureBudgetTokens))` (191-194); ratio retention `floor(messageBudgetTokens * retainRatio)` (195-197); `retainTokens < thresholdTokens` guard (198-204); `deepFreeze` (205-216).
- `packages/compaction/compaction-tool-result-pruner/src/index.ts` (185 lines) — replay-safe, model-free `ToolResultPruner extends Service`, `static inject = ['tokenMeter']` (44-47).
- Config `thresholdChars`/`headChars`/`tailChars` (49-53); `measureContent` sums text code points (68-74); `pruneContent` returns `null` when `totalChars <= thresholdChars` (85); `removedStart = headChars`, `removedEnd = totalChars - tailChars` (87-88); non-text blocks preserved in order (94-97); code-point slicing via `Array.from` (99).
- Per pruned result: `compaction/prune` with `shadowedRange {start:seq,end:seq}`, `shadowedSeqs: [seq]`, `shadowedTokenCount = estimateMessage(original)` (160-164); then new `tool/result` with `{ ...event.data, message }` + `surfaceOp replace` + `sourceEventSeqs: [seq]` (165-171); records `{originalSeq, replacementSeq, callId, charsBefore, charsAfter}` (172-178).
- `packages/compaction/compaction-tool-result-pruner/src/config.ts` (77 lines) — `PRUNE_MARKER = '\n\n[... tool result middle pruned ...]\n\n'` (7, 39 code points); `DEFAULTS = { thresholdChars: 8192, headChars: 4096, tailChars: 1024 }` (10-14); `codePointLength` (27-29); `resolveConfig` rejects unknown keys (37-44); guard `headChars + marker + tailChars <= thresholdChars` (55-63).
- `packages/compaction/compaction-basic/src/index.ts` (452 lines, just read) — the active task's subject.
- `routedTarget` (49-57) reads `session.requestHeader()?.config` and returns `{provider, model}` only when both non-empty; `reservedCompletionTokens` (65-68) = header `maxTokens ?? defaultMaxTokens ?? 0`; `conversationTarget` (71-79) falls back to `agent.options.provider/model`.
- Schemas 81-89, `modelPolicy` 91-103; `BasicCompactionEngine extends CompactionEngine`, `static inject = ['llm','tokenMeter','sessions']` (114); full `static Config` incl. `modelPolicies` array and `auto` boolean (116-128).
- Fields: `config` (131), `warnedPressureConfigTargets` (133), `overflowRetries: WeakMap<Agent, number>` (134), `overflowAgents: WeakMap<Session, Agent>` (135); constructor resolves config and calls `_registerAutomaticCompaction()` when `auto` (137-141).
- `_registerAutomaticCompaction` (148-…): `logResult` (150-156); `agent/pre-step` handler runs `this.compactIfNeeded(agent, 'pressure', signal)` when not aborted, swallows errors with `ctx.logger.warn('step compaction failed: …; continuing the turn')`, de-dupes `TargetPressureConfigError` warnings by `targetKey` (158-176); `agent/status` clears `overflowRetries` on `idle` (178-180); `session/event` resets the overflow-recovery sequence on `assistant/message` (182-186, comment: a successful response starts a fresh sequence even when tool calls continue the turn into another request) — the overflow handler body (≈187-240) was pruned from the read and is still unverified.
- `summarize` (247-257) resolves `conversationTarget` → `resolveTargetPolicy` → `summarizeWithLlm(this.ctx, config, input, agent, signal)`.
- `compactIfNeeded` (269-347): `routedTarget` undefined → `null`; `resolveTargetPolicy`; `measurement = meter.measure(session)`; `switch (trigger)` over `'context-overflow'`/`'pressure'` with `assertNever` default (279-287); `const prune = this.ctx.get('toolResultPruner')` (292).
- **Overflow branch** (294-302): if pruner present, `prune.pruneSession(agent.session)` then `measurement = meter.measure(agent.session)`; `selectCompactableRange(agent.session, measurement, 0)` — retain budget **0**, bypassing retention policy; `null` → return `null`; else `this.compactRegion(range.start, range.end, agent, signal)`. No `resolveModelInfo`, no threshold check, no retry loop, no `assertNoActiveCompaction`.
- **Pressure branch** (304-346): `await ctx.llm.resolveModelInfo(...)`; `assertNoActiveCompaction(session, 'automatic pressure compaction')`; `TargetPressureConfigError` if `info.context === undefined` (307-313); `resolveCompactSpec(policy, info.context.contextWindow, reservedCompletionTokens(agent, info.defaultMaxTokens))` (314-318); return `null` if `measurement.totalTokens < spec.thresholdTokens` (319); then pruner pass + remeasure (323-326) and re-check threshold (327); retry loop `for (attempt = 0; attempt <= spec.compactionRetries; attempt += 1)` selecting with `spec.retainTokens`, compacting, remeasuring, early-returning on success (329-341); else throws `` `compaction still above threshold after ${spec.compactionRetries + 1} compaction attempts (${...} estimated tokens >= threshold ${...})` `` (343-346).
- `compactRegion` (358-373) → `compactSurfaceRegion(deps, session, start, end, agent, { owner: 'current-turn', stability: 'whole-surface' }, signal)`.
- `compactNow` (383-435) → `agent.runMaintenance` with `AbortSignal.any([agentSignal, signal])`, `selectCompactableRange(..., 0)`, `owner: null`, `stability: 'selected-span'`, optional `sourceCommandId`, `flush: () => ctx.sessions.flush(session)`; maps aborts to `ManualCompactionError('cancelled', …)` and outer failures to `ManualCompactionError('busy', 'manual compaction requires an idle agent with no waking queued work', …)`.
- `regionDependencies` (438-449): `{ meter: this.ctx.tokenMeter, summarize: (input, owner, abort) => this.summarize(...), recover: (error, agent, sourceEventSeqs, signal) => this.ctx.waterfall('compaction/summary-error', {...}, () => false) }`; default export `BasicCompactionEngine` (452).
## Errors and Fixes
- No code errors or user corrections; no pushback on any answer.
- Self-corrected trap in task 2: the 80% ratio does not bind — `678,464 < 800,000`, so the headroom-capped budget is the real `thresholdTokens`; retention scales off `messageBudgetTokens` (744,000), not the raw 1,000,000 window.
- Tool-output truncation repeatedly pruned middle spans of reads (`index.ts` lines 95-244 in this latest read; earlier `summarizer.ts` 73-197, `region.ts` 92-575, `config.ts` 181-286). Recovered the needed ranges via `offset`/`limit`.
## Pending Jobs
- Task 4 answer not yet delivered.
- Unread/unverified in `index.ts`: the overflow-mode `session/event` handler body (≈187-240), which is where `CONTEXT_WINDOW_EXCEEDED_CODE`, `overflowRetries`, and `overflowAgents` are used and where `compactIfNeeded(agent, 'context-overflow', …)` is presumably invoked.
## Current Work
- Reading `packages/compaction/compaction-basic/src/index.ts` in segments to trace the context-overflow recovery path; have lines 1-94, 95-264 (partial, pruned middle), 265-429, and 430-452.
## Next Step
- Read `packages/compaction/compaction-basic/src/index.ts` with `offset` ≈ 182, `limit` ≈ 70 to capture the `session/event` overflow handler (lines ≈186-240), then answer task 4 with a numbered step-by-step path and `path:line` citations.
## Critical Context
- Citation discipline is mandatory: every claim needs `path:line`; do not answer from memory.
- Read-only checkout — verification is by reading, not running.
- Established architecture: `compaction-basic` is model-based (one LLM call, `purpose: 'compaction'`, KV-cache-prefix-preserving replay, fail-closed on `max-tokens`); `compaction-tool-result-pruner` is deterministic/model-free (character budgets, no LLM, replay-safe substring replacement), reached from `compaction-basic` only via the optional service lookup `this.ctx.get('toolResultPruner')` (so the package stays independently composable, `index.ts:289-292`).
- Task 3 answer delivered: the pruner's ordering vs summarization is caller-determined (no scheduling in either file); safe reading is prune-then-summarize; pruning inside an open compaction bracket would invalidate the span via `SurfaceChangedError`/stability checks. Pruned result = head 4096 + 39-code-point marker + tail 1024 code points (5159 ≤ 8192), non-text blocks preserved, original event left in log and shadowed by a replacement `tool/result` with identical `data` except `message`, prefixed by a `compaction/prune` shadow-price event.
- Key asymmetry for task 4 that must be called out: **overflow** takes the cheap path only — pruner first, then `selectCompactableRange(..., 0)` (zero retained tail) — and skips `resolveModelInfo`, the threshold comparison, `assertNoActiveCompaction`, and the retry loop; **pressure** does model-capacity resolution, threshold gating, pruner second-pass, and a bounded retry loop that throws when still above threshold.
- `retainTokens: 0` for overflow/manual means the whole compactable range can be replaced, which is why no retention policy applies; the summary must still be strictly cheaper than the span (`region.ts:417-422`).
- Error containment differs by path: `agent/pre-step` swallows failures and continues the turn (`index.ts:166-173`), whereas `compactNow` maps to `ManualCompactionError('busy'|'cancelled')`.
- `regionDependencies.recover` dispatches `ctx.waterfall('compaction/summary-error', …)` defaulting to `() => false` (`index.ts:442-447`), wiring the error-recovery hook to `region.ts`'s single retry.
- Task 2 arithmetic (already delivered): `messageBudgetTokens = 744,000`; `pressureBudgetTokens = 678,464`; `thresholdTokens = 678,464`; `retainTokens = 119,040`; valid since `119,040 < 678,464`.</compacted-summary>
第 9 次调用:dsh 发出的压缩请求体
{
"model": "deepseek-flash",
"stream": true,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Read packages/compaction/compaction-basic/src/summarizer.ts and packages/compaction/compaction-basic/src/region.ts. Explain in five bullets how a compaction request is assembled and how its result lands back in the session."
},
{
"type": "text",
"text": "<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\n# Workspace notes\n\n- TypeScript monorepo: every package lives under `packages/<group>/<name>/src`.\n- This checkout is read-only. Do not try to edit or write files.\n- When you explain code, cite `path:line` for every claim.\n\n</system-reminder>"
},
{
"type": "text",
"text": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"/tmp/dsh-lab/compaction/ws\"."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "",
"signature": "62a048f7-5ea2-4111-b43f-825d3e5721d5"
},
{
"type": "text",
"text": "I'll read both files."
},
{
"type": "tool_use",
"id": "call_00_ET_gb6NwxQvXf27RCVj6OTi5910",
"name": "read",
"input": {
"file_path": "packages/compaction/compaction-basic/src/summarizer.ts"
}
},
{
"type": "tool_use",
"id": "call_01_ET_i32zCYNMeST2K4IoK0UW5515",
"name": "read",
"input": {
"file_path": "packages/compaction/compaction-basic/src/region.ts"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "call_00_ET_gb6NwxQvXf27RCVj6OTi5910",
"content": [
{
"type": "text",
"text": "<path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/src/summarizer.ts</path>\n<type>file</type>\n<content>\n1: /**\n2: * Default one-shot summarization and durable checkpoint framing.\n3: *\n4: * @module @deepseek-ai/dsh-compaction-basic/summarizer\n5: */\n6: \n7: import type { Context } from '@deepseek-ai/cordis'\n8: import { contentHasImage, BlockAssembler, LlmError } from '@deepseek-ai/dsh-llm'\n9: import { deepFreeze } from '@deepseek-ai/dsh-util-values'\n10: import type {\n11: ContentBlock, FinishReason, GenerateOptions, Message, RequestMessage, TokenUsage, ToolSchema,\n12: } from '@deepseek-ai/dsh-llm'\n13: import type { Agent } from '@deepseek-ai/dsh-agent'\n14: \n15: interface SummaryConfig {\n16: readonly summarizationProvider: string\n17: readonly summarizationModel: string\n18: readonly maxTokens: number\n19: }\n20: \n21: /** Tags wrapping the structured summary inside the landed checkpoint node. */\n22: const SUMMARY_OPEN_TAG = '<compacted-summary>'\n23: const SUMMARY_CLOSE_TAG = '</compacted-summary>'\n24: \n25: /**\n26: * The summarization directive, delivered as the FINAL user message after the\n27: * replayed conversation rather than as a distinct summarizer system prompt.\n28: * Keeping the conversation's own system prompt, tools, and message prefix in\n29: * front of it makes the auxiliary call a genuine prefix of the last routed\n30: * request, so the provider's KV cache is reused instead of invalidated.\n31: */\n32: const COMPACTION_INSTRUCTION = [\n33: 'You are now acting as a compaction engine for this AI coding assistant. Condense the conversation ABOVE into a structured checkpoint that lets another model resume the work with no loss of essential context.',\n34: '',\n35: 'Output EXACTLY the Markdown structure below: keep every section, in order. Use terse bullets, not prose paragraphs. Write \"(none)\" for an empty section — never drop a section.',\n36: '',\n37: '## Primary Request and Intent',\n38: \"- [the user's original and evolving goals; quote verbatim where the exact wording matters]\",\n39: '',\n40: '## Key Technical Concepts',\n41: '- [technologies, frameworks, patterns, and conventions in play]',\n42: '',\n43: '## Files and Code',\n44: '- [exact path: why it matters, key changes or snippets]',\n45: '',\n46: '## Errors and Fixes',\n47: '- [error: how it was resolved, plus any related user feedback]',\n48: '',\n49: '## Pending Jobs',\n50: '- [explicitly requested work not yet completed]',\n51: '',\n52: '## Current Work',\n53: '- [precisely what was in progress at this checkpoint]',\n54: '',\n55: '## Next Step',\n56: '- [the single next action, directly in line with the most recent request, or \"(none)\"]',\n57: '',\n58: '## Critical Context',\n59: '- [decisions and their rationale, constraints, user preferences, open questions, data needed to continue]',\n60: '',\n61: 'Rules:',\n62: '- Write concise English engineering prose. Preserve exact file paths, commands, error strings, identifiers, numeric values, function signatures, and syntax fragments.',\n63: '- Capture user feedback and explicit instructions faithfully, especially corrections.',\n64: '- Do NOT mention this summarization request or that the context was compacted.',\n65: '- Output only the checkpoint text: do not call any tool or take any other action.',\n66: `- If the conversation already contains a ${SUMMARY_OPEN_TAG} block, it is a PRIOR checkpoint. Do not copy it forward verbatim: preserve still-true facts, drop stale ones, and merge newer information into a single consolidated summary under the same structure.`,\n67: ].join('\\n')\n68: \n69: /** Framing that makes the replacement user message established context. */\n70: const CHECKPOINT_PREAMBLE =\n71: 'This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.'\n72: \n73: /**\n74: * The replayed conversation sur\n\n[... tool result middle pruned ...]\n\nor | undefined {\n198: switch (finish.kind) {\n199: case 'error':\n200: case 'aborted': {\n201: return new LlmError(finish.failure.message, finish.failure.code, finish.failure)\n202: }\n203: case 'max-tokens': {\n204: const error = new Error('summarization truncated at the token cap (incomplete checkpoint)') as Error & { code?: string }\n205: error.code = 'MAX_TOKENS'\n206: return error\n207: }\n208: default:\n209: return undefined\n210: }\n211: }\n212: \n213: /** Reject visual output and keep only text before synthesizing a user message. */\n214: function summaryText(\n215: blocks: readonly ContentBlock[],\n216: ): Array<Extract<ContentBlock, { type: 'text' }>> {\n217: if (contentHasImage(blocks)) {\n218: throw new LlmError('compaction summary cannot contain image output', 'UNSUPPORTED_CONTENT')\n219: }\n220: return blocks.filter((block): block is Extract<ContentBlock, { type: 'text' }> => block.type === 'text')\n221: }\n\n(End of file - total 221 lines)\n</content>"
}
],
"is_error": false
},
{
"type": "tool_result",
"tool_use_id": "call_01_ET_i32zCYNMeST2K4IoK0UW5515",
"content": [
{
"type": "text",
"text": "<path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/src/region.ts</path>\n<type>file</type>\n<content>\n1: /**\n2: * Surface retention selection and the shared log-recorded compaction\n3: * transaction for automatic open-turn and manual idle-session compaction.\n4: *\n5: * @module @deepseek-ai/dsh-compaction-basic/region\n6: */\n7: \n8: import { randomUUID } from 'node:crypto'\n9: import { isDeepStrictEqual } from 'node:util'\n10: import {\n11: CompactionId,\n12: ManualCompactionError,\n13: compactCheckpointSource,\n14: toolPairingBalancedAfter,\n15: toolPairingBalancedBefore,\n16: } from '@deepseek-ai/dsh-compaction'\n17: import type { CompactionResult } from '@deepseek-ai/dsh-compaction'\n18: import type { CommandId } from '@deepseek-ai/dsh-commands/brand'\n19: import { createUserMessage, errorChain } from '@deepseek-ai/dsh-llm'\n20: import type { Message, UserMessage } from '@deepseek-ai/dsh-llm'\n21: import type { TokenMeasurement, TokenMeter } from '@deepseek-ai/dsh-token-meter'\n22: import { SessionSeq, type Session, type SessionEvent } from '@deepseek-ai/dsh-session'\n23: import type { Agent } from '@deepseek-ai/dsh-agent'\n24: import { frameSummary } from './summarizer.ts'\n25: import type { SummarizationInput, SummaryResult } from './summarizer.ts'\n26: interface RegionDependencies {\n27: readonly meter: TokenMeter\n28: summarize(input: SummarizationInput, agent: Agent, signal?: AbortSignal): Promise<SummaryResult>\n29: recover(error: unknown, agent: Agent, sourceEventSeqs: readonly SessionSeq[], signal?: AbortSignal): boolean\n30: }\n31: \n32: /** One validated inclusive span of current surface positions. */\n33: interface SurfaceSelection {\n34: readonly start: SessionSeq\n35: readonly end: SessionSeq\n36: readonly startIdx: number\n37: readonly endIdx: number\n38: readonly shadowedSeqs: readonly SessionSeq[]\n39: }\n40: \n41: /** A selection with its priced snapshot and the replay input built from it. */\n42: interface PreparedCompaction extends SurfaceSelection {\n43: readonly measurement: TokenMeasurement\n44: readonly selectedNodes: TokenMeasurement['nodes']\n45: readonly shadowedTokenCount: number\n46: /** Route-priced total of the selected span; the shrink comparison's unit. */\n47: readonly shadowedRouteTokenCount: number\n48: readonly input: SummarizationInput\n49: }\n50: \n51: type SummarizedCompaction = PreparedCompaction & SummaryResult & {\n52: readonly checkpointMessage: UserMessage\n53: }\n54: \n55: interface CompactionTransactionOptions {\n56: /** `current-turn` derives a numbered owner; `null` writes a standalone bracket. */\n57: readonly owner: 'current-turn' | null\n58: /** Surface relationship that must survive asynchronous summarization. */\n59: readonly stability: 'whole-surface' | 'selected-span'\n60: /** Optional durability checkpoint after a successfully closed bracket. */\n61: readonly flush?: () => Promise<void>\n62: /** Manual command that initiated this transaction, when present. */\n63: readonly sourceCommandId?: CommandId\n64: }\n65: \n66: interface CompactionEntryState {\n67: readonly openTurn: number | null\n68: readonly unmatchedCompactionStart: SessionEvent<'compaction/start'> | undefined\n69: readonly latestEndSeedSeq: SessionSeq | undefined\n70: }\n71: \n72: /**\n73: * Rejects a summary whose replacement boundaries are no longer the ones it was\n74: * built from, distinguished from summarizer and shrink failures so a manual\n75: * caller can report the two causes differently.\n76: */\n77: class SurfaceChangedError extends Error {}\n78: \n79: /** Whether the summary may still replace the span it was built from. */\n80: type StabilityCheck = (\n81: dependencies: RegionDependencies,\n82: session: Session,\n83: prepared: PreparedCompaction,\n84: ) => void\n85: \n86: /** Failure captured after `compaction/start` has committed. */\n87: interface TransactionFailure {\n88: readonly error: unknown\n89: readonly stage: 'summary' | 'commit'\n90: }\n91: \n92: /**\n93: * The `system/message` holding surface node 0, or `undefined` when another\n94: * message-producing event starts th\n\n[... tool result middle pruned ...]\n\nnst event = session.eventAt(SessionSeq(seq))!\n576: if (latestEndSeedSeq === undefined && event.type === 'session/end-seed') {\n577: latestEndSeedSeq = event.seq\n578: }\n579: if (!compactionEntryStateKnown) {\n580: if (event.type === 'compaction/start') {\n581: unmatchedCompactionStart = event\n582: compactionEntryStateKnown = true\n583: } else if (event.type === 'compaction/end') {\n584: compactionEntryStateKnown = true\n585: }\n586: }\n587: if (!openTurnStateKnown) {\n588: if (event.type === 'turn/start') {\n589: openTurn = event.data.turn\n590: openTurnStateKnown = true\n591: } else if (event.type === 'turn/end') {\n592: openTurnStateKnown = true\n593: }\n594: }\n595: if (openTurnStateKnown\n596: && compactionEntryStateKnown\n597: && latestEndSeedSeq !== undefined) break\n598: }\n599: return { openTurn, unmatchedCompactionStart, latestEndSeedSeq }\n600: }\n\n(End of file - total 600 lines)\n</content>"
}
],
"is_error": false
}
]
},
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "Let me write five bullets explaining assembly and landing back, with citations.\n\nKey claims:\n\n1. Range selection: selectCompactableRange picks span retaining tail (region.ts:117-155), system head excluded (region.ts:131), balance check (region.ts:143-147).\n2. Validation + prepare: validateSurfaceRegion checks start/end exist, ordered, tool pairing balanced (region.ts:337-358); prepareCompaction prices nodes via meter (region.ts:361-384) and builds summarization input (region.ts:382).\n3. Input assembly: buildSummarizationInput puts system head message + region messages (region.ts:544-563); summarizer adds instruction as final user message (summarizer.ts:145-151), options with toolHistory, tools, purpose 'compaction', sessionId, maxTokens, signal (summarizer.ts:152-162); key resolution priority (summarizer.ts:127-142).\n4. Result: streamed chunks assembled via BlockAssembler (summarizer.ts:163), finish errors fail closed, max-tokens error (summarizer.ts:197-211), text-only projection rejecting images (summarizer.ts:214-220), empty summary error (summarizer.ts:169-171), usage returned (summarizer.ts:172-180).\n5. Landing: frameSummary wraps with preamble + tags (summarizer.ts:188-194); summarizeCompaction creates user message with compactCheckpointSource (region.ts:410-413), shrink check vs route token count (region.ts:417-422); commitCompactionBody appends compaction/summary event then user/message with surfaceOp replace (region.ts:491-509); compaction/end (region.ts:237-239) and result completion (region.ts:525-530).\n\nCould also mention transaction bracket / locking as part of assembly (compaction/start as lock region.ts:204-210).\n\nFive bullets — combine. Let me write concisely but with citations.",
"signature": "4e70847f-72e2-498c-a3f7-0e7e2734c438"
},
{
"type": "text",
"text": "Both files read. Here's how a compaction request is built and how its outcome returns to the session.\n\n- **Span selection & validation (the \"what to compact\" decision).** `selectCompactableRange` walks the priced surface backwards accumulating tokens until the `retainTokens` tail budget is met, then walks forward to the first tool-pairing-balanced boundary, never letting a `system/message` at node 0 into the range (`packages/compaction/compaction-basic/src/region.ts:117-155`, head exclusion at `region.ts:131`). `validateSurfaceRegion` then confirms both seqs exist on the surface, are ordered, and are balanced on both sides so no tool-call/result pair or open step is split (`region.ts:337-358`).\n\n- **Transaction opening and the compaction lock.** The owner is derived from the open turn, or `null` for manual idle compaction, and a `compaction/start` event with a fresh `CompactionId` is appended *before* any `await` — the durable marker itself is the lock, re-checked by `assertCompactionInactive` (`region.ts:184-210`, `region.ts:308-320`). `prepareCompaction` then prices the selected nodes with the meter, checks the surface hasn't drifted, and captures both the heuristic and route-priced totals (`region.ts:361-384`).\n\n- **Request assembly (replay-then-instruction).** `buildSummarizationInput` reconstructs the last routed request's cacheable prefix: the `system/message` head's derived message (if any), the header's tool schemas, then the shadowed region's events projected to messages in surface order (`region.ts:544-563`). The summarizer appends `COMPACTION_INSTRUCTION` as the **final user message** rather than using a separate summarizer system prompt, precisely so the call is a byte-prefix of the real request and hits the provider's KV cache (`summarizer.ts:25-67`, `summarizer.ts:145-151`); target provider/model resolves as config → last routed header → agent options (`summarizer.ts:127-142`) and the call goes out via `ctx.llm.stream()` with `toolHistory`, `purpose: 'compaction'`, the session id, `maxTokens`, and any abort signal (`summarizer.ts:152-163`).\n\n- **Result collection and fail-closed shaping.** Chunks feed a `BlockAssembler`; terminal `error`/`aborted` finish reasons become an `LlmError`, and `max-tokens` becomes a hard `MAX_TOKENS` error rather than a silently truncated checkpoint (`summarizer.ts:163-165`, `summarizer.ts:197-211`). The output is projected to text-only, rejecting any image block, and an empty summary throws (`summarizer.ts:167-171`, `summarizer.ts:214-220`). `SummaryResult` carries the safe `summary`, the raw output plus `llmStreamCall: true` marker, and provider usage (`summarizer.ts:87-107`, `summarizer.ts:172-180`).\n\n- **Landing back in the session.** `frameSummary` wraps the text in the preamble plus `<compacted-summary>` tags (`summarizer.ts:188-194`), `summarizeCompaction` turns that into a `UserMessage` tagged by `compactCheckpointSource` and refuses it if the framed estimate isn't strictly smaller than the span's route-priced total (`region.ts:410-422`), re-running summarization once if `recover` accepts the failure (`region.ts:396-409`). `commitCompactionBody` then appends `compaction/summary` (range, seqs, token count, provider/model, usage, raw output) followed by a `user/message` carrying `surfaceOp: { op: 'replace', startSeq, endSeq }` and `sourceEventSeqs`, which is what swaps the span for the checkpoint on the surface (`region.ts:491-509`); the bracket closes with `compaction/end` and `completeCompaction` attaches `endSeq` to the returned `CompactionResult` (`region.ts:237-239`, `region.ts:525-530`). A failed close leaves the unmatched start detectable for the next caller (`region.ts:240-251`)."
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "Now read packages/compaction/compaction-basic/src/config.ts. With a 1,000,000-token context window, a 256,000-token output reservation and default settings, what are thresholdTokens and retainTokens? Show the arithmetic."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "Let me read config.ts.",
"signature": "2c12eb3f-af24-41a6-a36a-0ac2dc557db9"
},
{
"type": "tool_use",
"id": "call_00_W6wEHJZv5Fkx9ROyRgL23464",
"name": "read",
"input": {
"file_path": "packages/compaction/compaction-basic/src/config.ts"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "call_00_W6wEHJZv5Fkx9ROyRgL23464",
"content": [
{
"type": "text",
"text": "<path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/src/config.ts</path>\n<type>file</type>\n<content>\n1: /**\n2: * Load-time validation and routed-model policy resolution for compaction-basic.\n3: *\n4: * @module @deepseek-ai/dsh-compaction-basic/config\n5: */\n6: \n7: import type { LlmCallConfig } from '@deepseek-ai/dsh-llm'\n8: import { deepFreeze } from '@deepseek-ai/dsh-util-values'\n9: import type {\n10: BasicCompactionConfig,\n11: CompactionPolicyConfig,\n12: ModelCompactPolicyConfig,\n13: ResolvedCompactSpec,\n14: ResolvedConfig,\n15: ResolvedRetention,\n16: ResolvedTargetPolicy,\n17: } from './types.ts'\n18: \n19: /** Default request-pressure fraction for every routed model. */\n20: const DEFAULT_THRESHOLD_RATIO = 0.8\n21: \n22: /** Default verbatim-tail fraction for every routed model. */\n23: const DEFAULT_RETAIN_RATIO = 0.16\n24: \n25: /** Fields shared by top-level defaults and exact-target overrides. */\n26: const POLICY_CONFIG_KEYS = [\n27: 'thresholdRatio',\n28: 'headroomTokens',\n29: 'retainRatio',\n30: 'retainTokens',\n31: 'summarizationProvider',\n32: 'summarizationModel',\n33: 'maxTokens',\n34: 'compactionRetries',\n35: 'maxOverflowRetries',\n36: ] as const\n37: \n38: /** Complete public top-level configuration key set. */\n39: const BASIC_COMPACT_CONFIG_KEYS: ReadonlySet<string> = new Set([\n40: ...POLICY_CONFIG_KEYS,\n41: 'modelPolicies',\n42: 'auto',\n43: ])\n44: \n45: /** Complete exact-target override key set. */\n46: const MODEL_POLICY_KEYS: ReadonlySet<string> = new Set([\n47: 'provider',\n48: 'model',\n49: ...POLICY_CONFIG_KEYS,\n50: ])\n51: \n52: /** Target-specific pressure configuration failure eligible for warning suppression. */\n53: export class TargetPressureConfigError extends Error {\n54: /**\n55: * @param targetKey - exact provider/model route used as the warning key.\n56: * @param message - actionable configuration failure detail.\n57: */\n58: constructor(readonly targetKey: string, message: string) {\n59: super(message)\n60: }\n61: }\n62: \n63: /**\n64: * Resolve and validate service defaults plus exact-target partial overrides.\n65: * @param config - untrusted plugin configuration after Loader normalization.\n66: * @returns detached immutable defaults and validated exact-target overrides.\n67: */\n68: export function resolveConfig(config: BasicCompactionConfig = {}): ResolvedConfig {\n69: validateKeys(config, BASIC_COMPACT_CONFIG_KEYS, 'BasicCompactionConfig')\n70: validatePolicy(config, 'BasicCompactionConfig')\n71: if (config.auto !== undefined && typeof config.auto !== 'boolean') {\n72: throw new Error('BasicCompactionConfig: auto must be a boolean')\n73: }\n74: \n75: const headroomTokens = config.headroomTokens ?? 65_536\n76: const maxTokens = config.maxTokens ?? headroomTokens\n77: assertPositiveInteger('BasicCompactionConfig.maxTokens (explicit or from headroomTokens)', maxTokens)\n78: const thresholdRatio = config.thresholdRatio ?? DEFAULT_THRESHOLD_RATIO\n79: const retention = resolveRetention(config, { retainRatio: DEFAULT_RETAIN_RATIO })\n80: validateRatioRetention(thresholdRatio, retention, 'BasicCompactionConfig')\n81: const modelPolicies = resolveModelPolicies(config.modelPolicies)\n82: for (const [index, policy] of modelPolicies.entries()) {\n83: if (policy.maxTokens === undefined && config.maxTokens === undefined\n84: && policy.headroomTokens !== undefined) {\n85: policy.maxTokens = policy.headroomTokens\n86: }\n87: assertPositiveInteger(\n88: `BasicCompactionConfig: modelPolicies[${index}].maxTokens (explicit or from headroomTokens)`,\n89: policy.maxTokens ?? maxTokens,\n90: )\n91: validateRatioRetention(\n92: policy.thresholdRatio ?? thresholdRatio,\n93: resolveRetention(policy, retention),\n94: `BasicCompactionConfig: modelPolicies[${index}]`,\n95: )\n96: }\n97: \n98: return deepFreeze({\n99: thresholdRatio,\n100: headroomTokens,\n101: ...retention,\n102: summarizationProvider: config.summarizationProvider ?? '',\n103: summarizationModel: config.summarizationMod\n\n[... tool result middle pruned ...]\n\n if (typeof value !== 'string' || value.length === 0) {\n342: throw new Error(`${name} must be a non-empty string`)\n343: }\n344: }\n345: \n346: function assertPositiveInteger(name: string, value: unknown): asserts value is number {\n347: if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {\n348: throw new Error(`${name} (${String(value)}) must be a positive integer`)\n349: }\n350: }\n351: \n352: function assertNonNegativeInteger(name: string, value: unknown): asserts value is number {\n353: if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n354: throw new Error(`${name} (${String(value)}) must be a non-negative integer`)\n355: }\n356: }\n357: \n358: function assertRatio(name: string, value: unknown): asserts value is number {\n359: if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 1) {\n360: throw new Error(`${name} (${String(value)}) must be a number in (0, 1]`)\n361: }\n362: }\n\n(End of file - total 362 lines)\n</content>"
}
],
"is_error": false
}
]
},
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "The middle was pruned — I need the resolveRetention and the policy resolution function that computes thresholdTokens/retainTokens from contextWindow and output reservation. Let me read lines ~100-300.",
"signature": "77dda3c2-c945-43e3-a167-9d81fb44be11"
},
{
"type": "text",
"text": "The middle of the file was pruned — I need the resolution arithmetic."
},
{
"type": "tool_use",
"id": "call_00_O2Ug6dojgFL2bsEF6RgB1794",
"name": "read",
"input": {
"file_path": "packages/compaction/compaction-basic/src/config.ts",
"offset": 100,
"limit": 200
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "call_00_O2Ug6dojgFL2bsEF6RgB1794",
"content": [
{
"type": "text",
"text": "<path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/src/config.ts</path>\n<type>file</type>\n<content>\n100: headroomTokens,\n101: ...retention,\n102: summarizationProvider: config.summarizationProvider ?? '',\n103: summarizationModel: config.summarizationModel ?? '',\n104: maxTokens,\n105: compactionRetries: config.compactionRetries ?? 1,\n106: maxOverflowRetries: config.maxOverflowRetries ?? 1,\n107: modelPolicies,\n108: auto: config.auto ?? true,\n109: })\n110: }\n111: \n112: /**\n113: * Merge the exact provider/model override over the validated default policy.\n114: * @param config - validated service defaults and override table.\n115: * @param target - exact durable provider/model route to match.\n116: * @returns detached immutable policy before model-capacity scaling.\n117: */\n118: export function resolveTargetPolicy(\n119: config: ResolvedConfig,\n120: target: Pick<LlmCallConfig, 'provider' | 'model'>,\n121: ): ResolvedTargetPolicy {\n122: const override = config.modelPolicies.find(policy => (\n123: policy.provider === target.provider && policy.model === target.model\n124: ))\n125: const inheritedRetention: ResolvedRetention = config.retainTokens === undefined\n126: ? { retainRatio: config.retainRatio }\n127: : { retainTokens: config.retainTokens }\n128: return deepFreeze({\n129: target: { provider: target.provider, model: target.model },\n130: thresholdRatio: override?.thresholdRatio ?? config.thresholdRatio,\n131: headroomTokens: override?.headroomTokens ?? config.headroomTokens,\n132: ...resolveRetention(override ?? {}, inheritedRetention),\n133: summarizationProvider: override?.summarizationProvider ?? config.summarizationProvider,\n134: summarizationModel: override?.summarizationModel ?? config.summarizationModel,\n135: maxTokens: override?.maxTokens ?? config.maxTokens,\n136: compactionRetries: override?.compactionRetries ?? config.compactionRetries,\n137: maxOverflowRetries: override?.maxOverflowRetries ?? config.maxOverflowRetries,\n138: })\n139: }\n140: \n141: /**\n142: * Scale one routed policy into concrete token budgets for its model capacity.\n143: *\n144: * Pressure is capped by both the window fraction and the capacity remaining\n145: * after the routed output reservation plus compaction headroom. Retention scales\n146: * the message budget before headroom is deducted.\n147: *\n148: * @param policy - merged policy for the exact routed target.\n149: * @param contextWindow - positive adapter-owned capacity for that target.\n150: * @param reservedCompletionTokens - output tokens one routed request reserves.\n151: * @returns detached immutable pressure and retention budgets.\n152: */\n153: export function resolveCompactSpec(\n154: policy: ResolvedTargetPolicy,\n155: contextWindow: number,\n156: reservedCompletionTokens: number,\n157: ): ResolvedCompactSpec {\n158: const targetKey = `${policy.target.provider}/${policy.target.model}`\n159: if (!Number.isInteger(contextWindow) || contextWindow <= 0) {\n160: throw new TargetPressureConfigError(\n161: targetKey,\n162: `BasicCompactionConfig: contextWindow (${contextWindow}) must be a positive integer`,\n163: )\n164: }\n165: if (!Number.isInteger(reservedCompletionTokens) || reservedCompletionTokens < 0) {\n166: throw new TargetPressureConfigError(\n167: targetKey,\n168: `BasicCompactionConfig: reservedCompletionTokens (${reservedCompletionTokens}) `\n169: + 'must be a non-negative integer',\n170: )\n171: }\n172: const messageBudgetTokens = contextWindow - reservedCompletionTokens\n173: if (messageBudgetTokens <= 0) {\n174: throw new TargetPressureConfigError(\n175: targetKey,\n176: `compaction-basic: ${targetKey} reserves ${reservedCompletionTokens} completion tokens `\n177: + `of its ${contextWindow}-token context window, leaving no message budget; configure `\n178: + \"the adapter model's contextWindow above the effective request maxTokens\",\n179: )\n180: }\n181: const pressureBudgetTokens = messageBudgetTo\n\n[... tool result middle pruned ...]\n\ns\n286: const compactionRetries = config.compactionRetries\n287: const maxOverflowRetries = config.maxOverflowRetries\n288: if (thresholdRatio !== undefined) assertRatio(`${name}.thresholdRatio`, thresholdRatio)\n289: if (headroomTokens !== undefined) assertNonNegativeInteger(`${name}.headroomTokens`, headroomTokens)\n290: if (retainRatio !== undefined) assertRatio(`${name}.retainRatio`, retainRatio)\n291: if (retainTokens !== undefined) assertNonNegativeInteger(`${name}.retainTokens`, retainTokens)\n292: if (retainRatio !== undefined && retainTokens !== undefined) {\n293: throw new Error(`${name}: retainRatio and retainTokens are mutually exclusive`)\n294: }\n295: if (maxTokens !== undefined) assertPositiveInteger(`${name}.maxTokens`, maxTokens)\n296: if (compactionRetries !== undefined) {\n297: assertNonNegativeInteger(`${name}.compactionRetries`, compactionRetries)\n298: }\n299: if (maxOverflowRetries !== undefined) {\n\n(Showing lines 100-299 of 362. Use offset=300 to continue.)\n</content>"
}
],
"is_error": false
}
]
},
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "Defaults: thresholdRatio 0.8 (config.ts:20), retainRatio 0.16 (config.ts:23), headroomTokens default 65_536 (config.ts:75). maxTokens defaults to headroomTokens — but the question gives reservedCompletionTokens = 256,000, which is the routed output reservation, an input to resolveCompactSpec, separate from `maxTokens`. Actually reservedCompletionTokens comes from adapter. So use 256,000.\n\nCompute:\n- messageBudgetTokens = 1,000,000 - 256,000 = 744,000 (config.ts:172)\n- pressureBudgetTokens = 744,000 - 65,536 = 678,464 (config.ts:181)\n- thresholdTokens = floor(min(1,000,000 * 0.8, 678,464)) = floor(min(800,000, 678,464)) = 678,464 (config.ts:191-194)\n- retainTokens (default ratio, since retainTokens undefined) = floor(744,000 * 0.16) = floor(119,040) = 119,040 (config.ts:195-197)\n\nCheck retainTokens < thresholdTokens: 119,040 < 678,464 ✓ (config.ts:198).\n\nNote the headroom cap binds, not the 0.8 ratio — interesting point: pressure budget 678,464 is below 800,000.\n\nAlso note retainRatio is scaled against messageBudgetTokens, not contextWindow — 744,000*0.16 = 119,040 rather than 160,000.\n\nAlso note validateRatioRetention: 0.16 < 0.8 passes at load (config.ts:235).\n\nAlso mention maxTokens default = headroomTokens = 65,536 (config.ts:76) — but that's the summarization request cap, not the reservation. The reservedCompletionTokens is supplied separately (config.ts:150, 156).\n\nNumbers: 119,040 exactly (744000*0.16 = 119,040).",
"signature": "b7fe14ea-359f-43cd-ad43-f0a6d813e080"
},
{
"type": "text",
"text": "Defaults confirmed: `thresholdRatio` 0.8 (`config.ts:20`), `retainRatio` 0.16 (`config.ts:23`), `headroomTokens` 65,536 (`config.ts:75`). Since the default is a ratio, not a fixed `retainTokens`, the ratio path applies (`config.ts:195-197`); `reservedCompletionTokens` is an input supplied by the adapter, distinct from `maxTokens` (`config.ts:150-156`).\n\n**Arithmetic** (`resolveCompactSpec`, `config.ts:172-197`):\n\n```\nmessageBudgetTokens = contextWindow − reservedCompletionTokens\n = 1,000,000 − 256,000\n = 744,000 (config.ts:172)\n\npressureBudgetTokens = messageBudgetTokens − headroomTokens\n = 744,000 − 65,536\n = 678,464 (config.ts:181)\n\nthresholdTokens = floor( min( contextWindow × thresholdRatio,\n pressureBudgetTokens ) )\n = floor( min( 1,000,000 × 0.8, 678,464 ) )\n = floor( min( 800,000, 678,464 ) )\n = 678,464 (config.ts:191-194)\n\nretainTokens = floor( messageBudgetTokens × retainRatio ) [ratio default]\n = floor( 744,000 × 0.16 )\n = floor( 119,040.0 )\n = 119,040 (config.ts:195-197)\n```\n\n- **`thresholdTokens` = 678,464** — note the headroom cap binds, not the nominal 80% figure: 800,000 > 678,464, so the budget after the 256,000 output reservation and 65,536 headroom is the real ceiling. A larger reservation or headroom would silently dominate the ratio here.\n- **`retainTokens` = 119,040** — scaled against `messageBudgetTokens` (744,000), *not* the raw 1,000,000 window (which would have given 160,000), because retention is clamped to the message budget before headroom is deducted (`config.ts:145-146`, `config.ts:196`).\n\nThe result is valid: `retainTokens` (119,040) < `thresholdTokens` (678,464), so the guard at `config.ts:198-204` passes, and the load-time ratio check `0.16 < 0.8` passes at `config.ts:235-240`. Both budgets are returned under `deepFreeze` alongside `contextWindow` and the ratios (`config.ts:205-216`).\n\nOne caveat on literals: there is no rounding loss here, but `Math.floor` on both means a non-integer context window or an odd window/ratio pairing could shave up to 1 token off each figure rather than rounding to nearest."
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "Read packages/compaction/compaction-tool-result-pruner/src/index.ts and packages/compaction/compaction-tool-result-pruner/src/config.ts. When does the pruner run relative to summarization, and what exactly does a pruned tool result look like?"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "",
"signature": "58dfa939-83b6-4d54-8ea9-72d0a800c591"
},
{
"type": "tool_use",
"id": "call_00_ET_uZNbnKaMIkbydPq995NR5044",
"name": "read",
"input": {
"file_path": "packages/compaction/compaction-tool-result-pruner/src/index.ts"
}
},
{
"type": "tool_use",
"id": "call_01_ET_p0xGozRbRHw4CsHaI38A3444",
"name": "read",
"input": {
"file_path": "packages/compaction/compaction-tool-result-pruner/src/config.ts"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "call_00_ET_uZNbnKaMIkbydPq995NR5044",
"content": [
{
"type": "text",
"text": "<path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-tool-result-pruner/src/index.ts</path>\n<type>file</type>\n<content>\n1: /**\n2: * Replay-safe, model-free tool-result pruning service.\n3: *\n4: * @module @deepseek-ai/dsh-compaction-tool-result-pruner\n5: */\n6: \n7: import { Context, Service } from '@deepseek-ai/cordis'\n8: import z from '@deepseek-ai/schemastery'\n9: import { freezeMessage } from '@deepseek-ai/dsh-llm'\n10: import type { ContentBlock } from '@deepseek-ai/dsh-llm'\n11: import type { Session, SessionEvent, SessionSeq, ToolResultMessage } from '@deepseek-ai/dsh-session'\n12: // Type-only: the `compaction/*` SessionEventMap merges (the shadow-price event).\n13: import type {} from '@deepseek-ai/dsh-compaction'\n14: // Type-only: the `ctx.tokenMeter` Context merge for the declared injection.\n15: import type {} from '@deepseek-ai/dsh-token-meter'\n16: import { codePointLength, DEFAULTS, PRUNE_MARKER, resolveConfig } from './config.ts'\n17: import type {\n18: PrunedEntry,\n19: PruneResult,\n20: ResolvedConfig,\n21: ToolResultPruneConfig,\n22: } from './types.ts'\n23: \n24: export { codePointLength, DEFAULTS, PRUNE_MARKER, resolveConfig } from './config.ts'\n25: export type {\n26: PrunedEntry,\n27: PruneResult,\n28: ResolvedConfig,\n29: ToolResultPruneConfig,\n30: } from './types.ts'\n31: \n32: declare module '@deepseek-ai/cordis' {\n33: interface Context {\n34: toolResultPruner: ToolResultPruner\n35: }\n36: }\n37: \n38: interface SnapshotCandidate {\n39: readonly seq: SessionSeq\n40: readonly event: SessionEvent<'tool/result'>\n41: }\n42: \n43: /** Deterministic head/middle/tail pruning for current tool-result surface nodes. */\n44: export class ToolResultPruner extends Service {\n45: // The token meter prices each shadowed node for its logged shadow-price\n46: // event, so pruning genuinely requires the pricing capability.\n47: static inject = ['tokenMeter']\n48: \n49: static Config: z<ToolResultPruneConfig> = z.object({\n50: thresholdChars: z.number().step(1).min(1).default(DEFAULTS.thresholdChars),\n51: headChars: z.number().step(1).min(0).default(DEFAULTS.headChars),\n52: tailChars: z.number().step(1).min(0).default(DEFAULTS.tailChars),\n53: })\n54: \n55: /** Resolved and immutable character budgets. */\n56: readonly config: ResolvedConfig\n57: \n58: constructor(ctx: Context, config: ToolResultPruneConfig = {}) {\n59: super(ctx, 'toolResultPruner')\n60: this.config = resolveConfig(config)\n61: }\n62: \n63: /**\n64: * Measure text content in Unicode code points; non-text blocks cost zero.\n65: * @param blocks - tool-result content to measure.\n66: * @returns total Unicode code points across text blocks.\n67: */\n68: measureContent(blocks: readonly ContentBlock[]): number {\n69: let chars = 0\n70: for (const block of blocks) {\n71: if (block.type === 'text') chars += codePointLength(block.text)\n72: }\n73: return chars\n74: }\n75: \n76: /**\n77: * Replace an over-budget text middle while retaining rich-block order.\n78: * Text slicing is by Unicode code point, not UTF-16 code unit, so a retained\n79: * boundary cannot split a surrogate pair. Grapheme clusters may still split.\n80: * @param blocks - original tool-result content.\n81: * @returns pruned content, or `null` when the text is within budget.\n82: */\n83: pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null {\n84: const totalChars = this.measureContent(blocks)\n85: if (totalChars <= this.config.thresholdChars) return null\n86: \n87: const removedStart = this.config.headChars\n88: const removedEnd = totalChars - this.config.tailChars\n89: const pruned: ContentBlock[] = []\n90: let consumed = 0\n91: let markerInserted = false\n92: \n93: for (const block of blocks) {\n94: if (block.type !== 'text') {\n95: pruned.push(block)\n96: continue\n97: }\n98: \n99: const points = Array.from(block.text)\n100: const blockStart = consumed\n101: const blockEnd = blockStart + points.length\n102: const headEnd = Math.min(point\n\n[... tool result middle pruned ...]\n\nnt, so pure consumers subtract the\n159: // shadowed node's heuristic price without retaining per-node state.\n160: session.append('compaction/prune', {\n161: shadowedRange: { start: seq, end: seq },\n162: shadowedSeqs: [seq],\n163: shadowedTokenCount: this.ctx.tokenMeter.estimateMessage(original),\n164: })\n165: const replacement = session.append('tool/result', {\n166: ...event.data,\n167: message,\n168: }, {\n169: surfaceOp: { op: 'replace', startSeq: seq, endSeq: seq },\n170: sourceEventSeqs: [seq],\n171: })\n172: pruned.push({\n173: originalSeq: seq,\n174: replacementSeq: replacement.seq,\n175: callId: event.data.message.source.callId,\n176: charsBefore,\n177: charsAfter,\n178: })\n179: charsRemoved += charsBefore - charsAfter\n180: }\n181: return { pruned, charsRemoved }\n182: }\n183: }\n184: \n185: export default ToolResultPruner\n\n(End of file - total 185 lines)\n</content>"
}
],
"is_error": false
},
{
"type": "tool_result",
"tool_use_id": "call_01_ET_p0xGozRbRHw4CsHaI38A3444",
"content": [
{
"type": "text",
"text": "<path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-tool-result-pruner/src/config.ts</path>\n<type>file</type>\n<content>\n1: /** Configuration resolution for deterministic tool-result pruning. */\n2: \n3: import { deepFreeze } from '@deepseek-ai/dsh-util-values'\n4: import type { ResolvedConfig, ToolResultPruneConfig } from './types.ts'\n5: \n6: /** Fixed marker substituted for every removed middle span. */\n7: export const PRUNE_MARKER = '\\n\\n[... tool result middle pruned ...]\\n\\n'\n8: \n9: /** Low-friction defaults for coding-agent tool output. */\n10: export const DEFAULTS: ResolvedConfig = deepFreeze({\n11: thresholdChars: 8192,\n12: headChars: 4096,\n13: tailChars: 1024,\n14: })\n15: \n16: const CONFIG_KEYS: ReadonlySet<string> = new Set([\n17: 'thresholdChars',\n18: 'headChars',\n19: 'tailChars',\n20: ])\n21: \n22: /**\n23: * Count Unicode code points without splitting surrogate pairs.\n24: * @param text - text to measure.\n25: * @returns the Unicode code-point count.\n26: */\n27: export function codePointLength(text: string): number {\n28: return Array.from(text).length\n29: }\n30: \n31: /**\n32: * Resolve and validate pruning budgets.\n33: * @param config - raw plugin configuration.\n34: * @returns a detached deeply immutable configuration.\n35: */\n36: export function resolveConfig(config: ToolResultPruneConfig = {}): ResolvedConfig {\n37: for (const key of Object.keys(config)) {\n38: if (!CONFIG_KEYS.has(key)) {\n39: throw new Error(\n40: `ToolResultPruneConfig: unknown key \"${key}\" `\n41: + '(allowed: thresholdChars, headChars, tailChars)',\n42: )\n43: }\n44: }\n45: \n46: const resolved: ResolvedConfig = {\n47: thresholdChars: config.thresholdChars ?? DEFAULTS.thresholdChars,\n48: headChars: config.headChars ?? DEFAULTS.headChars,\n49: tailChars: config.tailChars ?? DEFAULTS.tailChars,\n50: }\n51: assertPositiveInteger('thresholdChars', resolved.thresholdChars)\n52: assertNonNegativeInteger('headChars', resolved.headChars)\n53: assertNonNegativeInteger('tailChars', resolved.tailChars)\n54: \n55: const emittedChars = resolved.headChars\n56: + codePointLength(PRUNE_MARKER)\n57: + resolved.tailChars\n58: if (emittedChars > resolved.thresholdChars) {\n59: throw new Error(\n60: `ToolResultPruneConfig: headChars + marker + tailChars (${emittedChars}) `\n61: + `must be at most thresholdChars (${resolved.thresholdChars})`,\n62: )\n63: }\n64: return deepFreeze(structuredClone(resolved))\n65: }\n66: \n67: function assertPositiveInteger(name: string, value: number): void {\n68: if (!Number.isInteger(value) || value <= 0) {\n69: throw new Error(`ToolResultPruneConfig: ${name} (${value}) must be a positive integer`)\n70: }\n71: }\n72: \n73: function assertNonNegativeInteger(name: string, value: number): void {\n74: if (!Number.isInteger(value) || value < 0) {\n75: throw new Error(`ToolResultPruneConfig: ${name} (${value}) must be a non-negative integer`)\n76: }\n77: }\n\n(End of file - total 77 lines)\n</content>"
}
],
"is_error": false
},
{
"type": "text",
"text": "You are now acting as a compaction engine for this AI coding assistant. Condense the conversation ABOVE into a structured checkpoint that lets another model resume the work with no loss of essential context.\n\nOutput EXACTLY the Markdown structure below: keep every section, in order. Use terse bullets, not prose paragraphs. Write \"(none)\" for an empty section — never drop a section.\n\n## Primary Request and Intent\n- [the user's original and evolving goals; quote verbatim where the exact wording matters]\n\n## Key Technical Concepts\n- [technologies, frameworks, patterns, and conventions in play]\n\n## Files and Code\n- [exact path: why it matters, key changes or snippets]\n\n## Errors and Fixes\n- [error: how it was resolved, plus any related user feedback]\n\n## Pending Jobs\n- [explicitly requested work not yet completed]\n\n## Current Work\n- [precisely what was in progress at this checkpoint]\n\n## Next Step\n- [the single next action, directly in line with the most recent request, or \"(none)\"]\n\n## Critical Context\n- [decisions and their rationale, constraints, user preferences, open questions, data needed to continue]\n\nRules:\n- Write concise English engineering prose. Preserve exact file paths, commands, error strings, identifiers, numeric values, function signatures, and syntax fragments.\n- Capture user feedback and explicit instructions faithfully, especially corrections.\n- Do NOT mention this summarization request or that the context was compacted.\n- Output only the checkpoint text: do not call any tool or take any other action.\n- If the conversation already contains a <compacted-summary> block, it is a PRIOR checkpoint. Do not copy it forward verbatim: preserve still-true facts, drop stale ones, and merge newer information into a single consolidated summary under the same structure."
}
]
}
],
"max_tokens": 65536,
"thinking": {
"type": "enabled"
},
"output_config": {
"effort": "high"
},
"system": "You are an AI agent powered by DeepSeek Harness.\n\nYou are a coding agent powered by the deepseek-flash model.\n\nUse the read tool — not shell commands like cat — to inspect text files. Use offset and limit to continue reading large files.",
"tools": [
{
"name": "read",
"description": "Read a UTF-8 text file and return line-numbered content.",
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to read, resolved by the filesystem backend."
},
"offset": {
"type": "number",
"description": "1-based first line to return. Defaults to 1."
},
"limit": {
"type": "number",
"description": "Maximum number of lines to return. Defaults to 2000."
}
},
"required": [
"file_path"
]
}
}
]
}
对照组:按 pi 格式拼的摘要请求体
{
"model": "deepseek-flash",
"max_tokens": 65536,
"thinking": {
"type": "enabled"
},
"output_config": {
"effort": "high"
},
"system": "You are a context summarization assistant. Your task is to read a conversation between a user and an AI assistant, then produce a structured summary following the exact format specified.\n\nDo NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "<conversation>\n[User]: Read packages/compaction/compaction-basic/src/summarizer.ts and packages/compaction/compaction-basic/src/region.ts. Explain in five bullets how a compaction request is assembled and how its result lands back in the session.\n\n[User]: <system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\n# Workspace notes\n\n- TypeScript monorepo: every package lives under `packages/<group>/<name>/src`.\n- This checkout is read-only. Do not try to edit or write files.\n- When you explain code, cite `path:line` for every claim.\n\n</system-reminder>\n\n[User]: Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"/tmp/dsh-lab/compaction/ws\".\n\n[Assistant]: I'll read both files.\n\n[Assistant tool calls]: read(file_path=\"packages/compaction/compaction-basic/src/summarizer.ts\"); read(file_path=\"packages/compaction/compaction-basic/src/region.ts\")\n\n[Tool result]: <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/src/summarizer.ts</path>\n<type>file</type>\n<content>\n1: /**\n2: * Default one-shot summarization and durable checkpoint framing.\n3: *\n4: * @module @deepseek-ai/dsh-compaction-basic/summarizer\n5: */\n6: \n7: import type { Context } from '@deepseek-ai/cordis'\n8: import { contentHasImage, BlockAssembler, LlmError } from '@deepseek-ai/dsh-llm'\n9: import { deepFreeze } from '@deepseek-ai/dsh-util-values'\n10: import type {\n11: ContentBlock, FinishReason, GenerateOptions, Message, RequestMessage, TokenUsage, ToolSchema,\n12: } from '@deepseek-ai/dsh-llm'\n13: import type { Agent } from '@deepseek-ai/dsh-agent'\n14: \n15: interface SummaryConfig {\n16: readonly summarizationProvider: string\n17: readonly summarizationModel: string\n18: readonly maxTokens: number\n19: }\n20: \n21: /** Tags wrapping the structured summary inside the landed checkpoint node. */\n22: const SUMMARY_OPEN_TAG = '<compacted-summary>'\n23: const SUMMARY_CLOSE_TAG = '</compacted-summary>'\n24: \n25: /**\n26: * The summarization directive, delivered as the FINAL user message after the\n27: * replayed conversation rather than as a distinct summarizer system prompt.\n28: * Keeping the conversation's own system prompt, tools, and message prefix in\n29: * front of it makes the auxiliary call a genuine prefix of the last routed\n30: * request, so the provider's KV cache is reused instead of invalidated.\n31: */\n32: const COMPACTION_INSTRUCTION = [\n33: 'You are now acting as a compaction engine for this AI coding assistant. Condense the conversation ABOVE into a structured checkpoint that lets another model resume the work with no loss of essential context.',\n34: '',\n35: 'Output EXACTLY the Markdown structure below: keep every section, in order. Use terse bullets, not prose paragraphs. Write \"(none)\" for an empty section — never drop a section.',\n36: '',\n37: '## Primary Request and Intent',\n38: \"- [the user's original and evolving g\n\n[... 3159 more characters truncated]\n\n[Tool result]: <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/src/region.ts</path>\n<type>file</type>\n<content>\n1: /**\n2: * Surface retention selection and the shared log-recorded compaction\n3: * transaction for automatic open-turn and manual idle-session compaction.\n4: *\n5: * @module @deepseek-ai/dsh-compaction-basic/region\n6: */\n7: \n8: import { randomUUID } from 'node:crypto'\n9: import { isDeepStrictEqual } from 'node:util'\n10: import {\n11: CompactionId,\n12: ManualCompactionError,\n13: compactCheckpointSource,\n14: toolPairingBalancedAfter,\n15: toolPairingBalancedBefore,\n16: } from '@deepseek-ai/dsh-compaction'\n17: import type { CompactionResult } from '@deepseek-ai/dsh-compaction'\n18: import type { CommandId } from '@deepseek-ai/dsh-commands/brand'\n19: import { createUserMessage, errorChain } from '@deepseek-ai/dsh-llm'\n20: import type { Message, UserMessage } from '@deepseek-ai/dsh-llm'\n21: import type { TokenMeasurement, TokenMeter } from '@deepseek-ai/dsh-token-meter'\n22: import { SessionSeq, type Session, type SessionEvent } from '@deepseek-ai/dsh-session'\n23: import type { Agent } from '@deepseek-ai/dsh-agent'\n24: import { frameSummary } from './summarizer.ts'\n25: import type { SummarizationInput, SummaryResult } from './summarizer.ts'\n26: interface RegionDependencies {\n27: readonly meter: TokenMeter\n28: summarize(input: SummarizationInput, agent: Agent, signal?: AbortSignal): Promise<SummaryResult>\n29: recover(error: unknown, agent: Agent, sourceEventSeqs: readonly SessionSeq[], signal?: AbortSignal): boolean\n30: }\n31: \n32: /** One validated inclusive span of current surface positions. */\n33: interface SurfaceSelection {\n34: readonly start: SessionSeq\n35: readonly end: SessionSeq\n36: readonly startIdx: number\n37: readonly endIdx: number\n38: readonly shadowedSeqs: readonly SessionSeq[]\n39: }\n40: \n41: /** A selection with its priced snapshot and the replay input built from it. */\n42: interface PreparedCompaction extends Sur\n\n[... 3159 more characters truncated]\n\n[Assistant]: Both files read. Here's how a compaction request is built and how its outcome returns to the session.\n\n- **Span selection & validation (the \"what to compact\" decision).** `selectCompactableRange` walks the priced surface backwards accumulating tokens until the `retainTokens` tail budget is met, then walks forward to the first tool-pairing-balanced boundary, never letting a `system/message` at node 0 into the range (`packages/compaction/compaction-basic/src/region.ts:117-155`, head exclusion at `region.ts:131`). `validateSurfaceRegion` then confirms both seqs exist on the surface, are ordered, and are balanced on both sides so no tool-call/result pair or open step is split (`region.ts:337-358`).\n\n- **Transaction opening and the compaction lock.** The owner is derived from the open turn, or `null` for manual idle compaction, and a `compaction/start` event with a fresh `CompactionId` is appended *before* any `await` — the durable marker itself is the lock, re-checked by `assertCompactionInactive` (`region.ts:184-210`, `region.ts:308-320`). `prepareCompaction` then prices the selected nodes with the meter, checks the surface hasn't drifted, and captures both the heuristic and route-priced totals (`region.ts:361-384`).\n\n- **Request assembly (replay-then-instruction).** `buildSummarizationInput` reconstructs the last routed request's cacheable prefix: the `system/message` head's derived message (if any), the header's tool schemas, then the shadowed region's events projected to messages in surface order (`region.ts:544-563`). The summarizer appends `COMPACTION_INSTRUCTION` as the **final user message** rather than using a separate summarizer system prompt, precisely so the call is a byte-prefix of the real request and hits the provider's KV cache (`summarizer.ts:25-67`, `summarizer.ts:145-151`); target provider/model resolves as config → last routed header → agent options (`summarizer.ts:127-142`) and the call goes out via `ctx.llm.stream()` with `toolHistory`, `purpose: 'compaction'`, the session id, `maxTokens`, and any abort signal (`summarizer.ts:152-163`).\n\n- **Result collection and fail-closed shaping.** Chunks feed a `BlockAssembler`; terminal `error`/`aborted` finish reasons become an `LlmError`, and `max-tokens` becomes a hard `MAX_TOKENS` error rather than a silently truncated checkpoint (`summarizer.ts:163-165`, `summarizer.ts:197-211`). The output is projected to text-only, rejecting any image block, and an empty summary throws (`summarizer.ts:167-171`, `summarizer.ts:214-220`). `SummaryResult` carries the safe `summary`, the raw output plus `llmStreamCall: true` marker, and provider usage (`summarizer.ts:87-107`, `summarizer.ts:172-180`).\n\n- **Landing back in the session.** `frameSummary` wraps the text in the preamble plus `<compacted-summary>` tags (`summarizer.ts:188-194`), `summarizeCompaction` turns that into a `UserMessage` tagged by `compactCheckpointSource` and refuses it if the framed estimate isn't strictly smaller than the span's route-priced total (`region.ts:410-422`), re-running summarization once if `recover` accepts the failure (`region.ts:396-409`). `commitCompactionBody` then appends `compaction/summary` (range, seqs, token count, provider/model, usage, raw output) followed by a `user/message` carrying `surfaceOp: { op: 'replace', startSeq, endSeq }` and `sourceEventSeqs`, which is what swaps the span for the checkpoint on the surface (`region.ts:491-509`); the bracket closes with `compaction/end` and `completeCompaction` attaches `endSeq` to the returned `CompactionResult` (`region.ts:237-239`, `region.ts:525-530`). A failed close leaves the unmatched start detectable for the next caller (`region.ts:240-251`).\n\n[User]: Now read packages/compaction/compaction-basic/src/config.ts. With a 1,000,000-token context window, a 256,000-token output reservation and default settings, what are thresholdTokens and retainTokens? Show the arithmetic.\n\n[Assistant tool calls]: read(file_path=\"packages/compaction/compaction-basic/src/config.ts\")\n\n[Tool result]: <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/src/config.ts</path>\n<type>file</type>\n<content>\n1: /**\n2: * Load-time validation and routed-model policy resolution for compaction-basic.\n3: *\n4: * @module @deepseek-ai/dsh-compaction-basic/config\n5: */\n6: \n7: import type { LlmCallConfig } from '@deepseek-ai/dsh-llm'\n8: import { deepFreeze } from '@deepseek-ai/dsh-util-values'\n9: import type {\n10: BasicCompactionConfig,\n11: CompactionPolicyConfig,\n12: ModelCompactPolicyConfig,\n13: ResolvedCompactSpec,\n14: ResolvedConfig,\n15: ResolvedRetention,\n16: ResolvedTargetPolicy,\n17: } from './types.ts'\n18: \n19: /** Default request-pressure fraction for every routed model. */\n20: const DEFAULT_THRESHOLD_RATIO = 0.8\n21: \n22: /** Default verbatim-tail fraction for every routed model. */\n23: const DEFAULT_RETAIN_RATIO = 0.16\n24: \n25: /** Fields shared by top-level defaults and exact-target overrides. */\n26: const POLICY_CONFIG_KEYS = [\n27: 'thresholdRatio',\n28: 'headroomTokens',\n29: 'retainRatio',\n30: 'retainTokens',\n31: 'summarizationProvider',\n32: 'summarizationModel',\n33: 'maxTokens',\n34: 'compactionRetries',\n35: 'maxOverflowRetries',\n36: ] as const\n37: \n38: /** Complete public top-level configuration key set. */\n39: const BASIC_COMPACT_CONFIG_KEYS: ReadonlySet<string> = new Set([\n40: ...POLICY_CONFIG_KEYS,\n41: 'modelPolicies',\n42: 'auto',\n43: ])\n44: \n45: /** Complete exact-target override key set. */\n46: const MODEL_POLICY_KEYS: ReadonlySet<string> = new Set([\n47: 'provider',\n48: 'model',\n49: ...POLICY_CONFIG_KEYS,\n50: ])\n51: \n52: /** Target-specific pressure configuration failure eligible for warning suppression. */\n53: export class TargetPressureConfigError extends Error {\n54: /**\n55: * @param targetKey - exact provider/model route used as the warning key.\n56: * @param message - actionable configuration failure detail.\n57: */\n58: constructor(readonly targetKey: string, message: string) {\n59: \n\n[... 3159 more characters truncated]\n\n[Assistant]: The middle of the file was pruned — I need the resolution arithmetic.\n\n[Assistant tool calls]: read(file_path=\"packages/compaction/compaction-basic/src/config.ts\", offset=100, limit=200)\n\n[Tool result]: <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-basic/src/config.ts</path>\n<type>file</type>\n<content>\n100: headroomTokens,\n101: ...retention,\n102: summarizationProvider: config.summarizationProvider ?? '',\n103: summarizationModel: config.summarizationModel ?? '',\n104: maxTokens,\n105: compactionRetries: config.compactionRetries ?? 1,\n106: maxOverflowRetries: config.maxOverflowRetries ?? 1,\n107: modelPolicies,\n108: auto: config.auto ?? true,\n109: })\n110: }\n111: \n112: /**\n113: * Merge the exact provider/model override over the validated default policy.\n114: * @param config - validated service defaults and override table.\n115: * @param target - exact durable provider/model route to match.\n116: * @returns detached immutable policy before model-capacity scaling.\n117: */\n118: export function resolveTargetPolicy(\n119: config: ResolvedConfig,\n120: target: Pick<LlmCallConfig, 'provider' | 'model'>,\n121: ): ResolvedTargetPolicy {\n122: const override = config.modelPolicies.find(policy => (\n123: policy.provider === target.provider && policy.model === target.model\n124: ))\n125: const inheritedRetention: ResolvedRetention = config.retainTokens === undefined\n126: ? { retainRatio: config.retainRatio }\n127: : { retainTokens: config.retainTokens }\n128: return deepFreeze({\n129: target: { provider: target.provider, model: target.model },\n130: thresholdRatio: override?.thresholdRatio ?? config.thresholdRatio,\n131: headroomTokens: override?.headroomTokens ?? config.headroomTokens,\n132: ...resolveRetention(override ?? {}, inheritedRetention),\n133: summarizationProvider: override?.summarizationProvider ?? config.summarizationProvider,\n134: summarizationModel: override?.summarizationModel ?? config.summarizationModel,\n135: maxTokens: override?.maxTokens ?? config.maxTokens,\n136: compactionRetries: override?.compactionRetries ?? config.compactionRetries,\n137: maxOverflowRetri\n\n[... 3159 more characters truncated]\n\n[Assistant]: Defaults confirmed: `thresholdRatio` 0.8 (`config.ts:20`), `retainRatio` 0.16 (`config.ts:23`), `headroomTokens` 65,536 (`config.ts:75`). Since the default is a ratio, not a fixed `retainTokens`, the ratio path applies (`config.ts:195-197`); `reservedCompletionTokens` is an input supplied by the adapter, distinct from `maxTokens` (`config.ts:150-156`).\n\n**Arithmetic** (`resolveCompactSpec`, `config.ts:172-197`):\n\n```\nmessageBudgetTokens = contextWindow − reservedCompletionTokens\n = 1,000,000 − 256,000\n = 744,000 (config.ts:172)\n\npressureBudgetTokens = messageBudgetTokens − headroomTokens\n = 744,000 − 65,536\n = 678,464 (config.ts:181)\n\nthresholdTokens = floor( min( contextWindow × thresholdRatio,\n pressureBudgetTokens ) )\n = floor( min( 1,000,000 × 0.8, 678,464 ) )\n = floor( min( 800,000, 678,464 ) )\n = 678,464 (config.ts:191-194)\n\nretainTokens = floor( messageBudgetTokens × retainRatio ) [ratio default]\n = floor( 744,000 × 0.16 )\n = floor( 119,040.0 )\n = 119,040 (config.ts:195-197)\n```\n\n- **`thresholdTokens` = 678,464** — note the headroom cap binds, not the nominal 80% figure: 800,000 > 678,464, so the budget after the 256,000 output reservation and 65,536 headroom is the real ceiling. A larger reservation or headroom would silently dominate the ratio here.\n- **`retainTokens` = 119,040** — scaled against `messageBudgetTokens` (744,000), *not* the raw 1,000,000 window (which would have given 160,000), because retention is clamped to the message budget before headroom is deducted (`config.ts:145-146`, `config.ts:196`).\n\nThe result is valid: `retainTokens` (119,040) < `thresholdTokens` (678,464), so the guard at `config.ts:198-204` passes, and the load-time ratio check `0.16 < 0.8` passes at `config.ts:235-240`. Both budgets are returned under `deepFreeze` alongside `contextWindow` and the ratios (`config.ts:205-216`).\n\nOne caveat on literals: there is no rounding loss here, but `Math.floor` on both means a non-integer context window or an odd window/ratio pairing could shave up to 1 token off each figure rather than rounding to nearest.\n\n[User]: Read packages/compaction/compaction-tool-result-pruner/src/index.ts and packages/compaction/compaction-tool-result-pruner/src/config.ts. When does the pruner run relative to summarization, and what exactly does a pruned tool result look like?\n\n[Assistant tool calls]: read(file_path=\"packages/compaction/compaction-tool-result-pruner/src/index.ts\"); read(file_path=\"packages/compaction/compaction-tool-result-pruner/src/config.ts\")\n\n[Tool result]: <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-tool-result-pruner/src/index.ts</path>\n<type>file</type>\n<content>\n1: /**\n2: * Replay-safe, model-free tool-result pruning service.\n3: *\n4: * @module @deepseek-ai/dsh-compaction-tool-result-pruner\n5: */\n6: \n7: import { Context, Service } from '@deepseek-ai/cordis'\n8: import z from '@deepseek-ai/schemastery'\n9: import { freezeMessage } from '@deepseek-ai/dsh-llm'\n10: import type { ContentBlock } from '@deepseek-ai/dsh-llm'\n11: import type { Session, SessionEvent, SessionSeq, ToolResultMessage } from '@deepseek-ai/dsh-session'\n12: // Type-only: the `compaction/*` SessionEventMap merges (the shadow-price event).\n13: import type {} from '@deepseek-ai/dsh-compaction'\n14: // Type-only: the `ctx.tokenMeter` Context merge for the declared injection.\n15: import type {} from '@deepseek-ai/dsh-token-meter'\n16: import { codePointLength, DEFAULTS, PRUNE_MARKER, resolveConfig } from './config.ts'\n17: import type {\n18: PrunedEntry,\n19: PruneResult,\n20: ResolvedConfig,\n21: ToolResultPruneConfig,\n22: } from './types.ts'\n23: \n24: export { codePointLength, DEFAULTS, PRUNE_MARKER, resolveConfig } from './config.ts'\n25: export type {\n26: PrunedEntry,\n27: PruneResult,\n28: ResolvedConfig,\n29: ToolResultPruneConfig,\n30: } from './types.ts'\n31: \n32: declare module '@deepseek-ai/cordis' {\n33: interface Context {\n34: toolResultPruner: ToolResultPruner\n35: }\n36: }\n37: \n38: interface SnapshotCandidate {\n39: readonly seq: SessionSeq\n40: readonly event: SessionEvent<'tool/result'>\n41: }\n42: \n43: /** Deterministic head/middle/tail pruning for current tool-result surface nodes. */\n44: export class ToolResultPruner extends Service {\n45: // The token meter prices each shadowed node for its logged shadow-price\n46: // event, so pruning genuinely requires the pricing capability.\n47: static inject = ['tokenMeter']\n48: \n49: static Config: z<ToolResultPruneConfig> = z.object({\n50: thresholdChars: z.n\n\n[... 3159 more characters truncated]\n\n[Tool result]: <path>/tmp/dsh-lab/compaction/ws/packages/compaction/compaction-tool-result-pruner/src/config.ts</path>\n<type>file</type>\n<content>\n1: /** Configuration resolution for deterministic tool-result pruning. */\n2: \n3: import { deepFreeze } from '@deepseek-ai/dsh-util-values'\n4: import type { ResolvedConfig, ToolResultPruneConfig } from './types.ts'\n5: \n6: /** Fixed marker substituted for every removed middle span. */\n7: export const PRUNE_MARKER = '\\n\\n[... tool result middle pruned ...]\\n\\n'\n8: \n9: /** Low-friction defaults for coding-agent tool output. */\n10: export const DEFAULTS: ResolvedConfig = deepFreeze({\n11: thresholdChars: 8192,\n12: headChars: 4096,\n13: tailChars: 1024,\n14: })\n15: \n16: const CONFIG_KEYS: ReadonlySet<string> = new Set([\n17: 'thresholdChars',\n18: 'headChars',\n19: 'tailChars',\n20: ])\n21: \n22: /**\n23: * Count Unicode code points without splitting surrogate pairs.\n24: * @param text - text to measure.\n25: * @returns the Unicode code-point count.\n26: */\n27: export function codePointLength(text: string): number {\n28: return Array.from(text).length\n29: }\n30: \n31: /**\n32: * Resolve and validate pruning budgets.\n33: * @param config - raw plugin configuration.\n34: * @returns a detached deeply immutable configuration.\n35: */\n36: export function resolveConfig(config: ToolResultPruneConfig = {}): ResolvedConfig {\n37: for (const key of Object.keys(config)) {\n38: if (!CONFIG_KEYS.has(key)) {\n39: throw new Error(\n40: `ToolResultPruneConfig: unknown key \"${key}\" `\n41: + '(allowed: thresholdChars, headChars, tailChars)',\n42: )\n43: }\n44: }\n45: \n46: const resolved: ResolvedConfig = {\n47: thresholdChars: config.thresholdChars ?? DEFAULTS.thresholdChars,\n48: headChars: config.headChars ?? DEFAULTS.headChars,\n49: tailChars: config.tailChars ?? DEFAULTS.tailChars,\n50: }\n51: assertPositiveInteger('thresholdChars', resolved.thresholdChars)\n52: assertNonNegativeInteger('headChars', resolved.h\n\n[... 1032 more characters truncated]\n</conversation>\n\nThe messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.\n\nUse this EXACT format:\n\n## Goal\n[What is the user trying to accomplish? Can be multiple items if the session covers different tasks.]\n\n## Constraints & Preferences\n- [Any constraints, preferences, or requirements mentioned by user]\n- [Or \"(none)\" if none were mentioned]\n\n## Progress\n### Done\n- [x] [Completed tasks/changes]\n\n### In Progress\n- [ ] [Current work]\n\n### Blocked\n- [Issues preventing progress, if any]\n\n## Key Decisions\n- **[Decision]**: [Brief rationale]\n\n## Next Steps\n1. [Ordered list of what should happen next]\n\n## Critical Context\n- [Any data, examples, or references needed to continue]\n- [Or \"(none)\" if not applicable]\n\nKeep each section concise. Preserve exact file paths, function names, and error messages."
}
]
}
]
}
-
@earendil-works/pi-coding-agent1.3.0 的dist/core/compaction/compaction.js与utils.js(SUMMARIZATION_SYSTEM_PROMPT、serializeConversation),2026-10-20 读;对照组照抄了它的两段提示词。 ↩ -
dsh 的决策记录摘要调用回放对话前缀以复用 KV Cache,2026-10-20 读。 ↩
-
dsh 的决策记录压缩检查点使用英语工程文体,2026-10-20 读。 ↩