Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/agent-core-dev/orient.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ So a Session-scoped service is not "L1" — e.g. `session` is Session-scoped but
| L3 | registries & capabilities | `tool`, `toolRegistry`, `permission*`, `flag`, `skill`, `plugin` |
| L4 | agent behaviour | `turn`, `loop`, `prompt`, `profile`, `contextMemory`, `goal`, `plan`, `swarm` |
| L5 | async lifecycle | `background`, `mcp`, `cron`, `agentTool` |
| L6 | coordination | `session`, `agentLifecycle`, `sessionMetadata`, `interaction`, `terminal` |
| L6 | coordination | `session`, `agentLifecycle`, `sessionMetadata`, `interaction`, `terminal`, `undo` |
| L7 | boundary / edge | `gateway`, `rpc`, `approval`, `question`, `*Legacy` |

## File-header comment convention
Expand Down
5 changes: 0 additions & 5 deletions .changeset/agent-scope-state-container.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/defer-user-tools.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/eager-scope-instantiation.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/fix-web-clipboard-paste.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/goal-steer-queued-messages.md

This file was deleted.

5 changes: 5 additions & 0 deletions .changeset/host-identity-system-prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/agent-core-v2": patch
---

Let embedding hosts customize the agent's product name and reply-style guidance in the system prompt when starting the server.
5 changes: 5 additions & 0 deletions .changeset/plugin-quota-note.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Show a quota consumption note after installing official plugins that bill against plan quota (such as Kimi Datasource).
5 changes: 5 additions & 0 deletions .changeset/plugin-update-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Show an update notice when a turn that used an outdated plugin ends and the Official Marketplace has a newer version; each new version is announced once. Run /plugins to install the latest version.
5 changes: 0 additions & 5 deletions .changeset/session-scope-state-container.md

This file was deleted.

9 changes: 5 additions & 4 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

This file tracks **what diverges from upstream** (`MoonshotAI/kimi-code`) so rebases do not drop local product behavior. Update it whenever a fork-only feature is added, restored, or abandoned.

**Upstream base we track:** `@moonshot-ai/kimi-code@0.29.1` / `main@origin` `c497af60e6cd20aab05e590f98a28fb15dd3491d`
**Upstream base we track:** `@moonshot-ai/kimi-code@0.29.2` / `main@origin` `77618e38c35a81e26134b3f83eb7f2b460c0ee05`
**Local main tip:** see `jj log -r main`
**Backup before this safe port:** bookmark `backup/pre-0.29.1` at `117f60d4816926a68e7d584f5d6f04e9dcd66411`
**Backup before this safe port:** bookmark `backup/pre-0.29.2-port` at `b01dc627cad94603a0246339ec527504690f7968`

## How to rebase without losing options

Expand Down Expand Up @@ -118,6 +118,7 @@ If something disappears after syncing origin, compare against `backup/pre-*` and
| Bookmark | Meaning |
| --- | --- |
| `main` | Shipping fork tip |
| `main@origin` | Upstream tip (`c497af60…`) |
| `backup/pre-0.29.1` | Pre-port local tip at `117f60d4816926a68e7d584f5d6f04e9dcd66411` |
| `main@origin` | Upstream tip (`77618e38…`, post-0.29.2) |
| `backup/pre-0.29.2-port` | Pre-port local tip at `b01dc627cad94603a0246339ec527504690f7968` |
| `backup/pre-0.29.1` | Older pre-0.29.1 local tip at `117f60d4816926a68e7d584f5d6f04e9dcd66411` |
| `feat/disabled-skills` | Branch for upstream PR #1983; keep untouched during fork ports |
20 changes: 20 additions & 0 deletions apps/kimi-code/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# @moonshot-ai/kimi-code

## 0.29.2

### Patch Changes

