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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +18 -0
- package/README.md +17 -9
- package/dist/cli-client.js +19 -9
- package/dist/commands/cancel.js +14 -3
- package/dist/commands/cli-helpers.js +5 -1
- package/dist/commands/recover-swarm.js +135 -0
- package/dist/commands/status.js +1 -1
- package/dist/commands/swarm.js +241 -87
- package/dist/companion.js +5 -1
- package/dist/git.js +5 -3
- package/dist/help.js +3 -2
- package/dist/hooks/config-safety.js +4 -3
- package/dist/job-store.js +13 -0
- package/dist/jobs.js +3 -0
- package/dist/kimi-engine.js +1 -0
- package/dist/kimi-errors.js +27 -21
- package/dist/parsing.js +1 -0
- package/dist/process-tree.js +48 -17
- package/dist/prompt-size.js +18 -0
- package/dist/swarm-activation.js +833 -0
- package/dist/swarm-recovery.js +134 -0
- package/dist/version.js +1 -1
- package/docs/migration.md +12 -2
- package/docs/registry-releases.md +13 -2
- package/docs/safety.md +20 -6
- package/docs/swarm-recovery.md +33 -0
- package/docs/windows-validation.md +39 -0
- package/package.json +7 -4
- package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/kimi-codex/dist/cli-client.js +19 -9
- package/plugins/kimi-codex/dist/commands/cancel.js +14 -3
- package/plugins/kimi-codex/dist/commands/cli-helpers.js +5 -1
- package/plugins/kimi-codex/dist/commands/recover-swarm.js +135 -0
- package/plugins/kimi-codex/dist/commands/status.js +1 -1
- package/plugins/kimi-codex/dist/commands/swarm.js +241 -87
- package/plugins/kimi-codex/dist/companion.js +5 -1
- package/plugins/kimi-codex/dist/git.js +5 -3
- package/plugins/kimi-codex/dist/help.js +3 -2
- package/plugins/kimi-codex/dist/hooks/config-safety.js +4 -3
- package/plugins/kimi-codex/dist/job-store.js +13 -0
- package/plugins/kimi-codex/dist/jobs.js +3 -0
- package/plugins/kimi-codex/dist/kimi-engine.js +1 -0
- package/plugins/kimi-codex/dist/kimi-errors.js +27 -21
- package/plugins/kimi-codex/dist/parsing.js +1 -0
- package/plugins/kimi-codex/dist/process-tree.js +48 -17
- package/plugins/kimi-codex/dist/prompt-size.js +18 -0
- package/plugins/kimi-codex/dist/swarm-activation.js +833 -0
- package/plugins/kimi-codex/dist/swarm-recovery.js +134 -0
- package/plugins/kimi-codex/dist/version.js +1 -1
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 のサンドボックスではありません。
|
package/dist/cli-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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;
|
package/dist/commands/cancel.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/commands/status.js
CHANGED
|
@@ -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
|
}
|