@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 +8 -0
- package/lib/client.js +64 -64
- package/lib/index.js +163 -104
- package/lib/types/live-driver.d.ts +0 -1
- package/lib/types/live-driver.js +32 -24
- package/lib/types/member-bridge-config.d.ts +2 -0
- package/lib/types/member-bridge-config.js +5 -1
- package/lib/types/session-mirror.js +37 -3
- package/lib/types/session-view.d.ts +11 -0
- package/lib/types/session-view.js +45 -1
- package/package.json +2 -2
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
|
-
"
|
|
346
|
+
"retryButton": "On5p7W_retryButton",
|
|
347
347
|
"loginPrompt": "On5p7W_loginPrompt",
|
|
348
|
-
"
|
|
349
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
369
|
-
"
|
|
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
|
-
"
|
|
692
|
-
"
|
|
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
|
-
"
|
|
698
|
-
"
|
|
682
|
+
"block": "_4eeJ7W_block",
|
|
683
|
+
"modelSave": "_4eeJ7W_modelSave",
|
|
699
684
|
"dialogActions": "_4eeJ7W_dialogActions",
|
|
700
|
-
"
|
|
701
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
709
|
-
"
|
|
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
|
-
"
|
|
1081
|
+
"chevronOpen": "apHkUa_chevronOpen",
|
|
1082
|
+
"saved": "apHkUa_saved",
|
|
1082
1083
|
"card": "apHkUa_card",
|
|
1083
|
-
"
|
|
1084
|
+
"rowLabel": "apHkUa_rowLabel",
|
|
1085
|
+
"name": "apHkUa_name",
|
|
1084
1086
|
"headText": "apHkUa_headText",
|
|
1085
|
-
"
|
|
1086
|
-
"
|
|
1087
|
-
"
|
|
1088
|
-
"endpointField": "apHkUa_endpointField",
|
|
1087
|
+
"endpointLabel": "apHkUa_endpointLabel",
|
|
1088
|
+
"info": "apHkUa_info",
|
|
1089
|
+
"row": "apHkUa_row",
|
|
1089
1090
|
"switchOn": "apHkUa_switchOn",
|
|
1090
|
-
"
|
|
1091
|
-
"
|
|
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
|
-
"
|
|
1097
|
+
"header": "apHkUa_header",
|
|
1095
1098
|
"body": "apHkUa_body",
|
|
1096
|
-
"
|
|
1097
|
-
"
|
|
1098
|
-
"
|
|
1099
|
-
"
|
|
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
|
-
"
|
|
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(
|
|
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(
|
|
2571
|
-
const
|
|
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
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
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
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
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;
|
package/lib/types/live-driver.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
883
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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.
|
|
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.
|
|
72
|
+
"@khorsheed/dsh-local-agent": "^0.2.2"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"@deepseek-ai/cordis": "^4.0.1",
|