lark-remote 0.5.19 → 0.5.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/bridge/approval-actions.d.ts +1 -1
  3. package/dist/bridge/approval-actions.js +1 -1
  4. package/dist/bridge/bash-run.d.ts +71 -0
  5. package/dist/bridge/bash-run.js +217 -0
  6. package/dist/bridge/compact.d.ts +155 -0
  7. package/dist/bridge/compact.js +450 -0
  8. package/dist/bridge/final-usage.js +4 -4
  9. package/dist/bridge/inbound-media.js +72 -46
  10. package/dist/bridge/index.d.ts +106 -153
  11. package/dist/bridge/index.js +400 -1000
  12. package/dist/bridge/job-watch.d.ts +68 -0
  13. package/dist/bridge/job-watch.js +146 -0
  14. package/dist/bridge/queue-manager.d.ts +3 -3
  15. package/dist/bridge/queue-manager.js +10 -29
  16. package/dist/card/approval-render.d.ts +2 -2
  17. package/dist/card/approval-render.js +141 -130
  18. package/dist/card/card-budget.js +123 -98
  19. package/dist/card/card-helpers.d.ts +3 -3
  20. package/dist/card/card-helpers.js +6 -6
  21. package/dist/card/final-answer-file.js +1 -1
  22. package/dist/card/run-renderer.d.ts +3 -3
  23. package/dist/card/run-renderer.js +114 -159
  24. package/dist/card/run-state.d.ts +1 -15
  25. package/dist/card/run-state.js +358 -292
  26. package/dist/card/tool-render.d.ts +1 -1
  27. package/dist/card/tool-render.js +21 -17
  28. package/dist/card/usage-format.d.ts +22 -7
  29. package/dist/card/usage-format.js +119 -100
  30. package/dist/cli.js +15 -3
  31. package/dist/clone.js +2 -2
  32. package/dist/common/text-width.js +36 -24
  33. package/dist/common/tool-summary.js +132 -90
  34. package/dist/config/codex-config.js +130 -106
  35. package/dist/config/index.d.ts +16 -2
  36. package/dist/config/index.js +23 -8
  37. package/dist/connector/index.js +6 -6
  38. package/dist/error-classification.js +3 -2
  39. package/dist/inbound/placeholder.d.ts +1 -1
  40. package/dist/inbound/placeholder.js +1 -1
  41. package/dist/index.js +387 -281
  42. package/dist/logger/index.js +6 -6
  43. package/dist/message-handlers.d.ts +0 -5
  44. package/dist/message-handlers.js +254 -237
  45. package/dist/model-catalog.d.ts +2 -2
  46. package/dist/model-catalog.js +3 -8
  47. package/dist/platform/identity.js +1 -1
  48. package/dist/platform/path.d.ts +1 -1
  49. package/dist/platform/path.js +1 -1
  50. package/dist/platform/sleep-blocker.js +1 -1
  51. package/dist/platform/terminator-win32.js +2 -1
  52. package/dist/router/commands/ls.d.ts +3 -0
  53. package/dist/router/commands/ls.js +256 -175
  54. package/dist/router/commands/order.js +184 -159
  55. package/dist/router/commands/resume.js +190 -152
  56. package/dist/router/commands/ws.js +219 -174
  57. package/dist/router/config/dsh.d.ts +4 -8
  58. package/dist/router/config/dsh.js +3 -10
  59. package/dist/router/config/kimi.js +1 -1
  60. package/dist/router/config/types.d.ts +1 -1
  61. package/dist/router/config-diff.js +1 -1
  62. package/dist/router/index.d.ts +56 -11
  63. package/dist/router/index.js +401 -391
  64. package/dist/runner/bash/runner.js +2 -4
  65. package/dist/runner/claude/session.d.ts +1 -1
  66. package/dist/runner/claude/session.js +6 -4
  67. package/dist/runner/codex/app-server/runner.d.ts +9 -2
  68. package/dist/runner/codex/app-server/runner.js +30 -25
  69. package/dist/runner/codex/app-server/translator.d.ts +2 -5
  70. package/dist/runner/codex/app-server/translator.js +291 -238
  71. package/dist/runner/common/acp/acp-approval.d.ts +1 -1
  72. package/dist/runner/common/acp/acp-approval.js +1 -1
  73. package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
  74. package/dist/runner/common/acp/base-acp-runner.js +26 -65
  75. package/dist/runner/common/acp/base-translator.d.ts +12 -2
  76. package/dist/runner/common/acp/base-translator.js +50 -26
  77. package/dist/runner/common/acp/protocol-types.d.ts +1 -1
  78. package/dist/runner/common/acp/protocol-types.js +2 -2
  79. package/dist/runner/common/connection-based-runner.d.ts +32 -1
  80. package/dist/runner/common/connection-based-runner.js +32 -2
  81. package/dist/runner/common/jsonl-stream.js +154 -187
  82. package/dist/runner/common/jsonrpc/client.d.ts +29 -0
  83. package/dist/runner/common/jsonrpc/client.js +43 -12
  84. package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
  85. package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
  86. package/dist/runner/common/spawning-runner.d.ts +6 -1
  87. package/dist/runner/common/spawning-runner.js +9 -4
  88. package/dist/runner/common/turn-started.d.ts +8 -0
  89. package/dist/runner/common/turn-started.js +11 -0
  90. package/dist/runner/dsh/approval.js +84 -51
  91. package/dist/runner/dsh/catalog-probe.d.ts +17 -0
  92. package/dist/runner/dsh/catalog-probe.js +10 -0
  93. package/dist/runner/dsh/client.d.ts +7 -11
  94. package/dist/runner/dsh/client.js +356 -289
  95. package/dist/runner/dsh/exit-cancel.d.ts +2 -2
  96. package/dist/runner/dsh/exit-cancel.js +2 -2
  97. package/dist/runner/dsh/inflight.d.ts +1 -1
  98. package/dist/runner/dsh/inflight.js +1 -1
  99. package/dist/runner/dsh/mux-stream.js +28 -8
  100. package/dist/runner/dsh/runner.d.ts +44 -4
  101. package/dist/runner/dsh/runner.js +296 -199
  102. package/dist/runner/dsh/translator.d.ts +1 -31
  103. package/dist/runner/dsh/translator.js +180 -178
  104. package/dist/runner/index.d.ts +1 -1
  105. package/dist/runner/kimi/acp/runner.d.ts +1 -2
  106. package/dist/runner/kimi/acp/runner.js +82 -46
  107. package/dist/runner/kimi/acp/translator.d.ts +1 -1
  108. package/dist/runner/kimi/acp/translator.js +3 -4
  109. package/dist/runner/opencode/acp/error-monitor.js +5 -5
  110. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  111. package/dist/runner/opencode/acp/runner.js +4 -4
  112. package/dist/runner/pi/rpc/client.js +4 -13
  113. package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
  114. package/dist/runner/pi/rpc/protocol-types.js +1 -1
  115. package/dist/runner/pi/rpc/runner.d.ts +3 -0
  116. package/dist/runner/pi/rpc/runner.js +27 -42
  117. package/dist/runner/pi/rpc/translator.js +61 -60
  118. package/dist/runner/types.d.ts +2 -2
  119. package/dist/runner/types.js +1 -1
  120. package/dist/session/claude/sessions.d.ts +45 -0
  121. package/dist/session/claude/sessions.js +160 -122
  122. package/dist/session/codex/rollout-reader.js +181 -182
  123. package/dist/session/common/throw-session-list-error.d.ts +10 -0
  124. package/dist/session/common/throw-session-list-error.js +16 -0
  125. package/dist/session/dsh/sessions.js +291 -235
  126. package/dist/session/kimi/sessions.d.ts +40 -8
  127. package/dist/session/kimi/sessions.js +373 -301
  128. package/dist/session/opencode/sessions.d.ts +142 -1
  129. package/dist/session/opencode/sessions.js +378 -321
  130. package/dist/session/pi/sessions.js +4 -5
  131. package/dist/session/session-store.d.ts +9 -1
  132. package/dist/session/session-store.js +99 -99
  133. package/dist/update/version-check.js +2 -1
  134. package/dist/workspace/index.d.ts +1 -1
  135. package/dist/workspace/index.js +1 -1
  136. package/package.json +1 -1
