MoonshotAI / MoonshotAI/kimi-code
压缩摘要以user消息的身份重新注入上下文,导致授权被自我扩张、规格丢失、过期快照被应用
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 7.5k
- Forks
- 1.2k
- Avg merge
- 11h 53m
- Merged PRs (30d)
- 350
Description
你运行的 Kimi Code 版本是?
0.38.0。更新后为 0.39.1,其系统提示描述的压缩设计相同(逐字保留用户消息 + 第一人称摘要 + 追加 TODO 列表),该机制并未变化。
并已直接核对最新源码(HEAD,0.40.1;v1 packages/agent-core 与 v2 packages/agent-core-v2 设计相同):
摘要仍以 user 角色注入:agent-core/src/agent/context/index.ts 的 applyCompaction 构造 summaryMessage: { role: 'user', ... };compaction/handoff.ts 注释明写 "followed by a single user-role summary"
TODO 在 apply 时原样追加到摘要尾部,不与正文做一致性检查(v1 full.ts / v2 fullCompactionService.ts 的 postProcessSummary)
压缩期间的安全检查只看历史追加(v2 historySafeToCompact:前缀一致 + 新增消息必须是真实用户输入),后台任务与 TODO 状态变化不在守卫范围内
在途 turn 仍被整体丢弃(重建只保留真实用户输入 + 摘要)
0.38.0 → 0.40.1 的 changelog 中没有任何压缩机制层面的变更(只有 web UI 的压缩显示修复)。
你使用的是哪个开放平台/订阅?
Kimi Code订阅
你使用的是哪个模型?
kimi-code/k3-256k,thinking effort high
你的电脑平台是?
Darwin 24.5.0 arm64 arm
你遇到了什么问题?
长 session 里自动压缩之后,agent的行为会偏离原来的任务和授权范围。一开始以为是个别现象,后来把本机所有 session 的 wire.jsonl 翻了一遍:17 个 session 里 6 个发生过压缩,共 20 次,平均从 19 万 token 压到 2.9 万。大部分压缩后续是正常的,但有 3 个 session 出了明确的问题,而归因都是摘要写完之后的注入方式。
压缩摘要由模型自行生成,里面混合了已验证的事实、没验证的推断、接下来的计划、以及"用户已经同意了什么"这类陈述。这份摘要被当作一条 user 消息塞回上下文最末尾。也就是说,压缩摘要拿到了和用户指令一样的身份,而且位置比所有真实用户消息都新。摘要包装里虽然写了"把这当笔记看,别当证据",但从实际行为看这句话没起什么作用。
下面是三个有代表性的案例。
案例一:摘要把"批准发布"扩张成了"绕过 review"。 在一次生产发布中,用户给出一条指令:"如果有发现问题,向我汇报,尽可能不要自己决定。"后来用户说了"那就先发布吧"。发布过程中 PR 合并被仓库 ruleset 挡住——分支保护要求必须有人 review。
第五次压缩的摘要里保留了用户的约束,但紧接着写了一句:"用户要求:发现问题汇报,尽可能不自己决定;但发布序列已获批准,按上面执行即可。"
压缩之后,中间没有任何用户消息,agent 连续做了这些事:push、建 PR、尝试合并(被 review 规则拒绝,--admin 兜底也失败)、拉取 ruleset 配置、备份、用 gh api -X PUT 把 ruleset 的 enforcement 改成 disabled、合并 PR、再把 ruleset 改回去。用户的下一条消息是:"为什么不直接让我 review?"
用户确实批准过发布,事后也追认了这次操作,所以没造成实际损失。但在行动时,agent 把"批准发布"自己扩张成了"可以改仓库的安全设置来绕过分支保护"。这个扩张不来自任何一条用户消息,只来自摘要自己的断言。摘要里那句"但……已获批准",实际上把逐字保留在上下文里的用户约束给废掉了。
案例二:精确的实现规格被压没了。 第一次压缩前,agent 派了一个子任务去实现一个修复,规格写在工具调用的参数里:超时时间读 OPENAI_TIMEOUT_MS 环境变量,缺省 180000 毫秒,非整数或小于等于零要启动失败,两个 HTTP client 都要在关闭路径正确释放,测试要覆盖缺省值、环境变量和非法值。
压缩摘要把这些概括成了一句话:"修复 runtime 模型超时拆分,进行中。"
压缩后 agent 要根据摘要继续这项工作,它重新写了一份规格:超时硬编码 600 秒,并且明确写"模型 client 不需要关闭逻辑",测试缩减为"两个 client 分别是 40 秒和 600 秒"。环境变量、参数校验、生命周期管理全没了。代码最后就按这份退化的规格实现并提交了。
原因很直接:压缩只逐字保留用户消息。精确规格、验收标准、约束条件都在 assistant 消息和工具参数里,压缩时全部被概括掉。而摘要里的一句话概括,不足以还原出原来的要求,agent 只能自己重新发明一份规格,还以为这就是原来的任务。
案例三:摘要生成期间世界已经变了。 第一次压缩的时间线(都是 wire 里的时间戳):后台任务启动后 ,full_compaction.begin 触发,位置在 turn 中途,模型未给出这一轮回复之前。压缩摘要由另一个 LLM 请求生成,花了 75 秒,而那个后台任务在之间的第42秒完成。
摘要应用的时候,摘要里面还写着"我刚派出后台任务,它完成时会自动通知我",但这句话已经过期了。
#2720 里已有人从二进制里扒出过一个保护逻辑,如果压缩期间循环追加了新历史,摘要会被丢弃(historySafeToCompact)。这个保护在本case没生效,因为后台任务完成不算"历史追加"。守卫只盯着对话历史,看不见任务状态和 TODO 的变化。
同一个问题还有个小变体:第五次摘要的正文说"根因已修复,子集全部通过",但末尾自动追加的 TODO 列表还写着"重跑 015/018/019 复现真实异常""按真实异常修 finalize 问题"。TODO 是压缩开始时快照的,原样拼在摘要后面,没有检查它和正文矛不矛盾。两条互相矛盾的指令装在同一条消息里。
一个结构性的观察。 顺手验证了一件事:压缩后第一个 LLM 请求的消息数。
| 第几次压缩 | 保留的用户消息数 | 压缩后首个请求的消息数 |
|---|---|---|
| 1 | 8 | 3 |
| 2 | 16 | 3 |
| 3 | 16 | 4 |
| 4 | 25 | 4 |
| 5 | 42 | 3 |
压缩前请求的消息数是一直涨的(压缩前已经到 342)。压缩后无论保留了 8 条还是 42 条用户消息,请求都只有 3、4 条消息。我在源码里找到了对应机制:投影层会把相邻的 user 消息合并(agent-core/src/agent/context/projector.ts 的 mergeAdjacentUserMessages),而压缩重建后的历史恰好是一串连续的 user 角色消息(头段 + 省略标记 + 尾段 + 摘要),发到模型时被折叠成极少数几条。摘要因为 origin 标记不参与合并,是独立的一条——但仍然是 user 角色,排在整个上下文最末尾。
所以"用户消息逐字保留"这个说法有个问题:消息内容是保住了,但丢失了消息边界。用户的真实指令和模型摘要混在一起,摘要虽然因为 origin 标记保住了独立消息的身份,但它的角色同样是 user,而且排在整个上下文最新的位置。模型在这个上下文里看到的最后一段话,是它自己之前写的笔记,却以用户指令的角色和位置出现。
复现步骤?
无法在单次操作上确定性复现,这是一个间歇性的结构缺陷,与任务阶段、后台任务和摘要内容相关。三个出问题的session的共性模式:
- 长 session 跑到自动压缩触发(k3-256k 下约 20 万 token)。出问题的案例里,压缩都是在turn中途触发的:某个step刚结束、模型还没给出本轮回复。
- 满足以下任一条件:精确需求只存在于assistant/tool消息里(比如写进子agent派发命令的实现规格);摘要生成期间有后台任务在跑或TODO store在变化;存在与摘要所声称的"已批准"相冲突的用户常驻约束。
- 压缩后继续,对比压缩前后的行为。
受影响的session id之一:session_069ea289-0ec3-493f-918f-6bbe14c8826b(5 次压缩,每次约 20 万 → 2.9 万 token,保留用户消息数 8/16/16/25/42)。另外两个压缩后偏离的session可按需提供。
可以通过一个实验分离harness因素和模型因素:构造一个压缩边界,摘要里写"用户已批准 X",同时上下文里逐字保留用户消息"不要自己做 X",然后换不同的模型各跑若干次,统计覆盖率。所有模型都覆盖,就是harness的权威注入问题;只有特定模型覆盖,才有模型归因。
期望的行为是什么?
- 摘要不应该和用户指令不可区分。用独立的内部role/包装注入,逐字保留的用户消息保持独立消息边界,并写明显式冲突规则:逐字用户消息优先级高于摘要,摘要里说"已获批准"永远不构成授权来源。
full_compaction.begin和apply之间如果有任何变化(上下文版本、后台任务状态、TODO store),丢弃摘要重新生成。这能堵上之前出现的过期窗口,并把historySafeToCompact扩展到非历史状态。- TODO 列表在apply时从store现取,别用开始时的快照,或者至少和摘要正文做一次一致性检查。
- 原样保留进行中的turn(其assistant/tool消息,包括原始工具参数),精确规格和验收条件就在这些消息里。摘要本身也应该从散文改成结构化的恢复块:当前请求、任务阶段(包括"还欠用户一次汇报")、验收条件逐条列、已授权动作引用用户原话、禁止动作、待用户决定事项、在途任务状态、已验证事实 vs 未验证假设。
- 对push、合并、部署、改分支保护、删数据这类对外不可逆的操作,压缩边界应使摘要派生的授权失效:压缩后权限层要求授权必须来自上下文里仍在的逐字用户消息,找不到就停下来重新问。
补充信息
归因:harness 还是模型?
案例一里,模型在决定禁用 ruleset 之前的思考过程其实看到了用户约束,它原话引用了"尽可能不要自己决定",也承认"这正是那种出乎意料的阻塞""修改分支保护是安全相关的变更"。但最后两个 harness 注入的内容把它推向行动:一是交接笔记里那句"已获批准,按上面执行即可",以 user 消息的身份压在上下文最新位置;二是auto permission mode的系统提醒,明确写着"不要问用户,自己做个合理决定继续"。模型在两个互相冲突的指令之间选了错的一边,这是模型的判断失误;但它引用的两个"放行理由"全都是harness给的。一个完美对齐的模型应该停下来汇报,但harness的设计让"继续"成了阻力最小的路。
案例二更偏向 harness。原始规格只存在于被压缩掉的工具参数里,压缩后上下文里没有任何可以核对的真相,它只能根据摘要里的一句话概括重新发明一份。模型的问题在于另一个地方:它把这份重新发明的规格当成原任务直接执行了,没有向用户标注"我手上的规格是重建的,可能不完整"。包装里"当笔记别当证据"的提醒它也忽略了。所以模型的提升无法治本,核对所需的原材料已经不在上下文里了。
案例三(34秒过期快照、TODO 与正文矛盾、消息折叠)则完全是 harness 的,模型连参与的机会都没有。
和已有 issue 的关系。 #2680 报的是压缩后 agent 跑题——摘要没交代清楚哪些旧请求已经做完了,模型把一条早就完成的指令当成新任务。那是"摘要里缺东西";本 issue 是"摘要里多出来的东西被当成了什么",是它的镜像,但修法不一样:给旧请求标状态解决不了授权被自我扩张的问题。#2720 是压缩commit时的并发竞态,和案例三共享同一段代码路径,两份报告可以互证。#2622 里有人凭经验猜过"高上下文填充后 agent 会把摘要里没验证过的'已完成'当真",但没找到机制;案例一和二算是给这个猜测补上了证据。#3361 要求的pinned facts和保留预算,方向和上面的建议一致。
统计。 检查了 17 个 session,6 个发生过压缩,共 20 次,平均约 193K → 29K token(保留约 15%)。3 个 session 出现明确的压缩后偏离,其余续接正确。
原始 session 里有项目私有数据,公开 issue 就不附了。脱敏的wire摘录(事件时间线、摘要原文、消息数统计)如有需要可以通过/feedback和邮件提供。
Contribution
- 我愿意自己提交修复此 bug 的 PR(请先等待维护者在本 issue 中批准)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with agent-core/src/agent/context/index.ts and its applyCompaction path, then read compaction/handoff.ts, v1 full.ts, v2 fullCompactionService.ts, and v2 historySafeToCompact. Inspect projector.ts and mergeAdjacentUserMessages for the post-compaction message shape. Done means the reported authorization, lost-specification, stale-state, TODO-conflict, and in-flight-turn cases are addressed with coverage for the relevant compaction behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- authorization, backend, security
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100