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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { RuntimeError } from "./errors.js";
|
|
2
|
+
import { collectDescendantIdentities, collectProcessGroupsIdentities, probeProcessIdentity, revalidateProcessIdentities } from "./process-tree.js";
|
|
3
|
+
export function recordRecoveryEvent(store, record, event) {
|
|
4
|
+
record.complete = false;
|
|
5
|
+
if (event.kind === "session") {
|
|
6
|
+
record.session = { sessionId: event.sessionId, workspaceId: event.workspaceId };
|
|
7
|
+
// Write-ahead: this must commit before the profile mutation is attempted.
|
|
8
|
+
record.sessionDirty = true;
|
|
9
|
+
}
|
|
10
|
+
else if (event.kind === "disabled") {
|
|
11
|
+
record.sessionDirty = false;
|
|
12
|
+
}
|
|
13
|
+
else if (event.kind.endsWith("-spawn")) {
|
|
14
|
+
record.launches.push({ kind: event.kind === "web-spawn" ? "web" : "print", pending: true, identities: [], quiescent: false, uncertain: false });
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
const kind = event.kind.startsWith("web-") ? "web" : "print";
|
|
18
|
+
const launch = [...record.launches].reverse().find((entry) => entry.kind === kind);
|
|
19
|
+
if (!launch)
|
|
20
|
+
throw recoveryError("Missing durable launch intent.");
|
|
21
|
+
if (event.kind === "web-exit" || event.kind === "print-exit") {
|
|
22
|
+
launch.quiescent = true;
|
|
23
|
+
launch.pending = false;
|
|
24
|
+
}
|
|
25
|
+
else if ("identities" in event) {
|
|
26
|
+
const all = [...launch.identities, ...event.identities];
|
|
27
|
+
launch.identities = [...new Map(all.map((identity) => [`${identity.pid}:${identity.startToken}:${identity.processGroupId}`, identity])).values()];
|
|
28
|
+
launch.pending = launch.identities.length === 0;
|
|
29
|
+
launch.uncertain ||= !event.complete;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
store.saveSwarmRecovery(record.jobId, record);
|
|
33
|
+
}
|
|
34
|
+
export function finishRecoveryRecord(store, record) {
|
|
35
|
+
record.complete = !record.sessionDirty && record.launches.every((entry) => entry.quiescent)
|
|
36
|
+
&& (record.workspaceRoot === null || record.patchPath !== null);
|
|
37
|
+
store.saveSwarmRecovery(record.jobId, record);
|
|
38
|
+
}
|
|
39
|
+
export function recoveryError(message) {
|
|
40
|
+
return new RuntimeError("SWARM_RECOVERY_REQUIRED", message, "swarm.recovery");
|
|
41
|
+
}
|
|
42
|
+
export async function assertRecoveryOwnerGone(record) {
|
|
43
|
+
if (!record.owner || process.platform === "win32")
|
|
44
|
+
throw recoveryError("No durable, verifiable owner identity; recovery requires manual inspection on this host.");
|
|
45
|
+
const probe = await probeProcessIdentity(record.ownerPid);
|
|
46
|
+
if (!probe.complete)
|
|
47
|
+
throw recoveryError("Cannot verify the original companion's identity.");
|
|
48
|
+
if (probe.identity?.startToken === record.owner.startToken && !/^[ZX]/i.test(probe.identity.state)) {
|
|
49
|
+
throw recoveryError("The original companion is still alive. Use cancel for a running job, not recovery.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Linux may fall back from /proc to ps for a whole-group snapshot while targeted
|
|
54
|
+
* identity reads still use /proc. Those start tokens are different namespaces,
|
|
55
|
+
* not different processes. Prove both representations current around the known
|
|
56
|
+
* identity read; never accept a member based on PID/PGID equality alone.
|
|
57
|
+
*/
|
|
58
|
+
export async function validateRecoveryGroupMembers(known, members, validate = revalidateProcessIdentities) {
|
|
59
|
+
const membership = await validate(members);
|
|
60
|
+
const ownership = await validate(known);
|
|
61
|
+
const membershipAfter = await validate(membership.identities);
|
|
62
|
+
for (const proof of [membership, ownership, membershipAfter]) {
|
|
63
|
+
if (!proof.complete || proof.groupChanged?.length)
|
|
64
|
+
throw recoveryError("Process ownership or group membership is uncertain.");
|
|
65
|
+
}
|
|
66
|
+
for (const member of membershipAfter.identities) {
|
|
67
|
+
const owner = ownership.identities.find((id) => id.pid === member.pid && id.processGroupId === member.processGroupId);
|
|
68
|
+
const crossNamespace = owner && ((owner.startToken.startsWith("proc:") && member.startToken.startsWith("ps:")) ||
|
|
69
|
+
(owner.startToken.startsWith("ps:") && member.startToken.startsWith("proc:")));
|
|
70
|
+
if (!owner || (owner.startToken !== member.startToken && !crossNamespace)) {
|
|
71
|
+
throw recoveryError("An unrecorded process survives in the recorded group; refusing group cleanup.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return ownership.identities;
|
|
75
|
+
}
|
|
76
|
+
/** Never signal an unproven group, PID replacement, or changed-PGID process. */
|
|
77
|
+
export async function recoverLaunch(launch, persist, signal) {
|
|
78
|
+
if (launch.quiescent)
|
|
79
|
+
return;
|
|
80
|
+
if (launch.pending || launch.uncertain || launch.identities.length === 0) {
|
|
81
|
+
throw recoveryError("Launch ownership was not fully recorded before interruption; manual inspection is required. No process was signalled.");
|
|
82
|
+
}
|
|
83
|
+
const root = launch.identities[0];
|
|
84
|
+
const rootProbe = await probeProcessIdentity(root.pid);
|
|
85
|
+
if (!rootProbe.complete)
|
|
86
|
+
throw recoveryError("Process identity probe is incomplete.");
|
|
87
|
+
if (rootProbe.identity?.startToken === root.startToken) {
|
|
88
|
+
if (rootProbe.identity.processGroupId !== root.processGroupId)
|
|
89
|
+
throw recoveryError("The recorded root changed process groups; refusing to signal it.");
|
|
90
|
+
const descendants = await collectDescendantIdentities(root.pid);
|
|
91
|
+
// Revalidate the ancestry anchor after collection before retaining new identities.
|
|
92
|
+
const after = await probeProcessIdentity(root.pid);
|
|
93
|
+
if (!after.complete || after.identity?.startToken !== root.startToken || after.identity.processGroupId !== root.processGroupId) {
|
|
94
|
+
throw recoveryError("Process ownership changed during recovery inspection.");
|
|
95
|
+
}
|
|
96
|
+
launch.identities = [...new Map([...launch.identities, ...descendants].map((id) => [`${id.pid}:${id.startToken}:${id.processGroupId}`, id])).values()];
|
|
97
|
+
persist();
|
|
98
|
+
}
|
|
99
|
+
const inspect = async () => {
|
|
100
|
+
const members = await collectProcessGroupsIdentities(launch.identities.map((id) => id.processGroupId));
|
|
101
|
+
if (members === undefined)
|
|
102
|
+
throw recoveryError("Process group membership is unreadable.");
|
|
103
|
+
return validateRecoveryGroupMembers(launch.identities, members);
|
|
104
|
+
};
|
|
105
|
+
for (const [killSignal, waitMs] of [["SIGTERM", 1500], ["SIGKILL", 500]]) {
|
|
106
|
+
signal?.throwIfAborted();
|
|
107
|
+
for (const identity of await inspect()) {
|
|
108
|
+
signal?.throwIfAborted();
|
|
109
|
+
const current = await revalidateProcessIdentities([identity]);
|
|
110
|
+
if (!current.complete || current.groupChanged?.length)
|
|
111
|
+
throw recoveryError("Process identity changed before signalling.");
|
|
112
|
+
if (current.identities.length) {
|
|
113
|
+
try {
|
|
114
|
+
process.kill(identity.pid, killSignal);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error.code !== "ESRCH")
|
|
118
|
+
throw recoveryError("Unable to stop the recorded process.");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const deadline = Date.now() + waitMs;
|
|
123
|
+
do {
|
|
124
|
+
signal?.throwIfAborted();
|
|
125
|
+
if ((await inspect()).length === 0) {
|
|
126
|
+
launch.quiescent = true;
|
|
127
|
+
persist();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
131
|
+
} while (Date.now() < deadline);
|
|
132
|
+
}
|
|
133
|
+
throw recoveryError("Recorded processes did not become quiescent; recovery remains pending.");
|
|
134
|
+
}
|
package/dist/version.js
CHANGED
package/docs/migration.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Compatibility and upgrades
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
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,19 @@ remain available. The package has no install scripts and no separate CLI.
|
|
|
6
6
|
|
|
7
7
|
## Publishing status
|
|
8
8
|
|
|
9
|
-
Plugin **2.
|
|
10
|
-
|
|
9
|
+
Plugin **2.4.2** adds durable interrupted-swarm recovery and Windows spawn diagnostics.
|
|
10
|
+
Exact CLI certification remains unchanged through **2.3.2**; this is not a new
|
|
11
|
+
Windows-support or large-prompt-transport claim. See the [release](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.4.2),
|
|
12
|
+
[recovery guide](./swarm-recovery.md), and [Windows community controls](./windows-validation.md).
|
|
13
|
+
Publication uses the same validated-tarball OIDC workflow below; release execution
|
|
14
|
+
and registry verification are recorded in the release notes.
|
|
15
|
+
|
|
16
|
+
Plugin **2.4.1** certifies exact CLI **2.3.2** for all eight operations with host-preactivated swarm sessions. [Release v2.4.1](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.4.1) is anchored at `06a0a47f2b34dffb8dd71e7ce93646212e74ed19`; its [CI check](https://github.com/linxule/kimi-plugin-cc/actions/runs/40236704464) and [release-event publishing run](https://github.com/linxule/kimi-plugin-cc/actions/runs/40237085945) passed. The registry tarball has SHA-1 `77fe6daaa0f23750cc9b5d857bdbdd5107a7d4ce`, contains 224 files, and matches the CI artifact byte-for-byte. Fresh installs from the CI artifact and npm registry both passed isolated setup/check for Claude Code and Codex. The earlier CI attempt 40231073897 exposed an intermittent root-exit cleanup issue; the root-continuity fix and independent review landed before tagging. Final closeout CI run 40238295139 passed.
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
The previous verified publication was **2.3.1**:
|
|
20
|
+
[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).
|
|
21
|
+
Earlier verified publication was **2.2.2**:
|
|
11
22
|
[Release v2.2.2](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.2.2)
|
|
12
23
|
is anchored at `256e78ff7cbf302fb19e8e523d8e3f943f7cb366`; its
|
|
13
24
|
[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.
|
|
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,26 @@ 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
|
+
For interrupted runs, see [durable recovery and its explicit limits](swarm-recovery.md).
|
|
211
|
+
|
|
212
|
+
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.
|
|
213
|
+
|
|
214
|
+
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:
|
|
215
|
+
|
|
216
|
+
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.
|
|
217
|
+
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.
|
|
218
|
+
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.
|
|
219
|
+
|
|
220
|
+
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.
|
|
221
|
+
|
|
208
222
|
## Read-only swarm (`/kimi:swarm`)
|
|
209
223
|
|
|
210
224
|
`/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
225
|
|
|
212
226
|
- **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.) **
|
|
227
|
+
- **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
228
|
- **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
229
|
- **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
230
|
- **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.
|
|
@@ -224,9 +238,9 @@ If another installed host still points to an older hook, that hook may veto thes
|
|
|
224
238
|
- **Writes are confined by the hook to a forge-proof trusted root.** The `swarm-write` hook label routes write/edit/shell through the **rescue allowlist verbatim** (workspace check, symlink/realpath reject, mutating-flag + exec-delegating-flag detector, git-mutation deny), but scoped to `KIMI_PLUGIN_CC_WORKSPACE_ROOT` — the worktree path the plugin exports on the spawn — **NOT** the hook payload `cwd`. The model running inside kimi cannot set an env var on its already-spawned process, so confinement does not depend on how upstream derives the payload cwd for an in-process subagent. A missing trusted root **fails closed** (all writes denied). An out-of-worktree write (absolute path into the user's real tree) is rejected by the allowlist's `isWithin` check; a write to the worktree's `.git` pointer is rejected by the `.git`-segment check.
|
|
225
239
|
- **`coder` subagents really can write, and the hook is the sole boundary.** Verified at kimi-code 0.42.0: swarm subagents get their own eager external-hooks service under the standard before-execute channel — there is no deny-all path on the swarm route (the `btw` side-question path is separate). Write-swarm's `coder` subagents DO have edit tools, and the managed PreToolUse hook is the **only** write boundary — which is why the trusted-root confinement above is load-bearing and gated tightly.
|
|
226
240
|
- **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
|
-
- **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.
|
|
241
|
+
- **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. Abandoned worktrees are retained, never age-deleted. Use the preview/apply recovery procedure in [swarm-recovery.md](swarm-recovery.md); uncertain ownership refuses automatic cleanup.
|
|
228
242
|
- **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
|
-
- **
|
|
243
|
+
- **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
244
|
- **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
245
|
|
|
232
246
|
## MCP transport and long-running jobs
|
|
@@ -279,9 +293,9 @@ at it (the runtime resolves that path instead of `kimi` on `PATH`; every spawn
|
|
|
279
293
|
still exports `KIMI_CODE_NO_AUTO_UPDATE=1`, so the pinned copy never drifts).
|
|
280
294
|
|
|
281
295
|
```sh
|
|
282
|
-
PIN=~/.local/share/kimi-plugin-cc/kimi-bin/2.3.
|
|
296
|
+
PIN=~/.local/share/kimi-plugin-cc/kimi-bin/2.3.2
|
|
283
297
|
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.
|
|
298
|
+
bun add @moonshot-ai/kimi-code@2.3.2
|
|
285
299
|
export KIMI_PLUGIN_CC_KIMI_BIN="$PIN/node_modules/.bin/kimi" # e.g. in ~/.zshenv
|
|
286
300
|
```
|
|
287
301
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Recovering an interrupted swarm
|
|
2
|
+
|
|
3
|
+
Normal stop: use `scripts/companion.sh cancel` for a shell-owned run, or the MCP job cancel tool for an MCP-owned run. A hard kill cannot execute cleanup. Cancellation still starts no fresh cleanup server.
|
|
4
|
+
|
|
5
|
+
For exact CLI 2.3.2 swarm runs created by this runtime, status includes a durable `swarm_recovery` record. It contains the resolved home/workspace, companion identity, launch intent, verified process identities and session identity. It never contains the web authentication token, credentials, prompt or config contents. Session identity is committed before the request enabling swarm mode.
|
|
6
|
+
|
|
7
|
+
## Preview, then recover
|
|
8
|
+
|
|
9
|
+
Run from the original repository with its original plugin data root and Kimi home:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
scripts/companion.sh recover-swarm <job-id>
|
|
13
|
+
scripts/companion.sh recover-swarm <job-id> --apply
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Preview does not signal processes, modify sessions, or capture patches. Apply:
|
|
17
|
+
|
|
18
|
+
1. Requires the original companion to be gone and acquires an identity-checked recovery claim. A second recovery cannot race the first; a dead recovery owner can be reclaimed by compare-and-swap.
|
|
19
|
+
2. Revalidates recorded processes and groups. It stops only verified individual identities, never a guessed PID or unproven group. An unknown surviving group member, changed group, incomplete probe or unrecorded launch refuses recovery.
|
|
20
|
+
3. If session cleanup is pending, requires the selected exact certified CLI and valid managed hook. It starts one bounded authenticated web lifecycle, disables the recorded session's swarm mode, verifies status and the exit journal, then verifies process quiescence. It does not resume a prompt, generate model output, install hooks or retry failed cleanup automatically. New recovery web launches are themselves journaled.
|
|
21
|
+
4. Captures a binary patch from a retained write worktree. It does not apply the patch or delete the tree. The result names both paths for inspection.
|
|
22
|
+
|
|
23
|
+
The recovery attempt has a 30-second cancellation signal, with bounded identity probes and normal teardown. Interrupting recovery retains its journal. An already-recovered record is an idempotent no-op. Failed/cancelled/completed job outcomes are immutable; a previously running abandoned job becomes failed, never completed. Recovery status is separate from task success.
|
|
24
|
+
|
|
25
|
+
## Limits and manual inspection
|
|
26
|
+
|
|
27
|
+
- SIGKILL between process creation and durable identity capture leaves an explicit pending launch. Apply refuses to guess ownership. A supervisor/OS-owned launch primitive would be required to eliminate this remaining crash window; the journal does not claim that guarantee.
|
|
28
|
+
- No recovery record means no automatic recovery: older runs, or a failure before the record was created, require inspection. Recovery is not general-purpose process or native-session cleanup.
|
|
29
|
+
- Windows does not currently provide the required durable process-identity proof. Preview is available; apply refuses. This does not change existing Windows direct-child cancellation limits.
|
|
30
|
+
- A changed home, missing/redirected worktree, CLI mismatch, invalid hook or uncertain identity leaves evidence intact and reports failure. Do not repair this by bypassing the hook or killing processes by name.
|
|
31
|
+
- Abandoned worktrees are no longer age-deleted. Inspect and recover them before explicitly removing them. This also protects legacy orphaned trees without a recovery record.
|
|
32
|
+
|
|
33
|
+
The trust boundary is the same local, host-owned plugin database used for execution plans. Recovery does not make an attacker-modified database trustworthy. Internal Git capture retains the repository-configuration trust boundary documented in [safety.md](safety.md).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Windows community validation
|
|
2
|
+
|
|
3
|
+
Windows remains unsupported; this is a narrow, credential-free diagnostics lane, not a support announcement. Contributions that reproduce native CLI and Git Bash behavior are welcome in [issue #14](https://github.com/linxule/kimi-plugin-cc/issues/14).
|
|
4
|
+
|
|
5
|
+
## What is covered
|
|
6
|
+
|
|
7
|
+
Community [PR #22](https://github.com/linxule/kimi-plugin-cc/pull/22) preserves structured spawn causes and distinguishes argument/resource exhaustion from a missing executable. Its author’s commits remain in the merge history.
|
|
8
|
+
|
|
9
|
+
The added command-line guard rejects an oversized Windows command before spawn. It counts UTF-16 units, argument quoting and the terminating null. The new error is `CLI_COMMAND_LINE_TOO_LONG`; it identifies prompt size rather than suggesting reinstall/setup. It carries counts, not prompt contents.
|
|
10
|
+
|
|
11
|
+
The Windows CI lane compiles the runtime, checks quoting with a real short Node subprocess, and sends a 103 KB prompt through the compiled client to prove refusal before spawn. It uses no credentials or provider calls.
|
|
12
|
+
|
|
13
|
+
## Reproduce the no-model controls
|
|
14
|
+
|
|
15
|
+
From a source checkout with Node 22.5+ and Bun:
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
bun install --frozen-lockfile
|
|
19
|
+
bunx tsc -p tsconfig.build.json
|
|
20
|
+
node --test tests/windows/spawn-preflight.mjs
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
These controls do not start Kimi or modify its configuration. The checked-in distribution contains the vendored JavaScript required by the compiled client.
|
|
24
|
+
|
|
25
|
+
## Evidence that would help next
|
|
26
|
+
|
|
27
|
+
For a native Kimi or Git Bash report, include:
|
|
28
|
+
|
|
29
|
+
- Windows, Node, Bun, Kimi and plugin versions; terminal/shell and install method.
|
|
30
|
+
- Whether a short invocation works with the same executable and environment.
|
|
31
|
+
- Whether a large review now refuses with `CLI_COMMAND_LINE_TOO_LONG`, without starting Kimi, rather than the misleading binary-unavailable error.
|
|
32
|
+
- Whether a smaller command still produces ENOENT, EACCES or another structured spawn code, especially through `.cmd` or shell wrappers.
|
|
33
|
+
- Sanitized error codes, numeric size fields, test output and an artificial reproducer—not credentials, raw config or a private repository diff.
|
|
34
|
+
|
|
35
|
+
Do not bypass the managed hook to make a reproduction pass. Native model calls, if a contributor chooses to exercise them, use that contributor's existing configuration and quota; CI does not perform them.
|
|
36
|
+
|
|
37
|
+
## Still open
|
|
38
|
+
|
|
39
|
+
The print prompt is still passed in argv. Upstream 2.3.2's audited print entry point has no supported stdin prompt input; an unused stdin helper is not a transport contract. Large reviews above the limit still cannot run through this route. Do not close #14 based only on better error classification or the no-model Windows checks. A new prompt transport needs its own upstream source audit and exact-binary verification, without silently changing prompt/session lineage or hook enforcement.
|
package/package.json
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimi-plugin-cc",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
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": {
|
|
7
7
|
"node": ">=22.5.0"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"check": "bun run build && bun run check:surfaces && bunx tsc --noEmit && bun test && git diff --exit-code -- dist plugins/kimi-codex && UNTRACKED=\"$(git ls-files --others --exclude-standard -- dist plugins/kimi-codex)\" && { [ -z \"$UNTRACKED\" ] || { echo \"drift gate: untracked dist/codex files (stage them and re-run check):\"; echo \"$UNTRACKED\"; exit 1; }; }",
|
|
10
|
+
"check": "bun run build && bun run check:contracts && bun run check:surfaces && bunx tsc --noEmit && bun test && git diff --exit-code -- dist plugins/kimi-codex && UNTRACKED=\"$(git ls-files --others --exclude-standard -- dist plugins/kimi-codex)\" && { [ -z \"$UNTRACKED\" ] || { echo \"drift gate: untracked dist/codex files (stage them and re-run check):\"; echo \"$UNTRACKED\"; exit 1; }; }",
|
|
11
11
|
"check:surfaces": "bun run scripts/generate-surfaces.ts --check",
|
|
12
12
|
"generate:surfaces": "bun run scripts/generate-surfaces.ts",
|
|
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",
|
|
17
|
-
"check:package": "node scripts/check-package.mjs"
|
|
16
|
+
"smoke:real": "KIMI_PLUGIN_CC_SMOKE=1 bun test tests/runtime/real-binary-smoke.test.ts --path-ignore-patterns '**/worktrees/**'",
|
|
17
|
+
"check:package": "node scripts/check-package.mjs",
|
|
18
|
+
"check:contracts": "node scripts/check-contract-size.mjs"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"@types/bun": "^1.4.2",
|
|
@@ -60,6 +61,8 @@
|
|
|
60
61
|
"scripts/companion.sh",
|
|
61
62
|
"scripts/review-gate-hook.sh",
|
|
62
63
|
"docs/safety.md",
|
|
64
|
+
"docs/swarm-recovery.md",
|
|
65
|
+
"docs/windows-validation.md",
|
|
63
66
|
"docs/migration.md",
|
|
64
67
|
"docs/models.md",
|
|
65
68
|
"docs/session-visibility.md",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimi",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
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"
|
|
@@ -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;
|
|
@@ -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,
|