@khorsheed/dsh-local-agent-kimi 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,12 @@
1
1
  # 变更记录
2
+ ## 0.2.2(2026-10-27)
3
+
4
+ - 成员的工具调用保留结构化参数,透镜和记录里能看到完整入参;用量记录不再重复计算。
5
+ - **重启恢复后不再往已经结束的轮次里补写**:之前恢复时可能把旧内容写进中间的轮次,导致整份子会话日志读不出来。
6
+ - **一轮中途插进来的消息不再另起一轮**;这一轮的最终回答始终是它的最后一步。
7
+ - **工具调用不再 60 秒就超时**:常驻进程和成员桥接都放宽到和提问卡一样的 30 分钟。
8
+ - 读图片等媒体返回的内容不再打断对话记录的整理。
9
+
2
10
  ## 0.2.1(2026-10-27)
3
11
 
4
12
  - **修复几个模式套件一起装时设置存不上**:卡片上的开关和设置改存在本 harness 自己的目录里,不再写进配置文件;原来配好的值照常生效。
package/lib/client.js CHANGED
@@ -343,30 +343,30 @@ window.__ModuleLoader__.load({
343
343
  document.head.appendChild(tag);
344
344
  }
345
345
  var ProviderAuthBlock_module_css_default = {
346
- "rowActions": "On5p7W_rowActions",
346
+ "retryButton": "On5p7W_retryButton",
347
347
  "loginPrompt": "On5p7W_loginPrompt",
348
- "rowCard": "On5p7W_rowCard",
349
- "openPage": "On5p7W_openPage",
350
- "loginButton": "On5p7W_loginButton",
351
- "section": "On5p7W_section",
352
- "promptText": "On5p7W_promptText",
348
+ "title": "On5p7W_title",
349
+ "rowActions": "On5p7W_rowActions",
353
350
  "codeInput": "On5p7W_codeInput",
354
- "retryButton": "On5p7W_retryButton",
355
- "dotOn": "On5p7W_dotOn",
356
- "dotPending": "On5p7W_dotPending",
357
351
  "dotOff": "On5p7W_dotOff",
358
- "rows": "On5p7W_rows",
359
- "rowHead": "On5p7W_rowHead",
360
352
  "status": "On5p7W_status",
353
+ "dotPending": "On5p7W_dotPending",
354
+ "rowName": "On5p7W_rowName",
355
+ "loginButton": "On5p7W_loginButton",
356
+ "rowIdentity": "On5p7W_rowIdentity",
361
357
  "intro": "On5p7W_intro",
362
- "rowTag": "On5p7W_rowTag",
363
- "title": "On5p7W_title",
364
- "logoutButton": "On5p7W_logoutButton",
365
- "credentialDot": "On5p7W_credentialDot",
358
+ "dotOn": "On5p7W_dotOn",
366
359
  "rosterError": "On5p7W_rosterError",
367
360
  "rowPending": "On5p7W_rowPending",
368
- "rowIdentity": "On5p7W_rowIdentity",
369
- "rowName": "On5p7W_rowName"
361
+ "credentialDot": "On5p7W_credentialDot",
362
+ "promptText": "On5p7W_promptText",
363
+ "openPage": "On5p7W_openPage",
364
+ "rowTag": "On5p7W_rowTag",
365
+ "rows": "On5p7W_rows",
366
+ "rowHead": "On5p7W_rowHead",
367
+ "rowCard": "On5p7W_rowCard",
368
+ "section": "On5p7W_section",
369
+ "logoutButton": "On5p7W_logoutButton"
370
370
  };
371
371
  //#endregion
372
372
  //#region ../local-agent/src/client/ProviderAuthBlock.tsx
@@ -645,10 +645,10 @@ window.__ModuleLoader__.load({
645
645
  document.head.appendChild(tag);
646
646
  }
647
647
  var AuthStatusDot_module_css_default = {
648
- "dotPending": "OFjNUG_dotPending",
649
648
  "dotOff": "OFjNUG_dotOff",
650
649
  "credentialDot": "OFjNUG_credentialDot",
651
- "dotOn": "OFjNUG_dotOn"
650
+ "dotOn": "OFjNUG_dotOn",
651
+ "dotPending": "OFjNUG_dotPending"
652
652
  };
653
653
  //#endregion
654
654
  //#region ../local-agent/src/client/AuthStatusDot.tsx
@@ -674,39 +674,39 @@ window.__ModuleLoader__.load({
674
674
  document.head.appendChild(tag);
675
675
  }
676
676
  var ModelListSection_module_css_default = {
677
- "saved": "_4eeJ7W_saved",
678
- "row": "_4eeJ7W_row",
679
- "modelRowDragging": "_4eeJ7W_modelRowDragging",
680
- "dialogItem": "_4eeJ7W_dialogItem",
681
- "errorText": "_4eeJ7W_errorText",
682
- "modelInput": "_4eeJ7W_modelInput",
683
- "dialogHead": "_4eeJ7W_dialogHead",
684
- "dialogTitle": "_4eeJ7W_dialogTitle",
685
677
  "modelRowValue": "_4eeJ7W_modelRowValue",
686
- "dialogClose": "_4eeJ7W_dialogClose",
687
- "dialogItemAdded": "_4eeJ7W_dialogItemAdded",
688
- "modelSave": "_4eeJ7W_modelSave",
689
- "modelDelete": "_4eeJ7W_modelDelete",
690
678
  "dialog": "_4eeJ7W_dialog",
691
- "blockTitle": "_4eeJ7W_blockTitle",
692
- "dialogConfirm": "_4eeJ7W_dialogConfirm",
693
- "modelRow": "_4eeJ7W_modelRow",
694
- "block": "_4eeJ7W_block",
695
- "modelDrag": "_4eeJ7W_modelDrag",
679
+ "dialogTitle": "_4eeJ7W_dialogTitle",
680
+ "dialogBackdrop": "_4eeJ7W_dialogBackdrop",
696
681
  "dialogHint": "_4eeJ7W_dialogHint",
697
- "dialogToolbar": "_4eeJ7W_dialogToolbar",
698
- "dialogSelectAll": "_4eeJ7W_dialogSelectAll",
682
+ "block": "_4eeJ7W_block",
683
+ "modelSave": "_4eeJ7W_modelSave",
699
684
  "dialogActions": "_4eeJ7W_dialogActions",
700
- "hint": "_4eeJ7W_hint",
701
- "modelAdd": "_4eeJ7W_modelAdd",
685
+ "dialogSelectAll": "_4eeJ7W_dialogSelectAll",
686
+ "modelRowLabel": "_4eeJ7W_modelRowLabel",
687
+ "modelInput": "_4eeJ7W_modelInput",
688
+ "modelRowDragging": "_4eeJ7W_modelRowDragging",
702
689
  "dialogItemValue": "_4eeJ7W_dialogItemValue",
703
- "dialogBackdrop": "_4eeJ7W_dialogBackdrop",
704
- "dialogList": "_4eeJ7W_dialogList",
705
690
  "dialogItemLabel": "_4eeJ7W_dialogItemLabel",
706
- "info": "_4eeJ7W_info",
691
+ "modelDrag": "_4eeJ7W_modelDrag",
692
+ "dialogItemAdded": "_4eeJ7W_dialogItemAdded",
693
+ "modelDefaultChip": "_4eeJ7W_modelDefaultChip",
694
+ "errorText": "_4eeJ7W_errorText",
695
+ "dialogConfirm": "_4eeJ7W_dialogConfirm",
696
+ "dialogItem": "_4eeJ7W_dialogItem",
697
+ "dialogList": "_4eeJ7W_dialogList",
698
+ "dialogHead": "_4eeJ7W_dialogHead",
699
+ "modelAdd": "_4eeJ7W_modelAdd",
700
+ "modelRow": "_4eeJ7W_modelRow",
707
701
  "modelList": "_4eeJ7W_modelList",
708
- "modelRowLabel": "_4eeJ7W_modelRowLabel",
709
- "modelDefaultChip": "_4eeJ7W_modelDefaultChip"
702
+ "blockTitle": "_4eeJ7W_blockTitle",
703
+ "modelDelete": "_4eeJ7W_modelDelete",
704
+ "hint": "_4eeJ7W_hint",
705
+ "saved": "_4eeJ7W_saved",
706
+ "info": "_4eeJ7W_info",
707
+ "dialogClose": "_4eeJ7W_dialogClose",
708
+ "row": "_4eeJ7W_row",
709
+ "dialogToolbar": "_4eeJ7W_dialogToolbar"
710
710
  };
711
711
  //#endregion
712
712
  //#region ../local-agent/src/client/ModelListSection.tsx
@@ -1078,33 +1078,33 @@ window.__ModuleLoader__.load({
1078
1078
  document.head.appendChild(tag);
1079
1079
  }
1080
1080
  var SettingsCard_module_css_default = {
1081
- "info": "apHkUa_info",
1081
+ "chevronOpen": "apHkUa_chevronOpen",
1082
+ "saved": "apHkUa_saved",
1082
1083
  "card": "apHkUa_card",
1083
- "header": "apHkUa_header",
1084
+ "rowLabel": "apHkUa_rowLabel",
1085
+ "name": "apHkUa_name",
1084
1086
  "headText": "apHkUa_headText",
1085
- "chevron": "apHkUa_chevron",
1086
- "blockTitle": "apHkUa_blockTitle",
1087
- "description": "apHkUa_description",
1088
- "endpointField": "apHkUa_endpointField",
1087
+ "endpointLabel": "apHkUa_endpointLabel",
1088
+ "info": "apHkUa_info",
1089
+ "row": "apHkUa_row",
1089
1090
  "switchOn": "apHkUa_switchOn",
1090
- "errorText": "apHkUa_errorText",
1091
- "configuredChip": "apHkUa_configuredChip",
1091
+ "cardOpen": "apHkUa_cardOpen",
1092
+ "switch": "apHkUa_switch",
1093
+ "knob": "apHkUa_knob",
1092
1094
  "hint": "apHkUa_hint",
1095
+ "chevron": "apHkUa_chevron",
1093
1096
  "nameRow": "apHkUa_nameRow",
1094
- "row": "apHkUa_row",
1097
+ "header": "apHkUa_header",
1095
1098
  "body": "apHkUa_body",
1096
- "endpointFields": "apHkUa_endpointFields",
1097
- "endpointLabel": "apHkUa_endpointLabel",
1098
- "chevronOpen": "apHkUa_chevronOpen",
1099
- "saved": "apHkUa_saved",
1100
- "knob": "apHkUa_knob",
1101
- "modelSave": "apHkUa_modelSave",
1099
+ "description": "apHkUa_description",
1100
+ "endpointField": "apHkUa_endpointField",
1101
+ "configuredChip": "apHkUa_configuredChip",
1102
+ "blockTitle": "apHkUa_blockTitle",
1102
1103
  "modelInput": "apHkUa_modelInput",
1104
+ "errorText": "apHkUa_errorText",
1105
+ "endpointFields": "apHkUa_endpointFields",
1103
1106
  "block": "apHkUa_block",
1104
- "cardOpen": "apHkUa_cardOpen",
1105
- "name": "apHkUa_name",
1106
- "rowLabel": "apHkUa_rowLabel",
1107
- "switch": "apHkUa_switch"
1107
+ "modelSave": "apHkUa_modelSave"
1108
1108
  };
1109
1109
  //#endregion
1110
1110
  //#region src/client/SettingsCard.tsx
package/lib/index.js CHANGED
@@ -1960,6 +1960,93 @@ async function ensureKimiPermissions(homeDir) {
1960
1960
  await writeFile(file, `${content.trimEnd()}\n${KIMI_PERMISSION_BLOCK}`);
1961
1961
  }
1962
1962
  //#endregion
1963
+ //#region lib/types/member-bridge-config.js
1964
+ /**
1965
+ * Kimi scoped-home MCP config injection for the member bridge. Kimi declares
1966
+ * stdio MCP servers in `$KIMI_CODE_HOME/mcp.json` (`mcpServers` map); the file
1967
+ * is read once per CLI session start, and edits never interrupt an open
1968
+ * session — so the family writes one entry PER RUN (keyed by the run's token
1969
+ * suffix, keeping concurrent delegations of the same scoped home race-free),
1970
+ * prunes it when the run settles, and drops stale family entries (crashed
1971
+ * host) on every write. User-declared servers are preserved verbatim.
1972
+ * @module @khorsheed/dsh-local-agent-kimi/member-bridge-config
1973
+ */
1974
+ /** Kimi's user-level MCP config filename inside the scoped home. */
1975
+ const MCP_CONFIG_FILENAME = "mcp.json";
1976
+ /** The member bridge's per-call bound: as long as a question card waits for the human (30 min). */
1977
+ const MEMBER_TOOL_TIMEOUT_MS = 18e5;
1978
+ /** Server-key prefix marking the family's per-run bridge entries. */
1979
+ const MEMBER_BRIDGE_SERVER_PREFIX = "dsh-member-";
1980
+ /**
1981
+ * The per-run MCP server key. The run's token suffix keeps concurrent runs
1982
+ * distinct; each `kimi -p` round is a fresh CLI session, so per-run (rather
1983
+ * than per-member) naming costs nothing.
1984
+ * @param token - the run's member-channel token.
1985
+ * @returns the mcp.json server key.
1986
+ */
1987
+ function memberBridgeServerKey(token) {
1988
+ return `${MEMBER_BRIDGE_SERVER_PREFIX}${token.slice(0, 8)}`;
1989
+ }
1990
+ /** Read the scoped mcp.json; a missing or malformed file reads as empty. */
1991
+ function readMcpConfig(file) {
1992
+ let parsed;
1993
+ try {
1994
+ parsed = JSON.parse(readFileSync(file, "utf8"));
1995
+ } catch {
1996
+ return {};
1997
+ }
1998
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
1999
+ return parsed;
2000
+ }
2001
+ /** Write the config back, preserving every non-family key. */
2002
+ function writeMcpConfig(file, config) {
2003
+ writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
2004
+ }
2005
+ /**
2006
+ * Declare the member bridge for one run: set the per-run server entry and
2007
+ * prune other family entries (settled or crashed runs — an open session's
2008
+ * already-registered server is unaffected, per kimi's mid-session semantics).
2009
+ * Synchronous read-modify-write: single-process callers race-free by event
2010
+ * loop; the file is small and written once per run.
2011
+ * @param homeDir - the kimi scoped home.
2012
+ * @param key - this run's server key ({@link memberBridgeServerKey}).
2013
+ * @param command - the bridge spawn command (`node` + entry path).
2014
+ * @param env - the bridge environment (socket path + per-run token).
2015
+ */
2016
+ function injectMemberBridge(homeDir, key, command, env) {
2017
+ const file = join(homeDir, MCP_CONFIG_FILENAME);
2018
+ const config = readMcpConfig(file);
2019
+ const servers = { ...config.mcpServers ?? {} };
2020
+ for (const name of Object.keys(servers)) if (name.startsWith("dsh-member-") && name !== key) delete servers[name];
2021
+ servers[key] = {
2022
+ command: command.command,
2023
+ args: command.args,
2024
+ env,
2025
+ toolTimeoutMs: MEMBER_TOOL_TIMEOUT_MS
2026
+ };
2027
+ writeMcpConfig(file, {
2028
+ ...config,
2029
+ mcpServers: servers
2030
+ });
2031
+ }
2032
+ /**
2033
+ * Remove one run's bridge entry (settle path). Absent entries and a missing
2034
+ * config are no-ops.
2035
+ * @param homeDir - the kimi scoped home.
2036
+ * @param key - the run's server key.
2037
+ */
2038
+ function removeMemberBridge(homeDir, key) {
2039
+ const file = join(homeDir, MCP_CONFIG_FILENAME);
2040
+ const config = readMcpConfig(file);
2041
+ if (config.mcpServers === void 0 || !(key in config.mcpServers)) return;
2042
+ const servers = { ...config.mcpServers };
2043
+ delete servers[key];
2044
+ writeMcpConfig(file, {
2045
+ ...config,
2046
+ mcpServers: servers
2047
+ });
2048
+ }
2049
+ //#endregion
1963
2050
  //#region lib/types/session-view.js
1964
2051
  /**
1965
2052
  * Kimi session viewer: read a scoped-home session's `wire.jsonl` event log
@@ -2047,6 +2134,8 @@ function editArgsOf(name, record) {
2047
2134
  const patch = record["patch"] ?? record["diff"];
2048
2135
  if (typeof patch === "string" && patch.trim() !== "") return patch;
2049
2136
  }
2137
+ /** Cap on a structured tool call's mirrored JSON arguments. */
2138
+ const STRUCTURED_ARGS_MAX = 16e3;
2050
2139
  /** Render a tool call's arguments into a compact summary. */
2051
2140
  function argsOf(name, args) {
2052
2141
  if (args === void 0) return void 0;
@@ -2055,6 +2144,10 @@ function argsOf(name, args) {
2055
2144
  const record = args;
2056
2145
  const edit = editArgsOf(name, record);
2057
2146
  if (edit !== void 0) return edit;
2147
+ if (Object.values(record).some((value) => typeof value === "object" && value !== null)) {
2148
+ const json = JSON.stringify(record);
2149
+ return json.length <= STRUCTURED_ARGS_MAX ? json : `${json.slice(0, STRUCTURED_ARGS_MAX)}…`;
2150
+ }
2058
2151
  for (const key of [
2059
2152
  "query",
2060
2153
  "url",
@@ -2094,6 +2187,8 @@ async function readKimiTranscript(sessionDir) {
2094
2187
  };
2095
2188
  let turn = 0;
2096
2189
  let turnSeen = false;
2190
+ /** The wire marks rounds with turn.prompt (current kimi): a user message it did not prompt is an injection. */
2191
+ let promptsMarkTurns = false;
2097
2192
  const callsByUuid = /* @__PURE__ */ new Map();
2098
2193
  const callsByToolCallId = /* @__PURE__ */ new Map();
2099
2194
  let text;
@@ -2147,6 +2242,7 @@ async function readKimiTranscript(sessionDir) {
2147
2242
  continue;
2148
2243
  }
2149
2244
  if (event.type === "turn.prompt") {
2245
+ promptsMarkTurns = true;
2150
2246
  lineModel = void 0;
2151
2247
  turnSeen = true;
2152
2248
  turn += 1;
@@ -2165,6 +2261,7 @@ async function readKimiTranscript(sessionDir) {
2165
2261
  if (role === "user" && textContent.trim() !== "" && !isSystemReminder(textContent)) {
2166
2262
  const last = lines[lines.length - 1];
2167
2263
  if (turnSeen && last?.kind === "user" && last.text === textContent) continue;
2264
+ if (promptsMarkTurns) continue;
2168
2265
  turnSeen = true;
2169
2266
  turn += 1;
2170
2267
  lineModel = void 0;
@@ -2221,7 +2318,7 @@ async function readKimiTranscript(sessionDir) {
2221
2318
  if (typeof call.toolCallId === "string") callsByToolCallId.set(call.toolCallId, lineIndex);
2222
2319
  } else if (loop.type === "tool.result") {
2223
2320
  const result = loop;
2224
- const output = result.result?.output;
2321
+ const output = toolOutputText(result.result?.output);
2225
2322
  let target = typeof result.parentUuid === "string" ? callsByUuid.get(result.parentUuid) : void 0;
2226
2323
  if (target === void 0 && typeof result.toolCallId === "string") target = callsByToolCallId.get(result.toolCallId);
2227
2324
  if (target === void 0) {
@@ -2254,6 +2351,26 @@ async function readKimiTranscript(sessionDir) {
2254
2351
  };
2255
2352
  }
2256
2353
  /**
2354
+ * A wire tool result's output as display text. Most tools report a string;
2355
+ * a media read (ReadMediaFile) reports content parts — text parts keep their
2356
+ * text, an image part becomes a short marker (its bytes stay in kimi's blob
2357
+ * store). Anything else reads as no output, never a parse failure: one
2358
+ * unreadable result must not stop the whole transcript (and the round's
2359
+ * usage) from folding.
2360
+ * @param raw - the result's `output` field.
2361
+ * @returns the text, or undefined when there is none.
2362
+ */
2363
+ function toolOutputText(raw) {
2364
+ if (typeof raw === "string") return raw;
2365
+ if (!Array.isArray(raw)) return void 0;
2366
+ return raw.map((part) => {
2367
+ if (part === null || typeof part !== "object") return "";
2368
+ const entry = part;
2369
+ if (entry.type === "text" && typeof entry.text === "string") return entry.text;
2370
+ return typeof entry.type === "string" && entry.type !== "" ? `[${entry.type}]` : "";
2371
+ }).join("");
2372
+ }
2373
+ /**
2257
2374
  * Map a wire `usage.record` payload onto the shared usage contract. kimi
2258
2375
  * reports input (uncached), cache read, cache creation, and output; the
2259
2376
  * creation counter is its cache-write analog. Non-numeric or missing
@@ -2419,6 +2536,13 @@ function nextWrittenKimiStep(childSession, turn) {
2419
2536
  }
2420
2537
  return last + 1;
2421
2538
  }
2539
+ /** Wire usage records already counted, per child session (see mirrorKimiSessionDelta). */
2540
+ const countedUsage = /* @__PURE__ */ new WeakMap();
2541
+ const usageSeen = (childSession) => {
2542
+ let seen = countedUsage.get(childSession);
2543
+ if (seen === void 0) countedUsage.set(childSession, seen = /* @__PURE__ */ new Set());
2544
+ return seen;
2545
+ };
2422
2546
  /** The run-progress delta text for one transcript line (any kind). */
2423
2547
  function kimiLineProgressText(line) {
2424
2548
  if (line.kind !== "tool") return line.text;
@@ -2552,6 +2676,8 @@ async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId,
2552
2676
  }
2553
2677
  const foldedUnits = holdAt < 0 ? units : units.slice(0, holdAt);
2554
2678
  const consumedTotal = fromLines + foldedUnits.reduce((n, unit) => n + unit.length, 0);
2679
+ const seen = usageSeen(childSession);
2680
+ const usageRecords = transcript.usageRecords.filter((record) => !seen.has(record.line));
2555
2681
  const unitUsage = [];
2556
2682
  const claimed = [];
2557
2683
  let unitStart = fromLines;
@@ -2564,11 +2690,13 @@ async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId,
2564
2690
  from: start,
2565
2691
  to: unitStart
2566
2692
  });
2567
- unitUsage.push(sumUsageRecords(transcript.usageRecords.filter((record) => record.line >= start && record.line < unitStart)));
2693
+ unitUsage.push(sumUsageRecords(usageRecords.filter((record) => record.line >= start && record.line < unitStart)));
2568
2694
  } else unitUsage.push(void 0);
2569
2695
  }
2570
- const unclaimedUsage = sumUsageRecords(transcript.usageRecords.filter((record) => record.line >= fromLines && record.line <= consumedTotal && !claimed.some((span) => record.line >= span.from && record.line < span.to)));
2571
- const deltaUsage = sumUsageRecords(transcript.usageRecords.filter((record) => record.line >= fromLines && record.line <= consumedTotal));
2696
+ const unclaimedUsage = sumUsageRecords(usageRecords.filter((record) => record.line >= fromLines && record.line <= consumedTotal && !claimed.some((span) => record.line >= span.from && record.line < span.to)));
2697
+ const windowRecords = usageRecords.filter((record) => record.line >= fromLines && record.line <= consumedTotal);
2698
+ const deltaUsage = sumUsageRecords(windowRecords);
2699
+ for (const record of windowRecords) seen.add(record.line);
2572
2700
  const takenSteps = /* @__PURE__ */ new Map();
2573
2701
  for (const event of childSession.snapshotEvents()) {
2574
2702
  const data = event.data;
@@ -2597,6 +2725,12 @@ async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId,
2597
2725
  written.add(step);
2598
2726
  takenSteps.set(turn, written);
2599
2727
  };
2728
+ const closedTurns = /* @__PURE__ */ new Set();
2729
+ for (const event of childSession.snapshotEvents()) {
2730
+ if (event.type !== "turn/end") continue;
2731
+ const closed = event.data.turn;
2732
+ if (typeof closed === "number") closedTurns.add(closed);
2733
+ }
2600
2734
  /** The window's accounting that no fold carried (dropped units' sums). */
2601
2735
  let carryUsage;
2602
2736
  let lastContentUnit = -1;
@@ -2612,6 +2746,7 @@ async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId,
2612
2746
  carryUsage = void 0;
2613
2747
  if (unitIndex === lastContentUnit) usage = addTokenUsage(usage, unclaimedUsage);
2614
2748
  const turn = head.turn;
2749
+ if (closedTurns.has(turn)) continue;
2615
2750
  if (head.kind === "user") {
2616
2751
  if (userAlreadyAppended(childSession, turn, head.text)) continue;
2617
2752
  childSession.append("user/message", userEvent(head.text), { surfaceOp: "append" });
@@ -2808,7 +2943,6 @@ function userAlreadyAppended(childSession, turn, text) {
2808
2943
  * mode's auto-approve behavior; the proposal's default policy).
2809
2944
  * @module @khorsheed/dsh-local-agent-kimi/live-driver
2810
2945
  */
2811
- /** Default idle lifetime of an unused resident runtime before reclaim. */
2812
2946
  const DEFAULT_LIVE_IDLE_MS = 18e5;
2813
2947
  /** Default timeout for a wire request acknowledgement (never for session/prompt). */
2814
2948
  const DEFAULT_LIVE_REQUEST_TIMEOUT_MS = 3e4;
@@ -3286,6 +3420,7 @@ var KimiAcpLiveDriver = class {
3286
3420
  graceMs: DEFAULT_DISPOSE_GRACE_MS,
3287
3421
  env: delegationEnv({
3288
3422
  KIMI_CODE_HOME: spec.homeDir,
3423
+ KIMI_MCP_TOOL_TIMEOUT_MS: String(MEMBER_TOOL_TIMEOUT_MS),
3289
3424
  ...endpointAuthEnv(endpoint)
3290
3425
  })
3291
3426
  };
@@ -3564,22 +3699,30 @@ var KimiAcpLiveDriver = class {
3564
3699
  * think-style lines. Each update carries the WHOLE plan, so the fold
3565
3700
  * replaces rather than appends.
3566
3701
  */
3567
- const renderPlan = (update) => {
3568
- return (Array.isArray(update["entries"]) ? update["entries"] : []).map((entry) => {
3569
- const content = typeof entry["content"] === "string" ? entry["content"] : "";
3570
- const status = entry["status"];
3571
- return `${status === "completed" ? "☑" : status === "in_progress" ? "▶" : "☐"} ${content}`;
3572
- }).filter((line) => line.trim() !== "☐" && line !== "").join("\n");
3573
- };
3702
+ /**
3703
+ * An ACP plan update is the member's todo list: written as the host's
3704
+ * `todo/write` whole-list snapshot (log-only UI state, Claude's TodoWrite
3705
+ * shape), only when it changed. Never a conversation step — a plan
3706
+ * folded as a step landed after the round's answer (it finalized at
3707
+ * settle) and the chat folded the answer into the process.
3708
+ */
3574
3709
  const foldPlan = (update) => {
3575
- const plan = renderPlan(update);
3576
- if (plan === "") return;
3577
- reserveStream(planKey, "think");
3578
- const stream = streams.get(planKey);
3579
- if (stream !== void 0 && stream.text !== plan) {
3580
- stream.text = plan;
3581
- publishStreamSnapshot(stream, true);
3710
+ const entries = Array.isArray(update["entries"]) ? update["entries"] : [];
3711
+ const todos = [];
3712
+ for (const entry of entries) {
3713
+ const content = typeof entry["content"] === "string" ? entry["content"].trim() : "";
3714
+ if (content === "") continue;
3715
+ const status = entry["status"];
3716
+ todos.push({
3717
+ content,
3718
+ status: status === "in_progress" || status === "completed" ? status : "pending"
3719
+ });
3582
3720
  }
3721
+ if (todos.length === 0) return;
3722
+ const last = childSession.snapshotEvents().filter((event) => event.type === "todo/write").at(-1);
3723
+ if (last !== void 0 && JSON.stringify(last.data) === JSON.stringify({ todos })) return;
3724
+ childSession.append("todo/write", { todos });
3725
+ persist();
3583
3726
  };
3584
3727
  /**
3585
3728
  * The live-output channel. Snapshots are browser-only previews (never
@@ -3919,7 +4062,7 @@ var KimiAcpLiveDriver = class {
3919
4062
  }
3920
4063
  }
3921
4064
  } else if (kind === "tool_call") {
3922
- const id = typeof update["toolCallId"] === "string" ? update["toolCallId"] : void 0;
4065
+ const id = typeof update["toolCallId"] === "string" ? update["toolCallId"].replace(/^\d+:/, "") : void 0;
3923
4066
  if (id === void 0 || !seenToolCalls.has(id)) {
3924
4067
  if (id !== void 0 && toolLogged(id)) seenToolCalls.add(id);
3925
4068
  else if (id !== void 0) {
@@ -4122,90 +4265,6 @@ var KimiAcpLiveDriver = class {
4122
4265
  /** The member handle each runtime carries (for its session declarations). */
4123
4266
  const runtimeMember = /* @__PURE__ */ new WeakMap();
4124
4267
  //#endregion
4125
- //#region lib/types/member-bridge-config.js
4126
- /**
4127
- * Kimi scoped-home MCP config injection for the member bridge. Kimi declares
4128
- * stdio MCP servers in `$KIMI_CODE_HOME/mcp.json` (`mcpServers` map); the file
4129
- * is read once per CLI session start, and edits never interrupt an open
4130
- * session — so the family writes one entry PER RUN (keyed by the run's token
4131
- * suffix, keeping concurrent delegations of the same scoped home race-free),
4132
- * prunes it when the run settles, and drops stale family entries (crashed
4133
- * host) on every write. User-declared servers are preserved verbatim.
4134
- * @module @khorsheed/dsh-local-agent-kimi/member-bridge-config
4135
- */
4136
- /** Kimi's user-level MCP config filename inside the scoped home. */
4137
- const MCP_CONFIG_FILENAME = "mcp.json";
4138
- /** Server-key prefix marking the family's per-run bridge entries. */
4139
- const MEMBER_BRIDGE_SERVER_PREFIX = "dsh-member-";
4140
- /**
4141
- * The per-run MCP server key. The run's token suffix keeps concurrent runs
4142
- * distinct; each `kimi -p` round is a fresh CLI session, so per-run (rather
4143
- * than per-member) naming costs nothing.
4144
- * @param token - the run's member-channel token.
4145
- * @returns the mcp.json server key.
4146
- */
4147
- function memberBridgeServerKey(token) {
4148
- return `${MEMBER_BRIDGE_SERVER_PREFIX}${token.slice(0, 8)}`;
4149
- }
4150
- /** Read the scoped mcp.json; a missing or malformed file reads as empty. */
4151
- function readMcpConfig(file) {
4152
- let parsed;
4153
- try {
4154
- parsed = JSON.parse(readFileSync(file, "utf8"));
4155
- } catch {
4156
- return {};
4157
- }
4158
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
4159
- return parsed;
4160
- }
4161
- /** Write the config back, preserving every non-family key. */
4162
- function writeMcpConfig(file, config) {
4163
- writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
4164
- }
4165
- /**
4166
- * Declare the member bridge for one run: set the per-run server entry and
4167
- * prune other family entries (settled or crashed runs — an open session's
4168
- * already-registered server is unaffected, per kimi's mid-session semantics).
4169
- * Synchronous read-modify-write: single-process callers race-free by event
4170
- * loop; the file is small and written once per run.
4171
- * @param homeDir - the kimi scoped home.
4172
- * @param key - this run's server key ({@link memberBridgeServerKey}).
4173
- * @param command - the bridge spawn command (`node` + entry path).
4174
- * @param env - the bridge environment (socket path + per-run token).
4175
- */
4176
- function injectMemberBridge(homeDir, key, command, env) {
4177
- const file = join(homeDir, MCP_CONFIG_FILENAME);
4178
- const config = readMcpConfig(file);
4179
- const servers = { ...config.mcpServers ?? {} };
4180
- for (const name of Object.keys(servers)) if (name.startsWith("dsh-member-") && name !== key) delete servers[name];
4181
- servers[key] = {
4182
- command: command.command,
4183
- args: command.args,
4184
- env
4185
- };
4186
- writeMcpConfig(file, {
4187
- ...config,
4188
- mcpServers: servers
4189
- });
4190
- }
4191
- /**
4192
- * Remove one run's bridge entry (settle path). Absent entries and a missing
4193
- * config are no-ops.
4194
- * @param homeDir - the kimi scoped home.
4195
- * @param key - the run's server key.
4196
- */
4197
- function removeMemberBridge(homeDir, key) {
4198
- const file = join(homeDir, MCP_CONFIG_FILENAME);
4199
- const config = readMcpConfig(file);
4200
- if (config.mcpServers === void 0 || !(key in config.mcpServers)) return;
4201
- const servers = { ...config.mcpServers };
4202
- delete servers[key];
4203
- writeMcpConfig(file, {
4204
- ...config,
4205
- mcpServers: servers
4206
- });
4207
- }
4208
- //#endregion
4209
4268
  //#region lib/types/kimi-cli-provider.js
4210
4269
  /**
4211
4270
  * One-shot and resumable Kimi CLI subagent lifecycle: spawn `kimi -p "<task>"`
@@ -42,7 +42,6 @@ import type { Context } from '@deepseek-ai/cordis';
42
42
  import type { Session } from '@deepseek-ai/dsh-session';
43
43
  import { type SubagentRun, type SubagentStartRequest, type SubagentStopReason } from '@deepseek-ai/dsh-subagent';
44
44
  import { type KimiEndpointAuth } from './endpoint-auth.ts';
45
- /** Default idle lifetime of an unused resident runtime before reclaim. */
46
45
  export declare const DEFAULT_LIVE_IDLE_MS: number;
47
46
  /** Default timeout for a wire request acknowledgement (never for session/prompt). */
48
47
  export declare const DEFAULT_LIVE_REQUEST_TIMEOUT_MS = 30000;
@@ -47,11 +47,11 @@ import { delegationEnv, persistChildSession, LiveStreamPublisher, LiveFlush, LIV
47
47
  import { endpointAuthEnv, hasEndpointCredential } from './endpoint-auth.js';
48
48
  import { ensureKimiEndpointProvider, KIMI_MANAGED_BASE_URL } from './provision.js';
49
49
  import { MEMBER_BRIDGE_SOCKET_ENV, MEMBER_BRIDGE_TOKEN_ENV } from '@khorsheed/dsh-local-agent/types';
50
+ import { MEMBER_TOOL_TIMEOUT_MS } from './member-bridge-config.js';
50
51
  import { DEFAULT_DISPOSE_GRACE_MS, mirrorKimiDelta, textTask, } from './kimi-cli-provider.js';
51
52
  import { assistantEvent, KIMI_INTERRUPTED_TOOL_RESULT, nextKimiSessionStep, nextWrittenKimiStep } from './session-mirror.js';
52
53
  import { addTokenUsage } from './session-view.js';
53
54
  import { writeKimiDefaultModel } from './provision.js';
54
- /** Default idle lifetime of an unused resident runtime before reclaim. */
55
55
  export const DEFAULT_LIVE_IDLE_MS = 30 * 60_000;
56
56
  /** Default timeout for a wire request acknowledgement (never for session/prompt). */
57
57
  export const DEFAULT_LIVE_REQUEST_TIMEOUT_MS = 30_000;
@@ -559,7 +559,10 @@ export class KimiAcpLiveDriver {
559
559
  cwd: spec.cwd,
560
560
  stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' },
561
561
  graceMs: DEFAULT_DISPOSE_GRACE_MS,
562
- env: delegationEnv({ KIMI_CODE_HOME: spec.homeDir, ...endpointAuthEnv(endpoint) }),
562
+ // The ACP session declares the member bridge inline, and ACP's MCP
563
+ // server shape carries no timeout: lift kimi's 60s MCP default for the
564
+ // whole process, or a question card dies before the human answers.
565
+ env: delegationEnv({ KIMI_CODE_HOME: spec.homeDir, KIMI_MCP_TOOL_TIMEOUT_MS: String(MEMBER_TOOL_TIMEOUT_MS), ...endpointAuthEnv(endpoint) }),
563
566
  };
564
567
  let child;
565
568
  try {
@@ -877,30 +880,30 @@ export class KimiAcpLiveDriver {
877
880
  * think-style lines. Each update carries the WHOLE plan, so the fold
878
881
  * replaces rather than appends.
879
882
  */
880
- const renderPlan = (update) => {
883
+ /**
884
+ * An ACP plan update is the member's todo list: written as the host's
885
+ * `todo/write` whole-list snapshot (log-only UI state, Claude's TodoWrite
886
+ * shape), only when it changed. Never a conversation step — a plan
887
+ * folded as a step landed after the round's answer (it finalized at
888
+ * settle) and the chat folded the answer into the process.
889
+ */
890
+ const foldPlan = (update) => {
881
891
  const entries = Array.isArray(update['entries']) ? update['entries'] : [];
882
- const lines = entries.map((entry) => {
883
- const content = typeof entry['content'] === 'string' ? entry['content'] : '';
892
+ const todos = [];
893
+ for (const entry of entries) {
894
+ const content = typeof entry['content'] === 'string' ? entry['content'].trim() : '';
895
+ if (content === '')
896
+ continue;
884
897
  const status = entry['status'];
885
- const mark = status === 'completed' ? '☑' : status === 'in_progress' ? '▶' : '☐';
886
- return `${mark} ${content}`;
887
- }).filter(line => line.trim() !== '☐' && line !== '');
888
- return lines.join('\n');
889
- };
890
- const foldPlan = (update) => {
891
- const plan = renderPlan(update);
892
- if (plan === '')
893
- return;
894
- // The plan's own snapshot stream (think-styled): every update replaces
895
- // the text and publishes at the reserved (turn, step); the stream
896
- // finalizes at settle (the wire carries no plan line that could
897
- // complete it) — or early, when a fold needs its step closed.
898
- reserveStream(planKey, 'think');
899
- const stream = streams.get(planKey);
900
- if (stream !== undefined && stream.text !== plan) {
901
- stream.text = plan;
902
- publishStreamSnapshot(stream, true);
898
+ todos.push({ content, status: status === 'in_progress' || status === 'completed' ? status : 'pending' });
903
899
  }
900
+ if (todos.length === 0)
901
+ return;
902
+ const last = childSession.snapshotEvents().filter(event => event.type === 'todo/write').at(-1);
903
+ if (last !== undefined && JSON.stringify(last.data) === JSON.stringify({ todos }))
904
+ return;
905
+ childSession.append('todo/write', { todos });
906
+ persist();
904
907
  };
905
908
  /**
906
909
  * The live-output channel. Snapshots are browser-only previews (never
@@ -1265,7 +1268,12 @@ export class KimiAcpLiveDriver {
1265
1268
  }
1266
1269
  }
1267
1270
  else if (kind === 'tool_call') {
1268
- const id = typeof update['toolCallId'] === 'string' ? update['toolCallId'] : undefined;
1271
+ // ACP prefixes the call id with kimi's turn index ("1:tool_x") from the
1272
+ // second round on; the wire records the bare id. Unprefixed, the two
1273
+ // name one call — otherwise the fold never releases the reservation
1274
+ // and settle re-writes the call as an interrupted orphan after the
1275
+ // answer (the chat then folds the answer into the process).
1276
+ const id = typeof update['toolCallId'] === 'string' ? update['toolCallId'].replace(/^\d+:/, '') : undefined;
1269
1277
  if (id === undefined || !seenToolCalls.has(id)) {
1270
1278
  // The wire flush can win the race: a call already folded into the
1271
1279
  // log (its notification arriving late) needs no reservation — one
@@ -8,6 +8,8 @@
8
8
  * host) on every write. User-declared servers are preserved verbatim.
9
9
  * @module @khorsheed/dsh-local-agent-kimi/member-bridge-config
10
10
  */
11
+ /** The member bridge's per-call bound: as long as a question card waits for the human (30 min). */
12
+ export declare const MEMBER_TOOL_TIMEOUT_MS: number;
11
13
  /** Server-key prefix marking the family's per-run bridge entries. */
12
14
  export declare const MEMBER_BRIDGE_SERVER_PREFIX = "dsh-member-";
13
15
  /**
@@ -12,6 +12,8 @@ import { readFileSync, writeFileSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
13
  /** Kimi's user-level MCP config filename inside the scoped home. */
14
14
  const MCP_CONFIG_FILENAME = 'mcp.json';
15
+ /** The member bridge's per-call bound: as long as a question card waits for the human (30 min). */
16
+ export const MEMBER_TOOL_TIMEOUT_MS = 30 * 60_000;
15
17
  /** Server-key prefix marking the family's per-run bridge entries. */
16
18
  export const MEMBER_BRIDGE_SERVER_PREFIX = 'dsh-member-';
17
19
  /**
@@ -60,7 +62,9 @@ export function injectMemberBridge(homeDir, key, command, env) {
60
62
  if (name.startsWith(MEMBER_BRIDGE_SERVER_PREFIX) && name !== key)
61
63
  delete servers[name];
62
64
  }
63
- servers[key] = { command: command.command, args: command.args, env };
65
+ // A bridge call may wait for the human (ask_user_question, a mode tool's
66
+ // approval): kimi's 60s MCP default fails it first and the answer is lost.
67
+ servers[key] = { command: command.command, args: command.args, env, toolTimeoutMs: MEMBER_TOOL_TIMEOUT_MS };
64
68
  writeMcpConfig(file, { ...config, mcpServers: servers });
65
69
  }
66
70
  /**
@@ -71,6 +71,14 @@ export function nextWrittenKimiStep(childSession, turn) {
71
71
  }
72
72
  return last + 1;
73
73
  }
74
+ /** Wire usage records already counted, per child session (see mirrorKimiSessionDelta). */
75
+ const countedUsage = new WeakMap();
76
+ const usageSeen = (childSession) => {
77
+ let seen = countedUsage.get(childSession);
78
+ if (seen === undefined)
79
+ countedUsage.set(childSession, seen = new Set());
80
+ return seen;
81
+ };
74
82
  /** The run-progress delta text for one transcript line (any kind). */
75
83
  export function kimiLineProgressText(line) {
76
84
  if (line.kind !== 'tool')
@@ -245,6 +253,13 @@ export async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSes
245
253
  // step-tagged unit claims its own line span's records; records no tagged
246
254
  // unit claims (untagged spans on older wires, or the boundary) sum onto the
247
255
  // pass's last folded content unit — the pre-tagging shape.
256
+ // A record counts once per child session, ever: the boundary is inclusive, so
257
+ // a pass whose offset cannot advance (a held unit) re-reads the same boundary
258
+ // record on every poll — one 320k-token record was carried ten times onto a
259
+ // cancelled round's step (3.2M). Wire line numbers only grow, so the line
260
+ // identifies the record across passes and rounds.
261
+ const seen = usageSeen(childSession);
262
+ const usageRecords = transcript.usageRecords.filter(record => !seen.has(record.line));
248
263
  const unitUsage = [];
249
264
  const claimed = [];
250
265
  let unitStart = fromLines;
@@ -254,15 +269,18 @@ export async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSes
254
269
  const head = unit[0];
255
270
  if (head.kind !== 'user' && head.wireStep !== undefined) {
256
271
  claimed.push({ from: start, to: unitStart });
257
- unitUsage.push(sumUsageRecords(transcript.usageRecords.filter(record => record.line >= start && record.line < unitStart)));
272
+ unitUsage.push(sumUsageRecords(usageRecords.filter(record => record.line >= start && record.line < unitStart)));
258
273
  }
259
274
  else {
260
275
  unitUsage.push(undefined);
261
276
  }
262
277
  }
263
- const unclaimedUsage = sumUsageRecords(transcript.usageRecords.filter(record => record.line >= fromLines && record.line <= consumedTotal
278
+ const unclaimedUsage = sumUsageRecords(usageRecords.filter(record => record.line >= fromLines && record.line <= consumedTotal
264
279
  && !claimed.some(span => record.line >= span.from && record.line < span.to)));
265
- const deltaUsage = sumUsageRecords(transcript.usageRecords.filter(record => record.line >= fromLines && record.line <= consumedTotal));
280
+ const windowRecords = usageRecords.filter(record => record.line >= fromLines && record.line <= consumedTotal);
281
+ const deltaUsage = sumUsageRecords(windowRecords);
282
+ for (const record of windowRecords)
283
+ seen.add(record.line);
266
284
  // The steps a fold may NOT take: every step already written (the session's
267
285
  // own event ledger), consulted per turn. A round mirrored across several
268
286
  // live polls keeps its earlier writes, so later polls never reuse them.
@@ -296,6 +314,20 @@ export async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSes
296
314
  written.add(step);
297
315
  takenSteps.set(turn, written);
298
316
  };
317
+ // Turns the log has already closed take no more writes: the released-log
318
+ // reader admits events only inside the open turn. A backlog the offset never
319
+ // covered (a fold that failed for rounds, then an offset lost on restart)
320
+ // would otherwise land old content inside a finished turn — or, after the
321
+ // next turn opened, corrupt the whole log. Those units are skipped; the
322
+ // offset still moves past them.
323
+ const closedTurns = new Set();
324
+ for (const event of childSession.snapshotEvents()) {
325
+ if (event.type !== 'turn/end')
326
+ continue;
327
+ const closed = event.data.turn;
328
+ if (typeof closed === 'number')
329
+ closedTurns.add(closed);
330
+ }
299
331
  /** The window's accounting that no fold carried (dropped units' sums). */
300
332
  let carryUsage;
301
333
  // The pass's last content unit carries the records no tagged unit claimed.
@@ -319,6 +351,8 @@ export async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSes
319
351
  // numbering) is the dsh round; no offset is needed because both count the
320
352
  // same rounds.
321
353
  const turn = head.turn;
354
+ if (closedTurns.has(turn))
355
+ continue;
322
356
  if (head.kind === 'user') {
323
357
  // The live driver appends the round's user/message at turn start (so the
324
358
  // question renders before the streamed think/text instead of after it).
@@ -104,6 +104,17 @@ export declare function findKimiSessionDir(homeDir: string, sessionId: string):
104
104
  * @returns the transcript.
105
105
  */
106
106
  export declare function readKimiTranscript(sessionDir: string): Promise<KimiSessionTranscript>;
107
+ /**
108
+ * A wire tool result's output as display text. Most tools report a string;
109
+ * a media read (ReadMediaFile) reports content parts — text parts keep their
110
+ * text, an image part becomes a short marker (its bytes stay in kimi's blob
111
+ * store). Anything else reads as no output, never a parse failure: one
112
+ * unreadable result must not stop the whole transcript (and the round's
113
+ * usage) from folding.
114
+ * @param raw - the result's `output` field.
115
+ * @returns the text, or undefined when there is none.
116
+ */
117
+ export declare function toolOutputText(raw: unknown): string | undefined;
107
118
  /**
108
119
  * Map a wire `usage.record` payload onto the shared usage contract. kimi
109
120
  * reports input (uncached), cache read, cache creation, and output; the
@@ -105,6 +105,8 @@ function editArgsOf(name, record) {
105
105
  return patch;
106
106
  return undefined;
107
107
  }
108
+ /** Cap on a structured tool call's mirrored JSON arguments. */
109
+ const STRUCTURED_ARGS_MAX = 16_000;
108
110
  /** Render a tool call's arguments into a compact summary. */
109
111
  function argsOf(name, args) {
110
112
  if (args === undefined)
@@ -119,6 +121,13 @@ function argsOf(name, args) {
119
121
  const edit = editArgsOf(name, record);
120
122
  if (edit !== undefined)
121
123
  return edit;
124
+ // Structured arguments (an MCP tool's nested questions, options, lists)
125
+ // keep their full JSON: the one-line summary below keeps only scalars and
126
+ // dropped them, so the member's ask_user_question read as called with nothing.
127
+ if (Object.values(record).some(value => typeof value === 'object' && value !== null)) {
128
+ const json = JSON.stringify(record);
129
+ return json.length <= STRUCTURED_ARGS_MAX ? json : `${json.slice(0, STRUCTURED_ARGS_MAX)}…`;
130
+ }
122
131
  // WebSearch/FetchURL expose a query/url; prefer the most meaningful scalar.
123
132
  for (const key of ['query', 'url', 'path', 'command']) {
124
133
  const value = record[key];
@@ -168,6 +177,8 @@ export async function readKimiTranscript(sessionDir) {
168
177
  // turn/start numbering.
169
178
  let turn = 0;
170
179
  let turnSeen = false;
180
+ /** The wire marks rounds with turn.prompt (current kimi): a user message it did not prompt is an injection. */
181
+ let promptsMarkTurns = false;
171
182
  // Tool calls by uuid and toolCallId, so a result attaches to its own call
172
183
  // even when several calls run in parallel within one step.
173
184
  const callsByUuid = new Map();
@@ -228,6 +239,7 @@ export async function readKimiTranscript(sessionDir) {
228
239
  continue;
229
240
  }
230
241
  if (event.type === 'turn.prompt') {
242
+ promptsMarkTurns = true;
231
243
  lineModel = undefined;
232
244
  // Each round's user prompt opens the next turn; the matching
233
245
  // context.append_message below is deduped against this text.
@@ -254,6 +266,13 @@ export async function readKimiTranscript(sessionDir) {
254
266
  // Duplicate of the prompt; skip.
255
267
  continue;
256
268
  }
269
+ // A wire that opens every round with turn.prompt: any other user
270
+ // message is kimi's own injection inside the round (a Skill's loaded
271
+ // instructions, a steer) — its loop events keep the same turnId. It
272
+ // opens no turn (the dsh round is still running: a turn here wrote
273
+ // steps into a turn the log never opened) and is not the human's.
274
+ if (promptsMarkTurns)
275
+ continue;
257
276
  turnSeen = true;
258
277
  turn += 1;
259
278
  lineModel = undefined;
@@ -309,7 +328,7 @@ export async function readKimiTranscript(sessionDir) {
309
328
  }
310
329
  else if (loop.type === 'tool.result') {
311
330
  const result = loop;
312
- const output = result.result?.output;
331
+ const output = toolOutputText(result.result?.output);
313
332
  let target = typeof result.parentUuid === 'string'
314
333
  ? callsByUuid.get(result.parentUuid)
315
334
  : undefined;
@@ -346,6 +365,31 @@ export async function readKimiTranscript(sessionDir) {
346
365
  }
347
366
  return { sessionId, lines, usageRecords, closedSteps, ...model === undefined ? {} : { model }, ...modelTurn === undefined ? {} : { modelTurn } };
348
367
  }
368
+ /**
369
+ * A wire tool result's output as display text. Most tools report a string;
370
+ * a media read (ReadMediaFile) reports content parts — text parts keep their
371
+ * text, an image part becomes a short marker (its bytes stay in kimi's blob
372
+ * store). Anything else reads as no output, never a parse failure: one
373
+ * unreadable result must not stop the whole transcript (and the round's
374
+ * usage) from folding.
375
+ * @param raw - the result's `output` field.
376
+ * @returns the text, or undefined when there is none.
377
+ */
378
+ export function toolOutputText(raw) {
379
+ if (typeof raw === 'string')
380
+ return raw;
381
+ if (!Array.isArray(raw))
382
+ return undefined;
383
+ const parts = raw.map((part) => {
384
+ if (part === null || typeof part !== 'object')
385
+ return '';
386
+ const entry = part;
387
+ if (entry.type === 'text' && typeof entry.text === 'string')
388
+ return entry.text;
389
+ return typeof entry.type === 'string' && entry.type !== '' ? `[${entry.type}]` : '';
390
+ });
391
+ return parts.join('');
392
+ }
349
393
  /**
350
394
  * Map a wire `usage.record` payload onto the shared usage contract. kimi
351
395
  * reports input (uncached), cache read, cache creation, and output; the
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "local-agent",
9
9
  "kimi"
10
10
  ],
11
- "version": "0.2.1",
11
+ "version": "0.2.2",
12
12
  "type": "module",
13
13
  "main": "lib/index.js",
14
14
  "types": "lib/types/index.d.ts",
@@ -69,7 +69,7 @@
69
69
  "dsh": ">=0.1.5-rc.1 <0.3.0-0"
70
70
  },
71
71
  "dependencies": {
72
- "@khorsheed/dsh-local-agent": "^0.2.1"
72
+ "@khorsheed/dsh-local-agent": "^0.2.2"
73
73
  },
74
74
  "peerDependencies": {
75
75
  "@deepseek-ai/cordis": "^4.0.1",