Skip to content

feat(plugins): hot-apply plugin changes to the current session on /plugins reload#218

Open
wbxl2000 wants to merge 2 commits into
mainfrom
feat/plugin-reload-hot-apply
Open

feat(plugins): hot-apply plugin changes to the current session on /plugins reload#218
wbxl2000 wants to merge 2 commits into
mainfrom
feat/plugin-reload-hot-apply

Conversation

@wbxl2000
Copy link
Copy Markdown
Collaborator

@wbxl2000 wbxl2000 commented May 29, 2026

Related Issue

No prior issue — the problem is described below.

Problem

Installing or enabling a plugin (or toggling one of its MCP servers) only changed on-disk state; the running session never picked up the new capabilities. To use a freshly installed plugin's skills or MCP tools, users had to start a new session with /new — losing their current conversation and context. (This is the same gap Claude Code closes with /reload-plugins.)

What changed

/plugins reload now hot-applies plugin changes to the current session — no /new required.

  • agent-core — two new entry points:
    • PluginManager.runtimeSnapshot() bundles the enabled plugins' skill roots, MCP servers, and sessionStarts into one immutable snapshot.
    • Session.applyPluginRuntimeSnapshot() applies the additive parts to the live session: loads newly added skills, refreshes builtin tools so the Skill tool appears once skills exist, and connects only newly enabled MCP servers (existing ones untouched; their tools register via the existing status mechanism and are available on the next turn).
  • Model learns new skills via injection, not a prompt rewrite. A new SkillRefreshInjector appends the current skill listing as a system reminder when it drifts from the prompt baseline (the listing's "DISREGARD any earlier skill listings" header supersedes the stale one). This keeps the prompt-cache prefix stable and preserves the "system prompt set once" invariant; it re-injects after compaction, and resume seeds the baseline so a post-resume reload still surfaces skills.
  • RPC / SDKreloadPlugins is now session-scoped and returns what was applied (PluginReloadResult with addedSkills / addedMcpServers / needsNewSession). Backward-compatible (a superset of the old ReloadSummary).
  • TUI/plugins reload reports a summary and refreshes skill slash-commands; the call-to-action copy points to a highlighted /plugins reload instead of /new.
  • Additive only; teardown/replace needs a new session. Disable, remove, and in-place updates are not torn down live. needsNewSession detects this — including a same-named MCP server whose config changed, or a skill removed/renamed by a plugin update — so the TUI tells the user when /new is still required.

Design choice: an explicit /plugins reload (like Claude Code's /reload-plugins) rather than auto-applying on install — predictable behavior, a single safe apply point between turns, and it avoids applying a Core-level shared-manager mutation to a specific session implicitly.

Docs (en + zh: customization/plugins.md, customization/mcp.md, reference/slash-commands.md) are updated to correct the previous "plugin changes apply to new sessions only" guidance.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

…ugins reload

`/plugins reload` now hot-loads newly installed or enabled plugin skills and
connects newly enabled MCP servers into the running session — no /new required.

- agent-core: add PluginManager.runtimeSnapshot() and
  Session.applyPluginRuntimeSnapshot(); re-render the main agent's system
  prompt and refresh builtin tools so new skills (and the Skill tool) appear;
  add McpConnectionManager.connect() for incremental, non-disruptive connects;
  make reloadPlugins session-scoped and keep apply failures from poisoning the
  plugin-load error state; report only servers that actually connected.
- node-sdk: reloadPlugins() returns the applied result (PluginReloadResult).
- kimi-code: /plugins reload reports what was applied and refreshes skill
  slash-commands; call-to-action points to a highlighted /plugins reload
  instead of /new.
- Disable/remove/update/sessionStart are not torn down live; reload flags this
  via needsNewSession.
- Tests, docs (en+zh), and a changeset.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 29, 2026

🦋 Changeset detected

Latest commit: 717018b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@moonshot-ai/agent-core Minor
@moonshot-ai/kimi-code-sdk Minor
@moonshot-ai/kimi-code Minor
@moonshot-ai/migration-legacy Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 29, 2026

pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@717018b
npx https://pkg.pr.new/@moonshot-ai/kimi-code@717018b

commit: 717018b

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bca3024eab

ℹ️ 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".

Comment on lines +422 to +424
const stalePluginSkills = [...this.loadedPluginSkillIds].some(
(id) => !desiredSkillPlugins.has(id),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Flag removed plugin skills for a new session

When an installed plugin is updated in place and keeps the same plugin id but removes or renames one of its skills, the old skill stays in SkillRegistry because loadRoots only adds/overwrites discovered skills and has no unload path. This check only compares plugin ids, so the updated plugin id is still considered desired and needsNewSession stays false; /plugins reload then refreshes slash commands/system prompt while the stale skill remains available without telling the user to start a new session.

Useful? React with 👍 / 👎.

Comment on lines +382 to +385
const existingServers = new Set(this.mcp.list().map((entry) => entry.name));
const newServers = Object.fromEntries(
Object.entries(snapshot.mcpServers).filter(([name]) => !existingServers.has(name)),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Flag existing plugin MCP config changes as unapplied

If a plugin is updated but keeps the same MCP server name while changing its command, env, cwd, or URL, reload never applies that new config because any existing runtime name is filtered out here. needsNewSession later compares only server names, so it also reports no drift; the user is told the reload is active while the session is still using the old MCP entry/config until a new session is started.

Useful? React with 👍 / 👎.

Comment on lines +428 to +430
const stalePluginServer = this.mcp
.list()
.some((entry) => entry.name.startsWith('plugin-') && !desiredServers.has(entry.name));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't classify user MCP servers by name prefix

User and project mcp.json server names are arbitrary (the loader accepts any string key), so a non-plugin server named like plugin-local or plugin-foo:bar is now treated as stale plugin runtime whenever /plugins reload runs and the plugin snapshot does not contain that name. In those sessions, reload incorrectly reports needsNewSession even though no plugin capability is stale; track plugin-origin server names instead of using the public server-name prefix.

Useful? React with 👍 / 👎.

const line = `Reload: +${summary.added.length} -${summary.removed.length}` +
(summary.errors.length > 0 ? ` (${summary.errors.length} errors)` : '');
const session = host.requireSession();
const summary = await session.reloadPlugins();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep hot reload out of active turns

/plugins is registered as available while streaming, and slash commands bypass the normal message queue, so running /plugins reload during an active response now mutates the live session immediately by loading skills, rerendering the main system prompt, and connecting MCP servers. The commit describes reload as a safe apply point between turns, but without an idle guard here the next tool-loop/model call in the same turn can see a different prompt/tool set than the one the turn started with; block the reload subcommand while streamingPhase/compaction is active or queue it until idle.

Useful? React with 👍 / 👎.

Comment on lines +439 to +440
if (desiredStarts.length !== activeStarts.size) return true;
return desiredStarts.some((key) => !activeStarts.has(key));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Detect unchanged-key sessionStart updates

When a plugin that already contributed a sessionStart is updated but keeps the same plugin id and skill name, this key-only comparison reports no drift even if the startup skill's content or plugin instructions changed. Since sessionStart blocks are injected only once and the old block is already in the conversation context, /plugins reload cannot apply that update, so the user needs /new but is not told so.

Useful? React with 👍 / 👎.

…n-place plugin updates

Two review fixes on top of the /plugins reload hot-apply:

1. Don't rewrite the main agent's system prompt on reload. Rewriting it busts
   the prompt-cache prefix for the whole conversation, breaks the "system prompt
   set once" invariant, and silently refreshed cwd/AGENTS.md too. Instead add a
   SkillRefreshInjector that appends the current skill listing as a system
   reminder when it drifts from the prompt baseline; the listing's "DISREGARD
   any earlier skill listings" header supersedes the stale one, and the base
   injector re-injects after compaction. Resume seeds the baseline so a
   post-resume reload still surfaces new skills (native resume skips useProfile).

2. needsNewSession now detects capabilities CHANGED IN PLACE by a plugin update,
   not just added/removed. Track a config digest per connected plugin MCP server
   and compare loaded plugin skills (additive registry) against the plugins'
   current declaration: a same-named MCP server whose command/args/env/cwd
   changed, or a removed/renamed skill, now flags needsNewSession. New skills /
   servers remain additive (needsNewSession stays false).

Removes Agent.rerenderSystemPrompt. Adds tests for the injector and for the
in-place-update and resume-baseline cases.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 717018bb6b

ℹ️ 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".

export * from './subagent-host';

function pluginSkillKey(pluginId: string, skillName: string): string {
return `${pluginId}${skillName}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Replace the literal NUL in the source

This template literal contains an actual NUL byte in the .ts file rather than the escaped sequence \0. For any workflow that scans this file as text, this makes the file look binary — for example, rg pluginSkillKey packages/agent-core/src/session/index.ts reports a binary match instead of showing lines — so code search, review tooling, and text-processing scripts can skip or mishandle this module. Use an escaped separator (\0) or another printable delimiter so the runtime key stays distinct without embedding a binary byte in source.

Useful? React with 👍 / 👎.

Comment on lines +197 to +200
const skillListing = this.skills.getModelSkillListing();
for (const agent of this.agents.values()) {
agent.systemPromptSkillListing ??= skillListing;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't seed resumed prompts with the live skill set

When a saved session is resumed after plugins changed while it was closed, the replayed system prompt still contains the old skill listing, but loadSkills() has already loaded the current plugin roots before this assignment. Seeding systemPromptSkillListing from the current registry makes SkillRefreshInjector believe the model has already seen those newly installed skills (or no longer sees removed ones), so even running /plugins reload after resume will not inject the corrected listing and the model keeps operating from the stale prompt.

Useful? React with 👍 / 👎.

mergeAllAvailableSkills: this.options.skills?.mergeAllAvailableSkills,
builtinDir: this.options.skills?.builtinDir,
});
await this.skills.loadRoots(roots);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid hot-swapping existing plugin skill bodies

When the same plugin id is reinstalled with the same skill name but changed SKILL.md content or skillInstructions, this reload path re-scans all plugin roots and SkillRegistry.loadRoots() overwrites the existing byName entry, so the running session silently starts invoking the updated skill while needsNewSession remains false because the skill key set did not change. That violates the additive-only contract for plugin updates and can change tool behavior mid-conversation without telling the user to start a new session.

Useful? React with 👍 / 👎.

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