lark-remote 0.5.2 → 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 +20 -0
- package/dist/bridge/index.js +27 -1
- package/dist/cli.js +2 -2
- package/dist/inbound/receipt.d.ts +1 -1
- package/dist/inbound/receipt.js +1 -1
- package/dist/platform/terminator-posix.js +1 -1
- package/dist/platform/terminator-win32.js +1 -1
- package/dist/runner/codex/app-server/translator.js +21 -7
- package/dist/runner/common/connection-based-runner.d.ts +14 -0
- package/dist/runner/common/connection-based-runner.js +29 -1
- package/dist/session/codex/rollout-reader.js +44 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ 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
|
+
|
|
18
|
+
## [0.5.3] - 2026-10-12
|
|
19
|
+
|
|
20
|
+
### 修复
|
|
21
|
+
|
|
22
|
+
- **中断后紧接着发下一条消息,不再报 `thread already has an active writer`**:点停止打断正在运行的 codex 时,中断流程会把复用的运行实例从缓存摘掉并立即销毁连接,而队列链在这一步的等待窗口里就把下一条消息发了出去、并已取到同一个实例——那条运行的长驻连接于是挂在无人托管的实例上,`codex app-server` 进程要等空闲超时(默认 30 分钟)才回收,期间一直持有该会话的写锁,同一会话的下一次恢复因此被拒。现在中断只把实例标为「待替换」,统一由下一次运行收尾时回收,标过的实例不再返回缓存;被摘掉的实例也会在运行结束时主动关闭连接,不再挂 30 分钟的超时。逐条消息新建实例的 agent 不受影响,行为照旧
|
|
23
|
+
|
|
24
|
+
### 变更
|
|
25
|
+
|
|
26
|
+
- **注释里的运维现场数据与仓库外引用清理**:一处测试注释记录的崩溃进程号、实例名与精确到秒的重启时间改为占位描述;若干注释不再指向仓库之外的维护规范文件名。无行为变化
|
|
27
|
+
|
|
8
28
|
## [0.5.2] - 2026-10-11
|
|
9
29
|
|
|
10
30
|
### 修复
|
package/dist/bridge/index.js
CHANGED
|
@@ -320,6 +320,17 @@ export class Bridge {
|
|
|
320
320
|
if (evicted && typeof evicted.unregisterExitHandlers === 'function') {
|
|
321
321
|
evicted.unregisterExitHandlers();
|
|
322
322
|
}
|
|
323
|
+
// 2026-10-11:先标记 orphan 再 dispose。evict 与「run 刚拿走本实例」竞态时
|
|
324
|
+
// (queue 链在 interruptCurrentRun await 期间推进),dispose 只能关掉当时
|
|
325
|
+
// 已存在的连接,run 后续 acquire 的新连接会挂在无人托管的实例上——orphan
|
|
326
|
+
// 标记让 executeTurn 在 run 结束时主动 dispose 这批连接,而不是挂 idle
|
|
327
|
+
// timer 等待 30 分钟(期间持有 agent 侧会话写锁,codex 报
|
|
328
|
+
// "thread already has an active writer")。鸭子探测与 dispose/unregister
|
|
329
|
+
// 一致:claude(stream-json,无长驻连接)无此方法,跳过。
|
|
330
|
+
if (evicted &&
|
|
331
|
+
typeof evicted.markOrphaned === 'function') {
|
|
332
|
+
evicted.markOrphaned();
|
|
333
|
+
}
|
|
323
334
|
// App-server runner owns a persistent connection — dispose it so the
|
|
324
335
|
// `codex app-server` child process does not leak across runs/evictions.
|
|
325
336
|
if (evicted && typeof evicted.dispose === 'function') {
|
|
@@ -574,7 +585,22 @@ export class Bridge {
|
|
|
574
585
|
// (eviction must also unregister the runner from the exit dispatcher)
|
|
575
586
|
// evict by the run's captured agentKind, not the live
|
|
576
587
|
// defaultAgent (may have changed mid-run via /config).
|
|
577
|
-
|
|
588
|
+
//
|
|
589
|
+
// 2026-10-11:workspace-lifetime runner 不能在这里立即 evict+dispose。
|
|
590
|
+
// interruptCurrentRun 的 await 窗口内 queue 链会推进下一条消息,其
|
|
591
|
+
// getRunner 可能刚拿走本实例(缓存命中先于本行执行)——立即 evict 会
|
|
592
|
+
// 让那个 run 的长驻连接脱管:进程残留至 idle TTL 才回收,期间持有
|
|
593
|
+
// agent 侧会话写锁(codex "thread already has an active writer",
|
|
594
|
+
// 线上实测)。改为标 stale,由 finalizeRun 在下一次 run 结束时统一
|
|
595
|
+
// evict+dispose;「stop 后下一 run 换新实例」的语义
|
|
596
|
+
// 保留(stale 命中即最后一轮复用)。run-per-message runner 无长驻
|
|
597
|
+
// 连接,保持立即 evict 不变。
|
|
598
|
+
if (active.runner.lifetime === 'workspace') {
|
|
599
|
+
this.staleRunnerSlots.add(`${cwd}\u0000${active.agentKind}`);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
this.evictRunnerSlot(cwd, active.agentKind);
|
|
603
|
+
}
|
|
578
604
|
// Reset queue executing count: the task was interrupted externally, so
|
|
579
605
|
// its promise-chain settle (.then/.catch) may lag behind the process
|
|
580
606
|
// kill. Without this reset, subsequent messages would incorrectly show
|
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* CLI 引导层(bootstrap):在 import 应用模块图之前决定运行时(bun/node)并拉起它。
|
|
4
4
|
*
|
|
5
|
-
* **这是全仓唯一被认可的 spawn
|
|
6
|
-
* platform/spawn.ts
|
|
5
|
+
* **这是全仓唯一被认可的 spawn 收口例外**(红线:子进程 spawn 只走
|
|
6
|
+
* platform/spawn.ts,此处显式豁免)。三个理由缺一不可:
|
|
7
7
|
* 1. 时序:preflight(Node 版本守卫、-v/-h/--advanced-help)必须在 import 应用
|
|
8
8
|
* 模块图之前跑完——dist/cli.js 刻意不使用 ES2020+ 语法以便在 node 12 上仍
|
|
9
9
|
* 可被解析,而 dist/index.js 是 ES2022;先 import platform/spawn 就等于先把
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 2. 拿不到的东西(不支持类型 / 超限 / 下载失败 / 媒体保存已关闭):明确告知,
|
|
7
7
|
* 而不是把占位符塞给 agent。
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 命名纪律:用户可见文案指代本程序时只能写 `lark-remote`。
|
|
10
10
|
*/
|
|
11
11
|
import type { PlaceholderKind } from './placeholder.js';
|
|
12
12
|
import type { RejectedItem } from './turn.js';
|
package/dist/inbound/receipt.js
CHANGED
|
@@ -9,7 +9,7 @@ function defaultLog(level, message) {
|
|
|
9
9
|
else
|
|
10
10
|
logger.info(message);
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
12
|
+
/** 退出判定统一口径(红线):exitCode !== null || signalCode !== null。 */
|
|
13
13
|
function isAlive(proc) {
|
|
14
14
|
return proc.exitCode === null && proc.signalCode === null;
|
|
15
15
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
14
14
|
import { agentStopperRegistry } from './agent-stopper.js';
|
|
15
|
-
/**
|
|
15
|
+
/** 退出判定统一口径(红线):exitCode !== null || signalCode !== null。 */
|
|
16
16
|
function isAlive(proc) {
|
|
17
17
|
return proc.exitCode === null && proc.signalCode === null;
|
|
18
18
|
}
|
|
@@ -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
|
-
|
|
469
|
-
|
|
470
|
-
cachedInputTokens
|
|
471
|
-
|
|
472
|
-
|
|
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
|
/**
|
|
@@ -31,6 +31,10 @@ export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent
|
|
|
31
31
|
protected forceFinish: boolean;
|
|
32
32
|
/** stop() 在 turn/start 或 prompt 响应前到达时置位,完成后再补发 cancel/interrupt。 */
|
|
33
33
|
protected stopRequested: boolean;
|
|
34
|
+
/** 缓存层除名标记(evictRunnerSlot → markOrphaned)。置位后 run 结束时主动
|
|
35
|
+
* dispose 全部连接而不是挂 idle timer:被除名实例上创建的连接无人托管,
|
|
36
|
+
* 不主动收会残留进程至 idle TTL(30 分钟),期间持有 agent 侧会话写锁。 */
|
|
37
|
+
private orphaned;
|
|
34
38
|
protected constructor(opts: {
|
|
35
39
|
kind: AgentKind;
|
|
36
40
|
sessionReader: AgentSessionReader;
|
|
@@ -96,6 +100,16 @@ export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent
|
|
|
96
100
|
* Dispose the runner: release the connection.
|
|
97
101
|
*/
|
|
98
102
|
dispose(): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Mark this runner as evicted from the bridge's runner cache while a run may
|
|
105
|
+
* still be holding/creating connections on it (queue-advance race during
|
|
106
|
+
* interruptCurrentRun's await window, 2026-10-11). The active run's finally
|
|
107
|
+
* disposes connections eagerly instead of arming the idle TTL timer —
|
|
108
|
+
* otherwise the agent server process lingers for up to 30 minutes holding
|
|
109
|
+
* its session write lock (codex: "thread already has an active writer").
|
|
110
|
+
* One-way by design: an evicted instance never returns to the cache.
|
|
111
|
+
*/
|
|
112
|
+
markOrphaned(): void;
|
|
99
113
|
getUsageAuthority(): 'live';
|
|
100
114
|
killOrphan(): void;
|
|
101
115
|
registerExitHandlers(): void;
|
|
@@ -22,6 +22,10 @@ export class ConnectionBasedRunner {
|
|
|
22
22
|
forceFinish = false;
|
|
23
23
|
/** stop() 在 turn/start 或 prompt 响应前到达时置位,完成后再补发 cancel/interrupt。 */
|
|
24
24
|
stopRequested = false;
|
|
25
|
+
/** 缓存层除名标记(evictRunnerSlot → markOrphaned)。置位后 run 结束时主动
|
|
26
|
+
* dispose 全部连接而不是挂 idle timer:被除名实例上创建的连接无人托管,
|
|
27
|
+
* 不主动收会残留进程至 idle TTL(30 分钟),期间持有 agent 侧会话写锁。 */
|
|
28
|
+
orphaned = false;
|
|
25
29
|
constructor(opts, defaultTurnIdleTimeoutMs = TURN_IDLE_TIMEOUT_MS) {
|
|
26
30
|
this.kind = opts.kind;
|
|
27
31
|
this.sessionReader = opts.sessionReader;
|
|
@@ -130,7 +134,19 @@ export class ConnectionBasedRunner {
|
|
|
130
134
|
this.currentTurnId = null;
|
|
131
135
|
this.stopRequested = false;
|
|
132
136
|
this.clearTurnState();
|
|
133
|
-
this.
|
|
137
|
+
if (this.orphaned) {
|
|
138
|
+
// 已被缓存层除名(evictRunnerSlot → markOrphaned):本次 run 创建的
|
|
139
|
+
// 连接挂在无人托管的实例上。主动收掉而不是挂 idle timer——否则 agent
|
|
140
|
+
// server 进程残留至 idle TTL,期间持有 agent 侧会话写锁(codex
|
|
141
|
+
// "thread already has an active writer",2026-10-11 线上实测)。
|
|
142
|
+
// dispose() 内部自带 try/catch,这里 fire-and-forget 不阻塞事件流收尾。
|
|
143
|
+
void this.disposeConnections().catch((err) => {
|
|
144
|
+
getLogger().warn(`[${this.logTag}] orphan dispose error: ${err.message}`);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
this.notifyIdle(opts.cwd);
|
|
149
|
+
}
|
|
134
150
|
}
|
|
135
151
|
}
|
|
136
152
|
/**
|
|
@@ -178,6 +194,18 @@ export class ConnectionBasedRunner {
|
|
|
178
194
|
getLogger().warn(`[${this.logTag}] dispose error: ${err.message}`);
|
|
179
195
|
}
|
|
180
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Mark this runner as evicted from the bridge's runner cache while a run may
|
|
199
|
+
* still be holding/creating connections on it (queue-advance race during
|
|
200
|
+
* interruptCurrentRun's await window, 2026-10-11). The active run's finally
|
|
201
|
+
* disposes connections eagerly instead of arming the idle TTL timer —
|
|
202
|
+
* otherwise the agent server process lingers for up to 30 minutes holding
|
|
203
|
+
* its session write lock (codex: "thread already has an active writer").
|
|
204
|
+
* One-way by design: an evicted instance never returns to the cache.
|
|
205
|
+
*/
|
|
206
|
+
markOrphaned() {
|
|
207
|
+
this.orphaned = true;
|
|
208
|
+
}
|
|
181
209
|
getUsageAuthority() {
|
|
182
210
|
return 'live';
|
|
183
211
|
}
|
|
@@ -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
|
|
145
|
-
//
|
|
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:
|
|
314
|
-
outputTokens:
|
|
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:
|
|
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
|
+
"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",
|