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 protocol 1.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.)
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.Environment
0.19.1(native), wire protocol1.4kimi-for-codingRoot cause
The outgoing request contains a
role:toolmessage whosetool_call_idhas no matching assistanttool_calls— an orphaned tool result sitting right after the compaction summary.A full compaction stores a fixed
compactedCountsplit index and later applies it via[summary, ...history.slice(compactedCount)]. When that index falls inside a parallel tool-call batch:the retained suffix starts at
result(B)while its owning assistant ([A, B]) is folded into the summary, orphaningresult(B).canSplitAfterforbids this split against the materialized array (the next message is atool), so the count is produced when it is computed against an in-flight batch (onlyAhad 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 acontext.apply_compactionwhosecompactedCountlands betweenresult(A)andresult(B)— through the real resume path, then inspect the projected request: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.)