lark-remote 0.5.20 → 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 (55) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/bridge/inbound-media.js +71 -45
  3. package/dist/bridge/index.d.ts +9 -0
  4. package/dist/bridge/index.js +230 -203
  5. package/dist/card/approval-render.js +32 -33
  6. package/dist/card/card-budget.js +123 -98
  7. package/dist/card/run-renderer.js +82 -68
  8. package/dist/card/run-state.d.ts +0 -14
  9. package/dist/card/run-state.js +335 -207
  10. package/dist/card/tool-render.js +20 -16
  11. package/dist/card/usage-format.d.ts +12 -9
  12. package/dist/card/usage-format.js +111 -97
  13. package/dist/common/text-width.js +36 -24
  14. package/dist/common/tool-summary.js +132 -90
  15. package/dist/index.js +226 -190
  16. package/dist/message-handlers.js +253 -236
  17. package/dist/router/commands/ls.d.ts +3 -0
  18. package/dist/router/commands/ls.js +256 -175
  19. package/dist/router/commands/order.js +184 -159
  20. package/dist/router/commands/resume.js +189 -133
  21. package/dist/router/commands/ws.js +219 -174
  22. package/dist/router/index.js +192 -201
  23. package/dist/runner/bash/runner.js +2 -4
  24. package/dist/runner/codex/app-server/translator.d.ts +2 -0
  25. package/dist/runner/codex/app-server/translator.js +287 -221
  26. package/dist/runner/common/acp/base-translator.js +19 -17
  27. package/dist/runner/common/jsonl-stream.js +154 -187
  28. package/dist/runner/common/jsonrpc/client.d.ts +29 -0
  29. package/dist/runner/common/jsonrpc/client.js +43 -12
  30. package/dist/runner/common/spawning-runner.d.ts +5 -0
  31. package/dist/runner/common/spawning-runner.js +8 -3
  32. package/dist/runner/common/turn-started.d.ts +8 -0
  33. package/dist/runner/common/turn-started.js +11 -0
  34. package/dist/runner/dsh/approval.js +84 -51
  35. package/dist/runner/dsh/client.d.ts +7 -9
  36. package/dist/runner/dsh/client.js +356 -287
  37. package/dist/runner/dsh/mux-stream.js +28 -8
  38. package/dist/runner/dsh/runner.d.ts +4 -3
  39. package/dist/runner/dsh/runner.js +88 -64
  40. package/dist/runner/dsh/translator.d.ts +1 -31
  41. package/dist/runner/dsh/translator.js +180 -178
  42. package/dist/runner/kimi/acp/runner.js +81 -44
  43. package/dist/runner/opencode/acp/error-monitor.js +5 -5
  44. package/dist/runner/pi/rpc/client.js +4 -13
  45. package/dist/runner/pi/rpc/translator.js +61 -60
  46. package/dist/session/claude/sessions.js +113 -101
  47. package/dist/session/codex/rollout-reader.js +179 -180
  48. package/dist/session/dsh/sessions.js +281 -231
  49. package/dist/session/kimi/sessions.d.ts +0 -7
  50. package/dist/session/kimi/sessions.js +129 -109
  51. package/dist/session/opencode/sessions.d.ts +0 -11
  52. package/dist/session/opencode/sessions.js +161 -137
  53. package/dist/session/session-store.d.ts +8 -0
  54. package/dist/session/session-store.js +98 -98
  55. package/package.json +1 -1
@@ -32,6 +32,49 @@ import { summarizeToolInput } from '../../common/tool-summary.js';
32
32
  * (若 DSH 支持分页 cursor 可改成分页,当前用有界大拉取)
33
33
  */
34
34
  const DSH_HISTORY_FETCH_LIMIT = 10_000;
