lark-remote 0.5.3 → 0.5.4

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.4] - 2026-10-12
9
+
10
+ ### 修复
11
+
12
+ - **codex「本 run」token 少算(多步运行只统计了最后一步)**:codex 每发起一次模型调用就记一条 token 用量,一个回合里有几趟工具往返就有几条,而运行结束卡片与 `/resume` 末尾统计此前只取最后一条。于是一个跑了 7 趟工具往返的回合,实际输出 3,648 个 token 只显示 679、未命中缓存的输入 3,022 只显示 212。现在按回合汇总该回合内所有调用的用量,与 codex 自己按回合统计的结果逐字段一致;「Context」仍显示最后一次调用的上下文窗口占用,累计用量与 `Total` 的取法不变。恢复会话时服务端可能重放上回合的用量,已按回合标识丢弃。更早的、不带回合标识的记录保持原读法
13
+
14
+ ### 变更
15
+
16
+ - **测试注释与用例类型对齐**:一处用例注释改为不点名具体消息;两处 app-server 用例的类型写法与协议定义对齐(这些文件不在既有类型检查覆盖范围内,改动不影响运行时行为)。无行为变化
17
+
8
18
  ## [0.5.3] - 2026-10-12
9
19
 
10
20
  ### 修复
@@ -222,6 +222,11 @@ export class CodexAppServerTranslator {
222
222
  this.threadId = params.threadId;
223
223
  this.turnId = params.turn.id;
224
224
  this.turnStatus = params.turn.status ?? 'inProgress';
225
+ // 单 turn 用量从零开始累计:turn 内每次采样都会发一条
226
+ // `thread/tokenUsage/updated`,本 run 的用量是这些增量之和(不是最后一条)。
227
+ // resume 后服务端重放的上一 turn 用量若在此之后到达,由 handleTokenUsageUpdated
228
+ // 的 turnId 闸门挡掉。
229
+ this.usage = { inputTokens: 0, outputTokens: 0 };
225
230
  this.textByItem.clear();
226
231
  this.reasoningByItem.clear();
227
232
  this.toolOutputByItem.clear();
@@ -455,22 +460,31 @@ export class CodexAppServerTranslator {
455
460
  ];
456
461
  }
457
462
  handleTokenUsageUpdated(params) {
463
+ // 归属闸门:resume 之后服务端可能重放上一 turn 的 tokenUsage(turnId 不同),
464
+ // 计入会把上一 turn 的消耗算成本 run。本轮 turnId 由 turn/started 确立。
465
+ if (params.turnId && this.turnId && params.turnId !== this.turnId) {
466
+ return [];
467
+ }
458
468
  const last = params.tokenUsage.last;
459
469
  // 协议原生 inputTokens 含 cached(total = input + output),而 lark-remote 的
460
470
  // 卡片契约是 ccusage 口径「input_tokens 表示未命中缓存的输入」(design §9.21)。
461
471
  // 在入口归一,否则 done 卡 Input 会等于 Context、cache% 分母重复计数、
462
472
  // Total 被 max(total, input+output+cache) 抬高一倍(实测 364K → 727K)。
463
473
  // jsonl 链路有同一处减法(session/codex/rollout-reader.ts buildUsageFromScan)。
474
+ // 单 turn 口径:一个 turn 内每次采样各发一条通知(实测 7 次工具往返 = 7 条),
475
+ // 本 run 是这些增量之和——只取最后一条会把多步 run 低报(实测 3,648 output 显示成 679)。
464
476
  const cachedInputTokens = last.cachedInputTokens === undefined
465
477
  ? undefined
466
478
  : Math.min(last.cachedInputTokens, last.inputTokens);
467
- this.usage = {
468
- inputTokens: last.inputTokens - (cachedInputTokens ?? 0),
469
- outputTokens: last.outputTokens,
470
- cachedInputTokens,
471
- totalTokens: last.totalTokens,
472
- contextLimit: params.tokenUsage.modelContextWindow,
473
- };
479
+ this.usage.inputTokens += last.inputTokens - (cachedInputTokens ?? 0);
480
+ this.usage.outputTokens += last.outputTokens;
481
+ if (cachedInputTokens !== undefined) {
482
+ this.usage.cachedInputTokens = (this.usage.cachedInputTokens ?? 0) + cachedInputTokens;
483
+ }
484
+ if (last.totalTokens !== undefined) {
485
+ this.usage.totalTokens = (this.usage.totalTokens ?? 0) + last.totalTokens;
486
+ }
487
+ this.usage.contextLimit = params.tokenUsage.modelContextWindow ?? this.usage.contextLimit;
474
488
  return [];
475
489
  }
476
490
  /**
@@ -42,6 +42,9 @@ function subtractRawUsage(a, b) {
42
42
  total_tokens: a.total_tokens - b.total_tokens,
43
43
  };
44
44
  }
45
+ function emptyTurnUsage() {
46
+ return { input: 0, cached: 0, output: 0, total: 0 };
47
+ }
45
48
  /**
46
49
  * `compacted` 事件自带的摘要调用 usage(`latest_token_usage_record.usage`)。
47
50
  * 用于把「压缩前窗口」与「摘要调用自身的消耗」区分开——两者都写在 compacted 前。
@@ -141,12 +144,19 @@ function scanCodexRollout(filePath, includeEvents) {
141
144
  // exactly the 0.153 compatibility incident shape.
142
145
  let userResponseItemCount = 0;
143
146
  // token_count tracking: codex emits cumulative `total_token_usage` and an
144
- // incremental `last_token_usage`. We want the LAST turn's usage (matches
145
- // pi/opencode /resume "last turn" display semantics). When
147
+ // incremental `last_token_usage` **per sampling call** — one turn with tool
148
+ // round-trips logs several `token_count` lines. We want the LAST turn's usage
149
+ // = the sum of that turn's samplings (matches pi/opencode /resume "last turn"
150
+ // display semantics). Turn boundaries come from `task_started`. When
146
151
  // last_token_usage is absent we derive it as total - previous_total.
147
152
  let lastRawUsage;
148
153
  /** 上一条被接受的 raw usage;compacted 分支回退一条时用(见下方注释)。 */
