@khorsheed/dsh-local-agent-kimi 0.1.0-rc.6

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 (50) hide show
  1. package/README.en.md +132 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +132 -0
  4. package/cordis.patch.yml +34 -0
  5. package/lib/client.js +1066 -0
  6. package/lib/index.js +4716 -0
  7. package/lib/invariant.js +31 -0
  8. package/lib/types/client/SettingsCard.d.ts +77 -0
  9. package/lib/types/client/SettingsCard.js +197 -0
  10. package/lib/types/client/icons.d.ts +18 -0
  11. package/lib/types/client/icons.js +2 -0
  12. package/lib/types/client/index.d.ts +42 -0
  13. package/lib/types/client/index.js +89 -0
  14. package/lib/types/client/locales.d.ts +64 -0
  15. package/lib/types/client/locales.js +55 -0
  16. package/lib/types/credential-guard.d.ts +29 -0
  17. package/lib/types/credential-guard.js +107 -0
  18. package/lib/types/index.d.ts +69 -0
  19. package/lib/types/index.js +270 -0
  20. package/lib/types/invariant.d.ts +16 -0
  21. package/lib/types/invariant.js +33 -0
  22. package/lib/types/kimi-cli-provider.d.ts +215 -0
  23. package/lib/types/kimi-cli-provider.js +855 -0
  24. package/lib/types/live-driver.d.ts +245 -0
  25. package/lib/types/live-driver.js +1312 -0
  26. package/lib/types/live-switch.d.ts +78 -0
  27. package/lib/types/live-switch.js +112 -0
  28. package/lib/types/member-bridge-config.d.ts +43 -0
  29. package/lib/types/member-bridge-config.js +80 -0
  30. package/lib/types/model-broker.d.ts +97 -0
  31. package/lib/types/model-broker.js +205 -0
  32. package/lib/types/model-catalog.d.ts +26 -0
  33. package/lib/types/model-catalog.js +86 -0
  34. package/lib/types/model-configuration.d.ts +11 -0
  35. package/lib/types/model-configuration.js +55 -0
  36. package/lib/types/preset-tools.d.ts +20 -0
  37. package/lib/types/preset-tools.js +36 -0
  38. package/lib/types/provision.d.ts +131 -0
  39. package/lib/types/provision.js +363 -0
  40. package/lib/types/records.d.ts +35 -0
  41. package/lib/types/records.js +85 -0
  42. package/lib/types/session-configuration.d.ts +5 -0
  43. package/lib/types/session-configuration.js +26 -0
  44. package/lib/types/session-mirror.d.ts +180 -0
  45. package/lib/types/session-mirror.js +446 -0
  46. package/lib/types/session-view.d.ts +121 -0
  47. package/lib/types/session-view.js +423 -0
  48. package/locale/en.json +6 -0
  49. package/locale/zh.json +6 -0
  50. package/package.json +107 -0
