kimi-plugin-cc 2.4.0 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +18 -0
  4. package/README.md +17 -9
  5. package/dist/cli-client.js +19 -9
  6. package/dist/commands/cancel.js +14 -3
  7. package/dist/commands/cli-helpers.js +5 -1
  8. package/dist/commands/recover-swarm.js +135 -0
  9. package/dist/commands/status.js +1 -1
  10. package/dist/commands/swarm.js +241 -87
  11. package/dist/companion.js +5 -1
  12. package/dist/git.js +5 -3
  13. package/dist/help.js +3 -2
  14. package/dist/hooks/config-safety.js +4 -3
  15. package/dist/job-store.js +13 -0
  16. package/dist/jobs.js +3 -0
  17. package/dist/kimi-engine.js +1 -0
  18. package/dist/kimi-errors.js +27 -21
  19. package/dist/parsing.js +1 -0
  20. package/dist/process-tree.js +48 -17
  21. package/dist/prompt-size.js +18 -0
  22. package/dist/swarm-activation.js +833 -0
  23. package/dist/swarm-recovery.js +134 -0
  24. package/dist/version.js +1 -1
  25. package/docs/migration.md +12 -2
  26. package/docs/registry-releases.md +13 -2
  27. package/docs/safety.md +20 -6
  28. package/docs/swarm-recovery.md +33 -0
  29. package/docs/windows-validation.md +39 -0
  30. package/package.json +7 -4
  31. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  32. package/plugins/kimi-codex/dist/cli-client.js +19 -9
  33. package/plugins/kimi-codex/dist/commands/cancel.js +14 -3
  34. package/plugins/kimi-codex/dist/commands/cli-helpers.js +5 -1
  35. package/plugins/kimi-codex/dist/commands/recover-swarm.js +135 -0
  36. package/plugins/kimi-codex/dist/commands/status.js +1 -1
  37. package/plugins/kimi-codex/dist/commands/swarm.js +241 -87
  38. package/plugins/kimi-codex/dist/companion.js +5 -1
  39. package/plugins/kimi-codex/dist/git.js +5 -3
  40. package/plugins/kimi-codex/dist/help.js +3 -2
  41. package/plugins/kimi-codex/dist/hooks/config-safety.js +4 -3
  42. package/plugins/kimi-codex/dist/job-store.js +13 -0
  43. package/plugins/kimi-codex/dist/jobs.js +3 -0
  44. package/plugins/kimi-codex/dist/kimi-engine.js +1 -0
  45. package/plugins/kimi-codex/dist/kimi-errors.js +27 -21
  46. package/plugins/kimi-codex/dist/parsing.js +1 -0
  47. package/plugins/kimi-codex/dist/process-tree.js +48 -17
  48. package/plugins/kimi-codex/dist/prompt-size.js +18 -0
  49. package/plugins/kimi-codex/dist/swarm-activation.js +833 -0
  50. package/plugins/kimi-codex/dist/swarm-recovery.js +134 -0
  51. package/plugins/kimi-codex/dist/version.js +1 -1
@@ -85,12 +85,13 @@ async function runGit(args, cwd) {
85
85
  }
86
86
  return result.stdout;
87
87
  }
