lark-remote 0.5.18 → 0.5.20

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 (123) hide show
  1. package/CHANGELOG.md +32 -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 +1 -1
  10. package/dist/bridge/index.d.ts +120 -119
  11. package/dist/bridge/index.js +257 -695
  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 +109 -97
  18. package/dist/card/bash-card-session.js +2 -0
  19. package/dist/card/card-budget.js +1 -1
  20. package/dist/card/card-helpers.d.ts +3 -3
  21. package/dist/card/card-helpers.js +6 -6
  22. package/dist/card/card-session.d.ts +17 -0
  23. package/dist/card/card-session.js +35 -0
  24. package/dist/card/final-answer-file.js +1 -1
  25. package/dist/card/run-card-session.js +2 -0
  26. package/dist/card/run-renderer.d.ts +3 -3
  27. package/dist/card/run-renderer.js +77 -102
  28. package/dist/card/run-state.d.ts +35 -8
  29. package/dist/card/run-state.js +117 -90
  30. package/dist/card/tool-render.d.ts +1 -1
  31. package/dist/card/tool-render.js +1 -1
  32. package/dist/card/usage-format.d.ts +12 -0
  33. package/dist/card/usage-format.js +8 -3
  34. package/dist/cli.js +15 -3
  35. package/dist/clone.js +2 -2
  36. package/dist/config/codex-config.js +130 -106
  37. package/dist/config/index.d.ts +16 -2
  38. package/dist/config/index.js +23 -8
  39. package/dist/connector/index.js +6 -6
  40. package/dist/error-classification.js +3 -2
  41. package/dist/inbound/placeholder.d.ts +1 -1
  42. package/dist/inbound/placeholder.js +1 -1
  43. package/dist/index.js +203 -133
  44. package/dist/logger/index.js +6 -6
  45. package/dist/message-handlers.d.ts +0 -5
  46. package/dist/message-handlers.js +4 -2
  47. package/dist/model-catalog.d.ts +2 -2
  48. package/dist/model-catalog.js +3 -8
  49. package/dist/platform/identity.js +1 -1
  50. package/dist/platform/path.d.ts +1 -1
  51. package/dist/platform/path.js +1 -1
  52. package/dist/platform/sleep-blocker.js +1 -1
  53. package/dist/platform/terminator-win32.js +2 -1
  54. package/dist/router/commands/resume.js +2 -20
  55. package/dist/router/config/dsh.d.ts +4 -8
  56. package/dist/router/config/dsh.js +3 -10
  57. package/dist/router/config/kimi.js +1 -1
  58. package/dist/router/config/types.d.ts +1 -1
  59. package/dist/router/config-diff.js +1 -1
  60. package/dist/router/index.d.ts +56 -11
  61. package/dist/router/index.js +222 -202
  62. package/dist/runner/claude/session.d.ts +1 -1
  63. package/dist/runner/claude/session.js +6 -4
  64. package/dist/runner/codex/app-server/runner.d.ts +9 -2
  65. package/dist/runner/codex/app-server/runner.js +30 -25
  66. package/dist/runner/codex/app-server/translator.d.ts +0 -5
  67. package/dist/runner/codex/app-server/translator.js +4 -17
  68. package/dist/runner/common/acp/acp-approval.d.ts +1 -1
  69. package/dist/runner/common/acp/acp-approval.js +1 -1
  70. package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
  71. package/dist/runner/common/acp/base-acp-runner.js +26 -65
  72. package/dist/runner/common/acp/base-translator.d.ts +12 -2
  73. package/dist/runner/common/acp/base-translator.js +31 -9
  74. package/dist/runner/common/acp/protocol-types.d.ts +1 -1
  75. package/dist/runner/common/acp/protocol-types.js +2 -2
  76. package/dist/runner/common/connection-based-runner.d.ts +32 -1
  77. package/dist/runner/common/connection-based-runner.js +32 -2
  78. package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
  79. package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
  80. package/dist/runner/common/spawning-runner.d.ts +1 -1
  81. package/dist/runner/common/spawning-runner.js +1 -1
  82. package/dist/runner/dsh/catalog-probe.d.ts +17 -0
  83. package/dist/runner/dsh/catalog-probe.js +10 -0
  84. package/dist/runner/dsh/client.d.ts +13 -8
  85. package/dist/runner/dsh/client.js +23 -15
  86. package/dist/runner/dsh/exit-cancel.d.ts +2 -2
  87. package/dist/runner/dsh/exit-cancel.js +2 -2
  88. package/dist/runner/dsh/inflight.d.ts +1 -1
  89. package/dist/runner/dsh/inflight.js +1 -1
  90. package/dist/runner/dsh/runner.d.ts +48 -2
  91. package/dist/runner/dsh/runner.js +288 -199
  92. package/dist/runner/dsh/translator.d.ts +1 -1
  93. package/dist/runner/dsh/translator.js +1 -1
  94. package/dist/runner/index.d.ts +1 -1
  95. package/dist/runner/kimi/acp/runner.d.ts +1 -2
  96. package/dist/runner/kimi/acp/runner.js +1 -2
  97. package/dist/runner/kimi/acp/translator.d.ts +1 -1
  98. package/dist/runner/kimi/acp/translator.js +3 -4
  99. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  100. package/dist/runner/opencode/acp/runner.js +4 -4
  101. package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
  102. package/dist/runner/pi/rpc/protocol-types.js +1 -1
  103. package/dist/runner/pi/rpc/runner.d.ts +3 -0
  104. package/dist/runner/pi/rpc/runner.js +27 -42
  105. package/dist/runner/types.d.ts +25 -8
  106. package/dist/runner/types.js +1 -1
  107. package/dist/session/claude/sessions.d.ts +45 -0
  108. package/dist/session/claude/sessions.js +97 -71
  109. package/dist/session/codex/rollout-reader.js +4 -4
  110. package/dist/session/common/throw-session-list-error.d.ts +10 -0
  111. package/dist/session/common/throw-session-list-error.js +16 -0
  112. package/dist/session/dsh/sessions.js +11 -5
  113. package/dist/session/kimi/sessions.d.ts +40 -1
  114. package/dist/session/kimi/sessions.js +244 -192
  115. package/dist/session/opencode/sessions.d.ts +153 -1
  116. package/dist/session/opencode/sessions.js +354 -321
  117. package/dist/session/pi/sessions.js +4 -5
  118. package/dist/session/session-store.d.ts +1 -1
  119. package/dist/session/session-store.js +1 -1
  120. package/dist/update/version-check.js +2 -1
  121. package/dist/workspace/index.d.ts +1 -1
  122. package/dist/workspace/index.js +1 -1
  123. package/package.json +1 -1