@@ -2,8 +2,10 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import { readJsonlLines as readJsonlLinesShared, readLastNJsonlLines } from '../common/jsonl.js';
5
- import { UsageAccumulator, cumulativeUsageFields } from '../common/usage-accumulator.js';
5
+ import { cumulativeUsageFields } from '../common/usage-accumulator.js';
6
+ import { throwSessionListError } from '../common/throw-session-list-error.js';
6
7
  import { getLogger } from '../../logger/index.js';
8
+ import { logNonEnoentDebug } from '../session-store.js';
7
9
  import { isStale } from '../common/constants.js';
8
10
  import { safeRealpath, samePath } from '../../platform/path.js';
9
11
  import { capEvents, paginate } from '../common/pagination.js';
@@ -134,6 +136,233 @@ export function checkCwdGuard(state, realCwd) {
134
136
  return 'verified';
135
137
  return 'failed';
136
138
  }
139
+ function emptyKimiTotals() {
140
+ return { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
141
+ }
142
+ /**
143
+ * kimi 主 wire usage 扫描累积器(纯函数):单遍解析,产出会话累计、末 turn
144
+ * flow、上下文水位与压缩统计。
145
+ *
146
+ * 产生:KimiWireUsageScan。依赖:入参 lines(已解析前的原始行)。不改任何
147
+ * 外部状态;畸形行跳过,畸形 usage.record 按字段 0 计但记为「有数据」。
148
+ */
149
+ export function scanKimiUsage(lines) {
150
+ const totals = emptyKimiTotals();
151
+ let turn;
152
+ // contextLength is only from the main wire's last usage.record
153
+ // (subagent context is an independent window).
154
+ let mainContextLength = 0;
155
+ // §6.3: Compaction stats — count compact events, track last pre-compact context.
156
+ let compactCount = 0;
157
+ let compactPreContextLength;
158
+ // Distinguish "no usage.record in any wire file" (no data → usage:
159
+ // undefined, downstream omits the token block) from "records exist but
160
+ // are all zero / malformed" (real data → aggregate with ?? 0 and return
161
+ // the zeros as-is). A malformed record (usage.record line with missing
162
+ // usage object/fields) still counts as data.
163
+ let hasUsageRecord = false;
164
+ for (const line of lines) {
165
+ let entry;
166
+ try {
167
+ entry = JSON.parse(line);
168
+ }
169
+ catch {
170
+ continue;
171
+ }
172
+ if (isUsageRecord(entry)) {
173
+ hasUsageRecord = true;
174
+ // Malformed records (missing usage object or missing fields) must
175
+ // not poison the accumulators with NaN: count missing fields as 0.
176
+ const inputOther = entry.usage?.inputOther ?? 0;
177
+ const output = entry.usage?.output ?? 0;
178
+ const inputCacheRead = entry.usage?.inputCacheRead ?? 0;
179
+ const inputCacheCreation = entry.usage?.inputCacheCreation ?? 0;
180
+ totals.input += inputOther;
181
+ totals.output += output;
182
+ totals.cacheRead += inputCacheRead;
183
+ totals.cacheCreation += inputCacheCreation;
184
+ if (turn) {
185
+ turn.input += inputOther;
186
+ turn.output += output;
187
+ turn.cacheRead += inputCacheRead;
188
+ turn.cacheCreation += inputCacheCreation;
189
+ }
190
+ // unified contract (§9.21): input + output + cacheRead + cacheCreation.
191
+ // 末轮 output 已进入历史、下一 prompt 全量携带——与 compact postTokens
192
+ // 水位同语义(此前漏加 output 会系统性低估长收尾 turn)。
193
+ mainContextLength = inputOther + output + inputCacheRead + inputCacheCreation;
194
+ continue;
195
+ }
196
+ // §6.3: Count compaction events and track pre-compact context length.
197
+ // 展示计数只计 context.apply_compaction:一次手动 /compact 会同时写
198
+ // full_compaction.complete + context.apply_compaction 两条(2026-08-16
199
+ // 实弹 wire.jsonl),双计会把一次压缩虚报成 2 次。full_compaction.complete
200
+ // 仅作 runCompact 轮询的完成信号(readCompactionRecords 仍返回两种)。
201
+ if (isCompactionComplete(entry) && entry.type === 'context.apply_compaction') {
202
+ compactCount++;
203
+ // Capture context length before this compaction as the pre-compact
204
+ // watermark (the most recent one is most relevant for display).
205
+ if (entry.tokensBefore !== undefined) {
206
+ compactPreContextLength = entry.tokensBefore;
207
+ }
208
+ continue;
209
+ }
210
+ // turn.prompt 是 turn 的起点:本 turn 的 flow 从这里重新累计。
211
+ if (isTurnPrompt(entry)) {
212
+ turn = emptyKimiTotals();
213
+ continue;
214
+ }
215
+ }
216
+ return { totals, turn, mainContextLength, compactCount, compactPreContextLength, hasUsageRecord };
217
+ }
218
+ /**
219
+ * kimi 子 agent wire usage 扫描(只读 I/O):agents/<name>/wire.jsonl(main
220
+ * 以外的兄弟目录)与主会话同属同一计费实体,token 消耗必须并入会话累计;
221
+ * contextLength/展示事件不取子 agent。目录不可读或文件缺失静默跳过。
222
+ *
223
+ * 产生:KimiSubagentUsageScan。依赖:文件系统(agentsDir 只读)。不改外部状态。
224
+ */
225
+ function scanKimiSubagentUsage(agentsDir) {
226
+ const totals = emptyKimiTotals();
227
+ let hasUsageRecord = false;
228
+ try {
229
+ for (const dirent of fs.readdirSync(agentsDir, { withFileTypes: true })) {
230
+ if (!dirent.isDirectory() || dirent.name === 'main') {
231
+ continue;
232
+ }
233
+ const subWirePath = path.join(agentsDir, dirent.name, 'wire.jsonl');
234
+ if (!fs.existsSync(subWirePath)) {
235
+ continue;
236
+ }
237
+ for (const subLine of readJsonlLines(subWirePath)) {
238
+ try {
239
+ const subEntry = JSON.parse(subLine);
240
+ if (isUsageRecord(subEntry)) {
241
+ hasUsageRecord = true;
242
+ totals.input += subEntry.usage?.inputOther ?? 0;
243
+ totals.output += subEntry.usage?.output ?? 0;
244
+ totals.cacheRead += subEntry.usage?.inputCacheRead ?? 0;
245
+ totals.cacheCreation += subEntry.usage?.inputCacheCreation ?? 0;
246
+ }
247
+ }
248
+ catch {
249
+ continue;
250
+ }
251
+ }
252
+ }
253
+ }
254
+ catch {
255
+ // agents dir unreadable — fall back to main-only usage
256
+ }
257
+ return { totals, hasUsageRecord };
258
+ }
259
+ /**
260
+ * kimi usage 装配(tail 装配段,纯函数):末 turn flow + 会话累计(主 wire +
261
+ * 子 agent)+ 主 wire 水位 + 压缩统计 → AgentSessionUsage。
262
+ *
263
+ * 有 turn.prompt 时本 run = 末个 turn 的 flow;末 turn 还没有记录时
264
+ * turn 为全 0。整份 wire 都没有 turn.prompt(旧数据/截断)时无从分段,
265
+ * 沿用「整份文件 = 一个 turn」的旧口径(含子 agent wire)。
266
+ */
267
+ function assembleKimiUsage(main, sub) {
268
+ const totals = {
269
+ input: main.totals.input + sub.totals.input,
270
+ output: main.totals.output + sub.totals.output,
271
+ cacheRead: main.totals.cacheRead + sub.totals.cacheRead,
272
+ cacheCreation: main.totals.cacheCreation + sub.totals.cacheCreation,
273
+ };
274
+ const flow = main.turn ?? totals;
275
+ // Kimi semantics: preserve 0 values (not undefined) for cache fields to match original
276
+ return {
277
+ inputTokens: flow.input,
278
+ outputTokens: flow.output,
279
+ contextLength: main.mainContextLength,
280
+ cacheReadTokens: flow.cacheRead > 0 ? flow.cacheRead : 0,
281
+ cacheCreationTokens: flow.cacheCreation > 0 ? flow.cacheCreation : 0,
282
+ totalTokens: flow.input + flow.output + flow.cacheRead + flow.cacheCreation,
283
+ // Cumulative (session-wide): sums every usage.record across all turns
284
+ // (subagent wires included) — the "累计" line, never the per-run value.
285
+ ...cumulativeUsageFields(totals),
286
+ // §6.3: compaction stats from wire.jsonl
287
+ compactCount: main.compactCount,
288
+ ...(main.compactPreContextLength !== undefined
289
+ ? { compactPreContextLength: main.compactPreContextLength }
290
+ : {}),
291
+ };
292
+ }
293
+ /**
294
+ * kimi 主 wire 展示装配(纯函数):turn.prompt 提取 displayTitle,loop event
295
+ * 映射为展示事件。tool.call/tool.result 是**独立的 loop event**(对照真实
296
+ * wire.jsonl 验证过的结构,勿合并/改形)。
297
+ *
298
+ * 产生:KimiEventScan。依赖:入参 lines。不改任何外部状态。
299
+ */
300
+ function scanKimiEvents(lines) {
301
+ const events = [];
302
+ let displayTitle;
303
+ for (const line of lines) {
304
+ let entry;
305
+ try {
306
+ entry = JSON.parse(line);
307
+ }
308
+ catch {
309
+ continue;
310
+ }
311
+ // displayTitle + display events now scan ALL lines (not a
312
+ // line-slice), then `events.slice(-maxEvents)` keeps the LAST N
313
+ // EVENTS. Previously `lines.slice(-maxEvents)` cut raw lines, but
314
+ // usage.record / step.begin / turn.prompt lines produce no display
315
+ // event, so "last 5 lines" could yield only 1~2 visible events —
316
+ // and `slice(-0)` returned the full array. displayTitle takes the
317
+ // last turn.prompt across the whole session (the most recent user
318
+ // prompt), which is the correct catch-up title.
319
+ // Extract display title from turn.prompt
320
+ if (isTurnPrompt(entry)) {
321
+ if (entry.input && entry.input.length > 0) {
322
+ displayTitle = entry.input[0].text;
323
+ }
324
+ continue;
325
+ }
326
+ // Convert loop events to content events
327
+ if (isAppendLoopEvent(entry)) {
328
+ const evt = entry.event;
329
+ if (evt.type === 'content.part' && evt.part) {
330
+ if (evt.part.type === 'think') {
331
+ // Skip thinking blocks for display
332
+ continue;
333
+ }
334
+ else if (evt.part.type === 'text' && evt.part.text) {
335
+ events.push({
336
+ type: 'text',
337
+ content: evt.part.text,
338
+ });
339
+ }
340
+ }
341
+ else if (evt.type === 'tool.call' && evt.name) {
342
+ // 复用 content-blocks 的 200 字符截断:大 args(整段文件内容
343
+ // 写入、长 prompt)原文进事件会把 resume 卡顶到 28KB 预算之外
344
+ // (2026-08-08 extreme_fallback 故障的推手之一)。
345
+ const inputStr = truncateToolInput(JSON.stringify(evt.args ?? {}));
346
+ events.push({
347
+ type: 'tool_use',
348
+ content: `${evt.name}(${inputStr})`,
349
+ });
350
+ }
351
+ else if (evt.type === 'tool.result') {
352
+ // result is { output }, { output, note }, or { isError, output }
353
+ // 对齐 opencode reader 的预折叠:单条 tool_result 上限
354
+ // TOOL_RESULT_MAX_BYTES,病理级输出(如 51KB 文件读取)不会
355
+ // 撑爆 resume 卡,也约束 events[] 内存占用。
356
+ const rawOut = evt.result?.output ?? `[tool result for ${evt.toolCallId}]`;
357
+ events.push({
358
+ type: 'tool_result',
359
+ content: truncateUtf8(rawOut, TOOL_RESULT_MAX_BYTES),
360
+ });
361
+ }
362
+ }
363
+ }
364
+ return { events, displayTitle };
365
+ }
137
366
  /**
138
367
  * Kimi Session Reader
139
368
  * Reads session history from ~/.kimi-code/
@@ -168,39 +397,6 @@ export class KimiSessionReader {
168
397
  findSessionIndexEntry(sessionId) {
169
398
  return this.scanSessionIndex().find((e) => e.sessionId === sessionId);
170
399
  }
171
- /**
172
- * Extract the last turn.prompt text from a wire.jsonl file.
173
- * Used as a fallback summary source when state.json has no title/lastPrompt
174
- * (v2 sessions). Scans the tail of the file for efficiency.
175
- * Returns undefined if no turn.prompt is found.
176
- */
177
- extractLastPromptFromWire(wirePath) {
178
- try {
179
- if (!fs.existsSync(wirePath))
180
- return undefined;
181
- // Scan backwards from the tail — turn.prompt is typically near the end
182
- // of a completed turn (before content.part / step.end / usage.record).
183
- const lines = readLastNJsonlLines(wirePath, 50);
184
- let lastPrompt;
185
- for (const line of lines) {
186
- try {
187
- const entry = JSON.parse(line);
188
- if (isTurnPrompt(entry)) {
189
- if (entry.input && entry.input.length > 0) {
190
- lastPrompt = entry.input[0].text;
191
- }
192
- }
193
- }
194
- catch {
195
- continue;
196
- }
197
- }
198
- return lastPrompt?.substring(0, 200);
199
- }
200
- catch {
201
- return undefined;
202
- }
203
- }
204
400
  /**
205
401
  * List all sessions for a given working directory
206
402
  */
@@ -208,87 +404,16 @@ export class KimiSessionReader {
208
404
  try {
209
405
  // 归一失败(陈旧/无权限 cwd)回退原样输入,让逐条 samePath 比较去决定
210
406
  // 空态——一次 realpathSync 抛错不得把整张列表清掉(与 opencode 同口径)。
211
- const realCwd = safeRealpath(cwd, (err) => {
212
- if (err.code === 'ENOENT')
213
- return;
214
- getLogger().debug(`[kimi-session-reader] realpath ${cwd} failed, using the path as given: ${err?.message ?? String(err)}`);
215
- });
407
+ const realCwd = safeRealpath(cwd, logKimiRealpathFailure.bind(null, cwd));
216
408
  const sessions = [];
217
409
  for (const entry of this.scanSessionIndex()) {
218
- // state.json 一次读取共用(cwd 兜底 + summary),I/O 减半。
219
- // 两次 catch 语义保留:解析失败 = 无法判 cwd → 跳过(continue);
220
- // 解析成功但字段缺失 = summary 落 wire.jsonl 提取。
221
- const statePath = path.join(entry.sessionDir, 'state.json');
222
- let state = null;
223
- let stateParsed = false;
224
- if (fs.existsSync(statePath)) {
225
- try {
226
- state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
227
- stateParsed = true;
228
- }
229
- catch (err) {
230
- getLogger().debug(`[kimi-session-reader] corrupt state.json skipped session=${entry.sessionId}: ${err?.message ?? String(err)}`);
231
- }
232
- }
233
- // Index workDir is v1-only; v2 sessions may have empty/missing workDir.
234
- // When index workDir is absent, fall back to state.json's cwd field.
235
- let entryWorkDir = entry.workDir;
236
- if (!entryWorkDir) {
237
- if (stateParsed && state)
238
- entryWorkDir = extractWorkDir(state);
239
- // Unreadable state — cannot determine cwd, entryWorkDir stays undefined
240
- }
241
- // cwd 判定与守卫(checkCwdGuard 的 samePath)同口径:严格字符串比较会
242
- // 把大小写(darwin)/尾分隔符形态不同的会话从列表里滤掉,而 /resume <id>
243
- // 的守卫却判等通过——「列表没有、按 id 却能读」。
244
- if (entryWorkDir === undefined || !samePath(entryWorkDir, realCwd)) {
245
- continue;
246
- }
247
- let summary = 'New Session';
248
- if (stateParsed && state) {
249
- // v1: state.title / state.lastPrompt; v2: these fields may be absent
250
- summary = state.title || state.lastPrompt || '';
251
- }
252
- // v2 state.json omits title/lastPrompt: extract from wire.jsonl's last
253
- // turn.prompt instead. Only scan a small tail to find it — the summary
254
- // only needs the most recent user prompt.
255
- if (!summary) {
256
- const wirePath = path.join(entry.sessionDir, 'agents', 'main', 'wire.jsonl');
257
- summary = this.extractLastPromptFromWire(wirePath) || 'New Session';
258
- }
259
- // Get mtime. the session DIRECTORY mtime only changes when
260
- // entries are created/deleted/renamed inside it — kimi appending to
261
- // agents/main/wire.jsonl during a run does NOT touch it, so dir-mtime
262
- // sorting lags real activity by minutes. Use the wire.jsonl mtime
263
- // (the file kimi keeps writing while the session is active) and fall
264
- // back to the dir mtime when the wire file is missing (e.g. a session
265
- // that never produced output).
266
- const wirePath = path.join(entry.sessionDir, 'agents', 'main', 'wire.jsonl');
267
- let mtimeMs;
268
- try {
269
- mtimeMs = fs.statSync(wirePath).mtimeMs;
270
- }
271
- catch {
272
- try {
273
- mtimeMs = fs.statSync(entry.sessionDir).mtimeMs;
274
- }
275
- catch (err) {
276
- if (err.code !== 'ENOENT') {
277
- getLogger().warn(`[kimi-session-reader] cannot stat ${entry.sessionDir}: ${err?.message ?? String(err)}`);
278
- }
279
- // 会话目录已不存在(或被清理):跳过该条,别让它拖垮整张列表。
280
- continue;
281
- }
282
- }
283
- sessions.push({
284
- sessionId: entry.sessionId,
285
- summary,
286
- mtime: mtimeMs,
287
- });
410
+ const row = collectKimiSession(entry, realCwd);
411
+ if (row)
412
+ sessions.push(row);
288
413
  }
289
414
  // Sort by mtime descending; same-mtime ties use a deterministic
290
415
  // secondary key so ordering is stable across scans.
291
- sortByRecencyDesc(sessions, (s) => s.mtime, (s) => s.sessionId);
416
+ sortByRecencyDesc(sessions, kimiRowMtime, kimiRowId);
292
417
  const { items, total } = paginate(sessions, opts ?? {});
293
418
  return {
294
419
  sessions: items,
@@ -302,9 +427,7 @@ export class KimiSessionReader {
302
427
  if (error.code === 'ENOENT') {
303
428
  return { sessions: [], total: 0 };
304
429
  }
305
- getLogger().error(`[kimi-session-reader] listSessions failed: ${error}`);
306
- const msg = error instanceof Error ? error.message : String(error);
307
- throw new Error(`kimi session list 读取失败: ${msg}`, { cause: error });
430
+ throwSessionListError('kimi', error);
308
431
  }
309
432
  }
310
433
  /**
@@ -315,6 +438,11 @@ export class KimiSessionReader {
315
438
  }
316
439
  /**
317
440
  * Read session content (events + usage)
441
+ *
442
+ * 编排:cwd 守卫定位主 wire(resolveSessionWirePath,与 isSessionActive
443
+ * 共用 verifySessionCwd 单源)→ 主 wire 两遍只读扫描(scanKimiEvents 出
444
+ * 展示流 + displayTitle,scanKimiUsage 出累计/flow/水位/压缩统计)→ 子
445
+ * agent wire 只进 usage 累计 → assembleKimiUsage 组装。
318
446
  */
319
447
  readSessionContent(sessionId, cwd, opts) {
320
448
  const wirePath = this.resolveSessionWirePath(sessionId, cwd);
@@ -323,144 +451,8 @@ export class KimiSessionReader {
323
451
  }
324
452
  try {
325
453
  const lines = Array.from(readJsonlLines(wirePath));
326
- const maxEvents = opts?.maxEvents ?? Number.MAX_SAFE_INTEGER;
327
- const events = [];
328
- let displayTitle;
329
- const acc = new UsageAccumulator();
330
- // contextLength is only from the main wire's last usage.record
331
- // (subagent context is an independent window). Tracked separately
332
- // from the accumulator's `last` (which could be a subagent record).
333
- let mainContextLength = 0;
334
- // §6.3: Compaction stats — count compact events, track last pre-compact context.
335
- let compactCount = 0;
336
- let compactPreContextLength;
337
- // Distinguish "no usage.record in any wire file" (no data → usage:
338
- // undefined, downstream omits the token block) from "records exist but
339
- // are all zero / malformed" (real data → aggregate with ?? 0 and return
340
- // the zeros as-is). A malformed record (usage.record line with missing
341
- // usage object/fields) still counts as data.
342
- let hasUsageRecord = false;
343
- // ccusage-style session-wide aggregation via UsageAccumulator: sum
344
- // every usage.record's per-component tokens; contextLength is the
345
- // current context window occupancy (not summable) — keep only the
346
- // last record's value, and only from the main wire (subagent context
347
- // is an independent window). Aggregation covers the whole wire.jsonl
348
- // and is independent of the maxEvents display slice
349
- // (usage = the session's overall token cost).
350
- //
351
- // 本 run 的 flow 字段是**末个 turn**(最后一个 turn.prompt 之后)的记录之和:
352
- // kimi 每次模型调用写一条 usage.record,带工具往返的 turn 会有多条。只取
353
- // 末条会低报,取会话总量(旧实现)又会把历史所有 run 的消耗算进这一轮。
354
- // 子 agent 的 wire 没有 turn 标记,只能进会话累计(无法归属到某一轮);
355
- // 整份 wire 都没有 turn.prompt(旧数据)时沿用「整份文件 = 一个 turn」。
356
- // 末个 turn 尚无 usage.record 时 flow = 0(当前契约是「末个 turn」,不是
357
- // 「末个非空 turn」)——不能退回上一轮的记录。
358
- let turnAcc = null;
359
- const accumulateUsage = (u, scope) => {
360
- hasUsageRecord = true;
361
- // Malformed records (missing usage object or missing fields) must
362
- // not poison the accumulators with NaN: count missing fields as 0.
363
- const inputOther = u?.inputOther ?? 0;
364
- const output = u?.output ?? 0;
365
- const inputCacheRead = u?.inputCacheRead ?? 0;
366
- const inputCacheCreation = u?.inputCacheCreation ?? 0;
367
- const record = {
368
- input: inputOther,
369
- output,
370
- cacheRead: inputCacheRead,
371
- cacheCreation: inputCacheCreation,
372
- };
373
- acc.add(record);
374
- if (scope !== 'main')
375
- return;
376
- turnAcc?.add(record);
377
- // unified contract (§9.21): input + output + cacheRead + cacheCreation.
378
- // 末轮 output 已进入历史、下一 prompt 全量携带——与 compact postTokens
379
- // 水位同语义(此前漏加 output 会系统性低估长收尾 turn)。
380
- mainContextLength = inputOther + output + inputCacheRead + inputCacheCreation;
381
- };
382
- for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
383
- const line = lines[lineIndex];
384
- try {
385
- const entry = JSON.parse(line);
386
- // Extract usage from usage.record.
387
- if (isUsageRecord(entry)) {
388
- accumulateUsage(entry.usage, 'main');
389
- continue;
390
- }
391
- // §6.3: Count compaction events and track pre-compact context length.
392
- // 展示计数只计 context.apply_compaction:一次手动 /compact 会同时写
393
- // full_compaction.complete + context.apply_compaction 两条(2026-08-16
394
- // 实弹 wire.jsonl),双计会把一次压缩虚报成 2 次。full_compaction.complete
395
- // 仅作 runCompact 轮询的完成信号(readCompactionRecords 仍返回两种)。
396
- if (isCompactionComplete(entry) && entry.type === 'context.apply_compaction') {
397
- compactCount++;
398
- // Capture context length before this compaction as the pre-compact
399
- // watermark (the most recent one is most relevant for display).
400
- if (entry.tokensBefore !== undefined) {
401
- compactPreContextLength = entry.tokensBefore;
402
- }
403
- continue;
404
- }
405
- // displayTitle + display events now scan ALL lines (not a
406
- // line-slice), then `events.slice(-maxEvents)` keeps the LAST N
407
- // EVENTS. Previously `lines.slice(-maxEvents)` cut raw lines, but
408
- // usage.record / step.begin / turn.prompt lines produce no display
409
- // event, so "last 5 lines" could yield only 1~2 visible events —
410
- // and `slice(-0)` returned the full array. displayTitle takes the
411
- // last turn.prompt across the whole session (the most recent user
412
- // prompt), which is the correct catch-up title.
413
- // Extract display title from turn.prompt
414
- if (isTurnPrompt(entry)) {
415
- // turn.prompt 是 turn 的起点:本 turn 的 flow 从这里重新累计。
416
- turnAcc = new UsageAccumulator();
417
- if (entry.input && entry.input.length > 0) {
418
- displayTitle = entry.input[0].text;
419
- }
420
- continue;
421
- }
422
- // Convert loop events to content events
423
- if (isAppendLoopEvent(entry)) {
424
- const evt = entry.event;
425
- if (evt.type === 'content.part' && evt.part) {
426
- if (evt.part.type === 'think') {
427
- // Skip thinking blocks for display
428
- continue;
429
- }
430
- else if (evt.part.type === 'text' && evt.part.text) {
431
- events.push({
432
- type: 'text',
433
- content: evt.part.text,
434
- });
435
- }
436
- }
437
- else if (evt.type === 'tool.call' && evt.name) {
438
- // 复用 content-blocks 的 200 字符截断:大 args(整段文件内容
439
- // 写入、长 prompt)原文进事件会把 resume 卡顶到 28KB 预算之外
440
- // (2026-08-08 extreme_fallback 故障的推手之一)。
441
- const inputStr = truncateToolInput(JSON.stringify(evt.args ?? {}));
442
- events.push({
443
- type: 'tool_use',
444
- content: `${evt.name}(${inputStr})`,
445
- });
446
- }
447
- else if (evt.type === 'tool.result') {
448
- // result is { output }, { output, note }, or { isError, output }
449
- // 对齐 opencode reader 的预折叠:单条 tool_result 上限
450
- // TOOL_RESULT_MAX_BYTES,病理级输出(如 51KB 文件读取)不会
451
- // 撑爆 resume 卡,也约束 events[] 内存占用。
452
- const rawOut = evt.result?.output ?? `[tool result for ${evt.toolCallId}]`;
453
- events.push({
454
- type: 'tool_result',
455
- content: truncateUtf8(rawOut, TOOL_RESULT_MAX_BYTES),
456
- });
457
- }
458
- }
459
- }
460
- catch {
461
- continue;
462
- }
463
- }
454
+ const { events, displayTitle } = scanKimiEvents(lines);
455
+ const mainUsage = scanKimiUsage(lines);
464
456
  // Subagent wire files (agents/<name>/wire.jsonl, e.g. Task-derived
465
457
  // agent-0..N) belong to the same session and the same billing entity:
466
458
  // their token cost must be summed into the session-wide usage.
@@ -470,56 +462,12 @@ export class KimiSessionReader {
470
462
  // wirePath = <sessionDir>/agents/main/wire.jsonl → agents dir is two
471
463
  // levels up (siblings of main hold subagent wires).
472
464
  const agentsDir = path.dirname(path.dirname(wirePath));
473
- try {
474
- for (const dirent of fs.readdirSync(agentsDir, { withFileTypes: true })) {
475
- if (!dirent.isDirectory() || dirent.name === 'main') {
476
- continue;
477
- }
478
- const subWirePath = path.join(agentsDir, dirent.name, 'wire.jsonl');
479
- if (!fs.existsSync(subWirePath)) {
480
- continue;
481
- }
482
- for (const subLine of readJsonlLines(subWirePath)) {
483
- try {
484
- const subEntry = JSON.parse(subLine);
485
- if (isUsageRecord(subEntry)) {
486
- accumulateUsage(subEntry.usage, 'subagent');
487
- }
488
- }
489
- catch {
490
- continue;
491
- }
492
- }
493
- }
494
- }
495
- catch {
496
- // agents dir unreadable — fall back to main-only usage
497
- }
498
- // Build the final AgentSessionUsage from the last turn's flow + session
499
- // cumulative + main contextLength.
500
- const t = acc.totals;
501
- // 有 turn.prompt 时本 run = 末个 turn 的 flow;末 turn 还没有记录时
502
- // turnAcc.count = 0 → flow = 0。整份 wire 都没有 turn.prompt(旧数据/
503
- // 截断)时无从分段,沿用「整份文件 = 一个 turn」的旧口径(含子 agent wire)。
504
- const flow = turnAcc ? turnAcc.totals : t;
505
- // Kimi semantics: preserve 0 values (not undefined) for cache fields to match original
506
- const totalUsage = {
507
- inputTokens: flow.input,
508
- outputTokens: flow.output,
509
- contextLength: mainContextLength,
510
- cacheReadTokens: flow.cacheRead > 0 ? flow.cacheRead : 0,
511
- cacheCreationTokens: flow.cacheCreation > 0 ? flow.cacheCreation : 0,
512
- totalTokens: flow.input + flow.output + flow.cacheRead + flow.cacheCreation,
513
- // Cumulative (session-wide): sums every usage.record across all turns
514
- // (subagent wires included) — the "累计" line, never the per-run value.
515
- ...cumulativeUsageFields(t),
516
- // §6.3: compaction stats from wire.jsonl
517
- compactCount,
518
- ...(compactPreContextLength !== undefined ? { compactPreContextLength } : {}),
519
- };
465
+ const subUsage = scanKimiSubagentUsage(agentsDir);
520
466
  return {
521
- events: capEvents(events, maxEvents),
522
- usage: hasUsageRecord ? totalUsage : undefined,
467
+ events: capEvents(events, opts?.maxEvents),
468
+ usage: mainUsage.hasUsageRecord || subUsage.hasUsageRecord
469
+ ? assembleKimiUsage(mainUsage, subUsage)
470
+ : undefined,
523
471
  displayTitle,
524
472
  };
525
473
  }
@@ -704,9 +652,133 @@ export class KimiSessionReader {
704
652
  // Consider the session inactive when its last loop event is step.end
705
653
  return !lastLoopEventIsStepEnd(lines);
706
654
  }
707
- catch {
655
+ catch (err) {
656
+ getLogger().debug('[kimi-session-reader] isSessionActive failed:', err);
708
657
  return false;
709
658
  }
710
659
  }
711
660
  }
661
+ // --- listSessions 装配原语(模块级命名函数 + 显式传参) ---
662
+ // 纯结构拆分:listSessions 只做 realpath / 遍历 / 排序分页 / 错误降级,
663
+ // 单条会话的 state 读取、cwd 守卫、summary 三级回退、mtime 回退全部下沉到
664
+ // 这些模块级函数。文案、跳过条件、排序键、分页口径逐项保持。
665
+ /** realpath 归一化失败(非 ENOENT)留痕;ENOENT 静默沿用原 cwd。 */
666
+ function logKimiRealpathFailure(cwd, err) {
667
+ logNonEnoentDebug(err, (m) => `[kimi-session-reader] realpath ${cwd} failed, using the path as given: ${m}`);
668
+ }
669
+ /** state.json 一次读取共用(cwd 兜底 + summary),I/O 减半。 */
670
+ function readKimiState(sessionDir, sessionId) {
671
+ const statePath = path.join(sessionDir, 'state.json');
672
+ if (!fs.existsSync(statePath))
673
+ return { state: null, stateParsed: false };
674
+ try {
675
+ return {
676
+ state: JSON.parse(fs.readFileSync(statePath, 'utf-8')),
677
+ stateParsed: true,
678
+ };
679
+ }
680
+ catch (err) {
681
+ // 解析失败 = 无法判 cwd → 不 continue,留给 index workDir 兜底。
682
+ getLogger().debug(`[kimi-session-reader] corrupt state.json skipped session=${sessionId}: ${err?.message ?? String(err)}`);
683
+ return { state: null, stateParsed: false };
684
+ }
685
+ }
686
+ /** index workDir 优先,缺失时回退 state.json 的 workDir/cwd。 */
687
+ function resolveKimiEntryWorkDir(entry, read) {
688
+ let entryWorkDir = entry.workDir;
689
+ if (!entryWorkDir && read.stateParsed && read.state) {
690
+ entryWorkDir = extractWorkDir(read.state);
691
+ }
692
+ return entryWorkDir;
693
+ }
694
+ /**
695
+ * 从 wire.jsonl 末段提取最后一条 turn.prompt。
696
+ * v2 state.json 省略 title/lastPrompt 时作为 summary 兜底;只扫尾部 50 行。
697
+ */
698
+ function extractLastPromptFromWire(wirePath) {
699
+ try {
700
+ if (!fs.existsSync(wirePath))
701
+ return undefined;
702
+ // Scan backwards from the tail — turn.prompt is typically near the end
703
+ // of a completed turn (before content.part / step.end / usage.record).
704
+ const lines = readLastNJsonlLines(wirePath, 50);
705
+ let lastPrompt;
706
+ for (const line of lines) {
707
+ try {
708
+ const entry = JSON.parse(line);
709
+ if (isTurnPrompt(entry)) {
710
+ if (entry.input && entry.input.length > 0) {
711
+ lastPrompt = entry.input[0].text;
712
+ }
713
+ }
714
+ }
715
+ catch {
716
+ continue;
717
+ }
718
+ }
719
+ return lastPrompt?.substring(0, 200);
720
+ }
721
+ catch {
722
+ return undefined;
723
+ }
724
+ }
725
+ /** summary:state.title / lastPrompt → wire turn.prompt → 'New Session'。 */
726
+ function kimiSessionSummary(sessionDir, read) {
727
+ let summary = 'New Session';
728
+ if (read.stateParsed && read.state) {
729
+ // v1: state.title / state.lastPrompt; v2: these fields may be absent
730
+ summary = read.state.title || read.state.lastPrompt || '';
731
+ }
732
+ if (!summary) {
733
+ const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
734
+ summary = extractLastPromptFromWire(wirePath) || 'New Session';
735
+ }
736
+ return summary;
737
+ }
738
+ /**
739
+ * 排序用 mtime:wire.jsonl 优先(run 期间持续写入),缺失回退会话目录
740
+ * mtime;目录也不可读(非 ENOENT 留痕)返回 null,由调用方跳过该条。
741
+ */
742
+ function kimiSessionMtime(sessionDir) {
743
+ const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
744
+ try {
745
+ return fs.statSync(wirePath).mtimeMs;
746
+ }
747
+ catch {
748
+ try {
749
+ return fs.statSync(sessionDir).mtimeMs;
750
+ }
751
+ catch (err) {
752
+ if (err.code !== 'ENOENT') {
753
+ getLogger().warn(`[kimi-session-reader] cannot stat ${sessionDir}: ${err?.message ?? String(err)}`);
754
+ }
755
+ // 会话目录已不存在(或被清理):跳过该条,别让它拖垮整张列表。
756
+ return null;
757
+ }
758
+ }
759
+ }
760
+ /** 单条 index entry → 列表行;cwd 不匹配 / 目录不可读时返回 null(跳过)。 */
761
+ function collectKimiSession(entry, realCwd) {
762
+ const read = readKimiState(entry.sessionDir, entry.sessionId);
763
+ const entryWorkDir = resolveKimiEntryWorkDir(entry, read);
764
+ if (entryWorkDir === undefined || !samePath(entryWorkDir, realCwd)) {
765
+ return null;
766
+ }
767
+ const summary = kimiSessionSummary(entry.sessionDir, read);
768
+ const mtimeMs = kimiSessionMtime(entry.sessionDir);
769
+ if (mtimeMs === null)
770
+ return null;
771
+ return {
772
+ sessionId: entry.sessionId,
773
+ summary,
774
+ mtime: mtimeMs,
775
+ };
776
+ }
777
+ /** 排序键(模块级:不把回调决策点算回 listSessions)。 */
778
+ function kimiRowMtime(s) {
779
+ return s.mtime;
780
+ }
781
+ function kimiRowId(s) {
782
+ return s.sessionId;
783
+ }
712
784
  //# sourceMappingURL=sessions.js.map