kimi-plugin-cc 2.4.5 → 2.5.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 (114) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +103 -29
  5. package/dist/commands/setup.js +78 -51
  6. package/dist/dsh/companion.js +177 -0
  7. package/dist/dsh/index.js +169 -0
  8. package/dist/hooks/install-paths.js +2 -2
  9. package/dist/hooks/managed-block.js +86 -20
  10. package/dist/version.js +1 -1
  11. package/docs/migration.md +4 -2
  12. package/docs/models.md +5 -1
  13. package/docs/registry-releases.md +84 -20
  14. package/docs/safety.md +25 -10
  15. package/docs/upstream-2.4.1-certification.md +6 -0
  16. package/package.json +75 -8
  17. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  18. package/plugins/kimi-codex/dist/commands/setup.js +78 -51
  19. package/plugins/kimi-codex/dist/dsh/companion.js +177 -0
  20. package/plugins/kimi-codex/dist/dsh/index.js +169 -0
  21. package/plugins/kimi-codex/dist/hooks/install-paths.js +2 -2
  22. package/plugins/kimi-codex/dist/hooks/managed-block.js +86 -20
  23. package/plugins/kimi-codex/dist/version.js +1 -1
  24. package/plugins/kimi-dsh/LICENSE +201 -0
  25. package/plugins/kimi-dsh/README.md +83 -0
  26. package/plugins/kimi-dsh/cordis.patch.yml +45 -0
  27. package/plugins/kimi-dsh/dist/background-spawn.js +216 -0
  28. package/plugins/kimi-dsh/dist/cli-cancellation.js +43 -0
  29. package/plugins/kimi-dsh/dist/cli-client.js +876 -0
  30. package/plugins/kimi-dsh/dist/commands/ask.js +336 -0
  31. package/plugins/kimi-dsh/dist/commands/cancel.js +216 -0
  32. package/plugins/kimi-dsh/dist/commands/cli-helpers.js +201 -0
  33. package/plugins/kimi-dsh/dist/commands/models.js +140 -0
  34. package/plugins/kimi-dsh/dist/commands/not-implemented.js +4 -0
  35. package/plugins/kimi-dsh/dist/commands/pursue.js +307 -0
  36. package/plugins/kimi-dsh/dist/commands/recover-swarm.js +135 -0
  37. package/plugins/kimi-dsh/dist/commands/registry.js +74 -0
  38. package/plugins/kimi-dsh/dist/commands/repair-sessions.js +160 -0
  39. package/plugins/kimi-dsh/dist/commands/replay.js +124 -0
  40. package/plugins/kimi-dsh/dist/commands/rescue.js +347 -0
  41. package/plugins/kimi-dsh/dist/commands/result.js +66 -0
  42. package/plugins/kimi-dsh/dist/commands/review-gate.js +390 -0
  43. package/plugins/kimi-dsh/dist/commands/review.js +220 -0
  44. package/plugins/kimi-dsh/dist/commands/setup.js +1074 -0
  45. package/plugins/kimi-dsh/dist/commands/status.js +27 -0
  46. package/plugins/kimi-dsh/dist/commands/swarm.js +754 -0
  47. package/plugins/kimi-dsh/dist/commands/worker.js +25 -0
  48. package/plugins/kimi-dsh/dist/companion.js +97 -0
  49. package/plugins/kimi-dsh/dist/config.js +22 -0
  50. package/plugins/kimi-dsh/dist/dsh/companion.js +177 -0
  51. package/plugins/kimi-dsh/dist/dsh/index.js +169 -0
  52. package/plugins/kimi-dsh/dist/errors.js +40 -0
  53. package/plugins/kimi-dsh/dist/git.js +224 -0
  54. package/plugins/kimi-dsh/dist/help.js +43 -0
  55. package/plugins/kimi-dsh/dist/hook-escape.js +71 -0
  56. package/plugins/kimi-dsh/dist/hooks/approval-hook.js +101 -0
  57. package/plugins/kimi-dsh/dist/hooks/approval-policy.js +211 -0
  58. package/plugins/kimi-dsh/dist/hooks/config-safety.js +653 -0
  59. package/plugins/kimi-dsh/dist/hooks/install-paths.js +428 -0
  60. package/plugins/kimi-dsh/dist/hooks/install.js +210 -0
  61. package/plugins/kimi-dsh/dist/hooks/managed-block.js +685 -0
  62. package/plugins/kimi-dsh/dist/hooks/review-gate-stop.js +19 -0
  63. package/plugins/kimi-dsh/dist/job-id.js +12 -0
  64. package/plugins/kimi-dsh/dist/job-store.js +659 -0
  65. package/plugins/kimi-dsh/dist/jobs.js +217 -0
  66. package/plugins/kimi-dsh/dist/kimi-command.js +70 -0
  67. package/plugins/kimi-dsh/dist/kimi-engine.js +632 -0
  68. package/plugins/kimi-dsh/dist/kimi-errors.js +260 -0
  69. package/plugins/kimi-dsh/dist/kimi-home.js +9 -0
  70. package/plugins/kimi-dsh/dist/kimi-timeouts.js +52 -0
  71. package/plugins/kimi-dsh/dist/kimi-version-probe.js +216 -0
  72. package/plugins/kimi-dsh/dist/logging.js +22 -0
  73. package/plugins/kimi-dsh/dist/mcp/binding.js +58 -0
  74. package/plugins/kimi-dsh/dist/mcp/job-control.js +345 -0
  75. package/plugins/kimi-dsh/dist/mcp/launcher.js +257 -0
  76. package/plugins/kimi-dsh/dist/mcp/operations.js +301 -0
  77. package/plugins/kimi-dsh/dist/mcp/protocol.js +275 -0
  78. package/plugins/kimi-dsh/dist/mcp/server.js +8 -0
  79. package/plugins/kimi-dsh/dist/mcp/stdio.js +78 -0
  80. package/plugins/kimi-dsh/dist/mcp/supervised-child.js +15 -0
  81. package/plugins/kimi-dsh/dist/mcp/tool-result.js +7 -0
  82. package/plugins/kimi-dsh/dist/mcp/workspace.js +34 -0
  83. package/plugins/kimi-dsh/dist/model-command.js +32 -0
  84. package/plugins/kimi-dsh/dist/native-v2-preflight.js +417 -0
  85. package/plugins/kimi-dsh/dist/package.json +1 -0
  86. package/plugins/kimi-dsh/dist/parsing.js +620 -0
  87. package/plugins/kimi-dsh/dist/paths.js +27 -0
  88. package/plugins/kimi-dsh/dist/plugin-data.js +98 -0
  89. package/plugins/kimi-dsh/dist/process-tree.js +479 -0
  90. package/plugins/kimi-dsh/dist/prompt-size.js +18 -0
  91. package/plugins/kimi-dsh/dist/render.js +177 -0
  92. package/plugins/kimi-dsh/dist/rescue-approval.js +865 -0
  93. package/plugins/kimi-dsh/dist/schemas/review-gate-output.js +43 -0
  94. package/plugins/kimi-dsh/dist/session-recovery.js +200 -0
  95. package/plugins/kimi-dsh/dist/session-title.js +293 -0
  96. package/plugins/kimi-dsh/dist/stream-json.js +346 -0
  97. package/plugins/kimi-dsh/dist/swarm-activation.js +834 -0
  98. package/plugins/kimi-dsh/dist/swarm-recovery.js +134 -0
  99. package/plugins/kimi-dsh/dist/types.js +1 -0
  100. package/plugins/kimi-dsh/dist/vendor/shell-quote/LICENSE +24 -0
  101. package/plugins/kimi-dsh/dist/vendor/shell-quote/index.js +1 -0
  102. package/plugins/kimi-dsh/dist/vendor/shell-quote/parse.js +229 -0
  103. package/plugins/kimi-dsh/dist/vendor/smol-toml/LICENSE +24 -0
  104. package/plugins/kimi-dsh/dist/vendor/smol-toml/date.js +156 -0
  105. package/plugins/kimi-dsh/dist/vendor/smol-toml/error.js +67 -0
  106. package/plugins/kimi-dsh/dist/vendor/smol-toml/extract.js +293 -0
  107. package/plugins/kimi-dsh/dist/vendor/smol-toml/parse.js +164 -0
  108. package/plugins/kimi-dsh/dist/vendor/smol-toml/primitive.js +151 -0
  109. package/plugins/kimi-dsh/dist/vendor/smol-toml/struct.js +156 -0
  110. package/plugins/kimi-dsh/dist/vendor/smol-toml/util.js +57 -0
  111. package/plugins/kimi-dsh/dist/version.js +7 -0
  112. package/plugins/kimi-dsh/package.json +78 -0
  113. package/plugins/kimi-dsh/scripts/companion.sh +37 -0
  114. package/plugins/kimi-dsh/scripts/review-gate-hook.sh +32 -0
