fix(session): align fork/undo context truncation to wire turns#2520
fix(session): align fork/undo context truncation to wire turns#2520Nas01010101 wants to merge 3 commits intoMoonshotAI:mainMoonshotAI/kimi-cli:mainfrom Nas01010101:fix-fork-context-alignmentNas01010101/kimi-cli:fix-fork-context-alignmentCopy head branch name to clipboard
Conversation
truncate_context_at_turn counted every non-checkpoint user-role record as a turn boundary, but the context file is not 1:1 with wire turns: compaction rewrites earlier turns into a single user-role summary record, steers inject extra user-role records mid-turn, and slash command turns never touch the context. After any of these, /undo and /fork truncated the context at the wrong record — e.g. rewinding past a post-compaction turn kept that turn in the model's context while the UI no longer showed it. Align context user records to wire turns by matching user text (positional counting kept as fallback when wire texts are unavailable).
|
For reviewer transparency — two edge cases the text-alignment approach cannot fully resolve (now documented in the
Both predate this PR — positional counting handled them strictly worse (every compaction/steer/slash turn corrupted the cut, not just these collisions) — and both are inherently ambiguous for any content-based heuristic: I checked LCS/sequence alignment ( The clean resolution for both is the explicit |
Related Issue
Resolve #2517
Also resolves #1974 (wire-only slash turns shifting the undo cut — covered by a dedicated regression test) and likely the root cause of #2049 (history mismatch after forks/undos).
Relation to open PR #2386: that PR maps wire turns to context turns for the slash-command case (credit to its author for identifying that direction). It does not handle synthetic user-role records in the context — compaction summaries, steers, notifications — so compacted/steered sessions still cut at the wrong record, and the compaction case leaks "undone" turns into the model's context. This PR covers both directions with tests for each; happy to instead rebase the additional coverage on top of #2386 if the maintainers prefer to land that one first.
Description
truncate_context_at_turn()counts non-checkpointrole="user"records to find the cut point for/undoand/fork, but the context file is not 1:1 with wire turns: compaction rewrites earlier turns into one user-role summary record, steers and notifications inject extra user-role records, and slash-command turns produce no context record at all. After any of these, the context is truncated at the wrong record:Fix: when the wire turn texts are available (from
enumerate_turns, whichfork_sessionalready computes for wire truncation), align context user records to wire turns by matching extracted user text, consuming turns in order. Records that match no remaining wire turn (summaries, steers, notifications) no longer advance the turn counter, and turns with no context record (slash commands, compacted-away turns) no longer consume a user record. Positional counting is kept as an explicit fallback when wire texts are unavailable, preserving the existing best-effort semantics.Real-behavior proof (deterministic, local mock provider)
Build a real session:
turn one→turn two→/compact→turn three; thenfork_session(dir, work_dir, turn_index=2)(the exact/undocode path):main: forked wire shows turns 0–2, but forkedcontext.jsonlstill containsuser: "turn three"and its assistant reply ❌turn twoblock ✅Verification
tests/core/test_session_fork.py::TestTruncateContextAlignment(4 fail onmain):compaction summary not counted as a turn; steer not counted; duplicate turn texts align in order; end-to-end
fork_sessionafter compaction drops the undone turn; positional fallback unchanged.uv run pytest tests/core/test_session_fork.py→ 34 passed; fulltests/suite green.ruff/pyrightclean.Checklist
## Unreleasedentry;make gen-changelogneeds kimi API auth).make gen-docs— N/A: no user-facing docs change.