MoonshotAI / MoonshotAI/kimi-cli

/undo and /fork truncate context.jsonl at the wrong turn in compacted or steered sessions

Open
#2,517 0 comments 0 reactions 0 assignees View on GitHub

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:

  1. Compaction rewrites all earlier turns into a single role="user" summary record (compaction.py: Message(role="user", content=[system(COMPACTION_OUTPUT_PREFIX), ...])).
  2. Steers append extra role="user" records mid-turn (kimisoul._consume_pending_steers).
  3. Notifications are injected as role="user" records (notifications/llm.py).
  4. Slash-command turns (e.g. /compact) produce a wire TurnBegin but 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):

  1. kimi --print -p "turn one" ; kimi --print -C -p "turn two" ; kimi --print -C -p "/compact" ; kimi --print -C -p "turn three"
  2. The session now has wire turns [turn one, turn two, /compact, turn three] and context user records [<compaction summary>, turn two, turn three].
  3. Fork/undo at turn index 2 (i.e. rewind to before "turn three") — via /undo in the TUI, POST /sessions/{id}/fork, or directly fork_session(session_dir, work_dir, turn_index=2).
  4. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.