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
@@ -22,6 +22,7 @@ import os from 'node:os';
22
22
  import path from 'node:path';
23
23
  import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
24
24
  import { isStale } from '../common/constants.js';
25
+ import { throwSessionListError } from '../common/throw-session-list-error.js';
25
26
  import { safeRealpath, samePath } from '../../platform/path.js';
26
27
  import { paginate, capEvents } from '../common/pagination.js';
27
28
  import { newestOf, sortByRecencyDesc } from '../common/recency.js';
@@ -165,6 +166,325 @@ function withCumulativeUsage(usage, cum) {
165
166
  cumulativeCacheCreationTokens: cum.cacheWrite,
166
167
  };
167
168
  }
169
+ function emptyFlow() {
170
+ return { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, total: 0 };
171
+ }
172
+ /**
173
+ * v1 扫描累积器(纯函数):单遍扫过全部 export 消息,产出 catch-up tail 的
174
+ * 位置(末条 user 消息)、本 run flow(tail 内非零 step-finish 求和)、末条
175
+ * 非零采样(contextLength 水位)与全会话累计。
176
+ *
177
+ * 产生:OpencodeV1Scan。依赖:入参 messages。不改任何外部状态。
178
+ *
179
+ * 口径(勿改):cumulative 对**所有** step-finish 逐字段求和、不设零 total
180
+ * 门槛;turn/latest 只认 tail 内非零 total 的 step(模型在 reasoning 中被
181
+ * 截断的退化 step 不算采样)。
182
+ */
183
+ export function scanV1Messages(messages) {
184
+ // Find the last user message position (for catch-up tail).
185
+ // messages are already parsed objects — scan directly.
186
+ let lastUserIndex = -1;
187
+ for (let i = 0; i < messages.length; i++) {
188
+ if (messages[i].info.role === 'user') {
189
+ lastUserIndex = i;
190
+ }
191
+ }
192
+ // Process messages from last user message onwards (catch-up tail)
193
+ const startIndex = lastUserIndex >= 0 ? lastUserIndex : 0;
194
+ const turn = emptyFlow();
195
+ const cum = emptyFlow();
196
+ let latest;
197
+ let tailDisplayTitle;
198
+ for (let i = 0; i < messages.length; i++) {
199
+ const inTail = i >= startIndex;
200
+ const msg = messages[i];
201
+ // Track usage from step-finish parts. v1 没有 idle 标记,turn 边界就是
202
+ // 末条 user 消息;同一 turn 可能有多个 step-finish(每次模型请求一条),
203
+ // flow 要全加起来,只留末条会把多步 run 低报。
204
+ for (const part of msg.parts) {
205
+ if (part.type === 'step-finish' && part.tokens) {
206
+ // cumulative:全会话求和,含零 total 的退化 step(字段为 0 时求和不
207
+ // 变,与拆分前行为一致)。
208
+ cum.input += part.tokens.input ?? 0;
209
+ cum.output += part.tokens.output ?? 0;
210
+ cum.total += part.tokens.total ?? 0;
211
+ cum.cacheRead += part.tokens.cache?.read ?? 0;
212
+ cum.cacheCreation += part.tokens.cache?.write ?? 0;
213
+ if (!inTail)
214
+ continue;
215
+ // Skip zero-total steps(模型在 reasoning 中被截断)——它们没有消耗,
216
+ // 也不该把本次请求当成一条真实采样。
217
+ // ccusage-aligned: cache.write -> cacheCreationTokens.
218
+ // contextLength = input + output + cacheRead + cacheCreation —
219
+ // the unified context-window occupancy contract across all readers
220
+ // (§9.21:末轮 output 已进入历史、下一 prompt 全量携带,与 compact
221
+ // postTokens 水位同语义).
222
+ if ((part.tokens.total ?? 0) === 0)
223
+ continue;
224
+ const input = part.tokens.input ?? 0;
225
+ const output = part.tokens.output ?? 0;
226
+ const cacheRead = part.tokens.cache?.read ?? 0;
227
+ const cacheWrite = part.tokens.cache?.write ?? 0;
228
+ turn.input += input;
229
+ turn.output += output;
230
+ turn.cacheRead += cacheRead;
231
+ turn.cacheCreation += cacheWrite;
232
+ turn.total += part.tokens.total;
233
+ latest = {
234
+ inputTokens: input,
235
+ outputTokens: output,
236
+ contextLength: input + output + cacheRead + cacheWrite,
237
+ cacheReadTokens: part.tokens.cache?.read,
238
+ cacheCreationTokens: part.tokens.cache?.write,
239
+ totalTokens: part.tokens.total,
240
+ };
241
+ }
242
+ }
243
+ // First user text after last user message is displayTitle
244
+ if (inTail && !tailDisplayTitle && msg.info.role === 'user') {
245
+ for (const part of msg.parts) {
246
+ if (part.type === 'text' && part.text) {
247
+ tailDisplayTitle = part.text;
248
+ break;
249
+ }
250
+ }
251
+ }
252
+ }
253
+ return { lastUserIndex, tailDisplayTitle, latest, turn, cum };
254
+ }
255
+ /**
256
+ * v1 catch-up tail 装配(纯函数):把 [startIndex, 末尾) 的消息映射为展示
257
+ * 事件(text / thinking / tool_use / tool_result)。tool 在同一 part 上先发
258
+ * use(有 callID+input)再发 result(有 callID+output)。
259
+ */
260
+ function assembleV1TailEvents(messages, startIndex) {
261
+ const events = [];
262
+ for (let i = startIndex; i < messages.length; i++) {
263
+ const msg = messages[i];
264
+ const timestamp = msg.info.time?.created
265
+ ? new Date(msg.info.time.created).toISOString()
266
+ : undefined;
267
+ for (const part of msg.parts) {
268
+ if (part.type === 'text') {
269
+ events.push({
270
+ type: 'text',
271
+ content: part.text ?? '',
272
+ timestamp,
273
+ });
274
+ }
275
+ else if (part.type === 'reasoning') {
276
+ events.push({
277
+ type: 'thinking',
278
+ content: part.text ?? '',
279
+ timestamp,
280
+ });
281
+ }
282
+ else if (part.type === 'tool') {
283
+ // Tool use
284
+ if (part.callID && part.state?.input) {
285
+ events.push({
286
+ type: 'tool_use',
287
+ content: JSON.stringify({
288
+ id: part.callID,
289
+ name: part.tool,
290
+ input: part.state.input,
291
+ }),
292
+ timestamp,
293
+ });
294
+ }
295
+ // Tool result (completed or error)
296
+ if (part.callID && part.state?.output) {
297
+ const isError = part.state.status === 'error';
298
+ const rawOut = isError
299
+ ? (part.state.error ?? part.state.output)
300
+ : (part.state.output ?? '');
301
+ // pre-fold pathological tool_results (e.g. a 500KB file
302
+ // listing) via the shared truncateUtf8 primitive, reusing the
303
+ // same …(已截断) suffix as the live renderer / card budget.
304
+ events.push({
305
+ type: 'tool_result',
306
+ content: truncateUtf8(rawOut, TOOL_RESULT_MAX_BYTES),
307
+ timestamp,
308
+ });
309
+ }
310
+ }
311
+ }
312
+ }
313
+ return events;
314
+ }
315
+ /** 全会话首条 user 文本(displayTitle 兜底;纯函数)。 */
316
+ function firstV1UserText(messages) {
317
+ for (const msg of messages) {
318
+ if (msg.info.role === 'user') {
319
+ for (const part of msg.parts) {
320
+ if (part.type === 'text' && part.text) {
321
+ return part.text;
322
+ }
323
+ }
324
+ }
325
+ }
326
+ return undefined;
327
+ }
328
+ /**
329
+ * usage 装配(v1/v2 共用,同 readSessionContent 内的既有共享粒度):
330
+ * flow 覆盖末条采样值(contextLength 保留末条——水位不求和),再落会话累计。
331
+ */
332
+ function finalizeTurnUsage(latest, turn, cum) {
333
+ if (latest) {
334
+ latest.inputTokens = turn.input;
335
+ latest.outputTokens = turn.output;
336
+ latest.cacheReadTokens = turn.cacheRead;
337
+ latest.cacheCreationTokens = turn.cacheCreation;
338
+ latest.totalTokens = turn.total;
339
+ }
340
+ return withCumulativeUsage(latest, {
341
+ total: cum.total,
342
+ input: cum.input,
343
+ output: cum.output,
344
+ cacheRead: cum.cacheRead,
345
+ cacheWrite: cum.cacheCreation,
346
+ });
347
+ }
348
+ /**
349
+ * v2 扫描累积器(纯函数):usageSample 已滤掉零 total 的退化消息;turn 边界
350
+ * 由 lastTurnStartIndex 判定(idle / user / synthetic / system,见其注释)。
351
+ *
352
+ * 产生:OpencodeV2UsageScan。依赖:入参 messages。不改任何外部状态。
353
+ */
354
+ export function scanV2Usage(messages) {
355
+ const turn = emptyFlow();
356
+ const cum = emptyFlow();
357
+ let latest;
358
+ // 本 turn(= 本 run)的 flow:末个 turn 内每次模型请求之和。contextLength
359
+ // 仍取末次请求的窗口水位(水位不可求和)。只取末条会把多步 run 低报
360
+ // (实测一条 25 请求的 run:input 20,040 显示成 543、output 10,358 显示成 1,250)。
361
+ const turnStart = lastTurnStartIndex(messages);
362
+ for (let i = 0; i < messages.length; i++) {
363
+ const sample = usageSample(messages[i]);
364
+ if (!sample)
365
+ continue;
366
+ cum.input += sample.input;
367
+ cum.output += sample.output;
368
+ cum.total += sample.total;
369
+ cum.cacheRead += sample.cacheRead;
370
+ cum.cacheCreation += sample.cacheCreation;
371
+ if (i > turnStart) {
372
+ turn.input += sample.input;
373
+ turn.output += sample.output;
374
+ turn.cacheRead += sample.cacheRead;
375
+ turn.cacheCreation += sample.cacheCreation;
376
+ turn.total += sample.total;
377
+ }
378
+ latest = {
379
+ inputTokens: sample.input,
380
+ outputTokens: sample.output,
381
+ // §9.21 口径:含末轮 output(与 postTokens 水位同语义)。
382
+ contextLength: sample.input + sample.output + sample.cacheRead + sample.cacheCreation,
383
+ cacheReadTokens: sample.cacheRead,
384
+ cacheCreationTokens: sample.cacheCreation,
385
+ totalTokens: sample.total,
386
+ };
387
+ }
388
+ return { latest, turn, cum };
389
+ }
390
+ /**
391
+ * v2 catch-up tail 装配(纯函数):末条 user 消息起的消息映射为展示事件
392
+ * (user / assistant / shell / compaction);displayTitle 取 tail 内首条
393
+ * user 文本,tail 无 user 文本(如附件-only)时回退全会话首条。
394
+ */
395
+ function assembleV2TailEvents(messages) {
396
+ const events = [];
397
+ let displayTitle;
398
+ let lastUserIndex = -1;
399
+ for (let i = 0; i < messages.length; i++) {
400
+ if (messages[i]?.type === 'user')
401
+ lastUserIndex = i;
402
+ }
403
+ const startIndex = lastUserIndex >= 0 ? lastUserIndex : 0;
404
+ for (let i = startIndex; i < messages.length; i++) {
405
+ const msg = messages[i];
406
+ if (!msg || typeof msg.type !== 'string')
407
+ continue;
408
+ const timestamp = msg.time?.created ? new Date(msg.time.created).toISOString() : undefined;
409
+ if (msg.type === 'user') {
410
+ if (typeof msg.text === 'string' && msg.text.length > 0) {
411
+ if (!displayTitle)
412
+ displayTitle = msg.text;
413
+ events.push({ type: 'text', content: msg.text, timestamp });
414
+ }
415
+ continue;
416
+ }
417
+ if (msg.type === 'assistant') {
418
+ for (const part of Array.isArray(msg.content) ? msg.content : []) {
419
+ if (part?.type === 'text' && typeof part.text === 'string') {
420
+ events.push({ type: 'text', content: part.text, timestamp });
421
+ continue;
422
+ }
423
+ if (part?.type === 'reasoning' && typeof part.text === 'string') {
424
+ events.push({ type: 'thinking', content: part.text, timestamp });
425
+ continue;
426
+ }
427
+ if (part?.type === 'tool') {
428
+ const toolId = part.id;
429
+ const toolName = part.name ?? '';
430
+ const input = parseToolInput(part.state?.input);
431
+ if (toolId) {
432
+ events.push({
433
+ type: 'tool_use',
434
+ content: JSON.stringify({ id: toolId, name: toolName, input }),
435
+ timestamp,
436
+ });
437
+ }
438
+ const output = v2ToolOutput(part.state);
439
+ if (toolId && output !== undefined) {
440
+ events.push({
441
+ type: 'tool_result',
442
+ content: truncateUtf8(output, TOOL_RESULT_MAX_BYTES),
443
+ timestamp,
444
+ });
445
+ }
446
+ }
447
+ }
448
+ continue;
449
+ }
450
+ // User-invoked shell messages are a first-class message type in v2.
451
+ if (msg.type === 'shell' && typeof msg.command === 'string') {
452
+ events.push({
453
+ type: 'tool_use',
454
+ content: JSON.stringify({
455
+ id: msg.callID ?? msg.id ?? 'shell',
456
+ name: 'bash',
457
+ input: { command: msg.command },
458
+ }),
459
+ timestamp,
460
+ });
461
+ if (typeof msg.output === 'string') {
462
+ events.push({
463
+ type: 'tool_result',
464
+ content: truncateUtf8(msg.output, TOOL_RESULT_MAX_BYTES),
465
+ timestamp,
466
+ });
467
+ }
468
+ continue;
469
+ }
470
+ // Keep the compact summary visible in the catch-up tail; other control
471
+ // messages (system/synthetic/agent-switched/model-switched) are noise.
472
+ if (msg.type === 'compaction' && typeof msg.summary === 'string' && msg.summary) {
473
+ events.push({ type: 'text', content: msg.summary, timestamp });
474
+ }
475
+ }
476
+ // If the last user message had no text (e.g. attachment-only), fall back to
477
+ // the first human text anywhere in the session for the card title.
478
+ if (!displayTitle) {
479
+ for (const msg of messages) {
480
+ if (msg?.type === 'user' && typeof msg.text === 'string' && msg.text.length > 0) {
481
+ displayTitle = msg.text;
482
+ break;
483
+ }
484
+ }
485
+ }
486
+ return { events, displayTitle };
487
+ }
168
488
  export class OpencodeSessionReader {
169
489
  binary;
170
490
  cacheTtlMs;
@@ -214,6 +534,9 @@ export class OpencodeSessionReader {
214
534
  }
215
535
  /**
216
536
  * Read session content: extract catch-up tail (last user message + everything after).
537
+ *
538
+ * 编排:定位会话文件(captureExport + parse)→ directory 精确匹配(与
539
+ * cwd-scoped list 同口径)→ 按 export 版本分发到 v1/v2 装配。
217
540
  */
218
541
  readSessionContent(sessionId, cwd, opts) {
219
542
  const realCwd = this.realpath(cwd);
@@ -251,342 +574,54 @@ export class OpencodeSessionReader {
251
574
  if (isV2Export(data)) {
252
575
  return this.readV2SessionContent(data, opts);
253
576
  }
254
- const events = [];
255
- let usage;
256
- /** 本 turn 的 flow 累计:末条 user 消息之后的全部 step-finish。 */
257
- let turn = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, total: 0 };
258
- let aiTitle;
259
- let displayTitle;
260
- // Find the last user message position (for catch-up tail).
261
- // messages are already parsed objects — scan directly.
262
- let lastUserIndex = -1;
263
- for (let i = 0; i < data.messages.length; i++) {
264
- if (data.messages[i].info.role === 'user') {
265
- lastUserIndex = i;
266
- }
267
- }
268
- // Process messages from last user message onwards (catch-up tail)
269
- const startIndex = lastUserIndex >= 0 ? lastUserIndex : 0;
270
- for (let i = startIndex; i < data.messages.length; i++) {
271
- const msg = data.messages[i];
272
- const timestamp = msg.info.time?.created
273
- ? new Date(msg.info.time.created).toISOString()
274
- : undefined;
275
- for (const part of msg.parts) {
276
- if (part.type === 'text') {
277
- // First user text after last user message is displayTitle
278
- if (msg.info.role === 'user' && part.text && !displayTitle) {
279
- displayTitle = part.text;
280
- }
281
- events.push({
282
- type: 'text',
283
- content: part.text ?? '',
284
- timestamp,
285
- });
286
- }
287
- else if (part.type === 'reasoning') {
288
- events.push({
289
- type: 'thinking',
290
- content: part.text ?? '',
291
- timestamp,
292
- });
293
- }
294
- else if (part.type === 'tool') {
295
- // Tool use
296
- if (part.callID && part.state?.input) {
297
- events.push({
298
- type: 'tool_use',
299
- content: JSON.stringify({
300
- id: part.callID,
301
- name: part.tool,
302
- input: part.state.input,
303
- }),
304
- timestamp,
305
- });
306
- }
307
- // Tool result (completed or error)
308
- if (part.callID && part.state?.output) {
309
- const isError = part.state.status === 'error';
310
- const rawOut = isError
311
- ? (part.state.error ?? part.state.output)
312
- : (part.state.output ?? '');
313
- // pre-fold pathological tool_results (e.g. a 500KB file
314
- // listing) via the shared truncateUtf8 primitive, reusing the
315
- // same …(已截断) suffix as the live renderer / card budget.
316
- events.push({
317
- type: 'tool_result',
318
- content: truncateUtf8(rawOut, TOOL_RESULT_MAX_BYTES),
319
- timestamp,
320
- });
321
- }
322
- }
323
- }
324
- // Track usage from step-finish parts. v1 没有 idle 标记,turn 边界就是
325
- // 上面那段"最后一条 user 消息";同一 turn 可能有多个 step-finish
326
- // (每次模型请求一条),flow 要全加起来,只留末条会把多步 run 低报。
327
- for (const part of msg.parts) {
328
- if (part.type === 'step-finish' && part.tokens) {
329
- // Skip zero-total steps(模型在 reasoning 中被截断)——它们没有消耗,
330
- // 也不该把本次请求当成一条真实采样。
331
- // ccusage-aligned: cache.write -> cacheCreationTokens.
332
- // contextLength = input + output + cacheRead + cacheCreation —
333
- // the unified context-window occupancy contract across all readers
334
- // (§9.21:末轮 output 已进入历史、下一 prompt 全量携带,与 compact
335
- // postTokens 水位同语义).
336
- if ((part.tokens.total ?? 0) !== 0) {
337
- const input = part.tokens.input ?? 0;
338
- const output = part.tokens.output ?? 0;
339
- const cacheRead = part.tokens.cache?.read ?? 0;
340
- const cacheWrite = part.tokens.cache?.write ?? 0;
341
- turn = {
342
- input: turn.input + input,
343
- output: turn.output + output,
344
- cacheRead: turn.cacheRead + cacheRead,
345
- cacheCreation: turn.cacheCreation + cacheWrite,
346
- total: turn.total + part.tokens.total,
347
- };
348
- usage = {
349
- inputTokens: input,
350
- outputTokens: output,
351
- contextLength: input + output + cacheRead + cacheWrite,
352
- cacheReadTokens: part.tokens.cache?.read,
353
- cacheCreationTokens: part.tokens.cache?.write,
354
- totalTokens: part.tokens.total,
355
- };
356
- }
357
- }
358
- }
359
- }
360
- // flow 覆盖末条采样值,contextLength 保留末条(水位不求和)。
361
- if (usage) {
362
- usage.inputTokens = turn.input;
363
- usage.outputTokens = turn.output;
364
- usage.cacheReadTokens = turn.cacheRead;
365
- usage.cacheCreationTokens = turn.cacheCreation;
366
- usage.totalTokens = turn.total;
367
- }
368
- // Cumulative (session-wide): sum input/output/total across ALL step-finish
369
- // parts in the whole session. The catch-up loop above only scans from
370
- // the last user message (= last run) for per-turn usage; cumulative
371
- // needs a full scan so multi-run sessions report the true total.
372
- let cumInput = 0;
373
- let cumOutput = 0;
374
- let cumTotal = 0;
375
- let cumCacheRead = 0;
376
- let cumCacheWrite = 0;
377
- for (const msg of data.messages) {
378
- for (const part of msg.parts) {
379
- if (part.type === 'step-finish' && part.tokens) {
380
- cumInput += part.tokens.input ?? 0;
381
- cumOutput += part.tokens.output ?? 0;
382
- cumTotal += part.tokens.total ?? 0;
383
- cumCacheRead += part.tokens.cache?.read ?? 0;
384
- cumCacheWrite += part.tokens.cache?.write ?? 0;
385
- }
386
- }
387
- }
388
- usage = withCumulativeUsage(usage, {
389
- total: cumTotal,
390
- input: cumInput,
391
- output: cumOutput,
392
- cacheRead: cumCacheRead,
393
- cacheWrite: cumCacheWrite,
394
- });
395
- // Use info.title as aiTitle
396
- if (data.info.title) {
397
- aiTitle = data.info.title;
398
- }
399
- // If no displayTitle yet, use first user text as fallback
400
- if (!displayTitle) {
401
- for (const msg of data.messages) {
402
- if (msg.info.role === 'user') {
403
- for (const part of msg.parts) {
404
- if (part.type === 'text' && part.text) {
405
- displayTitle = part.text;
406
- break;
407
- }
408
- }
409
- }
410
- if (displayTitle)
411
- break;
412
- }
413
- }
414
- // Apply maxEvents cap: keep the LAST N events (most recent), matching
415
- // CodexSessionReader's slice(-maxEvents). maxEvents limits the catch-up
416
- // tail so auto-resume cards don't load the entire session.
417
- const cappedEvents = capEvents(events, opts?.maxEvents);
418
- return {
419
- events: cappedEvents,
420
- usage,
421
- // opencode run doesn't have background tasks
422
- aiTitle,
423
- displayTitle,
424
- };
577
+ return this.readV1SessionContent(data, opts);
425
578
  }
426
579
  catch (err) {
427
580
  getLogger().warn(`[opencode-session-reader] readSessionContent build failed: ${err}`);
428
581
  return { events: [] };
429
582
  }
430
583
  }