149
154
  let previousRawUsage;
155
+ /**
156
+ * 末个 turn 的累计用量;`null` = 文件里没有 `task_started`(旧数据),
157
+ * 此时 flow 字段退回「末条 token_count」语义(见 buildUsageFromScan)。
158
+ */
159
+ let turnUsage = null;
150
160
  // Context window limit from the same token_count event as lastRawUsage
151
161
  // (codex reports info.model_context_window per turn; absent on old data).
152
162
  let lastContextLimit;
@@ -194,6 +204,12 @@ function scanCodexRollout(filePath, includeEvents) {
194
204
  }
195
205
  lastUserMessageSource = human.source;
196
206
  }
207
+ else if (payload.type === 'task_started') {
208
+ // turn 边界:一个 turn 一条 task_started(与 task_complete 成对,
209
+ // turn_id 见 session_meta 之后的 `turn_context`)。从这里开始重新
210
+ // 累计本 turn 的用量。
211
+ turnUsage = emptyTurnUsage();
212
+ }
197
213
  else if (payload.type === 'token_count') {
198
214
  const info = payload.info;
199
215
  const last = info?.last_token_usage;
@@ -217,6 +233,13 @@ function scanCodexRollout(filePath, includeEvents) {
217
233
  lastRawUsage = raw;
218
234
  const ctxWindow = info?.model_context_window;
219
235
  lastContextLimit = typeof ctxWindow === 'number' ? ctxWindow : undefined;
236
+ if (turnUsage) {
237
+ const cached = Math.min(raw.cached_input_tokens, raw.input_tokens);
238
+ turnUsage.input += raw.input_tokens - cached;
239
+ turnUsage.cached += cached;
240
+ turnUsage.output += raw.output_tokens;
241
+ turnUsage.total += raw.total_tokens;
242
+ }
220
243
  }
221
244
  // 压缩后的水位:压缩收尾 token_count 全 0(raw 不满足上面 zero-filter),
222
245
  // 但 total_tokens 表达真实窗口;之后若出现普通 turn(input>0),
@@ -288,6 +311,7 @@ function scanCodexRollout(filePath, includeEvents) {
288
311
  lastContextLimit,
289
312
  lastTotalUsage,
290
313
  previousTotals,
314
+ turnUsage,
291
315
  compactCount,
292
316
  compactPreContextLength,
293
317
  compactPostContextLength,
@@ -298,25 +322,38 @@ function scanCodexRollout(filePath, includeEvents) {
298
322
  * input = raw - cached (codex reports cached input separately), cache
299
323
  * creation is never reported by codex, reasoning is a subset of output
300
324
  * (display-only, never added to total).
325
+ *
326
+ * Flow fields (input/output/cache/total) are the **sum over the last turn's
327
+ * samplings** when the file marks turn boundaries (`task_started`) — one codex
328
+ * turn logs one `token_count` per sampling, so the last line alone understates
329
+ * a tool-looping run (measured: 212 / 679 displayed vs 3,022 / 3,648 consumed).
330
+ * Legacy files without `task_started` keep the last-`token_count` semantics.
301
331
  * contextLength = input_tokens (= (input_tokens-cached) + cached + 0),
302
332
  * i.e. input + cacheRead + cacheCreation — the unified context-window
303
333
  * occupancy contract across all five readers (excludes
304
334
  * output/reasoning).
305
335
  */
306
336
  function buildUsageFromScan(scan) {
307
- const { lastRawUsage, lastContextLimit, lastTotalUsage, compactCount, compactPreContextLength, compactPostContextLength, } = scan;
337
+ const { lastRawUsage, lastContextLimit, lastTotalUsage, turnUsage, compactCount, compactPreContextLength, compactPostContextLength, } = scan;
308
338
  if (!lastRawUsage)
309
339
  return undefined;
310
340
  const r = lastRawUsage;
311
341
  const cached = Math.min(r.cached_input_tokens, r.input_tokens);
342
+ // 本 run = 末个 turn 全部采样之和;旧数据(无 task_started)退回末条。
343
+ const flow = turnUsage ?? {
344
+ input: r.input_tokens - cached,
345
+ cached,
346
+ output: r.output_tokens,
347
+ total: r.total_tokens,
348
+ };
312
349
  const usage = {
313
- inputTokens: r.input_tokens - cached,
314
- outputTokens: r.output_tokens,
350
+ inputTokens: flow.input,
351
+ outputTokens: flow.output,
315
352
  contextLength: r.input_tokens,
316
353
  contextLimit: lastContextLimit,
317
- cacheReadTokens: cached,
354
+ cacheReadTokens: flow.cached,
318
355
  cacheCreationTokens: 0,
319
- totalTokens: r.total_tokens,
356
+ totalTokens: flow.total,
320
357
  };
321
358
  // Cumulative from the final total_token_usage (non-cached input +
322
359
  // output), summed across all turns in the session.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",