MoonshotAI / MoonshotAI/kimi-cli
/undo and /fork truncate context.jsonl at the wrong turn in compacted or steered sessions
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 11.4k
- Forks
- 1.3k
- Avg merge
- 9h 47m
- Merged PRs (30d)
- 2
Description
What version of Kimi Code CLI is running?
1.49.0 (reproduced on current main, commit 4a550ef)
Which open platform/subscription were you using?
Local OpenAI-compatible mock provider (bug is provider-independent; it is in local session-file handling)
Which model were you using?
n/a (any)
What platform is your computer?
macOS (Darwin arm64); the code path is platform-independent
What issue are you seeing?
truncate_context_at_turn() in src/kimi_cli/session_fork.py maps a wire turn index onto context.jsonl by counting non-checkpoint role == "user" records. But the context file is not 1:1 with wire turns:
- Compaction rewrites all earlier turns into a single
role="user"summary record (compaction.py:Message(role="user", content=[system(COMPACTION_OUTPUT_PREFIX), ...])). - Steers append extra
role="user"records mid-turn (kimisoul._consume_pending_steers). - Notifications are injected as
role="user"records (notifications/llm.py). - Slash-command turns (e.g.
/compact) produce a wireTurnBeginbut no context user record.
After any of these, the user-record count no longer matches the wire turn index, so /undo and /fork cut context.jsonl at the wrong record:
- Compacted session +
/undo: the "undone" turn silently stays in the model's context while the UI no longer shows it — the agent acts on a turn the user believes was removed. - Steered session +
/undo//fork: truncation stops too early — the forked context is missing turns that the resumed UI (wire replay) still displays. This matches the symptom reported in #2049 ("prior messages appear on the screen but the agent doesn't have it" after forks/undos).
What steps can reproduce the bug?
With any provider (below reproduced with a local OpenAI-compatible mock so it is fully deterministic):
kimi --print -p "turn one";kimi --print -C -p "turn two";kimi --print -C -p "/compact";kimi --print -C -p "turn three"- The session now has wire turns
[turn one, turn two, /compact, turn three]and context user records[<compaction summary>, turn two, turn three]. - Fork/undo at turn index 2 (i.e. rewind to before "turn three") — via
/undoin the TUI,POST /sessions/{id}/fork, or directlyfork_session(session_dir, work_dir, turn_index=2). - Inspect the forked session:
wire.jsonl: turns 0–2 only — "turn three" is gone from the UI. ✅context.jsonl: still contains the "turn three" user message and its assistant reply. ❌
Observed on main:
== forked wire turns == == forked context ==
turn 0: 'turn one' ... user '<system>Previous context has been compac...'
turn 1: 'turn two' ... user 'turn two'
turn 2: '/compact' ... assistant 'hello from mock'
... user 'turn three' <-- should have been dropped
... assistant 'hello from mock'
What is the expected behavior?
The forked/undone session's context should contain exactly the turns that remain visible in the wire log — nothing more (compaction case) and nothing less (steer case).
Additional information
Root cause: truncate_wire_at_turn counts TurnBegin records while truncate_context_at_turn counts user-role records; synthetic user-role records (compaction summary, steers, notifications) and context-less turns (slash commands) break the correspondence in both directions.
Relation to existing reports: #1974 and open PR #2386 cover one direction of this desync — wire-only slash-command turns shifting the undo cut point. This report is about the other, unreported direction: synthetic user-role records in the context (compaction summaries, steers, notifications). #2386's wire→context index mapping does not exclude these records, so compacted or steered sessions still truncate at the wrong place (and the compaction case leaks "undone" turns into the model's context, which is worse than landing on the wrong turn).
Proposed fix (PR attached): align context user records to wire turns by matching the extracted user text (turns consumed in order; records that match no remaining wire turn — summaries, steers, notifications — do not advance the turn counter). Positional counting is kept as a fallback when wire texts are unavailable. A more invasive alternative would be to write an explicit _turn marker record into context.jsonl at each turn begin; happy to go that way instead if you prefer a protocol-level fix.
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 in src/kimi_cli/session_fork.py with truncate_context_at_turn(), truncate_wire_at_turn(), and fork_session(); compare their turn-index handling against the context records produced by compaction.py, kimisoul._consume_pending_steers, and notifications/llm.py. Reproduce the listed compacted-session sequence, inspect wire.jsonl and context.jsonl, and confirm that undo or fork leaves exactly the turns still visible in the wire log.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100