kimi-plugin-cc 2.4.0 → 2.4.2

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.
Files changed (51) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +18 -0
  4. package/README.md +17 -9
  5. package/dist/cli-client.js +19 -9
  6. package/dist/commands/cancel.js +14 -3
  7. package/dist/commands/cli-helpers.js +5 -1
  8. package/dist/commands/recover-swarm.js +135 -0
  9. package/dist/commands/status.js +1 -1
  10. package/dist/commands/swarm.js +241 -87
  11. package/dist/companion.js +5 -1
  12. package/dist/git.js +5 -3
  13. package/dist/help.js +3 -2
  14. package/dist/hooks/config-safety.js +4 -3
  15. package/dist/job-store.js +13 -0
  16. package/dist/jobs.js +3 -0
  17. package/dist/kimi-engine.js +1 -0
  18. package/dist/kimi-errors.js +27 -21
  19. package/dist/parsing.js +1 -0
  20. package/dist/process-tree.js +48 -17
  21. package/dist/prompt-size.js +18 -0
  22. package/dist/swarm-activation.js +833 -0
  23. package/dist/swarm-recovery.js +134 -0
  24. package/dist/version.js +1 -1
  25. package/docs/migration.md +12 -2
  26. package/docs/registry-releases.md +13 -2
  27. package/docs/safety.md +20 -6
  28. package/docs/swarm-recovery.md +33 -0
  29. package/docs/windows-validation.md +39 -0
  30. package/package.json +7 -4
  31. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  32. package/plugins/kimi-codex/dist/cli-client.js +19 -9
  33. package/plugins/kimi-codex/dist/commands/cancel.js +14 -3
  34. package/plugins/kimi-codex/dist/commands/cli-helpers.js +5 -1
  35. package/plugins/kimi-codex/dist/commands/recover-swarm.js +135 -0
  36. package/plugins/kimi-codex/dist/commands/status.js +1 -1
  37. package/plugins/kimi-codex/dist/commands/swarm.js +241 -87
  38. package/plugins/kimi-codex/dist/companion.js +5 -1
  39. package/plugins/kimi-codex/dist/git.js +5 -3
  40. package/plugins/kimi-codex/dist/help.js +3 -2
  41. package/plugins/kimi-codex/dist/hooks/config-safety.js +4 -3
  42. package/plugins/kimi-codex/dist/job-store.js +13 -0
  43. package/plugins/kimi-codex/dist/jobs.js +3 -0
  44. package/plugins/kimi-codex/dist/kimi-engine.js +1 -0
  45. package/plugins/kimi-codex/dist/kimi-errors.js +27 -21
  46. package/plugins/kimi-codex/dist/parsing.js +1 -0
  47. package/plugins/kimi-codex/dist/process-tree.js +48 -17
  48. package/plugins/kimi-codex/dist/prompt-size.js +18 -0
  49. package/plugins/kimi-codex/dist/swarm-activation.js +833 -0
  50. package/plugins/kimi-codex/dist/swarm-recovery.js +134 -0
  51. package/plugins/kimi-codex/dist/version.js +1 -1
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Kimi for Claude Code: code questions, reviews, fixes, parallel tasks, and MCP tasks through the local Kimi Code CLI.",
8
- "version": "2.4.0"
8
+ "version": "2.4.2"
9
9
  },