@@ -0,0 +1,876 @@
1
+ // Subprocess client for `kimi -p --output-format stream-json`.
2
+ //
3
+ // Per-job env (`KIMI_PLUGIN_CC_CMD`) is set on this spawn so the
4
+ // PreToolUse hook can branch per command (review vs rescue vs ask). It is
5
+ // passed via the spawn `env` option, not exported to a shell, so concurrent
6
+ // jobs never share an env block.
7
+ import { spawn } from "node:child_process";
8
+ import { appendFile, mkdir } from "node:fs/promises";
9
+ import path from "node:path";
10
+ import { assertPromptFitsCommandLine } from "./prompt-size.js";
11
+ import { createHookEscapeMonitor } from "./hook-escape.js";
12
+ import { RuntimeError, formatError } from "./errors.js";
13
+ import { assertNoUnsafeExperimentalSelector, assertExecutionPlanMatchesSpawn, } from "./kimi-engine.js";
14
+ import { assertPrefixArgsSafe } from "./kimi-command.js";
15
+ import { resolveKimiHome } from "./kimi-home.js";
16
+ import { assertNativeV2Preflight } from "./native-v2-preflight.js";
17
+ import { parseVersionLine } from "./kimi-version-probe.js";
18
+ import { StreamJsonParser, extractSessionIdFromStderr, } from "./stream-json.js";
19
+ import { collectDescendantIdentities, revalidateProcessIdentities, waitForProcessTreeExit, } from "./process-tree.js";
20
+ /** Bytes of stderr retained for diagnostics on completion. Rolling buffer. */
21
+ const STDERR_TAIL_BYTES = 8192;
22
+ /** Hard cap on awaiting diagnostics log drain before resolving the result. */
23
+ const LOG_DRAIN_TIMEOUT_MS = 250;
24
+ /** Default 1500 ms delay between SIGTERM and SIGKILL after an abort. */
25
+ const DEFAULT_ESCALATION_MS = 1_500;
26
+ /** Maximum time spent confirming POSIX tree exit after SIGKILL dispatch. */
27
+ const POST_KILL_QUIESCENCE_MS = 500;
28
+ /** Hard bound for the one-shot descendant identity snapshot. */
29
+ const DESCENDANT_SNAPSHOT_TIMEOUT_MS = 1_000;
30
+ /** Hard bound for each batched identity revalidation pass. */
31
+ const IDENTITY_REVALIDATION_TIMEOUT_MS = 250;
32
+ const DESCENDANT_SNAPSHOT_TIMEOUT = Symbol("descendant-snapshot-timeout");
33
+ function spawnErrorDetails(error) {
34
+ const cause = error;
35
+ return {
36
+ spawn_code: cause?.code,
37
+ spawn_errno: cause?.errno,
38
+ spawn_message: formatError(error),
39
+ };
40
+ }
41
+ export async function runCliPrompt(opts) {
42
+ // Pre-aborted signal: refuse to spawn at all.
43
+ if (opts.signal?.aborted === true) {
44
+ throw new RuntimeError("CLI_ABORTED", "kimi subprocess request cancelled before spawn", "cli-client.pre-spawn", { details: { command: opts.command } });
45
+ }
46
+ assertExecutionPlanMatchesSpawn(opts.executionPlan, opts.command, opts.prefixArgs ?? [], opts.env);
47
+ if ((opts.commandLabel === "rescue" || opts.commandLabel === "swarm-write") &&
48
+ !opts.trustedWorkspaceRoot) {
49
+ throw new RuntimeError("CLI_TRUSTED_WORKSPACE_ROOT_MISSING", `Refusing ${opts.commandLabel}: write-capable plugin launches require a plugin-owned trusted workspace root.`, "cli-client.pre-spawn", { details: { command_label: opts.commandLabel } });
50
+ }
51
+ // The master experimental flag enables v2-only features (tower, subagent
52
+ // fork) outside the certified no-plan profile. Refuse it at the final
53
+ // boundary too, in case the environment changed after plan creation.
54
+ assertNoUnsafeExperimentalSelector(opts.env, "cli-client.pre-spawn");
55
+ // Validate the ACTUAL prefix argv used for this spawn — not only the
56
+ // env-resolved value — so a forged/corrupt persisted plan cannot smuggle a
57
+ // reserved kimi flag (e.g. `-r <session>`, which would resume a session the
58
+ // v2 preflight never scanned for plan taint).
59
+ assertPrefixArgsSafe(opts.prefixArgs ?? []);
60
+ if (opts.executionPlan.intendedEngine === "native-v2") {
61
+ // Re-run the cheap no-plan preconditions at the final boundary: the
62
+ // operator's config.toml and the resume journal are mutable between plan
63
+ // creation (possibly in another process) and this spawn.
64
+ await assertNativeV2Preflight({
65
+ kimiHome: resolveKimiHome(opts.env, opts.cwd),
66
+ env: opts.env,
67
+ resumeSessionId: opts.resumeSessionId,
68
+ stage: "cli-client.spawn",
69
+ });
70
+ }
71
+ const args = buildArgs(opts);
72
+ assertPromptFitsCommandLine(opts.command, args);
73
+ const env = buildKimiCliEnvironment(opts);
74
+ if (opts.logPath !== undefined) {
75
+ await mkdir(path.dirname(opts.logPath), { recursive: true }).catch(() => {
76
+ // Best-effort; if mkdir fails the appendFile below will error and be
77
+ // swallowed by the log chain (diagnostics are non-blocking).
78
+ });
79
+ }
80
+ let child;
81
+ let spawnedAt;
82
+ try {
83
+ spawnedAt = Date.now();
84
+ child = spawn(opts.command, args, {
85
+ cwd: opts.cwd,
86
+ env,
87
+ stdio: "pipe",
88
+ // POSIX gets a fresh process group so cancellation reaches kimi's
89
+ // descendants; Windows has no negative-pid process-group signaling.
90
+ detached: process.platform !== "win32",
91
+ });
92
+ }
93
+ catch (err) {
94
+ throw new RuntimeError("CLI_SPAWN_FAILED", `Failed to spawn kimi: ${formatError(err)}`, "cli-client.spawn", err instanceof Error
95
+ ? { cause: err, details: { command: opts.command, args, ...spawnErrorDetails(err) } }
96
+ : { details: { command: opts.command, args, ...spawnErrorDetails(err) } });
97
+ }
98
+ // Pre-attach an 'error' listener immediately. Bun (and Node) can emit
99
+ // 'error' asynchronously between spawn returning and our Promise body
100
+ // attaching the real listener — without a handler in place, EventEmitter
101
+ // crashes the process. Capture into an array (not a single slot) so two
102
+ // back-to-back errors don't lose the first; we replay the first one when
103
+ // the real handler installs. The replacement is atomic across two
104
+ // synchronous statements (flag-flip + .on attach), so no async work can
105
+ // interleave between them.
106
+ const earlyErrors = [];
107
+ let earlyErrorHandled = false;
108
+ child.on("error", function earlyErrorListener(err) {
109
+ if (earlyErrorHandled)
110
+ return;
111
+ earlyErrors.push(err);
112
+ });
113
+ const parser = new StreamJsonParser();
114
+ const records = [];
115
+ const malformed = [];
116
+ // Rolling stderr tail — keep only the trailing STDERR_TAIL_BYTES so a
117
+ // long-running kimi process emitting megabytes of stderr (tool.progress,
118
+ // thinking deltas) doesn't grow our RSS unboundedly.
119
+ let stderrTail = "";
120
+ let announcedSessionId;
121
+ let announcedGoalSummary;
122
+ let announcedCliError;
123
+ let announcedSystemVersion;
124
+ const hookMonitor = createHookEscapeMonitor({ commandLabel: opts.commandLabel,
125
+ operationKind: opts.executionPlan.operationKind, trustedWorkspaceRoot: opts.trustedWorkspaceRoot, cwd: opts.cwd });
126
+ const hookEscapes = [];
127
+ const noteHookEscape = (escape) => {
128
+ hookEscapes.push(escape);
129
+ appendLogLine({ event: "hook_escape", ...escape, command_label: opts.commandLabel,
130
+ operation_kind: opts.executionPlan.operationKind });
131
+ };
132
+ let engineMismatchDetected = false;
133
+ let handleEngineMismatch;
134
+ const plannedEngine = opts.executionPlan.intendedEngine;
135
+ const plannedVersion = opts.executionPlan.kimiVersion === null
136
+ ? null
137
+ : (parseVersionLine(opts.executionPlan.kimiVersion)?.raw ?? opts.executionPlan.kimiVersion);
138
+ let logChain = Promise.resolve();
139
+ const appendLogLine = (payload) => {
140
+ if (opts.logPath === undefined)
141
+ return;
142
+ const line = `${JSON.stringify({ ts: new Date().toISOString(), ...payload })}\n`;
143
+ logChain = logChain.then(() => appendFile(opts.logPath, line, "utf8").catch(() => {
144
+ // Diagnostics best-effort; never crash on log failure.
145
+ }));
146
+ };
147
+ appendLogLine({
148
+ event: "spawn",
149
+ command: opts.command,
150
+ args,
151
+ cwd: opts.cwd,
152
+ command_label: opts.commandLabel ?? null,
153
+ operation_kind: opts.executionPlan.operationKind,
154
+ intended_engine: opts.executionPlan.intendedEngine,
155
+ kimi_version: opts.executionPlan.kimiVersion,
156
+ plan_certification: opts.executionPlan.certification,
157
+ safety_profile: opts.executionPlan.safetyProfile,
158
+ swarm_max_concurrency: opts.swarmMaxConcurrency ?? null,
159
+ legacy_v1_forced: env.KIMI_CODE_LEGACY_FLAG === KIMI_LEGACY_FORCED_VALUE,
160
+ });
161
+ const invokeOnRecord = (record) => {
162
+ if (opts.onRecord === undefined)
163
+ return;
164
+ try {
165
+ opts.onRecord(record);
166
+ }
167
+ catch {
168
+ // Caller-supplied callback must never destabilize the parse loop.
169
+ }
170
+ };
171
+ // A native-v2 plan must see agent-core-v2's pre-tool system.version marker
172
+ // before any consumer-facing output; a legacy plan must never see it.
173
+ const requireMarkerBeforeOutput = (what) => {
174
+ if (plannedEngine !== "native-v2" || announcedSystemVersion !== undefined)
175
+ return false;
176
+ handleEngineMismatch?.({
177
+ observedEngine: null,
178
+ systemVersion: undefined,
179
+ reason: `${what} arrived before the native-v2 system.version marker; the engine that started cannot be established.`,
180
+ });
181
+ return true;
182
+ };
183
+ const consumeOutcomes = (outcomes) => {
184
+ for (const outcome of outcomes) {
185
+ if (engineMismatchDetected)
186
+ return;
187
+ if (outcome.cliError !== undefined) {
188
+ if (requireMarkerBeforeOutput("a CLI error record"))
189
+ return;
190
+ announcedCliError ??= outcome.cliError;
191
+ appendLogLine({ event: "cli_error", ...outcome.cliError });
192
+ continue;
193
+ }
194
+ if (outcome.unknownRecord !== undefined) {
195
+ // Unknown roles are diagnostic-only and excluded from consumer records;
196
+ // native-v2 still refuses any output before its marker.
197
+ if (requireMarkerBeforeOutput(`an unknown-role (${outcome.unknownRecord.role}) record`)) {
198
+ return;
199
+ }
200
+ appendLogLine({
201
+ event: "unknown_record",
202
+ role: outcome.unknownRecord.role,
203
+ });
204
+ continue;
205
+ }
206
+ if (outcome.goalSummary !== undefined) {
207
+ if (requireMarkerBeforeOutput("goal.summary"))
208
+ return;
209
+ // Out-of-band metadata; first-announce-wins, excluded from consumer records.
210
+ if (announcedGoalSummary === undefined) {
211
+ announcedGoalSummary = outcome.goalSummary;
212
+ appendLogLine({
213
+ event: "goal_summary",
214
+ goal_id: outcome.goalSummary.goalId,
215
+ status: outcome.goalSummary.status,
216
+ turns_used: outcome.goalSummary.turnsUsed,
217
+ tokens_used: outcome.goalSummary.tokensUsed,
218
+ wall_clock_ms: outcome.goalSummary.wallClockMs,
219
+ });
220
+ }
221
+ continue;
222
+ }
223
+ if (outcome.record !== undefined) {
224
+ // Meta records are out-of-band wrapper metadata, never consumer-facing
225
+ // assistant/tool records. Capture session.resume_hint (first-announce
226
+ // wins, mirroring the stderr capture below for kimi 0.1.x), and skip
227
+ // every recognized meta type, including 0.23.5's turn.step.retrying and
228
+ // agent-core-v2's system.version. This keeps records[] and onRecord
229
+ // stable while preserving the resume hint as the primary session-id
230
+ // source under kimi 0.2.0+.
231
+ if (outcome.record.role === "meta") {
232
+ // Contract: on a native-v2 plan the system.version marker is the
233
+ // FIRST stream-json line. Any other meta (e.g. session.resume_hint)
234
+ // arriving before it violates the ordering and means the started
235
+ // engine cannot be established — refuse before pinning a session id.
236
+ if (outcome.record.type !== "system.version" &&
237
+ requireMarkerBeforeOutput(`a ${outcome.record.type} record`)) {
238
+ return;
239
+ }
240
+ if (outcome.record.type === "session.resume_hint" &&
241
+ announcedSessionId === undefined) {
242
+ announcedSessionId = outcome.record.sessionId;
243
+ appendLogLine({
244
+ event: "session_announce",
245
+ source: "stream-json.meta",
246
+ session_id: outcome.record.sessionId,
247
+ });
248
+ }
249
+ else if (outcome.record.type === "system.version" &&
250
+ announcedSystemVersion === undefined) {
251
+ announcedSystemVersion = outcome.record.version;
252
+ appendLogLine({
253
+ event: "system_version",
254
+ source: "stream-json.meta",
255
+ version: outcome.record.version,
256
+ });
257
+ if (plannedEngine !== "native-v2") {
258
+ handleEngineMismatch?.({
259
+ observedEngine: "native-v2",
260
+ systemVersion: outcome.record.version,
261
+ reason: `the pre-tool system.version marker proves native-v2 ${outcome.record.version} started.`,
262
+ });
263
+ return;
264
+ }
265
+ // The marker's `version` is a structured field, not a CLI banner:
266
+ // require a single well-formed semver token. A multi-line or
267
+ // otherwise decorated value must not be normalized into a certified
268
+ // one by taking its first line.
269
+ const observedVersion = parseExactVersionToken(outcome.record.version);
270
+ if (observedVersion === undefined) {
271
+ handleEngineMismatch?.({
272
+ observedEngine: "native-v2",
273
+ systemVersion: outcome.record.version,
274
+ reason: `the system.version marker carries a malformed version value ${JSON.stringify(outcome.record.version)}; the started engine's version cannot be established.`,
275
+ });
276
+ return;
277
+ }
278
+ if (plannedVersion !== null && observedVersion !== plannedVersion) {
279
+ handleEngineMismatch?.({
280
+ observedEngine: "native-v2",
281
+ systemVersion: outcome.record.version,
282
+ reason: `the system.version marker reports ${outcome.record.version}, but the plan certified ${plannedVersion}.`,
283
+ });
284
+ return;
285
+ }
286
+ }
287
+ continue;
288
+ }
289
+ if (requireMarkerBeforeOutput(`a ${outcome.record.role} record`))
290
+ return;
291
+ const escape = hookMonitor.observe(outcome.record);
292
+ if (escape)
293
+ noteHookEscape(escape);
294
+ records.push(outcome.record);
295
+ appendLogLine({ event: "record", record: outcome.record });
296
+ invokeOnRecord(outcome.record);
297
+ }
298
+ else if (outcome.malformedLine !== undefined) {
299
+ // Diagnostics-only in general, but on a native-v2 plan a line that is
300
+ // not the marker arriving first still violates marker-first: the
301
+ // engine that produced it cannot be established.
302
+ if (requireMarkerBeforeOutput("a malformed stream-json line"))
303
+ return;
304
+ const entry = {
305
+ line: truncateChars(outcome.malformedLine, 200),
306
+ reason: outcome.malformedReason ?? "unknown",
307
+ };
308
+ malformed.push(entry);
309
+ appendLogLine({ event: "malformed", ...entry });
310
+ }
311
+ }
312
+ };
313
+ const errorDetails = () => ({
314
+ command: opts.command,
315
+ args,
316
+ command_label: opts.commandLabel ?? null,
317
+ });
318
+ return await new Promise(async (resolve, reject) => {
319
+ let aborted = false;
320
+ let cancellationTeardown;
321
+ let settlementStarted = false;
322
+ const settle = (kind, payload) => {
323
+ if (settlementStarted)
324
+ return;
325
+ // Denied calls still without a result (a cancel/budget race, an abnormal
326
+ // exit, or a turn that ended without the denial the hook owes) are reported
327
+ // alongside the real outcome, never instead of it.
328
+ for (const escape of hookMonitor.settle())
329
+ noteHookEscape(escape);
330
+ if (kind === "resolve" && hookEscapes.length > 0)
331
+ payload = { ...payload, hookEscapes: [...hookEscapes] };
332
+ settlementStarted = true;
333
+ opts.signal?.removeEventListener("abort", onAbort);
334
+ // Abort teardown can outlive the direct child's close event while a
335
+ // detached descendant is still being enumerated or escalated. Never
336
+ // expose a result/error until that one owned teardown path completes.
337
+ void (cancellationTeardown ?? Promise.resolve({ ok: true })).then((teardown) => {
338
+ if (!teardown.ok) {
339
+ appendLogLine({
340
+ event: "cancellation_teardown_error",
341
+ message: formatError(teardown.error),
342
+ });
343
+ reject(new RuntimeError("CLI_CANCELLATION_TEARDOWN_FAILED", `kimi cancellation teardown failed: ${formatError(teardown.error)}`, "cli-client.cancellation", teardown.error instanceof Error
344
+ ? { cause: teardown.error, details: errorDetails() }
345
+ : { details: errorDetails() }));
346
+ return;
347
+ }
348
+ if (kind === "resolve")
349
+ resolve({ ...payload, settledAt: Date.now() });
350
+ else
351
+ reject(payload);
352
+ });
353
+ };
354
+ const escalationMs = opts.escalationMs ?? DEFAULT_ESCALATION_MS;
355
+ const identityReader = opts.processIdentityReader;
356
+ let processExited = false;
357
+ child.once("exit", () => {
358
+ processExited = true;
359
+ });
360
+ const captureDescendantIdentities = async (childPid) => {
361
+ let snapshot;
362
+ try {
363
+ const collected = await raceWithValueTimeout((opts.descendantIdentityCollector ?? collectDescendantIdentities)(childPid), DESCENDANT_SNAPSHOT_TIMEOUT_MS, DESCENDANT_SNAPSHOT_TIMEOUT);
364
+ if (collected === DESCENDANT_SNAPSHOT_TIMEOUT) {
365
+ appendLogLine({
366
+ event: "descendant_collection_timeout",
367
+ timeout_ms: DESCENDANT_SNAPSHOT_TIMEOUT_MS,
368
+ });
369
+ return [];
370
+ }
371
+ snapshot = collected;
372
+ }
373
+ catch (err) {
374
+ appendLogLine({
375
+ event: "descendant_collection_error",
376
+ message: formatError(err),
377
+ });
378
+ return [];
379
+ }
380
+ const identities = [];
381
+ const seen = new Set();
382
+ for (const identity of snapshot) {
383
+ const key = `${identity.pid}:${identity.startToken}:${identity.processGroupId}`;
384
+ if (Number.isInteger(identity.pid) &&
385
+ identity.pid > 0 &&
386
+ identity.pid !== childPid &&
387
+ Number.isInteger(identity.processGroupId) &&
388
+ identity.processGroupId > 0 &&
389
+ identity.startToken.length > 0 &&
390
+ !seen.has(key)) {
391
+ seen.add(key);
392
+ identities.push({ ...identity });
393
+ }
394
+ }
395
+ return identities;
396
+ };
397
+ const trySignalPid = (pid, signal) => {
398
+ try {
399
+ process.kill(pid, signal);
400
+ }
401
+ catch (err) {
402
+ const ignored = isErrnoException(err, "ESRCH") || isErrnoException(err, "EPERM");
403
+ void ignored;
404
+ }
405
+ };
406
+ const signalCapturedTree = async (childPid, identities, signal) => {
407
+ // The live ChildProcess anchors its detached group. Signal the group
408
+ // before the direct child can exit and make that numeric PGID stale.
409
+ if (!processExited) {
410
+ trySignalPid(-childPid, signal);
411
+ try {
412
+ child.kill(signal);
413
+ }
414
+ catch {
415
+ // Best-effort direct-child signaling; ChildProcess owns this pid.
416
+ }
417
+ }
418
+ // One bounded pass replaces up to 512 sequential ps calls. A pid whose
419
+ // start token or PGID changed belongs to another process and is skipped.
420
+ const pidValidation = await revalidateProcessIdentities(identities, {
421
+ identityReader,
422
+ timeoutMs: IDENTITY_REVALIDATION_TIMEOUT_MS,
423
+ });
424
+ for (const current of pidValidation.identities) {
425
+ trySignalPid(current.pid, signal);
426
+ }
427
+ // Revalidate again immediately before signaling non-root groups. Never
428
+ // send to an unanchored numeric PGID left over from collection.
429
+ const groupValidation = await revalidateProcessIdentities(identities, {
430
+ identityReader,
431
+ timeoutMs: IDENTITY_REVALIDATION_TIMEOUT_MS,
432
+ });
433
+ const activeGroupIds = new Set(groupValidation.identities.map((identity) => identity.processGroupId));
434
+ activeGroupIds.delete(childPid);
435
+ for (const processGroupId of activeGroupIds) {
436
+ trySignalPid(-processGroupId, signal);
437
+ }
438
+ };
439
+ const teardownChildTree = async () => {
440
+ await opts.cancellationTeardownHook?.();
441
+ const childPid = child.pid;
442
+ if (childPid === undefined || processExited)
443
+ return;
444
+ const finiteEscalation = Number.isFinite(escalationMs);
445
+ if (process.platform === "win32") {
446
+ // Preserve the documented win32 behavior: only the direct child is
447
+ // managed; descendant enumeration/reaping remains unsupported.
448
+ try {
449
+ child.kill("SIGTERM");
450
+ }
451
+ catch {
452
+ // Best-effort.
453
+ }
454
+ const exited = await waitForProcessTreeExit([], escalationMs, {
455
+ isRootAlive: () => !processExited,
456
+ });
457
+ if (!exited && finiteEscalation) {
458
+ try {
459
+ child.kill("SIGKILL");
460
+ }
461
+ catch {
462
+ // Best-effort.
463
+ }
464
+ }
465
+ return;
466
+ }
467
+ const identities = await captureDescendantIdentities(childPid);
468
+ await signalCapturedTree(childPid, identities, "SIGTERM");
469
+ // The grace interval starts only after SIGTERM has reached every
470
+ // revalidated target. Slow process enumeration never consumes the grace.
471
+ const exited = await waitForProcessTreeExit(identities, escalationMs, {
472
+ identityReader,
473
+ isRootAlive: () => !processExited,
474
+ });
475
+ if (!exited && finiteEscalation) {
476
+ await signalCapturedTree(childPid, identities, "SIGKILL");
477
+ const quiescent = await waitForProcessTreeExit(identities, POST_KILL_QUIESCENCE_MS, {
478
+ identityReader,
479
+ isRootAlive: () => !processExited,
480
+ });
481
+ if (!quiescent) {
482
+ appendLogLine({
483
+ event: "post_kill_quiescence_timeout",
484
+ timeout_ms: POST_KILL_QUIESCENCE_MS,
485
+ descendant_count: identities.length,
486
+ });
487
+ }
488
+ }
489
+ };
490
+ const stopForPolicyFailure = (error) => {
491
+ engineMismatchDetected = true; // Stops all further consumer output.
492
+ cancellationTeardown ??= teardownChildTree().then(() => ({ ok: true }), (error) => ({ ok: false, error }));
493
+ settle("reject", error);
494
+ };
495
+ let engineMismatchError;
496
+ handleEngineMismatch = (mismatch) => {
497
+ if (engineMismatchError !== undefined)
498
+ return;
499
+ engineMismatchDetected = true;
500
+ engineMismatchError = new RuntimeError("CLI_ENGINE_PROVENANCE_MISMATCH", `Refusing kimi output: execution plan required ${plannedEngine}, but ${mismatch.reason}`, "cli-client.engine-provenance", {
501
+ details: {
502
+ operation_kind: opts.executionPlan.operationKind,
503
+ intended_engine: plannedEngine,
504
+ observed_engine: mismatch.observedEngine,
505
+ planned_kimi_version: opts.executionPlan.kimiVersion,
506
+ system_version: mismatch.systemVersion ?? null,
507
+ },
508
+ });
509
+ appendLogLine({
510
+ event: "engine_provenance_mismatch",
511
+ intended_engine: plannedEngine,
512
+ observed_engine: mismatch.observedEngine,
513
+ system_version: mismatch.systemVersion ?? null,
514
+ reason: mismatch.reason,
515
+ });
516
+ stopForPolicyFailure(engineMismatchError);
517
+ };
518
+ const onAbort = () => {
519
+ if (aborted)
520
+ return;
521
+ aborted = true;
522
+ cancellationTeardown = teardownChildTree().then(() => ({ ok: true }), (error) => ({ ok: false, error }));
523
+ void cancellationTeardown.then((outcome) => {
524
+ if (outcome.ok)
525
+ return;
526
+ // A bug in teardown must neither strand the outer promise nor leave the
527
+ // owned root running. The controlled child anchors this emergency kill.
528
+ const childPid = child.pid;
529
+ if (childPid !== undefined && !processExited) {
530
+ if (process.platform !== "win32")
531
+ trySignalPid(-childPid, "SIGKILL");
532
+ try {
533
+ child.kill("SIGKILL");
534
+ }
535
+ catch {
536
+ // Best-effort; settlement still surfaces the teardown failure.
537
+ }
538
+ }
539
+ settle("reject", new RuntimeError("CLI_CANCELLATION_TEARDOWN_FAILED", "kimi cancellation teardown failed", "cli-client.cancellation"));
540
+ });
541
+ };
542
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
543
+ // Abort may occur during any pre-spawn await, and addEventListener does not
544
+ // replay an already-aborted signal. Re-check after attaching the listener so
545
+ // the spawned child cannot escape cancellation teardown.
546
+ if (opts.signal?.aborted === true) {
547
+ onAbort();
548
+ }
549
+ if (child.pid !== undefined && opts.onSpawn) {
550
+ try {
551
+ await opts.onSpawn(child.pid);
552
+ }
553
+ catch (error) {
554
+ onAbort();
555
+ settle("reject", new RuntimeError("CLI_SPAWN_CALLBACK_FAILED", `Failed to record the spawned kimi process: ${formatError(error)}`, "cli-client.spawn", error instanceof Error ? { cause: error, details: errorDetails() } : { details: errorDetails() }));
556
+ return;
557
+ }
558
+ }
559
+ earlyErrorHandled = true;
560
+ child.on("error", (err) => {
561
+ appendLogLine({ event: "process_error", message: formatError(err) });
562
+ settle("reject", new RuntimeError("CLI_PROCESS_ERROR", `kimi subprocess error: ${formatError(err)}`, "cli-client.process", err instanceof Error
563
+ ? { cause: err, details: { ...errorDetails(), ...spawnErrorDetails(err) } }
564
+ : { details: { ...errorDetails(), ...spawnErrorDetails(err) } }));
565
+ });
566
+ if (earlyErrors.length > 0) {
567
+ const captured = earlyErrors[0];
568
+ queueMicrotask(() => {
569
+ appendLogLine({
570
+ event: "process_error",
571
+ message: formatError(captured),
572
+ early: true,
573
+ additional_count: earlyErrors.length - 1,
574
+ });
575
+ settle("reject", new RuntimeError("CLI_PROCESS_ERROR", `kimi subprocess error: ${formatError(captured)}`, "cli-client.process", { cause: captured, details: { ...errorDetails(), ...spawnErrorDetails(captured) } }));
576
+ });
577
+ }
578
+ child.on("close", async (exitCode, signal) => {
579
+ consumeOutcomes(parser.flush());
580
+ if (!engineMismatchDetected &&
581
+ exitCode === 0 &&
582
+ !aborted &&
583
+ requireMarkerBeforeOutput("a clean exit")) {
584
+ return;
585
+ }
586
+ // The stderr resume-hint channel is the kimi 0.1.x fallback and exists
587
+ // only for legacy-v1 plans; a native-v2 plan pins a session id from the
588
+ // stdout meta record alone (after the marker), so stderr can never pin
589
+ // an id ahead of, or different from, the provenance-checked stream.
590
+ const sessionId = announcedSessionId ??
591
+ (plannedEngine === "legacy-v1" ? extractSessionIdFromStderr(stderrTail) : undefined);
592
+ appendLogLine({
593
+ event: "exit",
594
+ exit_code: exitCode,
595
+ signal,
596
+ session_id: sessionId,
597
+ malformed_count: malformed.length,
598
+ record_count: records.length,
599
+ aborted,
600
+ });
601
+ // Cap the log drain wait so a wedged disk can't block result delivery.
602
+ // The result itself is in memory; diagnostics are best-effort.
603
+ await raceWithTimeout(logChain, LOG_DRAIN_TIMEOUT_MS);
604
+ settle("resolve", {
605
+ spawnedAt,
606
+ settledAt: 0,
607
+ sessionId,
608
+ goalSummary: announcedGoalSummary,
609
+ cliError: announcedCliError,
610
+ systemVersion: announcedSystemVersion,
611
+ observedEngine: announcedSystemVersion !== undefined
612
+ ? "native-v2"
613
+ : plannedEngine === "legacy-v1" &&
614
+ (exitCode === 0 ||
615
+ records.length > 0 ||
616
+ sessionId !== undefined ||
617
+ announcedGoalSummary !== undefined)
618
+ ? "legacy-v1"
619
+ : null,
620
+ records,
621
+ malformed,
622
+ stderrTail,
623
+ exitCode: exitCode ?? -1,
624
+ signal,
625
+ aborted,
626
+ });
627
+ });
628
+ // Stdin: kimi -p reads from argv, not stdin. End it immediately so the
629
+ // child never blocks. Stream method calls on a failed-spawn child can
630
+ // throw synchronously — guard so the 'error' event still wins.
631
+ try {
632
+ child.stdout.setEncoding("utf8");
633
+ child.stderr.setEncoding("utf8");
634
+ child.stdout.on("data", (chunk) => {
635
+ consumeOutcomes(parser.push(chunk));
636
+ });
637
+ child.stderr.on("data", (chunk) => {
638
+ // First-announce-wins across both channels: once a session id is
639
+ // pinned (from the stdout meta record or an earlier stderr line),
640
+ // never let a later stderr line overwrite it. The stderr channel is
641
+ // the 0.1.x fallback and is consulted ONLY for legacy-v1 plans — on a
642
+ // native-v2 plan a stderr line could otherwise pin a session id before
643
+ // (or different from) the provenance-checked stdout stream.
644
+ if (plannedEngine === "legacy-v1" && announcedSessionId === undefined) {
645
+ const nextSessionId = extractSessionIdFromStderr(stderrTail + chunk);
646
+ if (nextSessionId !== undefined) {
647
+ announcedSessionId = nextSessionId;
648
+ }
649
+ }
650
+ stderrTail = appendToTail(stderrTail, chunk, STDERR_TAIL_BYTES);
651
+ });
652
+ child.stdin.end();
653
+ }
654
+ catch (err) {
655
+ settle("reject", new RuntimeError("CLI_PROCESS_ERROR", `kimi subprocess stream setup failed: ${formatError(err)}`, "cli-client.streams", err instanceof Error
656
+ ? { cause: err, details: { ...errorDetails(), ...spawnErrorDetails(err) } }
657
+ : { details: { ...errorDetails(), ...spawnErrorDetails(err) } }));
658
+ }
659
+ });
660
+ }
661
+ /**
662
+ * Helper for callers that require a session id (replay-capable commands).
663
+ * Throws CLI_NO_SESSION_ID if the result lacks one, embedding the command
664
+ * label and exit code in details so the failure is actionable in logs.
665
+ *
666
+ * Use this AT the call site rather than baking the requirement into
667
+ * runCliPrompt — some callers (setup probes, smoke tests) are fine with
668
+ * an undefined sessionId.
669
+ */
670
+ export function requireSessionId(result, context) {
671
+ if (result.sessionId !== undefined && result.sessionId.length > 0) {
672
+ return result.sessionId;
673
+ }
674
+ throw new RuntimeError("CLI_NO_SESSION_ID", "kimi subprocess completed without announcing a session id; replay and resume will be unavailable", "cli-client.session-id", {
675
+ details: {
676
+ command_label: context.commandLabel ?? null,
677
+ exit_code: result.exitCode,
678
+ signal: result.signal,
679
+ aborted: result.aborted,
680
+ stderr_tail_len: result.stderrTail.length,
681
+ },
682
+ });
683
+ }
684
+ /** Settled timeout data; stream records are deliberately non-serializable.
685
+ * Without a partial result (teardown rejection), aborted means abort was issued,
686
+ * not that the process was observed aborting. */
687
+ export class ResponseTimeoutError extends RuntimeError {
688
+ constructor(budgetMs, stage, commandLabel, partialResult, cause) {
689
+ super("RESPONSE_TIMEOUT", `${stage} timed out after ${budgetMs}ms.`, stage, {
690
+ ...(cause instanceof Error ? { cause } : {}),
691
+ details: {
692
+ budget_ms: budgetMs,
693
+ command_label: commandLabel ?? null,
694
+ record_count: partialResult?.records.length ?? 0,
695
+ partial_output_chars: partialResult?.records.reduce((count, record) => count + (record.role === "assistant" && typeof record.content === "string" ? record.content.length : 0), 0) ?? 0,
696
+ aborted: partialResult?.aborted ?? true,
697
+ ...(partialResult ? { settled_post_abort: true } : {}),
698
+ ...(partialResult?.hookEscapes?.length ? { hook_escapes: partialResult.hookEscapes } : {}),
699
+ },
700
+ });
701
+ this.name = "ResponseTimeoutError";
702
+ // Never serialize stream records into job error details or error envelopes.
703
+ Object.defineProperty(this, "partialResult", { value: partialResult, enumerable: false });
704
+ }
705
+ }
706
+ /**
707
+ * Run `kimi -p` under a budget that also tells the subprocess to die
708
+ * when the budget expires.
709
+ *
710
+ * Why this exists separately from `withTimeout`:
711
+ *
712
+ * The generic `withTimeout` (runtime/kimi-timeouts.ts) only rejects
713
+ * the outer Promise — it has no handle on the kimi child process.
714
+ *
715
+ * Behavior:
716
+ *
717
+ * - Owns an internal AbortController. If the caller passes
718
+ * `opts.signal`, both signals are linked — abort on either side
719
+ * aborts the internal controller.
720
+ * - When the budget expires, abort the internal controller, await the
721
+ * subprocess/tree teardown, then reject with
722
+ * `RuntimeError("RESPONSE_TIMEOUT", …)` matching the legacy
723
+ * `withTimeout` code shape.
724
+ * - A clean non-aborted exit-0 result wins same-tick budget expiry.
725
+ * Otherwise a settled result on timeout is carried as partial output.
726
+ */
727
+ export async function runCliPromptWithBudget(opts, budgetMs, stage) {
728
+ const controller = new AbortController();
729
+ if (opts.signal?.aborted === true) {
730
+ controller.abort();
731
+ }
732
+ const onParentAbort = () => controller.abort();
733
+ opts.signal?.addEventListener("abort", onParentAbort, { once: true });
734
+ let timeoutFired = false;
735
+ let timer;
736
+ timer = setTimeout(() => {
737
+ timeoutFired = true;
738
+ controller.abort();
739
+ }, budgetMs);
740
+ timer.unref();
741
+ const timeoutError = (partialResult, cause) => new ResponseTimeoutError(budgetMs, stage, opts.commandLabel, partialResult, cause);
742
+ try {
743
+ let result;
744
+ try {
745
+ // Deliberately do not race this promise against an early timeout
746
+ // rejection. The timer aborts it, and runCliPrompt owns the complete
747
+ // child/descendant teardown before it can settle.
748
+ result = await runCliPrompt({ ...opts, signal: controller.signal });
749
+ }
750
+ catch (error) {
751
+ if (timeoutFired)
752
+ throw timeoutError(undefined, error);
753
+ throw error;
754
+ }
755
+ if (timeoutFired && (result.aborted || result.exitCode !== 0)) {
756
+ // A clean, non-aborted result won the settle/timeout boundary and is success.
757
+ // Budget aborts normally resolve with records, after owned teardown settles.
758
+ throw timeoutError(result);
759
+ }
760
+ return result;
761
+ }
762
+ finally {
763
+ if (timer !== undefined)
764
+ clearTimeout(timer);
765
+ opts.signal?.removeEventListener("abort", onParentAbort);
766
+ }
767
+ }
768
+ /**
769
+ * Parse a structured version FIELD (the system.version marker), not a CLI
770
+ * banner: the whole value must be one semver token (optional leading `v`,
771
+ * optional pre-release/build suffix) with no surrounding whitespace or extra
772
+ * lines. Returns the normalized `raw` form, or undefined when malformed.
773
+ */
774
+ function parseExactVersionToken(value) {
775
+ if (/\s/.test(value) || value.length === 0)
776
+ return undefined;
777
+ return parseVersionLine(value)?.raw;
778
+ }
779
+ const KIMI_LEGACY_ENV = "KIMI_CODE_LEGACY_FLAG";
780
+ const KIMI_LEGACY_FORCED_VALUE = "1";
781
+ function buildArgs(opts) {
782
+ const args = [...(opts.prefixArgs ?? [])];
783
+ args.push("--output-format", "stream-json");
784
+ if (opts.resumeSessionId !== undefined) {
785
+ args.push("-r", opts.resumeSessionId);
786
+ }
787
+ if (opts.model !== undefined) {
788
+ args.push("-m", opts.model);
789
+ }
790
+ if (opts.skillsDirs !== undefined) {
791
+ for (const dir of opts.skillsDirs) {
792
+ args.push("--skills-dir", dir);
793
+ }
794
+ }
795
+ // -p accepts the prompt as the option value. We pass it as a separate argv
796
+ // entry so prompts with leading dashes or whitespace are not interpreted as
797
+ // additional flags.
798
+ args.push("-p", opts.prompt);
799
+ return args;
800
+ }
801
+ export function buildKimiCliEnvironment(opts) {
802
+ const env = { ...opts.env };
803
+ if (opts.thinkingEffort !== undefined)
804
+ env.KIMI_MODEL_THINKING_EFFORT = opts.thinkingEffort;
805
+ // Ambient workspace trust starts project MCP stdio servers outside PreToolUse.
806
+ delete env.KIMI_CODE_TRUST_WORKSPACE;
807
+ // Legacy-v1 plans use this flag on releases whose unflagged -p default can
808
+ // route to native-v2. Always overwrite ambient false values; older releases
809
+ // ignore it. The engine choice stays plugin-owned, while certified native-v2
810
+ // plans use the no-plan construction rather than a legacy pin.
811
+ if (opts.executionPlan.intendedEngine === "legacy-v1") {
812
+ env[KIMI_LEGACY_ENV] = KIMI_LEGACY_FORCED_VALUE;
813
+ }
814
+ // The update preflight runs on the `-p` path too; a plugin spawn must never
815
+ // swap the certified binary underneath the execution plan.
816
+ env.KIMI_CODE_NO_AUTO_UPDATE = "1";
817
+ // Plugin-owned web lifecycles must not inherit ambient idle-session reclamation.
818
+ env.KIMI_CODE_SESSION_IDLE_RECLAIM_MS = "0";
819
+ // Rescue and pursue share the write label. Only the immutable execution
820
+ // plan can grant pursue's narrow goal-metadata access; never trust ambient
821
+ // state inherited from another plugin job.
822
+ env.KIMI_PLUGIN_CC_OPERATION = opts.executionPlan.operationKind;
823
+ env.KIMI_CODE_HOME = resolveKimiHome(opts.env, opts.cwd);
824
+ if (opts.commandLabel !== undefined) {
825
+ env.KIMI_PLUGIN_CC_CMD = opts.commandLabel;
826
+ }
827
+ if (opts.swarmMaxConcurrency !== undefined) {
828
+ env.KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY = String(opts.swarmMaxConcurrency);
829
+ }
830
+ if (opts.trustedWorkspaceRoot !== undefined) {
831
+ // Trusted allowlist root for write-capable PreToolUse cases. Forge-proof
832
+ // (env on the spawned process; the model cannot alter it). See field doc.
833
+ env.KIMI_PLUGIN_CC_WORKSPACE_ROOT = opts.trustedWorkspaceRoot;
834
+ }
835
+ return env;
836
+ }
837
+ function appendToTail(tail, chunk, maxBytes) {
838
+ const combined = tail + chunk;
839
+ if (combined.length <= maxBytes)
840
+ return combined;
841
+ return combined.slice(combined.length - maxBytes);
842
+ }
843
+ function truncateChars(value, maxChars) {
844
+ return value.length <= maxChars ? value : value.slice(0, maxChars);
845
+ }
846
+ function isErrnoException(err, code) {
847
+ return typeof err === "object" && err !== null && "code" in err && err.code === code;
848
+ }
849
+ function raceWithTimeout(promise, timeoutMs) {
850
+ let timer;
851
+ const timeoutPromise = new Promise((resolve) => {
852
+ timer = setTimeout(resolve, timeoutMs);
853
+ });
854
+ return Promise.race([
855
+ promise.then(() => { }, () => { }),
856
+ timeoutPromise,
857
+ ]).finally(() => {
858
+ if (timer !== undefined)
859
+ clearTimeout(timer);
860
+ });
861
+ }
862
+ async function raceWithValueTimeout(promise, timeoutMs, timeoutValue) {
863
+ let timer;
864
+ try {
865
+ return await Promise.race([
866
+ promise,
867
+ new Promise((resolve) => {
868
+ timer = setTimeout(() => resolve(timeoutValue), Math.max(0, timeoutMs));
869
+ }),
870
+ ]);
871
+ }
872
+ finally {
873
+ if (timer !== undefined)
874
+ clearTimeout(timer);
875
+ }
876
+ }