- [#2192](https://github.com/MoonshotAI/kimi-code/pull/2192) [`7799bd7`](https://github.com/MoonshotAI/kimi-code/commit/7799bd7346aaee11ec2b6d6883e1e4fe5ab10717) Thanks [@sailist](https://github.com/sailist)! - Hold per-agent runtime state of the experimental engine in the agent-scope state container, so it is observable in one place and disposed with the agent; state snapshots collapse class instances to name markers so resource graphs cannot exhaust memory during export.

- [#2119](https://github.com/MoonshotAI/kimi-code/pull/2119) [`f06eb5c`](https://github.com/MoonshotAI/kimi-code/commit/f06eb5c60e0a4e51162d1854dda1db41892b457c) Thanks [@pvzheroes125](https://github.com/pvzheroes125)! - Allow hosts to defer registered user-tool schemas until needed. Set `disclosure: "deferred"` when registering a tool.

- [#2192](https://github.com/MoonshotAI/kimi-code/pull/2192) [`7799bd7`](https://github.com/MoonshotAI/kimi-code/commit/7799bd7346aaee11ec2b6d6883e1e4fe5ab10717) Thanks [@sailist](https://github.com/sailist)! - Instantiate every registered service eagerly at scope creation on the experimental engine, following the dependency graph automatically, and drop the hand-maintained lists that resolved side-effect services one by one at startup.

- [#2120](https://github.com/MoonshotAI/kimi-code/pull/2120) [`0d00a07`](https://github.com/MoonshotAI/kimi-code/commit/0d00a07c02e334ca904077b2ea8c56cf58b44586) Thanks [@yicun](https://github.com/yicun)! - web: Fix copying selected chat text over plain HTTP from replacing the clipboard with an event placeholder.

- [#2210](https://github.com/MoonshotAI/kimi-code/pull/2210) [`0cef160`](https://github.com/MoonshotAI/kimi-code/commit/0cef160c4b900a3d78212cd5da4b80d335ea0b6f) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix goal pursuit being interrupted when a goal turn reaches the per-turn step limit (`loop_control.max_steps_per_turn`); the limit now splits goal work into more continuation turns instead of pausing the goal.

- [#2153](https://github.com/MoonshotAI/kimi-code/pull/2153) [`c497af6`](https://github.com/MoonshotAI/kimi-code/commit/c497af60e6cd20aab05e590f98a28fb15dd3491d) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix messages sent while a goal is running being rejected with a "Cannot launch a new turn while another turn is active" error; they are now steered into the active goal turn instead of being dropped.

- [#2192](https://github.com/MoonshotAI/kimi-code/pull/2192) [`7799bd7`](https://github.com/MoonshotAI/kimi-code/commit/7799bd7346aaee11ec2b6d6883e1e4fe5ab10717) Thanks [@sailist](https://github.com/sailist)! - Hold per-session runtime state of the experimental engine in the session-scope state container, so it is observable in one place and disposed with the session.

- [#2055](https://github.com/MoonshotAI/kimi-code/pull/2055) [`d40d0d3`](https://github.com/MoonshotAI/kimi-code/commit/d40d0d305d2866cb5ab8696e559e0813b5f92201) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix /undo to restore conversation history, todo lists, plan mode, and task notifications consistently.

## 0.29.1

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-code/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@moonshot-ai/kimi-code",
"version": "0.29.1",
"version": "0.29.2",
"description": "The Starting Point for Next-Gen Agents",
"license": "MIT",
"author": "Moonshot AI",
Expand Down
4 changes: 4 additions & 0 deletions apps/kimi-code/src/cli/sub/web/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ async function runServerInProcess(
allowRemoteTerminals: options.allowRemoteTerminals,
allowedHosts: options.allowedHosts,
disableAuth: options.dangerousBypassAuth,
// Attach the engine's cloud telemetry appender (still gated by the config
// `telemetry` toggle). Complements the v1 client registered above, which
// only covers host-level events.
telemetry: true,
// Seed the CLI's Kimi identity headers so the engine's outbound
// requests (model, WebSearch, FetchURL) carry the same User-Agent +
// X-Msh-* identity as direct CLI runs.
Expand Down
4 changes: 4 additions & 0 deletions apps/kimi-code/src/constant/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const KIMI_CODE_UPDATE_STATE_FILE_NAME = 'latest.json';
export const KIMI_CODE_UPDATE_INSTALL_STATE_FILE_NAME = 'install.json';
export const KIMI_CODE_UPDATE_INSTALL_LOCK_FILE_NAME = 'install.lock';
export const KIMI_CODE_UPDATE_ROLLOUT_LOG_FILE_NAME = 'rollout.log';
export const KIMI_CODE_PLUGIN_UPDATE_NOTICE_STATE_FILE_NAME = 'plugin-notices.json';
export const KIMI_CODE_INPUT_HISTORY_DIR_NAME = 'user-history';
export const KIMI_CODE_BANNER_DIR_NAME = 'banner';
export const KIMI_CODE_BANNER_STATE_FILE_NAME = 'state.json';
Expand Down Expand Up @@ -79,6 +80,9 @@ export const KIMI_CODE_CDN_LATEST_JSON_URL = `${KIMI_CODE_CDN_BASE}/latest.json`
export const KIMI_CODE_TIPS_BANNER_URL = 'https://cdn.kimi.com/kimi-code-tips/tips.json';
export const KIMI_CODE_PLUGIN_MARKETPLACE_URL = `${KIMI_CODE_CDN_BASE}/plugins/marketplace.json`;
export const KIMI_CODE_PLUGIN_MARKETPLACE_URL_ENV = 'KIMI_CODE_PLUGIN_MARKETPLACE_URL';
// Official plugins whose usage bills against the user's plan quota. Installing
// one of these shows a quota note after the install result.
export const QUOTA_CONSUMING_PLUGIN_IDS: readonly string[] = ['kimi-datasource'];
export const KIMI_CODE_INSTALL_SH_URL = `${KIMI_CODE_CDN_BASE}/install.sh`;
export const KIMI_CODE_INSTALL_PS1_URL = `${KIMI_CODE_CDN_BASE}/install.ps1`;
// Official download page, referenced by prompt copy that steers users away
Expand Down
14 changes: 13 additions & 1 deletion apps/kimi-code/src/tui/commands/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ import {
} from '../components/messages/plugins-status-panel';
import { UsagePanelComponent } from '../components/messages/usage-panel';
import { formatErrorMessage } from '../utils/event-payload';
import { formatPluginSourceLabel, isOfficialPluginSource } from '../utils/plugin-source-label';
import {
formatPluginSourceLabel,
isOfficialPluginInstall,
isOfficialPluginSource,
} from '../utils/plugin-source-label';
import { QUOTA_CONSUMING_PLUGIN_IDS } from '#/constant/app';
import { loadPluginMarketplace } from '#/utils/plugin-marketplace';
import { openUrl } from '#/utils/open-url';
import type { SlashCommandHost } from './dispatch';
Expand Down Expand Up @@ -492,6 +497,8 @@ async function installPluginFromSource(

const PLUGIN_RELOAD_HINT = 'Run /new or /reload to apply plugin changes.';

const PLUGIN_QUOTA_NOTE = 'Note: This plugin consumes your quota.';

function showPluginInstallResult(
host: SlashCommandHost,
beforeList: readonly PluginSummary[],
Expand All @@ -506,6 +513,11 @@ function showPluginInstallResult(
const action = describeInstallAction(previous, summary);
host.showStatus(`${action} (${summary.id}).${mcpHint}`);
host.showStatus(PLUGIN_RELOAD_HINT, 'warning');
// Gate on provenance, not just the id: a local/GitHub fork whose manifest
// reuses a billed plugin's id is not the official quota-consuming build.
if (QUOTA_CONSUMING_PLUGIN_IDS.includes(summary.id) && isOfficialPluginInstall(summary)) {
host.showStatus(PLUGIN_QUOTA_NOTE, 'warning');
}
}

function describeInstallAction(
Expand Down
208 changes: 208 additions & 0 deletions apps/kimi-code/src/tui/controllers/plugin-update-notifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import type { PluginSummary } from '@moonshot-ai/kimi-code-sdk';

import { KIMI_CODE_PLUGIN_MARKETPLACE_URL } from '#/constant/app';
import {
computeUpdateStatus,
loadPluginMarketplace,
type PluginMarketplace,
} from '#/utils/plugin-marketplace';
import {
readPluginUpdateNoticeState,
writePluginUpdateNoticeState,
} from '#/utils/plugin-update-notice-state';
import { isOfficialPluginInstall } from '../utils/plugin-source-label';

/**
* The slice of the SDK session the notifier reads. Structurally satisfied by
* the full SDK `Session`, and easy to fake in tests.
*/
export interface PluginUpdateNotifierSession {
listMcpServers(): Promise<readonly { name: string }[]>;
listPlugins(): Promise<readonly PluginSummary[]>;
}

export interface PluginUpdateNotifierDeps {
readonly getSession: () => PluginUpdateNotifierSession | undefined;
readonly workDir: string;
readonly notify: (message: string) => void;
/** Overridable for tests; defaults to the shared marketplace loader. */
readonly loadMarketplace?: () => Promise<PluginMarketplace>;
/** Overridable for tests; defaults to the updates dir under the data dir. */
readonly stateFile?: string;
}

const MCP_TOOL_NAME_PREFIX = 'mcp__';
const PLUGIN_MCP_TOOL_NAME_PREFIX = `${MCP_TOOL_NAME_PREFIX}plugin-`;
// Plugin MCP servers run under the runtime name `plugin-<id>:<server>`
// (pluginMcpRuntimeName in packages/agent-core/src/plugin/manager.ts).
const PLUGIN_MCP_RUNTIME_NAME = /^plugin-([a-z0-9][a-z0-9_-]{0,63}):/;

/** Cheap name check for plugin-provided MCP tools (`mcp__plugin-…`). */
export function isPluginMcpToolName(toolName: string): boolean {
return toolName.startsWith(PLUGIN_MCP_TOOL_NAME_PREFIX);
}

/**
* Mirror of sanitizeMcpNamePart in packages/agent-core/src/mcp/tool-naming.ts.
* MCP tool names on the wire carry the sanitized server name; the collapse
* step guarantees the `__` separator never appears inside a name part.
*/
function sanitizeMcpServerName(name: string): string {
return name.replaceAll(/[^a-zA-Z0-9_-]/g, '_').replaceAll(/_+/g, '_');
}

/**
* Find the plugin behind a qualified MCP tool name by longest-prefix match
* against known server names. Prefix matching (rather than splitting on the
* `__` separator) survives core's 64-char truncation, which can cut the
* separator before appending the hash suffix; the boundary check keeps a
* shorter server name from matching another server's name, and longest match
* wins when one server name is a prefix of another. A name truncated inside
* the server part itself cannot be attributed reliably and stays unresolved.
*/
function matchPluginByToolName(
toolName: string,
serverPluginIds: Map<string, string>,
): string | undefined {
let best: string | undefined;
let bestLength = 0;
for (const [serverName, pluginId] of serverPluginIds) {
const prefix = `${MCP_TOOL_NAME_PREFIX}${serverName}`;
if (!toolName.startsWith(prefix)) continue;
const boundary = toolName.charAt(prefix.length);
if (boundary !== '' && boundary !== '_') continue;
if (prefix.length > bestLength) {
best = pluginId;
bestLength = prefix.length;
}
}
return best;
}

/**
* Shows a one-time "update detected" notice for outdated plugins. Callers
* report completed plugin usage (a plugin MCP tool name, or the plugin id of
* a `/<plugin>:<command>` turn — both reported once the turn's output has
* ended); the notifier checks the marketplace and persists the last notified
* version, so a plugin is re-notified only when the marketplace advertises a
* newer version than the one already shown.
*
* Entry points are fire-and-forget in production (never reject — the notice
* is a background nicety and any failure, e.g. an offline marketplace, is
* swallowed) and return an awaitable promise so tests can settle the queue
* deterministically.
*/
export class PluginUpdateNotifier {
private marketplacePromise: Promise<PluginMarketplace> | undefined;
private mcpServerPluginIds: Map<string, string> | undefined;
private readonly inFlight = new Set<string>();
private queue: Promise<void> = Promise.resolve();

constructor(private readonly deps: PluginUpdateNotifierDeps) {}

handleMcpToolCompleted(toolName: string): Promise<void> {
// Cheap bail before touching the RPC layer — most tools are not MCP tools,
// let alone plugin ones.
if (!isPluginMcpToolName(toolName)) return Promise.resolve();
return this.resolvePluginId(toolName)
.then((pluginId) => {
if (pluginId !== undefined) return this.enqueue(pluginId);
return undefined;
})
.catch(() => {});
}

handlePluginCommandCompleted(pluginId: string): Promise<void> {
return this.enqueue(pluginId);
}

private enqueue(pluginId: string): Promise<void> {
// Serialize the read-modify-write cycle on the notice state file: two
// concurrent checks (e.g. a turn that used two outdated plugins) would
// otherwise read the same snapshot and the last write would drop the
// other plugin's entry.
this.queue = this.queue.then(() => this.checkAndNotify(pluginId)).catch(() => {});
return this.queue;
}

private async resolvePluginId(toolName: string): Promise<string | undefined> {
const hit = matchPluginByToolName(toolName, await this.getMcpServerPluginIds());
if (hit !== undefined) return hit;
// The map is memoized, but this notifier is reused across /reload, /new,
// and session switches, so plugins installed or enabled later in the same
// app run are missing from it. Refresh once on a miss before giving up.
return matchPluginByToolName(toolName, await this.loadMcpServerPluginIds());
}

private async getMcpServerPluginIds(): Promise<Map<string, string>> {
if (this.mcpServerPluginIds !== undefined) return this.mcpServerPluginIds;
return this.loadMcpServerPluginIds();
}

private async loadMcpServerPluginIds(): Promise<Map<string, string>> {
const map = new Map<string, string>();
const session = this.deps.getSession();
// Without a session there is nothing to list; leave the cache unset so
// the next lookup retries instead of pinning an empty map.
if (session === undefined) return map;
const servers = await session.listMcpServers();
for (const server of servers) {
const match = PLUGIN_MCP_RUNTIME_NAME.exec(server.name);
if (match?.[1] !== undefined) {
map.set(sanitizeMcpServerName(server.name), match[1]);
}
}
this.mcpServerPluginIds = map;
return map;
}

private async checkAndNotify(pluginId: string): Promise<void> {
if (this.inFlight.has(pluginId)) return;
this.inFlight.add(pluginId);
try {
const session = this.deps.getSession();
if (session === undefined) return;
const marketplace = await this.loadCatalog();
// Only the default official catalog can back an "Official Marketplace"
// notice — a custom catalog (KIMI_CODE_PLUGIN_MARKETPLACE_URL) may
// advertise anything under any id.
if (marketplace.source !== KIMI_CODE_PLUGIN_MARKETPLACE_URL) return;
const entry = marketplace.plugins.find((plugin) => plugin.id === pluginId);
if (entry === undefined) return;
const installed = (await session.listPlugins()).find((plugin) => plugin.id === pluginId);
if (installed === undefined) return;
// Only official installs are tracked against the Official Marketplace —
// a local/GitHub fork that happens to share a catalog id is not it.
if (!isOfficialPluginInstall(installed)) return;
const status = computeUpdateStatus(entry.version, installed.version, true);
if (status.kind !== 'update') return;
const state = await readPluginUpdateNoticeState(this.deps.stateFile);
if (state.notified[pluginId] === status.latest) return;
this.deps.notify(
`Update detected: ${installed.displayName} ${status.latest} is available. ` +
'Run /plugins to install the latest version from the Official Marketplace.',
);
await writePluginUpdateNoticeState(
{ ...state, notified: { ...state.notified, [pluginId]: status.latest } },
this.deps.stateFile,
);
} finally {
this.inFlight.delete(pluginId);
}
}

private loadCatalog(): Promise<PluginMarketplace> {
// Cached for the app run; a failed fetch is retried on the next invocation.
this.marketplacePromise ??= this.loadMarketplace().catch((error: unknown) => {
this.marketplacePromise = undefined;
throw error;
});
return this.marketplacePromise;
}

private loadMarketplace(): Promise<PluginMarketplace> {
const load = this.deps.loadMarketplace;
if (load !== undefined) return load();
return loadPluginMarketplace({ workDir: this.deps.workDir });
}
}
Loading
Loading