kimi-plugin-cc 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  // and AGENTS.md on every release. A future improvement would read this from
5
5
  // package.json at build time, but that adds a build-step dependency we
6
6
  // don't want yet.
7
- export const KIMI_PLUGIN_CC_VERSION = "2.4.0";
7
+ export const KIMI_PLUGIN_CC_VERSION = "2.4.1";
package/docs/migration.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Compatibility and upgrades
2
2
 
3
- For plugin 2.4.0, use Kimi Code CLI 2.3.1. The plugin certifies exact native-v2 versions `0.42.0`, `0.43.0`, `0.43.1`, `2.0.0`, `2.0.1`, `2.0.2`, `2.1.0`, `2.1.1`, `2.2.0`, `2.3.0`, and `2.3.1` for all eight operations. It does not automatically accept later patches or minor releases. The npm distribution adds an installation channel; it does not change this compatibility table.
3
+ Plugin 2.4.1 certifies Kimi Code CLI 2.3.2 for all eight operations, alongside exact native-v2 versions `0.42.0`, `0.43.0`, `0.43.1`, `2.0.0`, `2.0.1`, `2.0.2`, `2.1.0`, `2.1.1`, `2.2.0`, `2.3.0`, and `2.3.1`. Later patches and minor releases are not accepted automatically. The npm distribution adds an installation channel; it does not change this compatibility table.
4
4
 
5
5
  Explicitly pinned legacy-v1 binaries remain supported within `KIMI_TESTED_MINORS`, through 0.41.x, subject to each operation's minimum version. The [runtime table](../runtime/kimi-engine.ts) is the authority.
6
6
 
@@ -8,7 +8,17 @@ Use the [README installation and update steps](../README.md) for Claude Code or
8
8
 
9
9
  ## Plugin versions and upgrades
10
10
 
11
- Plugin versions advance independently using ordinary SemVer and may deliberately align with a certified CLI release. Plugin 2.4.0 certifies CLI 2.3.1; the runtime table records that mapping. Published versions and tags are never reused. Every upstream patch requires its own certification. The native-v2 no-plan profile, plugin/marketplace IDs and existing job store are preserved.
11
+ Plugin versions advance independently using ordinary SemVer and may deliberately align with a certified CLI release. Plugin 2.4.1 certifies exact CLI 2.3.2; the nearby numbers do not imply version alignment. Published versions and tags are never reused. Every upstream patch requires its own certification. The native-v2 no-plan profile, plugin/marketplace IDs and existing job store are preserved.
12
+
13
+ ## CLI 2.3.2 swarm activation
14
+
15
+ Upstream 2.3.2 removes model self-activation of `AgentSwarm` in fresh print sessions. On that exact version only, swarm commands and MCP calls use the same `runSwarm` lifecycle:
16
+
17
+ 1. A short-lived `web --no-open --host 127.0.0.1 --port 0 --log-level error` process uses authenticated loopback routes to verify the exact v2 server, choose the explicit or configured default model, create a session, bind the model and auto permission, enable and verify `swarm_mode`, and shut down.
18
+ 2. One ordinary `kimi -p -r` prompt runner resumes that session. Its recorded session-id source is `preactivated`, and the returned print session must match it.
19
+ 3. A second short-lived web lifecycle disables swarm mode, verifies status and the `swarm_mode.exit` journal record, and shuts down.
20
+
21
+ Activation is inside the job's finite budget. `runSwarm` solely owns deactivation: normal cleanup uses the remaining budget, while failure recovery has a separate two-second cancellation-aware bound and is never retried. Print/session failures stay primary. The ready token stays in memory and out of logs, and success cannot complete if cleanup fails. Post-mutation identity or process/group uncertainty preserves diagnostics and, for write-swarm, the recovery worktree. Process groups are rechecked after teardown; unknown survivors are unverified and unproven groups are not signalled. Older certified versions still launch swarm directly with `kimi -p`. This narrow lifecycle is not a full RPC transport or a model-driven activation workaround; the managed hook and workspace boundaries still govern every tool call.
12
22
 
13
23
  After updating the plugin, run Claude Code `/kimi:setup` or Codex `$kimi-setup`
14
24
  from that host's active install to re-pin its version-stamped hook path, then
@@ -6,8 +6,12 @@ remain available. The package has no install scripts and no separate CLI.
6
6
 
7
7
  ## Publishing status
8
8
 
