MoonshotAI / MoonshotAI/kimi-code
Session permanently bricked by 400 "tool_call_id is not found" after compaction splits a parallel tool-call batch
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 7.5k
- Forks
- 1.2k
- Avg merge
- 11h 53m
- Merged PRs (30d)
- 350
Description
Summary
After a context compaction, a session can become permanently unusable: every turn fails with 400 tool_call_id is not found (note the empty id — double space) before the model produces anything. Retrying or rephrasing keeps failing because the same corrupted history is re-sent on each turn; only starting a brand-new session recovers.
ERROR turn failed turnId=103
APIStatusError: 400 tool_call_id is not found
at convertOpenAIError (.../dist-native/intermediates/main.cjs)
WARN llm request failed turnStep=104.1 model=kimi-for-coding statusCode=400
Environment
- kimi-code
0.19.1(native), wire protocol1.4 - model:
kimi-for-coding - macOS (darwin arm64), Node 24
Root cause
The outgoing request contains a role:tool message whose tool_call_id has no matching assistant tool_calls — an orphaned tool result sitting right after the compaction summary.
A full compaction stores a fixed compactedCount split index and later applies it via [summary, ...history.slice(compactedCount)]. When that index falls inside a parallel tool-call batch:
history before compaction:
idx N assistant toolCalls=[A, B] <- one step, two PARALLEL calls
idx N+1 tool result(A)
idx N+2 tool result(B) <- compactedCount points HERE
the retained suffix starts at result(B) while its owning assistant ([A, B]) is folded into the summary, orphaning result(B).
canSplitAfter forbids this split against the materialized array (the next message is a tool), so the count is produced when it is computed against an in-flight batch (only A had landed) and later applied to the fully materialized history — most reliably on resume, where history is rebuilt deterministically with both results present. The request projection does no tool-call/result pairing validation, so the orphan is sent verbatim and the provider rejects every subsequent turn.
Minimal reproduction (no API key needed)
Replay a synthetic record stream — a parallel batch [A, B] plus a context.apply_compaction whose compactedCount lands between result(A) and result(B) — through the real resume path, then inspect the projected request:
const ctx = testAgent({ persistence: new InMemoryAgentRecordPersistence(records) });
await ctx.agent.resume();
const orphans = findOrphanToolMessages(ctx.agent.context.messages);
// orphans === [{ index: 1, id: 'call_B' }] -> request is rejected with 400
Suggested fix
Drop orphaned leading tool results when applying a compaction: a tool result can never legitimately begin the retained suffix. (A PR with this fix + a regression test is on the way.)
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 the context.apply_compaction handling and the real agent resume path, then reproduce the synthetic record stream using testAgent and InMemoryAgentRecordPersistence. Inspect how compactedCount is applied to the materialized message history and use findOrphanToolMessages to verify the projected request. Done means the resumed history contains no orphaned tool results and the regression case no longer produces the 400 response.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100