88
- async function runGitAllowFailure(args, cwd) {
88
+ async function runGitAllowFailure(args, cwd, signal) {
89
89
  try {
90
90
  const { stdout, stderr } = await execFileAsync("git", args, {
91
91
  cwd,
92
92
  env: process.env,
93
93
  maxBuffer: 10 * 1024 * 1024,
94
+ signal,
94
95
  });
95
96
  return { ok: true, stdout, stderr };
96
97
  }
@@ -163,11 +164,11 @@ export async function createEphemeralWorktree(repoRoot, base, worktreePath) {
163
164
  *
164
165
  * Returns the patch text (empty string if the swarm changed nothing).
165
166
  */
166
- export async function captureWorktreePatch(worktreePath) {
167
+ export async function captureWorktreePatch(worktreePath, signal) {
167
168
  // Intent-to-add is required so untracked files surface in `git diff HEAD`.
168
169
  // An add -N failure aborts capture rather than returning a patch that omits
169
170
  // new files.
170
- const intentToAdd = await runGitAllowFailure(["add", "-N", "--", "."], worktreePath);
171
+ const intentToAdd = await runGitAllowFailure(["add", "-N", "--", "."], worktreePath, signal);
171
172
  if (!intentToAdd.ok) {
172
173
  throw new RuntimeError("GIT_WORKTREE_INTENT_TO_ADD_FAILED", `git add -N (worktree capture) failed: ${intentToAdd.stderr || "unknown error"}`, "swarm.worktree", { details: { worktreePath } });
173
174
  }
@@ -176,6 +177,7 @@ export async function captureWorktreePatch(worktreePath) {
176
177
  cwd: worktreePath,
177
178
  env: process.env,
178
179
  maxBuffer: MAX_PATCH_BYTES,
180
+ signal,
179
181
  });
180
182
  return stdout;
181
183
  }
@@ -1,5 +1,5 @@
1
- import { hasHelpFlag, ASK_SUPPORTED_FLAGS, RESCUE_SUPPORTED_FLAGS, REVIEW_SUPPORTED_FLAGS, PURSUE_SUPPORTED_FLAGS, SWARM_SUPPORTED_FLAGS, JOB_LOOKUP_SUPPORTED_FLAGS, SETUP_SUPPORTED_FLAGS, REPAIR_SESSIONS_SUPPORTED_FLAGS, REPLAY_SUPPORTED_FLAGS, } from "./parsing.js";
2
- const SUBCOMMANDS = "setup, review, task, ask, status, result, cancel, replay, repair-sessions";
1
+ import { hasHelpFlag, ASK_SUPPORTED_FLAGS, RESCUE_SUPPORTED_FLAGS, REVIEW_SUPPORTED_FLAGS, PURSUE_SUPPORTED_FLAGS, SWARM_SUPPORTED_FLAGS, JOB_LOOKUP_SUPPORTED_FLAGS, SETUP_SUPPORTED_FLAGS, REPAIR_SESSIONS_SUPPORTED_FLAGS, REPLAY_SUPPORTED_FLAGS, RECOVER_SWARM_SUPPORTED_FLAGS, } from "./parsing.js";
2
+ const SUBCOMMANDS = "setup, review, task, ask, status, result, cancel, replay, repair-sessions, recover-swarm";
3
3
  const TASK_KINDS = "rescue, challenge, pursue, swarm";
4
4
  /** Help is a dispatch-only, side-effect-free exception to normal argument parsing. */
5
5
  export function companionHelp(argv) {
@@ -33,6 +33,7 @@ export function companionHelp(argv) {
33
33
  case "cancel":
34
34
  return usage(command, "[job-id] [options]", JOB_LOOKUP_SUPPORTED_FLAGS, command === "status" ? "Show job state as JSON." : command === "result" ? "Read a terminal artifact, optionally in a JSON envelope." : "Cancel a running job; without an id, target the repository's latest running job.");
35
35
  case "replay": return usage(command, "<job-id>", REPLAY_SUPPORTED_FLAGS, "Replay a completed job's persisted stream log.");
36
+ case "recover-swarm": return `Usage: companion.sh recover-swarm <job-id> [${RECOVER_SWARM_SUPPORTED_FLAGS}]. Preview durable swarm recovery; --apply stops verified orphans, disables the recorded session and captures a patch. Never resumes prompts or changes terminal job outcomes.`;
36
37
  case "repair-sessions": return usage(command, "[options]", REPAIR_SESSIONS_SUPPORTED_FLAGS, "Preview session metadata repair; --apply writes, --all selects all host-owned repositories.");
37
38
  case "worker": return "Usage: companion.sh worker <ask|rescue> <job-id>. Internal detached-worker entry point; launched by ask/rescue, not manually.";
38
39
  case "mcp": return "Usage: companion.sh mcp. Run the Kimi MCP stdio server; normally launched by Claude Code or Codex plugin configuration.";
@@ -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) and 2.3.1 (684bae9e): the additive events and HookDefSchema
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) {
@@ -122,6 +122,8 @@ export class JobStore {
122
122
  CREATE INDEX IF NOT EXISTS jobs_repo_updated_idx
123
123
  ON jobs (repo_id, updated_at DESC);
124
124
 
125
+ CREATE TABLE IF NOT EXISTS swarm_recovery (job_id TEXT PRIMARY KEY, payload TEXT NOT NULL, recovery_owner TEXT);
126
+
125
127
  CREATE INDEX IF NOT EXISTS jobs_repo_type_updated_idx
126
128
  ON jobs (repo_id, command_type, updated_at DESC);
127
129
 
@@ -186,6 +188,17 @@ export class JobStore {
186
188
  throw translateSqliteError(error);
187
189
  }
188
190
  }
191
+ getSwarmRecovery(jobId) {
192
+ const row = this.db.get("SELECT payload, recovery_owner FROM swarm_recovery WHERE job_id = ?", jobId);
193
+ return row ? { record: JSON.parse(row.payload), owner: row.recovery_owner } : null;
194
+ }
195
+ saveSwarmRecovery(jobId, record) {
196
+ this.db.run("INSERT INTO swarm_recovery (job_id, payload) VALUES (?, ?) ON CONFLICT(job_id) DO UPDATE SET payload = excluded.payload", [jobId, JSON.stringify(record)]);
197
+ }
198
+ claimSwarmRecovery(jobId, expected, owner) {
199
+ this.db.run("UPDATE swarm_recovery SET recovery_owner = ? WHERE job_id = ? AND recovery_owner IS ?", [owner, jobId, expected]);
200
+ return this.db.get("SELECT changes() AS count").count === 1;
201
+ }
189
202
  close() {
190
203
  this.db.close();
191
204
  }
@@ -85,6 +85,9 @@ export async function sweepStaleJobs(store, paths) {
85
85
  for (const job of store.listRunningJobsWithProcessHints()) {
86
86
  if (store.getMcpTask(job.job_id))
87
87
  continue;
88
+ // Recovery owns orphaned swarm resources; PID-only sweeping must not signal replacements.
89
+ if (store.getSwarmRecovery(job.job_id))
90
+ continue;
88
91
  if (!isStaleEnoughToSweep(job)) {
89
92
  continue;
90
93
  }
@@ -41,6 +41,7 @@ export const NATIVE_V2_CERTIFIED_VERSIONS = Object.freeze([
41
41
  "2.2.0",
42
42
  "2.3.0",
43
43
  "2.3.1",
44
+ "2.3.2",
44
45
  ]);
45
46
  /** Per-operation native-v2 certification. One green operation never certifies another. */
46
47
  export const NATIVE_V2_CERTIFIED = new Map([
@@ -44,6 +44,11 @@ export function classifyManagedCommandFailure(error, commandType, jobId, options
44
44
  const details = { availability: classification.kind };
45
45
  if (error instanceof RuntimeError) {
46
46
  details.cause_code = error.code;
47
+ for (const key of ["spawn_code", "spawn_errno", "spawn_message"]) {
48
+ if (error.details[key] !== undefined)
49
+ details[key] = key === "spawn_message" && typeof error.details[key] === "string"
50
+ ? truncateCauseMessageForDetails(error.details[key]) : error.details[key];
51
+ }
47
52
  if (error.code === "RESPONSE_TIMEOUT") {
48
53
  for (const key of ["budget_ms", "record_count", "partial_output_chars", "aborted", "settled_post_abort", "hook_escapes"]) {
49
54
  if (error.details[key] !== undefined)
@@ -129,28 +134,29 @@ function classifyKimiAvailability(error) {
129
134
  authProbe: "failed",
130
135
  };
131
136
  }
132
- if (error instanceof RuntimeError && error.code === "CLI_SPAWN_FAILED") {
133
- return {
134
- kind: "binary_unavailable",
135
- summary: "the Kimi CLI is missing from PATH or not executable in this environment.",
136
- nextStep: "Run `/kimi:setup` to verify the install, then expose `kimi` on PATH and retry.",
137
- runtimeProbe: "failed",
138
- authProbe: "failed",
139
- };
140
- }
141
- // v1.0 cli-client surfaces async ENOENT (Bun) as CLI_PROCESS_ERROR with
142
- // "spawn ... ENOENT" in the message. Map that to binary_unavailable so
143
- // the failure message points users at /kimi:setup.
144
137
  if (error instanceof RuntimeError &&
145
- error.code === "CLI_PROCESS_ERROR" &&
146
- /\bENOENT\b/.test(message)) {
147
- return {
148
- kind: "binary_unavailable",
149
- summary: "the Kimi CLI is missing from PATH or not executable in this environment.",
150
- nextStep: "Run `/kimi:setup` to verify the install, then expose `kimi` on PATH and retry.",
151
- runtimeProbe: "failed",
152
- authProbe: "failed",
153
- };
138
+ (error.code === "CLI_SPAWN_FAILED" || error.code === "CLI_PROCESS_ERROR")) {
139
+ const spawnCode = error.details.spawn_code;
140
+ const binaryUnavailable = typeof spawnCode === "string" && ["ENOENT", "EACCES", "ENOEXEC"].includes(spawnCode);
141
+ if (typeof spawnCode === "string" && !binaryUnavailable) {
142
+ return {
143
+ kind: "startup_failed",
144
+ summary: "the Kimi subprocess could not be started or used.",
145
+ nextStep: "Check the underlying process error; shorten the prompt or environment if an argument-size limit was exceeded, then retry.",
146
+ runtimeProbe: "failed",
147
+ authProbe: "failed",
148
+ };
149
+ }
150
+ // Keep opaque synchronous failures and legacy async ENOENT diagnostics compatible.
151
+ if (binaryUnavailable || error.code === "CLI_SPAWN_FAILED" || /\bENOENT\b/.test(message)) {
152
+ return {
153
+ kind: "binary_unavailable",
154
+ summary: "the Kimi CLI is missing from PATH or not executable in this environment.",
155
+ nextStep: "Run `/kimi:setup` to verify the install, then expose `kimi` on PATH and retry.",
156
+ runtimeProbe: "failed",
157
+ authProbe: "failed",
158
+ };
159
+ }
154
160
  }
155
161
  // v1.0 cli-client surfaces non-ENOENT process errors and non-zero
156
162
  // exits as CLI_PROCESS_ERROR / CLI_NONZERO_EXIT. Treat both as
@@ -18,6 +18,7 @@ export const RESCUE_SUPPORTED_FLAGS = "-m/--model <name>, --thinking <off|low|me
18
18
  export const REVIEW_SUPPORTED_FLAGS = "--base <ref>, -m/--model <name>, --thinking <off|low|medium|high>, --budget <30m|1h|90s>";
19
19
  export const JOB_LOOKUP_SUPPORTED_FLAGS = "--type <review|challenge|rescue|review_gate|ask>, --json";
20
20
  export const SETUP_SUPPORTED_FLAGS = "--check, --uninstall, --all (with --uninstall), --enable-review-gate, --disable-review-gate, --models [--json] (without other setup flags)";
21
+ export const RECOVER_SWARM_SUPPORTED_FLAGS = "--apply";
21
22
  export const REPAIR_SESSIONS_SUPPORTED_FLAGS = "--apply, --all";
22
23
  export const REPLAY_SUPPORTED_FLAGS = "none (one job-id required)";
23
24
  /** Validate syntax only; upstream per-model supportEfforts remains authoritative. */
@@ -6,7 +6,7 @@ const MAX_DESCENDANT_DEPTH = 8;
6
6
  const MAX_DESCENDANT_PIDS = 512;
7
7
  const PROCESS_EXIT_POLL_MS = 10;
8
8
  const PROCESS_SNAPSHOT_TIMEOUT_MS = 1_000;
9
- const PROCESS_IDENTITY_PROBE_TIMEOUT_MS = 250;
9
+ const PROCESS_IDENTITY_PROBE_TIMEOUT_MS = 1_000;
10
10
  const PROBE_TIMEOUT = Symbol("process-probe-timeout");
11
11
  /**
12
12
  * Capture descendant ancestry and reusable identities in the same bounded
@@ -23,6 +23,18 @@ export async function collectDescendantIdentities(rootPid) {
23
23
  }
24
24
  return snapshot;
25
25
  }
26
+ /** Return current members of the selected process groups, or undefined when unreadable. */
27
+ export async function collectProcessGroupsIdentities(processGroupIds) {
28
+ if (process.platform === "win32")
29
+ return undefined;
30
+ const groups = new Set(processGroupIds.filter((id) => Number.isInteger(id) && id > 0));
31
+ if (groups.size === 0)
32
+ return [];
33
+ const rows = await raceValueWithTimeout(collectProcessSnapshotRows(), PROCESS_SNAPSHOT_TIMEOUT_MS);
34
+ if (rows === PROBE_TIMEOUT || rows === undefined)
35
+ return undefined;
36
+ return rows.filter((row) => groups.has(row.processGroupId)).map(toProcessIdentity);
37
+ }
26
38
  /**
27
39
  * Compatibility surface for callers that only need numeric process listings.
28
40
  * Cancellation must use collectDescendantIdentities so ancestry and identity
@@ -36,15 +48,21 @@ export async function collectDescendants(rootPid) {
36
48
  return [];
37
49
  }
38
50
  }
39
- /** Read one reusable process identity. PID alone is not stable across teardown. */
40
- export async function readProcessIdentity(pid) {
51
+ /** Distinguish a missing process from a failed or timed-out identity probe. */
52
+ export async function probeProcessIdentity(pid) {
41
53
  if (!Number.isInteger(pid) || pid <= 0 || process.platform === "win32")
42
- return undefined;
54
+ return { complete: true };
43
55
  const read = process.platform === "linux"
44
56
  ? readProcessIdentityBatchFromProc([pid])
45
57
  : readProcessIdentityBatchWithPs([pid]);
46
58
  const batch = await raceValueWithTimeout(read, PROCESS_IDENTITY_PROBE_TIMEOUT_MS);
47
- return batch === PROBE_TIMEOUT ? undefined : batch.identities.get(pid);
59
+ if (batch === PROBE_TIMEOUT)
60
+ return { complete: false };
61
+ return { identity: batch.identities.get(pid), complete: batch.complete };
62
+ }
63
+ /** Read one reusable process identity. PID alone is not stable across teardown. */
64
+ export async function readProcessIdentity(pid) {
65
+ return (await probeProcessIdentity(pid)).identity;
48
66
  }
49
67
  /**
50
68
  * Revalidate all captured identities concurrently under one deadline. The
@@ -63,16 +81,26 @@ export async function revalidateProcessIdentities(expectedIdentities, options =
63
81
  if (batch === PROBE_TIMEOUT)
64
82
  return { identities: [], complete: false };
65
83
  const identities = [];
84
+ const groupChanged = [];
66
85
  for (const expectedIdentity of expected) {
67
86
  const current = batch.identities.get(expectedIdentity.pid);
68
- if (current !== undefined &&
69
- current.startToken === expectedIdentity.startToken &&
70
- current.processGroupId === expectedIdentity.processGroupId &&
71
- !isQuiescentState(current.state)) {
87
+ if (current === undefined ||
88
+ current.startToken !== expectedIdentity.startToken ||
89
+ isQuiescentState(current.state)) {
90
+ continue;
91
+ }
92
+ if (current.processGroupId !== expectedIdentity.processGroupId) {
93
+ groupChanged.push(current);
94
+ }
95
+ else {
72
96
  identities.push(current);
73
97
  }
74
98
  }
75
- return { identities, complete: batch.complete };
99
+ return {
100
+ identities,
101
+ complete: batch.complete,
102
+ ...(groupChanged.length > 0 ? { groupChanged } : {}),
103
+ };
76
104
  }
77
105
  /** Return the current identity only when pid, start token, and PGID still match. */
78
106
  export async function revalidateProcessIdentity(expected, identityReader) {
@@ -104,7 +132,7 @@ export async function waitForProcessTreeExit(identities, timeoutMs, options = {}
104
132
  identityReader: options.identityReader,
105
133
  timeoutMs: probeTimeoutMs,
106
134
  });
107
- active = !validation.complete || validation.identities.length > 0;
135
+ active = !validation.complete || validation.identities.length > 0 || (validation.groupChanged?.length ?? 0) > 0;
108
136
  }
109
137
  if (!active)
110
138
  return true;
@@ -117,13 +145,13 @@ export async function waitForProcessTreeExit(identities, timeoutMs, options = {}
117
145
  async function collectVerifiedDescendantIdentities(rootPid) {
118
146
  const initialRows = await collectProcessSnapshotRows();
119
147
  if (initialRows === undefined)
120
- return [];
148
+ throw new Error("process snapshot unavailable");
121
149
  const candidateRows = descendantRowsFromSnapshot(rootPid, initialRows);
122
150
  const initialRoot = initialRows.find((row) => row.pid === rootPid);
123
151
  if (initialRoot === undefined || candidateRows.length === 0)
124
152
  return [];
125
- // A second, targeted snapshot closes the cross-row /proc race: every child
126
- // identity and every numeric parent edge must still match before acceptance.
153
+ // A second, targeted snapshot closes the cross-row /proc race: each child
154
+ // must retain its PID and start token. Reparenting and PGID changes are not exit.
127
155
  const expectedRows = [initialRoot, ...candidateRows];
128
156
  const expectedByPid = new Map(expectedRows.map((row) => [row.pid, row]));
129
157
  const verificationRows = await readCurrentSnapshotRows(expectedRows);
@@ -131,11 +159,9 @@ async function collectVerifiedDescendantIdentities(rootPid) {
131
159
  const expected = expectedByPid.get(current.pid);
132
160
  return (expected !== undefined &&
133
161
  current.startToken === expected.startToken &&
134
- current.processGroupId === expected.processGroupId &&
135
- current.parentPid === expected.parentPid &&
136
162
  !isQuiescentState(current.state));
137
163
  });
138
- return descendantRowsFromSnapshot(rootPid, stableRows).map(toProcessIdentity);
164
+ return stableRows.map(toProcessIdentity);
139
165
  }
140
166
  function descendantRowsFromSnapshot(rootPid, rows) {
141
167
  const root = rows.find((row) => row.pid === rootPid);
@@ -186,6 +212,9 @@ async function readCurrentSnapshotRows(expectedRows) {
186
212
  Promise.all(procPids.map((pid) => readProcRow(pid))),
187
213
  collectProcessSnapshotRowsFromPs(psPids),
188
214
  ]);
215
+ if (procOutcomes.some((outcome) => outcome.status === "error") || (psPids.length > 0 && psRows === undefined)) {
216
+ throw new Error("process identity verification failed");
217
+ }
189
218
  return [
190
219
  ...procOutcomes.flatMap((outcome) => outcome.status === "found" ? [outcome.row] : []),
191
220
  ...(psRows ?? []),
@@ -212,6 +241,8 @@ async function collectProcessSnapshotRowsFromProc() {
212
241
  const outcomes = await Promise.all(entries
213
242
  .filter((entry) => /^\d+$/.test(entry))
214
243
  .map((entry) => readProcRow(Number.parseInt(entry, 10))));
244
+ if (outcomes.some((outcome) => outcome.status === "error"))
245
+ return undefined;
215
246
  return outcomes.flatMap((outcome) => outcome.status === "found" ? [outcome.row] : []);
216
247
  }
217
248
  async function collectProcessSnapshotRowsFromPs(pids) {
@@ -0,0 +1,18 @@
1
+ import { RuntimeError } from "./errors.js";
2
+ // Node/libuv's default CreateProcess argument quoting (UTF-16 code units).
3
+ export function windowsCommandLineLength(command, args) {
4
+ const quotedLength = (value) => {
5
+ if (value.length > 0 && !/[\s"]/u.test(value))
6
+ return value.length;
7
+ return 2 + value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1').length;
8
+ };
9
+ return [command, ...args].reduce((size, value) => size + quotedLength(value) + 1, 0);
10
+ }
11
+ export function assertPromptFitsCommandLine(command, args, platform = process.platform) {
12
+ if (platform !== "win32")
13
+ return;
14
+ const size = windowsCommandLineLength(command, args);
15
+ if (size <= 32_767)
16
+ return;
17
+ throw new RuntimeError("CLI_COMMAND_LINE_TOO_LONG", "The Kimi prompt exceeds the Windows command-line limit; the CLI was not started. Narrow the review or split it into smaller scopes. A short ask prompt can reference a saved diff inside the workspace. Reinstalling Kimi or running setup will not fix this. The current print transport does not support stdin prompts.", "cli-client.pre-spawn", { details: { command_line_utf16_units: size, limit_utf16_units: 32_767, platform } });
18
+ }