kimi-plugin-cc 2.3.1 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +5 -3
  3. package/.mcp.json +13 -0
  4. package/CHANGELOG.md +19 -0
  5. package/README.md +67 -7
  6. package/dist/cli-client.js +13 -3
  7. package/dist/commands/ask.js +4 -2
  8. package/dist/commands/cancel.js +43 -4
  9. package/dist/commands/cli-helpers.js +5 -1
  10. package/dist/commands/pursue.js +3 -2
  11. package/dist/commands/rescue.js +3 -2
  12. package/dist/commands/review-gate.js +2 -4
  13. package/dist/commands/review.js +4 -2
  14. package/dist/commands/swarm.js +195 -37
  15. package/dist/companion.js +13 -40
  16. package/dist/help.js +2 -0
  17. package/dist/hooks/config-safety.js +4 -3
  18. package/dist/job-id.js +12 -0
  19. package/dist/job-store.js +59 -0
  20. package/dist/jobs.js +9 -1
  21. package/dist/kimi-engine.js +1 -0
  22. package/dist/mcp/binding.js +58 -0
  23. package/dist/mcp/job-control.js +345 -0
  24. package/dist/mcp/launcher.js +257 -0
  25. package/dist/mcp/operations.js +301 -0
  26. package/dist/mcp/protocol.js +275 -0
  27. package/dist/mcp/server.js +8 -0
  28. package/dist/mcp/stdio.js +78 -0
  29. package/dist/mcp/supervised-child.js +15 -0
  30. package/dist/mcp/tool-result.js +7 -0
  31. package/dist/mcp/workspace.js +34 -0
  32. package/dist/model-command.js +32 -0
  33. package/dist/process-tree.js +48 -17
  34. package/dist/swarm-activation.js +812 -0
  35. package/dist/version.js +1 -1
  36. package/docs/migration.md +12 -2
  37. package/docs/registry-releases.md +11 -9
  38. package/docs/safety.md +31 -5
  39. package/package.json +8 -4
  40. package/plugins/kimi-codex/.codex-plugin/plugin.json +7 -4
  41. package/plugins/kimi-codex/.mcp.json +28 -0
  42. package/plugins/kimi-codex/dist/cli-client.js +13 -3
  43. package/plugins/kimi-codex/dist/commands/ask.js +4 -2
  44. package/plugins/kimi-codex/dist/commands/cancel.js +43 -4
  45. package/plugins/kimi-codex/dist/commands/cli-helpers.js +5 -1
  46. package/plugins/kimi-codex/dist/commands/pursue.js +3 -2
  47. package/plugins/kimi-codex/dist/commands/rescue.js +3 -2
  48. package/plugins/kimi-codex/dist/commands/review-gate.js +2 -4
  49. package/plugins/kimi-codex/dist/commands/review.js +4 -2
  50. package/plugins/kimi-codex/dist/commands/swarm.js +195 -37
  51. package/plugins/kimi-codex/dist/companion.js +13 -40
  52. package/plugins/kimi-codex/dist/help.js +2 -0
  53. package/plugins/kimi-codex/dist/hooks/config-safety.js +4 -3
  54. package/plugins/kimi-codex/dist/job-id.js +12 -0
  55. package/plugins/kimi-codex/dist/job-store.js +59 -0
  56. package/plugins/kimi-codex/dist/jobs.js +9 -1
  57. package/plugins/kimi-codex/dist/kimi-engine.js +1 -0
  58. package/plugins/kimi-codex/dist/mcp/binding.js +58 -0
  59. package/plugins/kimi-codex/dist/mcp/job-control.js +345 -0
  60. package/plugins/kimi-codex/dist/mcp/launcher.js +257 -0
  61. package/plugins/kimi-codex/dist/mcp/operations.js +301 -0
  62. package/plugins/kimi-codex/dist/mcp/protocol.js +275 -0
  63. package/plugins/kimi-codex/dist/mcp/server.js +8 -0
  64. package/plugins/kimi-codex/dist/mcp/stdio.js +78 -0
  65. package/plugins/kimi-codex/dist/mcp/supervised-child.js +15 -0
  66. package/plugins/kimi-codex/dist/mcp/tool-result.js +7 -0
  67. package/plugins/kimi-codex/dist/mcp/workspace.js +34 -0
  68. package/plugins/kimi-codex/dist/model-command.js +32 -0
  69. package/plugins/kimi-codex/dist/process-tree.js +48 -17
  70. package/plugins/kimi-codex/dist/swarm-activation.js +812 -0
  71. package/plugins/kimi-codex/dist/version.js +1 -1
