Skip to content

fix(soul): restore plan-mode tool bindings after /init creates throwaway soul#2489

Open
nankingjing wants to merge 3 commits into
MoonshotAI:mainfrom
nankingjing:fix-init-planmode-bindings
Open

fix(soul): restore plan-mode tool bindings after /init creates throwaway soul#2489
nankingjing wants to merge 3 commits into
MoonshotAI:mainfrom
nankingjing:fix-init-planmode-bindings

Conversation

@nankingjing

@nankingjing nankingjing commented Jul 10, 2026

Copy link
Copy Markdown

Bug

Fixes #2478 — when /init runs, it creates a throwaway KimiSoul(soul.agent, ...) that shares the live soul's agent (and therefore the same tool instances). The throwaway soul's __init__ calls _bind_plan_mode_tools(), which rebinds the shared ExitPlanMode, EnterPlanMode, WriteFile, and StrReplaceFile instances to closures that reference the throwaway soul's state.

After /init completes and the throwaway soul is discarded, those shared tools are left pointing at stale closures whose _plan_mode is always False. This creates a state split:

  • The plan-mode dynamic injection (reading soul.plan_mode on the live soul) correctly shows "Plan mode is active".
  • ExitPlanMode (reading the stale closure on the dead soul) reports "Not in plan mode".

The workaround "call EnterPlanMode again, then ExitPlanMode" works because EnterPlanMode toggles the stale throwaway soul's _plan_mode to True.

Fix

  1. New public KimiSoul.rebind_plan_mode_tools() method in src/kimi_cli/soul/kimisoul.py — wraps the existing _bind_plan_mode_tools() for external callers.

  2. Call soul.rebind_plan_mode_tools() in the /init handler (src/kimi_cli/soul/slash.py) inside a try/finally so bindings are always restored even if the INIT prompt run fails.

  3. Two regression tests in tests/core/test_plan_mode.py:

    • test_second_soul_clobbers_binding_and_rebind_restores — demonstrates that constructing a second soul over a shared agent clobbers the ExitPlanMode binding, and rebind_plan_mode_tools() restores it.
    • test_init_slash_command_preserves_plan_mode_binding — end-to-end: runs the actual /init handler and verifies the live soul can enter plan mode afterwards.

Root cause

The plan-mode tools (ExitPlanMode/EnterPlanMode/WriteFile/StrReplaceFile) are mutable instances stored on the shared agent toolset. Their bind() methods store closures over a specific KimiSoul via capturing self in _bind_plan_mode_tools. When two souls share the same agent, whichever soul calls _bind_plan_mode_tools last "wins" — the other soul loses its bindings.

Verification

  • Root cause confirmed by code-level trace: all three KimiSoul construction sites examined; only /init shares a live agent. The binding mechanism (closures capturing self._plan_mode) and the over-write-on-rebind behavior confirmed. The workaround mechanism (EnterPlanMode flips the stale soul's flag) also explained.

Open in Devin Review

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@nankingjing

Copy link
Copy Markdown
Author

Reviewed — the rebind fix in a finally block correctly restores shared plan-mode tools after /init, and the tests are thorough. Ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] ExitPlanMode reports "Not in plan mode" while system reminder claims plan mode is active / Plan mode 狀態不一致

1 participant