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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +5 -3
- package/.mcp.json +13 -0
- package/CHANGELOG.md +19 -0
- package/README.md +67 -7
- package/dist/cli-client.js +13 -3
- package/dist/commands/ask.js +4 -2
- package/dist/commands/cancel.js +43 -4
- package/dist/commands/cli-helpers.js +5 -1
- package/dist/commands/pursue.js +3 -2
- package/dist/commands/rescue.js +3 -2
- package/dist/commands/review-gate.js +2 -4
- package/dist/commands/review.js +4 -2
- package/dist/commands/swarm.js +195 -37
- package/dist/companion.js +13 -40
- package/dist/help.js +2 -0
- package/dist/hooks/config-safety.js +4 -3
- package/dist/job-id.js +12 -0
- package/dist/job-store.js +59 -0
- package/dist/jobs.js +9 -1
- package/dist/kimi-engine.js +1 -0
- package/dist/mcp/binding.js +58 -0
- package/dist/mcp/job-control.js +345 -0
- package/dist/mcp/launcher.js +257 -0
- package/dist/mcp/operations.js +301 -0
- package/dist/mcp/protocol.js +275 -0
- package/dist/mcp/server.js +8 -0
- package/dist/mcp/stdio.js +78 -0
- package/dist/mcp/supervised-child.js +15 -0
- package/dist/mcp/tool-result.js +7 -0
- package/dist/mcp/workspace.js +34 -0
- package/dist/model-command.js +32 -0
- package/dist/process-tree.js +48 -17
- package/dist/swarm-activation.js +812 -0
- package/dist/version.js +1 -1
- package/docs/migration.md +12 -2
- package/docs/registry-releases.md +11 -9
- package/docs/safety.md +31 -5
- package/package.json +8 -4
- package/plugins/kimi-codex/.codex-plugin/plugin.json +7 -4
- package/plugins/kimi-codex/.mcp.json +28 -0
- package/plugins/kimi-codex/dist/cli-client.js +13 -3
- package/plugins/kimi-codex/dist/commands/ask.js +4 -2
- package/plugins/kimi-codex/dist/commands/cancel.js +43 -4
- package/plugins/kimi-codex/dist/commands/cli-helpers.js +5 -1
- package/plugins/kimi-codex/dist/commands/pursue.js +3 -2
- package/plugins/kimi-codex/dist/commands/rescue.js +3 -2
- package/plugins/kimi-codex/dist/commands/review-gate.js +2 -4
- package/plugins/kimi-codex/dist/commands/review.js +4 -2
- package/plugins/kimi-codex/dist/commands/swarm.js +195 -37
- package/plugins/kimi-codex/dist/companion.js +13 -40
- package/plugins/kimi-codex/dist/help.js +2 -0
- package/plugins/kimi-codex/dist/hooks/config-safety.js +4 -3
- package/plugins/kimi-codex/dist/job-id.js +12 -0
- package/plugins/kimi-codex/dist/job-store.js +59 -0
- package/plugins/kimi-codex/dist/jobs.js +9 -1
- package/plugins/kimi-codex/dist/kimi-engine.js +1 -0
- package/plugins/kimi-codex/dist/mcp/binding.js +58 -0
- package/plugins/kimi-codex/dist/mcp/job-control.js +345 -0
- package/plugins/kimi-codex/dist/mcp/launcher.js +257 -0
- package/plugins/kimi-codex/dist/mcp/operations.js +301 -0
- package/plugins/kimi-codex/dist/mcp/protocol.js +275 -0
- package/plugins/kimi-codex/dist/mcp/server.js +8 -0
- package/plugins/kimi-codex/dist/mcp/stdio.js +78 -0
- package/plugins/kimi-codex/dist/mcp/supervised-child.js +15 -0
- package/plugins/kimi-codex/dist/mcp/tool-result.js +7 -0
- package/plugins/kimi-codex/dist/mcp/workspace.js +34 -0
- package/plugins/kimi-codex/dist/model-command.js +32 -0
- package/plugins/kimi-codex/dist/process-tree.js +48 -17
- package/plugins/kimi-codex/dist/swarm-activation.js +812 -0
- package/plugins/kimi-codex/dist/version.js +1 -1
package/dist/commands/swarm.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { allocateJobId } from "../job-id.js";
|
|
2
2
|
import { mkdir, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createCliCancellationHandlers } from "../cli-cancellation.js";
|
|
5
|
-
import { runCliPromptWithBudget } from "../cli-client.js";
|
|
5
|
+
import { ResponseTimeoutError, runCliPromptWithBudget } from "../cli-client.js";
|
|
6
|
+
import { deactivateSwarmSession, preactivateSwarmSession, requiresSwarmPreactivation, } from "../swarm-activation.js";
|
|
6
7
|
import { executionPlanFromPersisted, observedExecutionFields, persistedExecutionPlanFields, prepareKimiExecutionPlan, } from "../kimi-engine.js";
|
|
7
8
|
import { RuntimeError } from "../errors.js";
|
|
8
9
|
import { captureWorktreePatch, createEphemeralWorktree, hasBornHead, isWorkingTreeDirty, pruneWorktrees, removeWorktree, resolveRepoIdentity, } from "../git.js";
|
|
@@ -24,7 +25,8 @@ import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js
|
|
|
24
25
|
// only the wall-clock hard bound. Read/write modes reuse review/rescue lineage,
|
|
25
26
|
// and the mandatory PreToolUse hook enforces read-only policy or trusted-worktree
|
|
26
27
|
// write confinement on the coordinator and every subagent. Write mode requires
|
|
27
|
-
// kimi-code 0.18.0+.
|
|
28
|
+
// kimi-code 0.18.0+. Exact 2.3.2 additionally activates a same-job session through
|
|
29
|
+
// a bounded loopback web lifecycle, then resumes it through the normal print path.
|
|
28
30
|
const SWARM_SUMMARY_MAX = 120;
|
|
29
31
|
const SWARM_AGENT_PROFILE = "<swarm>";
|
|
30
32
|
const SWARM_WRITE_AGENT_PROFILE = "<swarm-write>";
|
|
@@ -87,7 +89,7 @@ export function resolveSwarmWriteMaxConcurrency(requested) {
|
|
|
87
89
|
* subagent, so slimming this text does not move the safety boundary. The optional
|
|
88
90
|
* `cap` is a SOFT subagent-count hint (the hook cannot enforce a count). The HARD
|
|
89
91
|
* concurrency ceiling is separate: --max-concurrency →
|
|
90
|
-
* KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY (see executeSwarmJob / cli-client
|
|
92
|
+
* KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY (see executeSwarmJob / cli-client buildKimiCliEnvironment).
|
|
91
93
|
* The --budget wall-clock ceiling is the always-on hard bound on the whole run.
|
|
92
94
|
*/
|
|
93
95
|
export function buildSwarmPrompt(objective, cap) {
|
|
@@ -180,7 +182,7 @@ async function runReadSwarm(parsed, objective, context) {
|
|
|
180
182
|
env: context.env,
|
|
181
183
|
});
|
|
182
184
|
const prompt = buildSwarmPrompt(objective, parsed.cap);
|
|
183
|
-
const jobId =
|
|
185
|
+
const jobId = allocateJobId(context);
|
|
184
186
|
const logPath = path.join(paths.logsDir, `swarm-${jobId}.jsonl`);
|
|
185
187
|
const job = store.createJob({
|
|
186
188
|
job_id: jobId,
|
|
@@ -257,7 +259,7 @@ async function runWriteSwarm(parsed, objective, context) {
|
|
|
257
259
|
context.stderr.write("[kimi-plugin-cc] /kimi:swarm --write bases its worktree on HEAD — your uncommitted changes are NOT visible to the swarm. Commit or stash them first if the swarm needs them.\n");
|
|
258
260
|
}
|
|
259
261
|
const prompt = buildSwarmWritePrompt(objective, parsed.cap);
|
|
260
|
-
const jobId =
|
|
262
|
+
const jobId = allocateJobId(context);
|
|
261
263
|
const logPath = path.join(paths.logsDir, `swarm-write-${jobId}.jsonl`);
|
|
262
264
|
// Namespace worktrees per repo so the cross-repo sweep below only ever
|
|
263
265
|
// touches THIS repo's subtree (the worktreesDir is a single global dir shared
|
|
@@ -343,7 +345,7 @@ async function runWriteSwarm(parsed, objective, context) {
|
|
|
343
345
|
}
|
|
344
346
|
finally {
|
|
345
347
|
if (preserveWorktree) {
|
|
346
|
-
context.stderr.write(`[kimi-plugin-cc] swarm-write patch capture failed; worktree retained at ${worktreePath}.\n`);
|
|
348
|
+
context.stderr.write(`[kimi-plugin-cc] swarm-write patch capture or session cleanup failed; worktree retained at ${worktreePath}.\n`);
|
|
347
349
|
}
|
|
348
350
|
else {
|
|
349
351
|
await removeWorktree(repoIdentity.repoRoot, worktreePath);
|
|
@@ -465,43 +467,163 @@ writeMode) {
|
|
|
465
467
|
}
|
|
466
468
|
}
|
|
467
469
|
const handlers = createCliCancellationHandlers();
|
|
470
|
+
const budgetDeadline = Date.now() + budgetMs;
|
|
471
|
+
let activationOptions;
|
|
472
|
+
let preactivated;
|
|
473
|
+
let deactivationRequired = false;
|
|
474
|
+
let cleanupAttempted = false;
|
|
475
|
+
let cleanupFailure;
|
|
476
|
+
let finalText;
|
|
477
|
+
const commandLabel = writeMode ? "swarm-write" : "swarm";
|
|
478
|
+
const recoveryCleanupTimeoutMs = 2_000;
|
|
479
|
+
const cleanupSignal = (timeoutMs) => AbortSignal.any([
|
|
480
|
+
handlers.signal,
|
|
481
|
+
AbortSignal.timeout(Math.max(1, timeoutMs)),
|
|
482
|
+
]);
|
|
483
|
+
const attemptDeactivation = async (identity, fallbackStage, timeoutMs = Math.max(1, budgetDeadline - Date.now())) => {
|
|
484
|
+
cleanupAttempted = true;
|
|
485
|
+
try {
|
|
486
|
+
await deactivateSwarmSession({
|
|
487
|
+
...activationOptions,
|
|
488
|
+
...identity,
|
|
489
|
+
signal: cleanupSignal(timeoutMs),
|
|
490
|
+
});
|
|
491
|
+
deactivationRequired = false;
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
return error instanceof RuntimeError
|
|
496
|
+
? error
|
|
497
|
+
: new RuntimeError("SWARM_SESSION_CLEANUP_FAILED", "Swarm session cleanup failed without a safe diagnostic payload.", fallbackStage);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
468
500
|
try {
|
|
469
501
|
const executionPlan = executionPlanFromPersisted(job);
|
|
502
|
+
activationOptions = {
|
|
503
|
+
cwd: writeMode?.spawnCwd ?? job.cwd,
|
|
504
|
+
env: context.env,
|
|
505
|
+
command: executionPlan.command,
|
|
506
|
+
prefixArgs: executionPlan.prefixArgs,
|
|
507
|
+
executionPlan,
|
|
508
|
+
commandLabel,
|
|
509
|
+
model: job.model ?? undefined,
|
|
510
|
+
thinkingEffort: job.thinking_effort ?? undefined,
|
|
511
|
+
swarmMaxConcurrency: maxConcurrency,
|
|
512
|
+
trustedWorkspaceRoot: writeMode?.workspaceRoot,
|
|
513
|
+
expectedVersion: executionPlan.kimiVersion ?? undefined,
|
|
514
|
+
};
|
|
515
|
+
if (requiresSwarmPreactivation(executionPlan.kimiVersion)) {
|
|
516
|
+
store.updateRunningJob(job.job_id, { phase: "turn-running" });
|
|
517
|
+
const activationController = new AbortController();
|
|
518
|
+
const onParentAbort = () => activationController.abort();
|
|
519
|
+
let activationTimedOut = false;
|
|
520
|
+
handlers.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
521
|
+
if (handlers.signal.aborted)
|
|
522
|
+
onParentAbort();
|
|
523
|
+
const activationTimer = setTimeout(() => {
|
|
524
|
+
activationTimedOut = true;
|
|
525
|
+
activationController.abort();
|
|
526
|
+
}, Math.max(1, budgetDeadline - Date.now()));
|
|
527
|
+
activationTimer.unref();
|
|
528
|
+
try {
|
|
529
|
+
preactivated = await preactivateSwarmSession({
|
|
530
|
+
...activationOptions,
|
|
531
|
+
signal: activationController.signal,
|
|
532
|
+
});
|
|
533
|
+
deactivationRequired = true;
|
|
534
|
+
store.updateRunningJob(job.job_id, {
|
|
535
|
+
kimi_session_id: preactivated.sessionId,
|
|
536
|
+
kimi_session_id_source: "preactivated",
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
const activationDidTimeOut = activationTimedOut;
|
|
541
|
+
clearTimeout(activationTimer);
|
|
542
|
+
handlers.signal.removeEventListener("abort", onParentAbort);
|
|
543
|
+
const details = error instanceof RuntimeError ? error.details : {};
|
|
544
|
+
const sessionId = typeof details.session_id === "string" ? details.session_id : undefined;
|
|
545
|
+
const workspaceId = typeof details.workspace_id === "string" ? details.workspace_id : undefined;
|
|
546
|
+
const hasSessionIdentity = sessionId !== undefined && workspaceId !== undefined;
|
|
547
|
+
if (error instanceof RuntimeError &&
|
|
548
|
+
error.code === "SWARM_SESSION_CLEANUP_FAILED" &&
|
|
549
|
+
details.process_cleanup_unverified === true) {
|
|
550
|
+
cleanupFailure = error;
|
|
551
|
+
}
|
|
552
|
+
if (hasSessionIdentity) {
|
|
553
|
+
store.updateRunningJob(job.job_id, {
|
|
554
|
+
kimi_session_id: sessionId,
|
|
555
|
+
kimi_session_id_source: "preactivated",
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
if (hasSessionIdentity && !handlers.cancelling) {
|
|
559
|
+
const sessionCleanupFailure = await attemptDeactivation({ sessionId, workspaceId }, "swarm.activation-cleanup", recoveryCleanupTimeoutMs);
|
|
560
|
+
if (sessionCleanupFailure)
|
|
561
|
+
cleanupFailure = sessionCleanupFailure;
|
|
562
|
+
}
|
|
563
|
+
else if (hasSessionIdentity && cleanupFailure === undefined) {
|
|
564
|
+
cleanupFailure = new RuntimeError("SWARM_SESSION_CLEANUP_FAILED", "Swarm session cleanup was not completed after activation was cancelled.", "swarm.activation-cleanup", { details: { cancelled: true, cleanup_unverified: true } });
|
|
565
|
+
}
|
|
566
|
+
if (activationDidTimeOut && !handlers.cancelling) {
|
|
567
|
+
throw new ResponseTimeoutError(budgetMs, "swarm.activation", commandLabel, undefined, error);
|
|
568
|
+
}
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
571
|
+
finally {
|
|
572
|
+
clearTimeout(activationTimer);
|
|
573
|
+
handlers.signal.removeEventListener("abort", onParentAbort);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const promptBudgetMs = budgetDeadline - Date.now();
|
|
577
|
+
if (promptBudgetMs <= 0)
|
|
578
|
+
throw new ResponseTimeoutError(budgetMs, "swarm.prompt", commandLabel);
|
|
470
579
|
store.updateRunningJob(job.job_id, { phase: "turn-running" });
|
|
471
580
|
const result = await runCliPromptWithBudget({
|
|
472
|
-
|
|
473
|
-
// worktree so coder subagents edit there (job.cwd stays the user's real
|
|
474
|
-
// cwd for lineage). This is the load-bearing isolation seam.
|
|
475
|
-
cwd: writeMode?.spawnCwd ?? job.cwd,
|
|
581
|
+
cwd: activationOptions.cwd,
|
|
476
582
|
env: context.env,
|
|
477
583
|
command: executionPlan.command,
|
|
478
584
|
prefixArgs: [...executionPlan.prefixArgs],
|
|
479
585
|
executionPlan,
|
|
480
586
|
prompt,
|
|
481
|
-
|
|
482
|
-
// PLUS rescue-grade write confinement to the trusted worktree root. The
|
|
483
|
-
// label drives the hook for the coordinator AND every spawned subagent.
|
|
484
|
-
commandLabel: writeMode ? "swarm-write" : "swarm",
|
|
485
|
-
// Always exported as KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY. kimi-code
|
|
486
|
-
// 0.18.0+ enforces this hard peak ceiling; older read-only binaries ignore
|
|
487
|
-
// it and retain only the wall-clock hard bound. Write mode requires
|
|
488
|
-
// 0.18.0+. Distinct from --cap, the soft total-count prompt hint.
|
|
587
|
+
commandLabel,
|
|
489
588
|
swarmMaxConcurrency: maxConcurrency,
|
|
490
|
-
// Trusted allowlist root for the swarm-write hook (forge-proof; ignored
|
|
491
|
-
// by other labels). Only set in write mode.
|
|
492
589
|
trustedWorkspaceRoot: writeMode?.workspaceRoot,
|
|
493
590
|
model: job.model ?? undefined,
|
|
494
591
|
thinkingEffort: job.thinking_effort ?? undefined,
|
|
592
|
+
resumeSessionId: preactivated?.sessionId,
|
|
495
593
|
logPath: job.stream_log_path,
|
|
594
|
+
onSpawn: (pid) => { store.updateRunningJob(job.job_id, { kimi_pid: pid }); },
|
|
496
595
|
signal: handlers.signal,
|
|
497
|
-
},
|
|
596
|
+
}, promptBudgetMs, "swarm.prompt");
|
|
498
597
|
store.updateRunningJob(job.job_id, observedExecutionFields(result));
|
|
598
|
+
finalText = reassembleProseFromRecords(result.records);
|
|
599
|
+
let resultError;
|
|
499
600
|
if (handlers.cancelling) {
|
|
500
|
-
|
|
601
|
+
resultError = new RuntimeError("SWARM_CANCELLED", "Swarm cancelled by user request.", "swarm.runtime");
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
try {
|
|
605
|
+
assertCliResultSuccess(result, "swarm.runtime");
|
|
606
|
+
if (preactivated && result.sessionId !== preactivated.sessionId) {
|
|
607
|
+
throw new RuntimeError("SWARM_SESSION_MISMATCH", "The resumed print session differs from the preactivated swarm session.", "swarm.session-provenance", { details: { expected_session_id: preactivated.sessionId, actual_session_id: result.sessionId ?? null } });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
resultError = error;
|
|
612
|
+
}
|
|
501
613
|
}
|
|
502
|
-
assertCliResultSuccess(result, "swarm.runtime");
|
|
503
614
|
warnHookEscapes(result, "swarm", context.stderr);
|
|
504
|
-
if (
|
|
615
|
+
if (preactivated) {
|
|
616
|
+
cleanupFailure = await attemptDeactivation(preactivated, "swarm.cleanup");
|
|
617
|
+
}
|
|
618
|
+
if (resultError !== undefined)
|
|
619
|
+
throw resultError;
|
|
620
|
+
if (handlers.cancelling) {
|
|
621
|
+
throw new RuntimeError("SWARM_CANCELLED", "Swarm cancelled by user request.", "swarm.runtime");
|
|
622
|
+
}
|
|
623
|
+
if (cleanupFailure)
|
|
624
|
+
throw cleanupFailure;
|
|
625
|
+
if (!preactivated &&
|
|
626
|
+
result.sessionId !== undefined &&
|
|
505
627
|
result.sessionId.length > 0 &&
|
|
506
628
|
result.sessionId !== job.kimi_session_id) {
|
|
507
629
|
store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
|
|
@@ -516,7 +638,7 @@ writeMode) {
|
|
|
516
638
|
});
|
|
517
639
|
warnIfSessionIdMissing(result, "swarm", job.job_id, context.stderr);
|
|
518
640
|
const prose = reassembleProseFromRecords(result.records);
|
|
519
|
-
|
|
641
|
+
finalText = prose;
|
|
520
642
|
if (writeMode) {
|
|
521
643
|
const captured = await capturePatchArtifact(paths, job.job_id, writeMode.workspaceRoot, context.stderr, writeMode.onPatchCaptureFailure);
|
|
522
644
|
if (captured === null) {
|
|
@@ -548,6 +670,21 @@ writeMode) {
|
|
|
548
670
|
}) ?? job);
|
|
549
671
|
}
|
|
550
672
|
catch (error) {
|
|
673
|
+
if (preactivated && deactivationRequired && activationOptions) {
|
|
674
|
+
if (handlers.cancelling) {
|
|
675
|
+
cleanupFailure = new RuntimeError("SWARM_SESSION_CLEANUP_FAILED", "Swarm session cleanup was not completed after cancellation.", "swarm.cleanup", { details: { cancelled: true, cleanup_unverified: true } });
|
|
676
|
+
}
|
|
677
|
+
else if (!cleanupAttempted) {
|
|
678
|
+
cleanupFailure = await attemptDeactivation(preactivated, "swarm.cleanup", recoveryCleanupTimeoutMs);
|
|
679
|
+
}
|
|
680
|
+
else if (error instanceof RuntimeError && error.code === "SWARM_SESSION_CLEANUP_FAILED") {
|
|
681
|
+
cleanupFailure ??= error;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (cleanupFailure) {
|
|
685
|
+
writeMode?.onPatchCaptureFailure?.();
|
|
686
|
+
context.stderr.write(`[kimi-plugin-cc] swarm session cleanup also failed: ${cleanupFailure.message}\n`);
|
|
687
|
+
}
|
|
551
688
|
let captureFailed = writeMode !== undefined && error instanceof RuntimeError && error.code === "SWARM_PATCH_CAPTURE_FAILED";
|
|
552
689
|
if (writeMode && !(error instanceof RuntimeError && error.code === "SWARM_PATCH_CAPTURE_FAILED")) {
|
|
553
690
|
const captured = await capturePatchArtifact(paths, job.job_id, writeMode.workspaceRoot, context.stderr, writeMode.onPatchCaptureFailure);
|
|
@@ -556,34 +693,55 @@ writeMode) {
|
|
|
556
693
|
context.stderr.write(`[kimi-plugin-cc] swarm-write run ended early; partial patch captured at ${captured.path}.\n`);
|
|
557
694
|
}
|
|
558
695
|
}
|
|
559
|
-
const recoveryDetails =
|
|
696
|
+
const recoveryDetails = {
|
|
697
|
+
...(error instanceof RuntimeError && typeof error.details.exit_code === "number"
|
|
698
|
+
? { exit_code: error.details.exit_code }
|
|
699
|
+
: {}),
|
|
700
|
+
...((captureFailed || cleanupFailure) && writeMode ? { worktreePath: writeMode.workspaceRoot } : {}),
|
|
701
|
+
...(cleanupFailure
|
|
702
|
+
? {
|
|
703
|
+
cleanup_error: {
|
|
704
|
+
code: cleanupFailure.code,
|
|
705
|
+
stage: cleanupFailure.stage,
|
|
706
|
+
cancelled: handlers.cancelling,
|
|
707
|
+
unverified: true,
|
|
708
|
+
},
|
|
709
|
+
...(finalText !== undefined ? { rawOutput: finalText } : {}),
|
|
710
|
+
}
|
|
711
|
+
: {}),
|
|
712
|
+
};
|
|
560
713
|
if (handlers.cancelling) {
|
|
714
|
+
const cancellationCause = classifyManagedCommandFailure(error, writeMode ? "rescue" : "review", job.job_id, {
|
|
715
|
+
preserveStage: true,
|
|
716
|
+
});
|
|
561
717
|
const cancelledError = new RuntimeError("SWARM_CANCELLED", captureFailed && writeMode
|
|
562
718
|
? `Swarm cancelled; patch capture failed and worktree retained at ${writeMode.workspaceRoot}.`
|
|
563
719
|
: "Swarm cancelled by user request.", "swarm.runtime", {
|
|
564
720
|
...(error instanceof Error ? { cause: error } : {}),
|
|
565
|
-
details:
|
|
721
|
+
details: {
|
|
722
|
+
...(cancellationCause instanceof RuntimeError ? cancellationCause.details : {}),
|
|
723
|
+
...recoveryDetails,
|
|
724
|
+
},
|
|
566
725
|
});
|
|
567
726
|
return await markJobCancelled(store, paths, job, "Swarm cancelled by user request.", cancelledError, {
|
|
568
727
|
phase: "cancelled",
|
|
569
728
|
});
|
|
570
729
|
}
|
|
730
|
+
if (error === cleanupFailure && budgetDeadline <= Date.now() && !(error instanceof ResponseTimeoutError)) {
|
|
731
|
+
error = new ResponseTimeoutError(budgetMs, "swarm.cleanup", commandLabel, undefined, cleanupFailure);
|
|
732
|
+
}
|
|
571
733
|
const classified = classifyManagedCommandFailure(error, writeMode ? "rescue" : "review", job.job_id, {
|
|
572
734
|
preserveStage: true,
|
|
573
735
|
});
|
|
574
|
-
|
|
575
|
-
|
|
736
|
+
const withRecovery = captureFailed || cleanupFailure
|
|
737
|
+
? classified instanceof RuntimeError
|
|
576
738
|
? new RuntimeError(classified.code, classified.message, classified.stage, {
|
|
577
739
|
cause: classified,
|
|
578
740
|
details: { ...classified.details, ...recoveryDetails },
|
|
579
741
|
})
|
|
580
|
-
: new RuntimeError("SWARM_PATCH_CAPTURE_FAILED", classified.message, "swarm.patch-capture", {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
});
|
|
584
|
-
return await markJobFailed(store, paths, job, withRecovery, "Swarm failed.", { phase: "failed" });
|
|
585
|
-
}
|
|
586
|
-
return await markJobFailed(store, paths, job, classified, "Swarm failed.", { phase: "failed" });
|
|
742
|
+
: new RuntimeError(cleanupFailure?.code ?? "SWARM_PATCH_CAPTURE_FAILED", classified.message, cleanupFailure?.stage ?? "swarm.patch-capture", { cause: classified, details: recoveryDetails })
|
|
743
|
+
: classified;
|
|
744
|
+
return await markJobFailed(store, paths, job, withRecovery, "Swarm failed.", { phase: "failed" });
|
|
587
745
|
}
|
|
588
746
|
finally {
|
|
589
747
|
handlers.dispose();
|
package/dist/companion.js
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { companionHelp } from "./help.js";
|
|
2
|
-
import { runAsk } from "./commands/ask.js";
|
|
3
2
|
import { runModelInventory } from "./commands/models.js";
|
|
4
3
|
import { runCancel } from "./commands/cancel.js";
|
|
5
|
-
import { notImplementedCompanionCommand } from "./commands/not-implemented.js";
|
|
6
4
|
import { runReplay } from "./commands/replay.js";
|
|
7
5
|
import { runRepairSessions } from "./commands/repair-sessions.js";
|
|
8
6
|
import { runResult } from "./commands/result.js";
|
|
9
|
-
import { runReview } from "./commands/review.js";
|
|
10
|
-
import { runRescue } from "./commands/rescue.js";
|
|
11
|
-
import { runPursue } from "./commands/pursue.js";
|
|
12
|
-
import { runSwarm } from "./commands/swarm.js";
|
|
13
7
|
import { renderSetupResult, runSetup } from "./commands/setup.js";
|
|
14
8
|
import { runStatus } from "./commands/status.js";
|
|
15
9
|
import { runWorker } from "./commands/worker.js";
|
|
16
10
|
import { formatError, RuntimeError } from "./errors.js";
|
|
17
11
|
import { resolveKimiHome } from "./kimi-home.js";
|
|
12
|
+
import { runModelCommand } from "./model-command.js";
|
|
13
|
+
import { runMcpChild } from "./mcp/supervised-child.js";
|
|
14
|
+
import { runMcpServer } from "./mcp/server.js";
|
|
18
15
|
async function main(argv) {
|
|
19
16
|
const help = companionHelp(argv);
|
|
20
17
|
if (help !== undefined) {
|
|
@@ -27,11 +24,16 @@ async function main(argv) {
|
|
|
27
24
|
cwd,
|
|
28
25
|
env: {
|
|
29
26
|
...process.env,
|
|
30
|
-
KIMI_CODE_HOME: resolveKimiHome(process.env, cwd),
|
|
27
|
+
KIMI_CODE_HOME: command === "mcp" ? process.env.KIMI_CODE_HOME : resolveKimiHome(process.env, cwd),
|
|
31
28
|
},
|
|
32
29
|
stdout: process.stdout,
|
|
33
30
|
stderr: process.stderr,
|
|
34
31
|
};
|
|
32
|
+
const modelResult = await runModelCommand(argv, context);
|
|
33
|
+
if (modelResult !== undefined) {
|
|
34
|
+
context.stdout.write(modelResult);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
35
37
|
switch (command) {
|
|
36
38
|
case "setup": {
|
|
37
39
|
if (rest.includes("--models")) {
|
|
@@ -57,41 +59,12 @@ async function main(argv) {
|
|
|
57
59
|
}
|
|
58
60
|
return;
|
|
59
61
|
}
|
|
60
|
-
case "
|
|
61
|
-
|
|
62
|
-
context.stdout.write(`${result}\n`);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
case "task": {
|
|
66
|
-
const [taskType, ...taskArgs] = rest;
|
|
67
|
-
if (taskType === "rescue") {
|
|
68
|
-
const result = await runRescue(taskArgs, context);
|
|
69
|
-
context.stdout.write(result);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
if (taskType === "challenge") {
|
|
73
|
-
const result = await runReview(taskArgs, context, "challenge");
|
|
74
|
-
context.stdout.write(`${result}\n`);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
if (taskType === "pursue") {
|
|
78
|
-
const result = await runPursue(taskArgs, context);
|
|
79
|
-
context.stdout.write(result);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
if (taskType === "swarm") {
|
|
83
|
-
const result = await runSwarm(taskArgs, context);
|
|
84
|
-
context.stdout.write(result);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
notImplementedCompanionCommand(taskType ? `task ${taskType}` : "task");
|
|
62
|
+
case "mcp":
|
|
63
|
+
await runMcpServer(context);
|
|
88
64
|
return;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const result = await runAsk(rest, context);
|
|
92
|
-
context.stdout.write(`${result}\n`);
|
|
65
|
+
case "mcp-child":
|
|
66
|
+
context.stdout.write(await runMcpChild(rest, context));
|
|
93
67
|
return;
|
|
94
|
-
}
|
|
95
68
|
case "status":
|
|
96
69
|
context.stdout.write(await runStatus(rest, context));
|
|
97
70
|
return;
|
package/dist/help.js
CHANGED
|
@@ -35,6 +35,8 @@ export function companionHelp(argv) {
|
|
|
35
35
|
case "replay": return usage(command, "<job-id>", REPLAY_SUPPORTED_FLAGS, "Replay a completed job's persisted stream log.");
|
|
36
36
|
case "repair-sessions": return usage(command, "[options]", REPAIR_SESSIONS_SUPPORTED_FLAGS, "Preview session metadata repair; --apply writes, --all selects all host-owned repositories.");
|
|
37
37
|
case "worker": return "Usage: companion.sh worker <ask|rescue> <job-id>. Internal detached-worker entry point; launched by ask/rescue, not manually.";
|
|
38
|
+
case "mcp": return "Usage: companion.sh mcp. Run the Kimi MCP stdio server; normally launched by Claude Code or Codex plugin configuration.";
|
|
39
|
+
case "mcp-child": return "Usage: companion.sh mcp-child <job-uuid> <model-operation> [arguments]. Internal supervised MCP child; launched by the MCP server, not manually.";
|
|
38
40
|
default: return undefined;
|
|
39
41
|
}
|
|
40
42
|
}
|
|
@@ -594,8 +594,8 @@ function validateVersionSensitiveEvents(events, version) {
|
|
|
594
594
|
}
|
|
595
595
|
// Exact schemas reviewed at 2.0.0 (1b89e4b0), 2.0.1 (caf7d4e2),
|
|
596
596
|
// 2.0.2 (9d07f634), 2.1.0 (52437299), 2.1.1 (f67e6398), 2.2.0 (472192c3),
|
|
597
|
-
// 2.3.0 (b5e28fd6)
|
|
598
|
-
// are unchanged. Require the full version string AND matching components;
|
|
597
|
+
// 2.3.0 (b5e28fd6), 2.3.1 (684bae9e) and 2.3.2 (b8dd3a13): the additive
|
|
598
|
+
// events and HookDefSchema are unchanged. Require the full version string AND matching components;
|
|
599
599
|
// no inferred 2.x compatibility.
|
|
600
600
|
if (version.major === 2 && ((version.minor === 0 && ((version.version === "2.0.0" && version.patch === 0) ||
|
|
601
601
|
(version.version === "2.0.1" && version.patch === 1) ||
|
|
@@ -604,7 +604,8 @@ function validateVersionSensitiveEvents(events, version) {
|
|
|
604
604
|
(version.version === "2.1.1" && version.patch === 1))) ||
|
|
605
605
|
(version.minor === 2 && ((version.version === "2.2.0" && version.patch === 0))) ||
|
|
606
606
|
(version.minor === 3 && ((version.version === "2.3.0" && version.patch === 0) ||
|
|
607
|
-
(version.version === "2.3.1" && version.patch === 1)
|
|
607
|
+
(version.version === "2.3.1" && version.patch === 1) ||
|
|
608
|
+
(version.version === "2.3.2" && version.patch === 2))))) {
|
|
608
609
|
return { valid: true };
|
|
609
610
|
}
|
|
610
611
|
if (version.major !== 0) {
|
package/dist/job-id.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { RuntimeError } from "./errors.js";
|
|
3
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4
|
+
export function allocateJobId(context) {
|
|
5
|
+
const fixed = context.mcpJobId;
|
|
6
|
+
if (fixed === undefined)
|
|
7
|
+
return randomUUID();
|
|
8
|
+
if (!UUID_PATTERN.test(fixed)) {
|
|
9
|
+
throw new RuntimeError("MCP_INVALID_JOB_ID", "The supervised job ID must be a valid UUID.", "mcp.child");
|
|
10
|
+
}
|
|
11
|
+
return fixed;
|
|
12
|
+
}
|
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;
|
package/dist/kimi-engine.js
CHANGED
|
@@ -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
|
+
}
|