35
+ /** curl 传输层失败(spawn error / 非零退出)→ 统一错误;成功返回 undefined。 */
36
+ function dshSpawnFailure(prefix, res) {
37
+ if (res.error || res.status !== 0) {
38
+ return new Error(`${prefix}: ${(res.error?.message ?? res.stderr) || 'unknown'}`);
39
+ }
40
+ return undefined;
41
+ }
42
+ /** 从 curl stdout 拆出末尾 HTTP 状态码与 body(`--write-out \n%{http_code}`)。 */
43
+ function splitDshHttpResponse(raw) {
44
+ const statusMatch = /\n(\d{3})$/.exec(raw);
45
+ const httpStatus = statusMatch ? Number(statusMatch[1]) : undefined;
46
+ const bodyText = statusMatch ? raw.slice(0, statusMatch.index) : raw;
47
+ return { httpStatus, bodyText };
48
+ }
49
+ /** 解析 result 信封;非 JSON / ok!==true 按原文案抛错。 */
50
+ function parseDshResult(method, bodyText) {
51
+ let parsed;
52
+ try {
53
+ parsed = JSON.parse(bodyText);
54
+ }
55
+ catch (err) {
56
+ throw new Error(`DSH ${method} returned non-JSON: ${err.message}`, {
57
+ cause: err,
58
+ });
59
+ }
60
+ if (parsed.result?.ok !== true) {
61
+ throw new Error(parsed.result?.error?.message ?? `DSH ${method} failed`);
62
+ }
63
+ return parsed.result.value;
64
+ }
65
+ /** 从 `-D -` 响应头里取第一条 set-cookie 的 name=value 段。 */
66
+ function firstSetCookie(header) {
67
+ const match = header.match(/^set-cookie:\s*(.+)$/im);
68
+ return match?.[1]?.split(';')[0]?.trim();
69
+ }
70
+ /** 重试仍失败:二次错为 cause,首错(cookie 失效)作为根因写进 message。 */
71
+ function cookieRefreshFailure(method, first, second) {
72
+ const firstMsg = first?.message ?? String(first);
73
+ const secondMsg = second?.message ?? String(second);
74
+ // 重试仍失败:首错(cookie 失效)才是根因。`cause` 必须是本次捕获的 err
75
+ // (lint preserve-caught-error),所以根因写进 message 一并留痕。
76
+ return new Error(`DSH ${method} failed after cookie refresh: ${secondMsg} (root cause: ${firstMsg})`, { cause: second });
77
+ }
35
78
  /** Synchronous unary POST via curl (the reader interface is synchronous). */
36
79
  function defaultSyncRequest(baseUrl, method, payload, cookie) {
37
80
  const body = JSON.stringify({
@@ -54,40 +97,23 @@ function defaultSyncRequest(baseUrl, method, payload, cookie) {
54
97
  '--write-out',
55
98
  '\n%{http_code}',
56
99
  ], { encoding: 'utf-8', timeout: 15_000, maxBuffer: 64 * 1024 * 1024 });
57
- if (res.error || res.status !== 0) {
58
- throw new Error(`DSH ${method} failed: ${(res.error?.message ?? res.stderr) || 'unknown'}`);
59
- }
60
- const raw = String(res.stdout);
61
- const statusMatch = /\n(\d{3})$/.exec(raw);
62
- const httpStatus = statusMatch ? Number(statusMatch[1]) : undefined;
63
- const bodyText = statusMatch ? raw.slice(0, statusMatch.index) : raw;
100
+ const failure = dshSpawnFailure(`DSH ${method} failed`, res);
101
+ if (failure)
102
+ throw failure;
103
+ const { httpStatus, bodyText } = splitDshHttpResponse(String(res.stdout));
64
104
  if (httpStatus === 401 || httpStatus === 403) {
65
105
  throw new Error(`DSH ${method} unauthorized (HTTP ${httpStatus})`);
66
106
  }
67
- let parsed;
68
- try {
69
- parsed = JSON.parse(bodyText);
70
- }
71
- catch (err) {
72
- throw new Error(`DSH ${method} returned non-JSON: ${err.message}`, {
73
- cause: err,
74
- });
75
- }
76
- if (parsed.result?.ok !== true) {
77
- throw new Error(parsed.result?.error?.message ?? `DSH ${method} failed`);
78
- }
79
- return parsed.result.value;
107
+ return parseDshResult(method, bodyText);
80
108
  }
81
109
  /** Synchronous index bootstrap; returns the v1 `name=value` cookie pair. */
82
110
  function defaultSyncConnect(baseUrl, token) {
83
111
  const url = `${baseUrl}/?token=${encodeURIComponent(token)}`;
84
112
  const res = spawnProcessSync('curl', ['-sS', '-D', '-', '-o', isWin32() ? 'NUL' : '/dev/null', '--max-time', '5', url], { encoding: 'utf-8', timeout: 10_000, maxBuffer: 1024 * 1024 });
85
- if (res.error || res.status !== 0) {
86
- throw new Error(`DSH auth failed: ${(res.error?.message ?? res.stderr) || 'unknown'}`);
87
- }
88
- const header = String(res.stdout);
89
- const match = header.match(/^set-cookie:\s*(.+)$/im);
90
- const cookie = match?.[1]?.split(';')[0]?.trim();
113
+ const failure = dshSpawnFailure('DSH auth failed', res);
114
+ if (failure)
115
+ throw failure;
116
+ const cookie = firstSetCookie(String(res.stdout));
91
117
  if (!cookie)
92
118
  throw new Error('DSH auth failed: index handshake did not return a cookie');
93
119
  return cookie;
@@ -149,11 +175,7 @@ export class DshSessionReader {
149
175
  return this.syncRequest(baseUrl, method, payload, this.resolveCookie());
150
176
  }
151
177
  catch (second) {
152
- // 重试仍失败:首错(cookie 失效)才是根因。`cause` 必须是本次捕获的 err
153
- // (lint preserve-caught-error),所以根因写进 message 一并留痕。
154
- const firstMsg = first?.message ?? String(first);
155
- const secondMsg = second?.message ?? String(second);
156
- throw new Error(`DSH ${method} failed after cookie refresh: ${secondMsg} (root cause: ${firstMsg})`, { cause: second });
178
+ throw cookieRefreshFailure(method, first, second);
157
179
  }
158
180
  }
159
181
  }
