kimi-plugin-cc 2.3.1 → 2.4.0

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 (61) 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 +10 -0
  5. package/README.md +57 -5
  6. package/dist/cli-client.js +11 -1
  7. package/dist/commands/ask.js +4 -2
  8. package/dist/commands/cancel.js +43 -4
  9. package/dist/commands/pursue.js +3 -2
  10. package/dist/commands/rescue.js +3 -2
  11. package/dist/commands/review-gate.js +2 -4
  12. package/dist/commands/review.js +4 -2
  13. package/dist/commands/swarm.js +4 -3
  14. package/dist/companion.js +13 -40
  15. package/dist/help.js +2 -0
  16. package/dist/job-id.js +12 -0
  17. package/dist/job-store.js +59 -0
  18. package/dist/jobs.js +9 -1
  19. package/dist/mcp/binding.js +58 -0
  20. package/dist/mcp/job-control.js +345 -0
  21. package/dist/mcp/launcher.js +257 -0
  22. package/dist/mcp/operations.js +301 -0
  23. package/dist/mcp/protocol.js +275 -0
  24. package/dist/mcp/server.js +8 -0
  25. package/dist/mcp/stdio.js +78 -0
  26. package/dist/mcp/supervised-child.js +15 -0
  27. package/dist/mcp/tool-result.js +7 -0
  28. package/dist/mcp/workspace.js +34 -0
  29. package/dist/model-command.js +32 -0
  30. package/dist/version.js +1 -1
  31. package/docs/migration.md +2 -2
  32. package/docs/registry-releases.md +6 -8
  33. package/docs/safety.md +16 -2
  34. package/package.json +7 -3
  35. package/plugins/kimi-codex/.codex-plugin/plugin.json +7 -4
  36. package/plugins/kimi-codex/.mcp.json +28 -0
  37. package/plugins/kimi-codex/dist/cli-client.js +11 -1
  38. package/plugins/kimi-codex/dist/commands/ask.js +4 -2
  39. package/plugins/kimi-codex/dist/commands/cancel.js +43 -4
  40. package/plugins/kimi-codex/dist/commands/pursue.js +3 -2
  41. package/plugins/kimi-codex/dist/commands/rescue.js +3 -2
  42. package/plugins/kimi-codex/dist/commands/review-gate.js +2 -4
  43. package/plugins/kimi-codex/dist/commands/review.js +4 -2
  44. package/plugins/kimi-codex/dist/commands/swarm.js +4 -3
  45. package/plugins/kimi-codex/dist/companion.js +13 -40
  46. package/plugins/kimi-codex/dist/help.js +2 -0
  47. package/plugins/kimi-codex/dist/job-id.js +12 -0
  48. package/plugins/kimi-codex/dist/job-store.js +59 -0
  49. package/plugins/kimi-codex/dist/jobs.js +9 -1
  50. package/plugins/kimi-codex/dist/mcp/binding.js +58 -0
  51. package/plugins/kimi-codex/dist/mcp/job-control.js +345 -0
  52. package/plugins/kimi-codex/dist/mcp/launcher.js +257 -0
  53. package/plugins/kimi-codex/dist/mcp/operations.js +301 -0
  54. package/plugins/kimi-codex/dist/mcp/protocol.js +275 -0
  55. package/plugins/kimi-codex/dist/mcp/server.js +8 -0
  56. package/plugins/kimi-codex/dist/mcp/stdio.js +78 -0
  57. package/plugins/kimi-codex/dist/mcp/supervised-child.js +15 -0
  58. package/plugins/kimi-codex/dist/mcp/tool-result.js +7 -0
  59. package/plugins/kimi-codex/dist/mcp/workspace.js +34 -0
  60. package/plugins/kimi-codex/dist/model-command.js +32 -0
  61. package/plugins/kimi-codex/dist/version.js +1 -1
package/dist/job-store.js CHANGED
@@ -99,6 +99,25 @@ export class JobStore {
99
99
  }
100
100
  }
