Appearance
功能规格说明:消息压缩
创建日期:2026-01-22
更新日期:2026-08-28
用户场景与测试 (必填)
用户故事:自动历史压缩(优先级:P1)
作为 AI 代理,当对话历史变得过长时,我希望自动总结较旧的消息,以便保持在模型的 token 限制内同时保持上下文。
为什么是这个优先级:对长时间运行的会话至关重要,以防止"超出上下文窗口"错误并降低成本。
独立测试:可以模拟 token 使用超过阈值并验证 AIManager 触发压缩周期并用摘要块替换旧消息。
验收场景:
- 假设估算的上下文 token 数超过
getMaxInputTokens(),当发送下一个 API 请求前执行检查时,则代理必须识别要压缩的消息并在请求发出前触发压缩 - 假设消息被识别用于压缩,当总结完成时,则原始消息必须被
compress块替换,后跟旧消息列表的最后 2 个 API 轮次 - 假设存在
compress块,当向 API 发送消息时,则它必须被转换为 user 消息(匹配 Claude Code 的自动压缩行为)
用户故事:请求前自动压缩触发(对齐 Claude Code)(优先级:P1)
作为 AI 代理,我希望在发送 API 请求之前基于估算的上下文大小判断是否压缩,而不是在响应返回后才发现超限,以便超限请求永远不会真正发出。
为什么是这个优先级:当前实现是在每次 API 响应返回后检查该次响应的 usage,超限时才压缩——判断慢一拍,导致输入超过 maxInputTokens 的请求真实发出(可能直接撞上模型 context length / 413 错误)。对齐 Claude Code 的 proactive 机制:请求前用「上次响应的真实 usage + 其后新增消息的估算」预测本次请求的上下文大小,超过阈值先压缩再发。
独立测试:模拟一次巨大工具输出后,验证下一次请求发出前即触发压缩(而非等响应返回);模拟 fork 路径(压缩、auto-memory 提取),验证不触发自动压缩;验证压缩完成后同一轮循环内不再重复触发。
验收场景:
- 假设存在上次 API 响应的
usage,当构造本次请求前执行上下文估算时,则估算值 = 上次响应的total_tokens+ 上次响应之后新增消息的字符估算(CJK 感知,与estimateTokens一致) - 假设估算的上下文 token 数超过
getMaxInputTokens(),当发送请求前检查时,则必须触发压缩,超限请求不得发出 - 假设估算的上下文 token 数未超过阈值,当发送请求前检查时,则请求正常发出,不触发压缩
- 假设agent 循环中一轮工具调用结束、下一轮请求将要发出,当发送请求前检查时,则每次请求前(包括工具调用后的后续轮次)都必须重新估算并检查
- 假设当前请求是压缩 fork 或 auto-memory 提取等 fork 路径,当发送请求前检查时,则必须跳过自动压缩判断(fork 继承完整历史,触发压缩会造成递归死锁)
- 假设压缩刚完成、同一轮 agent 循环继续,当发送请求前检查时,则不得基于压缩前的估算值再次触发压缩(压缩结果已保证在阈值内)
- 假设上次响应后的新消息估算与真实 token 数存在偏差,当请求真实发出后返回 413 / context length 错误时,则走既有错误处理路径(
invalid_request+ 友好错误消息),不引入新的响应后主动压缩检查
用户故事:压缩触发仅使用 total_tokens(去除缓存 token 双重计数)(优先级:P1)
作为 AI 代理,我希望自动压缩的判断只依据 total_tokens(已包含缓存命中 token),不再叠加 cache_read_input_tokens 和 cache_creation_input_tokens,以便上下文占用度量准确,压缩不会过早触发。
为什么是这个优先级:wave 走 OpenAI 兼容接口(OpenAI SDK + 网关),该格式下 prompt_tokens = 缓存命中 + 未命中、total_tokens = prompt_tokens + completion_tokens,缓存命中已计入 total_tokens(DeepSeek 官方文档明确 prompt_tokens 等于 prompt_cache_hit_tokens + prompt_cache_miss_tokens)。原实现把 prompt_tokens_details.cached_tokens 规范化后与 total_tokens 相加,造成缓存命中双重计数——缓存命中通常占上下文绝大部分,导致压缩过早触发(实际上下文远未到 maxInputTokens 就压缩)。该加法源自 Anthropic 原生格式(其 input_tokens 不含缓存字段),与 wave 实际使用的 OpenAI 兼容格式错配。
独立测试:构造含 cache_read_input_tokens(大值)和 cache_creation_input_tokens 的 usage,验证压缩判断仅使用 total_tokens,缓存字段不参与判断;验证 latestTotalTokens 展示语义同样仅使用 total_tokens(与压缩判断口径一致,缓存字段不参与展示)。
验收场景:
- 假设响应的
usage包含total_tokens、cache_read_input_tokens、cache_creation_input_tokens,当判断是否超过getMaxInputTokens()时,则只使用total_tokens(OpenAI 兼容格式下已含缓存命中),缓存字段不得叠加 - 假设响应仅含
cache_read_input_tokens(缓存命中极大)而total_tokens远低于阈值,当判断是否触发压缩时,则不得触发压缩 - 假设
usage中total_tokens未超阈值但叠加缓存字段后超过,当判断是否触发压缩时,则不得触发压缩 - 假设
latestTotalTokens展示(CLI / webview 的 token 用量显示),当展示 token 使用量时,则同样只使用total_tokens,缓存字段不叠加——与压缩触发判断口径一致,避免 UI 显示"100% context"而压缩迟迟不触发
用户故事:压缩熔断器(优先级:P2)
作为 AI 代理,当压缩反复失败时,我希望停止尝试压缩,以避免在不可恢复的上下文状态上浪费 API 调用。
为什么是这个优先级:当上下文损坏时防止级联故障和不必要的 API 成本。
独立测试:可以模拟 3 次连续压缩失败并验证第 4 次高 token 使用轮次完全跳过压缩。
验收场景:
- 假设压缩已连续失败 3 次,当token 使用再次超过阈值时,则必须跳过压缩并记录警告
- 假设压缩成功,当下一次压缩周期运行时,则连续失败计数器必须重置为 0
用户故事:压缩 fork 代理循环(优先级:P1)
作为 AI 代理,当触发压缩时,我希望在与主对话参数完全一致的 fork 代理循环中生成摘要,使压缩请求命中主对话缓存,且模型无法通过工具调用逃避输出文本摘要。
为什么是这个优先级:压缩请求包含整个对话历史,与主对话保持缓存前缀一致可大幅降低成本;在协议层拒绝工具调用是防止模型以工具调用代替文本摘要的结构性手段(弱模型在纯文本压缩调用中可能将工具调用语法混入正文)。对齐 Claude Code 的 fork 压缩路径。
独立测试:模拟压缩触发,检查 fork 请求的模型/系统提示/工具集/生成参数与主对话一致;模拟模型返回 tool_calls,验证工具未执行、拒绝结果被回灌、循环继续直至文本输出或轮次上限。
验收场景:
- 假设压缩被触发,当构造 fork 请求时,则必须使用主模型(非快速模型),且系统提示、工具集、生成参数与主对话请求完全一致,压缩指令作为末尾 user 消息追加,使命中主对话缓存前缀
- 假设fork 循环中模型返回 tool_calls,当处理该响应时,则任何工具都不得执行,必须向模型回灌"压缩期间不允许使用工具"的拒绝结果并继续循环
- 假设fork 循环达到最大轮次(3 轮)仍无文本输出,当循环结束时,则必须判定压缩失败并计入连续失败计数(最大轮次偏离 Claude Code 的 1 轮:deny 回路内自纠借助缓存命中成本极低)
- 假设构造 fork 请求,当设置生成参数时,则不得显式覆盖最大输出 token 数或 thinking 配置,必须与主对话保持一致(避免缓存键失配,对齐 Claude Code fork 路径)
- 假设压缩指令被构造,当作为 user 消息追加时,则必须以"仅输出文本、禁止调用任何工具、工具调用将被拒绝并导致任务失败"的强制声明开头,并在末尾重申该约束
用户故事:压缩摘要格式提取(优先级:P1)
作为 AI 代理,当压缩模型返回文本时,我希望按约定格式提取摘要正文,以便进入上下文的压缩结果干净、无分析草稿等包装内容。
为什么是这个优先级:压缩提示词要求模型在 <analysis> 标签中打草稿、在 <summary> 标签中输出正文;提取规则保证只有摘要正文进入上下文。行为严格对齐 Claude Code 的 formatCompactSummary,不引入其没有的内容级校验。
独立测试:模拟压缩模型返回带 <analysis>/<summary> 标签、不带标签、含连续空行等不同输出,验证进入上下文的摘要符合提取规则。
验收场景:
- 假设压缩模型输出包含
<analysis>块和<summary>块,当摘要被应用时,则必须剥离<analysis>块,仅保留<summary>标签内的内容 - 假设压缩模型输出不包含
<summary>标签,当摘要被应用时,则原始文本必须原样透传,不做格式拒绝或重试 - 假设压缩模型输出包含连续多个空行,当摘要被应用时,则必须压缩为单个空行并去除首尾空白
用户故事:压缩后上下文恢复(优先级:P2)
作为 AI 代理,在压缩替换对话历史后,我希望重要上下文被重新注入,以便我可以继续工作而不丢失对文件、目录、计划模式、技能和后台任务的追踪。
为什么是这个优先级:防止代理在压缩后丢失关键环境上下文。
独立测试:可以验证压缩后摘要包含最近文件读取、工作目录、计划模式状态、可用技能和后台任务状态的各个部分。
验收场景:
- 假设压缩产生摘要,当摘要被应用时,则它必须增强最近文件读取内容(最多 5 个文件,每个 5000 token)
- 假设压缩被应用,当上下文恢复运行时,则当前工作目录必须被包含
- 假设代理处于计划模式,当上下文恢复运行时,则计划文件路径和存在状态必须被包含
- 假设技能已被调用,当上下文恢复运行时,则必须重新注入该技能的正文与描述——正文取模型当时实际看到的那一份(已替换
${WAVE_SKILL_DIR}等目录占位符、首行含Base directory for this skill: <绝对路径>);记录里拿不到该内容时按技能文件重新渲染,且同样完成同样的替换与首行——不得把${WAVE_SKILL_DIR}这类占位符原样注入,也不得注入未解析的 frontmatter 语法(如>-) - 假设后台任务正在运行,当上下文恢复运行时,则每个代理的描述和状态必须被列出
用户故事:压缩后计划模式提醒保留(优先级:P2)
作为在长时间会话中在计划模式下工作的用户,我希望在对话压缩后重新注入计划模式指令,以便代理即使在历史被摘要替换后仍保持其只读约束和工作流指导。
为什么是这个优先级:如果不重新注入,压缩会从对话历史中移除所有计划模式 <system-reminder> 消息。代理将不知道计划模式约束,可能尝试编辑文件或采取计划文件之外的操作。
独立测试:在计划模式下触发压缩,然后验证完整的计划模式 <system-reminder> 提醒在压缩后作为用户消息注入,且代理继续遵守只读约束。
验收场景:
- 假设代理处于计划模式且发生压缩,当压缩摘要替换对话历史时,则完整的计划模式
<system-reminder>必须在下一个 API 请求中作为用户消息注入 - 假设代理处于计划模式且发生压缩,当计划文件存在时,则重新注入的计划模式提醒必须包含计划文件路径和存在状态
- 假设代理不在计划模式,当发生压缩时,则不注入计划模式提醒
- 假设代理处于计划模式且发生压缩,当重新注入的提醒是压缩后的第一个提醒时,则它必须是完整指令(非稀疏),因为所有先前的提醒已被压缩移除
用户故事:手动 /compact 命令(优先级:P2)
作为用户,我希望手动触发对话压缩并提供可选的自定义指令,以便我可以主动减少上下文使用并将摘要集中在对话的特定方面。
为什么是这个优先级:给予用户对上下文管理的控制,支持在自动压缩触发之前主动压缩,并允许自定义摘要重点。
独立测试:输入 /compact 或 /compact focus on the bug fix discussion,验证压缩发生且可选指令被应用于摘要。
验收场景:
- 假设对话中有消息,当用户输入
/compact时,则对话历史必须立即被压缩,无论 token 使用情况 - 假设对话中有消息,当用户输入
/compact focus on the API design时,则自定义指令必须被传递给压缩 API 调用并影响摘要 - 假设AI 响应正在进行中,当用户输入
/compact时,则/compact被忽略:AI 处理不被中止,对话历史保持不变;AI 空闲时/compact才执行压缩 - 假设压缩已在进行中,当用户再次输入
/compact时,则第二次压缩必须被跳过(熔断器) - 假设CLI 中已有一段包含多条消息的对话,当
/compact压缩完成,则终端屏幕保留压缩前的旧消息,压缩摘要块追加在消息列表末尾(不替换、不清除旧消息);消息总数超过 CLI 渲染上限(折叠 30 条 / 展开 10 条)时只渲染最近的上限条数
用户故事:压缩后旧消息在 UI 保留渲染(对齐 Claude Code 桌面端与 VS Code 插件)(优先级:P1)
作为用户,我希望压缩(自动或手动)之后,压缩之前的消息仍然渲染在界面上,压缩摘要以块的形式追加在消息列表末尾,以便我能回看被压缩掉的历史对话,而不必依赖重新加载会话。
为什么是这个优先级:webview 已支持虚拟列表(always-on),渲染任意数量的历史消息无性能问题;对齐 Claude Code 桌面端与 VS Code 插件的行为(压缩后保留全部旧消息,摘要以块展示,boundary 块位于压缩发生时列表末尾);CLI 因终端渲染性能限制只渲染最近 N 条消息(现有折叠 30 / 展开 10 上限),但内存与转录仍保留全量。
独立测试:模拟压缩发生,验证 UI 消息列表保留压缩前的全部旧消息、压缩摘要块追加在列表末尾;验证发给 API 的上下文仍从最后压缩边界开始(压缩对模型上下文的效果不变)。
验收场景:
- 假设自动压缩发生,当压缩完成时,则 UI 消息列表保留压缩前的全部旧消息,压缩摘要块追加在消息列表末尾,旧消息不得从 UI 移除
- 假设手动
/compact发生,当压缩完成时,则 UI 行为与自动压缩一致:旧消息保留、摘要块追加在列表末尾 - 假设压缩后对话继续,当新消息到达时,则新消息追加在压缩摘要块之后
- 假设同一会话发生多次压缩,当 UI 渲染时,则每个压缩摘要块按压缩发生的先后顺序保留在列表中(越早压缩的摘要块越靠前)
- 假设压缩完成,当构造下一个 API 请求时,则上下文仍从最后压缩边界开始,压缩前的旧消息不进入请求(压缩对模型上下文的效果不变)
- 假设CLI 中压缩后的消息总数超过渲染上限(折叠 30 / 展开 10),当渲染时,则只渲染最近的上限条数,内存与转录保留全量
用户故事:加载历史会话渲染全部消息(优先级:P1)
作为用户,我希望恢复历史会话时,压缩之前的消息也一并渲染(含历史压缩摘要块),以便我能看到完整会话,而不只是最后一次压缩之后的片段。
为什么是这个优先级:转录文件是 append-only(压缩时追加摘要块而非删除旧消息),加载时不应截断丢弃压缩前消息;webview 虚拟列表支撑全量渲染;对齐 Claude Code 桌面端(正常大小转录全量加载,仅超大转录才丢弃压缩边界前消息)。
独立测试:构造含压缩摘要块及其之前旧消息的转录文件,恢复会话后验证 UI 渲染全部消息,且 API 上下文仍从最后压缩边界开始。
验收场景:
- 假设转录文件包含压缩摘要块及其之前的旧消息,当恢复该会话时,则 UI 渲染全部消息:压缩前旧消息保留、摘要块保留
- 假设恢复的会话含多个压缩摘要块,当渲染时,则所有摘要块及各自之前的旧消息都渲染
- 假设恢复会话后用户发送新消息,当构造 API 请求时,则上下文从最后压缩摘要块开始,压缩前的旧消息不进入请求
- 假设会话发生过压缩,当压缩写入转录时,则保留的最后 API 轮次不得重复写盘(它们压缩前已写入;对齐 Claude Code 的 dedup-skipped),转录中每条消息唯一,加载无需去重即可全量渲染
- 假设CLI 恢复超长会话(消息数超过渲染上限),当渲染时,则只渲染最近的上限条数,不崩溃且性能可接受
用户故事:压缩过程中的 CLI 流式尾部提示(优先级:P2)
作为 CLI 用户,我希望压缩(手动 /compact 或自动触发)进行期间,在 "✻ Compacting" 加载提示后看到当前流式传输文本的最后 30 个字符,以便我知道压缩摘要正在生成并能实时看到输出进度。
为什么是这个优先级:压缩 fork 以流式模式请求(为避免网关空闲超时),但此前流式增量被静默消费、不向界面透出;在加载提示后展示流式尾部(与主对话/btw 的流式尾部样式一致)能让用户区分"正在思考"与"已经卡住",无需等待压缩结束。
独立测试:模拟压缩 fork 的 onContentUpdate/onReasoningUpdate 回调,验证回调内容被转发给 CLI 加载提示;验证超过 30 字符时仅显示 … 前缀截断的最后 30 个字符。
验收场景:
- 假设压缩进行中(手动或自动触发)且模型尚无任何流式输出,则 CLI 在消息列表下方显示 "✻ Compacting",不显示流式尾部。
- 假设压缩进行中且模型已输出文本(普通文本或推理内容,推理经内容通道混合),则 CLI 在 "✻ Compacting" 后显示该文本的最后 30 个字符;文本不超过 30 字符时原样显示,超过 30 字符时以
…前缀截断(换行折叠为\n,与主对话流式尾部样式一致)。 - 假设压缩完成(
compactionStateChange为 false),则 加载提示与流式尾部一并消失,旧消息保留在消息列表中,压缩摘要块追加在列表末尾。
用户故事:PreCompact 和 PostCompact 钩子事件(优先级:P2)
作为开发者,我希望配置在对话压缩之前和之后运行的钩子,以便我可以自定义压缩行为并以编程方式响应压缩后的摘要。
为什么是这个优先级:支持自定义压缩过程并允许下游系统响应对话摘要,但对基本功能不是关键的。
独立测试:配置 PreCompact 和 PostCompact 钩子,通过 /compact 命令或自动压缩触发压缩,验证 PreCompact 钩子在压缩前执行,PostCompact 钩子在压缩后执行。
验收场景:
- 假设配置了 PreCompact 钩子,当压缩被触发时,则钩子必须在压缩 API 调用之前执行,如果提供了自定义指令则在 JSON 输入中接收
compact_instructions,其 stdout 必须作为额外指令合并 - 假设配置了 PostCompact 钩子,当压缩成功完成时,则钩子必须在压缩 API 调用之后执行并在 JSON 输入中接收包含 AI 生成摘要的
compact_summary - 假设PreCompact 钩子返回退出码 2,当钩子完成时,则错误必须显示给用户但压缩必须继续(非阻塞)
- 假设PostCompact 钩子返回退出码 2,当钩子完成时,则错误必须显示给用户但执行必须继续(非阻塞)
- 假设同时配置了 PreCompact 和 PostCompact 钩子,当压缩被触发时,则PreCompact 必须先运行,然后压缩发生,然后 SessionStart 钩子运行,最后 PostCompact 运行
用户故事:手动压缩与压缩状态内联提示(优先级:P2)
作为 IDE 插件(VS Code 扩展与 JetBrains 插件)与桌面端用户,我希望能在客户端中手动触发对话压缩(/compact),并在压缩过程中于消息列表末尾的闪烁光标后看到"正在压缩对话"提示,压缩完成后提示自动消失,以便我主动管理上下文并实时知晓压缩状态,且不被通知弹窗或插入的系统消息打扰。
为什么是这个优先级:IDE 插件与桌面端以 stdio 子进程方式运行 Agent,无法直接调用 CLI 内部命令;若不做协议适配,/compact 会被当作普通消息发送给模型而失效。压缩可能耗时数秒,在共享 webview 的闪烁光标后内联展示状态提示,让三端行为一致,避免原生通知、聊天内系统消息等不同形式造成的体验割裂。VS Code 扩展、JetBrains 插件与桌面端共享同一 webview 包,三者行为必须一致。
独立测试:在三端分别输入 /compact,验证通过 stdio compact 请求触发压缩而非发送普通消息;模拟 compactionStateChange 通知,验证开始时 webview 在消息列表末尾的闪烁光标后显示压缩提示,完成时提示消失,且全程不出现原生通知弹窗,消息列表也不新增系统消息;模拟 compactionContentUpdate 通知,验证提示后显示流式尾部(最后 30 字符、… 前缀截断、换行折叠为 \n)。
验收场景:
- 假设对话中有消息,当用户在 IDE 插件或桌面端中输入
/compact时,则客户端必须通过 stdiocompact请求触发压缩,而非将文本作为普通消息发送给模型 - 假设用户输入
/compact focus on the API design,当请求发出时,则自定义指令必须作为customInstructions传递给compact请求并影响摘要 - 假设压缩开始(手动或自动触发),当子进程发送
compactionStateChange(isCompacting=true)时,则webview 必须在消息列表末尾的闪烁光标后显示"正在压缩对话"提示,并在压缩期间保持显示 - 假设压缩进行中且模型已输出文本,当子进程发送
compactionContentUpdate(累积的流式文本)时,则webview 必须在压缩提示后显示该文本的最后 30 个字符(换行折叠为\n,超过 30 字符以…前缀截断,与 CLI/btw 的流式尾部样式一致);模型尚无输出时不显示尾部 - 假设压缩完成,当子进程发送
compactionStateChange(isCompacting=false)时,则webview 必须移除该提示(含流式尾部) - 假设压缩已在进行中,当用户再次输入
/compact时,则第二次压缩必须被跳过(复用压缩熔断器) - 假设压缩状态变化,当宿主收到
compactionStateChange通知时,则宿主不得显示原生通知弹窗,也不得向消息列表插入系统消息 - 假设桌面端处于多窗格(分屏)状态,当某个会话窗格触发压缩时,则压缩提示(含流式尾部)必须仅显示在该窗格中
用户故事:压缩期间的会话保护(切换禁用 + SDK 写入兜底)(优先级:P1)
作为用户,我希望压缩进行期间「历史对话」切换与「新建对话 / /clear」像 streaming/后台任务运行时一样被禁用或忽略,以便我不会在压缩途中离开当前会话;同时即使某条路径绕过了 UI 守卫(状态通知在途的竞态窗口、CLI TUI 的 /clear、未来新增的切换路径),压缩摘要也绝不写进别的会话。
为什么是这个优先级:压缩是一次耗时数秒的 LLM 调用(fork),期间 IDE 插件的「历史对话」切换(restoreSession)和 /clear 都不被禁止;而压缩完成后的写入阶段直接操作 messageManager 的当前状态——若会话已被切走,摘要块会被追加进切换后会话的内存消息列表,并持久化写进其转录文件(PM 缺陷 3478352534577408:会话 2 执行 /compact,切到会话 3 后「对话已压缩」块出现在会话 3 中)。这是数据污染级缺陷:污染目标会话的 UI、转录与后续轮次的模型上下文。守卫分两层:webview 层与 streaming/后台任务共用同一守卫位(用户拍板口径),SDK 写入层兜底防竞态与非 webview 路径——CLI TUI、VS Code、JetBrains 与桌面端共享同一 SDK 路径,兜底必须在写入点统一执行。
独立测试:UI 层——向 webview 推送 compactionStateChange(true) 后点击历史会话与「新建对话」,验证不发 restoreSession/clearChat 且按钮禁用,回落 false 后恢复;SDK 层——在压缩 LLM 调用挂起期间把 messageManager 切到另一会话(变更 sessionId),放行压缩完成,验证不追加摘要块、不写转录、不触发 onCompactBlockAdded,且压缩状态仍正常回落为 false;对照组(会话未切换)验证行为与现状一致。
验收场景:
- 假设压缩正在进行,当用户在 webview 点击「历史对话」中的另一会话时,则切换被忽略(不发
restoreSession),与 streaming/后台任务的守卫行为一致 - 假设压缩正在进行,当用户点击「新建对话」按钮或输入
/clear时,则被忽略(不发clearChat),且「新建对话」按钮禁用 - 假设压缩结束(
compactionStateChange回落 false),当用户再点击历史会话或「新建对话」时,则恢复正常可用(守卫解除) - 假设压缩 LLM 调用挂起期间会话被切走(
restoreSession使 sessionId 变化),当压缩 LLM 调用返回时,则摘要块不得追加进切换后会话的内存消息列表,也不得写进其转录文件(放弃本次压缩写入) - 假设压缩 LLM 调用挂起期间会话被清空(
/clear产生新 sessionId),当压缩完成时,则同场景 4:摘要不得写进清空后的新会话 - 假设压缩因会话切换放弃写入,当压缩流程结束时,则压缩状态仍必须回落为 false(
compactionStateChange正常发出、webview 提示消失),且不向任何会话追加错误块、不触发onCompactBlockAdded - 假设压缩 LLM 调用失败且期间会话未被切换,当错误处理执行时,则错误块写回发起压缩的会话(现状不变)
- 假设压缩全程会话未切换且成功,当压缩完成时,则摘要块写回发起压缩的会话(现状不变)
边界情况
- 压缩期间会话切换(双层守卫):webview 层——压缩进行中(
isCompacting)时历史会话选择与「新建对话 //clear」被忽略、「新建对话」按钮禁用,与 streaming/后台任务共用守卫位(仅 IDE 语义,桌面端并行会话模型不受影响);SDK 写入层——压缩进入写入阶段前若 messageManager 的 sessionId 与发起压缩时不一致,则整个写入阶段(摘要块追加、压缩后上下文恢复、计划模式提醒注入、SessionStart/PostCompact 钩子、失败错误块)全部放弃;已消耗的 LLM 调用不回收,原会话保持未压缩状态(用户可再次手动压缩)。SDK 兜底覆盖 UI 守卫挡不住的路径:状态通知在途时点击的竞态窗口、CLI TUI 的/clear、未来新增的切换路径 - 递归压缩:当压缩已包含摘要的历史时,整个历史(包括旧摘要)被新的延续摘要替换
- 图片处理:图片必须在压缩 API 调用之前从消息中剥离以减少 token 使用
- Token 限制边缘:如果摘要本身太长(不太可能但有可能),系统应该优雅处理
- API 轮次边界:压缩绝对不能将 tool_use/tool_result 对分割在压缩边界两侧
- fork 循环无文本输出:达到最大轮次(3 轮)或 fork 请求抛异常(用户中止除外)即判定压缩失败,计入熔断器;压缩无 fallback 降级路径(快速模型稳定性不足,且 fork 失败多为上下文本身不可恢复)
- 模型将工具调用语法作为文本输出:不做内容级拦截,原样透传(与 Claude Code 一致的已知限制——Claude Code 依靠 agent loop 在协议层拒绝真实工具调用,对文本中的乱码双方均不拦截)
- 无上次响应的 usage 锚点:会话第一轮请求(或清空后)尚无真实 usage,请求前估算退化为纯字符估算(全部消息按
estimateTokens计算),不得因缺失锚点而跳过检查 - 请求前估算偏差:估算可能低于真实 token 数,真实超限由 413 / context length 错误路径兜底;不引入响应后主动压缩检查
- 压缩后估算基准:压缩后旧消息保留在内存(UI 展示),自动压缩的请求前估算必须基于「最后压缩边界之后的上下文消息」而非全量消息,否则压缩后估算仍超阈值导致压缩反复触发
- 多次压缩:每个压缩摘要块保留在各自对应的压缩点,API 上下文从最后一个摘要块开始
- 转录写入去重:压缩时转录只追加摘要块,保留的最后 API 轮次不重复写盘(压缩前已写入;对齐 Claude Code 的 dedup-skipped);转录保持每条消息唯一,加载无需去重。旧版本产生的含重复消息转录不受支持,无需迁移清理
- 重启后上下文:压缩时保留的最后 API 轮次仅在当次会话内存上下文中有效,重启恢复后上下文从最后压缩摘要块开始(对齐 Claude Code:kept 消息不写盘,恢复后仅 boundary + summary + 后续消息),UI 展示不受影响(全量渲染)
假设
- 用于总结的 AI 模型能够生成简洁准确的摘要
- 压缩 fork 请求与主对话请求前缀一致时可命中模型侧前缀缓存(多数模型服务默认开启服务端自动前缀缓存)
- token 计数工具相当准确
- 工具结果时间戳在工具完成执行时准确设置