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
@@ -7,6 +7,7 @@ import { samePath, encodeProjectDirName } from '../../platform/path.js';
7
7
  import { extractContentBlocks } from '../common/content-blocks.js';
8
8
  import { scanJsonlOnce, tailOffsetAfter, scanTailEvents } from '../common/two-pass.js';
9
9
  import { getLogger } from '../../logger/index.js';
10
+ import { logNonEnoentDebug } from '../session-store.js';
10
11
  import { UsageAccumulator, contextWindowOccupancy, cumulativeUsageFields, } from '../common/usage-accumulator.js';
11
12
  import { SessionIndex, extractText, isTaskNotificationText, CLAUDE_MAPPING, } from './session-index.js';
12
13
  /**
@@ -19,7 +20,7 @@ import { SessionIndex, extractText, isTaskNotificationText, CLAUDE_MAPPING, } fr
19
20
  * Note: this encoding is LOSSY (`my_data/foo` and `my-data/foo` both encode to
20
21
  * `my-data-foo`). It is only safe to use the result to **locate** files; never
21
22
  * to **decode** a directory name back to a cwd. The cwd must always be read
22
- * from the JSONL content (regression 2026-06-21 /resume & /cd 路径错乱).
23
+ * from the JSONL content (regression: /resume & /cd 路径错乱).
23
24
  */