584
+ /**
585
+ * v1 export(`{info, parts}` 信封 + step-finish 用量 part)的 catch-up tail
586
+ * 装配:扫描累积器(scanV1Messages)出 tail 位置与本 run/累计 usage,事件
587
+ * 装配(assembleV1TailEvents)出展示流,这里只做组装。
588
+ */
589
+ readV1SessionContent(data, opts) {
590
+ const scan = scanV1Messages(data.messages);
591
+ const startIndex = scan.lastUserIndex >= 0 ? scan.lastUserIndex : 0;
592
+ return {
593
+ // Apply maxEvents cap: keep the LAST N events (most recent), matching
594
+ // CodexSessionReader's slice(-maxEvents). maxEvents limits the catch-up
595
+ // tail so auto-resume cards don't load the entire session.
596
+ events: capEvents(assembleV1TailEvents(data.messages, startIndex), opts?.maxEvents),
597
+ usage: finalizeTurnUsage(scan.latest, scan.turn, scan.cum),
598
+ // opencode run doesn't have background tasks
599
+ aiTitle: data.info.title || undefined,
600
+ // tail 无 user 文本时回退全会话首条 user 文本
601
+ displayTitle: scan.tailDisplayTitle ?? firstV1UserText(data.messages),
602
+ };
603
+ }
431
604
  /**
432
605
  * v2 export: messages are flat (`type` + `text`/`content`) and assistant
433
606
  * usage lives on the message instead of a `step-finish` part. Keep this
434
607
  * mapping isolated from the v1 parser so v1 behaviour stays unchanged.
608
+ *
609
+ * turnStart 可能落在末条采样之后(新 turn 尚无采样)→ turn 全 0;latest
610
+ * 仍是上一轮末条采样,contextLength 保留水位。turnStart 小于最后一个采样
611
+ * 下标时,flow 覆盖末条值,只有 contextLength 保留末条(finalizeTurnUsage)。
435
612
  */