@@ -241,125 +263,7 @@ export class DshSessionReader {
241
263
  throughSeq,
242
264
  maxMessages: DSH_HISTORY_FETCH_LIMIT,
243
265
  });
244
- const events = [];
245
- let displayTitle;
246
- let cumInput = 0;
247
- let cumOutput = 0;
248
- let cumCacheRead = 0;
249
- let cumCacheWrite = 0;
250
- let lastUsage;
251
- // 末个 turn 内各采样用量之和("本 run / 末轮" scope)。DSH 每次模型调用
252
- // 一条 usage,工具往返的 turn 会写多条 assistant/message;只取末条会把
253
- // 多步 run 低报(实测 231/689 显示成整轮真实消耗)。turn 边界取
254
- // turn/start–turn/end,与 codex `task_started` 汇总同一口径(§9.21)。
255
- // null = 历史里没有 turn 标记(旧数据/截断)→ 退回末条 usage 语义。
256
- let turnUsage = null;
257
- let lastTurnUsage = null;
258
- /** 等结果的工具调用(见 MappedEvent.pendingTool)。 */
259
- let pendingTool;
260
- for (const entry of value.records ?? []) {
261
- const ev = entry.event;
262
- if (!ev)
263
- continue;
264
- if (ev.type === 'turn/start') {
265
- turnUsage = emptyUsageTotals();
266
- }
267
- else if (ev.type === 'turn/end') {
268
- if (turnUsage)
269
- lastTurnUsage = turnUsage;
270
- turnUsage = null;
271
- }
272
- const contentEvents = mapEvent(ev);
273
- if (contentEvents.pendingTool) {
274
- // 未完成的工具调用最多同时一个(DSH 按 step 串行执行;并行调用时按
275
- // 到达顺序配对,最坏情况是两次调用共用一个面板,不会丢信息)。
276
- if (pendingTool)
277
- events.push(pendingTool);
278
- pendingTool = contentEvents.pendingTool;
279
- }
280
- else if (ev.type === 'tool/result' && pendingTool && contentEvents.events[0]) {
281
- const result = contentEvents.events[0];
282
- // 标题沿用调用的 `🔧 name — summary`,失败改成 ❌(折叠态一眼看出结果)。
283
- const failed = result.title?.startsWith('🔴') === true;
284
- events.push({
285
- type: 'tool_use',
286
- title: failed
287
- ? (pendingTool.title ?? '❌ 工具').replace('🔧', '❌')
288
- : pendingTool.title,
289
- content: result.content,
290
- timestamp: pendingTool.timestamp,
291
- });
292
- pendingTool = undefined;
293
- }
294
- else {
295
- events.push(...contentEvents.events);
296
- }
297
- if (contentEvents.title)
298
- displayTitle = contentEvents.title;
299
- const u = contentEvents.usage;
300
- if (u) {
301
- // Cumulative (session-wide): sum across ALL assistant/message usage.
302
- cumInput += u.inputTokens ?? 0;
303
- cumOutput += u.outputTokens ?? 0;
304
- cumCacheRead += u.cacheReadTokens ?? 0;
305
- cumCacheWrite += u.cacheWriteTokens ?? 0;
306
- // 本 run 的 flow 字段 = 末个 turn 的采样之和;contextLength 用末次采样
307
- // 的窗口占用(input + cacheRead + cacheWrite,不含 output)。
308
- if (turnUsage) {
309
- turnUsage.input += u.inputTokens ?? 0;
310
- turnUsage.output += u.outputTokens ?? 0;
311
- turnUsage.cacheRead += u.cacheReadTokens ?? 0;
312
- turnUsage.cacheWrite += u.cacheWriteTokens ?? 0;
313
- }
314
- lastUsage = u;
315
- }
316
- }
317
- let sessionUsage;
318
- if (lastUsage) {
319
- const input = lastUsage.inputTokens ?? 0;
320
- const cacheRead = lastUsage.cacheReadTokens ?? 0;
321
- const cacheWrite = lastUsage.cacheWriteTokens ?? 0;
322
- // 在途 turn(turn/start 无 turn/end,run 进行中读历史)的采样优先于
323
- // 上一轮:旧实现只认已结算的 lastTurnUsage,mid-run 读取会把当前 turn
324
- // 已消耗的量显示成上一个完整 turn 的数字。
325
- const flow = turnUsage ??
326
- lastTurnUsage ?? {
327
- input,
328
- output: lastUsage.outputTokens ?? 0,
329
- cacheRead,
330
- cacheWrite,
331
- };
332
- sessionUsage = {
333
- inputTokens: flow.input,
334
- outputTokens: flow.output,
335
- // §9.21 口径:context 是末次采样的窗口水位(input + 末次 output +
336
- // cache),不把整 turn 的 output 求和(前几步 output 已进入后续 input)。
337
- contextLength: input + (lastUsage.outputTokens ?? 0) + cacheRead + cacheWrite,
338
- cacheReadTokens: flow.cacheRead,
339
- cacheCreationTokens: flow.cacheWrite,
340
- totalTokens: flow.input + flow.output + flow.cacheRead + flow.cacheWrite,
341
- ...cumulativeUsageFields({
342
- input: cumInput,
343
- output: cumOutput,
344
- cacheRead: cumCacheRead,
345
- cacheCreation: cumCacheWrite,
346
- }),
347
- };
348
- }
349
- // 中断/仍在执行的调用没有结果:把它当独立事件留下,别让这一条消失。
350
- if (pendingTool)
351
- events.push(pendingTool);
352
- // 头部信息口径:最近输入优先用服务端末轮 prompt(事件里可能只是当前 turn
353
- // 的一部分),标题用服务端 title;两者都缺时保持原行为。
354
- const headerLastInput = preview.lastUserMessage ?? displayTitle;
355
- return {
356
- // 展示仍按 maxEvents 尾截断(默认 50,保持原行为),usage 已在上面基于
357
- // 完整 history 累加。
358
- events: capEvents(events, opts?.maxEvents ?? 50),
359
- ...(sessionUsage ? { usage: sessionUsage } : {}),
360
- ...(headerLastInput ? { displayTitle: headerLastInput } : {}),
361
- ...(preview.title ? { aiTitle: preview.title } : {}),
362
- };
266
+ return buildDshSessionContent(value, preview, opts);
363
267
  }
