Skip to content

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

Draft
wbxl2000 wants to merge 4 commits into
mainfrom
feat/plugin-reload-hot-apply
Draft

feat(plugins): hot-apply plugin changes to the current session on /plugins reload#218
wbxl2000 wants to merge 4 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 enabling one of its MCP servers, updates plugin state on disk but the running session does not see the new capabilities until /new. That makes additive changes more disruptive than necessary because users have to leave the current conversation to use newly available plugin skills or MCP tools.

What changed

/plugins reload now hot-applies additive plugin runtime changes to the current session:

  • Loads newly added plugin skills and refreshes skill-related runtime state so the next turn can see them.
  • Connects newly enabled plugin MCP servers without reconnecting existing servers.
  • Reports when removed, disabled, or updated plugin capabilities still require /new to fully apply.
  • Returns applied reload details through RPC/SDK and refreshes TUI skill slash commands after reload.
  • Updates English and Chinese plugin, MCP, and slash-command docs.
  • Adds a changeset for the CLI, SDK, and agent core package changes.

Verification

  • pnpm --filter @moonshot-ai/agent-core exec vitest run test/agent/injection/skill-refresh.test.ts test/harness/plugin-reload-session.test.ts test/mcp/connection-manager.test.ts test/plugin/manager.test.ts test/rpc/plugins-rpc.test.ts
  • pnpm exec vitest run apps/kimi-code/test/tui/components/dialogs/plugins-selector.test.ts apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts
  • pnpm --filter @moonshot-ai/agent-core run typecheck
  • pnpm --filter @moonshot-ai/kimi-code-sdk run typecheck
  • pnpm --filter @moonshot-ai/kimi-code run typecheck
  • pnpm -C docs run build
  • git diff --check

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: b53aef0

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 👍 / 👎.

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: e0fc42449a

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

if (registry === undefined) return undefined;
const baseline = this.agent.systemPromptSkillListing;
const current = registry.getModelSkillListing();
if (current.length === 0) return undefined;
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 Surface empty skill listings after resume

When a saved session is resumed after its only plugin skill was removed while the app was closed, the replayed config.update still supplies the old system prompt (checked in Session.resume, which only re-bootstrap profiles when the prompt is empty). Fresh evidence is this new early return: because the current registry listing is now empty, SkillRefreshInjector emits nothing, so the model keeps seeing the stale baked-in skill listing even though the skill/tool is no longer registered and /plugins reload has no way to correct it. The empty-listing case still needs a reminder that tells the model to disregard earlier skill listings.

Useful? React with 👍 / 👎.

Comment on lines +476 to +478
const activeStarts = new Set(
(main?.pluginSessionStarts ?? []).map((start) => `${start.pluginId}:${start.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 Detect replayed sessionStart drift after resume

When a saved session is resumed after a plugin's sessionStart was renamed or removed while the app was closed, the conversation still contains the old replayed plugin_session_start injection, but the resumed main.pluginSessionStarts is populated from the current plugin manager state. This comparison therefore treats the current desired keys as already active and reports needsNewSession: false, so /plugins reload does not tell the user to start a new session even though the stale startup block remains in context and cannot be retracted or replaced.

Useful? React with 👍 / 👎.

@wbxl2000 wbxl2000 marked this pull request as draft June 2, 2026 12:53
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