kimi-plugin-cc 2.4.5 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +42 -26
  5. package/dist/dsh/companion.js +174 -0
  6. package/dist/dsh/index.js +156 -0
  7. package/dist/hooks/install-paths.js +2 -2
  8. package/dist/version.js +1 -1
  9. package/docs/models.md +5 -1
  10. package/docs/registry-releases.md +20 -17
  11. package/docs/safety.md +22 -9
  12. package/docs/upstream-2.4.1-certification.md +6 -0
  13. package/package.json +40 -6
  14. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  15. package/plugins/kimi-codex/dist/dsh/companion.js +174 -0
  16. package/plugins/kimi-codex/dist/dsh/index.js +156 -0
  17. package/plugins/kimi-codex/dist/hooks/install-paths.js +2 -2
  18. package/plugins/kimi-codex/dist/version.js +1 -1
  19. package/plugins/kimi-dsh/LICENSE +201 -0
  20. package/plugins/kimi-dsh/README.md +80 -0
  21. package/plugins/kimi-dsh/cordis.patch.yml +45 -0
  22. package/plugins/kimi-dsh/dist/background-spawn.js +216 -0
  23. package/plugins/kimi-dsh/dist/cli-cancellation.js +43 -0
  24. package/plugins/kimi-dsh/dist/cli-client.js +876 -0
  25. package/plugins/kimi-dsh/dist/commands/ask.js +336 -0
  26. package/plugins/kimi-dsh/dist/commands/cancel.js +216 -0
  27. package/plugins/kimi-dsh/dist/commands/cli-helpers.js +201 -0
  28. package/plugins/kimi-dsh/dist/commands/models.js +140 -0
  29. package/plugins/kimi-dsh/dist/commands/not-implemented.js +4 -0
  30. package/plugins/kimi-dsh/dist/commands/pursue.js +307 -0
  31. package/plugins/kimi-dsh/dist/commands/recover-swarm.js +135 -0
  32. package/plugins/kimi-dsh/dist/commands/registry.js +74 -0
  33. package/plugins/kimi-dsh/dist/commands/repair-sessions.js +160 -0
  34. package/plugins/kimi-dsh/dist/commands/replay.js +124 -0
  35. package/plugins/kimi-dsh/dist/commands/rescue.js +347 -0
  36. package/plugins/kimi-dsh/dist/commands/result.js +66 -0
  37. package/plugins/kimi-dsh/dist/commands/review-gate.js +390 -0
  38. package/plugins/kimi-dsh/dist/commands/review.js +220 -0
  39. package/plugins/kimi-dsh/dist/commands/setup.js +1047 -0
  40. package/plugins/kimi-dsh/dist/commands/status.js +27 -0
  41. package/plugins/kimi-dsh/dist/commands/swarm.js +754 -0
  42. package/plugins/kimi-dsh/dist/commands/worker.js +25 -0
  43. package/plugins/kimi-dsh/dist/companion.js +97 -0
  44. package/plugins/kimi-dsh/dist/config.js +22 -0
  45. package/plugins/kimi-dsh/dist/dsh/companion.js +174 -0
  46. package/plugins/kimi-dsh/dist/dsh/index.js +156 -0
  47. package/plugins/kimi-dsh/dist/errors.js +40 -0
  48. package/plugins/kimi-dsh/dist/git.js +224 -0
  49. package/plugins/kimi-dsh/dist/help.js +43 -0
  50. package/plugins/kimi-dsh/dist/hook-escape.js +71 -0
  51. package/plugins/kimi-dsh/dist/hooks/approval-hook.js +101 -0
  52. package/plugins/kimi-dsh/dist/hooks/approval-policy.js +211 -0
  53. package/plugins/kimi-dsh/dist/hooks/config-safety.js +653 -0
  54. package/plugins/kimi-dsh/dist/hooks/install-paths.js +428 -0
  55. package/plugins/kimi-dsh/dist/hooks/install.js +210 -0
  56. package/plugins/kimi-dsh/dist/hooks/managed-block.js +619 -0
  57. package/plugins/kimi-dsh/dist/hooks/review-gate-stop.js +19 -0
  58. package/plugins/kimi-dsh/dist/job-id.js +12 -0
  59. package/plugins/kimi-dsh/dist/job-store.js +659 -0
  60. package/plugins/kimi-dsh/dist/jobs.js +217 -0
  61. package/plugins/kimi-dsh/dist/kimi-command.js +70 -0
  62. package/plugins/kimi-dsh/dist/kimi-engine.js +632 -0
  63. package/plugins/kimi-dsh/dist/kimi-errors.js +260 -0
  64. package/plugins/kimi-dsh/dist/kimi-home.js +9 -0
  65. package/plugins/kimi-dsh/dist/kimi-timeouts.js +52 -0
  66. package/plugins/kimi-dsh/dist/kimi-version-probe.js +216 -0
  67. package/plugins/kimi-dsh/dist/logging.js +22 -0
  68. package/plugins/kimi-dsh/dist/mcp/binding.js +58 -0
  69. package/plugins/kimi-dsh/dist/mcp/job-control.js +345 -0
  70. package/plugins/kimi-dsh/dist/mcp/launcher.js +257 -0
  71. package/plugins/kimi-dsh/dist/mcp/operations.js +301 -0
  72. package/plugins/kimi-dsh/dist/mcp/protocol.js +275 -0
  73. package/plugins/kimi-dsh/dist/mcp/server.js +8 -0
  74. package/plugins/kimi-dsh/dist/mcp/stdio.js +78 -0
  75. package/plugins/kimi-dsh/dist/mcp/supervised-child.js +15 -0
  76. package/plugins/kimi-dsh/dist/mcp/tool-result.js +7 -0
  77. package/plugins/kimi-dsh/dist/mcp/workspace.js +34 -0
  78. package/plugins/kimi-dsh/dist/model-command.js +32 -0
  79. package/plugins/kimi-dsh/dist/native-v2-preflight.js +417 -0
  80. package/plugins/kimi-dsh/dist/package.json +1 -0
  81. package/plugins/kimi-dsh/dist/parsing.js +620 -0
  82. package/plugins/kimi-dsh/dist/paths.js +27 -0
  83. package/plugins/kimi-dsh/dist/plugin-data.js +98 -0
  84. package/plugins/kimi-dsh/dist/process-tree.js +479 -0
  85. package/plugins/kimi-dsh/dist/prompt-size.js +18 -0
  86. package/plugins/kimi-dsh/dist/render.js +177 -0
  87. package/plugins/kimi-dsh/dist/rescue-approval.js +865 -0
  88. package/plugins/kimi-dsh/dist/schemas/review-gate-output.js +43 -0
  89. package/plugins/kimi-dsh/dist/session-recovery.js +200 -0
  90. package/plugins/kimi-dsh/dist/session-title.js +293 -0
  91. package/plugins/kimi-dsh/dist/stream-json.js +346 -0
  92. package/plugins/kimi-dsh/dist/swarm-activation.js +834 -0
  93. package/plugins/kimi-dsh/dist/swarm-recovery.js +134 -0
  94. package/plugins/kimi-dsh/dist/types.js +1 -0
  95. package/plugins/kimi-dsh/dist/vendor/shell-quote/LICENSE +24 -0
  96. package/plugins/kimi-dsh/dist/vendor/shell-quote/index.js +1 -0
  97. package/plugins/kimi-dsh/dist/vendor/shell-quote/parse.js +229 -0
  98. package/plugins/kimi-dsh/dist/vendor/smol-toml/LICENSE +24 -0
  99. package/plugins/kimi-dsh/dist/vendor/smol-toml/date.js +156 -0
  100. package/plugins/kimi-dsh/dist/vendor/smol-toml/error.js +67 -0
  101. package/plugins/kimi-dsh/dist/vendor/smol-toml/extract.js +293 -0
  102. package/plugins/kimi-dsh/dist/vendor/smol-toml/parse.js +164 -0
  103. package/plugins/kimi-dsh/dist/vendor/smol-toml/primitive.js +151 -0
  104. package/plugins/kimi-dsh/dist/vendor/smol-toml/struct.js +156 -0
  105. package/plugins/kimi-dsh/dist/vendor/smol-toml/util.js +57 -0
  106. package/plugins/kimi-dsh/dist/version.js +7 -0
  107. package/plugins/kimi-dsh/package.json +48 -0
  108. package/plugins/kimi-dsh/scripts/companion.sh +37 -0
  109. package/plugins/kimi-dsh/scripts/review-gate-hook.sh +32 -0