10
10
  "plugins": [
11
11
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kimi",
3
3
  "description": "Kimi for Claude Code: code questions, independent reviews, bounded fixes, autonomous goals, parallel tasks, and MCP tasks through the local Kimi Code CLI, with PreToolUse safety hooks.",
4
- "version": "2.4.0",
4
+ "version": "2.4.2",
5
5
  "homepage": "https://github.com/linxule/kimi-plugin-cc",
6
6
  "repository": "https://github.com/linxule/kimi-plugin-cc",
7
7
  "license": "Apache-2.0",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  > **Post-1.0 release history (v1.0.1 -> present) lives in [ROADMAP-TO-GA.md § Post-GA audit log](./ROADMAP-TO-GA.md#post-ga-audit-log)** and the "Version" / "Upstream compat" lines of [AGENTS.md](./AGENTS.md). Docs-only kimi-code compat checkups that don't bump the plugin version (e.g. the 0.14.2 / 0.14.3 patches) are recorded there, not here. Notable releases are summarized below; the GA entry and full pre-GA detail follow.
4
4
 
5
+ ## 2.4.2 — 2026-10-26
6
+
7
+ - Integrate community PR #22's structured spawn-error classification, preserving contributor history. Bound the additional diagnostic message. Known argument/resource exhaustion no longer receives reinstall advice.
8
+ - Refuse oversized Windows command lines before spawn and add a credential-free compiled-runtime Windows CI lane. This improves diagnosis, not prompt transport; issue #14 remains open for native Windows/community verification and large-review support.
9
+ - Add durable, preview-first `recover-swarm` for exact 2.3.2 interrupted runs, with verified process ownership, pre-mutation session identity, recovery locking, explicit uncertainty refusals and patch preservation. Stop age-based deletion of abandoned worktrees. Exact CLI certification remains unchanged through 2.3.2.
10
+ - Shorten the agent contract sheet and enforce its 32 KiB limit during `check`.
11
+
12
+ Thanks to **@Nitjsefnie** for PR #22.
13
+
14
+ ## 2.4.1 — 2026-10-25
15
+
16
+ - Certify exact kimi-code 2.3.2 for all eight operations using the 15-test/92-assertion source scan, plan-ON/OFF controls, and the final 19-test/125-assertion production smoke. Keep the no-plan construction and legacy-v1 boundary at 0.41.x. See [evidence](docs/upstream-2.3.2-certification.md).
17
+ - Preserve swarm support after upstream removed model self-activation: exact-version, authenticated loopback activation creates a same-job session with the selected model, auto permission, and plan mode off, then resumes through ordinary `kimi -p`. `runSwarm` solely owns bounded deactivation and verifies the journal; older certified versions stay on direct print.
18
+ - Bind activation to exact provenance, finite budgets, process identity, and session identity (`preactivated`). Failure recovery is cancellation-aware and preserves primary print errors. Unknown late-fork or changed-PGID survivors remain unverified, never false quiescence or unproven signal targets. Cleanup failure blocks completion and retains raw output, structured diagnostics, and the write worktree. MCP uses the same implementation and existing task/job ownership.
19
+ - Close a certification blind spot found by adversarial review: AgentSwarm call records, aggregate denial text, and coordinator-only patches no longer count as fan-out. Smoke requires distinct child journals and correlates each assigned Write/Edit path and toolCallId with its own denial or successful result; the final production run passed 19/19 with 125 assertions in 296.67 seconds.
20
+
21
+ Contributors: **Kimi** (source audit, design, implementation, documentation, and verification), **Claude** and **Codex** (adversarial review and re-review).
22
+
5
23
  ## 2.4.0 — 2026-10-24
6
24
 
7
25
  - Add one dependency-free MCP stdio server for Claude Code and Codex, with typed schemas for seven model operations plus job status, result, and cancellation. The existing slash commands, skills, safety hooks, and review gate remain intact.
package/README.md CHANGED
@@ -40,9 +40,9 @@ You need:
40
40
  - macOS or Linux
41
41
  - Claude Code or Codex with plugin support
42
42
  - Node.js 22.5 or newer
43
- - Kimi Code CLI 2.3.1 (2.3.0 also certified), the recommended version for plugin 2.4.0
43
+ - Kimi Code CLI 2.3.2 (certified; 2.3.1 and 2.3.0 are also certified)
44
44
 
45
- Windows is not currently supported. Check your versions in a terminal:
45
+ Windows is not currently supported. Contributors can help with the [no-model Windows diagnostics checks](./docs/windows-validation.md). Check your versions in a terminal:
46
46
 
47
47
  ```sh
48
48
  node --version
@@ -100,15 +100,15 @@ If you use both Claude Code and Codex, run setup in each. Each app has its own h
100
100
  ## npm distribution (optional)
101
101
 
102
102
  The existing marketplace commands above remain the default for Claude Code and
103
- Codex. The same release is also available as
103
+ Codex. Published releases are also available as
104
104
  [`kimi-plugin-cc` on npm](https://www.npmjs.com/package/kimi-plugin-cc), including
105
105
  both compiled plugin distributions. No Bun, TypeScript, or install scripts are
106
106
  needed to use the package. Kimi Code CLI and Node are still prerequisites.
107
107
 
108
- For an explicitly versioned local Claude Code plugin:
108
+ For an explicitly versioned local Claude Code plugin, this example pins the published 2.4.2 package:
109
109
 
110
110
  ```sh
111
- npm install --prefix ~/.local/share/kimi-plugin-cc kimi-plugin-cc@2.4.0
111
+ npm install --prefix ~/.local/share/kimi-plugin-cc kimi-plugin-cc@2.4.2
112
112
  claude --plugin-dir ~/.local/share/kimi-plugin-cc/node_modules/kimi-plugin-cc
113
113
  ```
114
114
 
@@ -174,7 +174,7 @@ Use these commands to follow a task:
174
174
  | Read the result | `/kimi:result` | `$kimi-result` |
175
175
  | Cancel work | `/kimi:cancel` | `$kimi-cancel` |
176
176
 
177
- Without a job ID, cancel stops the latest running task for this repository. If several tasks are running, check which one you want to stop first. Use cancel rather than interrupting the app so write-swarm can save its patch.
177
+ Without a job ID, cancel stops the latest running task for this repository. If several tasks are running, check which one you want to stop first. Use cancel rather than interrupting the app so write-swarm can save its patch. For interrupted runs created by the unreleased recovery implementation, see [preview-first swarm recovery](./docs/swarm-recovery.md); it never applies a patch or deletes abandoned work.
178
178
 
179
179
  Canceling a task does not undo changes it has already made.
180
180
 
@@ -228,6 +228,8 @@ All refusal codes and their remedies are listed in the [refusal-code catalog](./
228
228
 
229
229
  The tool rules for ask, review, challenge, and read-only swarm deny file writes and shell commands. Rescue and pursue can write files in your workspace. Swarm with `--write` works in a temporary Git worktree. It starts from your latest commit, so it does not include uncommitted changes. It returns a patch without applying it to your project.
230
230
 
231
+ For swarm on CLI 2.3.2, the plugin briefly starts an authenticated loopback `kimi web` process to activate a session and another to deactivate swarm mode afterward; one ordinary `kimi -p -r` process still performs the task. Activation counts against the time limit, the access token stays in memory and out of logs, and cleanup failure prevents success. Older certified versions use `kimi -p` directly.
232
+
231
233
  The plugin does not commit changes. Review changes before you keep or apply them.
232
234
 
233
235
  Only use tasks that can write files in repositories you trust. Rescue, pursue, and swarm with `--write` can run tests and similar commands. These commands execute code from your repository. The hook restricts tool use; it is not an operating-system sandbox.
@@ -262,7 +264,7 @@ Contributors need Bun to run `bun run check`. The detailed engineering documents
262
264
  - macOS 或 Linux
263
265
  - 支持插件的 Claude Code 或 Codex
264
266
  - Node.js 22.5 或更高版本
265
- - Kimi Code CLI 2.3.1(2.3.0 亦已认证),这是插件 2.4.0 的推荐版本
267
+ - Kimi Code CLI 2.3.2(已认证;2.3.1 和 2.3.0 也已认证)
266
268
 
267
269
  目前不支持 Windows。在终端检查版本:
268
270
 
@@ -425,6 +427,8 @@ codex plugin add kimi@kimi-marketplace
425
427
 
426
428
  Ask、review、challenge 和只读 swarm 的工具规则禁止写入文件和执行 shell 命令。Rescue 和 pursue 可以修改工作区内的文件。带 `--write` 的 swarm 在临时 Git 工作树中操作。它从最新提交开始,不包含尚未提交的修改。任务结束后返回补丁,不会自动应用到项目。
427
429
 
430
+ 在 CLI 2.3.2 上运行 swarm 时,插件会短暂启动需要认证的本地回环 `kimi web` 进程来激活会话,并在结束后再次启动以停用 swarm 模式;任务本身仍由一个普通的 `kimi -p -r` 进程执行。激活计入任务时限,访问令牌只保存在内存且不会写入日志;无法确认清理完成时,任务不会报告成功。较早的已认证版本仍直接通过 `kimi -p` 运行。
431
+
428
432
  插件不会提交修改。保留或应用修改前,请先审查。
429
433
 
430
434
  只在你信任的仓库中运行能修改文件的任务。Rescue、pursue 和带 `--write` 的 swarm 可以运行测试等命令。这些命令会执行仓库中的代码。钩子限制的是工具调用,它不是操作系统沙箱。
@@ -459,7 +463,7 @@ Il vous faut :
459
463
  - macOS ou Linux
460
464
  - une version de Claude Code ou de Codex prenant en charge les plugins
461
465
  - Node.js 22.5 ou une version ultérieure
462
- - Kimi Code CLI 2.3.1 (2.3.0 également certifiée), la version recommandée pour le plugin 2.4.0
466
+ - Kimi Code CLI 2.3.2 (certifiée ; les versions 2.3.1 et 2.3.0 sont également certifiées)
463
467
 
464
468
  Windows n'est pas pris en charge actuellement. Vérifiez les versions dans un terminal :
465
469
 
@@ -622,6 +626,8 @@ Tous les codes de refus et leurs remèdes sont répertoriés dans le [catalogue
622
626
 
623
627
  Les règles des modes ask, review, challenge et swarm en lecture seule interdisent les écritures et les commandes shell. Rescue et pursue peuvent modifier les fichiers de votre espace de travail. Swarm avec `--write` travaille dans un worktree Git temporaire. Il part du dernier commit et n'inclut donc pas les modifications non commitées. Il renvoie un patch sans l'appliquer à votre projet.
624
628
 
629
+ Avec le CLI 2.3.2, les tâches swarm lancent brièvement un processus `kimi web` authentifié sur l'interface de boucle locale pour activer la session, puis un second pour désactiver le mode swarm ; un seul processus `kimi -p -r` ordinaire exécute toujours la tâche. L'activation est comptée dans la limite de temps, le jeton reste en mémoire et hors des journaux, et l'échec du nettoyage empêche de déclarer la réussite. Les versions certifiées antérieures utilisent directement `kimi -p`.
630
+
625
631
  Le plugin ne crée aucun commit. Examinez les modifications avant de les conserver ou de les appliquer.
626
632
 
627
633
  N'utilisez les tâches pouvant écrire des fichiers que dans des dépôts auxquels vous faites confiance. Rescue, pursue et swarm avec `--write` peuvent lancer des tests et des commandes similaires. Ces commandes exécutent du code du dépôt. Le hook limite l'utilisation des outils ; ce n'est pas un bac à sable du système d'exploitation.
@@ -656,7 +662,7 @@ Les contributeurs ont besoin de Bun pour exécuter `bun run check`. La documenta
656
662
  - macOS または Linux
657
663
  - プラグインに対応した Claude Code または Codex
658
664
  - Node.js 22.5 以降
659
- - Kimi Code CLI 2.3.1(2.3.0 も認証済み。プラグイン 2.4.0 の推奨バージョン)
665
+ - Kimi Code CLI 2.3.2(認証済み。2.3.1 と 2.3.0 も認証済み)
660
666
 
661
667
  現在、Windows には対応していません。ターミナルでバージョンを確認します。
662
668
 
@@ -819,6 +825,8 @@ codex plugin add kimi@kimi-marketplace
819
825
 
820
826
  Ask、review、challenge、読み取り専用 swarm のツールルールでは、ファイルへの書き込みとシェルコマンドを禁止しています。Rescue と pursue はワークスペース内のファイルを変更できます。`--write` を指定した swarm は、一時的な Git ワークツリーで作業します。最新のコミットから開始するため、未コミットの変更は含まれません。パッチを返すだけで、プロジェクトには自動適用しません。
821
827
 
828
+ CLI 2.3.2 の swarm では、セッションを有効化するため、そして終了後に swarm モードを無効化するため、認証付きのローカルループバック `kimi web` プロセスをそれぞれ短時間だけ起動します。タスク自体は従来どおり 1 つの `kimi -p -r` プロセスが実行します。有効化も制限時間に含まれ、トークンはメモリー内にのみ保持されてログには出ません。後処理を確認できない場合、タスクは成功と報告されません。以前の認証済みバージョンでは、引き続き `kimi -p` を直接使います。
829
+
822
830
  プラグインは変更をコミットしません。変更を残す、または適用する前に内容を確認してください。
823
831
 
824
832
  ファイルを書き換えるタスクは、信頼できるリポジトリでのみ実行してください。Rescue、pursue、`--write` 付きの swarm は、テストなどのコマンドを実行できます。これらのコマンドはリポジトリ内のコードを実行します。フックはツールの使用を制限しますが、OS のサンドボックスではありません。
@@ -7,6 +7,7 @@
7
7
  import { spawn } from "node:child_process";
8
8
  import { appendFile, mkdir } from "node:fs/promises";
9
9
  import path from "node:path";
10
+ import { assertPromptFitsCommandLine } from "./prompt-size.js";
10
11
  import { createHookEscapeMonitor } from "./hook-escape.js";
11
12
  import { RuntimeError, formatError } from "./errors.js";
12
13
  import { assertNoUnsafeExperimentalSelector, assertExecutionPlanMatchesSpawn, } from "./kimi-engine.js";
@@ -29,6 +30,14 @@ const DESCENDANT_SNAPSHOT_TIMEOUT_MS = 1_000;
29
30
  /** Hard bound for each batched identity revalidation pass. */
30
31
  const IDENTITY_REVALIDATION_TIMEOUT_MS = 250;
31
32
  const DESCENDANT_SNAPSHOT_TIMEOUT = Symbol("descendant-snapshot-timeout");
33
+ function spawnErrorDetails(error) {
34
+ const cause = error;
35
+ return {
36
+ spawn_code: cause?.code,
37
+ spawn_errno: cause?.errno,
38
+ spawn_message: formatError(error),
39
+ };
40
+ }
32
41
  export async function runCliPrompt(opts) {
33
42
  // Pre-aborted signal: refuse to spawn at all.
34
43
  if (opts.signal?.aborted === true) {
@@ -60,7 +69,8 @@ export async function runCliPrompt(opts) {
60
69
  });
61
70
  }
62
71
  const args = buildArgs(opts);
63
- const env = buildEnv(opts);
72
+ assertPromptFitsCommandLine(opts.command, args);
73
+ const env = buildKimiCliEnvironment(opts);
64
74
  if (opts.logPath !== undefined) {
65
75
  await mkdir(path.dirname(opts.logPath), { recursive: true }).catch(() => {
66
76
  // Best-effort; if mkdir fails the appendFile below will error and be
@@ -82,8 +92,8 @@ export async function runCliPrompt(opts) {
82
92
  }
83
93
  catch (err) {
84
94
  throw new RuntimeError("CLI_SPAWN_FAILED", `Failed to spawn kimi: ${formatError(err)}`, "cli-client.spawn", err instanceof Error
85
- ? { cause: err, details: { command: opts.command, args } }
86
- : { details: { command: opts.command, args } });
95
+ ? { cause: err, details: { command: opts.command, args, ...spawnErrorDetails(err) } }
96
+ : { details: { command: opts.command, args, ...spawnErrorDetails(err) } });
87
97
  }
88
98
  // Pre-attach an 'error' listener immediately. Bun (and Node) can emit
89
99
  // 'error' asynchronously between spawn returning and our Promise body
@@ -550,8 +560,8 @@ export async function runCliPrompt(opts) {
550
560
  child.on("error", (err) => {
551
561
  appendLogLine({ event: "process_error", message: formatError(err) });
552
562
  settle("reject", new RuntimeError("CLI_PROCESS_ERROR", `kimi subprocess error: ${formatError(err)}`, "cli-client.process", err instanceof Error
553
- ? { cause: err, details: errorDetails() }
554
- : { details: errorDetails() }));
563
+ ? { cause: err, details: { ...errorDetails(), ...spawnErrorDetails(err) } }
564
+ : { details: { ...errorDetails(), ...spawnErrorDetails(err) } }));
555
565
  });
556
566
  if (earlyErrors.length > 0) {
557
567
  const captured = earlyErrors[0];
@@ -562,7 +572,7 @@ export async function runCliPrompt(opts) {
562
572
  early: true,
563
573
  additional_count: earlyErrors.length - 1,
564
574
  });
565
- settle("reject", new RuntimeError("CLI_PROCESS_ERROR", `kimi subprocess error: ${formatError(captured)}`, "cli-client.process", { cause: captured, details: errorDetails() }));
575
+ settle("reject", new RuntimeError("CLI_PROCESS_ERROR", `kimi subprocess error: ${formatError(captured)}`, "cli-client.process", { cause: captured, details: { ...errorDetails(), ...spawnErrorDetails(captured) } }));
566
576
  });
567
577
  }
568
578
  child.on("close", async (exitCode, signal) => {
@@ -643,8 +653,8 @@ export async function runCliPrompt(opts) {
643
653
  }
644
654
  catch (err) {
645
655
  settle("reject", new RuntimeError("CLI_PROCESS_ERROR", `kimi subprocess stream setup failed: ${formatError(err)}`, "cli-client.streams", err instanceof Error
646
- ? { cause: err, details: errorDetails() }
647
- : { details: errorDetails() }));
656
+ ? { cause: err, details: { ...errorDetails(), ...spawnErrorDetails(err) } }
657
+ : { details: { ...errorDetails(), ...spawnErrorDetails(err) } }));
648
658
  }
649
659
  });
650
660
  }
@@ -788,7 +798,7 @@ function buildArgs(opts) {
788
798
  args.push("-p", opts.prompt);
789
799
  return args;
790
800
  }
791
- function buildEnv(opts) {
801
+ export function buildKimiCliEnvironment(opts) {
792
802
  const env = { ...opts.env };
793
803
  if (opts.thinkingEffort !== undefined)
794
804
  env.KIMI_MODEL_THINKING_EFFORT = opts.thinkingEffort;
@@ -36,6 +36,14 @@ export async function runCancel(argv, context) {
36
36
  (context.mcpCompanionIdentity === undefined || context.mcpKimiIdentity === undefined)) {
37
37
  throw new RuntimeError("MCP_CANCEL_REQUIRES_BINDING", `MCP job ${job.job_id} requires MCP job-control cancellation with recorded identities; use tasks/cancel or kimi_job_cancel.`, "mcp.cancel");
38
38
  }
39
+ const recovery = store.getSwarmRecovery(job.job_id)?.record;
40
+ if (recovery) {
41
+ const identity = recovery.owner;
42
+ const verified = identity && identity.pid === job.pid ? await revalidateProcessIdentities([identity]) : undefined;
43
+ if (!verified?.complete || verified.groupChanged?.length || verified.identities.length !== 1) {
44
+ throw new RuntimeError("SWARM_RECOVERY_REQUIRED", `Recorded swarm ownership is no longer live or verifiable. Preview companion.sh recover-swarm ${job.job_id}; no process was signalled.`, "cancel.identity");
45
+ }
46
+ }
39
47
  if (job.pid === null && job.kimi_pid === null) {
40
48
  throw new RuntimeError("CANCEL_NOT_SUPPORTED", `Job ${job.job_id} does not have a recorded process id to cancel.`, "cancel.runtime");
41
49
  }
@@ -75,7 +83,10 @@ export async function runCancel(argv, context) {
75
83
  }
76
84
  await assertMcpCompanionIdentity(job, context.mcpCompanionIdentity);
77
85
  await assertMcpKimiIdentity(job, context.mcpKimiIdentity);
78
- signalJobProcesses(job, "SIGTERM");
86
+ if (recovery)
87
+ await assertMcpCompanionIdentity(job, recovery.owner);
88
+ // The live swarm owner owns child teardown, including web phases after print exit.
89
+ signalJobProcesses(job, "SIGTERM", { companion: true, kimi: !recovery });
79
90
  const cancelled = await waitForCancellation(paths, job.job_id);
80
91
  if (cancelled) {
81
92
  return `${JSON.stringify({
@@ -84,10 +95,10 @@ export async function runCancel(argv, context) {
84
95
  message: "Cancellation completed.",
85
96
  }, null, 2)}\n`;
86
97
  }
87
- const companionMatches = await assertMcpCompanionIdentity(job, context.mcpCompanionIdentity, true);
98
+ const companionMatches = await assertMcpCompanionIdentity(job, recovery ? recovery.owner : context.mcpCompanionIdentity, true);
88
99
  const kimiMatches = await assertMcpKimiIdentity(job, context.mcpKimiIdentity, true);
89
100
  if (companionMatches || kimiMatches) {
90
- signalJobProcesses(job, "SIGKILL", { companion: companionMatches, kimi: kimiMatches });
101
+ signalJobProcesses(job, "SIGKILL", { companion: companionMatches, kimi: !recovery && kimiMatches });
91
102
  }
92
103
  await withJobStore(paths, async (forcedStore) => {
93
104
  const current = forcedStore.getJob(job.job_id);
@@ -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,
@@ -0,0 +1,135 @@
1
+ import { RECOVER_SWARM_SUPPORTED_FLAGS } from "../parsing.js";
2
+ import { randomUUID } from "node:crypto";
3
+ import { lstat, realpath, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { createCliCancellationHandlers } from "../cli-cancellation.js";
6
+ import { RuntimeError } from "../errors.js";
7
+ import { captureWorktreePatch, resolveRepoIdentity } from "../git.js";
8
+ import { verifyHookInstalled } from "../hooks/install.js";
9
+ import { markJobFailed } from "../jobs.js";
10
+ import { JobStore } from "../job-store.js";
11
+ import { executionPlanFromPersisted, prepareKimiExecutionPlan } from "../kimi-engine.js";
12
+ import { resolveKimiHome } from "../kimi-home.js";
13
+ import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
14
+ import { probeProcessIdentity } from "../process-tree.js";
15
+ import { deactivateSwarmSession } from "../swarm-activation.js";
16
+ import { assertRecoveryOwnerGone, finishRecoveryRecord, recordRecoveryEvent, recoverLaunch, recoveryError } from "../swarm-recovery.js";
17
+ export async function runRecoverSwarm(argv, context) {
18
+ const positional = argv.filter((arg) => !arg.startsWith("-"));
19
+ if (positional.length !== 1 || argv.some((arg) => arg.startsWith("-") && arg !== RECOVER_SWARM_SUPPORTED_FLAGS) || argv.filter((arg) => arg === RECOVER_SWARM_SUPPORTED_FLAGS).length > 1) {
20
+ throw new RuntimeError("INVALID_ARGS", `Usage: companion.sh recover-swarm <job-id> [${RECOVER_SWARM_SUPPORTED_FLAGS}]. Defaults to preview.`, "swarm.recovery.args");
21
+ }
22
+ const jobId = positional[0];
23
+ const apply = argv.includes(RECOVER_SWARM_SUPPORTED_FLAGS);
24
+ const paths = resolvePluginPaths(context.env);
25
+ await ensurePluginPaths(paths);
26
+ const repo = await resolveRepoIdentity(context.cwd);
27
+ const store = new JobStore(paths);
28
+ let claim;
29
+ const handlers = createCliCancellationHandlers();
30
+ const signal = AbortSignal.any([handlers.signal, AbortSignal.timeout(30_000)]);
31
+ try {
32
+ const job = store.getJob(jobId);
33
+ const saved = store.getSwarmRecovery(jobId);
34
+ if (!job || job.repo_id !== repo.repoId || !saved || saved.record.schema !== 1 || saved.record.jobId !== jobId) {
35
+ throw recoveryError("No recovery record owned by this repository. Older runs require manual inspection.");
36
+ }
37
+ let record = saved.record;
38
+ const summary = () => ({ job_id: jobId, mode: apply ? "apply" : "preview", recovery_complete: record.complete,
39
+ session_id: record.session?.sessionId ?? null, session_cleanup_pending: record.sessionDirty,
40
+ launches: record.launches, worktree: record.workspaceRoot, patch_path: record.patchPath,
41
+ next_step: record.complete ? "No recovery is needed. Retained worktrees may be inspected and removed manually."
42
+ : `Inspect this record; use companion.sh recover-swarm ${jobId} --apply only after the original job has stopped.` });
43
+ const settleAbandonedJob = async () => {
44
+ if (job.status === "running")
45
+ await markJobFailed(store, paths, job, new RuntimeError("SWARM_OWNER_DISAPPEARED", "The original swarm companion stopped without reporting a result; resource recovery completed, not the task.", "swarm.recovery"), "Abandoned swarm resources recovered; task did not complete.", { phase: "failed" });
46
+ };
47
+ if (!apply)
48
+ return `${JSON.stringify(summary(), null, 2)}\n`;
49
+ if (record.complete) {
50
+ if (job.status === "running") {
51
+ await assertRecoveryOwnerGone(record);
52
+ await settleAbandonedJob();
53
+ }
54
+ return `${JSON.stringify(summary(), null, 2)}\n`;
55
+ }
56
+ await assertRecoveryOwnerGone(record);
57
+ if (saved.owner !== null) {
58
+ const owner = JSON.parse(saved.owner);
59
+ const live = await probeProcessIdentity(owner.pid);
60
+ if (!live.complete || live.identity?.startToken === owner.startToken)
61
+ throw recoveryError("Another recovery owner is alive or cannot be verified.");
62
+ }
63
+ const self = await probeProcessIdentity(process.pid);
64
+ if (!self.complete || !self.identity)
65
+ throw recoveryError("Cannot establish recovery ownership on this host.");
66
+ claim = JSON.stringify(self.identity);
67
+ if (!store.claimSwarmRecovery(jobId, saved.owner, claim)) {
68
+ claim = undefined;
69
+ throw recoveryError("Another recovery acquired this job.");
70
+ }
71
+ // Another owner may have completed and released the claim between our first read
72
+ // and the CAS (null -> owner -> null). Never overwrite that newer journal.
73
+ record = store.getSwarmRecovery(jobId).record;
74
+ if (record.complete) {
75
+ await settleAbandonedJob();
76
+ return `${JSON.stringify(summary(), null, 2)}\n`;
77
+ }
78
+ const expectedRoot = job.operation_kind === "swarm-write"
79
+ ? path.join(paths.worktreesDir, job.repo_id, `swarm-write-${job.job_id}`) : null;
80
+ if (record.workspaceRoot !== expectedRoot || record.cwd !== (expectedRoot ?? job.cwd)
81
+ || record.kimiHome !== resolveKimiHome(context.env, record.cwd)) {
82
+ throw recoveryError("Recovery workspace or Kimi home differs from the recorded job. Use its original home and repository; do not guess a session.");
83
+ }
84
+ if (expectedRoot) {
85
+ const st = await lstat(expectedRoot);
86
+ if (!st.isDirectory() || st.isSymbolicLink())
87
+ throw recoveryError("Recovery worktree must be the original non-symlink directory.");
88
+ const parent = await realpath(path.dirname(expectedRoot));
89
+ if (await realpath(expectedRoot) !== path.join(parent, path.basename(expectedRoot)))
90
+ throw recoveryError("Recovery worktree path is redirected.");
91
+ }
92
+ const persist = () => store.saveSwarmRecovery(jobId, record);
93
+ for (const launch of record.launches)
94
+ await recoverLaunch(launch, persist, signal);
95
+ if (record.sessionDirty) {
96
+ if (!record.session || !/^[\w-]+$/.test(record.session.sessionId) || !/^[\w-]+$/.test(record.session.workspaceId))
97
+ throw recoveryError("Recovery session identity is missing or invalid.");
98
+ const installed = await verifyHookInstalled(context.env);
99
+ if (!installed.installed)
100
+ throw recoveryError(`Managed hook verification failed: ${installed.reason ?? "unknown"}. Recovery does not install or relax hooks.`);
101
+ const plan = executionPlanFromPersisted(job);
102
+ const current = await prepareKimiExecutionPlan({ operationKind: plan.operationKind, cwd: record.cwd, env: context.env });
103
+ if (current.command !== plan.command || current.kimiVersion !== plan.kimiVersion || JSON.stringify(current.prefixArgs) !== JSON.stringify(plan.prefixArgs))
104
+ throw recoveryError("The exact recorded CLI is not selected. Restore that certified binary before session cleanup.");
105
+ signal.throwIfAborted();
106
+ await deactivateSwarmSession({ cwd: record.cwd, env: context.env, command: current.command,
107
+ prefixArgs: current.prefixArgs, executionPlan: current, commandLabel: expectedRoot ? "swarm-write" : "swarm",
108
+ trustedWorkspaceRoot: expectedRoot ?? undefined, ...record.session, signal,
109
+ onRecoveryEvent: (event) => recordRecoveryEvent(store, record, event) });
110
+ }
111
+ signal.throwIfAborted();
112
+ if (expectedRoot) {
113
+ // Capture without applying or deleting. A crash after the write leaves a recoverable artifact.
114
+ const patch = await captureWorktreePatch(expectedRoot, signal);
115
+ const patchPath = path.join(paths.artifactsDir, `swarm-recovered-${jobId}-${randomUUID()}.patch`);
116
+ await writeFile(patchPath, patch, { flag: "wx", mode: 0o600 });
117
+ record.patchPath = patchPath;
118
+ }
119
+ finishRecoveryRecord(store, record);
120
+ if (!record.complete)
121
+ throw recoveryError("Recovery remains incomplete; evidence and worktree were retained.");
122
+ await settleAbandonedJob();
123
+ return `${JSON.stringify(summary(), null, 2)}\n`;
124
+ }
125
+ finally {
126
+ handlers.dispose();
127
+ try {
128
+ if (claim !== undefined)
129
+ store.claimSwarmRecovery(jobId, claim, null);
130
+ }
131
+ finally {
132
+ store.close();
133
+ }
134
+ }
135
+ }
@@ -22,6 +22,6 @@ export async function runStatus(argv, context) {
22
22
  throw new RuntimeError("JOB_NOT_FOUND", "No matching job was found for status.", "status.lookup");
23
23
  }
24
24
  const job = await reconcileHistoricalJobProvenance(store, found);
25
- return `${JSON.stringify({ ...job, thinking_effort_requested: job.thinking_effort ?? null, hook_escapes: parseHookEscapes(job.hook_escapes), budget_source: renderBudgetSource(job) }, null, 2)}\n`;
25
+ return `${JSON.stringify({ ...job, swarm_recovery: store.getSwarmRecovery(job.job_id)?.record ?? null, thinking_effort_requested: job.thinking_effort ?? null, hook_escapes: parseHookEscapes(job.hook_escapes), budget_source: renderBudgetSource(job) }, null, 2)}\n`;
26
26
  });
27
27
  }