kimi-plugin-cc 2.4.1 → 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 +9 -0
- package/README.md +4 -4
- package/dist/cli-client.js +17 -7
- package/dist/commands/cancel.js +14 -3
- package/dist/commands/recover-swarm.js +135 -0
- package/dist/commands/status.js +1 -1
- package/dist/commands/swarm.js +50 -53
- package/dist/companion.js +5 -1
- package/dist/git.js +5 -3
- package/dist/help.js +3 -2
- package/dist/job-store.js +13 -0
- package/dist/jobs.js +3 -0
- package/dist/kimi-errors.js +27 -21
- package/dist/parsing.js +1 -0
- package/dist/prompt-size.js +18 -0
- package/dist/swarm-activation.js +21 -0
- package/dist/swarm-recovery.js +134 -0
- package/dist/version.js +1 -1
- package/docs/registry-releases.md +8 -1
- package/docs/safety.md +3 -1
- package/docs/swarm-recovery.md +33 -0
- package/docs/windows-validation.md +39 -0
- package/package.json +6 -3
- package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/kimi-codex/dist/cli-client.js +17 -7
- package/plugins/kimi-codex/dist/commands/cancel.js +14 -3
- 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 +50 -53
- 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/job-store.js +13 -0
- package/plugins/kimi-codex/dist/jobs.js +3 -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/prompt-size.js +18 -0
- package/plugins/kimi-codex/dist/swarm-activation.js +21 -0
- package/plugins/kimi-codex/dist/swarm-recovery.js +134 -0
- package/plugins/kimi-codex/dist/version.js +1 -1
package/dist/kimi-errors.js
CHANGED
|
@@ -44,6 +44,11 @@ export function classifyManagedCommandFailure(error, commandType, jobId, options
|
|
|
44
44
|
const details = { availability: classification.kind };
|
|
45
45
|
if (error instanceof RuntimeError) {
|
|
46
46
|
details.cause_code = error.code;
|
|
47
|
+
for (const key of ["spawn_code", "spawn_errno", "spawn_message"]) {
|
|
48
|
+
if (error.details[key] !== undefined)
|
|
49
|
+
details[key] = key === "spawn_message" && typeof error.details[key] === "string"
|
|
50
|
+
? truncateCauseMessageForDetails(error.details[key]) : error.details[key];
|
|
51
|
+
}
|
|
47
52
|
if (error.code === "RESPONSE_TIMEOUT") {
|
|
48
53
|
for (const key of ["budget_ms", "record_count", "partial_output_chars", "aborted", "settled_post_abort", "hook_escapes"]) {
|
|
49
54
|
if (error.details[key] !== undefined)
|
|
@@ -129,28 +134,29 @@ function classifyKimiAvailability(error) {
|
|
|
129
134
|
authProbe: "failed",
|
|
130
135
|
};
|
|
131
136
|
}
|
|
132
|
-
if (error instanceof RuntimeError && error.code === "CLI_SPAWN_FAILED") {
|
|
133
|
-
return {
|
|
134
|
-
kind: "binary_unavailable",
|
|
135
|
-
summary: "the Kimi CLI is missing from PATH or not executable in this environment.",
|
|
136
|
-
nextStep: "Run `/kimi:setup` to verify the install, then expose `kimi` on PATH and retry.",
|
|
137
|
-
runtimeProbe: "failed",
|
|
138
|
-
authProbe: "failed",
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
// v1.0 cli-client surfaces async ENOENT (Bun) as CLI_PROCESS_ERROR with
|
|
142
|
-
// "spawn ... ENOENT" in the message. Map that to binary_unavailable so
|
|
143
|
-
// the failure message points users at /kimi:setup.
|
|
144
137
|
if (error instanceof RuntimeError &&
|
|
145
|
-
error.code === "CLI_PROCESS_ERROR"
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
138
|
+
(error.code === "CLI_SPAWN_FAILED" || error.code === "CLI_PROCESS_ERROR")) {
|
|
139
|
+
const spawnCode = error.details.spawn_code;
|
|
140
|
+
const binaryUnavailable = typeof spawnCode === "string" && ["ENOENT", "EACCES", "ENOEXEC"].includes(spawnCode);
|
|
141
|
+
if (typeof spawnCode === "string" && !binaryUnavailable) {
|
|
142
|
+
return {
|
|
143
|
+
kind: "startup_failed",
|
|
144
|
+
summary: "the Kimi subprocess could not be started or used.",
|
|
145
|
+
nextStep: "Check the underlying process error; shorten the prompt or environment if an argument-size limit was exceeded, then retry.",
|
|
146
|
+
runtimeProbe: "failed",
|
|
147
|
+
authProbe: "failed",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// Keep opaque synchronous failures and legacy async ENOENT diagnostics compatible.
|
|
151
|
+
if (binaryUnavailable || error.code === "CLI_SPAWN_FAILED" || /\bENOENT\b/.test(message)) {
|
|
152
|
+
return {
|
|
153
|
+
kind: "binary_unavailable",
|
|
154
|
+
summary: "the Kimi CLI is missing from PATH or not executable in this environment.",
|
|
155
|
+
nextStep: "Run `/kimi:setup` to verify the install, then expose `kimi` on PATH and retry.",
|
|
156
|
+
runtimeProbe: "failed",
|
|
157
|
+
authProbe: "failed",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
154
160
|
}
|
|
155
161
|
// v1.0 cli-client surfaces non-ENOENT process errors and non-zero
|
|
156
162
|
// exits as CLI_PROCESS_ERROR / CLI_NONZERO_EXIT. Treat both as
|
package/dist/parsing.js
CHANGED
|
@@ -18,6 +18,7 @@ export const RESCUE_SUPPORTED_FLAGS = "-m/--model <name>, --thinking <off|low|me
|
|
|
18
18
|
export const REVIEW_SUPPORTED_FLAGS = "--base <ref>, -m/--model <name>, --thinking <off|low|medium|high>, --budget <30m|1h|90s>";
|
|
19
19
|
export const JOB_LOOKUP_SUPPORTED_FLAGS = "--type <review|challenge|rescue|review_gate|ask>, --json";
|
|
20
20
|
export const SETUP_SUPPORTED_FLAGS = "--check, --uninstall, --all (with --uninstall), --enable-review-gate, --disable-review-gate, --models [--json] (without other setup flags)";
|
|
21
|
+
export const RECOVER_SWARM_SUPPORTED_FLAGS = "--apply";
|
|
21
22
|
export const REPAIR_SESSIONS_SUPPORTED_FLAGS = "--apply, --all";
|
|
22
23
|
export const REPLAY_SUPPORTED_FLAGS = "none (one job-id required)";
|
|
23
24
|
/** Validate syntax only; upstream per-model supportEfforts remains authoritative. */
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RuntimeError } from "./errors.js";
|
|
2
|
+
// Node/libuv's default CreateProcess argument quoting (UTF-16 code units).
|
|
3
|
+
export function windowsCommandLineLength(command, args) {
|
|
4
|
+
const quotedLength = (value) => {
|
|
5
|
+
if (value.length > 0 && !/[\s"]/u.test(value))
|
|
6
|
+
return value.length;
|
|
7
|
+
return 2 + value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1').length;
|
|
8
|
+
};
|
|
9
|
+
return [command, ...args].reduce((size, value) => size + quotedLength(value) + 1, 0);
|
|
10
|
+
}
|
|
11
|
+
export function assertPromptFitsCommandLine(command, args, platform = process.platform) {
|
|
12
|
+
if (platform !== "win32")
|
|
13
|
+
return;
|
|
14
|
+
const size = windowsCommandLineLength(command, args);
|
|
15
|
+
if (size <= 32_767)
|
|
16
|
+
return;
|
|
17
|
+
throw new RuntimeError("CLI_COMMAND_LINE_TOO_LONG", "The Kimi prompt exceeds the Windows command-line limit; the CLI was not started. Narrow the review or split it into smaller scopes. A short ask prompt can reference a saved diff inside the workspace. Reinstalling Kimi or running setup will not fix this. The current print transport does not support stdin prompts.", "cli-client.pre-spawn", { details: { command_line_utf16_units: size, limit_utf16_units: 32_767, platform } });
|
|
18
|
+
}
|
package/dist/swarm-activation.js
CHANGED
|
@@ -39,6 +39,7 @@ export async function preactivateSwarmSession(options) {
|
|
|
39
39
|
await verifyMeta(web, prepared.expectedVersion, options.signal, failureCode);
|
|
40
40
|
const model = await resolveProfileModel(web, options, failureCode);
|
|
41
41
|
identity = await createSession(web, options, failureCode);
|
|
42
|
+
options.onRecoveryEvent?.({ kind: "session", ...identity });
|
|
42
43
|
profileMutationAttempted = true;
|
|
43
44
|
await postSwarmProfile(web, identity, true, model, options.signal, failureCode);
|
|
44
45
|
await verifySwarmStatus(web, identity, true, model, options.signal, failureCode);
|
|
@@ -88,6 +89,7 @@ export async function deactivateSwarmSession(options) {
|
|
|
88
89
|
await verifySwarmStatus(web, options, false, undefined, options.signal, failureCode);
|
|
89
90
|
await shutdownWeb(web, options.signal, failureCode);
|
|
90
91
|
await requireSwarmJournal(prepared.kimiHome, options, "exit", options.signal, failureCode);
|
|
92
|
+
options.onRecoveryEvent?.({ kind: "disabled" });
|
|
91
93
|
throwIfAborted(options.signal, failureCode, "swarm-deactivation.complete");
|
|
92
94
|
}
|
|
93
95
|
catch (error) {
|
|
@@ -144,6 +146,7 @@ async function startWebChild(options, env, failureCode) {
|
|
|
144
146
|
"error",
|
|
145
147
|
];
|
|
146
148
|
let child;
|
|
149
|
+
options.onRecoveryEvent?.({ kind: "web-spawn" });
|
|
147
150
|
try {
|
|
148
151
|
child = spawn(options.command, args, {
|
|
149
152
|
cwd: options.cwd,
|
|
@@ -153,6 +156,7 @@ async function startWebChild(options, env, failureCode) {
|
|
|
153
156
|
});
|
|
154
157
|
}
|
|
155
158
|
catch {
|
|
159
|
+
options.onRecoveryEvent?.({ kind: "web-exit" });
|
|
156
160
|
throw failure(failureCode, "swarm-activation.spawn", "Failed to spawn the Kimi web lifecycle process.");
|
|
157
161
|
}
|
|
158
162
|
let exited = false;
|
|
@@ -184,6 +188,7 @@ async function startWebChild(options, env, failureCode) {
|
|
|
184
188
|
child,
|
|
185
189
|
closePromise,
|
|
186
190
|
descendants: [],
|
|
191
|
+
onRecoveryEvent: options.onRecoveryEvent,
|
|
187
192
|
trackingStopped: false,
|
|
188
193
|
lastSnapshotComplete: false,
|
|
189
194
|
snapshotIncomplete: false,
|
|
@@ -198,6 +203,8 @@ async function startWebChild(options, env, failureCode) {
|
|
|
198
203
|
try {
|
|
199
204
|
const ready = await waitForReadyLine(owned, options.signal, failureCode);
|
|
200
205
|
await refreshOwnedDescendants(owned);
|
|
206
|
+
if (owned.recoveryFailed)
|
|
207
|
+
throw failure(failureCode, "swarm-activation.recovery", "Cannot persist web ownership; refusing session mutation.");
|
|
201
208
|
return Object.assign(owned, ready);
|
|
202
209
|
}
|
|
203
210
|
catch (error) {
|
|
@@ -610,12 +617,26 @@ async function refreshOwnedDescendants(owned) {
|
|
|
610
617
|
owned.snapshotIncomplete = true;
|
|
611
618
|
owned.lastSnapshotComplete = false;
|
|
612
619
|
}
|
|
620
|
+
try {
|
|
621
|
+
owned.onRecoveryEvent?.({ kind: "web-processes", identities: [...(owned.rootIdentity ? [owned.rootIdentity] : []), ...owned.descendants], complete: owned.lastSnapshotComplete });
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
owned.recoveryFailed = true;
|
|
625
|
+
}
|
|
613
626
|
})().finally(() => {
|
|
614
627
|
owned.refreshPromise = undefined;
|
|
615
628
|
});
|
|
616
629
|
return await owned.refreshPromise;
|
|
617
630
|
}
|
|
618
631
|
async function teardownOwnedChild(owned) {
|
|
632
|
+
const quiescent = await teardownOwnedChildInner(owned);
|
|
633
|
+
if (quiescent && !owned.recoveryFailed) {
|
|
634
|
+
owned.onRecoveryEvent?.({ kind: "web-exit" });
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
async function teardownOwnedChildInner(owned) {
|
|
619
640
|
await stopDescendantTracking(owned);
|
|
620
641
|
const childPid = owned.child.pid;
|
|
621
642
|
if (childPid === undefined)
|
|
@@ -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
|
@@ -6,7 +6,14 @@ remain available. The package has no install scripts and no separate CLI.
|
|
|
6
6
|
|
|
7
7
|
## Publishing status
|
|
8
8
|
|
|
9
|
-
Plugin **2.4.
|
|
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.
|
|
10
17
|
|
|
11
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.
|
|
12
19
|
The previous verified publication was **2.3.1**:
|
package/docs/safety.md
CHANGED
|
@@ -207,6 +207,8 @@ If another installed host still points to an older hook, that hook may veto thes
|
|
|
207
207
|
|
|
208
208
|
## Swarm activation on CLI 2.3.2
|
|
209
209
|
|
|
210
|
+
For interrupted runs, see [durable recovery and its explicit limits](swarm-recovery.md).
|
|
211
|
+
|
|
210
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.
|
|
211
213
|
|
|
212
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:
|
|
@@ -236,7 +238,7 @@ Older certified versions keep the direct `kimi -p` path. The web processes are n
|
|
|
236
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.
|
|
237
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.
|
|
238
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.
|
|
239
|
-
- **The patch is captured on every terminal path, before cleanup.** Success, cancel, budget-expiry, and failure all capture the (possibly partial) patch BEFORE `git worktree remove --force` discards the tree, so a cancelled run never silently loses the work the subagents already did. If capture itself fails, the job fails instead of completing and the worktree is retained with its path in the error details for recovery. That guarantee covers the plugin's own terminal paths, including `companion.sh cancel`. It does **not** cover a harness interrupt: Esc/TaskStop delivers SIGTERM then SIGKILL after ~1.35s (measured, process-group-wide), which is shorter than the 1500ms child escalation plus quiescence plus patch capture — so a hard interrupt can kill the companion mid-teardown and lose the patch. `companion.sh cancel` signals from a separate process not racing that deadline and is the work-preserving stop.
|
|
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.
|
|
240
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`.
|
|
241
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.
|
|
242
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.
|
|
@@ -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
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"
|
|
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,6 +69,7 @@ export async function runCliPrompt(opts) {
|
|
|
60
69
|
});
|
|
61
70
|
}
|
|
62
71
|
const args = buildArgs(opts);
|
|
72
|
+
assertPromptFitsCommandLine(opts.command, args);
|
|
63
73
|
const env = buildKimiCliEnvironment(opts);
|
|
64
74
|
if (opts.logPath !== undefined) {
|
|
65
75
|
await mkdir(path.dirname(opts.logPath), { recursive: true }).catch(() => {
|
|
@@ -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
|
}
|
|
@@ -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);
|