24
25
  function projectDirForCwd(cwd, projectsDir) {
25
26
  return path.join(projectsDir, encodeProjectDirName(cwd));
@@ -32,7 +33,7 @@ function projectDirForCwd(cwd, projectsDir) {
32
33
  * MULTIPLE cwd values (pre- and post-relocate) — accept when ANY `cwd` field
33
34
  * matches. Early-stops on the first match (findJsonlLine), keeping the
34
35
  * single-pass parse-count anchors intact on the fallback path.
35
- * Regression 2026-08-04 EnterWorktree relocate.
36
+ * Needed for EnterWorktree relocate.
36
37
  */
37
38
  function fileContainsCwd(filePath, cwd) {
38
39
  return (findJsonlLine(filePath, (l) => {
@@ -50,7 +51,7 @@ function defaultProjectsDir() {
50
51
  return path.join(os.homedir(), '.claude', 'projects');
51
52
  }
52
53
  /**
53
- * EnterWorktree relocate support (2026-08-04): Claude Code MOVES the
54
+ * EnterWorktree relocate support: Claude Code MOVES the
54
55
  * transcript file to the new cwd's project dir mid-session, so the file may
55
56
  * not exist under the requested cwd's encoded dir. Fall back to locating
56
57
  * `<sessionId>.jsonl` across all project dirs. Deterministic: entries are
@@ -148,7 +149,7 @@ export function getNewestSession(cwd, opts = {}) {
148
149
  * JSONL output continuously. A file whose mtime is older than
149
150
  * `STALE_MS` cannot be from a running process. This catches the
150
151
  * common Claude CLI 2.x case where the tail is `last-prompt` / `mode`
151
- * (no `type:"result"`) and the process exited long ago (regression 2026-06-21).
152
+ * (no `type:"result"`) and the process exited long ago.
152
153
  *
153
154
  * 2. **last-line terminal marker**: if the last JSON line is `result`
154
155
  * (older CLI) or any explicit terminal event, the session is done
@@ -176,7 +177,7 @@ function hasRecentFileActivity(filePath, mtimeMs, now = Date.now()) {
176
177
  // Explicit terminal markers — regardless of mtime. Claude CLI 2.x uses
177
178
  // `last-prompt` / `mode` (no `result` event) to mark turn end, so a
178
179
  // 24-minute-old session with a last-prompt tail is already done
179
- // (regression 2026-06-21: previously falsely reported as active).
180
+ // (previously falsely reported as active).
180
181
  if (typeof obj.type === 'string' && CLAUDE_TERMINAL_TYPES.has(obj.type))
181
182
  return false;
182
183
  }
@@ -190,9 +191,7 @@ function hasRecentFileActivity(filePath, mtimeMs, now = Date.now()) {
190
191
  * 否则「文件不可读」会被当成「会话不活跃」。
191
192
  */
192
193
  function logStatFailure(filePath, err) {
193
- if (err.code === 'ENOENT')
194
- return;
195
- getLogger().debug(`[claude-session-reader] stat ${filePath} failed: ${err?.message ?? String(err)}`);
194
+ logNonEnoentDebug(err, (m) => `[claude-session-reader] stat ${filePath} failed: ${m}`);
196
195
  }
197
196
  /**
198
197
  * Check if a Claude session (by sessionId + cwd) is still active.
@@ -273,115 +272,42 @@ function isRealUserPrompt(obj) {
273
272
  return true;
274
273
  }
275
274
  /**
276
- * first-pass scan: stream the file once via `scanJsonlLines`
277
- * (no `string[]` materialized), parse each line exactly once, and collect
278
- * all metadata needed by readSessionContent (usage, tailOffset, aiTitle,
279
- * recap, lastUserMessage). Does NOT retain parsed objects — the events tail
280
- * is re-parsed separately by `extractEventsFromTail` so memory for both raw
281
- * line strings and parsed objects is O(tail), not O(whole file).
275
+ * 两条采样逐字段取 max(纯函数;UsageAccumulator 的 per-field max 语义)。
276
+ *
277
+ * RED LINE(token 口径单源的一部分):一条 streaming assistant 消息跨多行
278
+ * jsonl、共享同一 message id;部分后端(第三方网关,如 DeepSeek)在早期占位
279
+ * 行(空 thinking 块)写全零 usage,真实用量只在带 stop_reason 的末行。
280
+ * first-occurrence-wins 会把零记录钉死、丢掉真实用量(regression 2026-08-19:
281
+ * auto-resume 卡 per-run 统计全 0)。逐字段 max 与行序无关地重建完整用量;
282
+ * 官方 API 每行重复相同 usage,max 是 no-op。
283
+ *
284
+ * 产生:合并后的一条采样。依赖:入参。不改任何外部状态。
282
285
  */
283
- function scalarScan(filePath) {
286
+ export function maxUsageRecord(prev, usage, turnSeq) {
287
+ return {
288
+ input: Math.max(prev?.input ?? 0, usage.input_tokens ?? 0),
289
+ output: Math.max(prev?.output ?? 0, usage.output_tokens ?? 0),
290
+ cacheRead: Math.max(prev?.cacheRead ?? 0, usage.cache_read_input_tokens ?? 0),
291
+ cacheCreation: Math.max(prev?.cacheCreation ?? 0, usage.cache_creation_input_tokens ?? 0),
292
+ turn: prev?.turn ?? turnSeq,
293
+ };
294
+ }
295
+ /**
296
+ * usage 装配(纯函数):每个 message id 一条最优记录、按首次出现顺序喂
297
+ * UsageAccumulator(Map 保序),acc.last = 末条 message 的完整用量,totals
298
+ * 让每条 API 响应恰好多计一次。per-run flow 只取末个真人 turn 的记录——
299
+ * 工具往返的 turn 有多条 assistant 消息(一次模型请求一条),只取 `last`
300
+ * 会低报。
301
+ *
302
+ * 产生:AgentSessionUsage。依赖:入参 records + 标量。不改任何外部状态
303
+ * (UsageAccumulator 在函数内新建)。
304
+ */
305
+ export function buildClaudeUsage(records, opts) {
284
306
  const acc = new UsageAccumulator();
285
- // Per-message-id best usage record (per-field max). A streaming assistant
286
- // message spans multiple jsonl lines sharing the same message id; some
287
- // backends (third-party gateways, e.g. DeepSeek) write all-zero usage on
288
- // the early placeholder lines (empty thinking block) and the real usage
289
- // only on the final line carrying stop_reason. First-occurrence-wins dedup
290
- // would pin the zero record and discard the real one (regression
291
- // 2026-08-19: auto-resume card per-run stats all 0). Per-field max
292
- // reconstructs the full usage regardless of line order — on the official
293
- // API every line repeats identical usage, so max is a no-op there.
294
- const usageByMessageId = new Map();
295
- let lastPostTokens = 0;
296
- let tailOffset = -1;
297
- /** 真人 prompt 序号;末个 turn = 当前值。sidechain/注入/工具结果不递增。 */
298
- let turnSeq = 0;
299
- let lastAiTitle;
300
- let lastRecap;
301
- let lastUserMessage;
302
- scanJsonlOnce(filePath, (obj, line, offset) => {
303
- // --- usage aggregation (was aggregateSessionUsage) ---
304
- if (obj.type === 'assistant') {
305
- const msg = obj.message;
306
- const usage = msg?.usage;
307
- if (usage) {
308
- const msgId = msg?.id;
309
- if (msgId) {
310
- const prev = usageByMessageId.get(msgId);
311
- usageByMessageId.set(msgId, {
312
- input: Math.max(prev?.input ?? 0, usage.input_tokens ?? 0),
313
- output: Math.max(prev?.output ?? 0, usage.output_tokens ?? 0),
314
- cacheRead: Math.max(prev?.cacheRead ?? 0, usage.cache_read_input_tokens ?? 0),
315
- cacheCreation: Math.max(prev?.cacheCreation ?? 0, usage.cache_creation_input_tokens ?? 0),
316
- turn: prev?.turn ?? turnSeq,
317
- });
318
- }
319
- }
320
- }
321
- if (obj.type === 'system' && obj.subtype === 'compact_boundary') {
322
- acc.bumpCompact();
323
- const cm = obj.compactMetadata;
324
- if (cm?.postTokens) {
325
- lastPostTokens = cm.postTokens;
326
- }
327
- }
328
- // --- last user message offset ---
329
- // Record the byte offset where the tail should start: the beginning of
330
- // the NEXT line after this user message (see tailOffsetAfter).
331
- if (obj.type === 'user') {
332
- const msg = obj.message;
333
- if (msg?.role === 'user') {
334
- tailOffset = tailOffsetAfter(line, offset);
335
- }
336
- }
337
- // --- ai-title extraction ---
338
- if (obj.type === 'ai-title' && typeof obj.aiTitle === 'string') {
339
- lastAiTitle = obj.aiTitle;
340
- }
341
- // --- recap extraction (last isCompactSummary wins) ---
342
- if (obj.type === 'user' && obj.isCompactSummary === true) {
343
- const msg = obj.message;
344
- if (msg?.content) {
345
- const recapContent = extractText(msg.content);
346
- if (recapContent) {
347
- lastRecap = recapContent;
348
- }
349
- }
350
- }
351
- // --- last user message for displayTitle ---
352
- // Skip: 1) tool_result content, 2) isCompactSummary (recap),
353
- // 3) isMeta (skill injection / command echo / project rules)
354
- if (obj.type === 'user' && !obj.isCompactSummary && !obj.isMeta) {
355
- const msg = obj.message;
356
- if (msg?.role === 'user' && msg.content) {
357
- const content = msg.content;
358
- const isToolResult = Array.isArray(content) &&
359
- content.length > 0 &&
360
- content[0]?.type === 'tool_result';
361
- if (!isToolResult) {
362
- const userContent = extractText(content);
363
- if (userContent && !isTaskNotificationText(userContent)) {
364
- lastUserMessage = userContent;
365
- }
366
- }
367
- }
368
- }
369
- // --- turn boundary for per-run usage ---
370
- // 真人 prompt 开启新 turn;注入/工具结果/sidechain 不递增。usage 记录在
371
- // 解析到 assistant 行时已经带上当前 turnSeq,末尾只汇总末个 turn。
372
- if (isRealUserPrompt(obj))
373
- turnSeq++;
374
- });
375
- // Feed one best record per message id, in first-appearance order (Map
376
- // preserves insertion order), so acc.last is the LAST message's full usage
377
- // and totals sum each API response exactly once. Per-run flow is only the
378
- // records belonging to the last real-user turn — a tool-looping turn has
379
- // several assistant messages (one per model request), so `last` alone
380
- // under-reports it.
381
307
  const turn = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
382
- for (const record of usageByMessageId.values()) {
308
+ for (const record of records) {
383
309
  acc.add(record);
384
- if (turnSeq > 0 && record.turn === turnSeq) {
310
+ if (opts.turnSeq > 0 && record.turn === opts.turnSeq) {
385
311
  turn.input += record.input;
386
312
  turn.output += record.output;
387
313
  turn.cacheRead += record.cacheRead;
@@ -393,7 +319,7 @@ function scalarScan(filePath) {
393
319
  const l = acc.last;
394
320
  // 没有真人 user prompt(旧数据/截断文件)→ 无法划分 turn,回退末条采样,
395
321
  // 与 dsh/codex「无 turn 标记退回末条」一致;不能把整份文件当成一轮。
396
- if (turnSeq === 0 && l) {
322
+ if (opts.turnSeq === 0 && l) {
397
323
  turn.input = l.input;
398
324
  turn.output = l.output;
399
325
  turn.cacheRead = l.cacheRead;
@@ -406,7 +332,7 @@ function scalarScan(filePath) {
406
332
  // the input window). Take the max so a pre-compact turn larger than the
407
333
  // post-compact size is not under-reported.
408
334
  const lastWindow = l ? contextWindowOccupancy(l) : 0;
409
- let contextLength = lastPostTokens;
335
+ let contextLength = opts.lastPostTokens;
410
336
  if (contextLength === 0 || lastWindow > contextLength) {
411
337
  contextLength = lastWindow;
412
338
  }
@@ -415,22 +341,134 @@ function scalarScan(filePath) {
415
341
  // `total_token_usage`=session 累计)——否则 jsonl 兜底路径会把 session 累计
416
342
  // 当成"本 run"显示,与卡片"本 run ≤ 累计"不变量冲突(累计反而更小)。
417
343
  const lastTotal = turn.input + turn.output + turn.cacheRead + turn.cacheCreation;
418
- const usage = {
344
+ return {
419
345
  inputTokens: turn.input,
420
346
  outputTokens: turn.output,
421
347
  contextLength,
422
- compactCount: acc.compactCount,
348
+ compactCount: opts.compactCount,
423
349
  cacheReadTokens: turn.cacheRead > 0 ? turn.cacheRead : undefined,
424
350
  cacheCreationTokens: turn.cacheCreation > 0 ? turn.cacheCreation : undefined,
425
351
  totalTokens: lastTotal,
426
352
  ...cumulativeUsageFields(t),
427
353
  };
354
+ }
355
+ function newScalarScanState() {
356
+ // Per-message-id best usage record (per-field max). A streaming assistant
357
+ // message spans multiple jsonl lines sharing the same message id; some
358
+ // backends (third-party gateways, e.g. DeepSeek) write all-zero usage on
359
+ // the early placeholder lines (empty thinking block) and the real usage
360
+ // only on the final line carrying stop_reason. First-occurrence-wins dedup
361
+ // would pin the zero record and discard the real one (auto-resume card
362
+ // per-run stats all 0). Per-field max reconstructs the full usage
363
+ // regardless of line order — on the official API every line repeats
364
+ // identical usage, so max is a no-op there.
428
365
  return {
429
- tailOffset,
430
- usage,
431
- aiTitle: lastAiTitle,
432
- recap: lastRecap,
433
- lastUserMessage,
366
+ usageByMessageId: new Map(),
367
+ lastPostTokens: 0,
368
+ compactCount: 0,
369
+ tailOffset: -1,
370
+ turnSeq: 0,
371
+ };
372
+ }
373
+ /** assistant 行 → 按 message id 逐字段 max 合并 usage(was aggregateSessionUsage)。 */
374
+ function collectAssistantUsage(state, obj) {
375
+ if (obj.type !== 'assistant')
376
+ return;
377
+ const msg = obj.message;
378
+ const usage = msg?.usage;
379
+ if (!usage)
380
+ return;
381
+ const msgId = msg?.id;
382
+ if (!msgId)
383
+ return;
384
+ state.usageByMessageId.set(msgId, maxUsageRecord(state.usageByMessageId.get(msgId), usage, state.turnSeq));
385
+ }
386
+ /** compact_boundary → 计数 + 压缩后 token 水位。 */
387
+ function collectCompactBoundary(state, obj) {
388
+ if (obj.type !== 'system' || obj.subtype !== 'compact_boundary')
389
+ return;
390
+ state.compactCount++;
391
+ const cm = obj.compactMetadata;
392
+ if (cm?.postTokens) {
393
+ state.lastPostTokens = cm.postTokens;
394
+ }
395
+ }
396
+ /** 记录 tail 起点:本 user 行之后的下一行字节偏移(见 tailOffsetAfter)。 */
397
+ function collectTailOffset(state, obj, line, offset) {
398
+ if (obj.type !== 'user')
399
+ return;
400
+ const msg = obj.message;
401
+ if (msg?.role === 'user') {
402
+ state.tailOffset = tailOffsetAfter(line, offset);
403
+ }
404
+ }
405
+ /** ai-title:最后一条生效。 */
406
+ function collectAiTitle(state, obj) {
407
+ if (obj.type === 'ai-title' && typeof obj.aiTitle === 'string') {
408
+ state.lastAiTitle = obj.aiTitle;
409
+ }
410
+ }
411
+ /** recap:最后一条 isCompactSummary 生效。 */
412
+ function collectRecap(state, obj) {
413
+ if (obj.type !== 'user' || obj.isCompactSummary !== true)
414
+ return;
415
+ const msg = obj.message;
416
+ if (!msg?.content)
417
+ return;
418
+ const recapContent = extractText(msg.content);
419
+ if (recapContent) {
420
+ state.lastRecap = recapContent;
421
+ }
422
+ }
423
+ /** displayTitle 用的最近真人输入:跳过 tool_result / recap / isMeta / 任务通知。 */
424
+ function collectLastUserMessage(state, obj) {
425
+ if (obj.type !== 'user' || obj.isCompactSummary || obj.isMeta)
426
+ return;
427
+ const msg = obj.message;
428
+ if (msg?.role !== 'user' || !msg.content)
429
+ return;
430
+ const content = msg.content;
431
+ const isToolResult = Array.isArray(content) &&
432
+ content.length > 0 &&
433
+ content[0]?.type === 'tool_result';
434
+ if (isToolResult)
435
+ return;
436
+ const userContent = extractText(content);
437
+ if (userContent && !isTaskNotificationText(userContent)) {
438
+ state.lastUserMessage = userContent;
439
+ }
440
+ }
441
+ /** 真人 prompt 开启新 turn;注入/工具结果/sidechain 不递增。 */
442
+ function countRealUserPrompt(state, obj) {
443
+ if (isRealUserPrompt(obj))
444
+ state.turnSeq++;
445
+ }
446
+ /**
447
+ * 单行分派:收集器调用顺序与原回调逐字一致(尤其 turnSeq 在最后递增,
448
+ * 保证 assistant usage 记录归属的是「本行之前」的 turn)。
449
+ */
450
+ function scanScalarLine(state, obj, line, offset) {
451
+ collectAssistantUsage(state, obj);
452
+ collectCompactBoundary(state, obj);
453
+ collectTailOffset(state, obj, line, offset);
454
+ collectAiTitle(state, obj);
455
+ collectRecap(state, obj);
456
+ collectLastUserMessage(state, obj);
457
+ countRealUserPrompt(state, obj);
458
+ }
459
+ function scalarScan(filePath) {
460
+ const state = newScalarScanState();
461
+ scanJsonlOnce(filePath, scanScalarLine.bind(null, state));
462
+ return {
463
+ tailOffset: state.tailOffset,
464
+ usage: buildClaudeUsage(state.usageByMessageId.values(), {
465
+ turnSeq: state.turnSeq,
466
+ lastPostTokens: state.lastPostTokens,
467
+ compactCount: state.compactCount,
468
+ }),
469
+ aiTitle: state.lastAiTitle,
470
+ recap: state.lastRecap,
471
+ lastUserMessage: state.lastUserMessage,
434
472
  };
435
473
  }
436
474
  /**