fix(reload): re-inject plugin session-start reminder after /reload (#1086)

* fix(reload): re-inject plugin session-start reminder after /reload

Reload reloaded plugins and resumed the session, but the model kept seeing the stale plugin session-start reminder from before the reload, so plugin skill changes only took effect in a fresh session.

Append a fresh plugin_session_start reminder to the main agent after reload, gated on a new forcePluginSessionStartReminder flag that only the explicit /reload command sets, so config and experiment toggles that reuse the reload RPC do not spam the transcript.

* fix(reload): keep reload result fresh and neutralize stale plugin reminder

Append the plugin session-start reminder before constructing ResumeSessionResult so SDK callers reading getResumeState() see the refreshed plugin context instead of a pre-reload snapshot.

When a plugin with a prior plugin_session_start reminder is disabled or removed, append a neutralizing reminder so the model does not keep following stale plugin instructions.

* fix(reload): neutralize stale plugin reminder after compaction

A full compaction folds the discrete plugin_session_start reminder into a compaction_summary, so the origin-only scan no longer detects it. Also treat a compaction_summary in history as a signal to neutralize, so disabling or removing a plugin after compaction still emits a superseding 'no active plugin session starts' reminder.

* fix(reload): thread plugin reminder option through KimiHarness.reloadSession

KimiHarness.reloadSession is a public SDK entry point; forward forcePluginSessionStartReminder to both the active-session and RPC reload paths so SDK callers using the harness can opt into the refreshed plugin reminder too.
This commit is contained in:
qer 2026-06-25 23:16:55 +08:00 committed by GitHub
parent 46808e1cff
commit fe667d7c2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 498 additions and 30 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
`/reload` now refreshes the assistant's view of plugin skills, so plugin changes take effect in the current session instead of requiring a new one.

View file

@ -16,7 +16,7 @@ export async function handleReloadCommand(host: SlashCommandHost): Promise<void>
const session = host.session;
if (session !== undefined) {
await session.reloadSession();
await session.reloadSession({ forcePluginSessionStartReminder: true });
await host.reloadCurrentSessionView(session, 'Session reloaded.');
}

View file

@ -72,7 +72,9 @@ auto_install = false
await handleReloadCommand(host);
expect(session.reloadSession).toHaveBeenCalledOnce();
expect(session.reloadSession).toHaveBeenCalledWith({
forcePluginSessionStartReminder: true,
});
expect(host.reloadCurrentSessionView).toHaveBeenCalledWith(
session,
'Session reloaded.',

View file

@ -3,6 +3,47 @@ import type { SkillDefinition } from '../../skill';
import { escapeXmlAttr } from '../../utils/xml-escape';
import { DynamicInjector } from './injector';
export interface RenderPluginSessionStartReminderInput {
readonly sessionStarts: readonly EnabledPluginSessionStart[];
readonly registry:
| {
getPluginSkill(pluginId: string, name: string): SkillDefinition | undefined;
renderSkillPrompt(skill: SkillDefinition, args: string): string;
}
| undefined;
readonly log?: { warn(message: string, payload?: unknown): void };
}
/**
* Renders the `<plugin_session_start>` reminder blocks for the currently enabled
* plugin session starts. Returns `undefined` when there is nothing to render
* (no session starts, no registry, or no resolvable skills).
*
* Shared by the turn-loop injector (which dedups against history) and the
* explicit `/reload` flow (which force-appends a fresh reminder).
*/
export function renderPluginSessionStartReminder(
input: RenderPluginSessionStartReminderInput,
): string | undefined {
const { sessionStarts, registry, log } = input;
if (sessionStarts.length === 0) return undefined;
if (registry === undefined) return undefined;
const blocks: string[] = [];
for (const sessionStart of sessionStarts) {
const skill = registry.getPluginSkill(sessionStart.pluginId, sessionStart.skillName);
if (skill === undefined) {
log?.warn('plugin sessionStart skill not found', {
pluginId: sessionStart.pluginId,
skillName: sessionStart.skillName,
});
continue;
}
blocks.push(renderSessionStartBlock(sessionStart, skill, registry.renderSkillPrompt(skill, '')));
}
if (blocks.length === 0) return undefined;
return blocks.join('\n');
}
export class PluginSessionStartInjector extends DynamicInjector {
protected override readonly injectionVariant = 'plugin_session_start';
@ -17,24 +58,11 @@ export class PluginSessionStartInjector extends DynamicInjector {
this.injectedAt = replayedAt;
return undefined;
}
const sessionStarts = this.agent.pluginSessionStarts ?? [];
if (sessionStarts.length === 0) return undefined;
const registry = this.agent.skills?.registry;
if (registry === undefined) return undefined;
const blocks: string[] = [];
for (const sessionStart of sessionStarts) {
const skill = registry.getPluginSkill(sessionStart.pluginId, sessionStart.skillName);
if (skill === undefined) {
this.agent.log.warn('plugin sessionStart skill not found', {
pluginId: sessionStart.pluginId,
skillName: sessionStart.skillName,
});
continue;
}
blocks.push(renderSessionStartBlock(sessionStart, skill, registry.renderSkillPrompt(skill, '')));
}
if (blocks.length === 0) return undefined;
return blocks.join('\n');
return renderPluginSessionStartReminder({
sessionStarts: this.agent.pluginSessionStarts,
registry: this.agent.skills?.registry,
log: this.agent.log,
});
}
}

View file

@ -76,6 +76,13 @@ export interface ResumeSessionPayload {
export interface ReloadSessionPayload {
readonly sessionId: string;
/**
* When true, append a fresh `<plugin_session_start>` system reminder to the
* main agent after the session is reloaded, reflecting the currently enabled
* plugins. Used by the explicit `/reload` command so the model sees plugin
* changes without starting a new session. Defaults to false.
*/
readonly forcePluginSessionStartReminder?: boolean;
}
export interface ForkSessionPayload {

View file

@ -357,7 +357,11 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
async resumeSessionWithOverrides(
input: ResumeSessionPayload,
overrides: { kaos?: Kaos; persistenceKaos?: Kaos },
overrides: {
kaos?: Kaos;
persistenceKaos?: Kaos;
forcePluginSessionStartReminder?: boolean;
},
): Promise<ResumeSessionResult> {
const summary = await this.sessionStore.get(input.sessionId);
const parentKaosForRead = overrides.kaos ?? (await this.getKaos());
@ -432,6 +436,11 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
throw error;
}
this.sessions.set(summary.id, session);
if (overrides.forcePluginSessionStartReminder === true) {
// Append before constructing the result so the returned ResumeSessionResult
// (and any SDK caller's resumeState) reflects the refreshed plugin context.
await session.appendPluginSessionStartReminder();
}
return resumeSessionResult(summary, session, warning);
}
@ -454,7 +463,10 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
await active.closeForReload();
this.sessions.delete(summary.id);
}
return this.resumeSession({ sessionId: summary.id });
return this.resumeSessionWithOverrides(
{ sessionId: summary.id },
{ forcePluginSessionStartReminder: input.forcePluginSessionStartReminder },
);
}
async forkSession(input: ForkSessionPayload): Promise<ResumeSessionResult> {

View file

@ -10,6 +10,7 @@ import type { KimiConfig, SDKSessionRPC } from '#/rpc';
import { proxyWithExtraPayload } from '#/rpc/types';
import { Agent, type AgentOptions, type AgentType } from '../agent';
import { renderPluginSessionStartReminder } from '../agent/injection/plugin-session-start';
import { HookEngine, type HookDef } from './hooks';
import type { PermissionManagerOptions, PermissionRule } from '../agent/permission';
import {
@ -545,6 +546,56 @@ export class Session {
}
}
/**
* Appends a fresh `<plugin_session_start>` system reminder to the main agent
* using the currently enabled plugins, then flushes records so the reminder is
* persisted and visible on the wire. Used by the explicit `/reload` flow after
* the session has been re-resumed with reloaded plugin state.
*
* When no plugin session start is currently resolvable but an earlier
* When no plugin session start is currently resolvable but the context may still
* carry stale plugin guidance either an earlier `<plugin_session_start>`
* reminder, or a compaction summary that may have folded one in appends a
* neutralizing reminder instead, so the model does not keep following stale
* plugin instructions and the turn-loop injector does not dedup against them.
*/
async appendPluginSessionStartReminder(): Promise<void> {
await this.skillsReady;
const mainAgent = this.requireMainAgent();
const reminder = renderPluginSessionStartReminder({
sessionStarts: mainAgent.pluginSessionStarts,
registry: mainAgent.skills?.registry,
log: mainAgent.log,
});
if (reminder !== undefined) {
mainAgent.context.appendSystemReminder(
`${reminder}\n\nThis supersedes any earlier plugin_session_start reminder in this session.`,
{ kind: 'injection', variant: 'plugin_session_start' },
);
} else if (this.shouldNeutralizePluginSessionStart(mainAgent)) {
mainAgent.context.appendSystemReminder(
'There are currently no active plugin session starts. This supersedes any earlier plugin_session_start reminder in this session.',
{ kind: 'injection', variant: 'plugin_session_start' },
);
} else {
return;
}
await mainAgent.records.flush();
}
private shouldNeutralizePluginSessionStart(mainAgent: Agent): boolean {
return mainAgent.context.history.some((message) => {
const kind = message.origin?.kind;
if (kind === 'injection') {
return message.origin?.variant === 'plugin_session_start';
}
// A compaction summary replaces earlier messages (including any plugin
// session-start reminder) with a single summary that may still carry stale
// plugin guidance, so the origin-only check above is not sufficient.
return kind === 'compaction_summary';
});
}
get hasActiveTurn(): boolean {
for (const agent of this.readyAgents()) {
if (agent.turn.hasActiveTurn) return true;

View file

@ -2,7 +2,10 @@ import { describe, expect, it } from 'vitest';
import type { Agent } from '../../../src/agent';
import type { PromptOrigin } from '../../../src/agent/context';
import { PluginSessionStartInjector } from '../../../src/agent/injection/plugin-session-start';
import {
PluginSessionStartInjector,
renderPluginSessionStartReminder,
} from '../../../src/agent/injection/plugin-session-start';
import type { EnabledPluginSessionStart } from '../../../src/plugin/types';
import type { SkillDefinition } from '../../../src/skill/types';
@ -210,3 +213,59 @@ describe('PluginSessionStartInjector', () => {
expect(text).not.toContain('project body');
});
});
describe('renderPluginSessionStartReminder', () => {
function registryFor(skills: readonly SkillDefinition[]) {
const byPluginAndName = new Map(
skills.flatMap((s) =>
s.plugin === undefined ? [] : [[`${s.plugin.id}\0${s.name.toLowerCase()}`, s] as const],
),
);
return {
getPluginSkill: (pluginId: string, name: string) =>
byPluginAndName.get(`${pluginId}\0${name.toLowerCase()}`),
renderSkillPrompt: (s: SkillDefinition) => s.content,
};
}
it('renders a block per resolvable sessionStart', () => {
const text = renderPluginSessionStartReminder({
sessionStarts: [{ pluginId: 'superpowers', skillName: 'using-superpowers' }],
registry: registryFor([
skill('using-superpowers', 'plugin body', { id: 'superpowers' }),
]),
});
expect(text).toContain(
'<plugin_session_start plugin="superpowers" skill="using-superpowers">',
);
expect(text).toContain('plugin body');
});
it('returns undefined when there are no sessionStarts', () => {
expect(
renderPluginSessionStartReminder({ sessionStarts: [], registry: registryFor([]) }),
).toBeUndefined();
});
it('returns undefined when the registry is unavailable', () => {
expect(
renderPluginSessionStartReminder({
sessionStarts: [{ pluginId: 'demo', skillName: 'x' }],
registry: undefined,
}),
).toBeUndefined();
});
it('returns undefined and warns when the skill cannot be resolved', () => {
const warnings: Array<{ message: string; payload?: unknown }> = [];
const text = renderPluginSessionStartReminder({
sessionStarts: [{ pluginId: 'demo', skillName: 'missing' }],
registry: registryFor([]),
log: { warn: (message, payload) => warnings.push({ message, payload }) },
});
expect(text).toBeUndefined();
expect(warnings).toContainEqual(
expect.objectContaining({ message: 'plugin sessionStart skill not found' }),
);
});
});

View file

@ -921,8 +921,268 @@ base_url = "https://search.example.test/v1"
});
expect(core.sessions.get(created.id)).toBe(active);
});
it('appends a fresh plugin_session_start reminder on forced reload', async () => {
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
const homeDir = join(tmp, 'home');
const workDir = join(tmp, 'work');
const pluginRoot = join(tmp, 'plugin');
await mkdir(homeDir, { recursive: true });
await mkdir(workDir, { recursive: true });
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
await writeSessionStartPlugin(pluginRoot, 'OLD BODY');
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
const core = new KimiCore(coreRpc, { homeDir });
const rpc = await sdkRpc({
emitEvent: vi.fn(),
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
requestQuestion: vi.fn(async () => null),
toolCall: vi.fn(async () => ({ output: '' })),
});
await core.installPlugin({ source: pluginRoot });
const created = await rpc.createSession({
id: 'ses_runtime_reload_reminder',
workDir,
model: 'default-mock',
});
// Before any forced reload the model has not been told about the plugin yet
// (no turn has run, so the turn-loop injector has not fired).
expect(pluginSessionStartReminders(core, created.id)).toHaveLength(0);
// Update the skill content on disk so the reload must pick up the new body.
// Preserve the SKILL.md frontmatter — the parser requires it to register the skill.
await writeFile(
managedSkillPath(homeDir),
`---\nname: greeter\ndescription: A greeter skill\n---\nNEW BODY\n`,
);
const reloaded = await rpc.reloadSession({
sessionId: created.id,
forcePluginSessionStartReminder: true,
});
const reminders = pluginSessionStartReminders(core, created.id);
expect(reminders).toHaveLength(1);
expect(reminders[0]).toContain('<plugin_session_start plugin="demo" skill="greeter">');
expect(reminders[0]).toContain('NEW BODY');
expect(reminders[0]).not.toContain('OLD BODY');
expect(reminders[0]).toContain('supersedes any earlier plugin_session_start');
// The returned ResumeSessionResult must already include the fresh reminder
// (otherwise SDK callers reading getResumeState() see stale plugin context).
const resultReminders = remindersFromHistory(
reloaded.agents['main']?.context.history ?? [],
);
expect(resultReminders).toHaveLength(1);
expect(resultReminders[0]).toContain('NEW BODY');
});
it('neutralizes a stale plugin_session_start reminder when the plugin is removed', async () => {
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
const homeDir = join(tmp, 'home');
const workDir = join(tmp, 'work');
const pluginRoot = join(tmp, 'plugin');
await mkdir(homeDir, { recursive: true });
await mkdir(workDir, { recursive: true });
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
await writeSessionStartPlugin(pluginRoot, 'BODY');
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
const core = new KimiCore(coreRpc, { homeDir });
const rpc = await sdkRpc({
emitEvent: vi.fn(),
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
requestQuestion: vi.fn(async () => null),
toolCall: vi.fn(async () => ({ output: '' })),
});
await core.installPlugin({ source: pluginRoot });
const created = await rpc.createSession({
id: 'ses_runtime_reload_neutralize',
workDir,
model: 'default-mock',
});
// First forced reload appends an active reminder, establishing a prior
// plugin_session_start in history.
await rpc.reloadSession({
sessionId: created.id,
forcePluginSessionStartReminder: true,
});
expect(pluginSessionStartReminders(core, created.id)).toHaveLength(1);
// Removing the plugin means no sessionStart is resolvable on the next reload;
// the stale reminder must be neutralized rather than left in place.
await core.removePlugin({ id: 'demo' });
await rpc.reloadSession({
sessionId: created.id,
forcePluginSessionStartReminder: true,
});
const reminders = pluginSessionStartReminders(core, created.id);
expect(reminders).toHaveLength(2);
expect(reminders.at(-1)).toContain('no active plugin session starts');
expect(reminders.at(-1)).toContain('supersedes any earlier plugin_session_start');
});
it('does not append a plugin_session_start reminder on reload without the force flag', async () => {
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
const homeDir = join(tmp, 'home');
const workDir = join(tmp, 'work');
const pluginRoot = join(tmp, 'plugin');
await mkdir(homeDir, { recursive: true });
await mkdir(workDir, { recursive: true });
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
await writeSessionStartPlugin(pluginRoot, 'BODY');
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
const core = new KimiCore(coreRpc, { homeDir });
const rpc = await sdkRpc({
emitEvent: vi.fn(),
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
requestQuestion: vi.fn(async () => null),
toolCall: vi.fn(async () => ({ output: '' })),
});
await core.installPlugin({ source: pluginRoot });
const created = await rpc.createSession({
id: 'ses_runtime_reload_no_force',
workDir,
model: 'default-mock',
});
await rpc.reloadSession({ sessionId: created.id });
expect(pluginSessionStartReminders(core, created.id)).toHaveLength(0);
});
it('appends nothing on forced reload when no plugin declares a sessionStart', async () => {
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
const homeDir = join(tmp, 'home');
const workDir = join(tmp, 'work');
const pluginRoot = join(tmp, 'plugin');
await mkdir(homeDir, { recursive: true });
await mkdir(workDir, { recursive: true });
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
await mkdir(pluginRoot, { recursive: true });
await writeFile(
join(pluginRoot, 'kimi.plugin.json'),
JSON.stringify({ name: 'demo', version: '1.0.0' }),
);
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
const core = new KimiCore(coreRpc, { homeDir });
const rpc = await sdkRpc({
emitEvent: vi.fn(),
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
requestQuestion: vi.fn(async () => null),
toolCall: vi.fn(async () => ({ output: '' })),
});
await core.installPlugin({ source: pluginRoot });
const created = await rpc.createSession({
id: 'ses_runtime_reload_no_sessionstart',
workDir,
model: 'default-mock',
});
await rpc.reloadSession({
sessionId: created.id,
forcePluginSessionStartReminder: true,
});
expect(pluginSessionStartReminders(core, created.id)).toHaveLength(0);
});
it('neutralizes stale plugin guidance after compaction when no sessionStart is active', async () => {
tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-'));
const homeDir = join(tmp, 'home');
const workDir = join(tmp, 'work');
await mkdir(homeDir, { recursive: true });
await mkdir(workDir, { recursive: true });
await writeFile(join(homeDir, 'config.toml'), baseModelConfig());
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
const core = new KimiCore(coreRpc, { homeDir });
const rpc = await sdkRpc({
emitEvent: vi.fn(),
requestApproval: vi.fn(async (): Promise<ApprovalResponse> => ({ decision: 'rejected' })),
requestQuestion: vi.fn(async () => null),
toolCall: vi.fn(async () => ({ output: '' })),
});
const created = await rpc.createSession({
id: 'ses_runtime_reload_compacted',
workDir,
model: 'default-mock',
});
const session = core.sessions.get(created.id);
const main = session?.getReadyAgent('main');
// Simulate a compaction that folded earlier messages (and any plugin guidance)
// into a single summary, leaving no discrete plugin_session_start behind.
main?.context.appendMessage({
role: 'assistant',
content: [{ type: 'text', text: 'summary of earlier conversation with plugin guidance' }],
toolCalls: [],
origin: { kind: 'compaction_summary' },
});
await session?.appendPluginSessionStartReminder();
const reminders = pluginSessionStartReminders(core, created.id);
expect(reminders).toHaveLength(1);
expect(reminders[0]).toContain('no active plugin session starts');
});
});
async function writeSessionStartPlugin(root: string, skillBody: string): Promise<void> {
await mkdir(join(root, 'skills', 'greeter'), { recursive: true });
await writeFile(
join(root, 'kimi.plugin.json'),
JSON.stringify({
name: 'demo',
version: '1.0.0',
skills: ['./skills'],
sessionStart: { skill: 'greeter' },
}),
);
await writeFile(
join(root, 'skills', 'greeter', 'SKILL.md'),
`---\nname: greeter\ndescription: A greeter skill\n---\n${skillBody}\n`,
);
}
function managedSkillPath(homeDir: string): string {
return join(homeDir, 'plugins', 'managed', 'demo', 'skills', 'greeter', 'SKILL.md');
}
function pluginSessionStartReminders(core: KimiCore, sessionId: string): string[] {
const agent = core.sessions.get(sessionId)?.getReadyAgent('main');
if (agent === undefined) return [];
return remindersFromHistory(agent.context.history);
}
function remindersFromHistory(
history: ReadonlyArray<{
role: string;
origin?: { kind: string; variant?: string };
content: ReadonlyArray<{ type: string; text?: string }>;
}>,
): string[] {
return history
.filter(
(message) =>
message.role === 'user' &&
message.origin?.kind === 'injection' &&
message.origin.variant === 'plugin_session_start',
)
.map((message) => message.content.map((part) => part.text ?? '').join(''));
}
async function readMainWire(sessionDir: string): Promise<readonly Record<string, unknown>[]> {
const wire = await readFile(join(sessionDir, 'agents', 'main', 'wire.jsonl'), 'utf-8');
return wire

View file

@ -22,6 +22,7 @@ import type {
ListSessionsOptions,
RenameSessionInput,
ResumeSessionInput,
ReloadSessionInput,
SessionSummary,
TelemetryClient,
TelemetryContextPatch,
@ -142,16 +143,21 @@ export class KimiHarness {
return session;
}
async reloadSession(input: ResumeSessionInput): Promise<Session> {
async reloadSession(input: ReloadSessionInput): Promise<Session> {
const id = normalizeSessionId(input.id);
const active = this.activeSessions.get(id);
if (active !== undefined) {
await active.reloadSession();
await active.reloadSession({
forcePluginSessionStartReminder: input.forcePluginSessionStartReminder,
});
this.trackSessionEvent(active.id, 'session_reload');
return active;
}
const summary = await this.rpc.reloadSession({ sessionId: id });
const summary = await this.rpc.reloadSession({
sessionId: id,
forcePluginSessionStartReminder: input.forcePluginSessionStartReminder,
});
const session = new Session({
id: summary.id,
workDir: summary.workDir,

View file

@ -66,6 +66,10 @@ export interface SessionIdRpcInput {
readonly sessionId: string;
}
export interface ReloadSessionRpcInput extends SessionIdRpcInput {
readonly forcePluginSessionStartReminder?: boolean;
}
export interface SetSessionModelRpcInput extends SessionIdRpcInput {
readonly model: string;
}
@ -150,9 +154,12 @@ export abstract class SDKRpcClientBase {
return this.resumeSession(input);
}
async reloadSession(input: SessionIdRpcInput): Promise<ResumedSessionSummary> {
async reloadSession(input: ReloadSessionRpcInput): Promise<ResumedSessionSummary> {
const rpc = await this.getRpc();
return rpc.reloadSession({ sessionId: input.sessionId });
return rpc.reloadSession({
sessionId: input.sessionId,
forcePluginSessionStartReminder: input.forcePluginSessionStartReminder,
});
}
async forkSession(input: ForkSessionInput): Promise<SessionSummary> {

View file

@ -22,6 +22,7 @@ import type {
PluginInfo,
PluginSummary,
PromptInput,
ReloadSessionOptions,
ReloadSummary,
ResumedSessionState,
ResumedSessionSummary,
@ -68,9 +69,12 @@ export class Session {
return this.resumeState;
}
async reloadSession(): Promise<ResumedSessionSummary> {
async reloadSession(options?: ReloadSessionOptions): Promise<ResumedSessionSummary> {
this.ensureOpen();
const summary = await this.rpc.reloadSession({ sessionId: this.id });
const summary = await this.rpc.reloadSession({
sessionId: this.id,
forcePluginSessionStartReminder: options?.forcePluginSessionStartReminder,
});
this.summary = summary;
this.resumeState = resumeStateFromSummary(summary);
return summary;

View file

@ -118,6 +118,10 @@ export interface ResumeSessionInput {
readonly sessionStartedProperties?: TelemetryProperties;
}
export interface ReloadSessionInput extends ResumeSessionInput {
readonly forcePluginSessionStartReminder?: boolean;
}
export interface AddAdditionalDirInput {
readonly id: string;
readonly path: string;
@ -166,6 +170,10 @@ export interface CompactOptions {
readonly instruction?: string | undefined;
}
export interface ReloadSessionOptions {
readonly forcePluginSessionStartReminder?: boolean;
}
export interface PlanInfo {
readonly id: string;
readonly content: string;

View file

@ -387,4 +387,23 @@ micro_compaction = false
expect(session.getResumeState()?.agents['main']).toBeDefined();
await expect(session.getStatus()).resolves.toMatchObject({ model: 'kimi-for-coding' });
});
it('forwards forcePluginSessionStartReminder to the active session reload', async () => {
const homeDir = await makeTempDir();
const workDir = join(homeDir, 'work');
const configPath = join(homeDir, 'config.toml');
await writeFile(configPath, COMPLETE_TOML, 'utf-8');
const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY });
const session = await harness.createSession({
id: 'session-sdk-reload-forward',
workDir,
model: 'kimi-for-coding',
});
const reloadSpy = vi.spyOn(session, 'reloadSession').mockResolvedValue({} as never);
await harness.reloadSession({ id: session.id, forcePluginSessionStartReminder: true });
expect(reloadSpy).toHaveBeenCalledWith({ forcePluginSessionStartReminder: true });
});
});