@@ -0,0 +1,345 @@
1
+ import { formatError, RuntimeError } from "../errors.js";
2
+ import { markJobFailed, STALE_SWEEP_GRACE_MS } from "../jobs.js";
3
+ import { JobStore, parseHookEscapes, renderBudgetSource, } from "../job-store.js";
4
+ import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
5
+ import { readProcessIdentity, revalidateProcessIdentities, revalidateProcessIdentity, } from "../process-tree.js";
6
+ import { readArtifact, renderTerminalJobArtifact } from "../render.js";
7
+ import { runCancel } from "../commands/cancel.js";
8
+ import { assertMcpJobBinding, mcpCompanionIdentity, mcpKimiIdentity } from "./binding.js";
9
+ import { readJobControlArguments } from "./operations.js";
10
+ import { textResult } from "./tool-result.js";
11
+ import { resolveMcpWorkspace } from "./workspace.js";
12
+ export async function getMcpJobStatus(env, input) {
13
+ const { task, job } = await loadMcpJob(env, input);
14
+ const metadata = mcpJobMetadata(task, job);
15
+ return textResult(`${JSON.stringify(metadata, null, 2)}\n`, metadata);
16
+ }
17
+ export async function getMcpJobResult(env, input) {
18
+ const { task, job } = await loadMcpJob(env, input);
19
+ if (!job) {
20
+ if (task.launch_error)
21
+ return launchFailureResult(task);
22
+ throw new RuntimeError("JOB_NOT_TERMINAL", `MCP job ${task.task_id} is still starting.`, "mcp.result");
23
+ }
24
+ if (job.status === "running") {
25
+ throw new RuntimeError("JOB_NOT_TERMINAL", `Job ${job.job_id} is still running.`, "mcp.result");
26
+ }
27
+ return terminalJobResult(task, job);
28
+ }
29
+ export async function cancelMcpJob(env, input) {
30
+ const { task, job } = await loadMcpJob(env, input);
31
+ if (!job) {
32
+ if (task.launch_error)
33
+ return launchFailureResult(task);
34
+ throw new RuntimeError("JOB_NOT_TERMINAL", `MCP job ${task.task_id} has not attached to a companion job.`, "mcp.cancel");
35
+ }
36
+ if (job.status !== "running") {
37
+ const metadata = mcpJobMetadata(task, job);
38
+ return textResult(`${JSON.stringify({ ...metadata, message: "cancel is a no-op for terminal jobs." }, null, 2)}\n`, metadata);
39
+ }
40
+ const output = await runCancel([task.task_id], {
41
+ cwd: task.workspace_cwd,
42
+ env,
43
+ stdout: process.stdout,
44
+ stderr: process.stderr,
45
+ mcpCompanionIdentity: mcpCompanionIdentity(task),
46
+ mcpKimiIdentity: mcpKimiIdentity(task, job),
47
+ });
48
+ const current = await loadMcpJob(env, input);
49
+ const metadata = mcpJobMetadata(current.task, current.job);
50
+ return textResult(output, { ...metadata, cancellation: JSON.parse(output) });
51
+ }
52
+ export async function getMcpTaskView(env, input) {
53
+ let boundTask;
54
+ try {
55
+ const { task, job } = await loadMcpJob(env, input, (found) => { boundTask = found; });
56
+ if (!job) {
57
+ if (task.launch_error) {
58
+ return taskView(task, null, "completed", launchFailureResult(task), task.launch_error);
59
+ }
60
+ return taskView(task, null, "working", undefined, "Starting the supervised companion.");
61
+ }
62
+ switch (job.status) {
63
+ case "running":
64
+ return taskView(task, job, "working", undefined, `${task.operation_kind}: ${job.phase ?? "running"}`);
65
+ case "completed":
66
+ return taskView(task, job, "completed", await terminalJobResult(task, job), job.summary);
67
+ case "failed":
68
+ return taskView(task, job, "completed", await terminalJobResult(task, job), job.summary);
69
+ case "cancelled":
70
+ return taskView(task, job, "cancelled", undefined, job.summary);
71
+ }
72
+ }
73
+ catch (error) {
74
+ if (boundTask && error instanceof RuntimeError && error.code === "MCP_PROCESS_IDENTITY_UNAVAILABLE") {
75
+ return taskView(boundTask, null, "working", undefined, formatError(error));
76
+ }
77
+ if (boundTask && error instanceof RuntimeError && error.code !== "JOB_NOT_FOUND") {
78
+ return {
79
+ taskId: boundTask.task_id,
80
+ status: "failed",
81
+ createdAt: boundTask.created_at,
82
+ lastUpdatedAt: new Date().toISOString(),
83
+ ttlMs: null,
84
+ pollIntervalMs: 2_000,
85
+ error: {
86
+ code: -32_603,
87
+ message: formatError(error),
88
+ data: { job_id: boundTask.task_id, operation_kind: boundTask.operation_kind },
89
+ },
90
+ };
91
+ }
92
+ throw error;
93
+ }
94
+ }
95
+ async function loadMcpJob(env, input, observeTask) {
96
+ const args = readJobControlArguments(input);
97
+ const paths = resolvePluginPaths(env);
98
+ await ensurePluginPaths(paths);
99
+ const store = new JobStore(paths);
100
+ try {
101
+ let task = store.getMcpTask(args.jobId);
102
+ if (!task) {
103
+ throw new RuntimeError("JOB_NOT_FOUND", `MCP job ${args.jobId} was not found.`, "mcp.lookup");
104
+ }
105
+ const workspace = await resolveMcpWorkspace(env, args.workspace ?? task.workspace_cwd);
106
+ if (task.workspace_cwd !== workspace.cwd || task.repo_id !== workspace.repo.repoId) {
107
+ throw new RuntimeError("JOB_NOT_FOUND", `MCP job ${args.jobId} was not found in this workspace.`, "mcp.lookup");
108
+ }
109
+ observeTask?.(task);
110
+ let job = store.getJob(task.task_id);
111
+ if (!job) {
112
+ task = await reconcileUnattachedTask(store, task);
113
+ return { task, job: null };
114
+ }
115
+ await assertMcpJobBinding(task, job);
116
+ if (task.launch_state === "starting") {
117
+ let identity;
118
+ if (job.status === "running") {
119
+ const expected = mcpCompanionIdentity(task);
120
+ if (expected) {
121
+ const validation = await revalidateProcessIdentities([expected]);
122
+ if (!validation.complete) {
123
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_UNAVAILABLE", "The companion identity probe was inconclusive; the starting task was not attached or changed.", "mcp.reconcile");
124
+ }
125
+ if (validation.identities.length === 0) {
126
+ const taskUpdatedAt = Date.parse(task.updated_at);
127
+ const jobUpdatedAt = Date.parse(job.updated_at);
128
+ const stale = Number.isFinite(taskUpdatedAt) && Number.isFinite(jobUpdatedAt) &&
129
+ Date.now() - taskUpdatedAt >= STALE_SWEEP_GRACE_MS &&
130
+ Date.now() - jobUpdatedAt >= STALE_SWEEP_GRACE_MS;
131
+ if (!stale) {
132
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_MISMATCH", "The recorded companion process identity no longer matches; the starting task was not attached and no process was adopted.", "mcp.reconcile");
133
+ }
134
+ task = await reconcileStaleCompanion(store, paths, task, job);
135
+ job = store.getJob(task.task_id);
136
+ return { task, job };
137
+ }
138
+ identity = validation.identities[0];
139
+ }
140
+ else if (job.pid !== null) {
141
+ const updatedAt = Date.parse(task.updated_at);
142
+ if (!Number.isFinite(updatedAt) || Date.now() - updatedAt >= STALE_SWEEP_GRACE_MS) {
143
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_UNAVAILABLE", "The starting task has no reusable identity after the attach grace period; refusing to adopt a process by PID alone.", "mcp.reconcile");
144
+ }
145
+ identity = await readProcessIdentity(job.pid);
146
+ }
147
+ }
148
+ if (job.status !== "running" || identity) {
149
+ task = store.updateMcpTask(task.task_id, {
150
+ launch_state: "attached",
151
+ attached_at: new Date().toISOString(),
152
+ companion_pid: job.pid ?? task.companion_pid,
153
+ ...(identity ? { companion_identity: JSON.stringify(identity) } : {}),
154
+ });
155
+ }
156
+ }
157
+ else if (task.launch_error) {
158
+ throw new RuntimeError("MCP_JOB_BINDING_FAILED", task.launch_error, "mcp.binding");
159
+ }
160
+ if (job.status === "running") {
161
+ task = await refreshCompanionIdentity(store, task, job);
162
+ task = await captureKimiIdentity(store, task, job);
163
+ task = await reconcileStaleCompanion(store, paths, task, job);
164
+ job = store.getJob(task.task_id);
165
+ }
166
+ return { task, job };
167
+ }
168
+ finally {
169
+ store.close();
170
+ }
171
+ }
172
+ async function reconcileUnattachedTask(store, task) {
173
+ if (task.launch_state !== "starting")
174
+ return task;
175
+ const updatedAt = Date.parse(task.updated_at);
176
+ const fresh = Number.isFinite(updatedAt) && Date.now() - updatedAt < STALE_SWEEP_GRACE_MS;
177
+ if (task.companion_identity === null) {
178
+ if (fresh) {
179
+ if (task.companion_pid === null)
180
+ return task;
181
+ const current = await readProcessIdentity(task.companion_pid);
182
+ return current
183
+ ? store.updateMcpTask(task.task_id, { companion_identity: JSON.stringify(current) })
184
+ : task;
185
+ }
186
+ if (task.companion_pid !== null) {
187
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_UNAVAILABLE", "The unattached companion has no reusable identity; refusing to adopt a process by PID alone.", "mcp.reconcile");
188
+ }
189
+ return store.updateMcpTask(task.task_id, {
190
+ launch_state: "exited",
191
+ launch_error: `No companion process was recorded before creating job ${task.task_id}.`,
192
+ });
193
+ }
194
+ if (fresh)
195
+ return task;
196
+ const validation = await revalidateProcessIdentities([mcpCompanionIdentity(task)]);
197
+ if (!validation.complete) {
198
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_UNAVAILABLE", "The unattached companion identity probe was inconclusive; the task was not changed.", "mcp.reconcile");
199
+ }
200
+ if (validation.identities.length > 0)
201
+ return task;
202
+ return store.updateMcpTask(task.task_id, {
203
+ launch_state: "exited",
204
+ launch_error: `Companion pid ${task.companion_pid} disappeared before creating job ${task.task_id}.`,
205
+ });
206
+ }
207
+ async function refreshCompanionIdentity(store, task, job) {
208
+ if (task.companion_identity !== null || job.pid === null)
209
+ return task;
210
+ const updatedAt = Date.parse(task.updated_at);
211
+ if (!Number.isFinite(updatedAt) || Date.now() - updatedAt >= STALE_SWEEP_GRACE_MS) {
212
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_UNAVAILABLE", "The companion identity is missing after the attach grace period; refusing to adopt a process by PID alone.", "mcp.reconcile");
213
+ }
214
+ const identity = await readProcessIdentity(job.pid);
215
+ if (!identity)
216
+ return task;
217
+ return store.updateMcpTask(task.task_id, {
218
+ companion_pid: job.pid,
219
+ companion_identity: JSON.stringify(identity),
220
+ });
221
+ }
222
+ async function captureKimiIdentity(store, task, job) {
223
+ if (task.kimi_identity !== null)
224
+ return task;
225
+ const companion = mcpCompanionIdentity(task);
226
+ if (!companion || !(await revalidateProcessIdentity(companion)))
227
+ return task;
228
+ const identity = job.kimi_pid !== null
229
+ ? await readProcessIdentity(job.kimi_pid)
230
+ : undefined;
231
+ if (!identity)
232
+ return task;
233
+ return store.updateMcpTask(task.task_id, { kimi_identity: JSON.stringify(identity) });
234
+ }
235
+ async function reconcileStaleCompanion(store, paths, task, job) {
236
+ const updatedAt = Date.parse(job.updated_at);
237
+ if (!Number.isFinite(updatedAt) || Date.now() - updatedAt < STALE_SWEEP_GRACE_MS)
238
+ return task;
239
+ const companion = mcpCompanionIdentity(task);
240
+ if (!companion) {
241
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_UNAVAILABLE", "The stale companion has no reusable process identity; refusing PID-only reconciliation.", "mcp.reconcile");
242
+ }
243
+ const validation = await revalidateProcessIdentities([companion]);
244
+ if (!validation.complete) {
245
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_UNAVAILABLE", "The companion process identity probe was inconclusive; no process was signalled and the job was not changed.", "mcp.reconcile");
246
+ }
247
+ if (validation.identities.length > 0)
248
+ return task;
249
+ const orphanWarning = await terminateRecordedKimi(task, job);
250
+ await markJobFailed(store, paths, job, new RuntimeError("MCP_COMPANION_DISAPPEARED", `The supervised companion disappeared before settling the job.${orphanWarning ? ` ${orphanWarning}` : ""}`, "mcp.reconcile", orphanWarning ? { details: { orphan_warning: orphanWarning, kimi_pid: job.kimi_pid } } : undefined), "Supervised MCP companion disappeared before reporting a terminal state.", { phase: "failed" });
251
+ return store.getMcpTask(task.task_id);
252
+ }
253
+ async function terminateRecordedKimi(task, job) {
254
+ const identity = mcpKimiIdentity(task, job);
255
+ if (!identity) {
256
+ return job.kimi_pid === null
257
+ ? null
258
+ : `Kimi pid ${job.kimi_pid} was not signalled because no verified identity had been captured.`;
259
+ }
260
+ if (!(await revalidateProcessIdentity(identity)))
261
+ return null;
262
+ signalVerifiedIdentity(identity, "SIGTERM");
263
+ await sleep(1_500);
264
+ if (await revalidateProcessIdentity(identity))
265
+ signalVerifiedIdentity(identity, "SIGKILL");
266
+ return null;
267
+ }
268
+ function signalVerifiedIdentity(identity, signal) {
269
+ try {
270
+ process.kill(identity.processGroupId === identity.pid ? -identity.processGroupId : identity.pid, signal);
271
+ }
272
+ catch (error) {
273
+ const code = error.code;
274
+ if (code !== "ESRCH" && code !== "EPERM")
275
+ throw error;
276
+ }
277
+ }
278
+ async function terminalJobResult(task, job) {
279
+ const metadata = mcpJobMetadata(task, job);
280
+ const body = job.final_output_path
281
+ ? await readArtifact(job.final_output_path)
282
+ : `${renderTerminalJobArtifact(job)}\n`;
283
+ return textResult(body, {
284
+ ...metadata,
285
+ artifact_path: job.final_output_path,
286
+ partial_output: job.status === "failed" && Number(job.error?.details?.partial_output_chars ?? 0) > 0,
287
+ }, job.status !== "completed");
288
+ }
289
+ function launchFailureResult(task) {
290
+ const message = task.launch_error ?? "The companion failed before creating its job.";
291
+ return textResult(message, {
292
+ job_id: task.task_id,
293
+ operation_kind: task.operation_kind,
294
+ launch_state: task.launch_state,
295
+ error: { code: "MCP_CHILD_LAUNCH_FAILED", message },
296
+ }, true);
297
+ }
298
+ function mcpJobMetadata(task, job) {
299
+ if (!job) {
300
+ return {
301
+ job_id: task.task_id,
302
+ operation_kind: task.operation_kind,
303
+ workspace: task.workspace_cwd,
304
+ status: task.launch_error ? "failed" : "starting",
305
+ launch_state: task.launch_state,
306
+ created_at: task.created_at,
307
+ updated_at: task.updated_at,
308
+ error: task.launch_error,
309
+ };
310
+ }
311
+ return {
312
+ job_id: job.job_id,
313
+ operation_kind: job.operation_kind,
314
+ command_type: job.command_type,
315
+ workspace: task.workspace_cwd,
316
+ status: job.status,
317
+ phase: job.phase,
318
+ summary: job.summary,
319
+ created_at: job.created_at,
320
+ updated_at: job.updated_at,
321
+ budget_ms: job.budget_ms,
322
+ budget_source: renderBudgetSource(job),
323
+ model: job.model,
324
+ thinking_effort_requested: job.thinking_effort ?? null,
325
+ kimi_session_id: job.kimi_session_id,
326
+ kimi_session_id_source: job.kimi_session_id_source,
327
+ hook_escapes: parseHookEscapes(job.hook_escapes),
328
+ error: job.error,
329
+ };
330
+ }
331
+ function taskView(task, job, status, result, statusMessage) {
332
+ return {
333
+ taskId: task.task_id,
334
+ status,
335
+ ...(statusMessage ? { statusMessage } : {}),
336
+ createdAt: task.created_at,
337
+ lastUpdatedAt: job?.updated_at ?? task.updated_at,
338
+ ttlMs: null,
339
+ pollIntervalMs: 2_000,
340
+ ...(result ? { result: { resultType: "complete", ...result } } : {}),
341
+ };
342
+ }
343
+ function sleep(ms) {
344
+ return new Promise((resolve) => setTimeout(resolve, ms));
345
+ }
@@ -0,0 +1,257 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { access, open, stat } from "node:fs/promises";
4
+ import { constants as fsConstants } from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { RuntimeError } from "../errors.js";
8
+ import { preferStableNodePath } from "../hooks/install-paths.js";
9
+ import { JobStore } from "../job-store.js";
10
+ import { markJobFailed } from "../jobs.js";
11
+ import { resolveKimiHome } from "../kimi-home.js";
12
+ import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
13
+ import { readProcessIdentity, revalidateProcessIdentities, } from "../process-tree.js";
14
+ import { assertMcpJobBinding, mcpCompanionIdentity } from "./binding.js";
15
+ import { resolveMcpWorkspace } from "./workspace.js";
16
+ const compiled = import.meta.url.endsWith(".js");
17
+ const companionEntrypoint = fileURLToPath(new URL(compiled ? "../companion.js" : "../companion.ts", import.meta.url));
18
+ const companionProjectRoot = path.resolve(path.dirname(companionEntrypoint), "..");
19
+ export async function launchMcpOperation(invocation, env, options) {
20
+ const attachTimeoutMs = options?.attachTimeoutMs ?? 60_000;
21
+ const readIdentity = options?.identityReader ?? readProcessIdentity;
22
+ let preserveStartingTask = false;
23
+ const attachDeadline = Date.now() + attachTimeoutMs;
24
+ const paths = resolvePluginPaths(env);
25
+ await ensurePluginPaths(paths);
26
+ const workspace = await resolveMcpWorkspace(env, invocation.workspace);
27
+ const taskId = randomUUID();
28
+ const stdoutPath = path.join(paths.logsDir, `mcp-${taskId}.stdout.log`);
29
+ const stderrPath = path.join(paths.logsDir, `mcp-${taskId}.stderr.log`);
30
+ const store = new JobStore(paths);
31
+ let child;
32
+ let stdoutHandle;
33
+ let stderrHandle;
34
+ let exited;
35
+ let spawnError;
36
+ try {
37
+ const task = store.createMcpTask({
38
+ task_id: taskId,
39
+ operation_kind: invocation.operationKind,
40
+ repo_id: workspace.repo.repoId,
41
+ workspace_cwd: workspace.cwd,
42
+ stdout_path: stdoutPath,
43
+ stderr_path: stderrPath,
44
+ });
45
+ stdoutHandle = await open(stdoutPath, "wx", 0o600);
46
+ stderrHandle = await open(stderrPath, "wx", 0o600);
47
+ const nodeBinary = await resolveNodeBinary(env);
48
+ const args = compiled
49
+ ? [companionEntrypoint, "mcp-child", taskId, ...invocation.argv]
50
+ : ["--import", "tsx", companionEntrypoint, "mcp-child", taskId, ...invocation.argv];
51
+ const childEnv = sanitizedChildEnv(env, paths, workspace.cwd);
52
+ child = spawn(nodeBinary, args, {
53
+ cwd: companionProjectRoot,
54
+ detached: process.platform !== "win32",
55
+ stdio: ["ignore", stdoutHandle.fd, stderrHandle.fd],
56
+ env: childEnv,
57
+ });
58
+ child.once("error", (error) => { spawnError = error; });
59
+ child.once("close", (code, signal) => { exited = { code, signal }; });
60
+ if (child.pid !== undefined) {
61
+ const identity = await readIdentity(child.pid);
62
+ store.updateMcpTask(taskId, {
63
+ companion_pid: child.pid,
64
+ ...(identity ? { companion_identity: JSON.stringify(identity) } : {}),
65
+ });
66
+ }
67
+ await stdoutHandle.close();
68
+ await stderrHandle.close();
69
+ stdoutHandle = undefined;
70
+ stderrHandle = undefined;
71
+ for (;;) {
72
+ if (spawnError)
73
+ throw await markLaunchFailure(store, task, spawnError);
74
+ const job = store.getJob(taskId);
75
+ if (job && (job.status !== "running" || job.phase === "turn-running" || job.phase === "worker-running")) {
76
+ const currentTask = store.getMcpTask(taskId) ?? task;
77
+ await assertMcpJobBinding(currentTask, job);
78
+ let identity;
79
+ if (job.status === "running") {
80
+ const expected = mcpCompanionIdentity(currentTask);
81
+ if (expected) {
82
+ const validation = await revalidateProcessIdentities([expected], {
83
+ identityReader: options?.identityReader,
84
+ });
85
+ if (!validation.complete) {
86
+ preserveStartingTask = true;
87
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_UNAVAILABLE", "The attachment identity probe was inconclusive; the launch record was preserved.", "mcp.launch", { details: { job_id: taskId, operation_kind: invocation.operationKind } });
88
+ }
89
+ if (validation.identities.length === 0) {
90
+ preserveStartingTask = true;
91
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_MISMATCH", "The recorded companion process identity changed before attachment; no process was adopted and the launch record was preserved.", "mcp.launch", { details: { job_id: taskId, operation_kind: invocation.operationKind } });
92
+ }
93
+ identity = validation.identities[0];
94
+ }
95
+ else if (child.pid !== undefined) {
96
+ identity = await readIdentity(child.pid);
97
+ }
98
+ }
99
+ const attached = store.updateMcpTask(taskId, {
100
+ launch_state: "attached",
101
+ attached_at: new Date().toISOString(),
102
+ ...(identity ? { companion_identity: JSON.stringify(identity) } : {}),
103
+ });
104
+ child.unref();
105
+ return { task: attached, job };
106
+ }
107
+ if (exited) {
108
+ const diagnostic = await readTail(stderrPath);
109
+ const error = new RuntimeError("MCP_CHILD_LAUNCH_FAILED", `The companion exited before creating its job (code ${exited.code ?? "unknown"}${exited.signal ? `, signal ${exited.signal}` : ""}).${diagnostic ? `\n${diagnostic}` : ""}`, "mcp.launch", { details: { job_id: taskId, operation_kind: invocation.operationKind, exit_code: exited.code, signal: exited.signal } });
110
+ throw await markLaunchFailure(store, task, error, diagnostic);
111
+ }
112
+ if (Date.now() >= attachDeadline) {
113
+ const aborted = await abortMcpLaunch(store, paths, task, job, attachTimeoutMs, options?.identityReader);
114
+ if ("result" in aborted) {
115
+ child.unref();
116
+ return aborted.result;
117
+ }
118
+ preserveStartingTask = aborted.preserveStarting;
119
+ throw aborted.error;
120
+ }
121
+ await sleep(25);
122
+ }
123
+ }
124
+ catch (error) {
125
+ if (!preserveStartingTask && store.getMcpTask(taskId)?.launch_state === "starting") {
126
+ store.updateMcpTask(taskId, {
127
+ launch_state: "exited",
128
+ launch_error: (error instanceof Error ? error.message : String(error)).slice(-8_000),
129
+ });
130
+ }
131
+ child?.unref();
132
+ throw error;
133
+ }
134
+ finally {
135
+ store.close();
136
+ await stdoutHandle?.close().catch(() => undefined);
137
+ await stderrHandle?.close().catch(() => undefined);
138
+ }
139
+ }
140
+ async function abortMcpLaunch(store, paths, task, job, timeoutMs, identityReader) {
141
+ let signalDelivered = false;
142
+ let preserveStarting = true;
143
+ try {
144
+ const current = store.getMcpTask(task.task_id) ?? task;
145
+ const identity = mcpCompanionIdentity(current);
146
+ if (identity) {
147
+ const validation = await revalidateProcessIdentities([identity], { identityReader });
148
+ if (!validation.complete) {
149
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_UNAVAILABLE", "The companion identity probe was inconclusive; no signal was sent and the launch record was preserved.", "mcp.launch", { details: { job_id: task.task_id, operation_kind: task.operation_kind } });
150
+ }
151
+ if (validation.identities.length === 0) {
152
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_MISMATCH", "The recorded companion process identity no longer matches; no signal was sent and the launch record was preserved.", "mcp.launch", { details: { job_id: task.task_id, operation_kind: task.operation_kind } });
153
+ }
154
+ signalVerifiedIdentity(identity, "SIGTERM");
155
+ signalDelivered = true;
156
+ await sleep(1_500);
157
+ const remaining = await revalidateProcessIdentities([identity], { identityReader });
158
+ if (!remaining.complete) {
159
+ const error = new RuntimeError("MCP_PROCESS_IDENTITY_UNAVAILABLE", "The post-signal companion identity probe was inconclusive; the launch record was preserved.", "mcp.launch", { details: { job_id: task.task_id, operation_kind: task.operation_kind } });
160
+ if (job) {
161
+ await assertMcpJobBinding(current, job);
162
+ return { result: { task: current, job } };
163
+ }
164
+ return { error, preserveStarting };
165
+ }
166
+ if (remaining.identities.length > 0) {
167
+ signalVerifiedIdentity(identity, "SIGKILL");
168
+ }
169
+ preserveStarting = false;
170
+ }
171
+ const error = new RuntimeError("MCP_LAUNCH_TIMEOUT", `The companion did not become cancellation-ready within ${timeoutMs}ms.${signalDelivered ? " The companion was signalled." : " No verified process identity was available, so no signal was sent."}`, "mcp.launch", { details: { job_id: task.task_id, operation_kind: task.operation_kind, timeout_ms: timeoutMs, signal_delivered: signalDelivered } });
172
+ if (signalDelivered) {
173
+ if (job) {
174
+ await markJobFailed(store, paths, job, error, "MCP launch timed out before cancellation was ready.", { phase: "failed" });
175
+ }
176
+ return { error: await markLaunchFailure(store, current, error), preserveStarting };
177
+ }
178
+ if (job) {
179
+ await assertMcpJobBinding(current, job);
180
+ return { result: { task: current, job } };
181
+ }
182
+ return { error, preserveStarting };
183
+ }
184
+ catch (error) {
185
+ return { error: error instanceof Error ? error : new Error(String(error)), preserveStarting };
186
+ }
187
+ }
188
+ function signalVerifiedIdentity(identity, signal) {
189
+ try {
190
+ process.kill(identity.processGroupId === identity.pid ? -identity.processGroupId : identity.pid, signal);
191
+ }
192
+ catch (error) {
193
+ const code = error.code;
194
+ if (code !== "ESRCH")
195
+ throw error;
196
+ }
197
+ }
198
+ async function resolveNodeBinary(env) {
199
+ const override = env.KIMI_PLUGIN_CC_NODE_BIN;
200
+ const binary = override ? override : preferStableNodePath(process.execPath);
201
+ if (binary.includes("/") || binary.includes("\\")) {
202
+ try {
203
+ await access(binary, fsConstants.X_OK);
204
+ }
205
+ catch (error) {
206
+ throw new RuntimeError("MCP_NODE_BIN_INVALID", `Configured Node binary is not executable: ${binary}`, "mcp.launch", {
207
+ cause: error instanceof Error ? error : undefined,
208
+ });
209
+ }
210
+ }
211
+ return binary;
212
+ }
213
+ function sanitizedChildEnv(env, paths, workspace) {
214
+ return {
215
+ ...env,
216
+ KIMI_PLUGIN_CC_WORKSPACE_CWD: workspace,
217
+ KIMI_PLUGIN_CC_MCP_WORKSPACE: workspace,
218
+ KIMI_PLUGIN_CC_DATA: path.resolve(paths.claudePluginData),
219
+ KIMI_CODE_HOME: resolveKimiHome(env, workspace),
220
+ KIMI_PLUGIN_CC_WORKSPACE_ROOT: undefined,
221
+ KIMI_PLUGIN_CC_CMD: undefined,
222
+ KIMI_PLUGIN_CC_OPERATION: undefined,
223
+ KIMI_PLUGIN_CC_SKIP_HOOK_CHECK: undefined,
224
+ KIMI_PLUGIN_CC_SKIP_VERSION_PROBE: undefined,
225
+ KIMI_CODE_TRUST_WORKSPACE: undefined,
226
+ KIMI_CODE_EXPERIMENTAL_FLAG: undefined,
227
+ };
228
+ }
229
+ async function markLaunchFailure(store, task, error, diagnostic) {
230
+ const failure = error instanceof Error ? error : new Error(String(error));
231
+ store.updateMcpTask(task.task_id, {
232
+ launch_state: "exited",
233
+ launch_error: (diagnostic || failure.message).slice(-8_000),
234
+ });
235
+ return failure;
236
+ }
237
+ async function readTail(filePath, maximum = 8_000) {
238
+ try {
239
+ const info = await stat(filePath);
240
+ const handle = await open(filePath, "r");
241
+ try {
242
+ const length = Math.min(maximum, info.size);
243
+ const buffer = Buffer.alloc(length);
244
+ await handle.read(buffer, 0, length, Math.max(0, info.size - length));
245
+ return buffer.toString("utf8").trim();
246
+ }
247
+ finally {
248
+ await handle.close();
249
+ }
250
+ }
251
+ catch {
252
+ return "";
253
+ }
254
+ }
255
+ function sleep(ms) {
256
+ return new Promise((resolve) => setTimeout(resolve, ms));
257
+ }