9
- Plugin **2.3.1** certifies exact CLI **2.3.1**. [Release v2.3.1](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.3.1) is anchored at `20011ff0554c611afe0a426b30a68c2473175568`; its [CI check](https://github.com/linxule/kimi-plugin-cc/actions/runs/39977037533) and [release-event publishing run](https://github.com/linxule/kimi-plugin-cc/actions/runs/39977818482) passed. The registry tarball has SHA-1 `e309448480a2ce841b49840cdaeeed37acc6c50b`, contains 196 files, and matches the CI artifact byte-for-byte. See the [certification evidence](./upstream-2.3.1-certification.md).
10
- The previous verified publication was **2.2.2**:
9
+ Plugin **2.4.1** certifies exact CLI **2.3.2** for all eight operations with host-preactivated swarm sessions. Its [certification evidence](./upstream-2.3.2-certification.md) includes the source scan, plan controls, hardened 15-test/115-assertion candidate matrix, and final 19-test/125-assertion production smoke in 296.67 seconds. Independent review is complete: Claude reports no blockers and Codex returned RESOLVED. The final repository gate passed 1,268 tests with zero failures and 16,055 assertions across 78 files, and package validation passed 224 files. CI, tagging, and registry verification are pending; the currently published release remains 2.4.0 until that closeout completes.
10
+
11
+ Plugin **2.4.0** preserves exact CLI **2.3.1** certification and adds the shared MCP transport. [Release v2.4.0](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.4.0) is anchored at `9232d1ec98465454d8d9328e439d3f31bfc53e04`; its [CI check](https://github.com/linxule/kimi-plugin-cc/actions/runs/40040084944) and [release-event publishing run](https://github.com/linxule/kimi-plugin-cc/actions/runs/40040442610) passed. The registry tarball has SHA-1 `8243616529aa9f03f26bf4fef68c36d90e2304ae`, contains 222 files, and matches the CI artifact byte-for-byte. A fresh npm install passed isolated setup/check for both hosts.
12
+ The previous verified publication was **2.3.1**:
13
+ [Release v2.3.1](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.3.1) is anchored at `20011ff0554c611afe0a426b30a68c2473175568`; its [CI check](https://github.com/linxule/kimi-plugin-cc/actions/runs/39977037533) and [release-event publishing run](https://github.com/linxule/kimi-plugin-cc/actions/runs/39977818482) passed. The registry tarball has SHA-1 `e309448480a2ce841b49840cdaeeed37acc6c50b`, contains 196 files, and matches the CI artifact byte-for-byte. See the [certification evidence](./upstream-2.3.1-certification.md).
14
+ Earlier verified publication was **2.2.2**:
11
15
  [Release v2.2.2](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.2.2)
12
16
  is anchored at `256e78ff7cbf302fb19e8e523d8e3f943f7cb366`; its
13
17
  [CI check](https://github.com/linxule/kimi-plugin-cc/actions/runs/39755724024)
package/docs/safety.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This guide describes the current checkout runtime. Read-only tasks deny file writes and shell commands. Rescue and pursue can edit your workspace. Write-swarm returns a patch from a temporary Git worktree.
4
4
 
5
- Native agent-core-v2 is certified at exact CLI versions `0.42.0`, `0.43.0`, `0.43.1`, `2.0.0`, `2.0.1`, `2.0.2`, `2.1.0`, `2.1.1`, `2.2.0`, `2.3.0`, and `2.3.1`, with plan mode disabled. The [runtime certification table](../runtime/kimi-engine.ts) controls routing. The hook is not an operating-system sandbox.
5
+ Native agent-core-v2 is certified at exact CLI versions `0.42.0`, `0.43.0`, `0.43.1`, `2.0.0`, `2.0.1`, `2.0.2`, `2.1.0`, `2.1.1`, `2.2.0`, `2.3.0`, `2.3.1`, and `2.3.2`, with plan mode disabled. The [runtime certification table](../runtime/kimi-engine.ts) controls routing. The hook is not an operating-system sandbox.
6
6
 
7
7
  ## Why the safety story is hook-based
8
8
 
@@ -205,12 +205,24 @@ If another installed host still points to an older hook, that hook may veto thes
205
205
  - **Resume is intentionally not exposed yet.** In goal mode `goal.summary` emits a *goalId* distinct from the resume-hint *sessionId*; `kimi -r <sessionId>` re-enters the session but not necessarily the goal continuation. The plugin captures and surfaces the goalId but does not offer `--resume` for pursue until that split is reconciled upstream — exposing it now would be a silent-failure trap. **Re-verified against kimi-code 0.26.0 (2026-07-16):** the headless CLI still has no argv, prompt syntax, or session-resume path that calls `resumeGoal()`. `parseHeadlessGoalCreate` (`apps/kimi-code/src/cli/goal-prompt.ts`) accepts only the `/goal <objective>` *create* shape on both the v1 and experimental-v2 drivers, and upstream's own docs (`docs/en/guides/goals.md`, § "Use goal mode carefully") state that in non-interactive prompt mode *only goal creation is supported* — goal management is a TUI/kimi-web control. PR #552 makes a plain `-r <sessionId>` correctly *restore* the paused goal snapshot, but nothing in the headless path *acts* on it; the real `resumeGoal()` is wired only to the TUI (`/goal resume`) and kimi-web RPC (PR #1693, web-only). Implementing `--resume` would therefore require either prompt-injected model compliance (rejected — the plugin bounds autonomy with hard limits, not model goodwill) or abandoning the subprocess model for kimi-web's RPC. Re-check if upstream ever ships a headless `goal resume` verb.
206
206
  - **A model-invocable `kimi-pursue` agent (v1.3) wraps this command.** The main thread can dispatch autonomous goal mode, not just the human-typed `/kimi:pursue` — the same command↔agent mirror as rescue/review/swarm. Auto-dispatch does NOT widen the blast radius: pursue runs under `commandLabel: "rescue"`, so the managed PreToolUse hook + workspace allowlist gate every tool call on every continuation turn (no git mutation), and the mandatory `--budget` wall-clock ceiling bounds the autonomy regardless of who launched it. Because it is write-capable AND autonomous — the plugin's highest blast radius — the agent's description requires an explicit user request for hands-off multi-turn pursuit, steers single bounded tasks to `kimi-rescue`, and never auto-promotes a fix or a question into a goal loop. Like the command, it refuses without the hook and offers no `--resume`.
207
207
 
208
+ ## Swarm activation on CLI 2.3.2
209
+
210
+ Upstream 2.3.2 removes model self-activation of `AgentSwarm` in fresh print sessions. A rejected call can therefore coexist with zero child journals and a successful exit; a tool-call record, aggregate denial, or coordinator-produced patch does not prove fan-out. The plugin does not use prompt workarounds or replace its print transport with general RPC.
211
+
212
+ For versions requiring activation (currently exact 2.3.2), both slash-command and MCP swarm operations use the same `runSwarm` implementation and one ordinary `kimi -p -r` prompt runner per job, bracketed by short-lived host-managed web lifecycles:
213
+
214
+ 1. The exact planned CLI starts `web --no-open --host 127.0.0.1 --port 0 --log-level error`. The plugin accepts only the expected loopback ready URL, uses a supplied token for authenticated REST requests, and keeps that token in memory, never writing it to logs. `/api/v1/meta` must confirm backend v2, the exact planned version, and no dangerous authentication bypass. The plugin uses the explicit model or environment-model alias, otherwise reading the default alias from `/api/v1/config`; creates a session in the job cwd; binds that model with `permission_mode:auto` and `swarm_mode:true`; verifies those settings and `plan_mode:false` through `/status`; verifies clean shutdown; and requires the main journal's manual `swarm_mode.enter` record.
215
+ 2. The ordinary stream-json print runner resumes that session with `kimi -p -r`. The job records `kimi_session_id_source:"preactivated"`, and the session returned by print must exactly match the preactivated id. Every coordinator and child tool call remains governed by the same managed PreToolUse hook and operation allowlist.
216
+ 3. After the print run, a second web lifecycle sets `swarm_mode:false`, verifies `/status`, shuts down cleanly, and requires `swarm_mode.exit` in the main journal. Activation consumes part of the same finite job budget rather than extending it. `runSwarm` solely owns session deactivation: normal cleanup uses the remaining budget, while failure recovery has a separate two-second cancellation-aware bound and is never retried. Print/session failures stay primary, with exit codes and cleanup diagnostics preserved. Cancellation aborts the owned lifecycle and never starts a fresh cleanup server. Every post-mutation failure retains the session identity; process/group uncertainty is marked without inventing an ID; process groups are rechecked after teardown, unknown survivors are unverified, unproven groups are never signalled, and a same-PID/start-token process that changes PGID remains active but is not a validated signal target. Root loss requires a previously observed matching identity; first-missing roots and actual probe errors/timeouts remain unverified. Reparenting and PGID moves preserve the current verified child identity for revalidation rather than treating it as exited. Unverified cleanup prevents success and preserves raw output, structured diagnostics, and the write worktree for recovery.
217
+
218
+ Older certified versions keep the direct `kimi -p` path. The web processes are narrow, planned lifecycle helpers, not a second model runner and not a general RPC transport.
219
+
208
220
  ## Read-only swarm (`/kimi:swarm`)
209
221
 
210
222
  `/kimi:swarm` (v1.2) fans a **read-only** review out across subagents using kimi-code's `AgentSwarm` tool (kimi-code 0.12.0+). The coordinator launches one subagent per target (file/module/question); each inspects the workspace with read tools and reports findings, which the coordinator consolidates into one markdown report. It opens **zero new write surface**.
211
223
 
212
224
  - **The hook label is `swarm`, and it allows the read-only tool set PLUS `AgentSwarm`.** The coordinator must be allowed to call `AgentSwarm` (else the swarm never launches), but no write/edit/shell tool is allowed. The exact allowed tool name is `AgentSwarm` (verified at 0.42.0 against `packages/agent-core-v2/src/features/swarm/tools/agent-swarm/agentSwarmTool.ts`: `'AgentSwarm'`, strict schema). The singular `Agent` tool is deliberately *not* allowed — swarm is the fan-out surface, not arbitrary delegation.
213
- - **Every subagent inherits the `swarm` label and fires the same hook.** On native v2 (0.42.0) `AgentSwarm` children run in-process in a child DI scope with their OWN eager external-hooks service, so every child tool call passes the same before-execute channel and the same managed hook — there is no sub-vs-main branch and no deny-all shortcut on the swarm route. So a subagent's `Write`/`Edit`/`Bash` is denied exactly like a single-turn review's. The swarm listener in that channel is non-terminal and nothing auto-approves `AgentSwarm` ahead of the hook. (Legacy-v1 ≤ 0.41 reached the same result by placing the hook policy at index 0 of every agent's permission stack.) **This is locked by a real-binary smoke** (`tests/runtime/real-binary-smoke.test.ts`, run via `bun run smoke:real`): it spawns the real binary under the `swarm` label with an adversarial prompt that forces a spawned subagent to attempt a write, and asserts that **no file lands** and the hook deny marker appears — converting the subagent-denial claim from source-read-verified to runtime-test-verified.
225
+ - **Every subagent inherits the `swarm` label and fires the same hook.** On native v2 (0.42.0) `AgentSwarm` children run in-process in a child DI scope with their OWN eager external-hooks service, so every child tool call passes the same before-execute channel and the same managed hook — there is no sub-vs-main branch and no deny-all shortcut on the swarm route. So a subagent's `Write`/`Edit`/`Bash` is denied exactly like a single-turn review's. The swarm listener in that channel is non-terminal and nothing auto-approves `AgentSwarm` ahead of the hook. (Legacy-v1 ≤ 0.41 reached the same result by placing the hook policy at index 0 of every agent's permission stack.) **Required for final certification evidence** (`tests/runtime/real-binary-smoke.test.ts`, run via `bun run smoke:real`): distinct child journals must exist, and each assigned Write/Edit `path` must be correlated through its `toolCallId` with the managed denial in that exact tool result; unrelated denials and coordinator-only reports are insufficient. On 2.3.2, targeted controls also prove that direct OFF produces no children while the preactivated ON path produces the real child journals and denials.
214
226
  - **The managed hook is the read-only boundary; no `explore`-profile second layer is assumed.** Production deliberately omits `subagent_type` for read-only swarm rather than selecting kimi-code's `explore` profile. Every coordinator and subagent tool call is enforced by the same managed hook.
215
227
  - **Swarm refuses without the hook, like every model-spawning command.** A swarm makes the reason especially acute: it asks for up to `--cap` subagents, but this total-count hint is advisory, so a missing hook would remove enforcement from all of them at once. `KIMI_PLUGIN_CC_SKIP_HOOK_CHECK=1` remains an explicit tests/diagnostics escape hatch, not a recovery path.
216
228
  - **The only new risk is COST, and the plugin bounds it.** Read-only swarm writes nothing, so the new risk is runaway cost from N parallel model runs (and possible nested `AgentSwarm`). `/kimi:swarm` enforces a **mandatory finite wall-clock ceiling** via its AbortController (`--budget`, default 30m); the SIGTERM→SIGKILL descendant-reaping cancellation reaps the whole subagent process tree. Two **distinct** subagent bounds: `--cap N` is a *soft* prompt-injected hint on TOTAL subagent count (the hook is stateless and cannot enforce a count, so the model may exceed it), and `--max-concurrency N` is a *hard* ceiling on how many subagents kimi-code's `SubagentBatch` runs concurrently — exported as `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` (PR #888) on **kimi-code 0.18.0+** (older binaries ignore the unknown env var, so on < 0.18 only the soft hint applies). It **defaults to 4** when the user passes none (`resolveSwarmMaxConcurrency` in swarm.ts), so every swarm run — agent-dispatched or human-typed — carries an enforced peak-parallelism ceiling on 0.18.0+ rather than falling back to kimi-code's internal ramp; pass an explicit value to widen or throttle. **Never merge them into one flag:** concurrency ≠ total count — a hard concurrency cap of N still permits more than N subagents over the life of the run, just never more than N at once. The `--budget` bounds elapsed time, not a dollar amount or total token count; `--max-concurrency` bounds *peak* parallelism (the spike in simultaneous model spend), and `--cap` is only an advisory total-count nudge. Folding both onto one flag would create the "cap-as-total-count illusion" — a user reading `--cap N` would expect a total bound but receive only a peak-parallelism one.
@@ -226,7 +238,7 @@ If another installed host still points to an older hook, that hook may veto thes
226
238
  - **The plugin never touches git; the main thread owns the merge.** Subagents cannot run git (allowlist denies mutation). The plugin captures the change set with `git add -N` (intent-to-add, so new files appear in the diff WITHOUT writing loose objects into the shared object DB) + `git diff --binary` (untruncated, applyable), writes it to a `swarm-write-<jobId>.patch` artifact, and prints the path. It does **not** `git apply`, commit, or move any ref — the user/main thread reviews the patch and applies it (`git apply <path>`). The worktree shares the main repo's object DB and refs, but no ref/HEAD is ever moved, so the user's repository state is untouched.
227
239
  - **The patch is captured on every terminal path, before cleanup.** Success, cancel, budget-expiry, and failure all capture the (possibly partial) patch BEFORE `git worktree remove --force` discards the tree, so a cancelled run never silently loses the work the subagents already did. If capture itself fails, the job fails instead of completing and the worktree is retained with its path in the error details for recovery. That guarantee covers the plugin's own terminal paths, including `companion.sh cancel`. It does **not** cover a harness interrupt: Esc/TaskStop delivers SIGTERM then SIGKILL after ~1.35s (measured, process-group-wide), which is shorter than the 1500ms child escalation plus quiescence plus patch capture — so a hard interrupt can kill the companion mid-teardown and lose the patch. `companion.sh cancel` signals from a separate process not racing that deadline and is the work-preserving stop. A startup sweep reaps worktrees orphaned by a hard kill (SIGKILL of the plugin process); its reap TTL is derived as `MAX_DURATION_MS + 2h` so it always exceeds the longest a run can live (the 24h `--budget` cap) and can never force-remove a still-live worktree out from under a concurrent run — the sweep's only liveness signal is the worktree dir mtime, which a run editing only existing files leaves frozen at `git worktree add` time.
228
240
  - **Gated tightly.** Requires kimi-code **≥ 0.18.0** (below it `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` is ignored, so a write fan-out would have no hard peak bound — unacceptable for concurrent writers); because that cap cannot be guaranteed without a confirmed version, the write-mode version gate **fails closed** on an unconfirmable `kimi --version` probe (the shared execution-plan gate also refuses unverified or uncertified binaries for read-only swarm; older certified legacy versions may lack the hard concurrency setting). Also requires a git repo with a **committed HEAD**, and the PreToolUse hook (**refuses** without it, like read swarm). Bases the worktree on HEAD, so a **loud warning** fires when the working tree is dirty (uncommitted work is not visible to the swarm). Default `--max-concurrency` is **1** (serialize) since disjoint-target partitioning is prompt-only and unenforceable by the stateless hook; the coordinator prompt also forbids git mutation and nested `AgentSwarm`.
229
- - **Field-proven by a real-binary smoke.** `tests/runtime/real-binary-smoke.test.ts` runs the real `runSwarm --write` and asserts the POSITIVE end-to-end (a `coder` subagent's edits land in the worktree, captured as a patch; the user's real tree is byte-identical + git-clean; the worktree is cleaned up) plus the NEGATIVE (an absolute-path write outside the trusted root is hook-denied). It is the check that catches `tool_input` field-name drift — `Write`/`Edit` carry `path`, not `file_path`, and reading the wrong key denies every real write silently.
241
+ - **Required for final certification evidence.** `tests/runtime/real-binary-smoke.test.ts` runs the real `runSwarm --write` and requires the POSITIVE end-to-end: distinct child journals contain their assigned edits with `Write`/`Replaced` results on the same `toolCallId`, those child-authored changes land in the worktree and captured patch, the user's real tree is byte-identical + git-clean, and the worktree is cleaned up. A patch without per-call child provenance is not accepted as fan-out evidence. The NEGATIVE lane correlates each absolute out-of-root `path` and `toolCallId` with its managed denial. It is the check that catches `tool_input` field-name drift — `Write`/`Edit` carry `path`, not `file_path`, and reading the wrong key denies every real write silently.
230
242
  - **Model-invocable agent (v1.5), patch-only by construction.** Following the field-proof above, `--write` now has a `kimi-swarm-write` subagent so the main thread can dispatch a write fan-out on its own judgement — the same human-command + model-invocable-agent split the rest of the suite uses. Auto-dispatch is a safe extension here because it widens **no** write surface and removes **no** bound: the agent path runs the identical `runSwarm --write` code, so edits still land only in the throwaway worktree, the managed hook still gates every `coder` subagent, the patch is still handed back for the user to apply (**the plugin never applies or commits**), and `--budget` + default `--max-concurrency` 1 + the hook requirement all still hold. In fact write-swarm is a *less* dangerous agent than the `kimi-pursue` agent already shipped: pursue writes directly into the workspace, whereas write-swarm's output is a patch the human reviews before anything reaches a real tree. Because it is still a write surface, the agent description demands BOTH signals — many disjoint write targets AND explicit fan-out intent — and steers single edits to `kimi-rescue`, read-only fan-outs to `kimi-swarm`, and autonomous loops to `kimi-pursue`. Default `--max-concurrency` stays **1**; raising it (concurrent writers) remains a deliberate, human-or-explicitly-requested choice.
231
243
 
232
244
  ## MCP transport and long-running jobs
@@ -279,9 +291,9 @@ at it (the runtime resolves that path instead of `kimi` on `PATH`; every spawn
279
291
  still exports `KIMI_CODE_NO_AUTO_UPDATE=1`, so the pinned copy never drifts).
280
292
 
281
293
  ```sh
282
- PIN=~/.local/share/kimi-plugin-cc/kimi-bin/2.3.1
294
+ PIN=~/.local/share/kimi-plugin-cc/kimi-bin/2.3.2
283
295
  mkdir -p "$PIN" && cd "$PIN" && printf '{"name":"kimi-plugin-cc-pin","private":true}\n' > package.json
284
- bun add @moonshot-ai/kimi-code@2.3.1
296
+ bun add @moonshot-ai/kimi-code@2.3.2
285
297
  export KIMI_PLUGIN_CC_KIMI_BIN="$PIN/node_modules/.bin/kimi" # e.g. in ~/.zshenv
286
298
  ```
287
299
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kimi-plugin-cc",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Kimi for Claude Code and Codex: code questions, independent reviews, bounded fixes, autonomous goals, parallel tasks, and MCP tools through the local Kimi Code CLI.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -13,7 +13,7 @@
13
13
  "build": "rm -rf dist && bunx tsc -p tsconfig.build.json && mkdir -p dist/vendor/shell-quote dist/vendor/smol-toml && cp runtime/vendor/shell-quote/LICENSE dist/vendor/shell-quote/LICENSE && cp runtime/vendor/smol-toml/*.js runtime/vendor/smol-toml/LICENSE dist/vendor/smol-toml/ && echo '{\"type\":\"module\"}' > dist/package.json",
14
14
  "companion": "node --import tsx runtime/companion.ts",
15
15
  "test": "bun test",
16
- "smoke:real": "KIMI_PLUGIN_CC_SMOKE=1 bun test tests/runtime/real-binary-smoke.test.ts",
16
+ "smoke:real": "KIMI_PLUGIN_CC_SMOKE=1 bun test tests/runtime/real-binary-smoke.test.ts --path-ignore-patterns '**/worktrees/**'",
17
17
  "check:package": "node scripts/check-package.mjs"
18
18
  },
19
19
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kimi",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Kimi for Codex: code questions, independent reviews, bounded fixes, autonomous goals, parallel tasks, and MCP job controls through the local Kimi Code CLI.",
5
5
  "author": {
6
6
  "name": "linxule"
@@ -60,7 +60,7 @@ export async function runCliPrompt(opts) {
60
60
  });
61
61
  }
62
62
  const args = buildArgs(opts);
63
- const env = buildEnv(opts);
63
+ const env = buildKimiCliEnvironment(opts);
64
64
  if (opts.logPath !== undefined) {
65
65
  await mkdir(path.dirname(opts.logPath), { recursive: true }).catch(() => {
66
66
  // Best-effort; if mkdir fails the appendFile below will error and be
@@ -788,7 +788,7 @@ function buildArgs(opts) {
788
788
  args.push("-p", opts.prompt);
789
789
  return args;
790
790
  }
791
- function buildEnv(opts) {
791
+ export function buildKimiCliEnvironment(opts) {
792
792
  const env = { ...opts.env };
793
793
  if (opts.thinkingEffort !== undefined)
794
794
  env.KIMI_MODEL_THINKING_EFFORT = opts.thinkingEffort;
@@ -117,7 +117,11 @@ export async function captureTimeoutPartialOutput(classified, store, job, env, c
117
117
  return undefined;
118
118
  const result = timeout.partialResult;
119
119
  let sessionId = result.sessionId;
120
- let source = sessionId ? "announced" : null;
120
+ let source = sessionId
121
+ ? sessionId === job.kimi_session_id
122
+ ? job.kimi_session_id_source
123
+ : "announced"
124
+ : null;
121
125
  const details = classified instanceof RuntimeError ? classified.details : timeout.details;
122
126
  details.kimi_session_id_source = job.kimi_session_id_source;
123
127
  // The persisted plan is plugin-owned. Recovery never runs on cancellation,
@@ -2,7 +2,8 @@ import { allocateJobId } from "../job-id.js";
2
2
  import { mkdir, readdir, stat, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { createCliCancellationHandlers } from "../cli-cancellation.js";
5
- import { runCliPromptWithBudget } from "../cli-client.js";
5
+ import { ResponseTimeoutError, runCliPromptWithBudget } from "../cli-client.js";
6
+ import { deactivateSwarmSession, preactivateSwarmSession, requiresSwarmPreactivation, } from "../swarm-activation.js";
6
7
  import { executionPlanFromPersisted, observedExecutionFields, persistedExecutionPlanFields, prepareKimiExecutionPlan, } from "../kimi-engine.js";
7
8
  import { RuntimeError } from "../errors.js";
8
9
  import { captureWorktreePatch, createEphemeralWorktree, hasBornHead, isWorkingTreeDirty, pruneWorktrees, removeWorktree, resolveRepoIdentity, } from "../git.js";
@@ -24,7 +25,8 @@ import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js
24
25
  // only the wall-clock hard bound. Read/write modes reuse review/rescue lineage,
25
26
  // and the mandatory PreToolUse hook enforces read-only policy or trusted-worktree
26
27
  // write confinement on the coordinator and every subagent. Write mode requires
27
- // kimi-code 0.18.0+.
28
+ // kimi-code 0.18.0+. Exact 2.3.2 additionally activates a same-job session through
29
+ // a bounded loopback web lifecycle, then resumes it through the normal print path.
28
30
  const SWARM_SUMMARY_MAX = 120;
29
31
  const SWARM_AGENT_PROFILE = "<swarm>";
30
32
  const SWARM_WRITE_AGENT_PROFILE = "<swarm-write>";
@@ -87,7 +89,7 @@ export function resolveSwarmWriteMaxConcurrency(requested) {
87
89
  * subagent, so slimming this text does not move the safety boundary. The optional
88
90
  * `cap` is a SOFT subagent-count hint (the hook cannot enforce a count). The HARD
89
91
  * concurrency ceiling is separate: --max-concurrency →
90
- * KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY (see executeSwarmJob / cli-client buildEnv).
92
+ * KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY (see executeSwarmJob / cli-client buildKimiCliEnvironment).
91
93
  * The --budget wall-clock ceiling is the always-on hard bound on the whole run.
92
94
  */
93
95
  export function buildSwarmPrompt(objective, cap) {
@@ -343,7 +345,7 @@ async function runWriteSwarm(parsed, objective, context) {
343
345
  }
344
346
  finally {
345
347
  if (preserveWorktree) {
346
- context.stderr.write(`[kimi-plugin-cc] swarm-write patch capture failed; worktree retained at ${worktreePath}.\n`);
348
+ context.stderr.write(`[kimi-plugin-cc] swarm-write patch capture or session cleanup failed; worktree retained at ${worktreePath}.\n`);
347
349
  }
348
350
  else {
349
351
  await removeWorktree(repoIdentity.repoRoot, worktreePath);
@@ -465,44 +467,163 @@ writeMode) {
465
467
  }
466
468
  }
467
469
  const handlers = createCliCancellationHandlers();
470
+ const budgetDeadline = Date.now() + budgetMs;
471
+ let activationOptions;
472
+ let preactivated;
473
+ let deactivationRequired = false;
474
+ let cleanupAttempted = false;
475
+ let cleanupFailure;
476
+ let finalText;
477
+ const commandLabel = writeMode ? "swarm-write" : "swarm";
478
+ const recoveryCleanupTimeoutMs = 2_000;
479
+ const cleanupSignal = (timeoutMs) => AbortSignal.any([
480
+ handlers.signal,
481
+ AbortSignal.timeout(Math.max(1, timeoutMs)),
482
+ ]);
483
+ const attemptDeactivation = async (identity, fallbackStage, timeoutMs = Math.max(1, budgetDeadline - Date.now())) => {
484
+ cleanupAttempted = true;
485
+ try {
486
+ await deactivateSwarmSession({
487
+ ...activationOptions,
488
+ ...identity,
489
+ signal: cleanupSignal(timeoutMs),
490
+ });
491
+ deactivationRequired = false;
492
+ return undefined;
493
+ }
494
+ catch (error) {
495
+ return error instanceof RuntimeError
496
+ ? error
497
+ : new RuntimeError("SWARM_SESSION_CLEANUP_FAILED", "Swarm session cleanup failed without a safe diagnostic payload.", fallbackStage);
498
+ }
499
+ };
468
500
  try {
469
501
  const executionPlan = executionPlanFromPersisted(job);
502
+ activationOptions = {
503
+ cwd: writeMode?.spawnCwd ?? job.cwd,
504
+ env: context.env,
505
+ command: executionPlan.command,
506
+ prefixArgs: executionPlan.prefixArgs,
507
+ executionPlan,
508
+ commandLabel,
509
+ model: job.model ?? undefined,
510
+ thinkingEffort: job.thinking_effort ?? undefined,
511
+ swarmMaxConcurrency: maxConcurrency,
512
+ trustedWorkspaceRoot: writeMode?.workspaceRoot,
513
+ expectedVersion: executionPlan.kimiVersion ?? undefined,
514
+ };
515
+ if (requiresSwarmPreactivation(executionPlan.kimiVersion)) {
516
+ store.updateRunningJob(job.job_id, { phase: "turn-running" });
517
+ const activationController = new AbortController();
518
+ const onParentAbort = () => activationController.abort();
519
+ let activationTimedOut = false;
520
+ handlers.signal.addEventListener("abort", onParentAbort, { once: true });
521
+ if (handlers.signal.aborted)
522
+ onParentAbort();
523
+ const activationTimer = setTimeout(() => {
524
+ activationTimedOut = true;
525
+ activationController.abort();
526
+ }, Math.max(1, budgetDeadline - Date.now()));
527
+ activationTimer.unref();
528
+ try {
529
+ preactivated = await preactivateSwarmSession({
530
+ ...activationOptions,
531
+ signal: activationController.signal,
532
+ });
533
+ deactivationRequired = true;
534
+ store.updateRunningJob(job.job_id, {
535
+ kimi_session_id: preactivated.sessionId,
536
+ kimi_session_id_source: "preactivated",
537
+ });
538
+ }
539
+ catch (error) {
540
+ const activationDidTimeOut = activationTimedOut;
541
+ clearTimeout(activationTimer);
542
+ handlers.signal.removeEventListener("abort", onParentAbort);
543
+ const details = error instanceof RuntimeError ? error.details : {};
544
+ const sessionId = typeof details.session_id === "string" ? details.session_id : undefined;
545
+ const workspaceId = typeof details.workspace_id === "string" ? details.workspace_id : undefined;
546
+ const hasSessionIdentity = sessionId !== undefined && workspaceId !== undefined;
547
+ if (error instanceof RuntimeError &&
548
+ error.code === "SWARM_SESSION_CLEANUP_FAILED" &&
549
+ details.process_cleanup_unverified === true) {
550
+ cleanupFailure = error;
551
+ }
552
+ if (hasSessionIdentity) {
553
+ store.updateRunningJob(job.job_id, {
554
+ kimi_session_id: sessionId,
555
+ kimi_session_id_source: "preactivated",
556
+ });
557
+ }
558
+ if (hasSessionIdentity && !handlers.cancelling) {
559
+ const sessionCleanupFailure = await attemptDeactivation({ sessionId, workspaceId }, "swarm.activation-cleanup", recoveryCleanupTimeoutMs);
560
+ if (sessionCleanupFailure)
561
+ cleanupFailure = sessionCleanupFailure;
562
+ }
563
+ else if (hasSessionIdentity && cleanupFailure === undefined) {
564
+ cleanupFailure = new RuntimeError("SWARM_SESSION_CLEANUP_FAILED", "Swarm session cleanup was not completed after activation was cancelled.", "swarm.activation-cleanup", { details: { cancelled: true, cleanup_unverified: true } });
565
+ }
566
+ if (activationDidTimeOut && !handlers.cancelling) {
567
+ throw new ResponseTimeoutError(budgetMs, "swarm.activation", commandLabel, undefined, error);
568
+ }
569
+ throw error;
570
+ }
571
+ finally {
572
+ clearTimeout(activationTimer);
573
+ handlers.signal.removeEventListener("abort", onParentAbort);
574
+ }
575
+ }
576
+ const promptBudgetMs = budgetDeadline - Date.now();
577
+ if (promptBudgetMs <= 0)
578
+ throw new ResponseTimeoutError(budgetMs, "swarm.prompt", commandLabel);
470
579
  store.updateRunningJob(job.job_id, { phase: "turn-running" });
471
580
  const result = await runCliPromptWithBudget({
472
- // Read mode spawns in the user's cwd; WRITE mode spawns in the throwaway
473
- // worktree so coder subagents edit there (job.cwd stays the user's real
474
- // cwd for lineage). This is the load-bearing isolation seam.
475
- cwd: writeMode?.spawnCwd ?? job.cwd,
581
+ cwd: activationOptions.cwd,
476
582
  env: context.env,
477
583
  command: executionPlan.command,
478
584
  prefixArgs: [...executionPlan.prefixArgs],
479
585
  executionPlan,
480
586
  prompt,
481
- // "swarm" → read-only-plus-AgentSwarm allowlist; "swarm-write" → the same
482
- // PLUS rescue-grade write confinement to the trusted worktree root. The
483
- // label drives the hook for the coordinator AND every spawned subagent.
484
- commandLabel: writeMode ? "swarm-write" : "swarm",
485
- // Always exported as KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY. kimi-code
486
- // 0.18.0+ enforces this hard peak ceiling; older read-only binaries ignore
487
- // it and retain only the wall-clock hard bound. Write mode requires
488
- // 0.18.0+. Distinct from --cap, the soft total-count prompt hint.
587
+ commandLabel,
489
588
  swarmMaxConcurrency: maxConcurrency,
490
- // Trusted allowlist root for the swarm-write hook (forge-proof; ignored
491
- // by other labels). Only set in write mode.
492
589
  trustedWorkspaceRoot: writeMode?.workspaceRoot,
493
590
  model: job.model ?? undefined,
494
591
  thinkingEffort: job.thinking_effort ?? undefined,
592
+ resumeSessionId: preactivated?.sessionId,
495
593
  logPath: job.stream_log_path,
496
594
  onSpawn: (pid) => { store.updateRunningJob(job.job_id, { kimi_pid: pid }); },
497
595
  signal: handlers.signal,
498
- }, budgetMs, "swarm.prompt");
596
+ }, promptBudgetMs, "swarm.prompt");
499
597
  store.updateRunningJob(job.job_id, observedExecutionFields(result));
598
+ finalText = reassembleProseFromRecords(result.records);
599
+ let resultError;
500
600
  if (handlers.cancelling) {
501
- throw new RuntimeError("SWARM_CANCELLED", "Swarm cancelled by user request.", "swarm.runtime");
601
+ resultError = new RuntimeError("SWARM_CANCELLED", "Swarm cancelled by user request.", "swarm.runtime");
602
+ }
603
+ else {
604
+ try {
605
+ assertCliResultSuccess(result, "swarm.runtime");
606
+ if (preactivated && result.sessionId !== preactivated.sessionId) {
607
+ throw new RuntimeError("SWARM_SESSION_MISMATCH", "The resumed print session differs from the preactivated swarm session.", "swarm.session-provenance", { details: { expected_session_id: preactivated.sessionId, actual_session_id: result.sessionId ?? null } });
608
+ }
609
+ }
610
+ catch (error) {
611
+ resultError = error;
612
+ }
502
613
  }
503
- assertCliResultSuccess(result, "swarm.runtime");
504
614
  warnHookEscapes(result, "swarm", context.stderr);
505
- if (result.sessionId !== undefined &&
615
+ if (preactivated) {
616
+ cleanupFailure = await attemptDeactivation(preactivated, "swarm.cleanup");
617
+ }
618
+ if (resultError !== undefined)
619
+ throw resultError;
620
+ if (handlers.cancelling) {
621
+ throw new RuntimeError("SWARM_CANCELLED", "Swarm cancelled by user request.", "swarm.runtime");
622
+ }
623
+ if (cleanupFailure)
624
+ throw cleanupFailure;
625
+ if (!preactivated &&
626
+ result.sessionId !== undefined &&
506
627
  result.sessionId.length > 0 &&
507
628
  result.sessionId !== job.kimi_session_id) {
508
629
  store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
@@ -517,7 +638,7 @@ writeMode) {
517
638
  });
518
639
  warnIfSessionIdMissing(result, "swarm", job.job_id, context.stderr);
519
640
  const prose = reassembleProseFromRecords(result.records);
520
- let finalText = prose;
641
+ finalText = prose;
521
642
  if (writeMode) {
522
643
  const captured = await capturePatchArtifact(paths, job.job_id, writeMode.workspaceRoot, context.stderr, writeMode.onPatchCaptureFailure);
523
644
  if (captured === null) {
@@ -549,6 +670,21 @@ writeMode) {
549
670
  }) ?? job);
550
671
  }
551
672
  catch (error) {
673
+ if (preactivated && deactivationRequired && activationOptions) {
674
+ if (handlers.cancelling) {
675
+ cleanupFailure = new RuntimeError("SWARM_SESSION_CLEANUP_FAILED", "Swarm session cleanup was not completed after cancellation.", "swarm.cleanup", { details: { cancelled: true, cleanup_unverified: true } });
676
+ }
677
+ else if (!cleanupAttempted) {
678
+ cleanupFailure = await attemptDeactivation(preactivated, "swarm.cleanup", recoveryCleanupTimeoutMs);
679
+ }
680
+ else if (error instanceof RuntimeError && error.code === "SWARM_SESSION_CLEANUP_FAILED") {
681
+ cleanupFailure ??= error;
682
+ }
683
+ }
684
+ if (cleanupFailure) {
685
+ writeMode?.onPatchCaptureFailure?.();
686
+ context.stderr.write(`[kimi-plugin-cc] swarm session cleanup also failed: ${cleanupFailure.message}\n`);
687
+ }
552
688
  let captureFailed = writeMode !== undefined && error instanceof RuntimeError && error.code === "SWARM_PATCH_CAPTURE_FAILED";
553
689
  if (writeMode && !(error instanceof RuntimeError && error.code === "SWARM_PATCH_CAPTURE_FAILED")) {
554
690
  const captured = await capturePatchArtifact(paths, job.job_id, writeMode.workspaceRoot, context.stderr, writeMode.onPatchCaptureFailure);
@@ -557,34 +693,55 @@ writeMode) {
557
693
  context.stderr.write(`[kimi-plugin-cc] swarm-write run ended early; partial patch captured at ${captured.path}.\n`);
558
694
  }
559
695
  }
560
- const recoveryDetails = captureFailed && writeMode ? { worktreePath: writeMode.workspaceRoot } : {};
696
+ const recoveryDetails = {
697
+ ...(error instanceof RuntimeError && typeof error.details.exit_code === "number"
698
+ ? { exit_code: error.details.exit_code }
699
+ : {}),
700
+ ...((captureFailed || cleanupFailure) && writeMode ? { worktreePath: writeMode.workspaceRoot } : {}),
701
+ ...(cleanupFailure
702
+ ? {
703
+ cleanup_error: {
704
+ code: cleanupFailure.code,
705
+ stage: cleanupFailure.stage,
706
+ cancelled: handlers.cancelling,
707
+ unverified: true,
708
+ },
709
+ ...(finalText !== undefined ? { rawOutput: finalText } : {}),
710
+ }
711
+ : {}),
712
+ };
561
713
  if (handlers.cancelling) {
714
+ const cancellationCause = classifyManagedCommandFailure(error, writeMode ? "rescue" : "review", job.job_id, {
715
+ preserveStage: true,
716
+ });
562
717
  const cancelledError = new RuntimeError("SWARM_CANCELLED", captureFailed && writeMode
563
718
  ? `Swarm cancelled; patch capture failed and worktree retained at ${writeMode.workspaceRoot}.`
564
719
  : "Swarm cancelled by user request.", "swarm.runtime", {
565
720
  ...(error instanceof Error ? { cause: error } : {}),
566
- details: recoveryDetails,
721
+ details: {
722
+ ...(cancellationCause instanceof RuntimeError ? cancellationCause.details : {}),
723
+ ...recoveryDetails,
724
+ },
567
725
  });
568
726
  return await markJobCancelled(store, paths, job, "Swarm cancelled by user request.", cancelledError, {
569
727
  phase: "cancelled",
570
728
  });
571
729
  }
730
+ if (error === cleanupFailure && budgetDeadline <= Date.now() && !(error instanceof ResponseTimeoutError)) {
731
+ error = new ResponseTimeoutError(budgetMs, "swarm.cleanup", commandLabel, undefined, cleanupFailure);
732
+ }
572
733
  const classified = classifyManagedCommandFailure(error, writeMode ? "rescue" : "review", job.job_id, {
573
734
  preserveStage: true,
574
735
  });
575
- if (captureFailed && writeMode) {
576
- const withRecovery = classified instanceof RuntimeError
736
+ const withRecovery = captureFailed || cleanupFailure
737
+ ? classified instanceof RuntimeError
577
738
  ? new RuntimeError(classified.code, classified.message, classified.stage, {
578
739
  cause: classified,
579
740
  details: { ...classified.details, ...recoveryDetails },
580
741
  })
581
- : new RuntimeError("SWARM_PATCH_CAPTURE_FAILED", classified.message, "swarm.patch-capture", {
582
- cause: classified,
583
- details: recoveryDetails,
584
- });
585
- return await markJobFailed(store, paths, job, withRecovery, "Swarm failed.", { phase: "failed" });
586
- }
587
- return await markJobFailed(store, paths, job, classified, "Swarm failed.", { phase: "failed" });
742
+ : new RuntimeError(cleanupFailure?.code ?? "SWARM_PATCH_CAPTURE_FAILED", classified.message, cleanupFailure?.stage ?? "swarm.patch-capture", { cause: classified, details: recoveryDetails })
743
+ : classified;
744
+ return await markJobFailed(store, paths, job, withRecovery, "Swarm failed.", { phase: "failed" });
588
745
  }
589
746
  finally {
590
747
  handlers.dispose();
@@ -594,8 +594,8 @@ function validateVersionSensitiveEvents(events, version) {
594
594
  }
595
595
  // Exact schemas reviewed at 2.0.0 (1b89e4b0), 2.0.1 (caf7d4e2),
596
596
  // 2.0.2 (9d07f634), 2.1.0 (52437299), 2.1.1 (f67e6398), 2.2.0 (472192c3),
597
- // 2.3.0 (b5e28fd6) and 2.3.1 (684bae9e): the additive events and HookDefSchema
598
- // are unchanged. Require the full version string AND matching components;
597
+ // 2.3.0 (b5e28fd6), 2.3.1 (684bae9e) and 2.3.2 (b8dd3a13): the additive
598
+ // events and HookDefSchema are unchanged. Require the full version string AND matching components;
599
599
  // no inferred 2.x compatibility.
600
600
  if (version.major === 2 && ((version.minor === 0 && ((version.version === "2.0.0" && version.patch === 0) ||
601
601
  (version.version === "2.0.1" && version.patch === 1) ||
@@ -604,7 +604,8 @@ function validateVersionSensitiveEvents(events, version) {
604
604
  (version.version === "2.1.1" && version.patch === 1))) ||
605
605
  (version.minor === 2 && ((version.version === "2.2.0" && version.patch === 0))) ||
606
606
  (version.minor === 3 && ((version.version === "2.3.0" && version.patch === 0) ||
607
- (version.version === "2.3.1" && version.patch === 1))))) {
607
+ (version.version === "2.3.1" && version.patch === 1) ||
608
+ (version.version === "2.3.2" && version.patch === 2))))) {
608
609
  return { valid: true };
609
610
  }
610
611
  if (version.major !== 0) {
@@ -41,6 +41,7 @@ export const NATIVE_V2_CERTIFIED_VERSIONS = Object.freeze([
41
41
  "2.2.0",
42
42
  "2.3.0",
43
43
  "2.3.1",
44
+ "2.3.2",
44
45
  ]);
45
46
  /** Per-operation native-v2 certification. One green operation never certifies another. */
46
47
  export const NATIVE_V2_CERTIFIED = new Map([