package/lib/index.js ADDED
@@ -0,0 +1,4716 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { LIVE_FLUSH_INTERVAL_MS, LiveFlush, LiveStreamPublisher, ModelDirectoryCache, assertResumeCwdUnchanged, assertResumeScopeUnchanged, containerExecSpawn, containerScopedHome, delegationEnv, establishSubagentCatalogChild, extendModelDirectory, modelDirectoryContextKey, persistChildSession, probeCliVersion, resolveChildCwd, resolveRoundModel, settingsFace, subagentDelegationLabel, vol } from "@khorsheed/dsh-local-agent";
3
+ import { MEMBER_BRIDGE_SOCKET_ENV, MEMBER_BRIDGE_TOKEN_ENV, endpointHost } from "@khorsheed/dsh-local-agent/types";
4
+ import { randomUUID } from "node:crypto";
5
+ import { SessionId } from "@deepseek-ai/dsh-session";
6
+ import { NO_START_CAPABILITIES, settleRunResult, subprocessRunHandle } from "@deepseek-ai/dsh-subagent";
7
+ import { join } from "node:path";
8
+ import { copyFile, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
9
+ import { StringDecoder } from "node:string_decoder";
10
+ import { createAssistantMessage, createToolResultMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
11
+ import { execFile } from "node:child_process";
12
+ import { homedir } from "node:os";
13
+ import { readFileSync, writeFileSync } from "node:fs";
14
+ //#region ../../node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/date.js
15
+ /*!
16
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
17
+ * SPDX-License-Identifier: BSD-3-Clause
18
+ *
19
+ * Redistribution and use in source and binary forms, with or without
20
+ * modification, are permitted provided that the following conditions are met:
21
+ *
22
+ * 1. Redistributions of source code must retain the above copyright notice, this
23
+ * list of conditions and the following disclaimer.
24
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
25
+ * this list of conditions and the following disclaimer in the
26
+ * documentation and/or other materials provided with the distribution.
27
+ * 3. Neither the name of the copyright holder nor the names of its contributors
28
+ * may be used to endorse or promote products derived from this software without
29
+ * specific prior written permission.
30
+ *
31
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
32
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
33
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
34
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
35
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
36
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
37
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
39
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
40
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
41
+ */
42
+ let DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[T ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|[-+]\d{2}:\d{2})?$/i;
43
+ var TomlDate = class TomlDate extends Date {
44
+ #hasDate = false;
45
+ #hasTime = false;
46
+ #offset = null;
47
+ constructor(date) {
48
+ let hasDate = true;
49
+ let hasTime = true;
50
+ let offset = "Z";
51
+ if (typeof date === "string") {
52
+ let match = date.match(DATE_TIME_RE);
53
+ if (match) {
54
+ if (!match[1]) {
55
+ hasDate = false;
56
+ date = `0000-01-01T${date}`;
57
+ }
58
+ hasTime = !!match[2];
59
+ hasTime && date[10] === " " && (date = date.replace(" ", "T"));
60
+ if (match[2] && +match[2] > 23) date = "";
61
+ else {
62
+ offset = match[3] || null;
63
+ date = date.toUpperCase();
64
+ if (!offset && hasTime) date += "Z";
65
+ }
66
+ } else date = "";
67
+ }
68
+ super(date);
69
+ if (!isNaN(this.getTime())) {
70
+ this.#hasDate = hasDate;
71
+ this.#hasTime = hasTime;
72
+ this.#offset = offset;
73
+ }
74
+ }
75
+ isDateTime() {
76
+ return this.#hasDate && this.#hasTime;
77
+ }
78
+ isLocal() {
79
+ return !this.#hasDate || !this.#hasTime || !this.#offset;
80
+ }
81
+ isDate() {
82
+ return this.#hasDate && !this.#hasTime;
83
+ }
84
+ isTime() {
85
+ return this.#hasTime && !this.#hasDate;
86
+ }
87
+ isValid() {
88
+ return this.#hasDate || this.#hasTime;
89
+ }
90
+ toISOString() {
91
+ let iso = super.toISOString();
92
+ if (this.isDate()) return iso.slice(0, 10);
93
+ if (this.isTime()) return iso.slice(11, 23);
94
+ if (this.#offset === null) return iso.slice(0, -1);
95
+ if (this.#offset === "Z") return iso;
96
+ let offset = +this.#offset.slice(1, 3) * 60 + +this.#offset.slice(4, 6);
97
+ offset = this.#offset[0] === "-" ? offset : -offset;
98
+ return (/* @__PURE__ */ new Date(this.getTime() - offset * 6e4)).toISOString().slice(0, -1) + this.#offset;
99
+ }
100
+ static wrapAsOffsetDateTime(jsDate, offset = "Z") {
101
+ let date = new TomlDate(jsDate);
102
+ date.#offset = offset;
103
+ return date;
104
+ }
105
+ static wrapAsLocalDateTime(jsDate) {
106
+ let date = new TomlDate(jsDate);
107
+ date.#offset = null;
108
+ return date;
109
+ }
110
+ static wrapAsLocalDate(jsDate) {
111
+ let date = new TomlDate(jsDate);
112
+ date.#hasTime = false;
113
+ date.#offset = null;
114
+ return date;
115
+ }
116
+ static wrapAsLocalTime(jsDate) {
117
+ let date = new TomlDate(jsDate);
118
+ date.#hasDate = false;
119
+ date.#offset = null;
120
+ return date;
121
+ }
122
+ };
123
+ //#endregion
124
+ //#region ../../node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/error.js
125
+ /*!
126
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
127
+ * SPDX-License-Identifier: BSD-3-Clause
128
+ *
129
+ * Redistribution and use in source and binary forms, with or without
130
+ * modification, are permitted provided that the following conditions are met:
131
+ *
132
+ * 1. Redistributions of source code must retain the above copyright notice, this
133
+ * list of conditions and the following disclaimer.
134
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
135
+ * this list of conditions and the following disclaimer in the
136
+ * documentation and/or other materials provided with the distribution.
137
+ * 3. Neither the name of the copyright holder nor the names of its contributors
138
+ * may be used to endorse or promote products derived from this software without
139
+ * specific prior written permission.
140
+ *
141
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
142
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
143
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
144
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
145
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
146
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
147
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
148
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
149
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
150
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
151
+ */
152
+ function getLineColFromPtr(string, ptr) {
153
+ let lines = string.slice(0, ptr).split(/\r\n|\n|\r/g);
154
+ return [lines.length, lines.pop().length + 1];
155
+ }
156
+ function makeCodeBlock(string, line, column) {
157
+ let lines = string.split(/\r\n|\n|\r/g);
158
+ let codeblock = "";
159
+ let numberLen = (Math.log10(line + 1) | 0) + 1;
160
+ for (let i = line - 1; i <= line + 1; i++) {
161
+ let l = lines[i - 1];
162
+ if (!l) continue;
163
+ codeblock += i.toString().padEnd(numberLen, " ");
164
+ codeblock += ": ";
165
+ codeblock += l;
166
+ codeblock += "\n";
167
+ if (i === line) {
168
+ codeblock += " ".repeat(numberLen + column + 2);
169
+ codeblock += "^\n";
170
+ }
171
+ }
172
+ return codeblock;
173
+ }
174
+ var TomlError = class extends Error {
175
+ line;
176
+ column;
177
+ codeblock;
178
+ constructor(message, options) {
179
+ const [line, column] = getLineColFromPtr(options.toml, options.ptr);
180
+ const codeblock = makeCodeBlock(options.toml, line, column);
181
+ super(`Invalid TOML document: ${message}\n\n${codeblock}`, options);
182
+ this.line = line;
183
+ this.column = column;
184
+ this.codeblock = codeblock;
185
+ }
186
+ };
187
+ //#endregion
188
+ //#region ../../node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/util.js
189
+ /*!
190
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
191
+ * SPDX-License-Identifier: BSD-3-Clause
192
+ *
193
+ * Redistribution and use in source and binary forms, with or without
194
+ * modification, are permitted provided that the following conditions are met:
195
+ *
196
+ * 1. Redistributions of source code must retain the above copyright notice, this
197
+ * list of conditions and the following disclaimer.
198
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
199
+ * this list of conditions and the following disclaimer in the
200
+ * documentation and/or other materials provided with the distribution.
201
+ * 3. Neither the name of the copyright holder nor the names of its contributors
202
+ * may be used to endorse or promote products derived from this software without
203
+ * specific prior written permission.
204
+ *
205
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
206
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
207
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
208
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
209
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
210
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
211
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
212
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
213
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
214
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
215
+ */
216
+ /** @internal */
217
+ function indexOfNewline(str, start = 0) {
218
+ let idx = str.indexOf("\n", start);
219
+ if (str.charCodeAt(idx - 1) === 13) idx--;
220
+ return idx;
221
+ }
222
+ /** @internal */
223
+ function skipComment(ctx) {
224
+ for (; ctx.p < ctx.s.length; ctx.p++) {
225
+ let c = ctx.s.charCodeAt(ctx.p);
226
+ if (c === 10) break;
227
+ if (c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10) {
228
+ ctx.p++;
229
+ break;
230
+ }
231
+ if (c < 32 && c !== 9 || c === 127) throw new TomlError("control characters are not allowed in comments", {
232
+ toml: ctx.s,
233
+ ptr: ctx.p
234
+ });
235
+ }
236
+ }
237
+ /** @internal */
238
+ function skipVoid(ctx, banNewLines, banComments) {
239
+ let c;
240
+ while (1) {
241
+ while ((c = ctx.s.charCodeAt(ctx.p)) === 32 || c === 9 || !banNewLines && (c === 10 || c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10)) ctx.p++;
242
+ if (banComments || c !== 35) break;
243
+ skipComment(ctx);
244
+ }
245
+ }
246
+ /** @internal */
247
+ function skipUntil(ctx, sep, end) {
248
+ let ptr = ctx.p;
249
+ if (!end) {
250
+ ptr = indexOfNewline(ctx.s, ptr);
251
+ ctx.p = ptr < 0 ? ctx.s.length : ptr;
252
+ return;
253
+ }
254
+ for (; ctx.p < ctx.s.length; ctx.p++) {
255
+ let c = ctx.s.charCodeAt(ctx.p);
256
+ if (c === 35) skipComment(ctx);
257
+ else if (c === end || c === sep) return;
258
+ }
259
+ throw new TomlError("cannot find end of structure", {
260
+ toml: ctx.s,
261
+ ptr
262
+ });
263
+ }
264
+ //#endregion
265
+ //#region ../../node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/primitive.js
266
+ /*!
267
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
268
+ * SPDX-License-Identifier: BSD-3-Clause
269
+ *
270
+ * Redistribution and use in source and binary forms, with or without
271
+ * modification, are permitted provided that the following conditions are met:
272
+ *
273
+ * 1. Redistributions of source code must retain the above copyright notice, this
274
+ * list of conditions and the following disclaimer.
275
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
276
+ * this list of conditions and the following disclaimer in the
277
+ * documentation and/or other materials provided with the distribution.
278
+ * 3. Neither the name of the copyright holder nor the names of its contributors
279
+ * may be used to endorse or promote products derived from this software without
280
+ * specific prior written permission.
281
+ *
282
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
283
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
284
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
285
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
286
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
287
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
288
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
289
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
290
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
291
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
292
+ */
293
+ let INT_REGEX = /^((0x[0-9a-fA-F](_?[0-9a-fA-F])*)|(([+-]|0[ob])?\d(_?\d)*))$/;
294
+ let FLOAT_REGEX = /^[+-]?\d(_?\d)*(\.\d(_?\d)*)?([eE][+-]?\d(_?\d)*)?$/;
295
+ let LEADING_ZERO = /^[+-]?0[0-9_]/;
296
+ /** @internal */
297
+ function parseString(ctx) {
298
+ let start = ctx.p;
299
+ let c = ctx.s.charCodeAt(ctx.p++);
300
+ let first = c;
301
+ let isLiteral = c === 39;
302
+ let isMultiline = c === ctx.s.charCodeAt(ctx.p) && c === ctx.s.charCodeAt(ctx.p + 1);
303
+ if (isMultiline) {
304
+ if ((c = ctx.s.charCodeAt(ctx.p += 2)) === 10) ctx.p++;
305
+ else if (c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10) ctx.p += 2;
306
+ }
307
+ let parsed = "";
308
+ let sliceStart = ctx.p;
309
+ let state = 0;
310
+ for (; ctx.p < ctx.s.length; ctx.p++) {
311
+ c = ctx.s.charCodeAt(ctx.p);
312
+ if (isMultiline && (c === 10 || c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10)) state = state && 3;
313
+ else if (c < 32 && c !== 9 || c === 127) throw new TomlError("control characters are not allowed in strings", {
314
+ toml: ctx.s,
315
+ ptr: ctx.p
316
+ });
317
+ else if ((!state || state === 3) && c === first && (!isMultiline || ctx.s.charCodeAt(ctx.p + 1) === first && ctx.s.charCodeAt(ctx.p + 2) === first)) {
318
+ if (isMultiline) {
319
+ if (ctx.s.charCodeAt(ctx.p + 3) === first) ctx.p++;
320
+ if (ctx.s.charCodeAt(ctx.p + 3) === first) ctx.p++;
321
+ }
322
+ if (!state) parsed += ctx.s.slice(sliceStart, ctx.p);
323
+ ctx.p += isMultiline ? 3 : 1;
324
+ return parsed;
325
+ } else if (!state) {
326
+ if (!isLiteral && c === 92) {
327
+ parsed += ctx.s.slice(sliceStart, sliceStart = ctx.p);
328
+ state = 1;
329
+ }
330
+ } else if (state === 1) {
331
+ if (c === 120 || c === 117 || c === 85) {
332
+ let value = 0;
333
+ let len = c === 120 ? 2 : c === 117 ? 4 : 8;
334
+ for (let j = 0; j < len; j++, ctx.p++) {
335
+ let hex = ctx.s.charCodeAt(ctx.p + 1);
336
+ let digit = hex >= 48 && hex <= 57 ? hex - 48 : hex >= 65 && hex <= 70 ? hex - 65 + 10 : hex >= 97 && hex <= 102 ? hex - 97 + 10 : -1;
337
+ if (digit < 0) throw new TomlError("invalid non-hex character in unicode escape", {
338
+ toml: ctx.s,
339
+ ptr: ctx.p + 1
340
+ });
341
+ value = value << 4 | digit;
342
+ }
343
+ if (value < 0 || value > 1114111 || value >= 55296 && value <= 57343) throw new TomlError("invalid unicode escape", {
344
+ toml: ctx.s,
345
+ ptr: ctx.p
346
+ });
347
+ parsed += String.fromCodePoint(value);
348
+ sliceStart = ctx.p + 1;
349
+ state = 0;
350
+ } else if (c === 32 || c === 9) state = 2;
351
+ else {
352
+ if (c === 98) parsed += "\b";
353
+ else if (c === 116) parsed += " ";
354
+ else if (c === 110) parsed += "\n";
355
+ else if (c === 102) parsed += "\f";
356
+ else if (c === 114) parsed += "\r";
357
+ else if (c === 101) parsed += "\x1B";
358
+ else if (c === 34) parsed += "\"";
359
+ else if (c === 92) parsed += "\\";
360
+ else throw new TomlError("unrecognized escape sequence", {
361
+ toml: ctx.s,
362
+ ptr: ctx.p
363
+ });
364
+ sliceStart = ctx.p + 1;
365
+ state = 0;
366
+ }
367
+ } else if (c !== 32 && c !== 9) {
368
+ if (state === 2) throw new TomlError("invalid escape: only line-ending whitespace may be escaped", {
369
+ toml: ctx.s,
370
+ ptr: sliceStart
371
+ });
372
+ state = !isLiteral && c === 92 ? 1 : 0;
373
+ sliceStart = ctx.p;
374
+ }
375
+ }
376
+ throw new TomlError("unfinished string", {
377
+ toml: ctx.s,
378
+ ptr: start
379
+ });
380
+ }
381
+ function sliceAndTrimEndOf(ctx, start, end) {
382
+ let value = ctx.s.slice(start, end);
383
+ let commentIdx = value.indexOf("#");
384
+ if (commentIdx > 0) {
385
+ skipComment({
386
+ s: value,
387
+ p: commentIdx,
388
+ d: 0
389
+ });
390
+ value = value.slice(0, commentIdx);
391
+ }
392
+ return value.trimEnd();
393
+ }
394
+ /** @internal */
395
+ function parseValue(ctx, integersAsBigInt, end) {
396
+ let ptr = ctx.p;
397
+ let err = {
398
+ toml: ctx.s,
399
+ ptr
400
+ };
401
+ skipUntil(ctx, 44, end);
402
+ let value = sliceAndTrimEndOf(ctx, ptr, ctx.p);
403
+ if (!value) throw new TomlError("incomplete declaration: value expected", err);
404
+ if (value === "-inf") return -Infinity;
405
+ if (value === "inf" || value === "+inf") return Infinity;
406
+ if (value === "nan" || value === "+nan" || value === "-nan") return NaN;
407
+ if (value === "-0") return integersAsBigInt ? 0n : 0;
408
+ let isInt = INT_REGEX.test(value);
409
+ if (isInt || FLOAT_REGEX.test(value)) {
410
+ if (LEADING_ZERO.test(value)) throw new TomlError("leading zeroes are not allowed", err);
411
+ value = value.replace(/_/g, "");
412
+ let numeric = +value;
413
+ if (isNaN(numeric)) throw new TomlError("invalid number", err);
414
+ if (isInt) {
415
+ if ((isInt = !Number.isSafeInteger(numeric)) && !integersAsBigInt) throw new TomlError("integer value cannot be represented losslessly", err);
416
+ if (isInt || integersAsBigInt === true) numeric = BigInt(value);
417
+ }
418
+ return numeric;
419
+ }
420
+ const date = new TomlDate(value);
421
+ if (!date.isValid()) throw new TomlError("invalid value", err);
422
+ return date;
423
+ }
424
+ //#endregion
425
+ //#region ../../node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/extract.js
426
+ /*!
427
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
428
+ * SPDX-License-Identifier: BSD-3-Clause
429
+ *
430
+ * Redistribution and use in source and binary forms, with or without
431
+ * modification, are permitted provided that the following conditions are met:
432
+ *
433
+ * 1. Redistributions of source code must retain the above copyright notice, this
434
+ * list of conditions and the following disclaimer.
435
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
436
+ * this list of conditions and the following disclaimer in the
437
+ * documentation and/or other materials provided with the distribution.
438
+ * 3. Neither the name of the copyright holder nor the names of its contributors
439
+ * may be used to endorse or promote products derived from this software without
440
+ * specific prior written permission.
441
+ *
442
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
443
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
444
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
445
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
446
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
447
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
448
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
449
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
450
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
451
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
452
+ */
453
+ /** @internal */
454
+ function extractValue(ctx, end, integersAsBigInt) {
455
+ let ptr = ctx.p;
456
+ let c = ctx.s.charCodeAt(ptr);
457
+ if (c === 91 || c === 123) {
458
+ if (!ctx.d--) throw new TomlError("document contains excessively nested structures. aborting.", {
459
+ toml: ctx.s,
460
+ ptr
461
+ });
462
+ let value = c === 91 ? parseArray(ctx, integersAsBigInt) : parseInlineTable(ctx, integersAsBigInt);
463
+ ctx.d++;
464
+ return value;
465
+ }
466
+ if (c === 34 || c === 39) return parseString(ctx);
467
+ if (c === 116) {
468
+ if (ctx.s.charCodeAt(++ctx.p) !== 114 || ctx.s.charCodeAt(++ctx.p) !== 117 || ctx.s.charCodeAt(++ctx.p) !== 101) throw new TomlError("invalid value", {
469
+ toml: ctx.s,
470
+ ptr
471
+ });
472
+ ctx.p++;
473
+ return true;
474
+ }
475
+ if (c === 102) {
476
+ if (ctx.s.charCodeAt(++ctx.p) !== 97 || ctx.s.charCodeAt(++ctx.p) !== 108 || ctx.s.charCodeAt(++ctx.p) !== 115 || ctx.s.charCodeAt(++ctx.p) !== 101) throw new TomlError("invalid value", {
477
+ toml: ctx.s,
478
+ ptr
479
+ });
480
+ ctx.p++;
481
+ return false;
482
+ }
483
+ return parseValue(ctx, integersAsBigInt, end);
484
+ }
485
+ //#endregion
486
+ //#region ../../node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/struct.js
487
+ /*!
488
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
489
+ * SPDX-License-Identifier: BSD-3-Clause
490
+ *
491
+ * Redistribution and use in source and binary forms, with or without
492
+ * modification, are permitted provided that the following conditions are met:
493
+ *
494
+ * 1. Redistributions of source code must retain the above copyright notice, this
495
+ * list of conditions and the following disclaimer.
496
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
497
+ * this list of conditions and the following disclaimer in the
498
+ * documentation and/or other materials provided with the distribution.
499
+ * 3. Neither the name of the copyright holder nor the names of its contributors
500
+ * may be used to endorse or promote products derived from this software without
501
+ * specific prior written permission.
502
+ *
503
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
504
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
505
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
506
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
507
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
508
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
509
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
510
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
511
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
512
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
513
+ */
514
+ let KEY_PART_RE = /^[a-zA-Z0-9-_]+[ \t]*$/;
515
+ /** @internal */
516
+ function parseKey(ctx, end = "=") {
517
+ let start = ctx.p;
518
+ let dot = start - 1;
519
+ let parsed = [];
520
+ let endPtr = ctx.s.indexOf(end, start);
521
+ if (endPtr < 0) throw new TomlError("incomplete key-value: cannot find end of key", {
522
+ toml: ctx.s,
523
+ ptr: start
524
+ });
525
+ do {
526
+ let c = ctx.s.charCodeAt(ctx.p = ++dot);
527
+ if (c !== 32 && c !== 9) {
528
+ if (c === 34 || c === 39) {
529
+ if (c === ctx.s.charCodeAt(ctx.p + 1) && c === ctx.s.charCodeAt(ctx.p + 2)) throw new TomlError("multiline strings are not allowed in keys", {
530
+ toml: ctx.s,
531
+ ptr: ctx.p
532
+ });
533
+ let part = parseString(ctx);
534
+ dot = ctx.s.indexOf(".", ctx.p);
535
+ let strEnd = ctx.s.slice(ctx.p, dot < 0 || dot > endPtr ? endPtr : dot);
536
+ let newLine = indexOfNewline(strEnd);
537
+ if (newLine > -1) throw new TomlError("newlines are not allowed in keys", {
538
+ toml: ctx.s,
539
+ ptr: newLine
540
+ });
541
+ if (strEnd.trimStart()) throw new TomlError("found extra tokens after the string part", {
542
+ toml: ctx.s,
543
+ ptr: ctx.p
544
+ });
545
+ if (endPtr < ctx.p) {
546
+ endPtr = ctx.s.indexOf(end, ctx.p);
547
+ if (endPtr < 0) throw new TomlError("incomplete key-value: cannot find end of key", {
548
+ toml: ctx.s,
549
+ ptr: start
550
+ });
551
+ }
552
+ parsed.push(part);
553
+ } else {
554
+ dot = ctx.s.indexOf(".", ctx.p);
555
+ let part = ctx.s.slice(ctx.p, dot < 0 || dot > endPtr ? endPtr : dot);
556
+ if (!KEY_PART_RE.test(part)) throw new TomlError("only letter, numbers, dashes and underscores are allowed in keys", {
557
+ toml: ctx.s,
558
+ ptr: ctx.p
559
+ });
560
+ parsed.push(part.trimEnd());
561
+ }
562
+ }
563
+ } while (dot + 1 && dot < endPtr);
564
+ ctx.p = endPtr + 1;
565
+ skipVoid(ctx, true, true);
566
+ return parsed;
567
+ }
568
+ /** @internal */
569
+ function parseInlineTable(ctx, integersAsBigInt) {
570
+ let res = {};
571
+ let seen = /* @__PURE__ */ new Set();
572
+ let c;
573
+ ctx.p++;
574
+ while (ctx.p < ctx.s.length) {
575
+ skipVoid(ctx);
576
+ if ((c = ctx.s.charCodeAt(ctx.p)) === 125) {
577
+ ctx.p++;
578
+ return res;
579
+ }
580
+ let k;
581
+ let t = res;
582
+ let hasOwn = false;
583
+ let p = ctx.p;
584
+ let key = parseKey(ctx);
585
+ for (let i = 0; i < key.length; i++) {
586
+ if (i) t = hasOwn ? t[k] : t[k] = {};
587
+ k = key[i];
588
+ if ((hasOwn = Object.hasOwn(t, k)) && (typeof t[k] !== "object" || seen.has(t[k]))) throw new TomlError("trying to redefine an already defined value", {
589
+ toml: ctx.s,
590
+ ptr: p
591
+ });
592
+ if (!hasOwn && k === "__proto__") Object.defineProperty(t, k, {
593
+ enumerable: true,
594
+ configurable: true,
595
+ writable: true
596
+ });
597
+ }
598
+ if (hasOwn) throw new TomlError("trying to redefine an already defined value", {
599
+ toml: ctx.s,
600
+ ptr: ctx.p
601
+ });
602
+ let value = extractValue(ctx, 125, integersAsBigInt);
603
+ seen.add(t[k] = value);
604
+ skipVoid(ctx);
605
+ if ((c = ctx.s.charCodeAt(ctx.p++)) === 125) return res;
606
+ if (c !== 44) throw new TomlError("expected comma or end of structure", {
607
+ toml: ctx.s,
608
+ ptr: ctx.p - 1
609
+ });
610
+ }
611
+ throw new TomlError("unfinished table encountered", {
612
+ toml: ctx.s,
613
+ ptr: ctx.p
614
+ });
615
+ }
616
+ /** @internal */
617
+ function parseArray(ctx, integersAsBigInt) {
618
+ let res = [];
619
+ let c;
620
+ ctx.p++;
621
+ while (ctx.p < ctx.s.length) {
622
+ skipVoid(ctx);
623
+ if ((c = ctx.s.charCodeAt(ctx.p)) === 93) {
624
+ ctx.p++;
625
+ return res;
626
+ }
627
+ res.push(extractValue(ctx, 93, integersAsBigInt));
628
+ skipVoid(ctx);
629
+ if ((c = ctx.s.charCodeAt(ctx.p++)) === 93) return res;
630
+ if (c !== 44) throw new TomlError("expected comma or end of structure", {
631
+ toml: ctx.s,
632
+ ptr: ctx.p - 1
633
+ });
634
+ }
635
+ throw new TomlError("unfinished array encountered", {
636
+ toml: ctx.s,
637
+ ptr: ctx.p
638
+ });
639
+ }
640
+ //#endregion
641
+ //#region ../../node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/parse.js
642
+ /*!
643
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
644
+ * SPDX-License-Identifier: BSD-3-Clause
645
+ *
646
+ * Redistribution and use in source and binary forms, with or without
647
+ * modification, are permitted provided that the following conditions are met:
648
+ *
649
+ * 1. Redistributions of source code must retain the above copyright notice, this
650
+ * list of conditions and the following disclaimer.
651
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
652
+ * this list of conditions and the following disclaimer in the
653
+ * documentation and/or other materials provided with the distribution.
654
+ * 3. Neither the name of the copyright holder nor the names of its contributors
655
+ * may be used to endorse or promote products derived from this software without
656
+ * specific prior written permission.
657
+ *
658
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
659
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
660
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
661
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
662
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
663
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
664
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
665
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
666
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
667
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
668
+ */
669
+ function peekTable(key, table, meta, type) {
670
+ let t = table;
671
+ let m = meta;
672
+ let k;
673
+ let hasOwn = false;
674
+ let state;
675
+ for (let i = 0; i < key.length; i++) {
676
+ if (i) {
677
+ t = hasOwn ? t[k] : t[k] = {};
678
+ m = (state = m[k]).c;
679
+ if (type === 0 && (state.t === 1 || state.t === 2)) return null;
680
+ if (state.t === 2) {
681
+ let l = t.length - 1;
682
+ t = t[l];
683
+ m = m[l].c;
684
+ }
685
+ }
686
+ k = key[i];
687
+ if ((hasOwn = Object.hasOwn(t, k)) && m[k]?.t === 0 && m[k]?.d) return null;
688
+ if (!hasOwn) {
689
+ if (k === "__proto__") {
690
+ Object.defineProperty(t, k, {
691
+ enumerable: true,
692
+ configurable: true,
693
+ writable: true
694
+ });
695
+ Object.defineProperty(m, k, {
696
+ enumerable: true,
697
+ configurable: true,
698
+ writable: true
699
+ });
700
+ }
701
+ m[k] = {
702
+ t: i < key.length - 1 && type === 2 ? 3 : type,
703
+ d: false,
704
+ i: 0,
705
+ c: {}
706
+ };
707
+ }
708
+ }
709
+ state = m[k];
710
+ if (state.t !== type && !(type === 1 && state.t === 3)) return null;
711
+ if (type === 2) {
712
+ if (!state.d) {
713
+ state.d = true;
714
+ t[k] = [];
715
+ }
716
+ t[k].push(t = {});
717
+ state.c[state.i++] = state = {
718
+ t: 1,
719
+ d: false,
720
+ i: 0,
721
+ c: {}
722
+ };
723
+ }
724
+ if (state.d) return null;
725
+ state.d = true;
726
+ if (type === 1) t = hasOwn ? t[k] : t[k] = {};
727
+ else if (type === 0 && hasOwn) return null;
728
+ return [
729
+ k,
730
+ t,
731
+ state.c
732
+ ];
733
+ }
734
+ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
735
+ let ctx = {
736
+ s: toml,
737
+ p: 0,
738
+ d: maxDepth
739
+ };
740
+ let res = {};
741
+ let meta = {};
742
+ let tmp;
743
+ let tbl = res;
744
+ let m = meta;
745
+ skipVoid(ctx);
746
+ while (ctx.p < toml.length) {
747
+ if (toml.charCodeAt(ctx.p) === 91) {
748
+ let isTableArray = toml.charCodeAt(++ctx.p) === 91;
749
+ tmp = ctx.p += +isTableArray;
750
+ let k = parseKey(ctx, "]");
751
+ if (isTableArray) {
752
+ if (toml.charCodeAt(ctx.p - 1) !== 93) throw new TomlError("expected end of table declaration", {
753
+ toml,
754
+ ptr: ctx.p - 1
755
+ });
756
+ ctx.p++;
757
+ }
758
+ let p = peekTable(k, res, meta, isTableArray ? 2 : 1);
759
+ if (!p) throw new TomlError("trying to redefine an already defined table or value", {
760
+ toml,
761
+ ptr: tmp
762
+ });
763
+ m = p[2];
764
+ tbl = p[1];
765
+ } else {
766
+ tmp = ctx.p;
767
+ let p = peekTable(parseKey(ctx), tbl, m, 0);
768
+ if (!p) throw new TomlError("trying to redefine an already defined table or value", {
769
+ toml,
770
+ ptr: tmp
771
+ });
772
+ p[1][p[0]] = extractValue(ctx, void 0, integersAsBigInt);
773
+ }
774
+ skipVoid(ctx, true);
775
+ if (ctx.p < toml.length && (tmp = toml.charCodeAt(ctx.p)) !== 10 && tmp !== 13) throw new TomlError("each key-value declaration must be followed by an end-of-line", {
776
+ toml,
777
+ ptr: ctx.p
778
+ });
779
+ skipVoid(ctx);
780
+ }
781
+ return res;
782
+ }
783
+ //#endregion
784
+ //#region lib/types/model-configuration.js
785
+ const table = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
786
+ const string = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
787
+ /** Native configuration metadata only; provider credentials never leave this function. */
788
+ async function readKimiModelConfiguration(home, selectedModel) {
789
+ let text;
790
+ try {
791
+ text = await readFile(join(home, "config.toml"), "utf8");
792
+ } catch (error) {
793
+ if (error.code === "ENOENT") return { entries: [] };
794
+ throw error;
795
+ }
796
+ const config = parse(text);
797
+ const models = table(config["models"]);
798
+ const entries = Object.entries(models).map(([value, raw]) => {
799
+ const model = table(raw);
800
+ const efforts = model["support_efforts"];
801
+ const defaultEffort = string(model["default_effort"]);
802
+ return {
803
+ value,
804
+ label: string(model["display_name"]) ?? value,
805
+ source: "configuration",
806
+ ...!Array.isArray(efforts) ? {} : { reasoning: {
807
+ options: efforts.flatMap((value) => typeof value === "string" ? [{
808
+ value,
809
+ label: value
810
+ }] : []),
811
+ ...defaultEffort === void 0 ? {} : { default: defaultEffort }
812
+ } }
813
+ };
814
+ });
815
+ const defaultModel = string(config["default_model"]);
816
+ const selected = selectedModel ?? defaultModel;
817
+ const model = table(selected === void 0 ? void 0 : models[selected]);
818
+ const provider = table(table(config["providers"])[string(model["provider"]) ?? ""]);
819
+ const protocol = string(model["protocol"]) ?? string(provider["type"]);
820
+ const thinking = table(config["thinking"]);
821
+ const requestedEffort = string(thinking["effort"]);
822
+ const options = entries.find((entry) => entry.value === selected)?.reasoning?.options;
823
+ const effort = thinking["enabled"] === false ? void 0 : requestedEffort !== void 0 && options?.some((option) => option.value === requestedEffort) ? requestedEffort : string(model["default_effort"]);
824
+ return {
825
+ entries,
826
+ ...defaultModel === void 0 ? {} : { defaultModel },
827
+ ...effort === void 0 ? {} : { effort },
828
+ ...protocol === void 0 ? {} : { protocol }
829
+ };
830
+ }
831
+ /** Only the native Kimi protocol advertises a general exec effort override. */
832
+ async function kimiExecConfiguration(home, model, effort) {
833
+ if (effort === void 0) return {};
834
+ const config = await readKimiModelConfiguration(home, model);
835
+ if (!config.entries.find((entry) => entry.value === (model ?? config.defaultModel))?.reasoning?.options.some((option) => option.value === effort)) throw new Error("Kimi configuration does not advertise this model/effort combination");
836
+ if (config.protocol !== "kimi") throw new Error("This Kimi model protocol has no per-invocation exec effort control; use ACP live mode");
837
+ return { KIMI_MODEL_THINKING_EFFORT: effort };
838
+ }
839
+ //#endregion
840
+ //#region lib/types/model-catalog.js
841
+ const object = (value) => typeof value === "object" && value !== null ? value : void 0;
842
+ function options(value) {
843
+ if (!Array.isArray(value)) return [];
844
+ return value.flatMap((candidate) => {
845
+ const entry = object(candidate);
846
+ if (entry === void 0) return [];
847
+ if (Array.isArray(entry["options"])) return options(entry["options"]);
848
+ if (typeof entry["value"] !== "string") return [];
849
+ return [{
850
+ value: entry["value"],
851
+ label: typeof entry["name"] === "string" ? entry["name"] : entry["value"],
852
+ ...typeof entry["description"] === "string" ? { description: entry["description"] } : {}
853
+ }];
854
+ });
855
+ }
856
+ /** ACP v1 configOptions are authoritative; old models metadata is a read-only fallback. */
857
+ function kimiNativeConfiguration(value) {
858
+ const response = object(value);
859
+ if (response === void 0) return void 0;
860
+ const configuration = response["configOptions"];
861
+ if (Array.isArray(configuration)) {
862
+ const choices = configuration.map(object).filter((entry) => entry !== void 0 && entry["type"] === "select");
863
+ const model = choices.find((entry) => entry["category"] === "model");
864
+ const effort = choices.find((entry) => entry["category"] === "thought_level");
865
+ if (model !== void 0) {
866
+ const currentModel = typeof model["currentValue"] === "string" ? model["currentValue"] : void 0;
867
+ return {
868
+ directory: {
869
+ entries: options(model["options"]).map((option) => ({
870
+ ...option,
871
+ source: "native",
872
+ ...effort === void 0 || option.value !== currentModel ? {} : { reasoning: { options: options(effort["options"]) } }
873
+ })),
874
+ complete: true,
875
+ customInput: false
876
+ },
877
+ ...typeof model["id"] === "string" ? { modelConfigId: model["id"] } : {},
878
+ ...typeof effort?.["id"] === "string" ? { effortConfigId: effort["id"] } : {},
879
+ ...currentModel === void 0 ? {} : { currentModel },
880
+ ...typeof effort?.["currentValue"] === "string" ? { currentEffort: effort["currentValue"] } : {}
881
+ };
882
+ }
883
+ }
884
+ const legacy = object(response["models"]);
885
+ if (!Array.isArray(legacy?.["availableModels"])) return void 0;
886
+ return {
887
+ directory: {
888
+ entries: legacy["availableModels"].flatMap((candidate) => {
889
+ const model = object(candidate);
890
+ if (typeof model?.["modelId"] !== "string") return [];
891
+ return [{
892
+ value: model["modelId"],
893
+ label: typeof model["name"] === "string" ? model["name"] : model["modelId"],
894
+ source: "native",
895
+ ...typeof model["description"] === "string" ? { description: model["description"] } : {}
896
+ }];
897
+ }),
898
+ complete: true,
899
+ customInput: false
900
+ },
901
+ ...typeof legacy["currentModelId"] === "string" ? { currentModel: legacy["currentModelId"] } : {}
902
+ };
903
+ }
904
+ /** Reads configuration before a session exists; never starts a disposable ACP session. */
905
+ var KimiModelCatalog = class {
906
+ context;
907
+ cache = new ModelDirectoryCache({ load: async (key) => {
908
+ const context = JSON.parse(key);
909
+ if (context.native !== void 0) return context.native;
910
+ const { entries, defaultModel } = await readKimiModelConfiguration(context.homeDir);
911
+ return {
912
+ entries,
913
+ ...defaultModel === void 0 ? {} : { defaultModel },
914
+ complete: false,
915
+ customInput: true,
916
+ reason: "Configured candidates; native session model metadata is not available yet"
917
+ };
918
+ } });
919
+ constructor(context) {
920
+ this.context = context;
921
+ }
922
+ key(childSessionId) {
923
+ const context = this.context(childSessionId);
924
+ const stamp = modelDirectoryContextKey({
925
+ provider: "kimi",
926
+ homeDir: context.homeDir,
927
+ cwd: context.cwd ?? context.homeDir,
928
+ cli: ["kimi"],
929
+ files: [join(context.homeDir, "config.toml"), join(context.homeDir, "credentials", "kimi-code.json")]
930
+ });
931
+ return JSON.stringify({
932
+ homeDir: context.homeDir,
933
+ stamp,
934
+ childSessionId,
935
+ ...context.native === void 0 ? {} : { native: context.native.directory }
936
+ });
937
+ }
938
+ read(childSessionId) {
939
+ return this.cache.read(this.key(childSessionId));
940
+ }
941
+ refresh(childSessionId) {
942
+ return this.cache.refresh(this.key(childSessionId));
943
+ }
944
+ follow(childSessionId, signal) {
945
+ return this.cache.follow(() => this.key(childSessionId), signal);
946
+ }
947
+ dispose() {
948
+ this.cache.dispose();
949
+ }
950
+ };
951
+ //#endregion
952
+ //#region lib/types/session-configuration.js
953
+ /** Confirm model first: its returned options determine which efforts are valid. */
954
+ async function configureKimiSession(sessionId, initial, desired, request) {
955
+ let current = initial;
956
+ if (desired.model !== void 0 && current?.currentModel !== desired.model) {
957
+ if (current?.modelConfigId === void 0) throw new Error("This Kimi ACP session exposes no verified model selection control");
958
+ if (!current.directory.entries.some((entry) => entry.value === desired.model)) throw new Error("The running Kimi session does not advertise this model");
959
+ current = kimiNativeConfiguration(await request("session/set_config_option", {
960
+ sessionId,
961
+ configId: current.modelConfigId,
962
+ value: desired.model
963
+ }));
964
+ if (current?.currentModel !== desired.model) throw new Error("Kimi did not confirm the requested model");
965
+ }
966
+ if (desired.effort !== void 0 && current?.currentEffort !== desired.effort) {
967
+ const currentModel = current?.currentModel;
968
+ const entry = current?.directory.entries.find((entry) => entry.value === currentModel);
969
+ if (current?.effortConfigId === void 0 || !entry?.reasoning?.options.some((option) => option.value === desired.effort)) throw new Error("The running Kimi session does not advertise this model/effort combination");
970
+ current = kimiNativeConfiguration(await request("session/set_config_option", {
971
+ sessionId,
972
+ configId: current.effortConfigId,
973
+ value: desired.effort
974
+ }));
975
+ if (current?.currentEffort !== desired.effort || desired.model !== void 0 && current.currentModel !== desired.model) throw new Error("Kimi did not confirm the requested model/effort configuration");
976
+ }
977
+ return current;
978
+ }
979
+ //#endregion
980
+ //#region lib/types/session-view.js
981
+ /**
982
+ * Kimi session viewer: read a scoped-home session's `wire.jsonl` event log
983
+ * and render a readable transcript (user prompts, assistant thinking and
984
+ * replies, tool calls with arguments and results). The kimi ACP mode writes
985
+ * responses to the session file rather than pushing them over the protocol,
986
+ * and the dsh subagent record for a one-shot run carries only the final
987
+ * output — this is the way to see what the kimi subagent actually did.
988
+ *
989
+ * The transcript is harness-comparison grade: every line is tagged with its
990
+ * wire turn number (1-based, matching the dsh child session's `turn/start`
991
+ * numbering), kimi's auto-permission `<system-reminder>` user messages are
992
+ * filtered out, tool calls carry their rendered arguments, tool results are
993
+ * matched to their owning call by `parentUuid`, and every `usage.record`
994
+ * (kimi reports accounting per LLM request, not cumulative) is surfaced with
995
+ * its transcript position so a delta mirror can sum a round's total.
996
+ * @module @khorsheed/dsh-local-agent-kimi/session-view
997
+ */
998
+ /** The wire event log path for a session directory. */
999
+ function wirePath(sessionDir) {
1000
+ return join(sessionDir, "agents", "main", "wire.jsonl");
1001
+ }
1002
+ /**
1003
+ * Find a session directory under `<homeDir>/sessions/wd_*` whose id matches.
1004
+ * @param homeDir - the `kimi` harness's scoped home.
1005
+ * @param sessionId - the kimi session id.
1006
+ * @returns the session directory, or undefined when absent.
1007
+ */
1008
+ async function findKimiSessionDir(homeDir, sessionId) {
1009
+ const sessionsRoot = join(homeDir, "sessions");
1010
+ let workspaces;
1011
+ try {
1012
+ workspaces = await readdir(sessionsRoot);
1013
+ } catch (error) {
1014
+ if (error.code === "ENOENT") return void 0;
1015
+ throw error;
1016
+ }
1017
+ for (const workspace of workspaces) {
1018
+ const sessionDir = join(sessionsRoot, workspace, `session_${sessionId}`);
1019
+ try {
1020
+ await readFile(wirePath(sessionDir));
1021
+ return sessionDir;
1022
+ } catch {}
1023
+ }
1024
+ }
1025
+ /** Whether a user message is kimi's auto-permission reminder, never real task text. */
1026
+ function isSystemReminder(text) {
1027
+ return text.startsWith("<system-reminder>");
1028
+ }
1029
+ /** Flatten the text blocks of a message content array. */
1030
+ function textOf(content) {
1031
+ return (content ?? []).filter((block) => typeof block === "object" && block !== null && block.type === "text").map((block) => block.text ?? "").join("");
1032
+ }
1033
+ /**
1034
+ * Render a file-editing tool call's arguments apply-patch style: the path
1035
+ * plus the old/new content. Without this a delegation's edits collapsed to
1036
+ * the bare path (or nothing) and the transcript hid what the member actually
1037
+ * changed — the kimi twin of codex's fileChange → ApplyPatch fold.
1038
+ * @param name - the tool name the wire recorded.
1039
+ * @param record - the call's argument object.
1040
+ * @returns the patch-style summary, or undefined when the tool is not a
1041
+ * known file-editing shape.
1042
+ */
1043
+ function editArgsOf(name, record) {
1044
+ const path = typeof record["path"] === "string" ? record["path"] : void 0;
1045
+ if (name === "Edit" || name === "MultiEdit") {
1046
+ if (path === void 0 || path.trim() === "") return void 0;
1047
+ const edits = Array.isArray(record["edits"]) ? record["edits"] : [record];
1048
+ const hunks = [];
1049
+ for (const edit of edits) {
1050
+ const oldText = typeof edit["old_string"] === "string" ? edit["old_string"] : "";
1051
+ const newText = typeof edit["new_string"] === "string" ? edit["new_string"] : "";
1052
+ if (oldText === "" && newText === "") continue;
1053
+ hunks.push("@@", ...oldText.split("\n").map((line) => `-${line}`), ...newText.split("\n").map((line) => `+${line}`));
1054
+ }
1055
+ if (hunks.length === 0) return void 0;
1056
+ return [`update: ${path}`, ...hunks].join("\n");
1057
+ }
1058
+ if (name === "Write") {
1059
+ if (path === void 0 || path.trim() === "") return void 0;
1060
+ const content = typeof record["content"] === "string" ? record["content"] : "";
1061
+ if (content === "") return void 0;
1062
+ return [`add: ${path}`, ...content.split("\n").map((line) => `+${line}`)].join("\n");
1063
+ }
1064
+ const patch = record["patch"] ?? record["diff"];
1065
+ if (typeof patch === "string" && patch.trim() !== "") return patch;
1066
+ }
1067
+ /** Render a tool call's arguments into a compact summary. */
1068
+ function argsOf(name, args) {
1069
+ if (args === void 0) return void 0;
1070
+ if (typeof args === "string") return args.trim() === "" ? void 0 : args;
1071
+ if (typeof args !== "object" || args === null || Array.isArray(args)) return void 0;
1072
+ const record = args;
1073
+ const edit = editArgsOf(name, record);
1074
+ if (edit !== void 0) return edit;
1075
+ for (const key of [
1076
+ "query",
1077
+ "url",
1078
+ "path",
1079
+ "command"
1080
+ ]) {
1081
+ const value = record[key];
1082
+ if (typeof value === "string" && value.trim() !== "") return value.trim();
1083
+ }
1084
+ const pairs = [];
1085
+ for (const [key, value] of Object.entries(record)) if (typeof value === "string" && value.trim() !== "") pairs.push(`${key}=${value.trim()}`);
1086
+ else if (typeof value === "number" || typeof value === "boolean") pairs.push(`${key}=${String(value)}`);
1087
+ if (pairs.length === 0) return void 0;
1088
+ const summary = pairs.join(" ");
1089
+ return summary.length > 200 ? `${summary.slice(0, 200)}…` : summary;
1090
+ }
1091
+ /**
1092
+ * Parse a session's wire log into transcript lines plus every usage record.
1093
+ * @param sessionDir - the session directory.
1094
+ * @returns the transcript.
1095
+ */
1096
+ async function readKimiTranscript(sessionDir) {
1097
+ const sessionId = sessionDir.split("session_").pop() ?? sessionDir;
1098
+ const lines = [];
1099
+ const usageRecords = [];
1100
+ let model;
1101
+ let modelTurn;
1102
+ let lineModel;
1103
+ const push = (line) => {
1104
+ lines.push({
1105
+ ...line,
1106
+ ...lineModel === void 0 ? {} : { model: lineModel }
1107
+ });
1108
+ };
1109
+ let turn = 0;
1110
+ let turnSeen = false;
1111
+ const callsByUuid = /* @__PURE__ */ new Map();
1112
+ const callsByToolCallId = /* @__PURE__ */ new Map();
1113
+ let text;
1114
+ try {
1115
+ text = await readFile(wirePath(sessionDir), "utf8");
1116
+ } catch (error) {
1117
+ if (error.code === "ENOENT") return {
1118
+ sessionId,
1119
+ lines,
1120
+ usageRecords
1121
+ };
1122
+ throw error;
1123
+ }
1124
+ for (const raw of text.split("\n")) {
1125
+ const line = raw.trim();
1126
+ if (line === "") continue;
1127
+ let event;
1128
+ try {
1129
+ event = JSON.parse(line);
1130
+ } catch {
1131
+ continue;
1132
+ }
1133
+ if (event.type === "usage.record") {
1134
+ if (typeof event.model === "string" && event.model !== "") {
1135
+ model = lineModel = event.model;
1136
+ modelTurn = turn || 1;
1137
+ }
1138
+ const usage = usageFromWire(event.usage);
1139
+ if (usage !== void 0) usageRecords.push({
1140
+ line: lines.length,
1141
+ usage
1142
+ });
1143
+ continue;
1144
+ }
1145
+ if (event.type === "llm.request") {
1146
+ lineModel = typeof event.model === "string" && event.model !== "" ? event.model : void 0;
1147
+ if (typeof event.model === "string" && event.model !== "") {
1148
+ model = event.model;
1149
+ modelTurn = turn || 1;
1150
+ }
1151
+ continue;
1152
+ }
1153
+ if (event.type === "turn.prompt") {
1154
+ lineModel = void 0;
1155
+ turnSeen = true;
1156
+ turn += 1;
1157
+ const prompt = textOf(event.input ?? []);
1158
+ if (prompt.trim() !== "" && !isSystemReminder(prompt)) push({
1159
+ kind: "user",
1160
+ text: prompt,
1161
+ turn: turn === 0 ? 1 : turn
1162
+ });
1163
+ continue;
1164
+ }
1165
+ if (event.type === "context.append_message") {
1166
+ const message = event.message;
1167
+ const role = message?.role;
1168
+ const textContent = textOf(message?.content ?? []);
1169
+ if (role === "user" && textContent.trim() !== "" && !isSystemReminder(textContent)) {
1170
+ const last = lines[lines.length - 1];
1171
+ if (turnSeen && last?.kind === "user" && last.text === textContent) continue;
1172
+ turnSeen = true;
1173
+ turn += 1;
1174
+ lineModel = void 0;
1175
+ push({
1176
+ kind: "user",
1177
+ text: textContent,
1178
+ turn: turn === 0 ? 1 : turn
1179
+ });
1180
+ } else if (role === "assistant" && textContent.trim() !== "") push({
1181
+ kind: "assistant",
1182
+ text: textContent,
1183
+ turn: turn === 0 ? 1 : turn
1184
+ });
1185
+ continue;
1186
+ }
1187
+ if (event.type === "context.append_loop_event") {
1188
+ const loop = event.event;
1189
+ if (loop === void 0) continue;
1190
+ const loopTurn = typeof loop.turnId === "number" ? loop.turnId + 1 : void 0;
1191
+ if (loopTurn !== void 0) {
1192
+ turn = Math.max(turn, loopTurn);
1193
+ turnSeen = true;
1194
+ }
1195
+ const part = loop.part;
1196
+ const lineTurn = turn === 0 ? 1 : turn;
1197
+ if (part?.type === "text" && part.text !== void 0 && part.text.trim() !== "") push({
1198
+ kind: "assistant",
1199
+ text: part.text,
1200
+ turn: lineTurn
1201
+ });
1202
+ else if (part?.type === "think" && part.think !== void 0 && part.think.trim() !== "") push({
1203
+ kind: "think",
1204
+ text: part.think,
1205
+ turn: lineTurn
1206
+ });
1207
+ else if (loop.type === "tool.call") {
1208
+ const call = loop;
1209
+ const name = call.toolCall?.name ?? call.name ?? "tool";
1210
+ const args = argsOf(name, call.toolCall?.args ?? call.args);
1211
+ const lineIndex = lines.length;
1212
+ push({
1213
+ kind: "tool",
1214
+ id: typeof call.toolCallId === "string" ? call.toolCallId : typeof call.uuid === "string" ? call.uuid : `kimi-tool-${lineIndex}`,
1215
+ name,
1216
+ ...args === void 0 ? {} : { args },
1217
+ turn: lineTurn
1218
+ });
1219
+ if (typeof call.uuid === "string") callsByUuid.set(call.uuid, lineIndex);
1220
+ if (typeof call.toolCallId === "string") callsByToolCallId.set(call.toolCallId, lineIndex);
1221
+ } else if (loop.type === "tool.result") {
1222
+ const result = loop;
1223
+ const output = result.result?.output;
1224
+ let target = typeof result.parentUuid === "string" ? callsByUuid.get(result.parentUuid) : void 0;
1225
+ if (target === void 0 && typeof result.toolCallId === "string") target = callsByToolCallId.get(result.toolCallId);
1226
+ if (target === void 0) {
1227
+ for (let index = lines.length - 1; index >= 0; index -= 1) if (lines[index]?.kind === "tool") {
1228
+ target = index;
1229
+ break;
1230
+ }
1231
+ }
1232
+ if (output !== void 0 && output.trim() !== "" && target !== void 0) {
1233
+ const current = lines[target];
1234
+ if (current !== void 0 && current.kind === "tool") lines[target] = {
1235
+ ...current,
1236
+ result: output
1237
+ };
1238
+ }
1239
+ } else if (loop.type === "content.part" && part?.type !== void 0 && part.type !== "text" && part.type !== "think") push({
1240
+ kind: "assistant",
1241
+ text: `[未支持的内容类型 ${part.type}]`,
1242
+ turn: lineTurn
1243
+ });
1244
+ }
1245
+ }
1246
+ return {
1247
+ sessionId,
1248
+ lines,
1249
+ usageRecords,
1250
+ ...model === void 0 ? {} : { model },
1251
+ ...modelTurn === void 0 ? {} : { modelTurn }
1252
+ };
1253
+ }
1254
+ /**
1255
+ * Map a wire `usage.record` payload onto the shared usage contract. kimi
1256
+ * reports input (uncached), cache read, cache creation, and output; the
1257
+ * creation counter is its cache-write analog. Non-numeric or missing
1258
+ * counters are dropped so the record stays usable when kimi omits a field.
1259
+ * @param raw - the event's `usage` value.
1260
+ * @returns the shared usage record, or undefined when nothing was reported.
1261
+ */
1262
+ function usageFromWire(raw) {
1263
+ const counters = raw;
1264
+ if (counters === void 0 || typeof counters !== "object") return void 0;
1265
+ const num = (value) => {
1266
+ const n = Number(value);
1267
+ return Number.isFinite(n) ? n : void 0;
1268
+ };
1269
+ const input = num(counters.inputOther);
1270
+ const output = num(counters.output);
1271
+ const cacheRead = num(counters.inputCacheRead);
1272
+ const cacheWrite = num(counters.inputCacheCreation);
1273
+ if (input === void 0 && output === void 0 && cacheRead === void 0 && cacheWrite === void 0) return;
1274
+ const usage = {
1275
+ inputTokens: input ?? 0,
1276
+ outputTokens: output ?? 0
1277
+ };
1278
+ if (cacheRead !== void 0 && cacheRead > 0) usage.cacheReadTokens = cacheRead;
1279
+ if (cacheWrite !== void 0 && cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
1280
+ return usage;
1281
+ }
1282
+ /**
1283
+ * Sum per-request usage records into one round total. Each `usage.record` is
1284
+ * a single LLM request's accounting, so a round's totals are the sum of the
1285
+ * records inside it.
1286
+ * @param records - the usage records to add.
1287
+ * @returns the summed usage, or undefined when the list is empty.
1288
+ */
1289
+ function sumUsageRecords(records) {
1290
+ let input = 0;
1291
+ let output = 0;
1292
+ let cacheRead = 0;
1293
+ let cacheWrite = 0;
1294
+ for (const record of records) {
1295
+ input += record.usage.inputTokens ?? 0;
1296
+ output += record.usage.outputTokens ?? 0;
1297
+ cacheRead += record.usage.cacheReadTokens ?? 0;
1298
+ cacheWrite += record.usage.cacheWriteTokens ?? 0;
1299
+ }
1300
+ if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) return void 0;
1301
+ const usage = {
1302
+ inputTokens: input,
1303
+ outputTokens: output
1304
+ };
1305
+ if (cacheRead > 0) usage.cacheReadTokens = cacheRead;
1306
+ if (cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
1307
+ return usage;
1308
+ }
1309
+ /**
1310
+ * Merge two usage windows into one running total — the delegation run's
1311
+ * per-round accumulator: each mirror pass (live poll, settle) returns its
1312
+ * own window's accounting, and the disjoint windows sum to the round's
1313
+ * total. `undefined` operands pass through, so callers accumulate without
1314
+ * absence checks.
1315
+ * @param left - the running total, or undefined before the first window.
1316
+ * @param right - the next window's usage, or undefined when it had none.
1317
+ * @returns the merged total, or undefined when both operands are absent.
1318
+ */
1319
+ function addTokenUsage(left, right) {
1320
+ if (left === void 0) return right;
1321
+ if (right === void 0) return left;
1322
+ return sumUsageRecords([{
1323
+ line: 0,
1324
+ usage: left
1325
+ }, {
1326
+ line: 0,
1327
+ usage: right
1328
+ }]);
1329
+ }
1330
+ /** Render a transcript as command-reply text. */
1331
+ function renderTranscript(transcript) {
1332
+ if (transcript.lines.length === 0) return `kimi session ${transcript.sessionId}: no readable transcript yet.`;
1333
+ const parts = [`kimi session ${transcript.sessionId}:`];
1334
+ for (const line of transcript.lines) if (line.kind === "user") parts.push(`\n用户: ${line.text}`);
1335
+ else if (line.kind === "assistant") parts.push(`\nkimi: ${line.text}`);
1336
+ else if (line.kind === "think") parts.push(`\n[思考] ${line.text}`);
1337
+ else {
1338
+ const call = `[工具 ${line.name}]${line.args !== void 0 ? ` ${line.args}` : ""}`;
1339
+ parts.push(`\n ↳ ${call}${line.result !== void 0 ? ` → ${line.result}` : ""}`);
1340
+ }
1341
+ return parts.join("\n");
1342
+ }
1343
+ //#endregion
1344
+ //#region lib/types/session-mirror.js
1345
+ /**
1346
+ * Mirror a kimi session's transcript into a dsh subagent session, so the
1347
+ * delegation shows up in the standard 子代理 surface and opening the child
1348
+ * session shows the full conversation (user prompts, kimi replies, thinking,
1349
+ * and tool activity with arguments and results) instead of a silent one-shot
1350
+ * call.
1351
+ * @module @khorsheed/dsh-local-agent-kimi/session-mirror
1352
+ */
1353
+ /** One user-role message event. */
1354
+ function userEvent(text) {
1355
+ return createUserMessage({
1356
+ content: [{
1357
+ type: "text",
1358
+ text
1359
+ }],
1360
+ source: { kind: "user" }
1361
+ });
1362
+ }
1363
+ /** One assistant-role message event, attributed to the kimi route. */
1364
+ function assistantEvent(blocks, model) {
1365
+ return createAssistantMessage({
1366
+ content: blocks,
1367
+ source: {
1368
+ provider: "kimi-cli",
1369
+ model: model ?? "unobserved"
1370
+ }
1371
+ });
1372
+ }
1373
+ /**
1374
+ * The step the next fold pass would assign in one turn (the max existing
1375
+ * step + 1). The token-granularity live
1376
+ * driver reserves its stream merge key here LAZILY at the first delta, so
1377
+ * tool cards folded before the stream started keep their chronological
1378
+ * place below it instead of the stream squatting on step 1.
1379
+ * @param childSession - the run's child session.
1380
+ * @param turn - the round's turn number.
1381
+ * @returns the next free step in the turn.
1382
+ */
1383
+ function nextKimiSessionStep(childSession, turn) {
1384
+ let next = 1;
1385
+ for (const event of childSession.snapshotEvents()) {
1386
+ const data = event.data;
1387
+ if (data.turn === turn && typeof data.step === "number") next = Math.max(next, data.step + 1);
1388
+ }
1389
+ return next;
1390
+ }
1391
+ /**
1392
+ * Fold one non-tool transcript line into message blocks. Thinking maps to
1393
+ * the native `reasoning` block so the standard conversation renders it as
1394
+ * thinking rather than a `[思考]` text prefix; reply text stays text (content
1395
+ * is never filtered — kimi's own injections remain visible). Tool lines are
1396
+ * NOT folded here: they become native `tool/call`/`tool/result` event pairs
1397
+ * (see the mirror loop), so the standard conversation renders them as tool
1398
+ * cards instead of `[工具 X]` text.
1399
+ */
1400
+ function lineBlocks(line) {
1401
+ if (line.kind === "think") return [{
1402
+ type: "reasoning",
1403
+ text: line.text
1404
+ }];
1405
+ return [{
1406
+ type: "text",
1407
+ text: line.text
1408
+ }];
1409
+ }
1410
+ /** The run-progress delta text for one transcript line (any kind). */
1411
+ function kimiLineProgressText(line) {
1412
+ if (line.kind !== "tool") return line.text;
1413
+ return `${`[工具 ${line.name}]${line.args !== void 0 ? ` ${line.args}` : ""}`}${line.result !== void 0 ? ` → ${line.result}` : ""}`;
1414
+ }
1415
+ /**
1416
+ * Mirror one kimi session's transcript into the dsh subagent session: each
1417
+ * user prompt becomes a `user/message`, each assistant reply (including
1418
+ * folded thinking and tool activity) an `assistant/message`, then the events
1419
+ * are appended to persistence so the child session is viewable.
1420
+ *
1421
+ * A resumed round (or a live poll) passes the already-mirrored
1422
+ * transcript-line count so only the delta is appended — re-mirroring earlier
1423
+ * lines would duplicate their messages. Turn numbers come from the transcript
1424
+ * lines themselves (the wire's 1-based turn, matching the provider's
1425
+ * `turn/start` numbering); step numbers CONTINUE from the assistant messages
1426
+ * already in the child session, so a round mirrored across several live polls
1427
+ * does not restart step 1 per poll.
1428
+ *
1429
+ * Every folded step is wrapped in the `step/start`–`step/end` boundary pair
1430
+ * the live conversation assembler requires: without a boundary the step never
1431
+ * enters the location index, the assistant message resolves to a turn-level
1432
+ * location, and the real-time view renders nothing (only a full rebuild,
1433
+ * which mints step drafts from the explicit coordinates, recovers it). A
1434
+ * think/assistant line that completes an in-flight stream folds at the
1435
+ * stream's reserved step instead (see {@link KimiMirrorStreams}), and a
1436
+ * sequential fold never takes a step a stream reserved.
1437
+ *
1438
+ * Usage accounting is exactly-once ACROSS passes: each `usage.record` is
1439
+ * tagged with the transcript position it occurred at, and a pass attaches the
1440
+ * records in the half-open range `(fromLines, newTotal]` — the records that
1441
+ * trail THIS pass's lines — summed onto the delta's final assistant message.
1442
+ * (A record flushed only after its line was already mirrored belongs to no
1443
+ * later pass and is dropped; kimi flushes content and usage together per
1444
+ * request, so this window is a flush race, not the norm.)
1445
+ * @param ctx - host context carrying the session persistence service.
1446
+ * @param childSession - the dsh subagent session created for this delegation.
1447
+ * @param homeDir - the `kimi` harness's scoped home.
1448
+ * @param kimiSessionId - the kimi session to mirror; omitted mirrors the most
1449
+ * recent session (a one-shot `kimi -p` run always creates a fresh one).
1450
+ * @param fromLines - transcript lines already mirrored into the child session
1451
+ * (0 for the first round); only lines after this offset are appended.
1452
+ * @returns the new total and the newly mirrored lines' texts.
1453
+ */
1454
+ async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId, fromLines = 0, options) {
1455
+ let workspaces;
1456
+ try {
1457
+ workspaces = await readdir(join(homeDir, "sessions"));
1458
+ } catch {
1459
+ return {
1460
+ total: 0,
1461
+ texts: []
1462
+ };
1463
+ }
1464
+ let transcript;
1465
+ if (kimiSessionId !== void 0) {
1466
+ const dirName = kimiSessionId.startsWith("session_") ? kimiSessionId : `session_${kimiSessionId}`;
1467
+ for (const workspace of workspaces) {
1468
+ const dir = join(homeDir, "sessions", workspace, dirName);
1469
+ try {
1470
+ transcript = await readKimiTranscript(dir);
1471
+ } catch {
1472
+ continue;
1473
+ }
1474
+ if (transcript.lines.length > 0) break;
1475
+ }
1476
+ } else {
1477
+ let newest;
1478
+ for (const workspace of workspaces) {
1479
+ let sessions;
1480
+ try {
1481
+ sessions = await readdir(join(homeDir, "sessions", workspace));
1482
+ } catch {
1483
+ continue;
1484
+ }
1485
+ for (const name of sessions) {
1486
+ if (!name.startsWith("session_")) continue;
1487
+ const dir = join(homeDir, "sessions", workspace, name);
1488
+ const meta = await stat(dir);
1489
+ if (newest === void 0 || meta.mtimeMs > newest.time) newest = {
1490
+ dir,
1491
+ time: meta.mtimeMs
1492
+ };
1493
+ }
1494
+ }
1495
+ if (newest !== void 0) try {
1496
+ transcript = await readKimiTranscript(newest.dir);
1497
+ } catch {
1498
+ transcript = void 0;
1499
+ }
1500
+ }
1501
+ if (transcript === void 0 || transcript.lines.length === 0) return {
1502
+ total: 0,
1503
+ texts: []
1504
+ };
1505
+ const newTotal = transcript.lines.length;
1506
+ const delta = transcript.lines.slice(fromLines);
1507
+ const observedModel = options?.turn === void 0 || transcript.modelTurn === options.turn ? transcript.model : void 0;
1508
+ const toolCalls = roundToolCalls(transcript.lines, options?.turn);
1509
+ const deltaUsage = sumUsageRecords(transcript.usageRecords.filter((record) => record.line >= fromLines && record.line <= newTotal));
1510
+ let lastAssistant = -1;
1511
+ for (let index = 0; index < delta.length; index += 1) {
1512
+ const line = delta[index];
1513
+ if (line !== void 0 && line.kind !== "user" && line.kind !== "tool") lastAssistant = index;
1514
+ }
1515
+ const steps = /* @__PURE__ */ new Map();
1516
+ const openCalls = /* @__PURE__ */ new Map();
1517
+ const settledCalls = /* @__PURE__ */ new Set();
1518
+ for (const event of childSession.snapshotEvents()) {
1519
+ const data = event.data;
1520
+ if (typeof data.turn === "number" && typeof data.step === "number") steps.set(data.turn, Math.max(steps.get(data.turn) ?? 0, data.step + 1));
1521
+ if (event.type === "tool/call") {
1522
+ const call = event.data;
1523
+ openCalls.set(call.callId, {
1524
+ turn: call.turn,
1525
+ step: call.step,
1526
+ seq: event.seq
1527
+ });
1528
+ } else if (event.type === "tool/result") {
1529
+ const message = event.data.message;
1530
+ const id = message?.toolCallId ?? message?.content?.[0]?.toolCallId;
1531
+ if (id !== void 0) {
1532
+ openCalls.delete(id);
1533
+ settledCalls.add(id);
1534
+ }
1535
+ }
1536
+ }
1537
+ /**
1538
+ * The next sequential fold step in one turn: the running counter, shifted
1539
+ * past every step an in-flight stream reserved (a stream's snapshots and
1540
+ * its completion fold live at the reserved coordinate — a sequential fold
1541
+ * landing there would write a second step/start, which the assembler
1542
+ * rejects).
1543
+ */
1544
+ const nextFoldStep = (turn) => {
1545
+ let step = steps.get(turn) ?? 1;
1546
+ const reserved = options?.streams?.reservedSteps(turn);
1547
+ if (reserved !== void 0 && reserved.length > 0) {
1548
+ const skip = new Set(reserved);
1549
+ while (skip.has(step)) step += 1;
1550
+ }
1551
+ steps.set(turn, step + 1);
1552
+ return step;
1553
+ };
1554
+ /** Whether this pass attached its window's usage to a folded message. */
1555
+ let usageAttached = false;
1556
+ const texts = [];
1557
+ for (let index = 0; index < fromLines && index < transcript.lines.length; index += 1) {
1558
+ const line = transcript.lines[index];
1559
+ if (line === void 0 || line.kind !== "tool" || line.result === void 0) continue;
1560
+ const call = openCalls.get(line.id);
1561
+ if (call === void 0) continue;
1562
+ openCalls.delete(line.id);
1563
+ settledCalls.add(line.id);
1564
+ childSession.append("tool/result", {
1565
+ turn: call.turn,
1566
+ step: call.step,
1567
+ message: createToolResultMessage({
1568
+ callId: line.id,
1569
+ content: [{
1570
+ type: "text",
1571
+ text: line.result
1572
+ }],
1573
+ isError: false
1574
+ })
1575
+ }, {
1576
+ surfaceOp: "append",
1577
+ sourceEventSeqs: [call.seq]
1578
+ });
1579
+ texts.push(kimiLineProgressText(line));
1580
+ }
1581
+ if (delta.length === 0) {
1582
+ if (texts.length > 0) await persistChildSession(ctx, childSession);
1583
+ return {
1584
+ total: newTotal,
1585
+ texts,
1586
+ ...deltaUsage !== void 0 ? { usage: deltaUsage } : {},
1587
+ ...usageAttached ? { usageAttached: true } : {},
1588
+ ...observedModel !== void 0 ? { model: observedModel } : {},
1589
+ ...toolCalls !== void 0 ? { toolCalls } : {}
1590
+ };
1591
+ }
1592
+ for (let index = 0; index < delta.length; index += 1) {
1593
+ const line = delta[index];
1594
+ if (line === void 0) continue;
1595
+ const turn = line.turn;
1596
+ if (line.kind === "user") {
1597
+ if (userAlreadyAppended(childSession, turn, line.text)) continue;
1598
+ steps.set(turn, 1);
1599
+ childSession.append("user/message", userEvent(line.text), { surfaceOp: "append" });
1600
+ texts.push(line.text);
1601
+ } else if (line.kind === "tool") {
1602
+ const reserved = options?.streams?.completeTool?.(line);
1603
+ const step = reserved?.step ?? nextFoldStep(turn);
1604
+ steps.set(turn, Math.max(steps.get(turn) ?? 1, step + 1));
1605
+ if (reserved?.opened !== true) childSession.append("step/start", {
1606
+ turn,
1607
+ step
1608
+ });
1609
+ const call = childSession.append("tool/call", {
1610
+ turn,
1611
+ step,
1612
+ callId: line.id,
1613
+ name: line.name,
1614
+ arguments: line.args ?? ""
1615
+ });
1616
+ texts.push(kimiLineProgressText(line));
1617
+ if (line.result !== void 0) childSession.append("tool/result", {
1618
+ turn,
1619
+ step,
1620
+ message: createToolResultMessage({
1621
+ callId: line.id,
1622
+ content: [{
1623
+ type: "text",
1624
+ text: line.result
1625
+ }],
1626
+ isError: false
1627
+ })
1628
+ }, {
1629
+ surfaceOp: "append",
1630
+ sourceEventSeqs: [call.seq]
1631
+ });
1632
+ childSession.append("step/end", {
1633
+ turn,
1634
+ step
1635
+ });
1636
+ } else {
1637
+ const completion = options?.streams?.completeStream(line);
1638
+ if (completion !== void 0) {
1639
+ steps.set(turn, Math.max(steps.get(turn) ?? 1, completion.step + 1));
1640
+ if (!completion.opened) childSession.append("step/start", {
1641
+ turn,
1642
+ step: completion.step
1643
+ });
1644
+ if (index === lastAssistant && deltaUsage !== void 0) usageAttached = true;
1645
+ childSession.append("assistant/message", {
1646
+ turn,
1647
+ step: completion.step,
1648
+ message: assistantEvent(lineBlocks(line), line.model),
1649
+ stream: [],
1650
+ ...index === lastAssistant && deltaUsage !== void 0 ? { usage: deltaUsage } : {}
1651
+ }, { surfaceOp: "append" });
1652
+ childSession.append("step/end", {
1653
+ turn,
1654
+ step: completion.step
1655
+ });
1656
+ texts.push(kimiLineProgressText(line));
1657
+ continue;
1658
+ }
1659
+ const step = nextFoldStep(turn);
1660
+ childSession.append("step/start", {
1661
+ turn,
1662
+ step
1663
+ });
1664
+ if (index === lastAssistant && deltaUsage !== void 0) usageAttached = true;
1665
+ childSession.append("assistant/message", {
1666
+ turn,
1667
+ step,
1668
+ message: assistantEvent(lineBlocks(line), line.model),
1669
+ stream: [],
1670
+ ...index === lastAssistant && deltaUsage !== void 0 ? { usage: deltaUsage } : {}
1671
+ }, { surfaceOp: "append" });
1672
+ childSession.append("step/end", {
1673
+ turn,
1674
+ step
1675
+ });
1676
+ texts.push(kimiLineProgressText(line));
1677
+ }
1678
+ }
1679
+ await persistChildSession(ctx, childSession);
1680
+ return {
1681
+ total: newTotal,
1682
+ texts,
1683
+ ...deltaUsage !== void 0 ? { usage: deltaUsage } : {},
1684
+ ...usageAttached ? { usageAttached: true } : {},
1685
+ ...observedModel !== void 0 ? { model: observedModel } : {},
1686
+ ...toolCalls !== void 0 ? { toolCalls } : {}
1687
+ };
1688
+ }
1689
+ /**
1690
+ * Count one round's tool calls out of a kimi transcript, keyed by the name the
1691
+ * wire gave each tool. No turn named, or no `tool.call` line carrying it,
1692
+ * yields undefined — a round reports nothing rather than a zero it did not
1693
+ * observe. Reads only the transcript lines the mirror already folded.
1694
+ * @param lines - the whole session transcript.
1695
+ * @param turn - the round's 1-based turn, when the caller named one.
1696
+ * @returns the round's accounting, or undefined.
1697
+ */
1698
+ function roundToolCalls(lines, turn) {
1699
+ if (turn === void 0) return void 0;
1700
+ const byName = {};
1701
+ let count = 0;
1702
+ for (const line of lines) {
1703
+ if (line.kind !== "tool" || line.turn !== turn) continue;
1704
+ count += 1;
1705
+ byName[line.name] = (byName[line.name] ?? 0) + 1;
1706
+ }
1707
+ return count === 0 ? void 0 : {
1708
+ count,
1709
+ byName
1710
+ };
1711
+ }
1712
+ /**
1713
+ * Whether the turn already carries a user/message with this exact text (the
1714
+ * live driver's round-start append). Scoped to the events at or after this
1715
+ * turn's turn/start so identical prompts across rounds stay independent.
1716
+ */
1717
+ function userAlreadyAppended(childSession, turn, text) {
1718
+ let turnStartSeq = -1;
1719
+ for (const event of childSession.snapshotEvents()) if (event.type === "turn/start" && event.data.turn === turn) turnStartSeq = event.seq;
1720
+ if (turnStartSeq < 0) return false;
1721
+ for (const event of childSession.snapshotEvents()) {
1722
+ if (event.seq < turnStartSeq || event.type !== "user/message") continue;
1723
+ if ((event.data.content ?? []).map((block) => block.text ?? "").join("") === text) return true;
1724
+ }
1725
+ return false;
1726
+ }
1727
+ //#endregion
1728
+ //#region lib/types/credential-guard.js
1729
+ /**
1730
+ * Kimi credential sentinel. The kimi CLI occasionally wipes
1731
+ * `credentials/kimi-code.json` into an empty shell (both tokens zero-length)
1732
+ * on its own failure paths — observed twice in production, each time killing
1733
+ * every later delegation with "Authentication required" until someone
1734
+ * restored the file by hand. The sentinel:
1735
+ *
1736
+ * - backs the file up whenever a VALID credential is observed (the backup
1737
+ * lives inside `credentials/` so the family's logout — a directory delete —
1738
+ * clears it too);
1739
+ * - restores the backup when the file is an exact empty shell (both tokens
1740
+ * empty strings) — the only corruption signature we trust;
1741
+ * - logs every state transition with a process snapshot, so the next wipe
1742
+ * can be attributed (which process, at which moment).
1743
+ *
1744
+ * Restore is deliberately narrow: a revoked-elsewhere credential never takes
1745
+ * the empty-shell shape, and a stale backup at worst produces one more loud
1746
+ * 401, never silent misbehavior.
1747
+ * @module @khorsheed/dsh-local-agent-kimi/credential-guard
1748
+ */
1749
+ /** The credential file inside the scoped home, and its sentinel backup. */
1750
+ const CREDENTIAL_REL = join("credentials", "kimi-code.json");
1751
+ const BACKUP_REL = `${CREDENTIAL_REL}.bak`;
1752
+ /** Last observed state signature per home (`${mtimeMs}:${kind}`) — transition logging. */
1753
+ const lastSeen = /* @__PURE__ */ new Map();
1754
+ /** A one-line process snapshot for wipe attribution (kimi/dsh processes only). */
1755
+ function processSnapshot() {
1756
+ return new Promise((resolve) => {
1757
+ execFile("ps", ["-eo", "pid,ppid,etime,command"], { timeout: 5e3 }, (error, stdout) => {
1758
+ if (error !== null) {
1759
+ resolve(`(ps failed: ${error.message})`);
1760
+ return;
1761
+ }
1762
+ resolve(stdout.split("\n").filter((line) => /\bkimi\b|dsh/.test(line) && !line.includes("grep")).slice(0, 12).join(" ; "));
1763
+ });
1764
+ });
1765
+ }
1766
+ /** Classify the credential file's current content. */
1767
+ async function observe(file) {
1768
+ let raw;
1769
+ try {
1770
+ raw = await readFile(file, "utf8");
1771
+ } catch {
1772
+ return {
1773
+ state: "absent",
1774
+ mtimeMs: 0
1775
+ };
1776
+ }
1777
+ let mtimeMs = 0;
1778
+ try {
1779
+ mtimeMs = (await stat(file)).mtimeMs;
1780
+ } catch {}
1781
+ try {
1782
+ const parsed = JSON.parse(raw);
1783
+ const empty = (v) => typeof v === "string" && v.length === 0;
1784
+ if (empty(parsed.access_token) && empty(parsed.refresh_token)) return {
1785
+ state: "empty-shell",
1786
+ mtimeMs
1787
+ };
1788
+ return {
1789
+ state: "valid",
1790
+ mtimeMs
1791
+ };
1792
+ } catch {
1793
+ return {
1794
+ state: "absent",
1795
+ mtimeMs
1796
+ };
1797
+ }
1798
+ }
1799
+ /**
1800
+ * Guard the scoped home's kimi credential: back up a valid file, restore the
1801
+ * backup over an empty shell, log transitions.
1802
+ * @param homeDir - the `kimi` harness's scoped home.
1803
+ * @param log - warn channel (plugin logger when available).
1804
+ * @returns true when a usable credential exists after the guard ran.
1805
+ */
1806
+ async function guardKimiCredential(homeDir, log) {
1807
+ const file = join(homeDir, CREDENTIAL_REL);
1808
+ const backup = join(homeDir, BACKUP_REL);
1809
+ const { state, mtimeMs } = await observe(file);
1810
+ const signature = `${mtimeMs}:${state}`;
1811
+ const previous = lastSeen.get(homeDir);
1812
+ lastSeen.set(homeDir, signature);
1813
+ if (state === "valid") {
1814
+ if (previous !== void 0 && previous !== signature) log?.(`local-agent-kimi: credential refreshed (mtime ${new Date(mtimeMs).toISOString()}), snapshotting the backup`);
1815
+ await writeFile(backup, await readFile(file), { mode: 384 });
1816
+ return true;
1817
+ }
1818
+ if (state === "empty-shell") {
1819
+ const snapshot = await processSnapshot();
1820
+ log?.(`local-agent-kimi: CREDENTIAL EMPTY SHELL detected (mtime ${new Date(mtimeMs).toISOString()}); previous state ${previous ?? "unknown"}; processes: ${snapshot}`);
1821
+ let backupState = "absent";
1822
+ try {
1823
+ backupState = (await observe(backup)).state;
1824
+ } catch {}
1825
+ if (backupState === "valid") {
1826
+ await copyFile(backup, file);
1827
+ log?.("local-agent-kimi: credential restored from the sentinel backup — the next delegation should succeed");
1828
+ return true;
1829
+ }
1830
+ log?.("local-agent-kimi: no usable backup to restore; the harness needs a fresh login");
1831
+ return false;
1832
+ }
1833
+ return false;
1834
+ }
1835
+ //#endregion
1836
+ //#region lib/types/provision.js
1837
+ /**
1838
+ * Kimi Code scoped-home provisioning: a working `config.toml` (provider and
1839
+ * model definitions) is required for the ACP server to authenticate. The
1840
+ * scoped home is provisioned once — from the user's own config, redacted —
1841
+ * so the scoped setup mirrors the user's models while carrying no secrets.
1842
+ * Sign-out removes the credentials and OAuth cache so a later login can
1843
+ * authorize a different account. The agent preset the delegation tool row
1844
+ * needs is bootstrapped into the user preset root on first boot, so
1845
+ * installing the bundle needs no manual copy step.
1846
+ * @module @khorsheed/dsh-local-agent-kimi/provision
1847
+ */
1848
+ const REAL_CONFIG = join(homedir(), ".kimi-code", "config.toml");
1849
+ /** Credentials directory inside the scoped home; its presence marks auth. */
1850
+ const CREDENTIALS_DIR$1 = "credentials";
1851
+ /** OAuth token cache directory inside the scoped home. */
1852
+ const OAUTH_DIR = "oauth";
1853
+ /**
1854
+ * The managed provider's endpoint, written into a fresh scoped config and the
1855
+ * baseline the eval snapshot compares against: a scoped config still pointing
1856
+ * here routes through kimi's own service, i.e. no custom endpoint is in force.
1857
+ */
1858
+ const KIMI_MANAGED_BASE_URL = "https://api.kimi.com/coding/v1";
1859
+ /** Minimal managed config for a fresh home with no user config to mirror. */
1860
+ function minimalConfig(model, thinkingEffort) {
1861
+ return `default_model = "${model}"
1862
+
1863
+ [thinking]
1864
+ enabled = true
1865
+ effort = "${thinkingEffort}"
1866
+
1867
+ [providers."managed:kimi-code"]
1868
+ base_url = "${KIMI_MANAGED_BASE_URL}"
1869
+ type = "kimi"
1870
+ api_key = ""
1871
+
1872
+ [providers."managed:kimi-code".oauth]
1873
+ storage = "file"
1874
+ key = "oauth/kimi-code"
1875
+
1876
+ [models."${model}"]
1877
+ provider = "managed:kimi-code"
1878
+ model = "${model.split("/").pop()}"
1879
+ max_context_size = 1048576
1880
+ capabilities = [ "thinking", "always_thinking", "image_in", "video_in", "tool_use" ]
1881
+ display_name = "${model}"
1882
+ support_efforts = [ "low", "high", "max" ]
1883
+ default_effort = "${thinkingEffort}"
1884
+ `;
1885
+ }
1886
+ /**
1887
+ * Redact every api_key value so the scoped copy carries no secrets. The
1888
+ * kimi-managed provider keeps an empty api_key (its auth routes through
1889
+ * oauth), which is exactly what an empty value means here.
1890
+ * @param config - the user's config text.
1891
+ * @returns the same config with all api_key values blanked.
1892
+ */
1893
+ function redactApiKeys(config) {
1894
+ return config.replace(/api_key\s*=\s*"[^"]*"/gu, "api_key = \"\"");
1895
+ }
1896
+ /**
1897
+ * Provision `<homeDir>/config.toml` unless one already exists. A present
1898
+ * config is respected untouched (a person may have edited it); otherwise the
1899
+ * user's own real config is copied redacted so the scoped home mirrors the
1900
+ * user's models; with no real config, a minimal kimi-managed config is
1901
+ * written. `thinkingEffort` feeds the minimal config's `[thinking] effort`
1902
+ * (and the model's `default_effort`) — it applies at provision time only,
1903
+ * never retroactively to an existing config.
1904
+ * @param homeDir - the `kimi` harness's scoped home.
1905
+ * @param model - the kimi-managed model id for a fresh minimal config.
1906
+ * @param thinkingEffort - the reasoning effort a fresh minimal config pins.
1907
+ * @returns true when a config was written, false when one already existed.
1908
+ */
1909
+ async function provisionKimiConfig(homeDir, model, thinkingEffort) {
1910
+ try {
1911
+ await readFile(join(homeDir, "config.toml"));
1912
+ return false;
1913
+ } catch (error) {
1914
+ if (error.code !== "ENOENT") throw error;
1915
+ }
1916
+ let config = minimalConfig(model, thinkingEffort);
1917
+ try {
1918
+ config = redactApiKeys(await readFile(REAL_CONFIG, "utf8"));
1919
+ } catch (error) {
1920
+ if (error.code !== "ENOENT") throw error;
1921
+ }
1922
+ await writeFile(join(homeDir, "config.toml"), config, { flag: "wx" });
1923
+ return true;
1924
+ }
1925
+ /** The managed-service provider section provisioned into the scoped config. */
1926
+ const MANAGED_PROVIDER_SECTION = "providers.\"managed:kimi-code\"";
1927
+ /**
1928
+ * Read the scoped config's effective LLM endpoint: the
1929
+ * `[providers."managed:kimi-code"].base_url` key, if present. A missing or
1930
+ * malformed config yields undefined (the CLI falls back to its default
1931
+ * endpoint). Used only for diagnostics — the config itself is authoritative,
1932
+ * and a user-edited value is respected untouched.
1933
+ * @param homeDir - the `kimi` harness's scoped home.
1934
+ * @returns the configured base URL, or undefined when absent/unreadable.
1935
+ */
1936
+ async function readKimiBaseUrl(homeDir) {
1937
+ let text;
1938
+ try {
1939
+ text = await readFile(join(homeDir, "config.toml"), "utf8");
1940
+ } catch {
1941
+ return;
1942
+ }
1943
+ let section = "";
1944
+ for (const rawLine of text.split("\n")) {
1945
+ const line = rawLine.trim();
1946
+ const header = /^\[+([^\]]+)\]+$/.exec(line);
1947
+ if (header !== null) {
1948
+ section = header[1];
1949
+ continue;
1950
+ }
1951
+ if (section === MANAGED_PROVIDER_SECTION) {
1952
+ const match = /^base_url\s*=\s*"([^"]*)"$/.exec(line);
1953
+ if (match !== null) return match[1];
1954
+ }
1955
+ }
1956
+ }
1957
+ /**
1958
+ * Read the scoped config's configured default model: the top-level
1959
+ * `default_model` key — the selection a plain `kimi -p` round runs with. A
1960
+ * missing or malformed config, or no key, yields undefined — the honest
1961
+ * "unknown", never a guessed default. Read-only: the config is authoritative
1962
+ * and a user-edited value is reported as-is.
1963
+ * @param homeDir - the `kimi` harness's scoped home.
1964
+ * @returns the configured model identifier, or undefined when none is set.
1965
+ */
1966
+ async function readKimiDefaultModel(homeDir) {
1967
+ let text;
1968
+ try {
1969
+ text = await readFile(join(homeDir, "config.toml"), "utf8");
1970
+ } catch {
1971
+ return;
1972
+ }
1973
+ let section = "";
1974
+ for (const rawLine of text.split("\n")) {
1975
+ const line = rawLine.trim();
1976
+ const header = /^\[+([^\]]+)\]+$/.exec(line);
1977
+ if (header !== null) {
1978
+ section = header[1];
1979
+ continue;
1980
+ }
1981
+ if (section !== "") continue;
1982
+ const key = /^(\w+)\s*=\s*"([^"]*)"$/.exec(line);
1983
+ if (key !== null && key[1] === "default_model") return key[2];
1984
+ }
1985
+ }
1986
+ /**
1987
+ * List the model identifiers the scoped config DEFINES: every
1988
+ * `[models."…"]` table key (bare `[models.x]` keys included). This is the
1989
+ * pickable vocabulary a model surface can offer without hardcoding a catalog —
1990
+ * the same table `-m` resolves against. A missing or malformed config yields
1991
+ * an empty list. Read-only: the config is authoritative.
1992
+ * @param homeDir - the `kimi` harness's scoped home.
1993
+ * @returns the defined model identifiers, in file order, deduplicated.
1994
+ */
1995
+ async function listKimiConfigModels(homeDir) {
1996
+ let text;
1997
+ try {
1998
+ text = await readFile(join(homeDir, "config.toml"), "utf8");
1999
+ } catch {
2000
+ return [];
2001
+ }
2002
+ const models = [];
2003
+ for (const rawLine of text.split("\n")) {
2004
+ const header = /^\[+([^\]]+)\]+$/.exec(rawLine.trim());
2005
+ if (header === null) continue;
2006
+ const table = /^models\.(?:"([^"]+)"|([\w-]+))$/.exec(header[1]);
2007
+ const name = table?.[1] ?? table?.[2];
2008
+ if (name !== void 0 && name !== "" && !models.includes(name)) models.push(name);
2009
+ }
2010
+ return models;
2011
+ }
2012
+ /**
2013
+ * Rewrite the scoped config's top-level `default_model` to `model`, so the
2014
+ * NEXT process started under this home runs that model. This is the resident
2015
+ * path's only lever: `kimi acp` takes no model flag (verified against kimi
2016
+ * 0.39.1), while `kimi -p` takes `-m` — so the one-shot rounds pass the flag
2017
+ * and the live driver writes the file before spawning its runtime.
2018
+ *
2019
+ * The write is surgical: the first top-level `default_model` assignment is
2020
+ * replaced in place and everything else — model tables, providers, thinking
2021
+ * effort, comments, key order — is left byte-identical. A config with no
2022
+ * top-level `default_model` gains one as its first line; an absent or
2023
+ * unreadable config is left alone (provisioning owns creation, and inventing
2024
+ * a config here would hide a broken home).
2025
+ * @param homeDir - the `kimi` harness's scoped home.
2026
+ * @param model - the model identifier to pin as the default.
2027
+ * @returns true when the file now names `model`, false when there was no
2028
+ * config to write into.
2029
+ */
2030
+ async function writeKimiDefaultModel(homeDir, model) {
2031
+ const path = join(homeDir, "config.toml");
2032
+ let text;
2033
+ try {
2034
+ text = await readFile(path, "utf8");
2035
+ } catch {
2036
+ return false;
2037
+ }
2038
+ const assignment = `default_model = "${model.replace(/\\/gu, "\\\\").replace(/"/gu, "\\\"")}"`;
2039
+ const lines = text.split("\n");
2040
+ let section = "";
2041
+ for (const [index, rawLine] of lines.entries()) {
2042
+ const line = rawLine.trim();
2043
+ const header = /^\[+([^\]]+)\]+$/.exec(line);
2044
+ if (header !== null) {
2045
+ section = header[1];
2046
+ continue;
2047
+ }
2048
+ if (section !== "") continue;
2049
+ const key = /^(\w+)\s*=/.exec(line);
2050
+ if (key === null || key[1] !== "default_model") continue;
2051
+ if (line === assignment) return true;
2052
+ lines[index] = assignment;
2053
+ await writeFile(path, lines.join("\n"), "utf8");
2054
+ return true;
2055
+ }
2056
+ await writeFile(path, `${assignment}\n${text}`, "utf8");
2057
+ return true;
2058
+ }
2059
+ /**
2060
+ * Read the scoped config's effective reasoning effort: the `[thinking] effort`
2061
+ * key, falling back to a `[models."…"]` section's `default_effort` when the
2062
+ * thinking table carries none (the CLI resolves effort the same way). A
2063
+ * missing or malformed config, or no effort key anywhere, yields undefined —
2064
+ * the honest "unknown", never a guessed default. Read-only: the config is
2065
+ * authoritative, a user-edited value is reported as-is.
2066
+ * @param homeDir - the `kimi` harness's scoped home.
2067
+ * @returns the effective effort, or undefined when it cannot be determined.
2068
+ */
2069
+ async function readKimiReasoningEffort(homeDir) {
2070
+ let text;
2071
+ try {
2072
+ text = await readFile(join(homeDir, "config.toml"), "utf8");
2073
+ } catch {
2074
+ return;
2075
+ }
2076
+ let section = "";
2077
+ let thinkingEffort;
2078
+ let modelDefaultEffort;
2079
+ for (const rawLine of text.split("\n")) {
2080
+ const line = rawLine.trim();
2081
+ const header = /^\[+([^\]]+)\]+$/.exec(line);
2082
+ if (header !== null) {
2083
+ section = header[1];
2084
+ continue;
2085
+ }
2086
+ const key = /^(\w+)\s*=\s*"([^"]*)"$/.exec(line);
2087
+ if (key === null) continue;
2088
+ if (section === "thinking" && key[1] === "effort" && thinkingEffort === void 0) thinkingEffort = key[2];
2089
+ if (section.startsWith("models.") && key[1] === "default_effort" && modelDefaultEffort === void 0) modelDefaultEffort = key[2];
2090
+ }
2091
+ return thinkingEffort ?? modelDefaultEffort;
2092
+ }
2093
+ /**
2094
+ * Whether the scoped config's permission rules auto-approve the subagent's
2095
+ * tool use: the `Bash(*)` allow rule the provisioning gate
2096
+ * ({@link ensureKimiPermissions}) keys on, detected with the exact same
2097
+ * substring so the snapshot and the provisioning can never disagree about
2098
+ * the same file.
2099
+ * @param homeDir - the `kimi` harness's scoped home.
2100
+ * @returns whether the auto-approve rule is present.
2101
+ */
2102
+ async function readKimiAutoApprove(homeDir) {
2103
+ let text;
2104
+ try {
2105
+ text = await readFile(join(homeDir, "config.toml"), "utf8");
2106
+ } catch {
2107
+ return false;
2108
+ }
2109
+ return text.includes("pattern = \"Bash(*)");
2110
+ }
2111
+ /** Permission rule block letting the kimi subagent run shell commands. */
2112
+ const KIMI_PERMISSION_BLOCK = `
2113
+ [[permission.rules]]
2114
+ decision = "allow"
2115
+ pattern = "Bash(*)"
2116
+ reason = "let the kimi subagent run shell commands"
2117
+ `;
2118
+ /**
2119
+ * Ensure the scoped config allows the subagent's tool use. The one-shot
2120
+ * `kimi -p` child uses the config's permission rules, so without a rule it
2121
+ * answers without executing tools. Idempotent; an existing rule is respected.
2122
+ * @param homeDir - the `kimi` harness's scoped home.
2123
+ */
2124
+ async function ensureKimiPermissions(homeDir) {
2125
+ const file = join(homeDir, "config.toml");
2126
+ let content;
2127
+ try {
2128
+ content = await readFile(file, "utf8");
2129
+ } catch (error) {
2130
+ if (error.code === "ENOENT") return;
2131
+ throw error;
2132
+ }
2133
+ if (content.includes("pattern = \"Bash(*)")) return;
2134
+ await writeFile(file, `${content.trimEnd()}\n${KIMI_PERMISSION_BLOCK}`);
2135
+ }
2136
+ /**
2137
+ * Sign out of the scoped account: remove the credentials and OAuth token
2138
+ * cache, so `/<name> status` reports not authenticated and the next login
2139
+ * authorizes a fresh account. The kimi CLI has no `logout` command, so the
2140
+ * scoped files are the logout path.
2141
+ * @param homeDir - the `kimi` harness's scoped home.
2142
+ */
2143
+ async function kimiLogout(homeDir) {
2144
+ await Promise.all([rm(join(homeDir, CREDENTIALS_DIR$1), {
2145
+ recursive: true,
2146
+ force: true
2147
+ }), rm(join(homeDir, OAUTH_DIR), {
2148
+ recursive: true,
2149
+ force: true
2150
+ })]);
2151
+ }
2152
+ //#endregion
2153
+ //#region lib/types/live-driver.js
2154
+ /**
2155
+ * The kimi provider's live driver: one resident `kimi acp` process per member
2156
+ * (child session), driven over the vendor's Agent Client Protocol (ACP) —
2157
+ * JSON-RPC NDJSON over stdio (protocol-probed against kimi 0.36.1; the
2158
+ * harness `subagent-acp` package is the reference implementation). A
2159
+ * delegation round is a `session/prompt` on the member's resident session
2160
+ * instead of a fresh `kimi -p` process, which buys what the process boundary
2161
+ * could not: runtime-level graceful interrupt (`session/cancel`; the process
2162
+ * survives and the session stays continuable) and push-driven mirroring
2163
+ * (`session/update` notifications trigger throttled file-mirror passes — see
2164
+ * below why the transcript itself stays file-sourced).
2165
+ *
2166
+ * Mirroring contract (the kimi-specific call): kimi's ACP updates are
2167
+ * token-level chunks, NOT the wire.jsonl line fold the exec mirror owns — so
2168
+ * completed items always fold from the file: push events merely TRIGGER
2169
+ * throttled `mirrorKimiDelta` passes and the settle pass stays authoritative
2170
+ * (one fold, one offset, zero divergence between the two drivers). Every
2171
+ * transcript item folds 1:1 into the child session log.
2172
+ * Live output also mirrors the in-flight item: chunk
2173
+ * deltas accumulate per streaming item and append throttled snapshot
2174
+ * assistant/messages at the item's reserved (turn, step) — the host folds
2175
+ * repeated settles at one coordinate into one live-updating chat node — and
2176
+ * the wire line completing the item folds at the same step, finalizing it.
2177
+ *
2178
+ * Lifecycle mirrors the dsh/codex live drivers' discipline: lazy spawn with an
2179
+ * initialize handshake (loadSession capability required — crash recovery is
2180
+ * `session/load` of the recorded session id), one in-flight spawn per member,
2181
+ * idle-timeout reclaim (stdin EOF → grace → SIGTERM ladder), a channel breaker
2182
+ * with cooldown, `disposeAll` on unload, and cancellation honored in every
2183
+ * window. The wire adapter is self-contained (no `@agentclientprotocol/sdk`
2184
+ * dependency): StringDecoder framing, request correlation, and unattended
2185
+ * auto-answers for `session/request_permission` (allow — matching the exec
2186
+ * mode's auto-approve behavior; the proposal's default policy).
2187
+ * @module @khorsheed/dsh-local-agent-kimi/live-driver
2188
+ */
2189
+ /** Default idle lifetime of an unused resident runtime before reclaim. */
2190
+ const DEFAULT_LIVE_IDLE_MS = 18e5;
2191
+ /** Default timeout for a wire request acknowledgement (never for session/prompt). */
2192
+ const DEFAULT_LIVE_REQUEST_TIMEOUT_MS = 3e4;
2193
+ /** Default timeout for the ACP handshake (a cold boot can be slow). */
2194
+ const DEFAULT_LIVE_INITIALIZE_TIMEOUT_MS = 6e4;
2195
+ /** Bounded wait for an interrupted turn to converge while a run disposes. */
2196
+ const DEFAULT_LIVE_DISPOSE_CONVERGE_MS = 5e3;
2197
+ /** How long a tripped channel breaker stays on the exec fallback. */
2198
+ const DEFAULT_LIVE_CHANNEL_RETRY_MS = 3e5;
2199
+ /** Default minimum interval between one streaming item's snapshot messages. */
2200
+ const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = LIVE_FLUSH_INTERVAL_MS;
2201
+ /** Grace between stdin EOF and SIGTERM when reclaiming an ACP server. */
2202
+ const RECLAIM_EOF_GRACE_MS = 1e3;
2203
+ /** The ACP protocol version this driver speaks (kimi 0.36.1 probe). */
2204
+ const ACP_PROTOCOL_VERSION = 1;
2205
+ /**
2206
+ * The ACP channel could not come up (spawn failure, handshake
2207
+ * timeout/mismatch, or a missing loadSession capability). The provider
2208
+ * catches exactly this and falls back to the exec one-shot; any other error
2209
+ * is a real round failure.
2210
+ */
2211
+ var LiveChannelUnavailableError = class extends Error {
2212
+ constructor(message) {
2213
+ super(message);
2214
+ this.name = "LiveChannelUnavailableError";
2215
+ }
2216
+ };
2217
+ const DEFAULT_TIMEOUTS = {
2218
+ initializeMs: DEFAULT_LIVE_INITIALIZE_TIMEOUT_MS,
2219
+ requestMs: DEFAULT_LIVE_REQUEST_TIMEOUT_MS,
2220
+ convergeMs: DEFAULT_LIVE_DISPOSE_CONVERGE_MS,
2221
+ channelRetryMs: DEFAULT_LIVE_CHANNEL_RETRY_MS,
2222
+ mirrorThrottleMs: 500
2223
+ };
2224
+ /** Settle-mirror quiescence: poll cadence, required consecutive stable reads, and the overall bound. */
2225
+ const SETTLE_MIRROR_POLL_MS = 300;
2226
+ const SETTLE_MIRROR_STABLE_READS = 3;
2227
+ const SETTLE_MIRROR_QUIESCE_MS = 3e3;
2228
+ function thrown$1(value) {
2229
+ return value instanceof Error ? value : new Error(String(value));
2230
+ }
2231
+ /**
2232
+ * Auth-shaped failure signatures in kimi's error output: the endpoint
2233
+ * rejected the credential. Mirrors the exec path's detector
2234
+ * (KIMI_AUTH_FAILURE in kimi-cli-provider.ts); the live path matches against
2235
+ * session/prompt failure messages, which is where a mid-run 401 surfaces
2236
+ * when the process does not exit.
2237
+ */
2238
+ const KIMI_LIVE_AUTH_FAILURE = /401|unauthorized|invalid api key|not authenticated|authentication required/i;
2239
+ /**
2240
+ * kimi ACP session ids are directory names (`session_<uuid>`); the family
2241
+ * delegation record convention is the bare uuid (the exec path's settle-time
2242
+ * stderr parse). Strip the prefix so live- and exec-written records stay
2243
+ * interchangeable.
2244
+ */
2245
+ function bareKimiSessionId(acpSessionId) {
2246
+ return acpSessionId.startsWith("session_") ? acpSessionId.slice(8) : acpSessionId;
2247
+ }
2248
+ /** The ACP-native form of a recorded kimi session id (idempotent). */
2249
+ function acpKimiSessionId(recordedId) {
2250
+ return recordedId.startsWith("session_") ? recordedId : `session_${recordedId}`;
2251
+ }
2252
+ function delay(ms) {
2253
+ return new Promise((resolve) => setTimeout(resolve, ms));
2254
+ }
2255
+ /**
2256
+ * Map an ACP prompt stop reason onto the shared stop contract (the harness
2257
+ * subagent-acp mapping, verbatim): an unclean stop is never 'completed'.
2258
+ */
2259
+ function acpStopReasonToHarness(reason) {
2260
+ switch (reason) {
2261
+ case "end_turn": return "completed";
2262
+ case "max_tokens": return "max-tokens";
2263
+ case "refusal": return "refusal";
2264
+ case "cancelled": return "aborted";
2265
+ default: return "error";
2266
+ }
2267
+ }
2268
+ /**
2269
+ * The ACP wire peer: newline-delimited JSON-RPC over the child's stdio.
2270
+ * Self-contained on purpose (no SDK dependency): StringDecoder framing for
2271
+ * split UTF-8 tails, request correlation, and server→client request answering
2272
+ * (the permission surface).
2273
+ */
2274
+ var AcpWirePeer = class {
2275
+ output;
2276
+ warn;
2277
+ onServerRequest;
2278
+ onNotification;
2279
+ requestMs;
2280
+ buffer = "";
2281
+ decoder = new StringDecoder("utf8");
2282
+ nextId = 0;
2283
+ pending = /* @__PURE__ */ new Map();
2284
+ closed = false;
2285
+ constructor(input, output, warn, onServerRequest, onNotification, requestMs) {
2286
+ this.output = output;
2287
+ this.warn = warn;
2288
+ this.onServerRequest = onServerRequest;
2289
+ this.onNotification = onNotification;
2290
+ this.requestMs = requestMs;
2291
+ input?.on("data", (chunk) => {
2292
+ this.feed(this.decoder.write(chunk));
2293
+ });
2294
+ }
2295
+ write(message) {
2296
+ this.output?.write(JSON.stringify(message) + "\n");
2297
+ }
2298
+ /** Send one request; `timeoutMs` 0 means turn-length (no timer — session/prompt). */
2299
+ request(method, params, timeoutMs) {
2300
+ if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("subagent-kimi live: the wire is closed"));
2301
+ this.nextId += 1;
2302
+ const id = `req_${this.nextId}`;
2303
+ const timeout = timeoutMs ?? this.requestMs;
2304
+ return new Promise((resolve, reject) => {
2305
+ let timer;
2306
+ if (timeout > 0) {
2307
+ timer = setTimeout(() => {
2308
+ this.pending.delete(id);
2309
+ reject(/* @__PURE__ */ new Error(`subagent-kimi live: ${method} timed out`));
2310
+ timer?.unref();
2311
+ }, timeout);
2312
+ timer.unref();
2313
+ }
2314
+ this.pending.set(id, {
2315
+ resolve: (result) => resolve(result),
2316
+ reject,
2317
+ timer
2318
+ });
2319
+ this.write({
2320
+ jsonrpc: "2.0",
2321
+ id,
2322
+ method,
2323
+ params
2324
+ });
2325
+ });
2326
+ }
2327
+ notify(method, params) {
2328
+ this.write(params === void 0 ? {
2329
+ jsonrpc: "2.0",
2330
+ method
2331
+ } : {
2332
+ jsonrpc: "2.0",
2333
+ method,
2334
+ params
2335
+ });
2336
+ }
2337
+ /** Detach and reject every outstanding request (a turn-length prompt settles here on teardown). */
2338
+ close() {
2339
+ if (this.closed) return;
2340
+ this.closed = true;
2341
+ for (const request of this.pending.values()) {
2342
+ if (request.timer !== void 0) clearTimeout(request.timer);
2343
+ request.reject(/* @__PURE__ */ new Error("subagent-kimi live: the wire closed"));
2344
+ }
2345
+ this.pending.clear();
2346
+ }
2347
+ feed(text) {
2348
+ this.buffer += text;
2349
+ let index = this.buffer.indexOf("\n");
2350
+ while (index >= 0) {
2351
+ const line = this.buffer.slice(0, index);
2352
+ this.buffer = this.buffer.slice(index + 1);
2353
+ index = this.buffer.indexOf("\n");
2354
+ if (line.trim() !== "") this.dispatchLine(line);
2355
+ }
2356
+ }
2357
+ /** Handle one wire line; a malformed or hostile line never escapes this frame. */
2358
+ dispatchLine(line) {
2359
+ let message;
2360
+ try {
2361
+ message = JSON.parse(line);
2362
+ } catch {
2363
+ this.warn("subagent-kimi live: ignored a malformed wire line");
2364
+ return;
2365
+ }
2366
+ try {
2367
+ const hasId = typeof message.id === "string" || typeof message.id === "number";
2368
+ if (hasId && typeof message.method === "string") {
2369
+ const id = message.id;
2370
+ try {
2371
+ const result = this.onServerRequest(message.method, message.params ?? {});
2372
+ this.write({
2373
+ jsonrpc: "2.0",
2374
+ id,
2375
+ result
2376
+ });
2377
+ } catch (error) {
2378
+ this.write({
2379
+ jsonrpc: "2.0",
2380
+ id,
2381
+ error: {
2382
+ code: -32603,
2383
+ message: thrown$1(error).message
2384
+ }
2385
+ });
2386
+ }
2387
+ return;
2388
+ }
2389
+ if (hasId && ("result" in message || "error" in message)) {
2390
+ const request = this.pending.get(String(message.id));
2391
+ if (request === void 0) return;
2392
+ this.pending.delete(String(message.id));
2393
+ if (request.timer !== void 0) clearTimeout(request.timer);
2394
+ const wireError = message.error;
2395
+ if (wireError !== void 0 && wireError !== null) request.reject(/* @__PURE__ */ new Error(`subagent-kimi live: ${wireError.message ?? "wire error"}`));
2396
+ else request.resolve(message.result);
2397
+ return;
2398
+ }
2399
+ if (typeof message.method === "string") this.onNotification(message.method, message.params ?? {});
2400
+ } catch (error) {
2401
+ this.warn(`subagent-kimi live: wire line handler failed: ${thrown$1(error).message}`);
2402
+ }
2403
+ }
2404
+ };
2405
+ /**
2406
+ * One resident `kimi acp` process: the wire peer, the loaded session id, the
2407
+ * per-session turn chain (one prompt at a time — a cancelled turn converges
2408
+ * before the next prompt goes out), and the reclaim ladder.
2409
+ */
2410
+ var KimiLiveRuntime = class {
2411
+ child;
2412
+ dead = false;
2413
+ reclaimed = false;
2414
+ peer;
2415
+ /** The member's ACP session; assigned by the first round's session/new or session/load. */
2416
+ sessionId;
2417
+ /**
2418
+ * The model this process was spawned with (the scoped `default_model` value
2419
+ * rewritten just before spawn; undefined means the config decided). A round
2420
+ * resolving a different model for the member retires this runtime so the
2421
+ * respawn binds the new one.
2422
+ */
2423
+ configurationKey;
2424
+ boundModel;
2425
+ modelConfiguration;
2426
+ /** Serializes session/prompt per member (converge-before-next-turn). */
2427
+ turnChain = Promise.resolve();
2428
+ /** The active round's notification sink; installed per round, cleared at settle. */
2429
+ onSessionUpdate;
2430
+ onDead;
2431
+ stderrTail = "";
2432
+ stderrDecoder = new StringDecoder("utf8");
2433
+ constructor(child, onServerRequest, warn, requestMs) {
2434
+ this.child = child;
2435
+ this.peer = new AcpWirePeer(child.stdout, child.stdin, warn, onServerRequest, (method, params) => {
2436
+ if (method !== "session/update") return;
2437
+ if (params["sessionId"] !== this.sessionId) return;
2438
+ const update = params["update"] ?? {};
2439
+ if (update["sessionUpdate"] === "config_option_update") this.modelConfiguration = kimiNativeConfiguration(update);
2440
+ this.onSessionUpdate?.(update);
2441
+ }, requestMs);
2442
+ child.stderr?.on("data", (chunk) => {
2443
+ this.stderrTail = (this.stderrTail + this.stderrDecoder.write(chunk)).slice(-4096);
2444
+ });
2445
+ child.done.then(() => {
2446
+ this.markDead();
2447
+ }, () => {
2448
+ this.markDead();
2449
+ });
2450
+ }
2451
+ get diagnostics() {
2452
+ return this.stderrTail.trim();
2453
+ }
2454
+ markDead() {
2455
+ if (this.dead) return;
2456
+ this.dead = true;
2457
+ this.peer.close();
2458
+ this.onDead?.();
2459
+ }
2460
+ /** Graceful teardown: close the wire, EOF stdin, grace, then the terminate ladder. */
2461
+ async reclaim() {
2462
+ if (this.reclaimed) return;
2463
+ this.reclaimed = true;
2464
+ this.peer.close();
2465
+ if (!this.dead) {
2466
+ this.child.stdin?.end();
2467
+ await Promise.race([this.child.done.catch(() => {}), delay(RECLAIM_EOF_GRACE_MS)]);
2468
+ }
2469
+ if (!this.dead) {
2470
+ this.child.terminate();
2471
+ await this.child.waitForExit();
2472
+ }
2473
+ await this.child.done.catch(() => {});
2474
+ this.markDead();
2475
+ }
2476
+ };
2477
+ /**
2478
+ * The kimi live driver owns every resident ACP runtime of this provider
2479
+ * generation. Created only when `live: true`; disposed with the plugin.
2480
+ */
2481
+ var KimiAcpLiveDriver = class {
2482
+ ctx;
2483
+ config;
2484
+ timeouts;
2485
+ runtimes = /* @__PURE__ */ new Map();
2486
+ ensuring = /* @__PURE__ */ new Map();
2487
+ /** The model each in-flight spawn is binding (a stale-model spawn never serves a switched member). */
2488
+ ensuringModel = /* @__PURE__ */ new Map();
2489
+ ensuringConfiguration = /* @__PURE__ */ new Map();
2490
+ idleTimers = /* @__PURE__ */ new Map();
2491
+ /** Per-member round serialization (the resume lock covers resume-vs-resume only). */
2492
+ roundChains = /* @__PURE__ */ new Map();
2493
+ channelBrokenAt;
2494
+ disposed = false;
2495
+ /**
2496
+ * Set by drain() (a settings-driven generation handoff): new rounds are
2497
+ * refused so the provider falls back to exec, while in-flight rounds finish
2498
+ * on their runtime undisturbed. Unlike `disposed`, the driver still serves
2499
+ * what it already accepted.
2500
+ */
2501
+ draining = false;
2502
+ disposeController = new AbortController();
2503
+ constructor(ctx, config = {}, timeouts = DEFAULT_TIMEOUTS) {
2504
+ this.ctx = ctx;
2505
+ this.config = config;
2506
+ this.timeouts = timeouts;
2507
+ }
2508
+ /** Whether the channel is in its post-failure cooldown (rounds fall back to exec). */
2509
+ get disabled() {
2510
+ return this.channelBrokenAt !== void 0 && Date.now() - this.channelBrokenAt < (this.timeouts.channelRetryMs ?? 3e5);
2511
+ }
2512
+ /** Count of resident runtimes currently registered (zombie accounting in tests). */
2513
+ get liveCount() {
2514
+ return this.runtimes.size;
2515
+ }
2516
+ clearIdleTimer(key) {
2517
+ const timer = this.idleTimers.get(key);
2518
+ if (timer !== void 0) clearTimeout(timer);
2519
+ this.idleTimers.delete(key);
2520
+ }
2521
+ armIdleTimer(key) {
2522
+ this.clearIdleTimer(key);
2523
+ const idleMs = this.config.liveIdleMs ?? 18e5;
2524
+ const timer = setTimeout(() => {
2525
+ this.reclaim(key);
2526
+ }, idleMs);
2527
+ timer.unref();
2528
+ this.idleTimers.set(key, timer);
2529
+ }
2530
+ async reclaim(key) {
2531
+ const runtime = this.runtimes.get(key);
2532
+ this.clearIdleTimer(key);
2533
+ if (runtime === void 0) return;
2534
+ this.runtimes.delete(key);
2535
+ await runtime.reclaim();
2536
+ }
2537
+ /** Reclaim every runtime (plugin unload); no process survives the profile. */
2538
+ async disposeAll() {
2539
+ this.disposed = true;
2540
+ this.disposeController.abort();
2541
+ await Promise.all([...this.ensuring.values()].map((pending) => pending.catch(() => void 0)));
2542
+ for (const key of [...this.runtimes.keys()]) await this.reclaim(key);
2543
+ }
2544
+ /**
2545
+ * True while the member's runtime exists or is being spawned. The settings
2546
+ * controller gates a new generation on this: a fresh driver must not serve
2547
+ * a member whose retiring generation still hosts the (same) kimi session.
2548
+ */
2549
+ hasRuntime(key) {
2550
+ return this.runtimes.has(key) || this.ensuring.has(key);
2551
+ }
2552
+ /** @deprecated Compatibility no-op: live output is always incremental. */
2553
+ setLiveMirrorGranularity(_granularity) {}
2554
+ /**
2555
+ * Drain for a settings-driven generation handoff: refuse new rounds (the
2556
+ * provider's catch falls back to exec), let every in-flight round finish on
2557
+ * its runtime, then reclaim. Resolves when no runtime or spawn remains.
2558
+ * Unlike disposeAll, in-flight work is never interrupted.
2559
+ */
2560
+ async drain() {
2561
+ this.draining = true;
2562
+ const keys = /* @__PURE__ */ new Set([
2563
+ ...this.runtimes.keys(),
2564
+ ...this.ensuring.keys(),
2565
+ ...this.roundChains.keys()
2566
+ ]);
2567
+ await Promise.all([...keys].map(async (key) => {
2568
+ await (this.roundChains.get(key) ?? Promise.resolve()).catch(() => void 0);
2569
+ await (this.ensuring.get(key) ?? Promise.resolve()).catch(() => void 0);
2570
+ await this.reclaim(key);
2571
+ }));
2572
+ }
2573
+ /** The model a runtime spawned for this member right now would bind. */
2574
+ resolveBoundModel(key) {
2575
+ const model = this.config.model?.(key)?.trim();
2576
+ return model === void 0 || model === "" ? void 0 : model;
2577
+ }
2578
+ ensureRuntime(spec, signal) {
2579
+ const key = String(spec.childSession.id);
2580
+ const model = spec.configuration === void 0 ? this.resolveBoundModel(key) : spec.configuration.model;
2581
+ const configurationKey = spec.configuration === void 0 ? void 0 : JSON.stringify(spec.configuration);
2582
+ const existing = this.runtimes.get(key);
2583
+ if (existing !== void 0 && !existing.dead && existing.boundModel === model && existing.configurationKey === configurationKey) {
2584
+ this.clearIdleTimer(key);
2585
+ return Promise.resolve(existing);
2586
+ }
2587
+ if (existing !== void 0) {
2588
+ this.runtimes.delete(key);
2589
+ this.clearIdleTimer(key);
2590
+ if (!existing.dead) existing.reclaim();
2591
+ }
2592
+ const pending = this.ensuring.get(key);
2593
+ if (pending !== void 0 && this.ensuringModel.get(key) === model && this.ensuringConfiguration.get(key) === configurationKey) return pending;
2594
+ const spawn = (pending ?? Promise.resolve()).catch(() => void 0).then(async (stale) => {
2595
+ if (stale !== void 0 && !stale.dead) {
2596
+ if (this.runtimes.get(key) === stale) this.runtimes.delete(key);
2597
+ await stale.reclaim();
2598
+ }
2599
+ return this.spawnRuntime(spec, signal, model);
2600
+ }).finally(() => {
2601
+ if (this.ensuring.get(key) === spawn) {
2602
+ this.ensuring.delete(key);
2603
+ this.ensuringModel.delete(key);
2604
+ this.ensuringConfiguration.delete(key);
2605
+ }
2606
+ });
2607
+ this.ensuring.set(key, spawn);
2608
+ this.ensuringModel.set(key, model);
2609
+ this.ensuringConfiguration.set(key, configurationKey);
2610
+ return spawn;
2611
+ }
2612
+ /**
2613
+ * Retire one member's resident runtime (a composer-driven model switch):
2614
+ * the process is reclaimed and the member's next round respawns onto the
2615
+ * new model, resuming the same CLI session. A member with no runtime (or
2616
+ * only a dead one) is a no-op. Lazy: nothing spawns here.
2617
+ */
2618
+ async retireRuntime(childSessionId) {
2619
+ const pending = this.ensuring.get(childSessionId);
2620
+ if (pending !== void 0) await pending.catch(() => void 0);
2621
+ await this.reclaim(childSessionId);
2622
+ }
2623
+ /**
2624
+ * The model the member's live runtime is bound to, or undefined when the
2625
+ * member has no live runtime (or it bound none). The broker compares this
2626
+ * against the post-switch effective model to make a same-model set a no-op.
2627
+ */
2628
+ runtimeModel(childSessionId) {
2629
+ const runtime = this.runtimes.get(childSessionId);
2630
+ return runtime === void 0 || runtime.dead ? void 0 : runtime.boundModel;
2631
+ }
2632
+ runtimeConfiguration(childSessionId) {
2633
+ const runtime = this.runtimes.get(childSessionId);
2634
+ return runtime === void 0 || runtime.dead ? void 0 : runtime.modelConfiguration;
2635
+ }
2636
+ /**
2637
+ * Spawn the member's resident `kimi acp` and prove the wire: initialize +
2638
+ * the loadSession capability (crash recovery needs it — without it the
2639
+ * channel is not live-capable and the breaker trips). A cancellation
2640
+ * mid-spawn reclaims the half-started process without touching the breaker.
2641
+ */
2642
+ async spawnRuntime(spec, signal, boundModel) {
2643
+ const key = String(spec.childSession.id);
2644
+ if (this.disposed) throw new LiveChannelUnavailableError("the live driver is disposed");
2645
+ const member = this.registerMember(spec);
2646
+ if (spec.configuration === void 0 && boundModel !== void 0) await writeKimiDefaultModel(spec.homeDir, boundModel).catch((error) => {
2647
+ this.ctx.logger.warn(`local-agent-kimi: pinning default_model for the resident runtime failed: ${thrown$1(error).message}`);
2648
+ return false;
2649
+ });
2650
+ const spawnSpec = {
2651
+ argv: [
2652
+ "kimi",
2653
+ ...spec.configuration?.model === void 0 ? [] : ["--model", spec.configuration.model],
2654
+ "acp"
2655
+ ],
2656
+ cwd: spec.cwd,
2657
+ stdio: {
2658
+ stdin: "pipe",
2659
+ stdout: "pipe",
2660
+ stderr: "pipe"
2661
+ },
2662
+ graceMs: DEFAULT_DISPOSE_GRACE_MS,
2663
+ env: delegationEnv({ KIMI_CODE_HOME: spec.homeDir })
2664
+ };
2665
+ let child;
2666
+ try {
2667
+ child = this.ctx.subprocess.spawn(spawnSpec);
2668
+ } catch (error) {
2669
+ member?.release();
2670
+ this.markChannelBroken();
2671
+ throw new LiveChannelUnavailableError(`kimi acp failed to spawn: ${thrown$1(error).message}`);
2672
+ }
2673
+ const runtime = new KimiLiveRuntime(child, (method, params) => this.answerServerRequest(method, params), (message) => {
2674
+ this.ctx.logger.warn(message);
2675
+ }, this.timeouts.requestMs);
2676
+ runtime.onDead = () => {
2677
+ if (this.runtimes.get(key) === runtime) this.runtimes.delete(key);
2678
+ this.clearIdleTimer(key);
2679
+ member?.release();
2680
+ };
2681
+ const aborted = new Promise((_, reject) => {
2682
+ const cancel = () => {
2683
+ reject(/* @__PURE__ */ new Error("subagent-kimi: run cancelled locally"));
2684
+ };
2685
+ const unload = () => {
2686
+ reject(new LiveChannelUnavailableError("the live driver was disposed during spawn"));
2687
+ };
2688
+ if (signal.aborted) {
2689
+ cancel();
2690
+ return;
2691
+ }
2692
+ if (this.disposeController.signal.aborted) {
2693
+ unload();
2694
+ return;
2695
+ }
2696
+ signal.addEventListener("abort", cancel, { once: true });
2697
+ this.disposeController.signal.addEventListener("abort", unload, { once: true });
2698
+ });
2699
+ aborted.catch(() => {});
2700
+ try {
2701
+ if ((await Promise.race([runtime.peer.request("initialize", {
2702
+ protocolVersion: ACP_PROTOCOL_VERSION,
2703
+ clientCapabilities: {}
2704
+ }, this.timeouts.initializeMs), aborted]))?.agentCapabilities?.loadSession !== true) throw new Error("kimi acp does not advertise loadSession (crash recovery requires it)");
2705
+ } catch (error) {
2706
+ await runtime.reclaim();
2707
+ if (signal.aborted) throw thrown$1(error);
2708
+ this.markChannelBroken();
2709
+ throw new LiveChannelUnavailableError(`the kimi acp handshake failed: ${thrown$1(error).message}`);
2710
+ }
2711
+ if (this.disposed) {
2712
+ await runtime.reclaim();
2713
+ throw new LiveChannelUnavailableError("the live driver was disposed during spawn");
2714
+ }
2715
+ this.channelBrokenAt = void 0;
2716
+ runtime.boundModel = boundModel;
2717
+ runtime.configurationKey = spec.configuration === void 0 ? void 0 : JSON.stringify(spec.configuration);
2718
+ this.runtimes.set(key, runtime);
2719
+ runtimeMember.set(runtime, member);
2720
+ return runtime;
2721
+ }
2722
+ markChannelBroken() {
2723
+ this.channelBrokenAt = Date.now();
2724
+ }
2725
+ /**
2726
+ * Report an auth-shaped round failure to the family registry's auth-failure
2727
+ * mark (the exec path's post-exit 401 detection ported to a process that
2728
+ * never exits: the match runs against the settled error instead). Degrades
2729
+ * silently on a core predating reportAuthFailure.
2730
+ */
2731
+ reportAuthIfShaped(error, homeDir) {
2732
+ if (!KIMI_LIVE_AUTH_FAILURE.test(error.message)) return;
2733
+ if (homeDir !== void 0) guardKimiCredential(homeDir, (message) => {
2734
+ this.ctx.logger.warn(message);
2735
+ });
2736
+ const registry = this.ctx.localAgent;
2737
+ if (typeof registry.reportAuthFailure === "function") registry.reportAuthFailure("kimi", error.message.split("\n").find((line) => line.trim() !== "") ?? error.message);
2738
+ }
2739
+ /**
2740
+ * Register the member channel for one resident process (token + bridge
2741
+ * coordinates), duck-typed like the exec path's memberRun but WITHOUT the
2742
+ * mcp.json write — the ACP session declares the bridge inline.
2743
+ */
2744
+ registerMember(spec) {
2745
+ const registry = this.ctx.localAgent;
2746
+ if (typeof registry.registerMemberRun !== "function" || typeof registry.memberBridgeSocketPath !== "function" || typeof registry.memberBridgeCommand !== "function") return void 0;
2747
+ const token = registry.registerMemberRun({
2748
+ childSessionId: String(spec.childSession.id),
2749
+ parentSessionId: spec.parentSessionId,
2750
+ provider: "kimi-cli"
2751
+ });
2752
+ const bridge = registry.memberBridgeCommand();
2753
+ let released = false;
2754
+ return {
2755
+ mcpServers: [{
2756
+ name: "dsh-member",
2757
+ command: bridge.command,
2758
+ args: [...bridge.args],
2759
+ env: [{
2760
+ name: MEMBER_BRIDGE_SOCKET_ENV,
2761
+ value: registry.memberBridgeSocketPath()
2762
+ }, {
2763
+ name: MEMBER_BRIDGE_TOKEN_ENV,
2764
+ value: token
2765
+ }]
2766
+ }],
2767
+ release: () => {
2768
+ if (released) return;
2769
+ released = true;
2770
+ registry.unregisterMemberRun(token);
2771
+ }
2772
+ };
2773
+ }
2774
+ /**
2775
+ * Auto-answer the ACP permission surface unattended. `session/request_permission`
2776
+ * selects the first allow option (allow_once/allow_always) — matching the
2777
+ * exec mode's auto-approve behavior for `kimi -p` — and cancels when the
2778
+ * child offered none. Anything else (fs/terminal, which we never
2779
+ * advertised) is an error response.
2780
+ */
2781
+ answerServerRequest(method, params) {
2782
+ if (method === "session/request_permission") {
2783
+ const allow = (Array.isArray(params["options"]) ? params["options"] : []).find((o) => o.kind === "allow_once" || o.kind === "allow_always");
2784
+ if (allow !== void 0 && typeof allow.optionId === "string") return { outcome: {
2785
+ outcome: "selected",
2786
+ optionId: allow.optionId
2787
+ } };
2788
+ return { outcome: { outcome: "cancelled" } };
2789
+ }
2790
+ throw new Error(`subagent-kimi live: unsupported ACP request ${method}`);
2791
+ }
2792
+ /**
2793
+ * Drive one delegation round on the member's resident ACP session. Mirrors
2794
+ * the exec run's settlement contract exactly (settleRunResult + turn/end
2795
+ * bookkeeping); cancel is `session/cancel` and the process survives.
2796
+ */
2797
+ /** Initialize the native session and model controls without sending a prompt. */
2798
+ async prepare(spec, signal) {
2799
+ signal.throwIfAborted();
2800
+ if (this.draining || this.disabled) throw new LiveChannelUnavailableError("the live driver is unavailable for preparation");
2801
+ try {
2802
+ const runtime = await this.ensureRuntime(spec, signal);
2803
+ await this.configureSession(runtime, spec);
2804
+ signal.throwIfAborted();
2805
+ this.armIdleTimer(String(spec.childSession.id));
2806
+ } catch (error) {
2807
+ await this.reclaim(String(spec.childSession.id));
2808
+ throw error;
2809
+ }
2810
+ }
2811
+ async configureSession(rt, spec) {
2812
+ const member = runtimeMember.get(rt);
2813
+ if (rt.sessionId === void 0) {
2814
+ if (spec.resume === void 0) {
2815
+ const response = await rt.peer.request("session/new", {
2816
+ cwd: spec.cwd,
2817
+ mcpServers: member?.mcpServers ?? []
2818
+ });
2819
+ if (typeof response?.sessionId !== "string") throw new Error("subagent-kimi live: session/new returned no session id");
2820
+ rt.sessionId = response.sessionId;
2821
+ rt.modelConfiguration = kimiNativeConfiguration(response);
2822
+ } else {
2823
+ rt.sessionId = acpKimiSessionId(spec.resume.cliSessionId);
2824
+ rt.modelConfiguration = kimiNativeConfiguration(await rt.peer.request("session/load", {
2825
+ sessionId: rt.sessionId,
2826
+ cwd: spec.cwd,
2827
+ mcpServers: member?.mcpServers ?? []
2828
+ }));
2829
+ }
2830
+ }
2831
+ if (spec.configuration !== void 0) rt.modelConfiguration = await configureKimiSession(rt.sessionId, rt.modelConfiguration, spec.configuration, (method, params) => rt.peer.request(method, params));
2832
+ if (spec.resume === void 0 && rt.sessionId !== void 0) spec.onCliSessionId?.(bareKimiSessionId(rt.sessionId));
2833
+ }
2834
+ async startRound(request, spec) {
2835
+ if (this.draining) throw new LiveChannelUnavailableError("the live driver is draining (a settings change retired this generation)");
2836
+ const key = String(spec.childSession.id);
2837
+ const round = (this.roundChains.get(key) ?? Promise.resolve()).catch(() => {}).then(() => this.startRoundLocked(request, spec));
2838
+ this.roundChains.set(key, round.then((handle) => handle.result.catch(() => ({})), () => ({})));
2839
+ return round;
2840
+ }
2841
+ /** The serialized round body. */
2842
+ async startRoundLocked(request, spec) {
2843
+ const task = textTask(request.prompt);
2844
+ if (request.signal.aborted) throw new Error("subagent-kimi: request was aborted before the run started");
2845
+ if (this.draining) throw new LiveChannelUnavailableError("the live driver is draining (a settings change retired this generation)");
2846
+ if (this.disposed) throw new Error("subagent-kimi: the live driver is disposed");
2847
+ const turn = spec.resume?.turn ?? 1;
2848
+ const childSession = spec.childSession;
2849
+ const localAgent = this.ctx.get("localAgent");
2850
+ const runAbort = new AbortController();
2851
+ let roundSettled = false;
2852
+ let runtime;
2853
+ /** True while this round's session/prompt is in flight (cancel targets it). */
2854
+ let promptInFlight = false;
2855
+ let turnOpened = false;
2856
+ /** The round's accumulated assistant text (the run output — chunks are the only source). */
2857
+ let roundText = "";
2858
+ /**
2859
+ * The step ledger: every step a stream reservation consumed this round
2860
+ * (in increasing order — permanently, whether the stream later completes
2861
+ * or stays an orphan snapshot). The file fold skips these steps for its
2862
+ * sequential folds, so a tool card folded mid-stream lands ABOVE the
2863
+ * stream's step and the projection renders the round's chronological
2864
+ * order.
2865
+ */
2866
+ const reservedSteps = [];
2867
+ /**
2868
+ * The streaming items seen this round . kimi's ACP
2869
+ * chunks carry no item id, so the stream key is synthetic per kind per
2870
+ * round (the codex live driver's fallback shape): thought chunks share
2871
+ * one stream, message chunks another. Deltas accumulate into throttled
2872
+ * snapshot assistant/messages appended at the stream's reserved
2873
+ * (turn, step) — the host folds repeated settles at one coordinate into
2874
+ * one live-updating chat node. The wire line that completes the stream's
2875
+ * kind folds at the same step and finalizes it; an entry whose completion
2876
+ * never folds is force-finalized at settle.
2877
+ */
2878
+ const streams = /* @__PURE__ */ new Map();
2879
+ /** The stream the latest delta accumulated into (a kind switch force-snapshots it). */
2880
+ let activeStream;
2881
+ /**
2882
+ * The round's usage NOT carried by a folded line, summed across mirror
2883
+ * passes (each pass's window is disjoint). The settle's final snapshot
2884
+ * carries the remainder when no folded message did — host 0.1.5 has no
2885
+ * usage-backfill event.
2886
+ */
2887
+ let roundUsage;
2888
+ /**
2889
+ * The model identifier the wire named this round (a mirror pass's
2890
+ * `usage.record` / `llm.request` `model`, last one seen), reported as the
2891
+ * round's settled observation — the live drive's half of the exec path's
2892
+ * settle-mirror read-back.
2893
+ */
2894
+ let roundModel;
2895
+ let lastMirrorAt = 0;
2896
+ let mirrorQueue = Promise.resolve();
2897
+ let persistQueue = Promise.resolve();
2898
+ const persist = () => {
2899
+ persistQueue = persistQueue.then(() => persistChildSession(this.ctx, childSession)).catch((error) => {
2900
+ this.ctx.logger.warn(`subagent-kimi: live persistence failed: ${thrown$1(error).message}`);
2901
+ });
2902
+ };
2903
+ /** A stream's synthetic key: kimi's ACP deltas carry no item id, so kind + turn pairs them. */
2904
+ let generation = 0;
2905
+ const seenToolCalls = /* @__PURE__ */ new Set();
2906
+ const toolSteps = /* @__PURE__ */ new Map();
2907
+ const streamKey = (kind) => `kimi-stream-${kind}-${turn}-${generation}`;
2908
+ /** The plan stream's key: a snapshot stream beside the think/text delta streams. */
2909
+ const planKey = `kimi-stream-plan-${turn}`;
2910
+ /**
2911
+ * Fold one non-text chunk as a placeholder: image/resource_link content
2912
+ * has no text channel, so the transcript keeps a visible marker (and the
2913
+ * run output notes the drop) instead of the chunk vanishing. Flows exactly
2914
+ * like a text delta from here.
2915
+ */
2916
+ const foldDelta = (text) => {
2917
+ roundText += text;
2918
+ const key = streamKey("text");
2919
+ reserveStream(key, "text");
2920
+ const stream = streams.get(key);
2921
+ if (stream !== void 0) {
2922
+ stream.text += text;
2923
+ appendStreamSnapshot(stream, false, false);
2924
+ }
2925
+ localAgent?.reportRunProgress(childSession.id, {
2926
+ kind: "delta",
2927
+ text
2928
+ });
2929
+ };
2930
+ /**
2931
+ * Render one ACP plan snapshot ({entries: [{content, status}]}) as
2932
+ * think-style lines. Each update carries the WHOLE plan, so the fold
2933
+ * replaces rather than appends.
2934
+ */
2935
+ const renderPlan = (update) => {
2936
+ return (Array.isArray(update["entries"]) ? update["entries"] : []).map((entry) => {
2937
+ const content = typeof entry["content"] === "string" ? entry["content"] : "";
2938
+ const status = entry["status"];
2939
+ return `${status === "completed" ? "☑" : status === "in_progress" ? "▶" : "☐"} ${content}`;
2940
+ }).filter((line) => line.trim() !== "☐" && line !== "").join("\n");
2941
+ };
2942
+ const foldPlan = (update) => {
2943
+ const plan = renderPlan(update);
2944
+ if (plan === "") return;
2945
+ reserveStream(planKey, "think");
2946
+ const stream = streams.get(planKey);
2947
+ if (stream !== void 0 && stream.text !== plan) {
2948
+ stream.text = plan;
2949
+ appendStreamSnapshot(stream, true, false);
2950
+ }
2951
+ };
2952
+ /**
2953
+ * Append one snapshot of a streaming item at its reserved (turn, step).
2954
+ * Batched per stream on a bounded deadline unless `force`; the forced
2955
+ * final snapshot carries `interrupted` (a cancelled turn reads 已停止
2956
+ * legitimately) and, when `withUsage`, the round's uncarried usage. An
2957
+ * empty stream leaves no boundary at all.
2958
+ */
2959
+ const streamPublisher = localAgent?.liveStreams === void 0 ? void 0 : new LiveStreamPublisher(localAgent.liveStreams, childSession, turn, persist, (error) => this.ctx.logger.warn(`live checkpoint failed: ${String(error)}`));
2960
+ const liveFlush = new LiveFlush((error) => this.ctx.logger.warn(`live mirror flush failed: ${String(error)}`), this.config.snapshotMinIntervalMs ?? DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
2961
+ const pendingStreamArrival = /* @__PURE__ */ new Map();
2962
+ const appendStreamSnapshot = (stream, force, interrupted, withUsage = false, final = false) => {
2963
+ if (stream.text.trim() === "") return;
2964
+ const now = Date.now();
2965
+ if (!force) {
2966
+ if (!pendingStreamArrival.has(stream.step)) pendingStreamArrival.set(stream.step, now);
2967
+ liveFlush.schedule(stream.step, () => appendStreamSnapshot(stream, true, interrupted, withUsage));
2968
+ return;
2969
+ }
2970
+ liveFlush.cancel(stream.step);
2971
+ const receivedAt = pendingStreamArrival.get(stream.step) ?? now;
2972
+ pendingStreamArrival.delete(stream.step);
2973
+ if (streamPublisher !== void 0 && !final) {
2974
+ if (!stream.opened) {
2975
+ childSession.append("step/start", {
2976
+ turn,
2977
+ step: stream.step
2978
+ });
2979
+ stream.opened = true;
2980
+ }
2981
+ streamPublisher.update(stream.step, stream.kind, stream.text, { receivedAt });
2982
+ stream.lastSnapshotAt = now;
2983
+ stream.lastSnapshotLen = stream.text.length;
2984
+ return;
2985
+ }
2986
+ if (!stream.opened) {
2987
+ childSession.append("step/start", {
2988
+ turn,
2989
+ step: stream.step
2990
+ });
2991
+ stream.opened = true;
2992
+ }
2993
+ childSession.append("assistant/message", {
2994
+ turn,
2995
+ step: stream.step,
2996
+ message: assistantEvent([stream.kind === "think" ? {
2997
+ type: "reasoning",
2998
+ text: stream.text
2999
+ } : {
3000
+ type: "text",
3001
+ text: stream.text
3002
+ }], runtime?.modelConfiguration?.currentModel ?? roundModel),
3003
+ stream: [],
3004
+ ...withUsage && roundUsage !== void 0 ? { usage: roundUsage } : {},
3005
+ ...interrupted ? { interrupted: true } : {}
3006
+ }, { surfaceOp: "append" });
3007
+ if (withUsage) roundUsage = void 0;
3008
+ stream.lastSnapshotAt = now;
3009
+ stream.lastSnapshotLen = stream.text.length;
3010
+ persist();
3011
+ };
3012
+ /**
3013
+ * Reserve the step one streaming item will occupy — past every folded
3014
+ * line (the session's own event ledger: the file fold writes events
3015
+ * synchronously) and every earlier reservation. A kind switch forces one
3016
+ * freshness snapshot of the current stream (skipped when its text has not
3017
+ * grown since the last one); its completion still folds at its own
3018
+ * reserved step when the wire line lands.
3019
+ */
3020
+ const reserveStream = (key, kind) => {
3021
+ if (activeStream !== void 0 && activeStream !== key) {
3022
+ const previous = streams.get(activeStream);
3023
+ if (previous !== void 0 && previous.text.length !== previous.lastSnapshotLen) appendStreamSnapshot(previous, true, false);
3024
+ activeStream = void 0;
3025
+ }
3026
+ let stream = streams.get(key);
3027
+ if (stream === void 0) {
3028
+ let step = nextKimiSessionStep(childSession, turn);
3029
+ for (const reserved of reservedSteps) step = Math.max(step, reserved + 1);
3030
+ reservedSteps.push(step);
3031
+ stream = {
3032
+ kind,
3033
+ step,
3034
+ text: "",
3035
+ lastSnapshotAt: 0,
3036
+ lastSnapshotLen: 0,
3037
+ opened: false
3038
+ };
3039
+ streams.set(key, stream);
3040
+ }
3041
+ activeStream = key;
3042
+ };
3043
+ /**
3044
+ * The fold-pass coordination surface : the file
3045
+ * fold skips reserved steps for sequential folds and pairs a completed
3046
+ * think/assistant line with its stream — scoped to THIS round's turn, so
3047
+ * an earlier round's leftover lines never consume this round's stream.
3048
+ */
3049
+ const mirrorStreams = {
3050
+ reservedSteps: (t) => t === turn ? reservedSteps : [],
3051
+ completeTool: (line) => {
3052
+ if (line.turn !== turn) return void 0;
3053
+ const step = toolSteps.get(line.id);
3054
+ if (step === void 0) return void 0;
3055
+ toolSteps.delete(line.id);
3056
+ return {
3057
+ step,
3058
+ opened: true
3059
+ };
3060
+ },
3061
+ completeStream: (line) => {
3062
+ if (line.turn !== turn) return void 0;
3063
+ const kind = line.kind === "think" ? "think" : "text";
3064
+ const entry = [...streams.entries()].find(([key, stream]) => key !== planKey && stream.kind === kind);
3065
+ if (entry === void 0) return void 0;
3066
+ const [key, stream] = entry;
3067
+ liveFlush.cancel(stream.step);
3068
+ streamPublisher?.finish(stream.step);
3069
+ streams.delete(key);
3070
+ if (activeStream === key) activeStream = void 0;
3071
+ return {
3072
+ step: stream.step,
3073
+ opened: stream.opened
3074
+ };
3075
+ }
3076
+ };
3077
+ const mirrorOptions = () => ({ streams: mirrorStreams });
3078
+ /**
3079
+ * Observation accounting across mirror passes: a pass whose window
3080
+ * attached its usage to a folded message is already carried; anything else
3081
+ * sums into the round's uncarried remainder (the final snapshot's payload).
3082
+ * The wire's model rides every pass that saw one — the settle report reads
3083
+ * the latest.
3084
+ */
3085
+ const noteMirrorUsage = (delta) => {
3086
+ if (delta.usage !== void 0 && delta.usageAttached !== true) roundUsage = addTokenUsage(roundUsage, delta.usage);
3087
+ if (delta.model !== void 0) roundModel = delta.model;
3088
+ };
3089
+ /** Throttled push-triggered mirror pass; the file fold stays the only transcript source. */
3090
+ const triggerMirror = () => {
3091
+ const now = Date.now();
3092
+ if (now - lastMirrorAt < this.timeouts.mirrorThrottleMs) return;
3093
+ lastMirrorAt = now;
3094
+ mirrorQueue = mirrorQueue.then(() => mirrorKimiDelta(this.ctx, childSession, spec.homeDir, runtime?.sessionId === void 0 ? void 0 : bareKimiSessionId(runtime.sessionId), mirrorOptions()).then((delta) => {
3095
+ noteMirrorUsage(delta);
3096
+ }).catch((error) => {
3097
+ this.ctx.logger.warn(`subagent-kimi: live mirror pass failed: ${thrown$1(error).message}`);
3098
+ }));
3099
+ };
3100
+ const requestCancel = () => {
3101
+ if (roundSettled || runAbort.signal.aborted) return;
3102
+ runAbort.abort(/* @__PURE__ */ new Error("subagent-kimi: run cancelled locally"));
3103
+ if (promptInFlight && runtime?.sessionId !== void 0 && !runtime.dead) runtime.peer.notify("session/cancel", { sessionId: runtime.sessionId });
3104
+ };
3105
+ const onAbort = () => {
3106
+ requestCancel();
3107
+ };
3108
+ request.signal.addEventListener("abort", onAbort, { once: true });
3109
+ const abortBranch = new Promise((_, reject) => {
3110
+ runAbort.signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("subagent-kimi: run cancelled locally")), { once: true });
3111
+ });
3112
+ const collectOutput = () => {
3113
+ return roundText.trim() === "" ? [] : [{
3114
+ type: "text",
3115
+ text: roundText
3116
+ }];
3117
+ };
3118
+ /** The round's update sink (installed on the runtime while it runs). */
3119
+ const onSessionUpdate = (update) => {
3120
+ const kind = update["sessionUpdate"];
3121
+ if (kind === "agent_message_chunk") {
3122
+ const content = update["content"];
3123
+ if (content !== void 0 && content.type !== void 0 && content.type !== "text") {
3124
+ foldDelta(`[未支持的内容类型 ${content.type}]`);
3125
+ triggerMirror();
3126
+ return;
3127
+ }
3128
+ const text = content?.text ?? "";
3129
+ if (text !== "") foldDelta(text);
3130
+ } else if (kind === "agent_thought_chunk") {
3131
+ const text = update["content"]?.text ?? "";
3132
+ if (text !== "") {
3133
+ const key = streamKey("think");
3134
+ reserveStream(key, "think");
3135
+ const stream = streams.get(key);
3136
+ if (stream !== void 0) {
3137
+ stream.text += text;
3138
+ appendStreamSnapshot(stream, false, false);
3139
+ }
3140
+ }
3141
+ } else if (kind === "tool_call") {
3142
+ const id = typeof update["toolCallId"] === "string" ? update["toolCallId"] : void 0;
3143
+ if (id === void 0 || !seenToolCalls.has(id)) {
3144
+ if (id !== void 0) {
3145
+ seenToolCalls.add(id);
3146
+ let step = nextKimiSessionStep(childSession, turn);
3147
+ for (const reserved of reservedSteps) step = Math.max(step, reserved + 1);
3148
+ reservedSteps.push(step);
3149
+ toolSteps.set(id, step);
3150
+ childSession.append("step/start", {
3151
+ turn,
3152
+ step
3153
+ });
3154
+ persist();
3155
+ }
3156
+ generation++;
3157
+ }
3158
+ } else if (kind === "plan") foldPlan(update);
3159
+ triggerMirror();
3160
+ };
3161
+ const accepted = (async () => {
3162
+ const rt = await this.ensureRuntime(spec, request.signal);
3163
+ runtime = rt;
3164
+ if (runAbort.signal.aborted) {
3165
+ await this.reclaim(String(childSession.id));
3166
+ throw new Error("subagent-kimi: run cancelled locally");
3167
+ }
3168
+ try {
3169
+ await this.configureSession(rt, spec);
3170
+ } catch (error) {
3171
+ if (!runAbort.signal.aborted) await this.reclaim(String(childSession.id));
3172
+ throw thrown$1(error);
3173
+ }
3174
+ rt.onSessionUpdate = onSessionUpdate;
3175
+ childSession.append("turn/start", { turn });
3176
+ childSession.append("user/message", createUserMessage({
3177
+ content: [{
3178
+ type: "text",
3179
+ text: task
3180
+ }],
3181
+ source: { kind: "user" }
3182
+ }), { surfaceOp: "append" });
3183
+ turnOpened = true;
3184
+ })();
3185
+ const attempt = accepted.then(async () => {
3186
+ if (runAbort.signal.aborted) throw new Error("subagent-kimi: run cancelled locally");
3187
+ const rt = runtime;
3188
+ const processFailure = rt.child.done.then((outcome) => Promise.reject(/* @__PURE__ */ new Error(`subagent-kimi: the live runtime exited mid-round (code ${String(outcome.exitCode)}, signal ${String(outcome.signal)})`)), (error) => Promise.reject(thrown$1(error)));
3189
+ processFailure.catch(() => {});
3190
+ const sendPrompt = () => {
3191
+ if (runAbort.signal.aborted) return Promise.reject(/* @__PURE__ */ new Error("subagent-kimi: run cancelled locally"));
3192
+ promptInFlight = true;
3193
+ return rt.peer.request("session/prompt", {
3194
+ sessionId: rt.sessionId,
3195
+ prompt: [{
3196
+ type: "text",
3197
+ text: task
3198
+ }]
3199
+ }, 0);
3200
+ };
3201
+ const promptPromise = rt.turnChain.catch(() => {}).then(sendPrompt);
3202
+ rt.turnChain = promptPromise;
3203
+ const promptResult = await Promise.race([promptPromise, processFailure]);
3204
+ const stopReason = acpStopReasonToHarness(String(promptResult?.stopReason ?? ""));
3205
+ if (stopReason === "completed" && collectOutput().length === 0) throw new Error("subagent-kimi live: the turn completed but produced no answer");
3206
+ return {
3207
+ output: collectOutput(),
3208
+ stopReason
3209
+ };
3210
+ });
3211
+ /**
3212
+ * Settle reconciliation: the kimi wire flushes asynchronously past the
3213
+ * prompt response (prompt lines early, the answer by turn end), so the
3214
+ * final mirror folds until consecutive reads see no growth — bounded, so
3215
+ * a stuck flush cannot pin the round beyond the quiescence window.
3216
+ */
3217
+ const settleMirror = async () => {
3218
+ await mirrorQueue.catch(() => {});
3219
+ const sessionId = runtime?.sessionId === void 0 ? void 0 : bareKimiSessionId(runtime.sessionId);
3220
+ let lastTotal = -1;
3221
+ let stableReads = 0;
3222
+ const deadline = Date.now() + SETTLE_MIRROR_QUIESCE_MS;
3223
+ for (;;) {
3224
+ const delta = await mirrorKimiDelta(this.ctx, childSession, spec.homeDir, sessionId, mirrorOptions());
3225
+ noteMirrorUsage(delta);
3226
+ localAgent?.reportRunProgress(childSession.id, {
3227
+ kind: "mirror",
3228
+ mirroredLines: delta.total
3229
+ });
3230
+ stableReads = delta.total === lastTotal ? stableReads + 1 : 0;
3231
+ lastTotal = delta.total;
3232
+ if (stableReads >= SETTLE_MIRROR_STABLE_READS) break;
3233
+ if (Date.now() >= deadline) break;
3234
+ await delay(SETTLE_MIRROR_POLL_MS);
3235
+ }
3236
+ };
3237
+ const result = settleRunResult({
3238
+ attempt: () => Promise.race([attempt, abortBranch]),
3239
+ collectOutput,
3240
+ cancelled: () => runAbort.signal.aborted,
3241
+ onError: (error, stopReason) => {
3242
+ const diagnostics = runtime?.diagnostics ?? "";
3243
+ const suffix = diagnostics === "" ? "" : `; ${diagnostics}`;
3244
+ this.ctx.logger.warn(`subagent-kimi: live round failed (${stopReason}): ${error.message}${suffix}`);
3245
+ this.reportAuthIfShaped(error, spec.homeDir);
3246
+ },
3247
+ signal: request.signal,
3248
+ onAbort
3249
+ }).then(async (settled) => {
3250
+ roundSettled = true;
3251
+ liveFlush.dispose();
3252
+ promptInFlight = false;
3253
+ if (turnOpened) {
3254
+ try {
3255
+ await settleMirror();
3256
+ } catch (error) {
3257
+ this.ctx.logger.warn(`subagent-kimi: live settle mirror failed: ${thrown$1(error).message}`);
3258
+ }
3259
+ if (streams.size > 0) {
3260
+ const remaining = [...streams.values()];
3261
+ for (const [position, stream] of remaining.entries()) {
3262
+ appendStreamSnapshot(stream, true, settled.stopReason !== "completed", position === remaining.length - 1, true);
3263
+ streamPublisher?.finish(stream.step);
3264
+ if (stream.opened) childSession.append("step/end", {
3265
+ turn,
3266
+ step: stream.step
3267
+ });
3268
+ }
3269
+ streams.clear();
3270
+ activeStream = void 0;
3271
+ persist();
3272
+ }
3273
+ for (const step of toolSteps.values()) childSession.append("step/end", {
3274
+ turn,
3275
+ step
3276
+ });
3277
+ toolSteps.clear();
3278
+ if (settled.stopReason === "completed") childSession.append("turn/end", {
3279
+ turn,
3280
+ reason: { kind: "completed" }
3281
+ });
3282
+ else if (settled.stopReason === "aborted") childSession.append("turn/end", {
3283
+ turn,
3284
+ reason: {
3285
+ kind: "aborted",
3286
+ reason: { kind: "parent" }
3287
+ }
3288
+ });
3289
+ else childSession.append("turn/end", {
3290
+ turn,
3291
+ reason: {
3292
+ kind: "error",
3293
+ error: {
3294
+ message: "the live round did not complete",
3295
+ code: "UNKNOWN"
3296
+ }
3297
+ }
3298
+ });
3299
+ }
3300
+ streamPublisher?.dispose();
3301
+ if (runtime !== void 0) runtime.onSessionUpdate = void 0;
3302
+ if (turnOpened) {
3303
+ persist();
3304
+ await persistQueue;
3305
+ }
3306
+ return settled;
3307
+ });
3308
+ result.then(() => {
3309
+ if (turnOpened) {
3310
+ const round = { ...roundModel === void 0 ? {} : { observedModel: roundModel } };
3311
+ localAgent?.recordRoundSettled?.(childSession.id, round);
3312
+ }
3313
+ this.armIdleTimer(String(childSession.id));
3314
+ });
3315
+ await Promise.race([accepted, abortBranch.catch(() => void 0)]);
3316
+ return subprocessRunHandle({
3317
+ id: childSession.id,
3318
+ result,
3319
+ signal: request.signal,
3320
+ onAbort,
3321
+ requestCancel,
3322
+ teardown: async () => {
3323
+ if (roundSettled) return;
3324
+ const rt = runtime;
3325
+ if (rt === void 0) return;
3326
+ await Promise.race([rt.turnChain.catch(() => {}), delay(this.timeouts.convergeMs)]);
3327
+ }
3328
+ });
3329
+ }
3330
+ };
3331
+ /** The member handle each runtime carries (for its session declarations). */
3332
+ const runtimeMember = /* @__PURE__ */ new WeakMap();
3333
+ //#endregion
3334
+ //#region lib/types/member-bridge-config.js
3335
+ /**
3336
+ * Kimi scoped-home MCP config injection for the member bridge. Kimi declares
3337
+ * stdio MCP servers in `$KIMI_CODE_HOME/mcp.json` (`mcpServers` map); the file
3338
+ * is read once per CLI session start, and edits never interrupt an open
3339
+ * session — so the family writes one entry PER RUN (keyed by the run's token
3340
+ * suffix, keeping concurrent delegations of the same scoped home race-free),
3341
+ * prunes it when the run settles, and drops stale family entries (crashed
3342
+ * host) on every write. User-declared servers are preserved verbatim.
3343
+ * @module @khorsheed/dsh-local-agent-kimi/member-bridge-config
3344
+ */
3345
+ /** Kimi's user-level MCP config filename inside the scoped home. */
3346
+ const MCP_CONFIG_FILENAME = "mcp.json";
3347
+ /** Server-key prefix marking the family's per-run bridge entries. */
3348
+ const MEMBER_BRIDGE_SERVER_PREFIX = "dsh-member-";
3349
+ /**
3350
+ * The per-run MCP server key. The run's token suffix keeps concurrent runs
3351
+ * distinct; each `kimi -p` round is a fresh CLI session, so per-run (rather
3352
+ * than per-member) naming costs nothing.
3353
+ * @param token - the run's member-channel token.
3354
+ * @returns the mcp.json server key.
3355
+ */
3356
+ function memberBridgeServerKey(token) {
3357
+ return `${MEMBER_BRIDGE_SERVER_PREFIX}${token.slice(0, 8)}`;
3358
+ }
3359
+ /** Read the scoped mcp.json; a missing or malformed file reads as empty. */
3360
+ function readMcpConfig(file) {
3361
+ let parsed;
3362
+ try {
3363
+ parsed = JSON.parse(readFileSync(file, "utf8"));
3364
+ } catch {
3365
+ return {};
3366
+ }
3367
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
3368
+ return parsed;
3369
+ }
3370
+ /** Write the config back, preserving every non-family key. */
3371
+ function writeMcpConfig(file, config) {
3372
+ writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
3373
+ }
3374
+ /**
3375
+ * Declare the member bridge for one run: set the per-run server entry and
3376
+ * prune other family entries (settled or crashed runs — an open session's
3377
+ * already-registered server is unaffected, per kimi's mid-session semantics).
3378
+ * Synchronous read-modify-write: single-process callers race-free by event
3379
+ * loop; the file is small and written once per run.
3380
+ * @param homeDir - the kimi scoped home.
3381
+ * @param key - this run's server key ({@link memberBridgeServerKey}).
3382
+ * @param command - the bridge spawn command (`node` + entry path).
3383
+ * @param env - the bridge environment (socket path + per-run token).
3384
+ */
3385
+ function injectMemberBridge(homeDir, key, command, env) {
3386
+ const file = join(homeDir, MCP_CONFIG_FILENAME);
3387
+ const config = readMcpConfig(file);
3388
+ const servers = { ...config.mcpServers ?? {} };
3389
+ for (const name of Object.keys(servers)) if (name.startsWith("dsh-member-") && name !== key) delete servers[name];
3390
+ servers[key] = {
3391
+ command: command.command,
3392
+ args: command.args,
3393
+ env
3394
+ };
3395
+ writeMcpConfig(file, {
3396
+ ...config,
3397
+ mcpServers: servers
3398
+ });
3399
+ }
3400
+ /**
3401
+ * Remove one run's bridge entry (settle path). Absent entries and a missing
3402
+ * config are no-ops.
3403
+ * @param homeDir - the kimi scoped home.
3404
+ * @param key - the run's server key.
3405
+ */
3406
+ function removeMemberBridge(homeDir, key) {
3407
+ const file = join(homeDir, MCP_CONFIG_FILENAME);
3408
+ const config = readMcpConfig(file);
3409
+ if (config.mcpServers === void 0 || !(key in config.mcpServers)) return;
3410
+ const servers = { ...config.mcpServers };
3411
+ delete servers[key];
3412
+ writeMcpConfig(file, {
3413
+ ...config,
3414
+ mcpServers: servers
3415
+ });
3416
+ }
3417
+ //#endregion
3418
+ //#region lib/types/kimi-cli-provider.js
3419
+ /**
3420
+ * One-shot and resumable Kimi CLI subagent lifecycle: spawn `kimi -p "<task>"`
3421
+ * through the subprocess seam, capture its printed response as the run output,
3422
+ * and dispose to whole-tree quiescence. Mirrors the official one-shot Codex
3423
+ * provider, and adds the family's own resume: a later round calls the tool with
3424
+ * the first round's dsh child session id, the family registry maps it back to
3425
+ * the kimi session, and the provider spawns `kimi -S session_<id> -p` so the
3426
+ * SAME kimi conversation continues inside the SAME dsh child session.
3427
+ * @module @khorsheed/dsh-local-agent-kimi/kimi-cli-provider
3428
+ */
3429
+ /** Default POSIX grace between subprocess termination tiers. */
3430
+ const DEFAULT_DISPOSE_GRACE_MS = 3e3;
3431
+ /**
3432
+ * Auth-shaped failure signatures in kimi's stderr or output: the endpoint
3433
+ * rejected the credential. Narrow on purpose — matched against
3434
+ * provider-controlled error output only.
3435
+ */
3436
+ const KIMI_AUTH_FAILURE = /401|unauthorized|invalid api key|not authenticated/i;
3437
+ /**
3438
+ * One-shot Kimi CLI subagent provider: every accepted fresh run starts a
3439
+ * fresh `kimi -p` process in the delegating Session's workspace, under the
3440
+ * harness scoped home; a resume round (the family tool's staged resume intent)
3441
+ * continues the SAME kimi session with `kimi -S session_<id> -p` inside the
3442
+ * SAME dsh child session. Mirrors the official one-shot Codex provider for the
3443
+ * fresh path, including `NO_START_CAPABILITIES` — continuation is the family's
3444
+ * own resume mechanism, not the official Agent-type continuable seam.
3445
+ */
3446
+ /**
3447
+ * Mark the harness credential verified after a completed round, degrading
3448
+ * silently on a core that predates the mark (the family's companion-pair
3449
+ * rule: a provider paired with an older core loses the grade, never the run).
3450
+ * @param ctx - host context carrying the family registry.
3451
+ */
3452
+ function markCredentialVerified(ctx, scope) {
3453
+ const registry = ctx.get("localAgent");
3454
+ if (registry === void 0 || typeof registry.reportAuthSuccess !== "function") return;
3455
+ registry.reportAuthSuccess("kimi", scope);
3456
+ }
3457
+ /**
3458
+ * The version `kimi --version` reports, probed through the shared subprocess
3459
+ * seam inside the SCOPED home (never the user's own installation) and cached
3460
+ * by the family probe against the executable's identity. Kimi's wire log names
3461
+ * no build, so this is the only version channel the harness has — both the
3462
+ * effective-settings snapshot and a settled round's read-back read it.
3463
+ * @param ctx - host context carrying the subprocess seam.
3464
+ * @param homeDir - the harness's scoped home.
3465
+ * @returns the version, or undefined when the CLI cannot be asked.
3466
+ */
3467
+ function kimiCliVersion(ctx, homeDir) {
3468
+ const subprocess = ctx.get("subprocess");
3469
+ if (subprocess === void 0) return Promise.resolve(void 0);
3470
+ return probeCliVersion({
3471
+ argv: ["kimi", "--version"],
3472
+ cwd: homeDir,
3473
+ spawn: (spec) => subprocess.spawn(spec),
3474
+ env: delegationEnv({ KIMI_CODE_HOME: homeDir })
3475
+ });
3476
+ }
3477
+ var KimiCliProvider = class {
3478
+ ctx;
3479
+ live;
3480
+ model;
3481
+ memberModels;
3482
+ name = "kimi-cli";
3483
+ capabilities = NO_START_CAPABILITIES;
3484
+ inheritsParentContext = false;
3485
+ /**
3486
+ * @param live - the live driver, or a resolver returning the current
3487
+ * generation's driver per member (the settings toggle swaps generations;
3488
+ * a resolver may return undefined to steer one member's round to exec
3489
+ * while a retiring generation still hosts it).
3490
+ * @param model - resolver for the configured model, read PER ROUND so a
3491
+ * settings-card write takes effect on the next delegation without a
3492
+ * reload. Undefined (the resolver absent, or returning undefined) leaves
3493
+ * the argv exactly as it was before the key rode every round — the scoped
3494
+ * config.toml's `default_model` then decides, as it always did.
3495
+ * @param memberModels - the broker's member-model ledger: the provider
3496
+ * records each delegation's start model here (the live driver's spawn
3497
+ * resolver binds it) and reads the session-level override as the exec
3498
+ * path's top layer. Absent (older wiring), both drop out.
3499
+ */
3500
+ constructor(ctx, live, model, memberModels) {
3501
+ this.ctx = ctx;
3502
+ this.live = live;
3503
+ this.model = model;
3504
+ this.memberModels = memberModels;
3505
+ }
3506
+ /** Resolve the live driver for one round's member, if live is on for it. */
3507
+ liveDriver(childSessionId) {
3508
+ const live = this.live;
3509
+ if (live === void 0) return void 0;
3510
+ return typeof live === "function" ? live(childSessionId) : live;
3511
+ }
3512
+ /**
3513
+ * Register one run with the member channel and declare the bridge MCP server
3514
+ * in the scoped home's mcp.json (per-run entry, socket + token in its env).
3515
+ * Returns the release handle, or undefined when the mounted core
3516
+ * predates the member channel (declare-and-degrade: the run proceeds
3517
+ * exactly as before). Token-only auth: host 0.1.5 removed the child pid the
3518
+ * parentage cross-check used; the per-run token is the sole credential (see
3519
+ * the package README's threat model). `release` invalidates the token and
3520
+ * prunes the config entry on any settle path.
3521
+ */
3522
+ memberRun(childSessionId, parentSessionId, homeDir) {
3523
+ const registry = this.ctx.localAgent;
3524
+ if (typeof registry.registerMemberRun !== "function" || typeof registry.memberBridgeSocketPath !== "function" || typeof registry.memberBridgeCommand !== "function") return void 0;
3525
+ const token = registry.registerMemberRun({
3526
+ childSessionId,
3527
+ parentSessionId,
3528
+ provider: this.name
3529
+ });
3530
+ const key = memberBridgeServerKey(token);
3531
+ try {
3532
+ injectMemberBridge(homeDir, key, registry.memberBridgeCommand(), {
3533
+ [MEMBER_BRIDGE_SOCKET_ENV]: registry.memberBridgeSocketPath(),
3534
+ [MEMBER_BRIDGE_TOKEN_ENV]: token
3535
+ });
3536
+ } catch (error) {
3537
+ registry.unregisterMemberRun(token);
3538
+ this.ctx.logger.warn(`subagent-kimi: member bridge config injection failed: ${error instanceof Error ? error.message : String(error)}`);
3539
+ return;
3540
+ }
3541
+ let released = false;
3542
+ return { release: () => {
3543
+ if (released) return;
3544
+ released = true;
3545
+ registry.unregisterMemberRun(token);
3546
+ try {
3547
+ removeMemberBridge(homeDir, key);
3548
+ } catch (error) {
3549
+ this.ctx.logger.warn(`subagent-kimi: member bridge config cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
3550
+ }
3551
+ } };
3552
+ }
3553
+ async start(request) {
3554
+ const intent = this.ctx.localAgent.takeDelegationIntent(request.parent.session.id, this.name);
3555
+ const scope = intent?.scope;
3556
+ const homeDir = this.ctx.localAgent.homeDir("kimi", scope);
3557
+ const cwd = resolveChildCwd(request.parent.session.header.cwd, intent?.cwd);
3558
+ if (cwd === void 0) throw new Error("subagent-kimi: the parent session has no working directory to run the CLI in");
3559
+ const exec = intent?.exec;
3560
+ if (exec !== void 0) containerScopedHome(exec, "KIMI_CODE_HOME", "subagent-kimi");
3561
+ if (intent !== void 0 && intent.kind === "resume") {
3562
+ const record = this.ctx.localAgent.getDelegation(intent.childSessionId);
3563
+ assertResumeCwdUnchanged(record, cwd, "subagent-kimi");
3564
+ assertResumeScopeUnchanged(record, scope, "subagent-kimi");
3565
+ if (typeof this.ctx.localAgent.withMemberConfigurationRound !== "function") return this.startKimiResume(request, intent, cwd, homeDir, exec, scope, record?.model);
3566
+ return this.ctx.localAgent.withMemberConfigurationRound({
3567
+ childSessionId: intent.childSessionId,
3568
+ provider: this.name,
3569
+ parentSessionId: request.parent.session.id,
3570
+ cwd,
3571
+ ...scope === void 0 ? {} : { scope },
3572
+ ...record?.model === void 0 ? {} : { model: record.model },
3573
+ ...record?.effort === void 0 ? {} : { effort: record.effort },
3574
+ ...record?.configurationLock === void 0 ? {} : { configurationLock: record.configurationLock }
3575
+ }, (configuration) => this.startKimiResume(request, intent, cwd, homeDir, exec, scope, record?.model, configuration), request.signal, intent?.onAdmitted);
3576
+ }
3577
+ const childSessionId = SessionId(intent?.preparedMemberId ?? randomUUID());
3578
+ if (typeof this.ctx.localAgent.withMemberConfigurationRound !== "function") {
3579
+ if (intent?.effort !== void 0) throw new Error("Kimi effort requires the configuration admission core");
3580
+ return this.startKimiFresh(request, cwd, homeDir, exec, scope, intent?.model, childSessionId);
3581
+ }
3582
+ return this.ctx.localAgent.withMemberConfigurationRound({
3583
+ childSessionId,
3584
+ provider: this.name,
3585
+ parentSessionId: request.parent.session.id,
3586
+ cwd,
3587
+ ...scope === void 0 ? {} : { scope },
3588
+ ...intent?.model === void 0 ? {} : { model: intent.model },
3589
+ ...intent?.effort === void 0 ? {} : { effort: intent.effort },
3590
+ ...intent?.configurationLock === void 0 ? {} : { configurationLock: intent.configurationLock }
3591
+ }, (configuration) => this.startKimiFresh(request, cwd, homeDir, exec, scope, intent?.model, childSessionId, configuration), request.signal, intent?.onAdmitted);
3592
+ }
3593
+ /** Fresh round: record the child session, spawn `kimi -p`, mirror after settle. */
3594
+ async startKimiFresh(request, cwd, homeDir, exec, scope, requestedModel, runId, configuration) {
3595
+ let childSession;
3596
+ try {
3597
+ const sessions = this.ctx.get("sessions");
3598
+ if (sessions === void 0) throw new Error("the sessions service is not mounted");
3599
+ childSession = sessions.get(runId) ?? sessions.create(runId, { meta: {
3600
+ cwd,
3601
+ parentSession: request.parent.session.id,
3602
+ origin: "subagent",
3603
+ delegationDepth: (request.parent.session.header.delegationDepth ?? 0) + 1
3604
+ } });
3605
+ const harness = this.ctx.localAgent.get("kimi");
3606
+ const label = subagentDelegationLabel(harness?.displayName ?? "kimi", request.descriptor.label);
3607
+ childSession.append("subagent/descriptor", {
3608
+ ...request.descriptor,
3609
+ label
3610
+ });
3611
+ establishSubagentCatalogChild(request.parent.session, childSession.header, label);
3612
+ } catch (error) {
3613
+ this.ctx.logger.warn(`subagent-kimi: subagent session record failed: ${error instanceof Error ? error.message : String(error)}`);
3614
+ }
3615
+ const baseUrl = await readKimiBaseUrl(homeDir).catch(() => void 0);
3616
+ this.ctx.logger.info(`subagent-kimi: delegating via ${baseUrl ?? "kimi default endpoint"}`);
3617
+ const live = exec === void 0 ? this.liveDriver(runId) : void 0;
3618
+ if (live !== void 0 && childSession !== void 0 && !live.disabled) {
3619
+ if (requestedModel !== void 0) this.memberModels?.noteStartModel(runId, requestedModel);
3620
+ try {
3621
+ return await live.startRound(request, {
3622
+ cwd,
3623
+ homeDir,
3624
+ childSession,
3625
+ ...configuration === void 0 ? {} : { configuration: configuration.resolved },
3626
+ parentSessionId: request.parent.session.id,
3627
+ onCliSessionId: (cliSessionId) => {
3628
+ this.ctx.localAgent.recordDelegation({
3629
+ childSessionId: runId,
3630
+ provider: this.name,
3631
+ parentSessionId: request.parent.session.id,
3632
+ cliSessionId,
3633
+ cwd,
3634
+ ...scope === void 0 ? {} : { scope },
3635
+ ...requestedModel === void 0 ? {} : { model: requestedModel }
3636
+ });
3637
+ }
3638
+ });
3639
+ } catch (error) {
3640
+ if (!(error instanceof LiveChannelUnavailableError) || request.signal.aborted) throw error;
3641
+ this.ctx.logger.warn(`subagent-kimi: live driver unavailable, using the exec one-shot: ${error.message}`);
3642
+ }
3643
+ }
3644
+ const member = exec === void 0 && scope === void 0 ? this.memberRun(runId, request.parent.session.id, homeDir) : void 0;
3645
+ try {
3646
+ const run = await startKimiCliRun(request, {
3647
+ cwd,
3648
+ env: delegationEnv({ KIMI_CODE_HOME: homeDir }),
3649
+ ...exec === void 0 ? {} : { exec },
3650
+ endpointLabel: baseUrl,
3651
+ ...configuration?.resolved ?? resolveRoundModel(this.memberModels?.overrideFor(runId) ?? requestedModel, this.model),
3652
+ ...configuration === void 0 ? {} : { controlled: true },
3653
+ disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
3654
+ spawn: (spec) => this.ctx.subprocess.spawn(spec),
3655
+ onError: (error, stopReason) => {
3656
+ this.ctx.logger.warn(`subagent-kimi: child run failed (${stopReason}) via ${baseUrl ?? "kimi default endpoint"}: ${error instanceof Error ? error.message : String(error)}`);
3657
+ },
3658
+ onAuthFailure: (detail) => {
3659
+ guardKimiCredential(homeDir, (message) => {
3660
+ this.ctx.logger.warn(message);
3661
+ });
3662
+ this.ctx.localAgent.reportAuthFailure("kimi", detail, scope);
3663
+ },
3664
+ onAuthSuccess: () => {
3665
+ markCredentialVerified(this.ctx, scope);
3666
+ },
3667
+ cliVersion: () => kimiCliVersion(this.ctx, homeDir),
3668
+ childSession,
3669
+ homeDir,
3670
+ ctx: this.ctx,
3671
+ onCliSessionId: (cliSessionId) => {
3672
+ if (cliSessionId === void 0) return;
3673
+ this.ctx.localAgent.recordDelegation({
3674
+ childSessionId: runId,
3675
+ provider: this.name,
3676
+ parentSessionId: request.parent.session.id,
3677
+ cliSessionId,
3678
+ cwd,
3679
+ ...scope === void 0 ? {} : { scope },
3680
+ ...requestedModel === void 0 ? {} : { model: requestedModel }
3681
+ });
3682
+ },
3683
+ onRoundSettled: (round) => {
3684
+ this.ctx.localAgent.recordRoundSettled(runId, round);
3685
+ }
3686
+ });
3687
+ if (member !== void 0) run.result.then(member.release, member.release);
3688
+ return run;
3689
+ } catch (error) {
3690
+ member?.release();
3691
+ throw error;
3692
+ }
3693
+ }
3694
+ /** Resume round: continue the recorded kimi session inside the existing child session. */
3695
+ async startKimiResume(request, intent, cwd, homeDir, exec, scope, requestedModel, configuration) {
3696
+ if (!this.ctx.localAgent.acquireResumeLock(intent.childSessionId)) throw new Error(`subagent-kimi: 该子会话有进行中的委派,等其完成后再追问 (child session ${intent.childSessionId})`);
3697
+ try {
3698
+ const childSession = this.ctx.get("sessions")?.get(SessionId(intent.childSessionId));
3699
+ if (childSession === void 0) throw new Error(`subagent-kimi: resume target child session ${intent.childSessionId} is not live — start a fresh delegation instead`);
3700
+ const nextTurn = childSession.snapshotEvents().filter((event) => event.type === "turn/start").length + 1;
3701
+ const live = exec === void 0 ? this.liveDriver(intent.childSessionId) : void 0;
3702
+ if (live !== void 0 && !live.disabled) {
3703
+ if (requestedModel !== void 0) this.memberModels?.noteStartModel(intent.childSessionId, requestedModel);
3704
+ try {
3705
+ const liveRun = await live.startRound(request, {
3706
+ cwd,
3707
+ homeDir,
3708
+ childSession,
3709
+ ...configuration === void 0 ? {} : { configuration: configuration.resolved },
3710
+ parentSessionId: request.parent.session.id,
3711
+ resume: {
3712
+ cliSessionId: intent.cliSessionId,
3713
+ turn: nextTurn
3714
+ }
3715
+ });
3716
+ liveRun.result.then(() => {
3717
+ this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
3718
+ }, () => {
3719
+ this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
3720
+ });
3721
+ return liveRun;
3722
+ } catch (error) {
3723
+ if (!(error instanceof LiveChannelUnavailableError) || request.signal.aborted) throw error;
3724
+ this.ctx.logger.warn(`subagent-kimi: live driver unavailable, using the exec one-shot: ${error.message}`);
3725
+ }
3726
+ }
3727
+ const member = exec === void 0 && scope === void 0 ? this.memberRun(intent.childSessionId, request.parent.session.id, homeDir) : void 0;
3728
+ const baseUrl = await readKimiBaseUrl(homeDir).catch(() => void 0);
3729
+ this.ctx.logger.info(`subagent-kimi: resuming via ${baseUrl ?? "kimi default endpoint"}`);
3730
+ let run;
3731
+ try {
3732
+ run = await startKimiCliRun(request, {
3733
+ cwd,
3734
+ env: delegationEnv({ KIMI_CODE_HOME: homeDir }),
3735
+ ...exec === void 0 ? {} : { exec },
3736
+ endpointLabel: baseUrl,
3737
+ ...configuration?.resolved ?? resolveRoundModel(this.memberModels?.overrideFor(intent.childSessionId) ?? requestedModel, this.model),
3738
+ ...configuration === void 0 ? {} : { controlled: true },
3739
+ disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
3740
+ spawn: (spec) => this.ctx.subprocess.spawn(spec),
3741
+ onError: (error, stopReason) => {
3742
+ this.ctx.logger.warn(`subagent-kimi: child run failed (${stopReason}) via ${baseUrl ?? "kimi default endpoint"}: ${error instanceof Error ? error.message : String(error)}`);
3743
+ },
3744
+ onAuthFailure: (detail) => {
3745
+ guardKimiCredential(homeDir, (message) => {
3746
+ this.ctx.logger.warn(message);
3747
+ });
3748
+ this.ctx.localAgent.reportAuthFailure("kimi", detail, scope);
3749
+ },
3750
+ onAuthSuccess: () => {
3751
+ markCredentialVerified(this.ctx, scope);
3752
+ },
3753
+ cliVersion: () => kimiCliVersion(this.ctx, homeDir),
3754
+ childSession,
3755
+ homeDir,
3756
+ ctx: this.ctx,
3757
+ resume: {
3758
+ cliSessionId: intent.cliSessionId,
3759
+ turn: nextTurn
3760
+ },
3761
+ onRoundSettled: (round) => {
3762
+ this.ctx.localAgent.recordRoundSettled(intent.childSessionId, round);
3763
+ }
3764
+ });
3765
+ } catch (error) {
3766
+ member?.release();
3767
+ throw error;
3768
+ }
3769
+ run.result.then(() => {
3770
+ this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
3771
+ member?.release();
3772
+ }, () => {
3773
+ this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
3774
+ member?.release();
3775
+ });
3776
+ return run;
3777
+ } catch (error) {
3778
+ this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
3779
+ throw error;
3780
+ }
3781
+ }
3782
+ };
3783
+ function thrown(value) {
3784
+ /* v8 ignore next -- typed subprocess failures reject with Error. */
3785
+ return value instanceof Error ? value : new Error(String(value));
3786
+ }
3787
+ /**
3788
+ * Extract the kimi session id from the CLI's printed response. `kimi -p`
3789
+ * prints a resume hint (`To resume this session: kimi -r session_<id>`) on
3790
+ * stdout after a run; mirroring that exact session instead of the newest-by-
3791
+ * mtime keeps concurrent delegations from cross-mirroring.
3792
+ * @param output - the collected CLI stdout.
3793
+ * @returns the session id, or undefined when the hint is absent.
3794
+ */
3795
+ function kimiSessionIdFromOutput(output) {
3796
+ return /kimi -r session_([0-9a-fA-F-]+)/.exec(output)?.[1];
3797
+ }
3798
+ /**
3799
+ * Validate and join the one-shot task before crossing the process boundary.
3800
+ * @param prompt - task content accepted from the shared subagent service.
3801
+ * @returns the joined non-empty text.
3802
+ */
3803
+ function textTask(prompt) {
3804
+ if (prompt.length === 0) throw new Error("subagent-kimi: the one-shot task must contain only text blocks");
3805
+ const texts = [];
3806
+ for (const block of prompt) {
3807
+ if (block.type !== "text") throw new Error("subagent-kimi: the one-shot task must contain only text blocks");
3808
+ texts.push(block.text);
3809
+ }
3810
+ if (texts.every((text) => text.trim().length === 0)) throw new Error("subagent-kimi: the one-shot task must not be empty");
3811
+ return texts.join("\n");
3812
+ }
3813
+ /**
3814
+ * Mirror the transcript delta after the recorded offset, advance the offset,
3815
+ * and report one `delta` progress per newly mirrored line. Shared by the live
3816
+ * poll and the settle-time mirror so the two paths cannot drift. The offset
3817
+ * read (`kimiMirroredLines`) and write (`setKimiMirroredLines`) are the same
3818
+ * bookkeeping the settle path used before live mirroring existed.
3819
+ * @param ctx - host context carrying localAgent and session persistence.
3820
+ * @param childSession - the dsh child session being mirrored into.
3821
+ * @param homeDir - the `kimi` harness's scoped home.
3822
+ * @param kimiSessionId - the kimi session to mirror.
3823
+ * @returns the mirror delta (new total + newly mirrored line texts).
3824
+ */
3825
+ async function mirrorKimiDelta(ctx, childSession, homeDir, kimiSessionId, options) {
3826
+ const localAgent = ctx.get("localAgent");
3827
+ const delta = await mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId, localAgent?.kimiMirroredLines(childSession.id) ?? 0, options);
3828
+ if (delta.texts.length === 0) return delta;
3829
+ localAgent?.setKimiMirroredLines(childSession.id, delta.total);
3830
+ for (const text of delta.texts) localAgent?.reportRunProgress(childSession.id, {
3831
+ kind: "delta",
3832
+ text
3833
+ });
3834
+ return delta;
3835
+ }
3836
+ /**
3837
+ * Mirror the kimi session's transcript into the child session AFTER the CLI
3838
+ * process has exited, whatever its stop reason. A cancelled or failed round
3839
+ * still preserves whatever the wire already recorded (partial answer, tool
3840
+ * activity, usage) instead of leaving the child blank — the run result
3841
+ * settles 'aborted'/'error' at the cancel moment, but the CLI may have
3842
+ * produced content before the kill landed, and that content belongs in the
3843
+ * 子代理 surface with its real token accounting. Also records the delegation
3844
+ * (fresh rounds) so a later resume can continue the session.
3845
+ * @param spec - the run spec carrying the child session, home, and context.
3846
+ * @param stderr - the collected CLI stderr (the resume hint lives here).
3847
+ * @returns this pass's observation — the wire's model identifier and the
3848
+ * window's usage — or undefined when nothing was mirrored; the caller sums
3849
+ * the window usage into the round total and reports the round's settled
3850
+ * observation.
3851
+ */
3852
+ async function mirrorKimiAfterExit(spec, stderr) {
3853
+ if (spec.childSession === void 0 || spec.homeDir === void 0 || spec.ctx === void 0) return void 0;
3854
+ try {
3855
+ const kimiSessionId = spec.resume?.cliSessionId ?? kimiSessionIdFromOutput(stderr);
3856
+ if (spec.resume === void 0) spec.onCliSessionId?.(kimiSessionId);
3857
+ const delta = await mirrorKimiDelta(spec.ctx, spec.childSession, spec.homeDir, kimiSessionId, { turn: spec.resume?.turn ?? 1 });
3858
+ spec.ctx.get("localAgent")?.reportRunProgress(spec.childSession.id, {
3859
+ kind: "mirror",
3860
+ mirroredLines: delta.total
3861
+ });
3862
+ return {
3863
+ ...delta.model === void 0 ? {} : { model: delta.model },
3864
+ ...delta.usage === void 0 ? {} : { usage: delta.usage },
3865
+ ...delta.toolCalls === void 0 ? {} : { toolCalls: delta.toolCalls }
3866
+ };
3867
+ } catch (error) {
3868
+ spec.onError?.(thrown(error), "error");
3869
+ return;
3870
+ }
3871
+ }
3872
+ /**
3873
+ * Start the real `kimi -p` (fresh) or `kimi -S session_<id> -p` (resume)
3874
+ * child and publish its run. The kimi response is printed to stdout; stderr
3875
+ * is inherited for diagnostics. The run id is the child session id for a
3876
+ * session-backed run, so the tool's `resume="<id>"` self-description names
3877
+ * the same handle the registry records.
3878
+ * @param request - resolved shared subagent request.
3879
+ * @param spec - workspace, environment, process service, and diagnostic policy.
3880
+ * @returns the published run after the child starts.
3881
+ */
3882
+ async function startKimiCliRun(request, spec) {
3883
+ const task = textTask(request.prompt);
3884
+ if (request.signal.aborted) throw new Error("subagent-kimi: request was aborted before the CLI started");
3885
+ const turn = spec.resume?.turn ?? 1;
3886
+ spec.childSession?.append("turn/start", { turn });
3887
+ const modelArgv = spec.model === void 0 ? [] : ["-m", spec.model];
3888
+ const argv = spec.resume === void 0 ? [
3889
+ "kimi",
3890
+ ...modelArgv,
3891
+ "-p",
3892
+ task
3893
+ ] : [
3894
+ "kimi",
3895
+ "-S",
3896
+ spec.resume.cliSessionId.startsWith("session_") ? spec.resume.cliSessionId : `session_${spec.resume.cliSessionId}`,
3897
+ ...modelArgv,
3898
+ "-p",
3899
+ task
3900
+ ];
3901
+ const configurationHome = spec.homeDir ?? spec.env["KIMI_CODE_HOME"];
3902
+ if (spec.controlled && spec.effort !== void 0 && !configurationHome) throw new Error("Kimi effort requires an explicit scoped configuration home");
3903
+ const configEnv = spec.controlled && spec.effort !== void 0 ? await kimiExecConfiguration(configurationHome, spec.model, spec.effort) : {};
3904
+ const env = {
3905
+ ...spec.env,
3906
+ ...configEnv
3907
+ };
3908
+ const launch = spec.exec === void 0 ? {
3909
+ argv,
3910
+ env
3911
+ } : containerExecSpawn(spec.exec, {
3912
+ argv,
3913
+ env
3914
+ }, "subagent-kimi");
3915
+ const child = spec.spawn({
3916
+ argv: launch.argv,
3917
+ cwd: spec.cwd,
3918
+ stdio: {
3919
+ stdin: "ignore",
3920
+ stdout: "pipe",
3921
+ stderr: "pipe"
3922
+ },
3923
+ graceMs: spec.disposeGraceMs,
3924
+ env: launch.env
3925
+ });
3926
+ let output = "";
3927
+ child.stdout?.on("data", (chunk) => {
3928
+ output += chunk.toString();
3929
+ });
3930
+ let stderr = "";
3931
+ child.stderr?.on("data", (chunk) => {
3932
+ stderr += chunk.toString();
3933
+ });
3934
+ let roundUsage;
3935
+ const addRoundUsage = (usage) => {
3936
+ roundUsage = addTokenUsage(roundUsage, usage);
3937
+ };
3938
+ let mirrorQueue = Promise.resolve();
3939
+ const enqueueMirror = (task) => {
3940
+ mirrorQueue = mirrorQueue.then(task);
3941
+ };
3942
+ if (spec.childSession !== void 0 && spec.homeDir !== void 0 && spec.ctx !== void 0) {
3943
+ const childSession = spec.childSession;
3944
+ const homeDir = spec.homeDir;
3945
+ const mirrorCtx = spec.ctx;
3946
+ const poll = async () => {
3947
+ const kimiSessionId = spec.resume?.cliSessionId ?? kimiSessionIdFromOutput(stderr);
3948
+ if (kimiSessionId === void 0) return;
3949
+ const delta = await mirrorKimiDelta(mirrorCtx, childSession, homeDir, kimiSessionId);
3950
+ addRoundUsage(delta.usage);
3951
+ if (delta.texts.length > 0) mirrorCtx.get("localAgent")?.reportRunProgress(childSession.id, {
3952
+ kind: "mirror",
3953
+ mirroredLines: delta.total
3954
+ });
3955
+ };
3956
+ const liveMirror = setInterval(() => {
3957
+ enqueueMirror(async () => {
3958
+ try {
3959
+ await poll();
3960
+ } catch (error) {
3961
+ mirrorCtx.logger.warn(`subagent-kimi: live mirror poll failed: ${thrown(error).message}`);
3962
+ }
3963
+ });
3964
+ }, spec.liveMirrorIntervalMs ?? 2e3);
3965
+ liveMirror.unref();
3966
+ child.done.then(() => {
3967
+ clearInterval(liveMirror);
3968
+ }, () => {
3969
+ clearInterval(liveMirror);
3970
+ });
3971
+ }
3972
+ const disposeProcess = async () => {
3973
+ child.terminate();
3974
+ await child.waitForExit().catch(() => false);
3975
+ await child.done.catch(() => {});
3976
+ };
3977
+ const runAbort = new AbortController();
3978
+ const requestCancel = () => {
3979
+ if (runAbort.signal.aborted) return;
3980
+ runAbort.abort(/* @__PURE__ */ new Error("subagent-kimi: run cancelled locally"));
3981
+ };
3982
+ const onAbort = () => {
3983
+ requestCancel();
3984
+ };
3985
+ request.signal.addEventListener("abort", onAbort, { once: true });
3986
+ const abortBranch = new Promise((_, reject) => {
3987
+ runAbort.signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("subagent-kimi: run cancelled locally")), { once: true });
3988
+ });
3989
+ const processFailure = child.done.then((outcome) => Promise.reject(/* @__PURE__ */ new Error(`subagent-kimi: CLI exited before the run settled (code ${String(outcome.exitCode)}, signal ${String(outcome.signal)})`)), (error) => Promise.reject(thrown(error)));
3990
+ processFailure.catch(() => {});
3991
+ const collectOutput = () => {
3992
+ const drained = child.collected.stdout?.readFrom(0).text;
3993
+ const text = (drained !== void 0 && drained !== "" ? drained : output).trim();
3994
+ return text === "" ? [] : [{
3995
+ type: "text",
3996
+ text
3997
+ }];
3998
+ };
3999
+ const result = settleRunResult({
4000
+ attempt: () => Promise.race([
4001
+ child.done.then((outcome) => {
4002
+ if (outcome.exitCode !== 0) {
4003
+ const via = spec.endpointLabel ?? "kimi default endpoint";
4004
+ throw new Error(`subagent-kimi: kimi -p exited with code ${String(outcome.exitCode)} via ${via}`);
4005
+ }
4006
+ const output = collectOutput();
4007
+ if (output.length === 0) throw new Error("subagent-kimi: kimi -p exited 0 but produced no answer");
4008
+ return {
4009
+ output,
4010
+ stopReason: "completed"
4011
+ };
4012
+ }),
4013
+ processFailure,
4014
+ abortBranch
4015
+ ]),
4016
+ collectOutput,
4017
+ cancelled: () => runAbort.signal.aborted,
4018
+ onError: spec.onError,
4019
+ signal: request.signal,
4020
+ onAbort
4021
+ }).then((settled) => {
4022
+ if (settled.stopReason === "completed") spec.onAuthSuccess?.();
4023
+ if (spec.childSession !== void 0) {
4024
+ if (settled.stopReason === "completed") spec.childSession.append("turn/end", {
4025
+ turn,
4026
+ reason: { kind: "completed" }
4027
+ });
4028
+ else if (settled.stopReason === "aborted") spec.childSession.append("turn/end", {
4029
+ turn,
4030
+ reason: {
4031
+ kind: "aborted",
4032
+ reason: { kind: "parent" }
4033
+ }
4034
+ });
4035
+ else spec.childSession.append("turn/end", {
4036
+ turn,
4037
+ reason: {
4038
+ kind: "error",
4039
+ error: {
4040
+ message: "kimi -p exited before the run completed",
4041
+ code: "UNKNOWN"
4042
+ }
4043
+ }
4044
+ });
4045
+ }
4046
+ return settled;
4047
+ });
4048
+ result.then(() => child.done).then(() => {
4049
+ if (spec.onAuthFailure !== void 0) {
4050
+ const authDetail = `${child.collected.stderr?.readFrom(0).text || stderr}\n${child.collected.stdout?.readFrom(0).text || output}`;
4051
+ if (KIMI_AUTH_FAILURE.test(authDetail)) spec.onAuthFailure(authDetail.split("\n").find((line) => line.trim() !== "") ?? "auth failure");
4052
+ }
4053
+ enqueueMirror(async () => {
4054
+ const settled = await mirrorKimiAfterExit(spec, stderr);
4055
+ addRoundUsage(settled?.usage);
4056
+ const cliVersion = await spec.cliVersion?.();
4057
+ spec.onRoundSettled?.({
4058
+ ...settled?.model === void 0 ? {} : { observedModel: settled.model },
4059
+ ...cliVersion === void 0 ? {} : { cliVersion },
4060
+ ...roundUsage === void 0 ? {} : { usage: roundUsage },
4061
+ ...settled?.toolCalls === void 0 ? {} : { toolCalls: settled.toolCalls }
4062
+ });
4063
+ });
4064
+ }, () => {});
4065
+ return Promise.resolve(subprocessRunHandle({
4066
+ id: spec.childSession?.id ?? SessionId(randomUUID()),
4067
+ result,
4068
+ signal: request.signal,
4069
+ onAbort,
4070
+ requestCancel,
4071
+ teardown: disposeProcess
4072
+ }));
4073
+ }
4074
+ //#endregion
4075
+ //#region lib/types/live-switch.js
4076
+ /**
4077
+ * The settings-driven live-driver switch: owns the active driver generation
4078
+ * and retires old ones on a settings change, without a plugin reload.
4079
+ *
4080
+ * Toggle OFF (or unload): the retiring generation is DRAINED, not killed —
4081
+ * new rounds are refused (the provider's catch falls back to exec), in-flight
4082
+ * rounds finish on their runtime, idle runtimes are reclaimed at once.
4083
+ * Toggling ON builds the next generation lazily (no process until the first
4084
+ * round). Legacy granularity settings are ignored.
4085
+ *
4086
+ * @module @khorsheed/dsh-local-agent-kimi — internal, unit-tested directly.
4087
+ */
4088
+ var LiveDriverSwitch = class {
4089
+ ctx;
4090
+ liveIdleMs;
4091
+ model;
4092
+ liveOn = false;
4093
+ active;
4094
+ /** Retiring generations still draining their in-flight rounds. */
4095
+ draining = /* @__PURE__ */ new Set();
4096
+ unwatch;
4097
+ constructor(ctx, face, liveIdleMs, model) {
4098
+ this.ctx = ctx;
4099
+ this.liveIdleMs = liveIdleMs;
4100
+ this.model = model;
4101
+ this.apply(face.get());
4102
+ this.unwatch = face.watch((next) => {
4103
+ this.apply(next);
4104
+ });
4105
+ }
4106
+ /**
4107
+ * Resolve the live driver for one round's member (the provider's call
4108
+ * site). A retiring generation still hosting the member gates the new one —
4109
+ * the round falls back to exec rather than double-loading the same kimi
4110
+ * session into two processes.
4111
+ */
4112
+ resolve = (childSessionId) => {
4113
+ if (this.active === void 0) return void 0;
4114
+ for (const retiring of this.draining) if (retiring.hasRuntime(childSessionId)) return void 0;
4115
+ return this.active;
4116
+ };
4117
+ /** Mirror the resolved settings into the driver generation. */
4118
+ apply(next) {
4119
+ if (next.live === this.liveOn) return;
4120
+ this.liveOn = next.live;
4121
+ const retiring = this.active;
4122
+ this.active = next.live ? new KimiAcpLiveDriver(this.ctx, {
4123
+ ...this.liveIdleMs === void 0 ? {} : { liveIdleMs: this.liveIdleMs },
4124
+ ...this.model === void 0 ? {} : { model: this.model }
4125
+ }) : void 0;
4126
+ if (retiring !== void 0) {
4127
+ this.draining.add(retiring);
4128
+ retiring.drain().then(() => {
4129
+ this.draining.delete(retiring);
4130
+ }, (error) => {
4131
+ this.draining.delete(retiring);
4132
+ this.ctx.logger.warn(`local-agent-kimi: live driver drain failed: ${error instanceof Error ? error.message : String(error)}`);
4133
+ });
4134
+ }
4135
+ }
4136
+ /**
4137
+ * Retire one member's runtime on the ACTIVE generation (a composer-driven
4138
+ * model switch): the next round respawns onto the new model. A member still
4139
+ * hosted by a draining generation is left alone — its rounds already gate
4140
+ * to exec, and the drain reclaims the process.
4141
+ */
4142
+ async retireMemberRuntime(childSessionId) {
4143
+ await this.active?.retireRuntime(childSessionId);
4144
+ }
4145
+ /**
4146
+ * The model the member's active-generation runtime is bound to, or
4147
+ * undefined when no live runtime serves the member (the broker's
4148
+ * same-model no-op check).
4149
+ */
4150
+ memberRuntimeModel(childSessionId) {
4151
+ return this.active?.runtimeModel(childSessionId);
4152
+ }
4153
+ /**
4154
+ * Whether the active generation hosts (or is spawning) the member's
4155
+ * runtime — the broker's retire decision needs this apart from the bound
4156
+ * model, since a runtime that bound NO model reports undefined either way.
4157
+ */
4158
+ memberHasRuntime(childSessionId) {
4159
+ return this.active?.hasRuntime(childSessionId) ?? false;
4160
+ }
4161
+ memberRuntimeConfiguration(childSessionId) {
4162
+ for (const driver of [this.active, ...this.draining]) {
4163
+ const configuration = driver?.runtimeConfiguration(childSessionId);
4164
+ if (configuration !== void 0) return configuration;
4165
+ }
4166
+ }
4167
+ /** Plugin unload: interrupt whatever survives (disposeAll, not drain). */
4168
+ dispose() {
4169
+ this.unwatch();
4170
+ this.active?.disposeAll();
4171
+ for (const retiring of this.draining) retiring.disposeAll();
4172
+ this.draining.clear();
4173
+ }
4174
+ };
4175
+ //#endregion
4176
+ //#region lib/types/model-broker.js
4177
+ /**
4178
+ * The kimi harness's model broker: the member/harness model surface the
4179
+ * family gateway routes to (`harnessModel` for the settings card,
4180
+ * `memberModel`/`setMemberModel` for the composer picker). Resolution follows
4181
+ * the family's fixed order — the session-level override (the composer's
4182
+ * picker write) first, then the delegation's recorded model, then the
4183
+ * plugin-config `model` key, then the scoped config's own `default_model`,
4184
+ * and last the CLI's built-in default, which names nothing.
4185
+ *
4186
+ * Per-member state is IN MEMORY by design: the session-level override is a
4187
+ * composer gesture that deliberately does not survive a host restart, and the
4188
+ * start-model ledger only mirrors what the delegation records already persist
4189
+ * (the core hands the recorded value back as `delegationModel` on reads).
4190
+ * The spawn side of a switch is lazy: `setMemberModel` retires the member's
4191
+ * resident runtime when its bound model no longer matches, and the member's
4192
+ * next round respawns onto the new model, `session/load`-ing the same CLI
4193
+ * session — the conversation carries over, the process fact changes.
4194
+ * @module @khorsheed/dsh-local-agent-kimi/model-broker
4195
+ */
4196
+ /** The refusal a mid-round switch gets (in flight = the runtime must not die). */
4197
+ const SWITCH_IN_FLIGHT_REASON = "该成员有进行中的委派轮次,等其完成后再切换模型";
4198
+ /** Dedupe a candidate list, dropping blanks and keeping first-seen order. */
4199
+ function dedupe(candidates) {
4200
+ const seen = /* @__PURE__ */ new Set();
4201
+ const out = [];
4202
+ for (const candidate of candidates) {
4203
+ const value = candidate?.trim();
4204
+ if (value === void 0 || value === "" || seen.has(value)) continue;
4205
+ seen.add(value);
4206
+ out.push(value);
4207
+ }
4208
+ return out;
4209
+ }
4210
+ /**
4211
+ * The kimi harness's {@link LocalAgentModelBroker}. Also the provider-facing
4212
+ * member-model ledger: the provider records each delegation's start model
4213
+ * here and reads the session-level override for its exec-path resolution, so
4214
+ * the picker write reaches the very next round on either driver.
4215
+ */
4216
+ var KimiModelBroker = class {
4217
+ ctx;
4218
+ deps;
4219
+ /** Session-level overrides per member (the composer picker's writes). */
4220
+ overrides = /* @__PURE__ */ new Map();
4221
+ /** Each member's START model — the model its delegation's first round named. */
4222
+ startModels = /* @__PURE__ */ new Map();
4223
+ /**
4224
+ * @param ctx - plugin context carrying the family registry (the in-flight
4225
+ * read behind `switchable`).
4226
+ * @param deps - the harness's live readers: the scoped home, the settings
4227
+ * layer, the card's recent-model memory, the live toggle, and the driver
4228
+ * switch a switch retires runtimes through.
4229
+ */
4230
+ constructor(ctx, deps) {
4231
+ this.ctx = ctx;
4232
+ this.deps = deps;
4233
+ }
4234
+ configurationAdapter(binding) {
4235
+ const home = this.ctx.localAgent.homeDir("kimi", binding.scope);
4236
+ const resolve = async (selection) => {
4237
+ const model = selection.model.mode === "value" ? selection.model.value : (selection.model.mode === "inherit" ? binding.model : void 0) ?? this.deps.settingsModel() ?? await readKimiDefaultModel(home);
4238
+ const configured = await readKimiModelConfiguration(home, model);
4239
+ const entry = this.deps.liveSwitch.memberRuntimeConfiguration(binding.childSessionId)?.directory.entries.find((entry) => entry.value === model) ?? configured.entries.find((entry) => entry.value === model);
4240
+ const effort = (selection.effort.mode === "value" ? selection.effort.value : selection.effort.mode === "inherit" ? binding.effort : void 0) ?? configured.effort;
4241
+ if (effort !== void 0 && entry?.reasoning !== void 0 && !entry.reasoning.options.some((option) => option.value === effort)) throw new Error("Kimi has not advertised this reasoning effort for the selected model");
4242
+ if (!this.deps.isLive() && effort !== void 0 && configured.protocol !== "kimi") throw new Error("This model protocol has no Kimi exec effort override; use ACP live mode");
4243
+ return {
4244
+ ...model === void 0 ? {} : { model },
4245
+ ...effort === void 0 ? {} : { effort }
4246
+ };
4247
+ };
4248
+ return {
4249
+ validate: async (selection) => {
4250
+ await resolve(selection);
4251
+ },
4252
+ prepare: resolve,
4253
+ apply: async (selection) => {
4254
+ const resolved = await resolve(selection);
4255
+ await this.deps.liveSwitch.retireMemberRuntime(binding.childSessionId);
4256
+ return resolved;
4257
+ },
4258
+ reconcile: async (state) => {
4259
+ if (this.ctx.localAgent.isDelegationActive(binding.childSessionId)) return {
4260
+ active: true,
4261
+ matches: "unknown",
4262
+ resolved: {}
4263
+ };
4264
+ await this.deps.liveSwitch.retireMemberRuntime(binding.childSessionId);
4265
+ return {
4266
+ active: false,
4267
+ matches: "current",
4268
+ resolved: await resolve(state.current.selection)
4269
+ };
4270
+ }
4271
+ };
4272
+ }
4273
+ async modelDirectory(childSessionId, refresh = false) {
4274
+ if (refresh) await this.deps.catalog?.refresh(childSessionId);
4275
+ return (await this.modelInfo(childSessionId)).directory ?? {
4276
+ entries: [],
4277
+ complete: false,
4278
+ customInput: true,
4279
+ status: "unsupported",
4280
+ refreshing: false,
4281
+ revision: 0
4282
+ };
4283
+ }
4284
+ async *followModelDirectory(childSessionId, signal) {
4285
+ if (this.deps.catalog === void 0) {
4286
+ yield await this.modelDirectory(childSessionId);
4287
+ return;
4288
+ }
4289
+ for await (const _snapshot of this.deps.catalog.follow(childSessionId, signal)) yield await this.modelDirectory(childSessionId);
4290
+ }
4291
+ /**
4292
+ * Record the model a member's delegation STARTED with (the provider's call
4293
+ * site, fresh and resume rounds alike). The live driver's spawn resolver
4294
+ * ranks it between the override and the settings layer.
4295
+ */
4296
+ noteStartModel(childSessionId, model) {
4297
+ const value = model.trim();
4298
+ if (value !== "") this.startModels.set(childSessionId, value);
4299
+ }
4300
+ /**
4301
+ * The member's session-level override, when the composer picker set one —
4302
+ * the exec path's top layer (it outranks even the delegation's recorded
4303
+ * model, so the provider folds it into the requested slot).
4304
+ */
4305
+ overrideFor(childSessionId) {
4306
+ return this.overrides.get(childSessionId);
4307
+ }
4308
+ /**
4309
+ * The member's spawn-binding resolver (the live driver config): override,
4310
+ * then the member's start model, then the settings layer. Undefined writes
4311
+ * nothing — the scoped config's own `default_model` decides, as it always
4312
+ * did.
4313
+ */
4314
+ spawnModel = (childSessionId) => this.overrideFor(childSessionId) ?? this.startModels.get(childSessionId) ?? this.deps.settingsModel();
4315
+ /**
4316
+ * Read the model surface. With no member this is the harness default (the
4317
+ * settings card's "what would a round run with"): override and delegation
4318
+ * stay absent by construction.
4319
+ */
4320
+ async modelInfo(childSessionId, delegationModel) {
4321
+ const override = childSessionId === void 0 ? void 0 : this.overrideFor(childSessionId);
4322
+ const delegation = childSessionId === void 0 ? void 0 : delegationModel ?? this.startModels.get(childSessionId);
4323
+ const settings = this.deps.settingsModel();
4324
+ const homeDir = this.deps.homeDir(childSessionId);
4325
+ const native = this.deps.catalog?.read(childSessionId);
4326
+ const [cliDefault, discovered] = await Promise.all([readKimiDefaultModel(homeDir).catch(() => void 0), listKimiConfigModels(homeDir).catch(() => [])]);
4327
+ const choices = dedupe([
4328
+ settings,
4329
+ cliDefault,
4330
+ ...discovered,
4331
+ ...native?.entries.filter((entry) => !entry.hidden).map((entry) => entry.value) ?? [],
4332
+ ...this.deps.recentModels()
4333
+ ]);
4334
+ const [effective, source] = override !== void 0 ? [override, "override"] : delegation !== void 0 ? [delegation, "delegation"] : settings !== void 0 ? [settings, "settings"] : cliDefault !== void 0 ? [cliDefault, "cli-config"] : [void 0, "cli-builtin"];
4335
+ const inFlight = childSessionId !== void 0 && this.ctx.localAgent.activeDelegations().includes(childSessionId);
4336
+ return {
4337
+ ...effective === void 0 ? {} : { effective },
4338
+ source,
4339
+ ...override === void 0 ? {} : { override },
4340
+ ...delegation === void 0 ? {} : { delegation },
4341
+ ...settings === void 0 ? {} : { settings },
4342
+ ...cliDefault === void 0 ? {} : { cliDefault },
4343
+ choices,
4344
+ ...native === void 0 ? {} : { directory: extendModelDirectory(native, [
4345
+ settings,
4346
+ cliDefault,
4347
+ ...discovered
4348
+ ], this.deps.recentModels()) },
4349
+ live: this.deps.isLive(),
4350
+ switchable: !inFlight,
4351
+ ...inFlight ? { reason: SWITCH_IN_FLIGHT_REASON } : {}
4352
+ };
4353
+ }
4354
+ /**
4355
+ * Set (or clear) the member's session-level override. Throws while a round
4356
+ * is in flight for the member — the switch retires the resident runtime,
4357
+ * and retiring mid-round would kill the run. A same-value set (or clearing
4358
+ * an unset override) is a no-op; otherwise a live runtime whose bound model
4359
+ * differs from the new effective model is retired so the next round
4360
+ * respawns onto it, resuming the same CLI session.
4361
+ */
4362
+ async setMemberModel(childSessionId, model) {
4363
+ if (typeof this.ctx.localAgent.setMemberModel === "function") return this.ctx.localAgent.setMemberModel(childSessionId, model);
4364
+ if (this.ctx.localAgent.activeDelegations().includes(childSessionId)) throw new Error(SWITCH_IN_FLIGHT_REASON);
4365
+ const value = model?.trim();
4366
+ const previous = this.overrides.get(childSessionId);
4367
+ if (value === void 0 || value === "") {
4368
+ if (previous === void 0) return;
4369
+ this.overrides.delete(childSessionId);
4370
+ } else {
4371
+ if (previous === value) return;
4372
+ this.overrides.set(childSessionId, value);
4373
+ }
4374
+ const effective = (await this.modelInfo(childSessionId)).effective;
4375
+ if (this.deps.liveSwitch.memberHasRuntime(childSessionId) && this.deps.liveSwitch.memberRuntimeModel(childSessionId) !== effective) await this.deps.liveSwitch.retireMemberRuntime(childSessionId);
4376
+ }
4377
+ };
4378
+ //#endregion
4379
+ //#region lib/types/records.js
4380
+ /**
4381
+ * Kimi Code session records adapter: reads the scoped home's
4382
+ * `session_index.jsonl`, kimi's own append-only index.
4383
+ * @module @khorsheed/dsh-local-agent-kimi/records
4384
+ */
4385
+ /** Credentials live under this directory inside the scoped home. */
4386
+ const CREDENTIALS_DIR = "credentials";
4387
+ /**
4388
+ * Whether the scoped home holds usable login credentials, guarded by the
4389
+ * credential sentinel: a VALID credential file (both tokens non-empty) reads
4390
+ * true and refreshes the backup; an empty shell (the kimi CLI's failure-path
4391
+ * wipe) restores the backup when one exists and reads false otherwise. The
4392
+ * pre-sentinel "directory non-empty" check could not tell the empty shell
4393
+ * from a real credential at all.
4394
+ * @param homeDir - the `kimi` harness's scoped home.
4395
+ * @returns true when usable credentials are present (after any restore).
4396
+ */
4397
+ async function kimiAuthenticated(homeDir) {
4398
+ return guardKimiCredential(homeDir);
4399
+ }
4400
+ /**
4401
+ * The newest credential file's modification stamp (epoch ms), undefined when
4402
+ * no credential exists. A completed device-code login rewrites the
4403
+ * credentials directory, so the stamp distinguishes a fresh login from a
4404
+ * leftover (possibly expired) credential.
4405
+ * @param homeDir - the `kimi` harness's scoped home.
4406
+ * @returns the newest credential mtime, or undefined when none exists.
4407
+ */
4408
+ async function kimiCredentialStamp(homeDir) {
4409
+ try {
4410
+ const entries = await readdir(join(homeDir, CREDENTIALS_DIR));
4411
+ let newest;
4412
+ for (const entry of entries) {
4413
+ const { mtimeMs } = await stat(join(homeDir, CREDENTIALS_DIR, entry));
4414
+ if (newest === void 0 || mtimeMs > newest) newest = mtimeMs;
4415
+ }
4416
+ return newest;
4417
+ } catch {
4418
+ return;
4419
+ }
4420
+ }
4421
+ /**
4422
+ * Read the scoped home's session index. The index is kimi's own
4423
+ * newline-delimited JSONL; a torn tail line (the file is append-only) is
4424
+ * skipped rather than failing the whole list.
4425
+ * @param homeDir - the `kimi` harness's scoped home.
4426
+ * @returns the parsed records in file order, empty when no index exists yet.
4427
+ */
4428
+ async function listKimiSessions(homeDir) {
4429
+ const indexPath = join(homeDir, "session_index.jsonl");
4430
+ let text;
4431
+ try {
4432
+ text = await readFile(indexPath, "utf8");
4433
+ } catch (error) {
4434
+ if (error.code === "ENOENT") return [];
4435
+ throw error;
4436
+ }
4437
+ const records = [];
4438
+ for (const line of text.split("\n")) {
4439
+ const trimmed = line.trim();
4440
+ if (trimmed === "") continue;
4441
+ try {
4442
+ const parsed = JSON.parse(trimmed);
4443
+ records.push({
4444
+ id: parsed.sessionId,
4445
+ workDir: parsed.workDir,
4446
+ ...parsed.title !== void 0 && typeof parsed.title === "string" && { title: parsed.title },
4447
+ ...parsed.createdAt !== void 0 && typeof parsed.createdAt === "string" && { startedAt: Date.parse(parsed.createdAt) }
4448
+ });
4449
+ } catch {}
4450
+ }
4451
+ return records;
4452
+ }
4453
+ //#endregion
4454
+ //#region lib/types/preset-tools.js
4455
+ /**
4456
+ * Legacy kimi preset-variant cleanup. Earlier versions of this bundle
4457
+ * bootstrapped `standard-kimi`, `code-kimi`, `minimal-kimi`, and `kimi`
4458
+ * preset variants into the user preset root so an official-style preset could
4459
+ * carry the `subagent_kimi` tool row. The tool row now mounts at the profile
4460
+ * root (see the bundle patch), so every preset delegates without variants;
4461
+ * this module removes the leftovers once on startup. The repo is pre-release,
4462
+ * so no compatibility path is kept — variants that still exist are deleted.
4463
+ * @module @khorsheed/dsh-local-agent-kimi/preset-tools
4464
+ */
4465
+ /** The variant preset ids earlier bundle versions bootstrapped. */
4466
+ const LEGACY_VARIANTS = [
4467
+ "standard-kimi",
4468
+ "code-kimi",
4469
+ "minimal-kimi",
4470
+ "kimi"
4471
+ ];
4472
+ /**
4473
+ * Remove the legacy kimi preset variants from the user preset root. Uses the
4474
+ * agent-presets service's remove (which also clears a default that named one
4475
+ * of them), so a session later created under a different default no longer
4476
+ * points at a deleted composition.
4477
+ * @param ctx - host context carrying the agent-presets service.
4478
+ */
4479
+ async function removeLegacyVariants(ctx) {
4480
+ const presets = ctx.get("agentPresets");
4481
+ if (presets === void 0) return;
4482
+ for (const id of LEGACY_VARIANTS) try {
4483
+ await presets.remove(id);
4484
+ } catch (error) {
4485
+ if (error.code === "ENOENT") continue;
4486
+ throw error;
4487
+ }
4488
+ }
4489
+ //#endregion
4490
+ //#region lib/types/index.js
4491
+ /**
4492
+ * Kimi Code harness instantiation of the local-agent family. Registers the
4493
+ * `kimi` harness into the core registry: scoped home under the shared homes
4494
+ * root (`KIMI_CODE_HOME`), device-code login through `kimi login`, and the
4495
+ * `session_index.jsonl` records adapter. Delegation mounts through the
4496
+ * `kimi-cli` provider (one-shot `kimi -p` under the scoped home); the
4497
+ * bundle patch's `tool-subagent-kimi` row puts the `subagent_kimi` tool on
4498
+ * the profile root, so every agent preset can delegate without variants.
4499
+ *
4500
+ * @module @khorsheed/dsh-local-agent-kimi
4501
+ */
4502
+ /** Stable Cordis plugin name; the bundle patch row id. */
4503
+ const name = "local-agent-kimi";
4504
+ /** Services required before the harness can register. */
4505
+ const inject = [
4506
+ "localAgent",
4507
+ "subagents",
4508
+ "subprocess",
4509
+ "settings"
4510
+ ];
4511
+ /**
4512
+ * The settings fields (`live`, `liveMirrorGranularity`, `model`,
4513
+ * `recentModels`) ride the plugin Config, marked `.volatile()` where the
4514
+ * host's schemastery has it (3.18.4+, the rc.1 line — probed, never sniffed):
4515
+ * there the SettingsForms writes hot-track the running fiber's Volatile
4516
+ * references without a remount, and the official one-shot import moves an old
4517
+ * settings.yaml section into this row's config. On 0.1.5 the call is absent,
4518
+ * the fields stay plain config keys (a semantic superset — the profile row
4519
+ * can now preset them too), and the legacy settings namespace below carries
4520
+ * the user layer exactly as before.
4521
+ */
4522
+ const SETTINGS_FIELDS = {
4523
+ live: z.boolean().default(false),
4524
+ liveMirrorGranularity: z.union([z.const("event"), z.const("token")]).default("token"),
4525
+ model: z.string(),
4526
+ recentModels: z.array(z.string()).default([])
4527
+ };
4528
+ /** Mark one schema field volatile when the host's schemastery has the method (rc.1), pass it through plain when not (0.1.5). */
4529
+ function volatilize(field) {
4530
+ return typeof field.volatile === "function" ? field.volatile() : field;
4531
+ }
4532
+ const Config = z.object({
4533
+ model: volatilize(SETTINGS_FIELDS.model),
4534
+ thinkingEffort: z.union([
4535
+ z.const("low"),
4536
+ z.const("high"),
4537
+ z.const("max")
4538
+ ]),
4539
+ live: volatilize(SETTINGS_FIELDS.live),
4540
+ liveIdleMs: z.number().default(DEFAULT_LIVE_IDLE_MS),
4541
+ liveMirrorGranularity: volatilize(SETTINGS_FIELDS.liveMirrorGranularity),
4542
+ recentModels: volatilize(SETTINGS_FIELDS.recentModels)
4543
+ });
4544
+ /**
4545
+ * Settings namespace for the settings-page card. The Cordis config feeds the
4546
+ * composition `base` layer, so a field absent from the user layer inherits the
4547
+ * YAML value — the card only ever stores deliberate overrides.
4548
+ */
4549
+ const KIMI_SETTINGS_NAMESPACE = "local-agent-kimi";
4550
+ /**
4551
+ * The card's schema; field defaults are the innermost layer below `base`.
4552
+ * `model` deliberately carries NO default: an unset key must resolve to
4553
+ * undefined, which is what keeps the pre-key behavior byte-identical.
4554
+ */
4555
+ const KIMI_SETTINGS_SCHEMA = z.object(SETTINGS_FIELDS);
4556
+ /**
4557
+ * Handle the harness's extra subcommands: `session <id>` renders a kimi
4558
+ * session transcript. Returns synchronously with undefined for any input
4559
+ * that is not this harness's own.
4560
+ * @param input - the trimmed subcommand input.
4561
+ * @param invocation - the full command invocation.
4562
+ * @returns the command result promise, or undefined when not this harness's.
4563
+ */
4564
+ function handleSubcommand(input, invocation) {
4565
+ const [verb] = input.split(/\s+/);
4566
+ if (verb === "session") return handleSessionSubcommand(input, invocation);
4567
+ }
4568
+ /** Run one `session` subcommand: render the transcript of a kimi session. */
4569
+ async function handleSessionSubcommand(input, invocation) {
4570
+ const id = input.split(/\s+/)[1];
4571
+ if (id === void 0) return {
4572
+ kind: "error",
4573
+ text: "Usage: /kimi session <session-id> (see /kimi sessions for ids)"
4574
+ };
4575
+ const sessionDir = await findKimiSessionDir(invocation.agent.ctx.localAgent.homeDir("kimi"), id);
4576
+ if (sessionDir === void 0) return {
4577
+ kind: "error",
4578
+ text: `no kimi session "${id}" in the scoped home`
4579
+ };
4580
+ try {
4581
+ return {
4582
+ kind: "success",
4583
+ text: renderTranscript(await readKimiTranscript(sessionDir))
4584
+ };
4585
+ } catch (error) {
4586
+ return {
4587
+ kind: "error",
4588
+ text: error instanceof Error ? error.message : String(error)
4589
+ };
4590
+ }
4591
+ }
4592
+ /**
4593
+ * Register the Kimi harness into the local-agent registry.
4594
+ * @param ctx - plugin context carrying the registry.
4595
+ */
4596
+ function apply(ctx, config) {
4597
+ ctx.effect(() => {
4598
+ const homeDir = ctx.localAgent.homeDir("kimi");
4599
+ const thinkingEffort = config.thinkingEffort ?? "high";
4600
+ removeLegacyVariants(ctx).catch((error) => {
4601
+ ctx.logger.warn(`local-agent-kimi: legacy preset cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
4602
+ });
4603
+ provisionKimiConfig(homeDir, vol(config.model) ?? "kimi-code/k3", thinkingEffort).then(() => ensureKimiPermissions(homeDir)).catch((error) => {
4604
+ ctx.logger.warn(`local-agent-kimi: config provisioning failed: ${error instanceof Error ? error.message : String(error)}`);
4605
+ });
4606
+ const readSettings = () => {
4607
+ const model = vol(config.model);
4608
+ const recentModels = vol(config.recentModels);
4609
+ return {
4610
+ live: vol(config.live) ?? false,
4611
+ liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? "token",
4612
+ ...model === void 0 ? {} : { model },
4613
+ ...recentModels === void 0 ? {} : { recentModels }
4614
+ };
4615
+ };
4616
+ const legacyBase = {};
4617
+ for (const field of [
4618
+ "live",
4619
+ "liveMirrorGranularity",
4620
+ "model"
4621
+ ]) {
4622
+ const value = vol(config[field]);
4623
+ if (value !== void 0) legacyBase[field] = value;
4624
+ }
4625
+ const face = settingsFace(ctx, {
4626
+ namespace: KIMI_SETTINGS_NAMESPACE,
4627
+ legacySchema: KIMI_SETTINGS_SCHEMA,
4628
+ legacyBase,
4629
+ read: readSettings
4630
+ });
4631
+ const resolveModel = () => {
4632
+ const model = face.get().model?.trim();
4633
+ return model === void 0 || model === "" ? void 0 : model;
4634
+ };
4635
+ let broker;
4636
+ const liveSwitch = new LiveDriverSwitch(ctx, face, config.liveIdleMs, (child) => broker.spawnModel(child));
4637
+ const modelCatalog = new KimiModelCatalog((childSessionId) => {
4638
+ const record = childSessionId === void 0 ? void 0 : ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId);
4639
+ const native = childSessionId === void 0 ? void 0 : liveSwitch.memberRuntimeConfiguration(childSessionId);
4640
+ return {
4641
+ homeDir: ctx.localAgent.homeDir("kimi", record?.scope),
4642
+ ...record?.cwd === void 0 ? {} : { cwd: record.cwd },
4643
+ ...native === void 0 ? {} : { native }
4644
+ };
4645
+ });
4646
+ broker = new KimiModelBroker(ctx, {
4647
+ homeDir: (childSessionId) => ctx.localAgent.homeDir("kimi", childSessionId === void 0 ? void 0 : (ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId))?.scope),
4648
+ catalog: modelCatalog,
4649
+ settingsModel: resolveModel,
4650
+ recentModels: () => face.get().recentModels ?? [],
4651
+ isLive: () => face.get().live,
4652
+ liveSwitch
4653
+ });
4654
+ const disposeProvider = ctx.subagents.registerProvider(new KimiCliProvider(ctx, liveSwitch.resolve, resolveModel, broker));
4655
+ const disposeHarness = ctx.localAgent.register({
4656
+ prepareMember: async ({ binding, childSession, configuration, signal }) => {
4657
+ const driver = liveSwitch.resolve(binding.childSessionId);
4658
+ if (driver === void 0 || driver.disabled) throw new Error("Enable the native live harness before preparing a coordinator");
4659
+ await driver.prepare({
4660
+ cwd: binding.cwd,
4661
+ homeDir: ctx.localAgent.homeDir("kimi", binding.scope),
4662
+ childSession,
4663
+ parentSessionId: binding.parentSessionId,
4664
+ configuration: configuration.resolved
4665
+ }, signal);
4666
+ },
4667
+ name: "kimi",
4668
+ displayName: "Kimi Code",
4669
+ homeEnvVar: "KIMI_CODE_HOME",
4670
+ delegationProvider: "kimi-cli",
4671
+ login: {
4672
+ command: "kimi",
4673
+ args: ["login"]
4674
+ },
4675
+ records: { listSessions: (homeDir) => listKimiSessions(homeDir) },
4676
+ provision: async (scopedHome) => {
4677
+ await provisionKimiConfig(scopedHome, vol(config.model) ?? "kimi-code/k3", thinkingEffort);
4678
+ await ensureKimiPermissions(scopedHome);
4679
+ },
4680
+ isAuthenticated: kimiAuthenticated,
4681
+ credentialStamp: kimiCredentialStamp,
4682
+ logout: kimiLogout,
4683
+ subcommand: handleSubcommand,
4684
+ modelBroker: broker,
4685
+ effectiveSettings: async (scopedHome) => {
4686
+ const [reasoningEffort, baseUrl, autoApprove, defaultModel, cliVersion] = await Promise.all([
4687
+ readKimiReasoningEffort(scopedHome).catch(() => void 0),
4688
+ readKimiBaseUrl(scopedHome).catch(() => void 0),
4689
+ readKimiAutoApprove(scopedHome).catch(() => false),
4690
+ readKimiDefaultModel(scopedHome).catch(() => void 0),
4691
+ kimiCliVersion(ctx, scopedHome).catch(() => void 0)
4692
+ ]);
4693
+ const custom = baseUrl !== void 0 && baseUrl !== "https://api.kimi.com/coding/v1";
4694
+ const baseUrlHost = custom ? endpointHost(baseUrl) : void 0;
4695
+ const model = resolveModel() ?? defaultModel;
4696
+ return {
4697
+ drive: face.get().live ? "live" : "exec",
4698
+ autoApprove,
4699
+ ...reasoningEffort !== void 0 ? { reasoningEffort } : {},
4700
+ baseUrlSet: custom,
4701
+ ...baseUrlHost !== void 0 ? { baseUrlHost } : {},
4702
+ ...model !== void 0 ? { model } : {},
4703
+ ...cliVersion !== void 0 ? { cliVersion } : {}
4704
+ };
4705
+ }
4706
+ });
4707
+ return () => {
4708
+ modelCatalog.dispose();
4709
+ disposeProvider();
4710
+ disposeHarness();
4711
+ liveSwitch.dispose();
4712
+ };
4713
+ }, "local-agent-kimi: harness");
4714
+ }
4715
+ //#endregion
4716
+ export { Config, KIMI_SETTINGS_NAMESPACE, apply, inject, name };