Skip to content

fix(shell): reload MCP and model sessions cleanly - #51

Merged
elkaix merged 24 commits into
mainfrom
fix/reload-mcp-model-context
Jun 2, 2026
Merged

fix(shell): reload MCP and model sessions cleanly#51
elkaix merged 24 commits into
mainfrom
fix/reload-mcp-model-context

Conversation

@elkaix

@elkaix elkaix commented Jun 2, 2026

Copy link
Copy Markdown
Member

Summary

  • Re-read MCP config inputs inside each reload iteration so /reload sees servers added after the process started.
  • Clean up per-instance MCP/model-refresh resources during same-process reloads without killing persisted background tasks.
  • Start a fresh session when /model changes the active model, while preserving added workspace directories.
  • Fix /reload to raise a Reload() instance explicitly.

Root cause analysis

The shell reload loop parsed the default/global MCP config once during initial CLI argument handling and then reused that captured mcp_configs list for every same-process reload. If a user ran pythinker mcp add ... while the shell was open, /reload rebuilt the agent from stale in-memory MCP config, so new MCP tools were unavailable until a full terminal restart reparsed the global MCP file.

Reload control flow also preserved background tasks by skipping the shutdown path entirely. That avoided killing user tasks, but it also skipped per-instance cleanup for MCP transports and the managed-model refresh task. The fix separates reusable runtime cleanup from background-task termination.

For model switching, /model saved the new default model and reloaded the same session, preserving the prior context. That allowed old-context turns to carry into a different model. The fix creates a new session on model changes so the next agent starts with fresh context.

Verification

  • uv run pytest tests/core/test_cli_reload.py tests/ui_and_conv/test_shell_slash_commands.py -q
  • uv run ruff check src/pythinker_code/cli/__init__.py src/pythinker_code/app.py src/pythinker_code/ui/shell/slash.py src/pythinker_code/ui/shell/setup.py tests/core/test_cli_reload.py tests/ui_and_conv/test_shell_slash_commands.py
  • uv run pyright src/pythinker_code/cli/__init__.py src/pythinker_code/app.py src/pythinker_code/ui/shell/slash.py src/pythinker_code/ui/shell/setup.py
  • make check
  • make test

Summary by CodeRabbit

  • New Features

    • Added a thinking-effort option for finer control over AI reasoning modes and persisted preference.
  • Improvements

    • /model now starts a fresh session when switching models to avoid cross-model context carry-over.
    • Better cleanup of background tasks and MCP connections during reload/shutdown.
    • CLI now rechecks and reloads MCP server configs added after startup.
  • Tests

    • Added regression and integration tests covering MCP reloads and fresh-session model switches.