101
101
  this.db.exec(`
102
+ CREATE TABLE IF NOT EXISTS mcp_tasks (
103
+ task_id TEXT PRIMARY KEY,
104
+ operation_kind TEXT NOT NULL,
105
+ repo_id TEXT NOT NULL,
106
+ workspace_cwd TEXT NOT NULL,
107
+ companion_pid INTEGER,
108
+ companion_identity TEXT,
109
+ kimi_identity TEXT,
110
+ stdout_path TEXT NOT NULL,
111
+ stderr_path TEXT NOT NULL,
112
+ launch_state TEXT NOT NULL,
113
+ launch_error TEXT,
114
+ created_at TEXT NOT NULL,
115
+ attached_at TEXT,
116
+ updated_at TEXT NOT NULL
117
+ );
118
+
119
+ CREATE INDEX IF NOT EXISTS mcp_tasks_workspace_updated_idx
120
+ ON mcp_tasks (workspace_cwd, updated_at DESC);
102
121
 
103
122
  CREATE INDEX IF NOT EXISTS jobs_repo_updated_idx
104
123
  ON jobs (repo_id, updated_at DESC);
@@ -150,6 +169,9 @@ export class JobStore {
150
169
  -- 0.1.6 migration: rename adversarial_review to challenge
151
170
  UPDATE jobs SET command_type = 'challenge' WHERE command_type = 'adversarial_review';
152
171
  `);
172
+ if (!mcpTableHasColumn(this.db, "kimi_identity")) {
173
+ this.db.exec("ALTER TABLE mcp_tasks ADD COLUMN kimi_identity TEXT;");
174
+ }
153
175
  }
