Repository navigation
fix(kimi-agent): retry rejected request structure, restore state on undo, count CJK per character - #13
Merged
Conversation
…sitives Two real defects, both now covered: - The remote-control page prefix was embedded in an injected `<script>` block through `JSON.stringify`, which escapes quotes and backslashes but not `<`. A prefix containing `</script>` closed the block early and the rest was parsed as markup. Today's only caller percent-encodes both halves, so this was hardening rather than a live hole — but the function takes an arbitrary string and the escaping it relied on did not make that true. - A provider error body is parsed for its HTML `<title>` with a lazy span that re-scans the tail from every opening tag: measured against the old form, 40k opening tags cost over a second, and the body arrives over the network. The title is now found by locating the open tag and then the next close tag. Two smaller changes: the MCP tool-name decoder states its `indexOf` not-found case instead of folding it into `<= 0` next to a length comparison, and the private-key redaction rule in the prompt-metadata sanitizer — the one that could actually leak a credential — finally has a test. The remaining 30 alerts were triaged rather than code-changed: fourteen `/\/+$/`-shaped findings are linear (a single-quantifier anchored pattern has nothing to backtrack into), one `/<[^>]*>/g` likewise, and three are documented won't-fixes. Each dismissal carries the reason.
…ctor `&` was decoded first, so the entities decoded after it were re-read: a double-encoded `&lt;script&gt;` came out of `extractText` as a live `<script>`. Decoding `&` last makes the pass mean what a single decode of a document means. Verified directly — the payload stays encoded, and ordinary named and numeric entities are unaffected.
…r rejects the request's shape A provider that answers 400/422 about the request's structure — a duplicated tool_call_id, roles that do not alternate, an empty text block — was ending the turn as a hard failure. The turn loop now spends at most one strict retry per turn: it removes the duplicated tool call declarations and the second result for an already-answered call, then resends. The verdict is deliberately narrow. Context overflow and media rejection share the 400 and are classified first, because their recoveries (compaction, media stripping) actually work and a strict projection would trade one for a wasted round trip. The retry is also only spent when the dedupe repaired something, so a shape problem it cannot see does not burn the attempt.
…2 copy Six provenance notes described the reference as "v2" while actually pointing at the fork's own retired agent-core-v2 copy, a tree that no longer exists anywhere. Because those citations all supported "already aligned with v2", the error was directional rather than noise. Checked against upstream/main @ 21406fb: injectCacheControlOnLastBlock (kosong anthropic.ts:352-362) only touches messages.at(-1) with no length >= 4 branch, so the stable-history breakpoint is fork-added and upstream emits 3 cache slots where the fork emits 4. A tree-wide search for the tool names behind Knowledge, Team, lsp, Memory, session_query, run_code and sandbox returns no files under agent-core-v2/src. No anthropic-cache-breakpoints.ts and no tool/realpath-access.ts exist upstream either. Comments only; no behavior changes.
…haracter POST /undo deleted transcript rows but left todo.json and the plan describing work the model can no longer see, so the next turn inherited a stale plan. Only the REPL's /undo ever popped a checkpoint. The route now unwinds the checkpoint stack after the rows are gone: the compaction-boundary refusal happens first, and a failure is reported rather than swallowed, since a silent divergence is worse than a visible error. The stack is LIFO, so count=N pops N times. The status line estimated context tokens as content.len() / 4, charging a quarter token per byte and so reading about 25% low on CJK — the gauge drifted low exactly in the sessions filling up. It now shares compaction's estimator with the compaction trigger, so the two cannot disagree about how full the context is.
…fingerprint The ledger's own reliability was the subject: 58 sampled claims were checked by opening the cited files rather than confirming the paths exist, which is all the current gate does. Ten pointed at the retired v2 copy or at paths in neither tree, and eight of those backed a conclusion about parity with upstream — so the errors ran in one direction rather than scattering. The most consequential correction unblocks work: micro compaction's detect() was marked "no reference implementation, needs a decision", but the reference is intact and the gap is the two gates plus about 40 lines, not a new subsystem. A separate sample found 13 more stale citations, including line numbers that now point at a different function and two provenance reversals. Also adds MEMORY.md, the environment notes this machine keeps re-learning: SSH is required because github.com:443 is unreachable here, and bulk file rewrites through PowerShell convert the whole file to CRLF. Finally refreshes the kimi-agent fingerprint, which covers the engine sources touched by the three commits above.
…ch touched check:normify flagged seven modules as drifted because the engine sources they cover changed in this branch. Refreshed through the tool itself rather than by hand, with each child paired with its parent since refreshing only the child leaves the parent's aggregate stale.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requirement or Bug
整理一批已完成但未提交的引擎改动,并修正审计台账中指向错误参考树的 provenance 声明。
本 PR 走 PR 路径而非直推 main:仓库 ruleset
all要求 CodeQL 无 error 告警才允许推 main,而 CodeQL 由push到 main 触发(codeql.yml:3-6),直推会互为前提地卡住。codeql.yml:8-9对目标为 main 的 PR 同样触发扫描,因此合并路径可行。Bug Reproduction Steps
见下面 Root Cause 的三项,每项都对应一个可复现的用户可见现象。
Root Cause
三个独立缺陷,都属于缺失的接线而非设计问题:
provider 拒绝请求结构时整轮失败。 历史里出现重复的
tool_call_id(崩溃后续跑、重试、fork 合并)会被所有 provider 以 400 拒绝,而 turn loop 只把 400 归类为「上下文溢出」或「媒体拒绝」,两者各有能生效的恢复(压缩 / 去媒体),结构问题则无人处理,直接失败告终。缺的是 v2dedupeDuplicateToolCalls这一项修复,以及触发它的入口。POST /undo只删转录行,不回滚状态。StateStore::rollback在生产代码里只有 REPL/undo一个调用方。REST 路由删掉 transcript 行后,todo.json与 plan 仍描述着模型已看不到的工作,下一轮继承一份过期计划。台账原引的三处「已有模式」全是假的。状态栏 token 估算按字节计。
content.len() / 4每一字节算 1/4 token,CJK 每字 3 字节 → 低报约 25%。仪表恰好在会话变满时读数偏低。且与压缩触发器用了不同估算器,两者对「有多满」可能给出分歧。均为根本性修复,非变通方案。
Code Changes
三个功能改动彼此独立:
run_turn() — provider 报错后的恢复分支 overflow arm (压缩) → 先于新分支 media arm (剥离媒体) → 先于新分支 + structure arm (严格投影重发) ├─ is_recoverable_request_structure_error(&err) │ ├─ status ∉ {400, 422} → false │ ├─ tool_use / tool_result 抱怨 → true │ └─ 角色不交替 / 空 text block → true ├─ dedupe_duplicate_tool_calls(&mut messages) │ └─ 返回 false(无可修复)→ 不消耗这一次重试 └─ continue 'overflow_recovery → 用修好的请求重发分类顺序是有意的:上下文溢出与媒体拒绝共用 400,它们的恢复确实有效,结构投影换掉只会白费一轮往返。
去重逻辑本身:
dedupe(messages) assistant 分支 ├─ 重复的 tool_call id → 丢弃该 call ├─ call 全被丢光且无正文无 blocks → 丢弃整条消息(v2 vacuous_message_dropped) └─ 存活 → 保留正文,calls 换成过滤后的结果 tool 分支 └─ 已回答过的 tool_call_id → 丢弃这条结果另有一处死代码清理:
native/event_store/mod.rs里的dedupe_duplicate_tool_calls与turn_loop/tool_call_id.rs里的那份重复(前者操作Message、后者操作LLMMessage),且无任何生产调用方——只有它自己的测试在调。已删除 253 行。provenance 订正为纯注释,涉及 6 个文件。核实基准是
upstream/main @ 21406fb4c8:injectCacheControlOnLastBlock(kosong/anthropic.ts:352-362)只处理messages.at(-1),无length >= 4分支 → 上游发 3 个 cache slot,fork 发 4 个,stable 位是 fork 自加CACHEABLE_TYPES在:341-350,词汇表本身未受影响,只是出处被写错agent-core-v2/src全树搜knowledge/teamTool/lsp/Memory/session_query/run_code/sandbox只命中散文,无对应文件 → 这七个工具是 fork 自创,不是移植anthropic-cache-breakpoints.ts、无tool/realpath-access.ts、无workspace/sandbox/Impact Scope
llm/request_structure.rs(新)、turn_loop/tool_call_id.rs、turn_loop/run_turn.rsa_turn_resends_with_the_strict_projection_after_a_shape_rejection走真实 turn loop,用一个「只看请求内容决定成败」的 mock —— 也会在去重没真发生时误绿)server/mod.rsserver/engine.rsnative/event_store/mod.rs本地验证全绿:
cargo fmt --check、cargo clippy --all-targets --features cli -- -D warnings、cargo test --no-default-features --features cli,workflow-js(3126 passed / 0 failed)、bun run lint、check:architecture、check:parity、check:roadmap-refs(79 处文件引用 + 147 处测试名全部解析)。未验证的部分:
check:normify未跑(改动未触及normify-kimi-code/下的模块,但架构指纹变更理论上可能影响它)。真实 provider 的 400 行为是按 v2 的错误分类逐条对照移植的,未在真实 provider 上端到端复现过。Checklist
gen-changesetsskill, or this PR needs no changeset.(三条 patch:结构拒绝自动恢复、undo 恢复工作区状态、CJK 仪表修正)gen-docsskill, or this PR needs no doc update.(内部行为修复,已由 changeset 覆盖;docs/下无相关用户文档需同步)