436
613
  readV2SessionContent(data, opts) {
437
614
  const messages = Array.isArray(data.messages) ? data.messages : [];
438
- const events = [];
439
615
  const aiTitle = typeof data.info.title === 'string' && data.info.title.length > 0
440
616
  ? data.info.title
441
617
  : undefined;
442
- let displayTitle;
443
- let lastUserIndex = -1;
444
- for (let i = 0; i < messages.length; i++) {
445
- if (messages[i]?.type === 'user')
446
- lastUserIndex = i;
447
- }
448
- const startIndex = lastUserIndex >= 0 ? lastUserIndex : 0;
449
- for (let i = startIndex; i < messages.length; i++) {
450
- const msg = messages[i];
451
- if (!msg || typeof msg.type !== 'string')
452
- continue;
453
- const timestamp = msg.time?.created ? new Date(msg.time.created).toISOString() : undefined;
454
- if (msg.type === 'user') {
455
- if (typeof msg.text === 'string' && msg.text.length > 0) {
456
- if (!displayTitle)
457
- displayTitle = msg.text;
458
- events.push({ type: 'text', content: msg.text, timestamp });
459
- }
460
- continue;
461
- }
462
- if (msg.type === 'assistant') {
463
- for (const part of Array.isArray(msg.content) ? msg.content : []) {
464
- if (part?.type === 'text' && typeof part.text === 'string') {
465
- events.push({ type: 'text', content: part.text, timestamp });
466
- continue;
467
- }
468
- if (part?.type === 'reasoning' && typeof part.text === 'string') {
469
- events.push({ type: 'thinking', content: part.text, timestamp });
470
- continue;
471
- }
472
- if (part?.type === 'tool') {
473
- const toolId = part.id;
474
- const toolName = part.name ?? '';
475
- const input = parseToolInput(part.state?.input);
476
- if (toolId) {
477
- events.push({
478
- type: 'tool_use',
479
- content: JSON.stringify({ id: toolId, name: toolName, input }),
480
- timestamp,
481
- });
482
- }
483
- const output = v2ToolOutput(part.state);
484
- if (toolId && output !== undefined) {
485
- events.push({
486
- type: 'tool_result',
487
- content: truncateUtf8(output, TOOL_RESULT_MAX_BYTES),
488
- timestamp,
489
- });
490
- }
491
- }
492
- }
493
- continue;
494
- }
495
- // User-invoked shell messages are a first-class message type in v2.
496
- if (msg.type === 'shell' && typeof msg.command === 'string') {
497
- events.push({
498
- type: 'tool_use',
499
- content: JSON.stringify({
500
- id: msg.callID ?? msg.id ?? 'shell',
501
- name: 'bash',
502
- input: { command: msg.command },
503
- }),
504
- timestamp,
505
- });
506
- if (typeof msg.output === 'string') {
507
- events.push({
508
- type: 'tool_result',
509
- content: truncateUtf8(msg.output, TOOL_RESULT_MAX_BYTES),
510
- timestamp,
511
- });
512
- }
513
- continue;
514
- }
515
- // Keep the compact summary visible in the catch-up tail; other control
516
- // messages (system/synthetic/agent-switched/model-switched) are noise.
517
- if (msg.type === 'compaction' && typeof msg.summary === 'string' && msg.summary) {
518
- events.push({ type: 'text', content: msg.summary, timestamp });
519
- }
520
- }
521
- // If the last user message had no text (e.g. attachment-only), fall back to
522
- // the first human text anywhere in the session for the card title.
523
- if (!displayTitle) {
524
- for (const msg of messages) {
525
- if (msg?.type === 'user' && typeof msg.text === 'string' && msg.text.length > 0) {
526
- displayTitle = msg.text;
527
- break;
528
- }
529
- }
530
- }
531
- let latest;
532
- let cumInput = 0;
533
- let cumOutput = 0;
534
- let cumTotal = 0;
535
- let cumCacheRead = 0;
536
- let cumCacheWrite = 0;
537
- // 本 turn(= 本 run)的 flow:末个 turn 内每次模型请求之和。contextLength
538
- // 仍取末次请求的窗口水位(水位不可求和)。只取末条会把多步 run 低报
539
- // (实测一条 25 请求的 run:input 20,040 显示成 543、output 10,358 显示成 1,250)。
540
- const turnStart = lastTurnStartIndex(messages);
541
- const turn = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, total: 0 };
542
- for (let i = 0; i < messages.length; i++) {
543
- const sample = usageSample(messages[i]);
544
- if (!sample)
545
- continue;
546
- cumInput += sample.input;
547
- cumOutput += sample.output;
548
- cumTotal += sample.total;
549
- cumCacheRead += sample.cacheRead;
550
- cumCacheWrite += sample.cacheCreation;
551
- if (i > turnStart) {
552
- turn.input += sample.input;
553
- turn.output += sample.output;
554
- turn.cacheRead += sample.cacheRead;
555
- turn.cacheCreation += sample.cacheCreation;
556
- turn.total += sample.total;
557
- }
558
- latest = {
559
- inputTokens: sample.input,
560
- outputTokens: sample.output,
561
- // §9.21 口径:含末轮 output(与 postTokens 水位同语义)。
562
- contextLength: sample.input + sample.output + sample.cacheRead + sample.cacheCreation,
563
- cacheReadTokens: sample.cacheRead,
564
- cacheCreationTokens: sample.cacheCreation,
565
- totalTokens: sample.total,
566
- };
567
- }
568
- // turnStart 可能落在末条采样之后(新 turn 尚无采样)→ turn 全 0;latest
569
- // 仍是上一轮末条采样,contextLength 保留水位。turnStart 小于最后一个采样
570
- // 下标时,flow 覆盖末条值,只有 contextLength 保留末条。
571
- if (latest) {
572
- latest.inputTokens = turn.input;
573
- latest.outputTokens = turn.output;
574
- latest.cacheReadTokens = turn.cacheRead;
575
- latest.cacheCreationTokens = turn.cacheCreation;
576
- latest.totalTokens = turn.total;
577
- }
578
- latest = withCumulativeUsage(latest, {
579
- total: cumTotal,
580
- input: cumInput,
581
- output: cumOutput,
582
- cacheRead: cumCacheRead,
583
- cacheWrite: cumCacheWrite,
584
- });
618
+ const tail = assembleV2TailEvents(messages);
619
+ const usage = scanV2Usage(messages);
585
620
  return {
586
- events: capEvents(events, opts?.maxEvents),
587
- usage: latest,
621
+ events: capEvents(tail.events, opts?.maxEvents),
622
+ usage: finalizeTurnUsage(usage.latest, usage.turn, usage.cum),
588
623
  aiTitle,
589
- displayTitle,
624
+ displayTitle: tail.displayTitle,
590
625
  };
591
626
  }
592
627
  /**
@@ -699,9 +734,7 @@ export class OpencodeSessionReader {
699
734
  if (err.code === 'ENOENT') {
700
735
  return [];
701
736
  }
702
- getLogger().error(`[opencode-session-reader] session list failed: ${err}`);
703
- const msg = err instanceof Error ? err.message : String(err);
704
- throw new Error(`opencode session list 读取失败: ${msg}`, { cause: err });
737
+ throwSessionListError('opencode', err);
705
738
  }
706
739
  }
707
740
  /**