@@ -211,8 +211,12 @@ export class DshSessionReader {
211
211
  return { sessions: page, total };
212
212
  }
213
213
  catch (error) {
214
- getLogger().error(`[dsh-session-reader] listSessions error: ${error.message}`);
215
- return { sessions: [], total: 0 };
214
+ // 远端 RPC 没有真空态:daemon 未启动 / cookie 失效 / 超时全是故障态。
215
+ // error 日志 + 上抛,router 显示「列表失败」——静默 [] 会让用户误以为
216
+ // 没有 session,与真空不可区分(与 kimi/opencode 同口径)。
217
+ getLogger().error(`[dsh-session-reader] listSessions failed: ${error}`);
218
+ const msg = error instanceof Error ? error.message : String(error);
219
+ throw new Error(`dsh session list 读取失败: ${msg}`, { cause: error });
216
220
  }
217
221
  }
218
222
  getNewestSession(cwd) {
@@ -358,7 +362,9 @@ export class DshSessionReader {
358
362
  };
359
363
  }
360
364
  catch (error) {
361
- getLogger().error(`[dsh-session-reader] readSessionContent error: ${error.message}`);
365
+ // 降级为空卡(与 claude/pi 同口径),但完整错误对象入日志保留 stack,
366
+ // 否则 wire 截断 / 权限问题只剩一句 message 无法定位。
367
+ getLogger().error(`[dsh-session-reader] readSessionContent failed: ${error}`);
362
368
  return { events: [] };
363
369
  }