364
268
  catch (error) {
365
269
  // 降级为空卡(与 claude/pi 同口径),但完整错误对象入日志保留 stack,
@@ -508,97 +412,107 @@ function asBlocks(value) {
508
412
  return [];
509
413
  return value.filter((b) => !!b && typeof b === 'object');
510
414
  }
511
- function mapEvent(ev) {
415
+ function mapUserMessage(ev) {
416
+ const out = { events: [] };
417
+ const content = asBlocks(ev.data.content);
418
+ const text = content
419
+ .filter((c) => c.type === 'text')
420
+ .map((c) => c.text ?? '')
421
+ .join('');
422
+ if (!text.trim())
423
+ return out;
424
+ // DSH 把上下文注入也写成 user/message,靠 `source.kind` 区分。真实会话里
425
+ // 30 条 user/message 中只有 8 条是用户本人说的(其余是 time-context /
426
+ // agent-instructions / skill-catalog / goal / tool-jobs…)——照单全收会让
427
+ // 回顾卡里塞满"Time sampled while preparing turn…",还把系统提示标成 👤 你。
428
+ const source = ev.data.source;
429
+ const kind = typeof source?.kind === 'string' ? source.kind : undefined;
430
+ if (kind && CONTEXT_INJECTION_KINDS.has(kind))
431
+ return out;
432
+ const isNotice = kind !== undefined && kind !== 'user';
433
+ out.events.push({
434
+ type: isNotice ? 'notice' : 'user',
435
+ ...(isNotice ? { title: '🔔 通知' } : {}),
436
+ content: isNotice ? truncateUtf8(text.trim(), NOTICE_MAX_BYTES) : text,
437
+ timestamp: safeEventTimestamp(ev.time),
438
+ });
439
+ return out;
440
+ }
441
+ function mapAssistantMessage(ev) {
512
442
  const out = { events: [] };
513
- switch (ev.type) {
514
- case 'user/message': {
515
- const content = asBlocks(ev.data.content);
516
- const text = content
517
- .filter((c) => c.type === 'text')
518
- .map((c) => c.text ?? '')
519
- .join('');
520
- if (!text.trim())
521
- break;
522
- // DSH 把上下文注入也写成 user/message,靠 `source.kind` 区分。真实会话里
523
- // 30 条 user/message 中只有 8 条是用户本人说的(其余是 time-context /
524
- // agent-instructions / skill-catalog / goal / tool-jobs…)——照单全收会让
525
- // 回顾卡里塞满"Time sampled while preparing turn…",还把系统提示标成 👤 你。
526
- const source = ev.data.source;
527
- const kind = typeof source?.kind === 'string' ? source.kind : undefined;
528
- if (kind && CONTEXT_INJECTION_KINDS.has(kind))
529
- break;
530
- const isNotice = kind !== undefined && kind !== 'user';
443
+ const content = asBlocks(ev.data.message?.content);
444
+ const timestamp = safeEventTimestamp(ev.time);
445
+ // DSH 的块类型是 text / reasoning / tool-call(不是 Anthropic 的
446
+ // thinking)。只认 text 会让 resume 卡"只有工具调用、没有思考与输出"
447
+ // (一个 31 turn 的会话里 reasoning 块 371 个、text 12 个,
448
+ // 全被丢掉)。tool-call 块不在这里取——独立的 `tool/call` 事件带完整参数。
449
+ for (const block of content) {
450
+ const text = typeof block.text === 'string' ? block.text.trim() : '';
451
+ if (!text)
452
+ continue;
453
+ if (block.type === 'text') {
531
454
  out.events.push({
532
- type: isNotice ? 'notice' : 'user',
533
- ...(isNotice ? { title: '🔔 通知' } : {}),
534
- content: isNotice ? truncateUtf8(text.trim(), NOTICE_MAX_BYTES) : text,
535
- timestamp: safeEventTimestamp(ev.time),
455
+ type: 'assistant',
456
+ content: truncateUtf8(text, TEXT_MAX_BYTES),
457
+ timestamp,
536
458
  });
537
- break;
538
- }
539
- case 'assistant/message': {
540
- const content = asBlocks(ev.data.message?.content);
541
- const timestamp = safeEventTimestamp(ev.time);
542
- // DSH 的块类型是 text / reasoning / tool-call(不是 Anthropic 的
543
- // thinking)。只认 text 会让 resume 卡"只有工具调用、没有思考与输出"
544
- // (一个 31 turn 的会话里 reasoning 块 371 个、text 12 个,
545
- // 全被丢掉)。tool-call 块不在这里取——独立的 `tool/call` 事件带完整参数。
546
- for (const block of content) {
547
- const text = typeof block.text === 'string' ? block.text.trim() : '';
548
- if (!text)
549
- continue;
550
- if (block.type === 'text') {
551
- out.events.push({
552
- type: 'assistant',
553
- content: truncateUtf8(text, TEXT_MAX_BYTES),
554
- timestamp,
555
- });
556
- }
557
- else if (block.type === 'reasoning') {
558
- out.events.push({
559
- type: 'thinking',
560
- // 思考是过程信息:resume 卡里留头几百字足够判断"当时在想什么"。
561
- content: truncateUtf8(text, REASONING_MAX_BYTES),
562
- timestamp,
563
- });
564
- }
565
- }
566
- const usage = ev.data.usage;
567
- if (usage && typeof usage === 'object')
568
- out.usage = usage;
569
- break;
570
459
  }
571
- case 'tool/call': {
572
- // 调用本身先不产出事件:DSH 的 tool/call 与 tool/result 是一对,resume 卡
573
- // 里合成一个面板(`🔧 bash — cmd` + 输出),而不是两个各占一屏。
574
- const name = String(ev.data.name ?? '');
575
- const args = typeof ev.data.arguments === 'string' ? ev.data.arguments : '';
576
- const summary = summarizeToolInput(name, args);
577
- out.pendingTool = {
578
- type: 'tool_use',
579
- title: summary ? `🔧 ${name} — ${summary}` : `🔧 ${name}`,
580
- content: truncateUtf8(args, TOOL_INPUT_MAX_BYTES),
581
- timestamp: safeEventTimestamp(ev.time),
582
- };
583
- break;
584
- }
585
- case 'tool/result': {
586
- const msg = ev.data.message;
587
- const raw = stringifyContent(msg?.content);
588
- const isError = msg?.isError === true;
460
+ else if (block.type === 'reasoning') {
589
461
  out.events.push({
590
- type: 'tool_result',
591
- title: isError ? '🔴 结果(失败)' : '🟢 结果',
592
- content: truncateUtf8(raw, TOOL_RESULT_MAX_BYTES),
593
- timestamp: safeEventTimestamp(ev.time),
462
+ type: 'thinking',
463
+ // 思考是过程信息:resume 卡里留头几百字足够判断"当时在想什么"。
464
+ content: truncateUtf8(text, REASONING_MAX_BYTES),
465
+ timestamp,
594
466
  });
595
- break;
596
467
  }
597
- default:
598
- break;
599
468
  }
469
+ const usage = ev.data.usage;
470
+ if (usage && typeof usage === 'object')
471
+ out.usage = usage;
472
+ return out;
473
+ }
474
+ function mapToolCall(ev) {
475
+ const out = { events: [] };
476
+ // 调用本身先不产出事件:DSH 的 tool/call 与 tool/result 是一对,resume 卡
477
+ // 里合成一个面板(`🔧 bash — cmd` + 输出),而不是两个各占一屏。
478
+ const name = String(ev.data.name ?? '');
479
+ const args = typeof ev.data.arguments === 'string' ? ev.data.arguments : '';
480
+ const summary = summarizeToolInput(name, args);
481
+ out.pendingTool = {
482
+ type: 'tool_use',
483
+ title: summary ? `🔧 ${name} — ${summary}` : `🔧 ${name}`,
484
+ content: truncateUtf8(args, TOOL_INPUT_MAX_BYTES),
485
+ timestamp: safeEventTimestamp(ev.time),
486
+ };
487
+ return out;
488
+ }
489
+ function mapToolResult(ev) {
490
+ const out = { events: [] };
491
+ const msg = ev.data.message;
492
+ const raw = stringifyContent(msg?.content);
493
+ const isError = msg?.isError === true;
494
+ out.events.push({
495
+ type: 'tool_result',
496
+ title: isError ? '🔴 结果(失败)' : '🟢 结果',
497
+ content: truncateUtf8(raw, TOOL_RESULT_MAX_BYTES),
498
+ timestamp: safeEventTimestamp(ev.time),
499
+ });
600
500
  return out;
601
501
  }
502
+ /**
503
+ * 事件类型 → 命名 handler。用 Map 而非对象字面量 / switch:`constructor` /
504
+ * `toString` 这类外来 type 会命中 Object.prototype,查表会拿到非函数值。
505
+ */
506
+ const EVENT_MAPPERS = new Map([
507
+ ['user/message', mapUserMessage],
508
+ ['assistant/message', mapAssistantMessage],
509
+ ['tool/call', mapToolCall],
510
+ ['tool/result', mapToolResult],
511
+ ]);
512
+ function mapEvent(ev) {
513
+ const mapper = EVENT_MAPPERS.get(ev.type);
514
+ return mapper ? mapper(ev) : { events: [] };
515
+ }
602
516
  function stringifyContent(content) {
603
517
  if (content == null)
604
518
  return '[tool result]';
@@ -613,4 +527,140 @@ function stringifyContent(content) {
613
527
  }
614
528
  return JSON.stringify(content);
615
529
  }
530
+ function newDshContentAccumulator() {
531
+ return {
532
+ events: [],
533
+ cumInput: 0,
534
+ cumOutput: 0,
535
+ cumCacheRead: 0,
536
+ cumCacheWrite: 0,
537
+ turnUsage: null,
538
+ lastTurnUsage: null,
539
+ };
540
+ }
541
+ /** turn 边界标记推进:turn/start 开新账本,turn/end 结算为「上一轮」。 */
542
+ function advanceTurnMarker(acc, ev) {
543
+ if (ev.type === 'turn/start') {
544
+ acc.turnUsage = emptyUsageTotals();
545
+ }
546
+ else if (ev.type === 'turn/end') {
547
+ if (acc.turnUsage)
548
+ acc.lastTurnUsage = acc.turnUsage;
549
+ acc.turnUsage = null;
550
+ }
551
+ }
552
+ /** 工具调用/结果配对:一对合成一个面板;未配对的分支按原顺序推入。 */
553
+ function pushMappedContentEvents(acc, ev, contentEvents) {
554
+ if (contentEvents.pendingTool) {
555
+ // 未完成的工具调用最多同时一个(DSH 按 step 串行执行;并行调用时按
556
+ // 到达顺序配对,最坏情况是两次调用共用一个面板,不会丢信息)。
557
+ if (acc.pendingTool)
558
+ acc.events.push(acc.pendingTool);
559
+ acc.pendingTool = contentEvents.pendingTool;
560
+ }
561
+ else if (ev.type === 'tool/result' && acc.pendingTool && contentEvents.events[0]) {
562
+ const result = contentEvents.events[0];
563
+ // 标题沿用调用的 `🔧 name — summary`,失败改成 ❌(折叠态一眼看出结果)。
564
+ const failed = result.title?.startsWith('🔴') === true;
565
+ acc.events.push({
566
+ type: 'tool_use',
567
+ title: failed
568
+ ? (acc.pendingTool.title ?? '❌ 工具').replace('🔧', '❌')
569
+ : acc.pendingTool.title,
570
+ content: result.content,
571
+ timestamp: acc.pendingTool.timestamp,
572
+ });
573
+ acc.pendingTool = undefined;
574
+ }
575
+ else {
576
+ acc.events.push(...contentEvents.events);
577
+ }
578
+ }
579
+ /** 单条 usage:会话累计 + 在途 turn 累计 + 末次采样水位。 */
580
+ function accumulateUsage(acc, u) {
581
+ // Cumulative (session-wide): sum across ALL assistant/message usage.
582
+ acc.cumInput += u.inputTokens ?? 0;
583
+ acc.cumOutput += u.outputTokens ?? 0;
584
+ acc.cumCacheRead += u.cacheReadTokens ?? 0;
585
+ acc.cumCacheWrite += u.cacheWriteTokens ?? 0;
586
+ // 本 run 的 flow 字段 = 末个 turn 的采样之和;contextLength 用末次采样
587
+ // 的窗口占用(input + cacheRead + cacheWrite,不含 output)。
588
+ if (acc.turnUsage) {
589
+ acc.turnUsage.input += u.inputTokens ?? 0;
590
+ acc.turnUsage.output += u.outputTokens ?? 0;
591
+ acc.turnUsage.cacheRead += u.cacheReadTokens ?? 0;
592
+ acc.turnUsage.cacheWrite += u.cacheWriteTokens ?? 0;
593
+ }
594
+ acc.lastUsage = u;
595
+ }
596
+ /** 单条 record → 更新累积状态(无闭包留在 readSessionContent 内)。 */
597
+ function accumulateDshEvent(acc, ev) {
598
+ advanceTurnMarker(acc, ev);
599
+ const contentEvents = mapEvent(ev);
600
+ pushMappedContentEvents(acc, ev, contentEvents);
601
+ if (contentEvents.title)
602
+ acc.displayTitle = contentEvents.title;
603
+ if (contentEvents.usage)
604
+ accumulateUsage(acc, contentEvents.usage);
605
+ }
606
+ /** 末轮 flow + 会话累计 → AgentSessionUsage(§9.21 口径)。 */
607
+ function finalizeDshSessionUsage(acc) {
608
+ const lastUsage = acc.lastUsage;
609
+ if (!lastUsage)
610
+ return undefined;
611
+ const input = lastUsage.inputTokens ?? 0;
612
+ const cacheRead = lastUsage.cacheReadTokens ?? 0;
613
+ const cacheWrite = lastUsage.cacheWriteTokens ?? 0;
614
+ // 在途 turn(turn/start 无 turn/end,run 进行中读历史)的采样优先于
615
+ // 上一轮:旧实现只认已结算的 lastTurnUsage,mid-run 读取会把当前 turn
616
+ // 已消耗的量显示成上一个完整 turn 的数字。
617
+ const flow = acc.turnUsage ??
618
+ acc.lastTurnUsage ?? {
619
+ input,
620
+ output: lastUsage.outputTokens ?? 0,
621
+ cacheRead,
622
+ cacheWrite,
623
+ };
624
+ return {
625
+ inputTokens: flow.input,
626
+ outputTokens: flow.output,
627
+ // §9.21 口径:context 是末次采样的窗口水位(input + 末次 output +
628
+ // cache),不把整 turn 的 output 求和(前几步 output 已进入后续 input)。
629
+ contextLength: input + (lastUsage.outputTokens ?? 0) + cacheRead + cacheWrite,
630
+ cacheReadTokens: flow.cacheRead,
631
+ cacheCreationTokens: flow.cacheWrite,
632
+ totalTokens: flow.input + flow.output + flow.cacheRead + flow.cacheWrite,
633
+ ...cumulativeUsageFields({
634
+ input: acc.cumInput,
635
+ output: acc.cumOutput,
636
+ cacheRead: acc.cumCacheRead,
637
+ cacheCreation: acc.cumCacheWrite,
638
+ }),
639
+ };
640
+ }
641
+ /** session/page 响应 + 服务端预览 → 详情卡内容(纯结构装配)。 */
642
+ function buildDshSessionContent(value, preview, opts) {
643
+ const acc = newDshContentAccumulator();
644
+ for (const entry of value.records ?? []) {
645
+ const ev = entry.event;
646
+ if (!ev)
647
+ continue;
648
+ accumulateDshEvent(acc, ev);
649
+ }
650
+ const sessionUsage = finalizeDshSessionUsage(acc);
651
+ // 中断/仍在执行的调用没有结果:把它当独立事件留下,别让这一条消失。
652
+ if (acc.pendingTool)
653
+ acc.events.push(acc.pendingTool);
654
+ // 头部信息口径:最近输入优先用服务端末轮 prompt(事件里可能只是当前 turn
655
+ // 的一部分),标题用服务端 title;两者都缺时保持原行为。
656
+ const headerLastInput = preview.lastUserMessage ?? acc.displayTitle;
657
+ return {
658
+ // 展示仍按 maxEvents 尾截断(默认 50,保持原行为),usage 已在上面基于
659
+ // 完整 history 累加。
660
+ events: capEvents(acc.events, opts?.maxEvents ?? 50),
661
+ ...(sessionUsage ? { usage: sessionUsage } : {}),
662
+ ...(headerLastInput ? { displayTitle: headerLastInput } : {}),
663
+ ...(preview.title ? { aiTitle: preview.title } : {}),
664
+ };
665
+ }
616
666
  //# sourceMappingURL=sessions.js.map
@@ -99,13 +99,6 @@ export declare class KimiSessionReader implements AgentSessionReader {
99
99
  * Find a single index entry by sessionId, or undefined if not found.
100
100
  */
101
101
  private findSessionIndexEntry;
102
- /**
103
- * Extract the last turn.prompt text from a wire.jsonl file.
104
- * Used as a fallback summary source when state.json has no title/lastPrompt
105
- * (v2 sessions). Scans the tail of the file for efficiency.
106
- * Returns undefined if no turn.prompt is found.
107
- */
108
- private extractLastPromptFromWire;
109
102
  /**
110
103
  * List all sessions for a given working directory
111
104
  */