154
176
  catch (error) {
155
177
  // Migration failed — close the adapter so the OS handle is released
@@ -258,6 +280,40 @@ export class JobStore {
258
280
  `, repoId, sessionId);
259
281
  return row ? hydrateRow(row) : null;
260
282
  }
283
+ createMcpTask(input) {
284
+ const now = new Date().toISOString();
285
+ try {
286
+ this.db.run(`
287
+ INSERT INTO mcp_tasks (
288
+ task_id, operation_kind, repo_id, workspace_cwd, companion_pid, companion_identity, kimi_identity,
289
+ stdout_path, stderr_path, launch_state, launch_error, created_at, attached_at, updated_at
290
+ ) VALUES (
291
+ @task_id, @operation_kind, @repo_id, @workspace_cwd, NULL, NULL, NULL,
292
+ @stdout_path, @stderr_path, 'starting', NULL, @created_at, NULL, @updated_at
293
+ )
294
+ `, { ...input, created_at: now, updated_at: now });
295
+ }
296
+ catch (error) {
297
+ throw translateSqliteError(error);
298
+ }
299
+ return this.getMcpTask(input.task_id);
300
+ }
301
+ getMcpTask(taskId) {
302
+ return this.db.get("SELECT * FROM mcp_tasks WHERE task_id = ?", taskId) ?? null;
303
+ }
304
+ updateMcpTask(taskId, patch) {
305
+ const updates = ["updated_at = @updated_at"];
306
+ const params = { task_id: taskId, updated_at: new Date().toISOString() };
307
+ for (const key of ["companion_pid", "companion_identity", "kimi_identity", "launch_state", "launch_error", "attached_at"]) {
308
+ const value = patch[key];
309
+ if (value === undefined)
310
+ continue;
311
+ updates.push(`${key} = @${key}`);
312
+ params[key] = value;
313
+ }
314
+ this.db.run(`UPDATE mcp_tasks SET ${updates.join(", ")} WHERE task_id = @task_id`, params);
315
+ return this.getMcpTask(taskId);
316
+ }
261
317
  updateRunningJob(jobId, patch) {
262
318
  return this.updateWhere(jobId, patch, "status = 'running'");
263
319
  }
@@ -524,6 +580,9 @@ function tableHasColumn(db, columnName) {
524
580
  const columns = db.all(`PRAGMA table_info(jobs)`);
525
581
  return columns.some((column) => column.name === columnName);
526
582
  }
583
+ function mcpTableHasColumn(db, columnName) {
584
+ return db.all("PRAGMA table_info(mcp_tasks)").some((column) => column.name === columnName);
585
+ }
527
586
  function isSqliteConstraintError(error) {
528
587
  const message = formatError(error).toLowerCase();
529
588
  return message.includes("unique constraint") || message.includes("constraint failed");
package/dist/jobs.js CHANGED
@@ -30,6 +30,9 @@ export function normalizeJobError(error) {
30
30
  };
31
31
  }
32
32
  export async function markJobFailed(store, paths, job, error, summary = "Job failed.", options) {
33
+ const current = store.getJob(job.job_id) ?? job;
34
+ if (current.status !== "running")
35
+ return current;
33
36
  if (options?.partialOutput) {
34
37
  const label = job.command_type[0].toUpperCase() + job.command_type.slice(1);
35
38
  summary = `${label} timed out after ${formatBudgetDuration(options.partialOutput.budgetMs)}; partial output captured (${options.partialOutput.text.trim().length} chars).`;
@@ -55,6 +58,9 @@ export async function markJobFailed(store, paths, job, error, summary = "Job fai
55
58
  }) ?? artifactJob);
56
59
  }
57
60
  export async function markJobCancelled(store, paths, job, summary, error, options) {
61
+ const current = store.getJob(job.job_id) ?? job;
62
+ if (current.status !== "running")
63
+ return current;
58
64
  const normalized = error ? normalizeJobError(error) : null;
59
65
  const phase = options?.phase;
60
66
  const artifactJob = {
@@ -77,6 +83,8 @@ export async function markJobCancelled(store, paths, job, summary, error, option
77
83
  }
78
84
  export async function sweepStaleJobs(store, paths) {
79
85
  for (const job of store.listRunningJobsWithProcessHints()) {
86
+ if (store.getMcpTask(job.job_id))
87
+ continue;
80
88
  if (!isStaleEnoughToSweep(job)) {
81
89
  continue;
82
90
  }
@@ -109,7 +117,7 @@ export async function sweepStaleJobs(store, paths) {
109
117
  await markJobFailed(store, paths, job, new RuntimeError("FOREGROUND_PROCESS_DISAPPEARED", describeMissingForegroundProcess(job, companionMissing, kimiMissing), "jobs.sweep"), "Foreground Kimi job disappeared before reporting a terminal state.", job.command_type === "ask" || job.command_type === "rescue" ? { phase: "failed" } : undefined);
110
118
  }
111
119
  }
112
- const STALE_SWEEP_GRACE_MS = 15_000;
120
+ export const STALE_SWEEP_GRACE_MS = 15_000;
113
121
  function isStaleEnoughToSweep(job) {
114
122
  const updatedAt = Date.parse(job.updated_at);
115
123
  return Number.isFinite(updatedAt) && Date.now() - updatedAt >= STALE_SWEEP_GRACE_MS;
@@ -0,0 +1,58 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import { RuntimeError } from "../errors.js";
3
+ export async function assertMcpJobBinding(task, job) {
4
+ if (!job)
5
+ throw bindingError(task, "The supervised job row does not exist.");
6
+ if (job.job_id !== task.task_id)
7
+ throw bindingError(task, "Task and job IDs differ.");
8
+ if (job.operation_kind !== task.operation_kind)
9
+ throw bindingError(task, "Task and job operation kinds differ.");
10
+ if (job.repo_id !== task.repo_id)
11
+ throw bindingError(task, "Task and job repositories differ.");
12
+ if (job.background)
13
+ throw bindingError(task, "Supervised MCP jobs must retain foreground companion ownership.");
14
+ if (job.status === "running" && (task.companion_pid === null || job.pid !== task.companion_pid)) {
15
+ throw bindingError(task, "Task and job companion process IDs differ or are missing.");
16
+ }
17
+ let jobCwd;
18
+ try {
19
+ jobCwd = await realpath(job.cwd);
20
+ }
21
+ catch (error) {
22
+ throw bindingError(task, "The job workspace cannot be resolved.", error);
23
+ }
24
+ if (jobCwd !== task.workspace_cwd)
25
+ throw bindingError(task, "Task and job workspaces differ.");
26
+ return job;
27
+ }
28
+ export function mcpCompanionIdentity(task) {
29
+ return parseIdentity(task.companion_identity, task.companion_pid, task, "companion");
30
+ }
31
+ export function mcpKimiIdentity(task, job) {
32
+ return parseIdentity(task.kimi_identity, job.kimi_pid, task, "Kimi");
33
+ }
34
+ function parseIdentity(serialized, expectedPid, task, label) {
35
+ if (serialized === null)
36
+ return null;
37
+ try {
38
+ const value = JSON.parse(serialized);
39
+ if (!Number.isInteger(value.pid) || (value.pid ?? 0) <= 0 ||
40
+ !Number.isInteger(value.processGroupId) ||
41
+ typeof value.startToken !== "string" || value.startToken.length === 0 ||
42
+ typeof value.state !== "string") {
43
+ throw new Error("invalid identity shape");
44
+ }
45
+ if (expectedPid !== null && expectedPid !== value.pid)
46
+ throw new Error("identity and recorded PID differ");
47
+ return value;
48
+ }
49
+ catch (error) {
50
+ throw bindingError(task, `The ${label} process identity is invalid.`, error);
51
+ }
52
+ }
53
+ function bindingError(task, message, cause) {
54
+ return new RuntimeError("MCP_JOB_BINDING_FAILED", message, "mcp.binding", {
55
+ ...(cause instanceof Error ? { cause } : {}),
56
+ details: { job_id: task.task_id, operation_kind: task.operation_kind },
57
+ });
58
+ }
@@ -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
+ }