364
370
  }
@@ -367,7 +373,7 @@ export class DshSessionReader {
367
373
  return this.resolveSession(sessionId, cwd)?.running === true;
368
374
  }
369
375
  catch (error) {
370
- getLogger().error(`[dsh-session-reader] isSessionActive error: ${error.message}`);
376
+ getLogger().error(`[dsh-session-reader] isSessionActive failed: ${error}`);
371
377
  return false;
372
378
  }
373
379
  }
@@ -535,7 +541,7 @@ function mapEvent(ev) {
535
541
  const timestamp = safeEventTimestamp(ev.time);
536
542
  // DSH 的块类型是 text / reasoning / tool-call(不是 Anthropic 的
537
543
  // thinking)。只认 text 会让 resume 卡"只有工具调用、没有思考与输出"
538
- // (2026-10-19 反馈:一个 31 turn 的会话里 reasoning 块 371 个、text 12 个,
544
+ // (一个 31 turn 的会话里 reasoning 块 371 个、text 12 个,
539
545
  // 全被丢掉)。tool-call 块不在这里取——独立的 `tool/call` 事件带完整参数。
540
546
  for (const block of content) {
541
547
  const text = typeof block.text === 'string' ? block.text.trim() : '';
@@ -12,7 +12,7 @@ export interface KimiSessionState {
12
12
  /**
13
13
  * Compaction completion record in wire.jsonl (§6.3 / kimi-code wire-manifest).
14
14
  *
15
- * REAL shape (2026-08-16 实弹 wire.jsonl + kimi-code 源码):
15
+ * REAL shape(真实 wire.jsonl + kimi-code 源码):
16
16
  * - `context.apply_compaction`:带 `compactedCount` / `tokensBefore` /
17
17
  * `tokensAfter`(以及 summary 等);
18
18
  * - `full_compaction.complete`:只有 `{type, time}`。
@@ -50,6 +50,39 @@ export type CwdGuardResult = 'verified' | 'failed' | 'unverifiable';
50
50
  * cwd guard semantics.
51
51
  */
52
52
  export declare function checkCwdGuard(state: KimiSessionState, realCwd: string): CwdGuardResult;
53
+ /** 逐字段 token 累计(usage.record 的四个分量;缺字段按 0 计)。 */
54
+ interface KimiUsageTotals {
55
+ input: number;
56
+ output: number;
57
+ cacheRead: number;
58
+ cacheCreation: number;
59
+ }
60
+ /** kimi 主 wire usage 扫描累积器结果(纯数据)。 */
61
+ export interface KimiWireUsageScan {
62
+ /** 会话累计:主 wire 全部 usage.record 逐字段求和(子 agent 由调用方并入)。 */
63
+ totals: KimiUsageTotals;
64
+ /**
65
+ * 末个 turn(最后一个 turn.prompt 之后)的主 wire 记录之和(本 run flow);
66
+ * 整份 wire 无 turn.prompt(旧数据/截断)= undefined,由装配段回退全会话。
67
+ */
68
+ turn: KimiUsageTotals | undefined;
69
+ /** 主 wire 末条 usage.record 的上下文水位(§9.21;子 agent 独立窗口不进)。 */
70
+ mainContextLength: number;
71
+ /** §6.3 展示计数:只数 context.apply_compaction(一次手动 /compact 只写一条)。 */
72
+ compactCount: number;
73
+ /** 最近一次压缩的 tokensBefore 水位。 */
74
+ compactPreContextLength: number | undefined;
75
+ /** 主 wire 出现过 usage.record(畸形 record 也算有数据,全零照实返回)。 */
76
+ hasUsageRecord: boolean;
77
+ }
78
+ /**
79
+ * kimi 主 wire usage 扫描累积器(纯函数):单遍解析,产出会话累计、末 turn
80
+ * flow、上下文水位与压缩统计。
81
+ *
82
+ * 产生:KimiWireUsageScan。依赖:入参 lines(已解析前的原始行)。不改任何
83
+ * 外部状态;畸形行跳过,畸形 usage.record 按字段 0 计但记为「有数据」。
84
+ */
85
+ export declare function scanKimiUsage(lines: readonly string[]): KimiWireUsageScan;
53
86
  /**
54
87
  * Kimi Session Reader
55
88
  * Reads session history from ~/.kimi-code/
@@ -89,6 +122,11 @@ export declare class KimiSessionReader implements AgentSessionReader {
89
122
  getNewestSession(cwd: string): AgentSession | null;
90
123
  /**
91
124
  * Read session content (events + usage)
125
+ *
126
+ * 编排:cwd 守卫定位主 wire(resolveSessionWirePath,与 isSessionActive
127
+ * 共用 verifySessionCwd 单源)→ 主 wire 两遍只读扫描(scanKimiEvents 出
128
+ * 展示流 + displayTitle,scanKimiUsage 出累计/flow/水位/压缩统计)→ 子
129
+ * agent wire 只进 usage 累计 → assembleKimiUsage 组装。
92
130
  */
93
131
  readSessionContent(sessionId: string, cwd: string, opts?: {
94
132
  maxEvents?: number;
@@ -130,3 +168,4 @@ export declare class KimiSessionReader implements AgentSessionReader {
130
168
  */
131
169
  isSessionActive(sessionId: string, cwd: string): boolean;
132
170
  }
171
+ export {};
@@ -2,7 +2,8 @@ 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';
7
8
  import { isStale } from '../common/constants.js';
8
9
  import { safeRealpath, samePath } from '../../platform/path.js';
@@ -134,6 +135,233 @@ export function checkCwdGuard(state, realCwd) {
134
135
  return 'verified';
135
136
  return 'failed';
136
137
  }
138
+ function emptyKimiTotals() {
139
+ return { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
140
+ }
141
+ /**
142
+ * kimi 主 wire usage 扫描累积器(纯函数):单遍解析,产出会话累计、末 turn
143
+ * flow、上下文水位与压缩统计。
144
+ *
145
+ * 产生:KimiWireUsageScan。依赖:入参 lines(已解析前的原始行)。不改任何
146
+ * 外部状态;畸形行跳过,畸形 usage.record 按字段 0 计但记为「有数据」。
147
+ */
148
+ export function scanKimiUsage(lines) {
149
+ const totals = emptyKimiTotals();
150
+ let turn;
151
+ // contextLength is only from the main wire's last usage.record
152
+ // (subagent context is an independent window).
153
+ let mainContextLength = 0;
154
+ // §6.3: Compaction stats — count compact events, track last pre-compact context.
155
+ let compactCount = 0;
156
+ let compactPreContextLength;
157
+ // Distinguish "no usage.record in any wire file" (no data → usage:
158
+ // undefined, downstream omits the token block) from "records exist but
159
+ // are all zero / malformed" (real data → aggregate with ?? 0 and return
160
+ // the zeros as-is). A malformed record (usage.record line with missing
161
+ // usage object/fields) still counts as data.
162
+ let hasUsageRecord = false;
163
+ for (const line of lines) {
164
+ let entry;
165
+ try {
166
+ entry = JSON.parse(line);
167
+ }
168
+ catch {
169
+ continue;
170
+ }
171
+ if (isUsageRecord(entry)) {
172
+ hasUsageRecord = true;
173
+ // Malformed records (missing usage object or missing fields) must
174
+ // not poison the accumulators with NaN: count missing fields as 0.
175
+ const inputOther = entry.usage?.inputOther ?? 0;
176
+ const output = entry.usage?.output ?? 0;
177
+ const inputCacheRead = entry.usage?.inputCacheRead ?? 0;
178
+ const inputCacheCreation = entry.usage?.inputCacheCreation ?? 0;
179
+ totals.input += inputOther;
180
+ totals.output += output;
181
+ totals.cacheRead += inputCacheRead;
182
+ totals.cacheCreation += inputCacheCreation;
183
+ if (turn) {
184
+ turn.input += inputOther;
185
+ turn.output += output;
186
+ turn.cacheRead += inputCacheRead;
187
+ turn.cacheCreation += inputCacheCreation;
188
+ }
189
+ // unified contract (§9.21): input + output + cacheRead + cacheCreation.
190
+ // 末轮 output 已进入历史、下一 prompt 全量携带——与 compact postTokens
191
+ // 水位同语义(此前漏加 output 会系统性低估长收尾 turn)。
192
+ mainContextLength = inputOther + output + inputCacheRead + inputCacheCreation;
193
+ continue;
194
+ }
195
+ // §6.3: Count compaction events and track pre-compact context length.
196
+ // 展示计数只计 context.apply_compaction:一次手动 /compact 会同时写
197
+ // full_compaction.complete + context.apply_compaction 两条(2026-08-16
198
+ // 实弹 wire.jsonl),双计会把一次压缩虚报成 2 次。full_compaction.complete
199
+ // 仅作 runCompact 轮询的完成信号(readCompactionRecords 仍返回两种)。
200
+ if (isCompactionComplete(entry) && entry.type === 'context.apply_compaction') {
201
+ compactCount++;
202
+ // Capture context length before this compaction as the pre-compact
203
+ // watermark (the most recent one is most relevant for display).
204
+ if (entry.tokensBefore !== undefined) {
205
+ compactPreContextLength = entry.tokensBefore;
206
+ }
207
+ continue;
208
+ }
209
+ // turn.prompt 是 turn 的起点:本 turn 的 flow 从这里重新累计。
210
+ if (isTurnPrompt(entry)) {
211
+ turn = emptyKimiTotals();
212
+ continue;
213
+ }
214
+ }
215
+ return { totals, turn, mainContextLength, compactCount, compactPreContextLength, hasUsageRecord };
216
+ }
217
+ /**
218
+ * kimi 子 agent wire usage 扫描(只读 I/O):agents/<name>/wire.jsonl(main
219
+ * 以外的兄弟目录)与主会话同属同一计费实体,token 消耗必须并入会话累计;
220
+ * contextLength/展示事件不取子 agent。目录不可读或文件缺失静默跳过。
221
+ *
222
+ * 产生:KimiSubagentUsageScan。依赖:文件系统(agentsDir 只读)。不改外部状态。
223
+ */
224
+ function scanKimiSubagentUsage(agentsDir) {
225
+ const totals = emptyKimiTotals();
226
+ let hasUsageRecord = false;
227
+ try {
228
+ for (const dirent of fs.readdirSync(agentsDir, { withFileTypes: true })) {
229
+ if (!dirent.isDirectory() || dirent.name === 'main') {
230
+ continue;
231
+ }
232
+ const subWirePath = path.join(agentsDir, dirent.name, 'wire.jsonl');
233
+ if (!fs.existsSync(subWirePath)) {
234
+ continue;
235
+ }
236
+ for (const subLine of readJsonlLines(subWirePath)) {
237
+ try {
238
+ const subEntry = JSON.parse(subLine);
239
+ if (isUsageRecord(subEntry)) {
240
+ hasUsageRecord = true;
241
+ totals.input += subEntry.usage?.inputOther ?? 0;
242
+ totals.output += subEntry.usage?.output ?? 0;
243
+ totals.cacheRead += subEntry.usage?.inputCacheRead ?? 0;
244
+ totals.cacheCreation += subEntry.usage?.inputCacheCreation ?? 0;
245
+ }
246
+ }
247
+ catch {
248
+ continue;
249
+ }
250
+ }
251
+ }
252
+ }
253
+ catch {
254
+ // agents dir unreadable — fall back to main-only usage
255
+ }
256
+ return { totals, hasUsageRecord };
257
+ }
258
+ /**
259
+ * kimi usage 装配(tail 装配段,纯函数):末 turn flow + 会话累计(主 wire +
260
+ * 子 agent)+ 主 wire 水位 + 压缩统计 → AgentSessionUsage。
261
+ *
262
+ * 有 turn.prompt 时本 run = 末个 turn 的 flow;末 turn 还没有记录时
263
+ * turn 为全 0。整份 wire 都没有 turn.prompt(旧数据/截断)时无从分段,
264
+ * 沿用「整份文件 = 一个 turn」的旧口径(含子 agent wire)。
265
+ */
266
+ function assembleKimiUsage(main, sub) {
267
+ const totals = {
268
+ input: main.totals.input + sub.totals.input,
269
+ output: main.totals.output + sub.totals.output,
270
+ cacheRead: main.totals.cacheRead + sub.totals.cacheRead,
271
+ cacheCreation: main.totals.cacheCreation + sub.totals.cacheCreation,
272
+ };
273
+ const flow = main.turn ?? totals;
274
+ // Kimi semantics: preserve 0 values (not undefined) for cache fields to match original
275
+ return {
276
+ inputTokens: flow.input,
277
+ outputTokens: flow.output,
278
+ contextLength: main.mainContextLength,
279
+ cacheReadTokens: flow.cacheRead > 0 ? flow.cacheRead : 0,
280
+ cacheCreationTokens: flow.cacheCreation > 0 ? flow.cacheCreation : 0,
281
+ totalTokens: flow.input + flow.output + flow.cacheRead + flow.cacheCreation,
282
+ // Cumulative (session-wide): sums every usage.record across all turns
283
+ // (subagent wires included) — the "累计" line, never the per-run value.
284
+ ...cumulativeUsageFields(totals),
285
+ // §6.3: compaction stats from wire.jsonl
286
+ compactCount: main.compactCount,
287
+ ...(main.compactPreContextLength !== undefined
288
+ ? { compactPreContextLength: main.compactPreContextLength }
289
+ : {}),
290
+ };
291
+ }
292
+ /**
293
+ * kimi 主 wire 展示装配(纯函数):turn.prompt 提取 displayTitle,loop event
294
+ * 映射为展示事件。tool.call/tool.result 是**独立的 loop event**(对照真实
295
+ * wire.jsonl 验证过的结构,勿合并/改形)。
296
+ *
297
+ * 产生:KimiEventScan。依赖:入参 lines。不改任何外部状态。
298
+ */
299
+ function scanKimiEvents(lines) {
300
+ const events = [];
301
+ let displayTitle;
302
+ for (const line of lines) {
303
+ let entry;
304
+ try {
305
+ entry = JSON.parse(line);
306
+ }
307
+ catch {
308
+ continue;
309
+ }
310
+ // displayTitle + display events now scan ALL lines (not a
311
+ // line-slice), then `events.slice(-maxEvents)` keeps the LAST N
312
+ // EVENTS. Previously `lines.slice(-maxEvents)` cut raw lines, but
313
+ // usage.record / step.begin / turn.prompt lines produce no display
314
+ // event, so "last 5 lines" could yield only 1~2 visible events —
315
+ // and `slice(-0)` returned the full array. displayTitle takes the
316
+ // last turn.prompt across the whole session (the most recent user
317
+ // prompt), which is the correct catch-up title.
318
+ // Extract display title from turn.prompt
319
+ if (isTurnPrompt(entry)) {
320
+ if (entry.input && entry.input.length > 0) {
321
+ displayTitle = entry.input[0].text;
322
+ }
323
+ continue;
324
+ }
325
+ // Convert loop events to content events
326
+ if (isAppendLoopEvent(entry)) {
327
+ const evt = entry.event;
328
+ if (evt.type === 'content.part' && evt.part) {
329
+ if (evt.part.type === 'think') {
330
+ // Skip thinking blocks for display
331
+ continue;
332
+ }
333
+ else if (evt.part.type === 'text' && evt.part.text) {
334
+ events.push({
335
+ type: 'text',
336
+ content: evt.part.text,
337
+ });
338
+ }
339
+ }
340
+ else if (evt.type === 'tool.call' && evt.name) {
341
+ // 复用 content-blocks 的 200 字符截断:大 args(整段文件内容
342
+ // 写入、长 prompt)原文进事件会把 resume 卡顶到 28KB 预算之外
343
+ // (2026-08-08 extreme_fallback 故障的推手之一)。
344
+ const inputStr = truncateToolInput(JSON.stringify(evt.args ?? {}));
345
+ events.push({
346
+ type: 'tool_use',
347
+ content: `${evt.name}(${inputStr})`,
348
+ });
349
+ }
350
+ else if (evt.type === 'tool.result') {
351
+ // result is { output }, { output, note }, or { isError, output }
352
+ // 对齐 opencode reader 的预折叠:单条 tool_result 上限
353
+ // TOOL_RESULT_MAX_BYTES,病理级输出(如 51KB 文件读取)不会
354
+ // 撑爆 resume 卡,也约束 events[] 内存占用。
355
+ const rawOut = evt.result?.output ?? `[tool result for ${evt.toolCallId}]`;
356
+ events.push({
357
+ type: 'tool_result',
358
+ content: truncateUtf8(rawOut, TOOL_RESULT_MAX_BYTES),
359
+ });
360
+ }
361
+ }
362
+ }
363
+ return { events, displayTitle };
364
+ }
137
365
  /**
138
366
  * Kimi Session Reader
139
367
  * Reads session history from ~/.kimi-code/
@@ -302,9 +530,7 @@ export class KimiSessionReader {
302
530
  if (error.code === 'ENOENT') {
303
531
  return { sessions: [], total: 0 };
304
532
  }
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 });
533
+ throwSessionListError('kimi', error);
308
534
  }
309
535
  }
310
536
  /**
@@ -315,6 +541,11 @@ export class KimiSessionReader {
315
541
  }
316
542
  /**
317
543
  * Read session content (events + usage)
544
+ *
545
+ * 编排:cwd 守卫定位主 wire(resolveSessionWirePath,与 isSessionActive
546
+ * 共用 verifySessionCwd 单源)→ 主 wire 两遍只读扫描(scanKimiEvents 出
547
+ * 展示流 + displayTitle,scanKimiUsage 出累计/flow/水位/压缩统计)→ 子
548
+ * agent wire 只进 usage 累计 → assembleKimiUsage 组装。
318
549
  */
319
550
  readSessionContent(sessionId, cwd, opts) {
320
551
  const wirePath = this.resolveSessionWirePath(sessionId, cwd);
@@ -323,144 +554,8 @@ export class KimiSessionReader {
323
554
  }
324
555
  try {
325
556
  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
- }
557
+ const { events, displayTitle } = scanKimiEvents(lines);
558
+ const mainUsage = scanKimiUsage(lines);
464
559
  // Subagent wire files (agents/<name>/wire.jsonl, e.g. Task-derived
465
560
  // agent-0..N) belong to the same session and the same billing entity:
466
561
  // their token cost must be summed into the session-wide usage.
@@ -470,56 +565,12 @@ export class KimiSessionReader {
470
565
  // wirePath = <sessionDir>/agents/main/wire.jsonl → agents dir is two
471
566
  // levels up (siblings of main hold subagent wires).
472
567
  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
- };
568
+ const subUsage = scanKimiSubagentUsage(agentsDir);
520
569
  return {
521
- events: capEvents(events, maxEvents),
522
- usage: hasUsageRecord ? totalUsage : undefined,
570
+ events: capEvents(events, opts?.maxEvents),
571
+ usage: mainUsage.hasUsageRecord || subUsage.hasUsageRecord
572
+ ? assembleKimiUsage(mainUsage, subUsage)
573
+ : undefined,
523
574
  displayTitle,
524
575
  };
525
576
  }
@@ -704,7 +755,8 @@ export class KimiSessionReader {
704
755
  // Consider the session inactive when its last loop event is step.end
705
756
  return !lastLoopEventIsStepEnd(lines);
706
757
  }
707
- catch {
758
+ catch (err) {
759
+ getLogger().debug('[kimi-session-reader] isSessionActive failed:', err);
708
760
  return false;
709
761
  }
710
762
  }