elkaix added 22 commits June 1, 2026 17:36
web/static/ is a build output (scripts/build_web.py rmtree's + repopulates it
from the vite build, whose brand files come from web/public/brand). The two
brand assets are force-committed only so the test job — which doesn't run the
web build — has the files the OAuth callback reads. Add a guard asserting those
committed copies stay byte-identical to web/public/brand, so a brand-source
change can't silently leave the branding tests validating a stale fixture.
The OAuth callback page embeds icon.svg/favicon.ico as data URIs. These are
build outputs (web/static) and normally always present, but a broken build
shouldn't crash login over a cosmetic asset. Catch OSError in the data-uri
helper, log a warning, and embed an empty source so the callback still renders.
Add a test for the degraded path.
Under auto_deliberate, an irreversible shell action (rm -rf, git push
--force, git reset --hard, dd, truncate) is bounced once before running --
even under auto/yolo -- so the agent weighs alternatives first. One-shot:
the identical re-issue runs, so deliberation never permanently whitelists
the command. ApprovalResult gains a deliberation variant whose feedback is
not framed as a user rejection.
Pure helpers for the Shift+Tab thinking feature: next_thinking_level()
cycles off->minimal->low->medium->high->xhigh->off, and thinking_frame_color/
thinking_frame_style map each level to a prompt border color (grey->blue->
violet->purple) per theme. Keymap + prompt-handler wiring follows.
Restructure the active-work shimmer into a four-phase loop: a wave
sweeps right-to-left, splashes outward from the middle, sweeps back
left-to-right, splashes again, then repeats. Replaces the previous
single-direction repeating sweep. Stays purely time-derived so the
prompt, activity tree, and pinned-todo renderers animate in sync.
Recolor the active-work shimmer: highlight is now silver (#D8DCE2)
instead of violet, and the verb-spinner/pinned-todo base is a muted
orange-yellow (#D49E5A) instead of golden amber. Point palette tests at
the motion constants so future shade tweaks don't churn test literals.
The per-turn '※ recap:' line quoted the assistant's first sentence,
which is always an intent preamble ('I'll start by gathering...') rather
than what the turn accomplished. Add an outcome-sentence heuristic that
skips intent/offer/question/path-noise lines and prefers the closing
summary, and append factual deltas (N files changed · M steps). Wire the
live view to count files changed per turn from diff display blocks.

Also improve /recap: session bullets lead with the session outcome
instead of the first user message, and a single short no-op session is
reported plainly as a light day rather than padded into a bullet list.
Make "minimal" a first-class ThinkingEffort: OpenAI round-trips it
natively, the Pythinker provider preserves it instead of collapsing to
low, Anthropic clamps it down to its floor (low) rather than up to high,
and Gemini maps it to its lowest thinking level/budget.
… setting

Add a persisted default_thinking_effort alongside the legacy
default_thinking bool, with the effort string as the source of truth
(falling back to the bool only for pre-existing configs). create_llm now
takes an explicit effort, clamps it to model capabilities, preserves
levels like xhigh/minimal instead of collapsing every enabled request to
high, and threads the effort through the CLI, ACP, review, web API,
subagents, login flows, setup, and the /model and /thinking selectors.
Always-thinking models surface native reasoning instead of an effort
dial.
…y effort

Repurpose Shift+Tab from plan-mode toggle to a thinking-effort cycle over
the model's available levels, with a toast and telemetry. The prompt
separator and bottom-toolbar label now reflect the active effort (or
"native reasoning"), and the effort color ramp moves from a grey/violet
scale to a cool-to-warm slate→pink ramp. Tips and keybinding help updated
to match.
…tion

Add an "auto_deliberate" ask-user policy: in auto mode, instead of
silently dismissing AskUserQuestion, run an independent tool-less advisor
that blind-ranks the agent's own options and hand the verdict back so the
agent self-decides. Destructive auto-approved actions are bounced once for
deliberation via a tool-agnostic classifier, and the auto-mode prompt
invites the tool at genuine forks under this policy.
Render report blocks as a rounded, padded Rich panel (a standalone
reading surface) with hanging-indented wrapped locations, and keep a
one-row seam when a report fence follows streamed prose. Ask-question
cards gain blank-row separation between header and questions.
Add a prompt-scoped refresh loop so the pinned-status shimmer stays
frame-based when wire events are sparse (e.g. a long-running subagent),
honoring reduced-motion with a slower interval.
Map each login/logout entry to the managed provider keys that signal it is
configured, so /login and /logout report real status and a bare /logout
opens a selector over only the logged-in providers (with a single OpenAI
entry covering both OAuth and API-key credentials).
Two RunAgents calls differing only in base_prompt produced the same
fingerprint; include it so distinct launches are not deduplicated.
Update rendering tests to match the committed compaction/recap seam
behavior: compaction commits a leading blank row plus block, and turn
recaps are framed by blank rows.
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d0821f8e-ae1c-4618-a68d-3a18458e132a

📥 Commits

Reviewing files that changed from the base of the PR and between 6498c15 and 619254c.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • src/pythinker_code/app.py
  • src/pythinker_code/ui/shell/slash.py
  • tests/core/test_cli_reload.py
  • tests/ui_and_conv/test_shell_slash_commands.py

📝 Walkthrough

Walkthrough

Adds thinking-effort plumbing and UI changes, factors MCP config loading into a helper that re-resolves the default file, adds per-instance runtime cleanup integrated into shutdown flows, makes /model create a fresh session for managed:lm-studio switches, and adds tests plus a changelog entry.

Changes

Thinking effort and lifecycle management updates

Layer / File(s) Summary
MCP config loader and CLI wiring
src/pythinker_code/cli/__init__.py
Adds module-level json import and _load_mcp_configs_from_cli_inputs(...); removes inline parsing in the CLI callback and wires the helper into PythinkerCLI.create and the callback finally-block so default MCP file is re-resolved and errors map to typer.BadParameter.
Per-instance runtime cleanup
src/pythinker_code/app.py
Adds PythinkerCLI.cleanup_runtime_resources() to cancel managed-model refresh tasks and best-effort toolset.cleanup() for PythinkerToolset; shutdown_background_tasks() now calls it before existing shutdown logic.
UI thinking configuration and reload behavior
src/pythinker_code/ui/shell/setup.py
Setup now selects a ThinkingEffort, _SetupResult includes thinking_effort, UI shows native reasoning or selected effort, config persists default_thinking_effort, and reload raises Reload().
Model switch creates fresh session
src/pythinker_code/ui/shell/slash.py
For managed:lm-studio model changes, /model creates a new Session, clones state.additional_dirs, saves state when needed, prints status, and raises Reload with the new session id.
Tests and changelog
tests/core/test_cli_reload.py, tests/ui_and_conv/test_shell_slash_commands.py, CHANGELOG.md
Add regression test ensuring default MCP config is rechecked between loads, add model-switch test verifying fresh-session creation and config persistence, and update Unreleased changelog entry.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.82% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title follows conventional commits format (fix(shell): reload MCP and model sessions cleanly) and clearly describes the main changes across multiple files.
Description check ✅ Passed Description includes summary, root cause analysis, and verification steps. Related issue linking and all checklist items appear addressed based on changelog and test additions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/reload-mcp-model-context

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pythinker_code/app.py`:
- Around line 481-484: The current contextlib.suppress around awaiting
toolset.cleanup() hides all exceptions; change it to catch Exception as e and
log the failure before continuing so MCP transport cleanup errors are visible.
Locate the block using self._soul.agent.toolset and PythinkerToolset and replace
the contextlib.suppress(...) with an async-aware try/except that awaits
toolset.cleanup() and logs the exception (using the module logger or existing
logging facility, e.g., logger.exception or logger.error with exception info)
while allowing execution to continue.

In `@src/pythinker_code/ui/shell/slash.py`:
- Around line 360-363: The synchronous call to session.save_state() inside the
async model handler blocks the event loop; change the persistence call to run
off the loop (e.g., call await asyncio.to_thread(session.save_state) when you
create the new session in the model path) or convert Session.save_state() and
its internals (load_session_state/save_session_state/atomic_json_write) to async
and internally thread the blocking I/O. Specifically, locate the Session.create
usage and replace the direct session.save_state() invocation with an awaited
asyncio.to_thread wrapper (or update Session.save_state to an async method and
await it) so file write/flush/fsync happen off the event loop.

In `@tests/core/test_cli_reload.py`:
- Around line 11-21: The test relies on the real global MCP config path; patch
get_global_mcp_config_file() inside tests/core/test_cli_reload.py to return a
file under the pytest tmp_path (or tmp_path_factory) so the test writes and
reads only within the temporary directory. Update the test to monkeypatch or
inject a stub for get_global_mcp_config_file() before calling
_load_mcp_configs_from_cli_inputs(None, None), create the parent dir and write
the expected JSON into that stubbed path, and then assert the loader returns the
expected result; ensure you reference get_global_mcp_config_file and
_load_mcp_configs_from_cli_inputs in your change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a3efeec4-68a0-482f-998b-2509a1f9d46e

📥 Commits

Reviewing files that changed from the base of the PR and between 1dd9ed2 and 6498c15.

📒 Files selected for processing (6)
  • src/pythinker_code/app.py
  • src/pythinker_code/cli/__init__.py
  • src/pythinker_code/ui/shell/setup.py
  • src/pythinker_code/ui/shell/slash.py
  • tests/core/test_cli_reload.py
  • tests/ui_and_conv/test_shell_slash_commands.py

Comment thread src/pythinker_code/app.py
Comment thread src/pythinker_code/ui/shell/slash.py Outdated
Comment thread tests/core/test_cli_reload.py Outdated
@elkaix
elkaix merged commit 31a1342 into main Jun 2, 2026
26 checks passed
@elkaix
elkaix deleted the fix/reload-mcp-model-context branch June 2, 2026 03:33
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.

1 participant