fix(session): map undo wire turns to context turns#2386
Conversation
Undo and fork select turns from wire.jsonl, but slash/local command turns can be present in wire without adding a real user message to context.jsonl. Reusing the wire turn index for context truncation could therefore keep too much context and leave the forked session inconsistent. Map each selected wire TurnBegin to the matching real context user turn before truncating context, so non-context local command turns no longer shift the cutoff. Fixes MoonshotAI#1974 Related MoonshotAI#2049 Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 688ffecbd6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if ( | ||
| next_context_turn < len(context_user_texts) | ||
| and turn.user_text == context_user_texts[next_context_turn] | ||
| ): | ||
| context_turn_index = next_context_turn | ||
| next_context_turn += 1 |
There was a problem hiding this comment.
Handle context-only user turns when mapping history
When a session contains a model-visible user message that has no matching wire TurnBegin text, this sequential equality check gets stuck and all later wire turns map to the last earlier context turn. One concrete path is a configured Stop hook with a block reason: KimiSoul.run calls _turn(Message(role="user", content=result.reason)) without emitting another TurnBegin, so selecting any later /undo or fork turn truncates away the later real prompt from context.jsonl. Skill slash commands have the same shape because the wire text is /skill:... while the context text is the loaded skill prompt.
Useful? React with 👍 / 👎.
Related Issue
Resolve #1974
Related #2049
Description
/undoand fork currently use awire.jsonlTurnBeginindex for both wire truncation and context truncation. That only works when every wire turn also writes a real user message tocontext.jsonl.Local slash-command turns such as
/usageor/sessionsare present inwire.jsonl, but do not represent real model-visible user turns incontext.jsonl. When users select one of the later turns in the undo picker, the context truncation uses a too-large index and keeps extra user/assistant messages. This makes the resumed/forked session land earlier/later than the selected turn and can show history in the UI that the agent does not actually have, which matches #1974 and the history mismatch reported in #2049.Fix
TurnBegin.user_inputtext fromwire.jsonlagainst real non-checkpoint user messages fromcontext.jsonl.context.jsonl; wire truncation still uses the original wire index.Checklist
CHANGELOG.md(manual Unreleased entry, following the existing one-line-per-bullet style;make gen-changelognot run because this is a focused bug fix).make gen-docsnot run — N/A: no user-visible CLI/config/docs behavior change.Test plan
UV_PYTHON=3.12 uv run ruff check src/kimi_cli/session_fork.py tests/core/test_session_fork.py→ cleanUV_PYTHON=3.12 uv run ruff format --check src/kimi_cli/session_fork.py tests/core/test_session_fork.py→ cleanUV_PYTHON=3.12 uv run pytest tests/core/test_session_fork.py -q→30 passedProof of fix
New regression test:
test_fork_maps_wire_turns_to_real_context_users.It builds a session where wire has five turns:
but context only has three model-visible user turns:
Forking at wire turn
3now keeps onlyreal 0andreal 1in context, instead of incorrectly keepingreal 2as well.Made with Cursor