@@ -0,0 +1,834 @@
1
+ import { spawn } from "node:child_process";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { open } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { buildKimiCliEnvironment, } from "./cli-client.js";
6
+ import { RuntimeError } from "./errors.js";
7
+ import { assertPrefixArgsSafe } from "./kimi-command.js";
8
+ import { assertExecutionPlanMatchesSpawn, assertNoUnsafeExperimentalSelector, } from "./kimi-engine.js";
9
+ import { resolveKimiHome } from "./kimi-home.js";
10
+ import { assertNativeV2Preflight } from "./native-v2-preflight.js";
11
+ import { collectDescendantIdentities, collectProcessGroupsIdentities, probeProcessIdentity, revalidateProcessIdentities, waitForProcessTreeExit, } from "./process-tree.js";
12
+ const PREACTIVATION_VERSIONS = ["2.3.2", "2.3.3", "2.4.0", "2.4.1"];
13
+ const READY_TIMEOUT_MS = 10_000;
14
+ const REST_TIMEOUT_MS = 5_000;
15
+ // Exact 2.4.0 can take longer than 5s to drain session/workspace lifecycle work.
16
+ const SHUTDOWN_TIMEOUT_MS = 15_000;
17
+ const JOURNAL_TIMEOUT_MS = 2_000;
18
+ const JOURNAL_POLL_MS = 50;
19
+ const READY_LINE_MAX_CHARS = 65_536;
20
+ const REST_RESPONSE_MAX_BYTES = 1024 * 1024;
21
+ const JOURNAL_MAX_BYTES = 64 * 1024 * 1024;
22
+ const DESCENDANT_SNAPSHOT_TIMEOUT_MS = 2_000;
23
+ const DESCENDANT_TRACK_INTERVAL_MS = 50;
24
+ const IDENTITY_REVALIDATION_TIMEOUT_MS = 250;
25
+ const ESCALATION_MS = 1_500;
26
+ const POST_KILL_QUIESCENCE_MS = 500;
27
+ const ENV_MODEL_ALIAS = "__kimi_env_model__";
28
+ export function requiresSwarmPreactivation(version) {
29
+ return version !== null && PREACTIVATION_VERSIONS.includes(version);
30
+ }
31
+ export async function preactivateSwarmSession(options) {
32
+ const failureCode = "SWARM_ACTIVATION_FAILED";
33
+ let web;
34
+ let identity;
35
+ let profileMutationAttempted = false;
36
+ try {
37
+ const prepared = await prepareWebSpawn(options, failureCode);
38
+ throwIfAborted(options.signal, failureCode, "swarm-activation.pre-spawn");
39
+ web = await startWebChild(options, prepared.env, failureCode);
40
+ await verifyMeta(web, prepared.expectedVersion, options.signal, failureCode);
41
+ const model = await resolveProfileModel(web, options, failureCode);
42
+ identity = await createSession(web, options, failureCode);
43
+ options.onRecoveryEvent?.({ kind: "session", ...identity });
44
+ profileMutationAttempted = true;
45
+ await postSwarmProfile(web, identity, true, model, options.signal, failureCode);
46
+ await verifySwarmStatus(web, identity, true, model, options.signal, failureCode);
47
+ await shutdownWeb(web, options.signal, failureCode);
48
+ await requireSwarmJournal(prepared.kimiHome, identity, "enter", options.signal, failureCode);
49
+ throwIfAborted(options.signal, failureCode, "swarm-activation.complete");
50
+ return identity;
51
+ }
52
+ catch (error) {
53
+ const primary = asSafeFailure(error, failureCode, "swarm-activation");
54
+ if (web !== undefined && !(await teardownOwnedChild(web))) {
55
+ const cleanupError = failure("SWARM_SESSION_CLEANUP_FAILED", "swarm-activation.process-cleanup", "The Kimi web process tree did not become quiescent after activation failed.");
56
+ throw failure("SWARM_SESSION_CLEANUP_FAILED", "swarm-activation.process-cleanup", "Swarm preactivation failed and its web process tree could not be verified quiescent.", {
57
+ activation_error: errorReference(primary),
58
+ cleanup_error: errorReference(cleanupError),
59
+ process_cleanup_unverified: true,
60
+ cancelled: primary.details.cancelled === true,
61
+ ...(profileMutationAttempted && identity !== undefined
62
+ ? { session_id: identity.sessionId, workspace_id: identity.workspaceId }
63
+ : {}),
64
+ });
65
+ }
66
+ if (profileMutationAttempted && identity !== undefined) {
67
+ throw new RuntimeError(primary.code, primary.message, primary.stage, {
68
+ details: {
69
+ ...primary.details,
70
+ cleanup_unverified: true,
71
+ session_id: identity.sessionId,
72
+ workspace_id: identity.workspaceId,
73
+ },
74
+ });
75
+ }
76
+ throw primary;
77
+ }
78
+ }
79
+ export async function deactivateSwarmSession(options) {
80
+ const failureCode = "SWARM_SESSION_CLEANUP_FAILED";
81
+ let web;
82
+ try {
83
+ // Cleanup must still run when activation left no journal or the print run tainted it;
84
+ // the print spawn owns resume plan-taint refusal.
85
+ const prepared = await prepareWebSpawn(options, failureCode);
86
+ throwIfAborted(options.signal, failureCode, "swarm-deactivation.pre-spawn");
87
+ web = await startWebChild(options, prepared.env, failureCode);
88
+ await verifyMeta(web, prepared.expectedVersion, options.signal, failureCode);
89
+ await postSwarmProfile(web, options, false, undefined, options.signal, failureCode);
90
+ await verifySwarmStatus(web, options, false, undefined, options.signal, failureCode);
91
+ await shutdownWeb(web, options.signal, failureCode);
92
+ await requireSwarmJournal(prepared.kimiHome, options, "exit", options.signal, failureCode);
93
+ options.onRecoveryEvent?.({ kind: "disabled" });
94
+ throwIfAborted(options.signal, failureCode, "swarm-deactivation.complete");
95
+ }
96
+ catch (error) {
97
+ const primary = asSafeFailure(error, failureCode, "swarm-deactivation");
98
+ if (web !== undefined && !(await teardownOwnedChild(web))) {
99
+ throw failure("SWARM_SESSION_CLEANUP_FAILED", "swarm-deactivation.process-cleanup", `Swarm deactivation failed and the Kimi web process tree did not become quiescent (${web.cleanupSummary ?? "summary unavailable"}).`, { cleanup_error: errorReference(primary) });
100
+ }
101
+ throw primary;
102
+ }
103
+ }
104
+ async function prepareWebSpawn(options, failureCode) {
105
+ throwIfAborted(options.signal, failureCode, "swarm-activation.pre-spawn");
106
+ const prefixArgs = options.prefixArgs ?? [];
107
+ assertExecutionPlanMatchesSpawn(options.executionPlan, options.command, prefixArgs, options.env);
108
+ if (options.commandLabel === "swarm-write" && !options.trustedWorkspaceRoot) {
109
+ throw new RuntimeError("CLI_TRUSTED_WORKSPACE_ROOT_MISSING", "Refusing swarm-write: write-capable plugin launches require a plugin-owned trusted workspace root.", "swarm-activation.pre-spawn", { details: { command_label: options.commandLabel } });
110
+ }
111
+ assertNoUnsafeExperimentalSelector(options.env, "swarm-activation.pre-spawn");
112
+ assertPrefixArgsSafe(prefixArgs);
113
+ if (options.expectedVersion !== undefined &&
114
+ options.executionPlan.kimiVersion !== null &&
115
+ options.expectedVersion !== options.executionPlan.kimiVersion) {
116
+ throw failure(failureCode, "swarm-activation.version", "expectedVersion must match the execution plan version when that version is known.");
117
+ }
118
+ const expectedVersion = options.expectedVersion ?? options.executionPlan.kimiVersion;
119
+ if (expectedVersion === null || !requiresSwarmPreactivation(expectedVersion)) {
120
+ throw failure(failureCode, "swarm-activation.version", `Swarm web preactivation requires exact kimi-code ${PREACTIVATION_VERSIONS.join(" or ")}.`);
121
+ }
122
+ const kimiHome = resolveKimiHome(options.env, options.cwd);
123
+ if (options.executionPlan.intendedEngine === "native-v2") {
124
+ await assertNativeV2Preflight({
125
+ kimiHome,
126
+ env: options.env,
127
+ stage: "swarm-activation.pre-spawn",
128
+ });
129
+ }
130
+ throwIfAborted(options.signal, failureCode, "swarm-activation.pre-spawn");
131
+ return {
132
+ expectedVersion,
133
+ env: buildKimiCliEnvironment(options),
134
+ kimiHome,
135
+ };
136
+ }
137
+ async function startWebChild(options, env, failureCode) {
138
+ const args = [
139
+ ...(options.prefixArgs ?? []),
140
+ "web",
141
+ "--no-open",
142
+ "--host",
143
+ "127.0.0.1",
144
+ "--port",
145
+ "0",
146
+ "--log-level",
147
+ "error",
148
+ ];
149
+ let child;
150
+ options.onRecoveryEvent?.({ kind: "web-spawn" });
151
+ try {
152
+ child = spawn(options.command, args, {
153
+ cwd: options.cwd,
154
+ env,
155
+ stdio: "pipe",
156
+ detached: process.platform !== "win32",
157
+ });
158
+ }
159
+ catch {
160
+ options.onRecoveryEvent?.({ kind: "web-exit" });
161
+ throw failure(failureCode, "swarm-activation.spawn", "Failed to spawn the Kimi web lifecycle process.");
162
+ }
163
+ let exited = false;
164
+ let processError = false;
165
+ let settleClose;
166
+ const closePromise = new Promise((resolve) => {
167
+ settleClose = resolve;
168
+ });
169
+ child.once("exit", () => {
170
+ exited = true;
171
+ });
172
+ child.once("close", (exitCode, signal) => {
173
+ exited = true;
174
+ settleClose({ exitCode, signal });
175
+ });
176
+ child.on("error", () => {
177
+ processError = true;
178
+ });
179
+ child.stdout.on("data", () => undefined);
180
+ child.stderr.on("data", () => undefined);
181
+ child.stdout.setEncoding("utf8");
182
+ child.stderr.setEncoding("utf8");
183
+ try {
184
+ child.stdin.end();
185
+ }
186
+ catch {
187
+ }
188
+ const owned = {
189
+ child,
190
+ closePromise,
191
+ descendants: [],
192
+ onRecoveryEvent: options.onRecoveryEvent,
193
+ trackingStopped: false,
194
+ lastSnapshotComplete: false,
195
+ snapshotIncomplete: false,
196
+ get exited() {
197
+ return exited;
198
+ },
199
+ get processError() {
200
+ return processError;
201
+ },
202
+ };
203
+ startDescendantTracking(owned);
204
+ try {
205
+ const ready = await waitForReadyLine(owned, options.signal, failureCode);
206
+ await refreshOwnedDescendants(owned);
207
+ if (owned.recoveryFailed)
208
+ throw failure(failureCode, "swarm-activation.recovery", "Cannot persist web ownership; refusing session mutation.");
209
+ return Object.assign(owned, ready);
210
+ }
211
+ catch (error) {
212
+ const primary = asSafeFailure(error, failureCode, "swarm-activation.ready");
213
+ if (!(await teardownOwnedChild(owned))) {
214
+ throw failure("SWARM_SESSION_CLEANUP_FAILED", "swarm-activation.ready-cleanup", "The Kimi web process tree did not become quiescent after startup failed.", { activation_error: errorReference(primary), process_cleanup_unverified: true });
215
+ }
216
+ throw primary;
217
+ }
218
+ }
219
+ async function waitForReadyLine(owned, signal, failureCode) {
220
+ throwIfAborted(signal, failureCode, "swarm-activation.ready");
221
+ return await new Promise((resolve, reject) => {
222
+ let buffered = "";
223
+ let settled = false;
224
+ const timer = setTimeout(() => {
225
+ settle(failure(failureCode, "swarm-activation.ready", "Timed out waiting for the Kimi web ready line.", { timeout_ms: READY_TIMEOUT_MS }));
226
+ }, READY_TIMEOUT_MS);
227
+ const cleanup = () => {
228
+ clearTimeout(timer);
229
+ signal?.removeEventListener("abort", onAbort);
230
+ owned.child.stdout.off("data", onStdout);
231
+ owned.child.off("error", onError);
232
+ owned.child.off("close", onClose);
233
+ };
234
+ const settle = (result) => {
235
+ if (settled)
236
+ return;
237
+ settled = true;
238
+ cleanup();
239
+ if (result instanceof RuntimeError)
240
+ reject(result);
241
+ else
242
+ resolve(result);
243
+ };
244
+ const onAbort = () => {
245
+ settle(cancelledFailure(failureCode, "swarm-activation.ready"));
246
+ };
247
+ const onError = () => {
248
+ settle(failure(failureCode, "swarm-activation.process", "The Kimi web process failed before becoming ready."));
249
+ };
250
+ const onClose = () => {
251
+ settle(failure(failureCode, "swarm-activation.process", "The Kimi web process exited before becoming ready."));
252
+ };
253
+ const onStdout = (chunk) => {
254
+ buffered += chunk;
255
+ if (buffered.length > READY_LINE_MAX_CHARS) {
256
+ settle(failure(failureCode, "swarm-activation.ready", "The Kimi web ready line exceeded the bounded parser limit."));
257
+ return;
258
+ }
259
+ while (buffered.includes("\n")) {
260
+ const newline = buffered.indexOf("\n");
261
+ const line = buffered.slice(0, newline).replace(/\r$/, "");
262
+ buffered = buffered.slice(newline + 1);
263
+ if (!line.startsWith("Kimi server: "))
264
+ continue;
265
+ try {
266
+ settle(parseReadyLine(line));
267
+ }
268
+ catch {
269
+ settle(failure(failureCode, "swarm-activation.ready", "The Kimi web ready line was not an authenticated loopback URL in the expected format."));
270
+ }
271
+ return;
272
+ }
273
+ };
274
+ signal?.addEventListener("abort", onAbort, { once: true });
275
+ owned.child.stdout.on("data", onStdout);
276
+ owned.child.on("error", onError);
277
+ owned.child.on("close", onClose);
278
+ if (signal?.aborted === true)
279
+ onAbort();
280
+ });
281
+ }
282
+ function parseReadyLine(line) {
283
+ const candidate = line.slice("Kimi server: ".length);
284
+ const parsed = new URL(candidate);
285
+ if (parsed.protocol !== "http:" ||
286
+ parsed.hostname !== "127.0.0.1" ||
287
+ parsed.port.length === 0 ||
288
+ parsed.pathname !== "/" ||
289
+ parsed.search.length > 0 ||
290
+ parsed.username.length > 0 ||
291
+ parsed.password.length > 0) {
292
+ throw new Error("invalid ready URL");
293
+ }
294
+ if (!parsed.hash.startsWith("#token=") || parsed.hash.length === "#token=".length) {
295
+ throw new Error("invalid ready token");
296
+ }
297
+ return {
298
+ origin: parsed.origin,
299
+ token: parsed.hash.slice("#token=".length),
300
+ };
301
+ }
302
+ async function verifyMeta(web, expectedVersion, signal, failureCode) {
303
+ const data = await restData(web, "GET", "/api/v1/meta", undefined, signal, failureCode);
304
+ if (!isRecord(data)) {
305
+ throw failure(failureCode, "swarm-activation.meta", "Kimi web /meta returned an invalid payload.");
306
+ }
307
+ if (data.backend !== "v2") {
308
+ throw failure(failureCode, "swarm-activation.meta", "Kimi web /meta did not confirm the v2 backend.");
309
+ }
310
+ if (data.dangerous_bypass_auth !== false) {
311
+ throw failure(failureCode, "swarm-activation.meta", "Refusing a Kimi web server with authentication bypass enabled.");
312
+ }
313
+ if (data.server_version !== expectedVersion) {
314
+ throw failure(failureCode, "swarm-activation.meta", "Kimi web /meta server_version did not exactly match the execution plan.");
315
+ }
316
+ }
317
+ async function resolveProfileModel(web, options, failureCode) {
318
+ if (options.model !== undefined) {
319
+ if (options.model.length === 0) {
320
+ throw failure(failureCode, "swarm-activation.model", "The explicit swarm model alias is empty.");
321
+ }
322
+ return options.model;
323
+ }
324
+ if (options.env.KIMI_MODEL_NAME?.trim())
325
+ return ENV_MODEL_ALIAS;
326
+ const data = await restData(web, "GET", "/api/v1/config", undefined, options.signal, failureCode);
327
+ if (!isRecord(data) || typeof data.default_model !== "string" || data.default_model.trim().length === 0) {
328
+ throw failure(failureCode, "swarm-activation.model", "Kimi web /config did not provide a usable default_model.");
329
+ }
330
+ return data.default_model;
331
+ }
332
+ async function createSession(web, options, failureCode) {
333
+ const data = await restData(web, "POST", "/api/v1/sessions", { metadata: { cwd: options.cwd } }, options.signal, failureCode);
334
+ if (!isRecord(data) || !validPathId(data.id) || !validPathId(data.workspace_id)) {
335
+ throw failure(failureCode, "swarm-activation.create-session", "Kimi web returned an invalid session identity.");
336
+ }
337
+ return { sessionId: data.id, workspaceId: data.workspace_id };
338
+ }
339
+ async function postSwarmProfile(web, identity, active, model, signal, failureCode) {
340
+ const agentConfig = {
341
+ swarm_mode: active,
342
+ permission_mode: "auto",
343
+ };
344
+ if (active)
345
+ agentConfig.model = model;
346
+ await restData(web, "POST", `/api/v1/sessions/${encodeURIComponent(identity.sessionId)}/profile`, { agent_config: agentConfig }, signal, failureCode);
347
+ }
348
+ async function verifySwarmStatus(web, identity, active, model, signal, failureCode) {
349
+ const data = await restData(web, "GET", `/api/v1/sessions/${encodeURIComponent(identity.sessionId)}/status`, undefined, signal, failureCode);
350
+ if (!isRecord(data)) {
351
+ throw failure(failureCode, "swarm-activation.status", "Kimi web /status returned an invalid payload.");
352
+ }
353
+ if (data.swarm_mode !== active) {
354
+ throw failure(failureCode, "swarm-activation.status", `Kimi web /status did not confirm swarm_mode=${active}.`, { expected_swarm_mode: active });
355
+ }
356
+ if (data.plan_mode !== false) {
357
+ throw failure(failureCode, "swarm-activation.status", "Kimi web /status did not confirm plan mode is off.");
358
+ }
359
+ if (data.permission !== "auto") {
360
+ throw failure(failureCode, "swarm-activation.status", "Kimi web /status did not confirm auto permission mode.");
361
+ }
362
+ if (active && data.model !== model) {
363
+ throw failure(failureCode, "swarm-activation.status", "Kimi web /status did not confirm the selected model alias.");
364
+ }
365
+ }
366
+ async function shutdownWeb(web, signal, failureCode) {
367
+ await restData(web, "POST", "/api/v1/shutdown", undefined, signal, failureCode);
368
+ const settlement = await waitForChildClose(web, signal, failureCode);
369
+ if (!(await teardownOwnedChild(web))) {
370
+ throw failure(failureCode, "swarm-activation.shutdown", "The Kimi web process tree did not become quiescent after shutdown.", { exit_code: settlement.exitCode, signal: settlement.signal });
371
+ }
372
+ if (web.processError || settlement.exitCode !== 0) {
373
+ throw failure(failureCode, "swarm-activation.shutdown", "The Kimi web process did not exit cleanly after shutdown.", { exit_code: settlement.exitCode, signal: settlement.signal });
374
+ }
375
+ }
376
+ async function waitForChildClose(web, signal, failureCode) {
377
+ let timer;
378
+ let onAbort;
379
+ try {
380
+ return await Promise.race([
381
+ web.closePromise,
382
+ new Promise((_resolve, reject) => {
383
+ timer = setTimeout(() => {
384
+ reject(failure(failureCode, "swarm-activation.shutdown", "Timed out waiting for the Kimi web process to shut down.", { timeout_ms: SHUTDOWN_TIMEOUT_MS }));
385
+ }, SHUTDOWN_TIMEOUT_MS);
386
+ onAbort = () => reject(cancelledFailure(failureCode, "swarm-activation.shutdown"));
387
+ signal?.addEventListener("abort", onAbort, { once: true });
388
+ if (signal?.aborted === true)
389
+ onAbort();
390
+ }),
391
+ ]);
392
+ }
393
+ finally {
394
+ if (timer !== undefined)
395
+ clearTimeout(timer);
396
+ if (onAbort !== undefined)
397
+ signal?.removeEventListener("abort", onAbort);
398
+ }
399
+ }
400
+ async function restData(web, method, route, body, signal, failureCode) {
401
+ throwIfAborted(signal, failureCode, "swarm-activation.rest");
402
+ await refreshOwnedDescendants(web);
403
+ throwIfAborted(signal, failureCode, "swarm-activation.rest");
404
+ const controller = new AbortController();
405
+ let timedOut = false;
406
+ const onAbort = () => controller.abort();
407
+ const timer = setTimeout(() => {
408
+ timedOut = true;
409
+ controller.abort();
410
+ }, REST_TIMEOUT_MS);
411
+ signal?.addEventListener("abort", onAbort, { once: true });
412
+ if (signal?.aborted === true)
413
+ onAbort();
414
+ try {
415
+ const headers = { accept: "application/json" };
416
+ if (web.token !== undefined)
417
+ headers.authorization = `Bearer ${web.token}`;
418
+ if (body !== undefined)
419
+ headers["content-type"] = "application/json";
420
+ const response = await fetch(`${web.origin}${route}`, {
421
+ method,
422
+ headers,
423
+ body: body === undefined ? undefined : JSON.stringify(body),
424
+ redirect: "error",
425
+ signal: controller.signal,
426
+ });
427
+ await refreshOwnedDescendants(web);
428
+ if (!response.ok) {
429
+ await response.body?.cancel().catch(() => undefined);
430
+ throw failure(failureCode, "swarm-activation.rest", "Kimi web REST returned an unsuccessful HTTP status.", { route, http_status: response.status });
431
+ }
432
+ const text = await readBoundedResponse(response, failureCode, route);
433
+ let envelope;
434
+ try {
435
+ envelope = JSON.parse(text);
436
+ }
437
+ catch {
438
+ throw failure(failureCode, "swarm-activation.rest", "Kimi web REST returned malformed JSON.", { route });
439
+ }
440
+ if (!isRecord(envelope) || !("data" in envelope)) {
441
+ throw failure(failureCode, "swarm-activation.rest", "Kimi web REST returned an invalid response envelope.", { route });
442
+ }
443
+ if (envelope.code !== 0) {
444
+ throw failure(failureCode, "swarm-activation.rest", "Kimi web REST returned an unsuccessful business response.", {
445
+ route,
446
+ business_code: typeof envelope.code === "number" ? envelope.code : null,
447
+ });
448
+ }
449
+ return envelope.data;
450
+ }
451
+ catch (error) {
452
+ if (signal?.aborted === true)
453
+ throw cancelledFailure(failureCode, "swarm-activation.rest");
454
+ if (timedOut) {
455
+ throw failure(failureCode, "swarm-activation.rest", "Kimi web REST timed out.", { route, timeout_ms: REST_TIMEOUT_MS });
456
+ }
457
+ if (error instanceof RuntimeError)
458
+ throw error;
459
+ throw failure(failureCode, "swarm-activation.rest", "Kimi web REST failed before a valid response was received.", { route });
460
+ }
461
+ finally {
462
+ clearTimeout(timer);
463
+ signal?.removeEventListener("abort", onAbort);
464
+ }
465
+ }
466
+ async function readBoundedResponse(response, failureCode, route) {
467
+ if (response.body === null) {
468
+ throw failure(failureCode, "swarm-activation.rest", "Kimi web REST returned an empty response.", { route });
469
+ }
470
+ const reader = response.body.getReader();
471
+ const chunks = [];
472
+ let size = 0;
473
+ while (true) {
474
+ const { done, value } = await reader.read();
475
+ if (done)
476
+ break;
477
+ if (value === undefined)
478
+ continue;
479
+ size += value.byteLength;
480
+ if (size > REST_RESPONSE_MAX_BYTES) {
481
+ await reader.cancel().catch(() => undefined);
482
+ throw failure(failureCode, "swarm-activation.rest", "Kimi web REST response exceeded the bounded size limit.", { route, max_bytes: REST_RESPONSE_MAX_BYTES });
483
+ }
484
+ chunks.push(value);
485
+ }
486
+ const combined = new Uint8Array(size);
487
+ let offset = 0;
488
+ for (const chunk of chunks) {
489
+ combined.set(chunk, offset);
490
+ offset += chunk.byteLength;
491
+ }
492
+ return new TextDecoder().decode(combined);
493
+ }
494
+ async function requireSwarmJournal(kimiHome, identity, expected, signal, failureCode) {
495
+ const stage = expected === "enter" ? "swarm-activation.journal" : "swarm-deactivation.journal";
496
+ const journalPath = path.join(kimiHome, "sessions", identity.workspaceId, identity.sessionId, "agents", "main", "wire.jsonl");
497
+ const deadline = Date.now() + JOURNAL_TIMEOUT_MS;
498
+ let observed = "missing";
499
+ while (true) {
500
+ throwIfAborted(signal, failureCode, stage);
501
+ observed = await readSwarmJournalState(journalPath);
502
+ if (observed === expected)
503
+ return;
504
+ const remaining = deadline - Date.now();
505
+ if (remaining <= 0) {
506
+ throw failure(failureCode, stage, `The main-agent journal did not confirm swarm_mode.${expected === "enter" ? "enter with manual trigger" : "exit"}.`, { journal_state: observed, timeout_ms: JOURNAL_TIMEOUT_MS });
507
+ }
508
+ await delay(Math.min(JOURNAL_POLL_MS, remaining), signal, failureCode, stage);
509
+ }
510
+ }
511
+ async function readSwarmJournalState(journalPath) {
512
+ let handle;
513
+ try {
514
+ handle = await open(journalPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0) | (fsConstants.O_NONBLOCK ?? 0));
515
+ }
516
+ catch (error) {
517
+ const code = error.code;
518
+ return code === "ENOENT" || code === "ENOTDIR" ? "missing" : "unreadable";
519
+ }
520
+ try {
521
+ const stats = await handle.stat({ bigint: true });
522
+ if (!stats.isFile() || stats.size > BigInt(JOURNAL_MAX_BYTES))
523
+ return "unreadable";
524
+ const size = Number(stats.size);
525
+ const buffer = Buffer.alloc(size);
526
+ let offset = 0;
527
+ while (offset < size) {
528
+ const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
529
+ if (bytesRead === 0)
530
+ break;
531
+ offset += bytesRead;
532
+ }
533
+ let state = "missing";
534
+ for (const rawLine of buffer.subarray(0, offset).toString("utf8").split("\n")) {
535
+ const line = rawLine.trim();
536
+ if (line.length === 0)
537
+ continue;
538
+ let record;
539
+ try {
540
+ record = JSON.parse(line);
541
+ }
542
+ catch {
543
+ if (line.includes("swarm_mode."))
544
+ return "unreadable";
545
+ continue;
546
+ }
547
+ if (!isRecord(record))
548
+ continue;
549
+ if (record.type === "swarm_mode.enter") {
550
+ state = record.trigger === "manual" ? "enter" : "other";
551
+ }
552
+ else if (record.type === "swarm_mode.exit") {
553
+ state = "exit";
554
+ }
555
+ }
556
+ return state;
557
+ }
558
+ catch {
559
+ return "unreadable";
560
+ }
561
+ finally {
562
+ await handle.close().catch(() => undefined);
563
+ }
564
+ }
565
+ function startDescendantTracking(owned) {
566
+ if (process.platform === "win32")
567
+ return;
568
+ owned.trackerPromise = (async () => {
569
+ while (!owned.trackingStopped && !owned.exited) {
570
+ await refreshOwnedDescendants(owned);
571
+ if (owned.trackingStopped || owned.exited)
572
+ break;
573
+ await new Promise((resolve) => setTimeout(resolve, DESCENDANT_TRACK_INTERVAL_MS));
574
+ }
575
+ })();
576
+ }
577
+ async function stopDescendantTracking(owned) {
578
+ owned.trackingStopped = true;
579
+ await owned.trackerPromise;
580
+ await owned.refreshPromise;
581
+ }
582
+ async function refreshOwnedDescendants(owned) {
583
+ if (process.platform === "win32" || owned.exited)
584
+ return;
585
+ if (owned.refreshPromise !== undefined)
586
+ return await owned.refreshPromise;
587
+ const childPid = owned.child.pid;
588
+ if (childPid === undefined) {
589
+ owned.lastSnapshotComplete = true;
590
+ return;
591
+ }
592
+ owned.refreshPromise = (async () => {
593
+ const snapshot = await captureDescendantIdentities(childPid);
594
+ const previousRoot = owned.rootIdentity;
595
+ if (snapshot.root !== undefined) {
596
+ if (previousRoot === undefined) {
597
+ owned.rootIdentity = snapshot.root;
598
+ }
599
+ else if (previousRoot.startToken !== snapshot.root.startToken) {
600
+ owned.snapshotIncomplete = true;
601
+ owned.lastSnapshotComplete = false;
602
+ return;
603
+ }
604
+ }
605
+ const seen = new Set(owned.descendants.map(identityKey));
606
+ for (const identity of snapshot.identities) {
607
+ const key = identityKey(identity);
608
+ if (seen.has(key))
609
+ continue;
610
+ seen.add(key);
611
+ owned.descendants.push(identity);
612
+ }
613
+ if (snapshot.complete && (snapshot.root !== undefined || previousRoot !== undefined)) {
614
+ if (!owned.snapshotIncomplete)
615
+ owned.lastSnapshotComplete = true;
616
+ }
617
+ else {
618
+ owned.snapshotIncomplete = true;
619
+ owned.lastSnapshotComplete = false;
620
+ }
621
+ try {
622
+ owned.onRecoveryEvent?.({ kind: "web-processes", identities: [...(owned.rootIdentity ? [owned.rootIdentity] : []), ...owned.descendants], complete: owned.lastSnapshotComplete });
623
+ }
624
+ catch {
625
+ owned.recoveryFailed = true;
626
+ }
627
+ })().finally(() => {
628
+ owned.refreshPromise = undefined;
629
+ });
630
+ return await owned.refreshPromise;
631
+ }
632
+ async function teardownOwnedChild(owned) {
633
+ const quiescent = await teardownOwnedChildInner(owned);
634
+ if (quiescent && !owned.recoveryFailed) {
635
+ owned.onRecoveryEvent?.({ kind: "web-exit" });
636
+ return true;
637
+ }
638
+ return false;
639
+ }
640
+ async function teardownOwnedChildInner(owned) {
641
+ await stopDescendantTracking(owned);
642
+ const childPid = owned.child.pid;
643
+ if (childPid === undefined)
644
+ return true;
645
+ try {
646
+ if (process.platform === "win32") {
647
+ if (!owned.exited) {
648
+ try {
649
+ owned.child.kill("SIGTERM");
650
+ }
651
+ catch {
652
+ }
653
+ }
654
+ const exited = await waitForProcessTreeExit([], ESCALATION_MS, {
655
+ isRootAlive: () => !owned.exited,
656
+ });
657
+ if (!exited) {
658
+ try {
659
+ owned.child.kill("SIGKILL");
660
+ }
661
+ catch {
662
+ }
663
+ return await waitForProcessTreeExit([], POST_KILL_QUIESCENCE_MS, {
664
+ isRootAlive: () => !owned.exited,
665
+ });
666
+ }
667
+ return true;
668
+ }
669
+ if (!owned.exited)
670
+ await refreshOwnedDescendants(owned);
671
+ const identities = owned.descendants;
672
+ await signalCapturedTree(owned, childPid, identities, "SIGTERM");
673
+ const exited = await waitForProcessTreeExit(identities, ESCALATION_MS, {
674
+ isRootAlive: () => !owned.exited,
675
+ });
676
+ let quiescent = exited && await processGroupsQuiescent(owned, childPid);
677
+ let killWaitExited;
678
+ if (!quiescent) {
679
+ await signalCapturedTree(owned, childPid, identities, "SIGKILL");
680
+ killWaitExited = await waitForProcessTreeExit(identities, POST_KILL_QUIESCENCE_MS, {
681
+ isRootAlive: () => !owned.exited,
682
+ });
683
+ quiescent = killWaitExited && await processGroupsQuiescent(owned, childPid);
684
+ }
685
+ owned.cleanupSummary = [
686
+ `root_exited=${owned.exited}`,
687
+ `first_wait_exited=${exited}`,
688
+ `kill_wait_exited=${killWaitExited ?? "not-run"}`,
689
+ `groups_quiescent=${quiescent}`,
690
+ `last_snapshot_complete=${owned.lastSnapshotComplete}`,
691
+ `descendants=${identities.length}`,
692
+ ].join(", ");
693
+ return quiescent && owned.lastSnapshotComplete;
694
+ }
695
+ catch {
696
+ if (!owned.exited) {
697
+ if (process.platform !== "win32")
698
+ trySignalPid(-childPid, "SIGKILL");
699
+ try {
700
+ owned.child.kill("SIGKILL");
701
+ }
702
+ catch {
703
+ }
704
+ }
705
+ return false;
706
+ }
707
+ }
708
+ async function processGroupsQuiescent(owned, childPid) {
709
+ const groupIds = new Set([childPid]);
710
+ for (const identity of owned.descendants)
711
+ groupIds.add(identity.processGroupId);
712
+ const members = await collectProcessGroupsIdentities([...groupIds]);
713
+ return members !== undefined && members.every((identity) => /^[ZX]/i.test(identity.state.trim()));
714
+ }
715
+ async function captureDescendantIdentities(childPid) {
716
+ let timer;
717
+ try {
718
+ const snapshot = await Promise.race([
719
+ Promise.all([
720
+ collectDescendantIdentities(childPid),
721
+ probeProcessIdentity(childPid),
722
+ ]),
723
+ new Promise((resolve) => {
724
+ timer = setTimeout(() => resolve(null), DESCENDANT_SNAPSHOT_TIMEOUT_MS);
725
+ }),
726
+ ]);
727
+ if (snapshot === null)
728
+ return { identities: [], complete: false };
729
+ const [collected, rootProbe] = snapshot;
730
+ const identities = [];
731
+ const seen = new Set();
732
+ for (const identity of collected) {
733
+ const key = identityKey(identity);
734
+ if (Number.isInteger(identity.pid) &&
735
+ identity.pid > 0 &&
736
+ identity.pid !== childPid &&
737
+ Number.isInteger(identity.processGroupId) &&
738
+ identity.processGroupId > 0 &&
739
+ identity.startToken.length > 0 &&
740
+ !seen.has(key)) {
741
+ seen.add(key);
742
+ identities.push({ ...identity });
743
+ }
744
+ }
745
+ return { identities, complete: rootProbe.complete, root: rootProbe.identity };
746
+ }
747
+ catch {
748
+ return { identities: [], complete: false };
749
+ }
750
+ finally {
751
+ if (timer !== undefined)
752
+ clearTimeout(timer);
753
+ }
754
+ }
755
+ function identityKey(identity) {
756
+ return `${identity.pid}:${identity.startToken}:${identity.processGroupId}`;
757
+ }
758
+ async function signalCapturedTree(owned, childPid, identities, signal) {
759
+ if (!owned.exited) {
760
+ trySignalPid(-childPid, signal);
761
+ try {
762
+ owned.child.kill(signal);
763
+ }
764
+ catch {
765
+ }
766
+ }
767
+ const pidValidation = await revalidateProcessIdentities(identities, {
768
+ timeoutMs: IDENTITY_REVALIDATION_TIMEOUT_MS,
769
+ });
770
+ for (const identity of pidValidation.identities)
771
+ trySignalPid(identity.pid, signal);
772
+ const activeGroupIds = new Set(pidValidation.identities.map((identity) => identity.processGroupId));
773
+ if (owned.exited) {
774
+ if (!pidValidation.identities.some((identity) => identity.processGroupId === childPid)) {
775
+ activeGroupIds.delete(childPid);
776
+ }
777
+ }
778
+ else {
779
+ activeGroupIds.delete(childPid);
780
+ }
781
+ for (const processGroupId of activeGroupIds)
782
+ trySignalPid(-processGroupId, signal);
783
+ }
784
+ function trySignalPid(pid, signal) {
785
+ try {
786
+ process.kill(pid, signal);
787
+ }
788
+ catch {
789
+ }
790
+ }
791
+ function throwIfAborted(signal, code, stage) {
792
+ if (signal?.aborted === true)
793
+ throw cancelledFailure(code, stage);
794
+ }
795
+ function cancelledFailure(code, stage) {
796
+ return failure(code, stage, "Swarm web lifecycle cancelled by the caller.", { cancelled: true });
797
+ }
798
+ function failure(code, stage, message, details = {}) {
799
+ return new RuntimeError(code, message, stage, { details });
800
+ }
801
+ function asSafeFailure(error, code, stage) {
802
+ if (error instanceof RuntimeError)
803
+ return error;
804
+ return failure(code, stage, "The swarm web lifecycle failed without a safe diagnostic payload.");
805
+ }
806
+ function errorReference(error) {
807
+ return {
808
+ code: error.code,
809
+ stage: error.stage,
810
+ cancelled: error.details.cancelled === true,
811
+ };
812
+ }
813
+ function validPathId(value) {
814
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,255}$/.test(value);
815
+ }
816
+ function isRecord(value) {
817
+ return typeof value === "object" && value !== null && !Array.isArray(value);
818
+ }
819
+ async function delay(ms, signal, code, stage) {
820
+ throwIfAborted(signal, code, stage);
821
+ await new Promise((resolve, reject) => {
822
+ const timer = setTimeout(() => {
823
+ signal?.removeEventListener("abort", onAbort);
824
+ resolve();
825
+ }, ms);
826
+ const onAbort = () => {
827
+ clearTimeout(timer);
828
+ reject(cancelledFailure(code, stage));
829
+ };
830
+ signal?.addEventListener("abort", onAbort, { once: true });
831
+ if (signal?.aborted === true)
832
+ onAbort();
833
+ });
834
+ }