fix(soul): repair orphan tool_calls when replaying history#2383
Open
Pluviobyte wants to merge 1 commit into
Open
fix(soul): repair orphan tool_calls when replaying history#2383Pluviobyte wants to merge 1 commit into
Pluviobyte wants to merge 1 commit into
Conversation
If a previous session was killed mid-turn (e.g. under memory pressure), the persisted history can contain an assistant message whose tool_calls were written without the matching tool-role responses. On resume, the provider rejects the next request with `400 ... tool_call_ids did not have response messages`, leaving the conversation permanently unresumable. normalize_history() now scans for orphan tool_call_ids and inserts a short placeholder tool message for each before the request goes out. The persisted history is untouched -- only the wire-shape sent to the provider is patched, so existing well-formed turns are left as-is and the next user message can proceed normally. Fixes MoonshotAI#2336 Co-authored-by: Cursor <cursoragent@cursor.com>
This was referenced May 29, 2026
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.
Related Issue
Resolve #2336
Description
When a kimi-cli session is killed mid-turn — the reporter describes high memory pressure, but the same failure shape can come from
kill -9, terminal close, OOM, etc. — the persistedcontext.jsonlcan contain anassistantmessage whosetool_callswere written without the matchingtoolrole responses. On resume the next provider request fails with:Because the corrupted history is permanent on disk, every retry of the same prompt hits the same error and the saved conversation becomes a write-off.
Fix
normalize_history()already runs immediately before each provider call (KimiSoul._step, side-question replay inbtw.py). Extend it with a final pass that scans for assistant messages carryingtool_calls, looks at the immediately followingtoolrole messages, and inserts a synthetictoolplaceholder for anytool_call_idthat was never responded to.(tool result unavailable: the previous session was interrupted before this tool call completed)) so it adds minimal context tokens on resume while giving the model a clear signal that the previous tool result is missing.What this PR does not change
It does not change the persistence layer to flush tool responses atomically — the on-disk history can still be left in the same state under a hard crash. The aim here is to make a saved session recoverable, not to prevent the corruption in the first place. Happy to land an atomic-flush follow-up if that direction is in scope.
Checklist
CHANGELOG.md(manual Unreleased entry, following the existing one-line-per-bullet style;make gen-changelognot run because the change is mechanical).make gen-docsnot run — N/A: no user-visible CLI / config change.Test plan
uv run ruff check src/kimi_cli/soul/dynamic_injection.py tests/core/test_normalize_history.py→ cleanuv run ruff format --check src/kimi_cli/soul/dynamic_injection.py tests/core/test_normalize_history.py→ cleanuv run pyright src/kimi_cli/soul/dynamic_injection.py tests/core/test_normalize_history.py→0 errors, 0 warnings, 0 informationsuv run pytest tests/core/test_normalize_history.py tests/core/test_kimisoul_steer.py tests/core/test_soul_message.py -q→52 passedProof of fix
New tests in
tests/core/test_normalize_history.py:test_orphan_tool_call_synthesized_when_followed_by_user— exact [Bug] Session corruption under memory pressure: lost conversation + 400 tool_call response error on resume #2336 shape (assistant withtool_calls, then user turn, notoolmessage in between).test_orphan_tool_call_synthesized_at_history_tail— assistant withtool_callsis the last message; resume must still work.test_complete_tool_response_not_duplicated— well-formed pair is untouched.test_partial_orphan_only_missing_ids_synthesized— paralleltool_calls, only the missing id gets a placeholder.test_multiple_assistant_tool_call_groups_independent— an earlier orphan does not steal responses meant for a later assistant message.test_assistant_without_tool_calls_untouched— assistant with notool_callsis passed through unchanged.Made with Cursor