lark-remote 0.5.15 → 0.5.16
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 +16 -0
- package/dist/bridge/index.js +17 -11
- package/dist/runner/common/connection-based-runner.d.ts +3 -1
- package/dist/runner/common/connection-based-runner.js +4 -0
- package/dist/runner/kimi/acp/runner.d.ts +2 -0
- package/dist/runner/kimi/acp/runner.js +4 -0
- package/dist/runner/opencode/acp/runner.d.ts +2 -0
- package/dist/runner/opencode/acp/runner.js +4 -0
- package/dist/runner/types.d.ts +8 -1
- package/dist/session/claude/sessions.js +58 -6
- package/dist/session/dsh/sessions.d.ts +2 -0
- package/dist/session/dsh/sessions.js +7 -4
- package/dist/session/kimi/sessions.js +6 -6
- package/dist/session/opencode/sessions.d.ts +2 -0
- package/dist/session/opencode/sessions.js +11 -2
- package/dist/session/pi/sessions.js +35 -15
- package/dist/session/registry.d.ts +2 -0
- package/dist/session/registry.js +4 -0
- package/dist/session-contract.d.ts +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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.16] - 2026-11-01
|
|
9
|
+
|
|
10
|
+
### 修复
|
|
11
|
+
|
|
12
|
+
- **Claude / Pi 的「本 run」用量在多步回合下不再低报**:卡片「本 run」与 `/resume`、自动恢复卡、完成通知卡的 Input / Output 由会话文件兜底读取,此前这两个 agent 只取**最后一条 assistant 消息**,而一条消息等于一次模型请求——带工具往返的回合里显示的是最后一次请求的量,整轮消耗被系统性低估。现在汇总末个真人回合内的所有 assistant 消息;`Context` 仍是末次请求的窗口水位、`累计` 仍是会话总量,口径不变。没有真人输入的旧数据或截断文件退回末条采样,不会把整份会话当成一轮
|
|
13
|
+
- **DSH 回合进行中的读数不再显示上一轮**:进程重启后的自动恢复与 `/resume`、没有活动用量时的中途读取、完成通知卡,此前优先取**上一个已结算回合**的数字(取值顺序与它自己的注释相反),当前回合已消耗的量被显示成上一轮。现在在途回合优先
|
|
14
|
+
- **DSH 的 `Context` 不再虚高**:窗口水位此前把整回合的输出一路相加,而前几步的输出早已计入后续请求的输入,属重复计数;`Context` 始终以会话文件为准,于是终态卡的上下文占用行随回合变长而虚高、百分比一起失真。现在只取末次请求的输出
|
|
15
|
+
- **Kimi / opencode 新回合刚开始时显示 0,不再显示上一轮残留**:回合已发出、模型还没产生任何用量采样的时刻,两个 reader 此前退回上一轮的最后一条记录,卡片把「刚开始的空回合」画成「刚结束那一轮」的消耗。现在按「末个回合」契约显示 0(`Context` 保留上一轮水位,因为窗口占用确实延续);整份记录里没有回合标记的旧数据沿用原有语义
|
|
16
|
+
- **`/compact` 收尾不再做无用功**:opencode 与 DSH 的会话读取给不出压缩次数,此前的重试把「这个 agent 没有该字段」当成「还没落盘」,每次压缩固定多跑两次完整导出 / 读取且永远等不到。现在 reader 显式声明能否产出压缩计数,声明不能的不再重试
|
|
17
|
+
|
|
18
|
+
### 维护
|
|
19
|
+
|
|
20
|
+
- **用量来源与 result 事件语义拆成两个字段**:原先一个字段同时承担「Input/Output 来自 live 还是会话文件」和「result 事件是否按 live 语义处理(启动前的 result 丢弃、会话指针写回)」,而 Kimi / opencode 的 live 通知只带上下文占用、没有输入输出拆分,却被迫声明成 live 来源。现在前者单独声明为会话文件,result 事件语义走新的声明位。无行为变化
|
|
21
|
+
- **跨 agent 的用量口径断言集中到一处**:本 run / `Context` / 累计三条不变量原本每个 reader 各写一套期望值,容易把错误语义固化成测试基线;现在 Claude / Pi / DSH / Kimi / opencode 的回归用例共用同一组断言。无行为变化
|
|
22
|
+
- **新增 token 用量口径文档**(中英文):三个 scope 的硬不变量、live 与会话文件两条生产路径、同一低报问题反复复发的原因、六个 reader 各自的回合边界与 `Context` 公式,以及尚未落地的共享聚合器与子 agent 用量过滤缺口
|
|
23
|
+
|
|
8
24
|
## [0.5.15] - 2026-10-31
|
|
9
25
|
|
|
10
26
|
### 修复
|
package/dist/bridge/index.js
CHANGED
|
@@ -1220,7 +1220,7 @@ export class Bridge {
|
|
|
1220
1220
|
* @param dropFromCard 该 result 是陈旧重放,不得进卡片(调用方 continue 跳过 push)
|
|
1221
1221
|
*/
|
|
1222
1222
|
observeResultEvent(event, opts) {
|
|
1223
|
-
const { sawInit,
|
|
1223
|
+
const { sawInit, liveResultEvents, usage, writeback, runId } = opts;
|
|
1224
1224
|
const preInitResult = !sawInit;
|
|
1225
1225
|
const preInitTerminal = preInitResult && (event.subtype === 'error' || event.subtype === 'interrupted')
|
|
1226
1226
|
? {
|
|
@@ -1228,7 +1228,7 @@ export class Bridge {
|
|
|
1228
1228
|
errorMessage: event.errorMessage,
|
|
1229
1229
|
}
|
|
1230
1230
|
: undefined;
|
|
1231
|
-
if (preInitResult &&
|
|
1231
|
+
if (preInitResult && !liveResultEvents) {
|
|
1232
1232
|
getLogger().info(`[lark-remote] pre-init result ignored (resume replay) runId=${runId}`);
|
|
1233
1233
|
return { sawResult: false, dropFromCard: true, preInitTerminal };
|
|
1234
1234
|
}
|
|
@@ -1236,11 +1236,11 @@ export class Bridge {
|
|
|
1236
1236
|
getLogger().info(preInitResult
|
|
1237
1237
|
? `[lark-remote] pre-init result from live runner (app-server setup failure) runId=${runId}`
|
|
1238
1238
|
: `[lark-remote] result event received runId=${runId}`);
|
|
1239
|
-
//
|
|
1240
|
-
// turn_started 事件,store 不会写回新线程 id;runner 已在 result
|
|
1241
|
-
//
|
|
1242
|
-
//
|
|
1243
|
-
if (
|
|
1239
|
+
// 连接型 runner 的 setup 失败路径(thread/start 成功后 turn/start 失败)
|
|
1240
|
+
// 不会有 turn_started 事件,store 不会写回新线程 id;runner 已在 result 里
|
|
1241
|
+
// 兜底上报。仅当 runner 声明 live result 语义且会话指针未动时写回,避免
|
|
1242
|
+
// 下条消息再开一个孤儿线程。其余 agent 不在此列(各自有 init/turn_started 写回)。
|
|
1243
|
+
if (liveResultEvents)
|
|
1244
1244
|
writeBackResultSession(writeback, event.session_id);
|
|
1245
1245
|
return { sawResult: !preInitResult, dropFromCard: false, preInitTerminal };
|
|
1246
1246
|
}
|
|
@@ -1353,9 +1353,12 @@ export class Bridge {
|
|
|
1353
1353
|
* Handles idle watchdog, event processing, session sync, and state transitions.
|
|
1354
1354
|
*/
|
|
1355
1355
|
async runAgentStreamToEnd(runner, cardSession, activeRun, sessionId, sessionEpochAtStart, ctx, cwd, runId, message, agentKind = this.config.defaultAgent) {
|
|
1356
|
-
// Runner 声明的 usage 权威来源:codex app-server = 'live'
|
|
1357
|
-
// (
|
|
1356
|
+
// Runner 声明的 usage 权威来源:codex app-server / pi RPC = 'live'
|
|
1357
|
+
// (live 带 input/output);opencode/kimi ACP live 只有 context,声明
|
|
1358
|
+
// 'jsonl'。result 事件的 live 语义(pre-init/写回)单独声明,不能和
|
|
1359
|
+
// 用量来源共用一个字段。
|
|
1358
1360
|
const usageAuthority = runner.getUsageAuthority?.();
|
|
1361
|
+
const liveResultEvents = runner.usesLiveResultEvents?.() ?? usageAuthority === 'live';
|
|
1359
1362
|
// §9.12 空闲看门狗:判定口径见 startIdleWatchdog;武装点在 cardSession.start() 之后。
|
|
1360
1363
|
let watchdog = null;
|
|
1361
1364
|
let sawResult = false;
|
|
@@ -1453,7 +1456,7 @@ export class Bridge {
|
|
|
1453
1456
|
if (event.type === 'result') {
|
|
1454
1457
|
const outcome = this.observeResultEvent(event, {
|
|
1455
1458
|
sawInit,
|
|
1456
|
-
|
|
1459
|
+
liveResultEvents,
|
|
1457
1460
|
usage,
|
|
1458
1461
|
writeback,
|
|
1459
1462
|
runId,
|
|
@@ -1975,8 +1978,11 @@ export class Bridge {
|
|
|
1975
1978
|
// 压缩结束后从会话 jsonl 读权威统计(压缩后上下文、压缩次数、本次压缩
|
|
1976
1979
|
// token 消耗与会话累计)。thread/compacted 通知与 jsonl 落盘几乎同时
|
|
1977
1980
|
// (实测差 ~9ms),compactCount 未读到则短重试,仍失败优雅降级(无统计)。
|
|
1981
|
+
// 不支持 compactCount 的 reader(opencode/dsh)不重试——否则每次 compact
|
|
1982
|
+
// 都会固定多跑两次完整 export/读取,且永远等不到该字段。
|
|
1983
|
+
const compactCountSupported = this.sessionReaderRegistry.tryGet(agentKind)?.supportsCompactCount !== false;
|
|
1978
1984
|
let finalUsage = resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, sessionId, cwd, agentKind);
|
|
1979
|
-
for (let attempt = 0; !finalUsage?.compactCount && attempt < 2; attempt++) {
|
|
1985
|
+
for (let attempt = 0; compactCountSupported && !finalUsage?.compactCount && attempt < 2; attempt++) {
|
|
1980
1986
|
await sleep(150);
|
|
1981
1987
|
finalUsage = resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, sessionId, cwd, agentKind);
|
|
1982
1988
|
}
|
|
@@ -122,7 +122,9 @@ export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent
|
|
|
122
122
|
* One-way by design: an evicted instance never returns to the cache.
|
|
123
123
|
*/
|
|
124
124
|
markOrphaned(): void;
|
|
125
|
-
getUsageAuthority(): 'live';
|
|
125
|
+
getUsageAuthority(): 'live' | 'jsonl';
|
|
126
|
+
/** 连接型 runner 的 result 事件都是 runner 自己产出的 live 语义。 */
|
|
127
|
+
usesLiveResultEvents(): boolean;
|
|
126
128
|
killOrphan(): void;
|
|
127
129
|
registerExitHandlers(): void;
|
|
128
130
|
unregisterExitHandlers(): void;
|
|
@@ -221,6 +221,10 @@ export class ConnectionBasedRunner {
|
|
|
221
221
|
getUsageAuthority() {
|
|
222
222
|
return 'live';
|
|
223
223
|
}
|
|
224
|
+
/** 连接型 runner 的 result 事件都是 runner 自己产出的 live 语义。 */
|
|
225
|
+
usesLiveResultEvents() {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
224
228
|
killOrphan() {
|
|
225
229
|
// No-op: the connection is managed by ConnectionManager.
|
|
226
230
|
}
|
|
@@ -56,6 +56,8 @@ export interface KimiAcpRunnerOptions {
|
|
|
56
56
|
permissionMode?: 'manual' | 'auto' | 'yolo';
|
|
57
57
|
}
|
|
58
58
|
export declare class KimiAcpRunner extends BaseAcpRunner<KimiAcpTranslator> {
|
|
59
|
+
/** ACP usage_update 只有 context,没有 input/output:flow 走 jsonl。 */
|
|
60
|
+
getUsageAuthority(): 'jsonl';
|
|
59
61
|
private permissionMode;
|
|
60
62
|
private thinkingEffort?;
|
|
61
63
|
private readonly compactIdleTimeoutMs;
|
|
@@ -134,6 +134,10 @@ function buildQuestionResponse(action, pending, response) {
|
|
|
134
134
|
// Runner
|
|
135
135
|
// =============================================================================
|
|
136
136
|
export class KimiAcpRunner extends BaseAcpRunner {
|
|
137
|
+
/** ACP usage_update 只有 context,没有 input/output:flow 走 jsonl。 */
|
|
138
|
+
getUsageAuthority() {
|
|
139
|
+
return 'jsonl';
|
|
140
|
+
}
|
|
137
141
|
permissionMode;
|
|
138
142
|
thinkingEffort;
|
|
139
143
|
compactIdleTimeoutMs;
|
|
@@ -53,6 +53,8 @@ export interface OpencodeAcpRunnerOptions {
|
|
|
53
53
|
errorMonitorPollIntervalMs?: number;
|
|
54
54
|
}
|
|
55
55
|
export declare class OpencodeAcpRunner extends BaseAcpRunner<OpencodeAcpTranslator> {
|
|
56
|
+
/** ACP usage_update 只有 context,没有 input/output:flow 走 jsonl。 */
|
|
57
|
+
getUsageAuthority(): 'jsonl';
|
|
56
58
|
/** Configured session mode applied via session/set_mode. */
|
|
57
59
|
private configuredMode;
|
|
58
60
|
/** Current session mode id, parsed from session/new|resume configOptions. */
|
|
@@ -30,6 +30,10 @@ import { OpencodeLogErrorMonitor, resolveOpencodeLogPath } from './error-monitor
|
|
|
30
30
|
// Runner
|
|
31
31
|
// =============================================================================
|
|
32
32
|
export class OpencodeAcpRunner extends BaseAcpRunner {
|
|
33
|
+
/** ACP usage_update 只有 context,没有 input/output:flow 走 jsonl。 */
|
|
34
|
+
getUsageAuthority() {
|
|
35
|
+
return 'jsonl';
|
|
36
|
+
}
|
|
33
37
|
/** Configured session mode applied via session/set_mode. */
|
|
34
38
|
configuredMode;
|
|
35
39
|
/** Current session mode id, parsed from session/new|resume configOptions. */
|
package/dist/runner/types.d.ts
CHANGED
|
@@ -581,8 +581,15 @@ export interface AgentRunner extends Runner {
|
|
|
581
581
|
readonly sessionReader: AgentSessionReader;
|
|
582
582
|
/** Return current status info for /status display. */
|
|
583
583
|
getStatusInfo(): AgentStatusInfo;
|
|
584
|
-
/**
|
|
584
|
+
/** flow 字段的权威来源:live 有 input/output 用 live,否则用 jsonl。 */
|
|
585
585
|
getUsageAuthority?(): 'live' | 'jsonl';
|
|
586
|
+
/**
|
|
587
|
+
* 该 runner 的 result 事件是否具备 live 语义(pre-init result 处理与
|
|
588
|
+
* session 写回)。与用量来源分开:ACP live 只有 context,用量来源是
|
|
589
|
+
* jsonl,但 result 事件仍需按 live 语义处理。
|
|
590
|
+
* 未实现 = 按 getUsageAuthority() === 'live' 兼容旧行为。
|
|
591
|
+
*/
|
|
592
|
+
usesLiveResultEvents?(): boolean;
|
|
586
593
|
/**
|
|
587
594
|
* 回传审批决策(codex thread/permissions、ACP elicitation、DSH approval
|
|
588
595
|
* resolve)。未实现的 agent 由协调器按「无人应答」超时收尾。
|
|
@@ -246,6 +246,32 @@ export function isClaudeSessionActive(sessionId, cwd, opts = {}) {
|
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* 是否是“真人发起新 turn”的 user 行。
|
|
251
|
+
*
|
|
252
|
+
* 只用于 turn 边界,不用于展示标题。排除:tool_result、compact summary、
|
|
253
|
+
* isMeta(skill/命令注入)、task notification、sidechain(子 agent prompt)。
|
|
254
|
+
* 不要求有可展示文本——attachment-only 的输入同样是一次真人 turn。
|
|
255
|
+
*/
|
|
256
|
+
function isRealUserPrompt(obj) {
|
|
257
|
+
if (obj.type !== 'user' || obj.isCompactSummary === true || obj.isMeta === true) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
if (obj.isSidechain === true)
|
|
261
|
+
return false;
|
|
262
|
+
const msg = obj.message;
|
|
263
|
+
if (msg?.role !== 'user' || msg.content === undefined)
|
|
264
|
+
return false;
|
|
265
|
+
const content = msg.content;
|
|
266
|
+
if (Array.isArray(content) &&
|
|
267
|
+
content.some((block) => block?.type === 'tool_result')) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
const text = extractText(content);
|
|
271
|
+
if (text && isTaskNotificationText(text))
|
|
272
|
+
return false;
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
249
275
|
/**
|
|
250
276
|
* first-pass scan: stream the file once via `scanJsonlLines`
|
|
251
277
|
* (no `string[]` materialized), parse each line exactly once, and collect
|
|
@@ -268,6 +294,8 @@ function scalarScan(filePath) {
|
|
|
268
294
|
const usageByMessageId = new Map();
|
|
269
295
|
let lastPostTokens = 0;
|
|
270
296
|
let tailOffset = -1;
|
|
297
|
+
/** 真人 prompt 序号;末个 turn = 当前值。sidechain/注入/工具结果不递增。 */
|
|
298
|
+
let turnSeq = 0;
|
|
271
299
|
let lastAiTitle;
|
|
272
300
|
let lastRecap;
|
|
273
301
|
let lastUserMessage;
|
|
@@ -285,6 +313,7 @@ function scalarScan(filePath) {
|
|
|
285
313
|
output: Math.max(prev?.output ?? 0, usage.output_tokens ?? 0),
|
|
286
314
|
cacheRead: Math.max(prev?.cacheRead ?? 0, usage.cache_read_input_tokens ?? 0),
|
|
287
315
|
cacheCreation: Math.max(prev?.cacheCreation ?? 0, usage.cache_creation_input_tokens ?? 0),
|
|
316
|
+
turn: prev?.turn ?? turnSeq,
|
|
288
317
|
});
|
|
289
318
|
}
|
|
290
319
|
}
|
|
@@ -337,16 +366,39 @@ function scalarScan(filePath) {
|
|
|
337
366
|
}
|
|
338
367
|
}
|
|
339
368
|
}
|
|
369
|
+
// --- turn boundary for per-run usage ---
|
|
370
|
+
// 真人 prompt 开启新 turn;注入/工具结果/sidechain 不递增。usage 记录在
|
|
371
|
+
// 解析到 assistant 行时已经带上当前 turnSeq,末尾只汇总末个 turn。
|
|
372
|
+
if (isRealUserPrompt(obj))
|
|
373
|
+
turnSeq++;
|
|
340
374
|
});
|
|
341
375
|
// Feed one best record per message id, in first-appearance order (Map
|
|
342
376
|
// preserves insertion order), so acc.last is the LAST message's full usage
|
|
343
|
-
// and totals sum each API response exactly once.
|
|
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
|
+
const turn = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
344
382
|
for (const record of usageByMessageId.values()) {
|
|
345
383
|
acc.add(record);
|
|
384
|
+
if (turnSeq > 0 && record.turn === turnSeq) {
|
|
385
|
+
turn.input += record.input;
|
|
386
|
+
turn.output += record.output;
|
|
387
|
+
turn.cacheRead += record.cacheRead;
|
|
388
|
+
turn.cacheCreation += record.cacheCreation;
|
|
389
|
+
}
|
|
346
390
|
}
|
|
347
391
|
// Build usage result (same logic as old aggregateSessionUsage)
|
|
348
392
|
const t = acc.totals;
|
|
349
393
|
const l = acc.last;
|
|
394
|
+
// 没有真人 user prompt(旧数据/截断文件)→ 无法划分 turn,回退末条采样,
|
|
395
|
+
// 与 dsh/codex「无 turn 标记退回末条」一致;不能把整份文件当成一轮。
|
|
396
|
+
if (turnSeq === 0 && l) {
|
|
397
|
+
turn.input = l.input;
|
|
398
|
+
turn.output = l.output;
|
|
399
|
+
turn.cacheRead = l.cacheRead;
|
|
400
|
+
turn.cacheCreation = l.cacheCreation;
|
|
401
|
+
}
|
|
350
402
|
// contextLength = current context-window occupancy. Prefer compact_boundary
|
|
351
403
|
// postTokens (the precise post-compact size); otherwise fall back to the
|
|
352
404
|
// last turn's input + cacheRead + cacheCreation (unified
|
|
@@ -362,14 +414,14 @@ function scalarScan(filePath) {
|
|
|
362
414
|
// 对齐 codex reader 语义(§9.21:`last_token_usage`=单 turn 增量、
|
|
363
415
|
// `total_token_usage`=session 累计)——否则 jsonl 兜底路径会把 session 累计
|
|
364
416
|
// 当成"本 run"显示,与卡片"本 run ≤ 累计"不变量冲突(累计反而更小)。
|
|
365
|
-
const lastTotal =
|
|
417
|
+
const lastTotal = turn.input + turn.output + turn.cacheRead + turn.cacheCreation;
|
|
366
418
|
const usage = {
|
|
367
|
-
inputTokens:
|
|
368
|
-
outputTokens:
|
|
419
|
+
inputTokens: turn.input,
|
|
420
|
+
outputTokens: turn.output,
|
|
369
421
|
contextLength,
|
|
370
422
|
compactCount: acc.compactCount,
|
|
371
|
-
cacheReadTokens:
|
|
372
|
-
cacheCreationTokens:
|
|
423
|
+
cacheReadTokens: turn.cacheRead > 0 ? turn.cacheRead : undefined,
|
|
424
|
+
cacheCreationTokens: turn.cacheCreation > 0 ? turn.cacheCreation : undefined,
|
|
373
425
|
totalTokens: lastTotal,
|
|
374
426
|
...cumulativeUsageFields(t),
|
|
375
427
|
};
|
|
@@ -41,6 +41,8 @@ interface DshSessionReaderOptions {
|
|
|
41
41
|
syncConnect?: (baseUrl: string, token: string) => string;
|
|
42
42
|
}
|
|
43
43
|
export declare class DshSessionReader implements AgentSessionReader {
|
|
44
|
+
/** DSH session/page 没有 compactCount;compact 收尾不应因此重试读取。 */
|
|
45
|
+
readonly supportsCompactCount = false;
|
|
44
46
|
private readonly hostProvider;
|
|
45
47
|
private readonly dshHomeProvider;
|
|
46
48
|
private readonly tokenProvider;
|
|
@@ -98,6 +98,8 @@ function isAuthFailure(err) {
|
|
|
98
98
|
return /\b401\b|\b403\b|unauthori[sz]ed|forbidden|invalid[ -]?cookie/i.test(msg);
|
|
99
99
|
}
|
|
100
100
|
export class DshSessionReader {
|
|
101
|
+
/** DSH session/page 没有 compactCount;compact 收尾不应因此重试读取。 */
|
|
102
|
+
supportsCompactCount = false;
|
|
101
103
|
hostProvider;
|
|
102
104
|
dshHomeProvider;
|
|
103
105
|
tokenProvider;
|
|
@@ -315,8 +317,8 @@ export class DshSessionReader {
|
|
|
315
317
|
// 在途 turn(turn/start 无 turn/end,run 进行中读历史)的采样优先于
|
|
316
318
|
// 上一轮:旧实现只认已结算的 lastTurnUsage,mid-run 读取会把当前 turn
|
|
317
319
|
// 已消耗的量显示成上一个完整 turn 的数字。
|
|
318
|
-
const flow =
|
|
319
|
-
|
|
320
|
+
const flow = turnUsage ??
|
|
321
|
+
lastTurnUsage ?? {
|
|
320
322
|
input,
|
|
321
323
|
output: lastUsage.outputTokens ?? 0,
|
|
322
324
|
cacheRead,
|
|
@@ -325,8 +327,9 @@ export class DshSessionReader {
|
|
|
325
327
|
sessionUsage = {
|
|
326
328
|
inputTokens: flow.input,
|
|
327
329
|
outputTokens: flow.output,
|
|
328
|
-
// §9.21
|
|
329
|
-
|
|
330
|
+
// §9.21 口径:context 是末次采样的窗口水位(input + 末次 output +
|
|
331
|
+
// cache),不把整 turn 的 output 求和(前几步 output 已进入后续 input)。
|
|
332
|
+
contextLength: input + (lastUsage.outputTokens ?? 0) + cacheRead + cacheWrite,
|
|
330
333
|
cacheReadTokens: flow.cacheRead,
|
|
331
334
|
cacheCreationTokens: flow.cacheWrite,
|
|
332
335
|
totalTokens: flow.input + flow.output + flow.cacheRead + flow.cacheWrite,
|
|
@@ -353,8 +353,9 @@ export class KimiSessionReader {
|
|
|
353
353
|
// 末条会低报,取会话总量(旧实现)又会把历史所有 run 的消耗算进这一轮。
|
|
354
354
|
// 子 agent 的 wire 没有 turn 标记,只能进会话累计(无法归属到某一轮);
|
|
355
355
|
// 整份 wire 都没有 turn.prompt(旧数据)时沿用「整份文件 = 一个 turn」。
|
|
356
|
+
// 末个 turn 尚无 usage.record 时 flow = 0(当前契约是「末个 turn」,不是
|
|
357
|
+
// 「末个非空 turn」)——不能退回上一轮的记录。
|
|
356
358
|
let turnAcc = null;
|
|
357
|
-
let lastMainUsage;
|
|
358
359
|
const accumulateUsage = (u, scope) => {
|
|
359
360
|
hasUsageRecord = true;
|
|
360
361
|
// Malformed records (missing usage object or missing fields) must
|
|
@@ -372,7 +373,6 @@ export class KimiSessionReader {
|
|
|
372
373
|
acc.add(record);
|
|
373
374
|
if (scope !== 'main')
|
|
374
375
|
return;
|
|
375
|
-
lastMainUsage = record;
|
|
376
376
|
turnAcc?.add(record);
|
|
377
377
|
// unified contract (§9.21): input + output + cacheRead + cacheCreation.
|
|
378
378
|
// 末轮 output 已进入历史、下一 prompt 全量携带——与 compact postTokens
|
|
@@ -498,10 +498,10 @@ export class KimiSessionReader {
|
|
|
498
498
|
// Build the final AgentSessionUsage from the last turn's flow + session
|
|
499
499
|
// cumulative + main contextLength.
|
|
500
500
|
const t = acc.totals;
|
|
501
|
-
// 有 turn.prompt 时本 run = 末个 turn 的 flow
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
const flow = turnAcc ?
|
|
501
|
+
// 有 turn.prompt 时本 run = 末个 turn 的 flow;末 turn 还没有记录时
|
|
502
|
+
// turnAcc.count = 0 → flow = 0。整份 wire 都没有 turn.prompt(旧数据/
|
|
503
|
+
// 截断)时无从分段,沿用「整份文件 = 一个 turn」的旧口径(含子 agent wire)。
|
|
504
|
+
const flow = turnAcc ? turnAcc.totals : t;
|
|
505
505
|
// Kimi semantics: preserve 0 values (not undefined) for cache fields to match original
|
|
506
506
|
const totalUsage = {
|
|
507
507
|
inputTokens: flow.input,
|
|
@@ -31,6 +31,8 @@ interface OpencodeSessionReaderOptions {
|
|
|
31
31
|
export declare class OpencodeSessionReader implements AgentSessionReader {
|
|
32
32
|
private readonly binary;
|
|
33
33
|
private readonly cacheTtlMs;
|
|
34
|
+
/** opencode export 没有 compactCount;compact 收尾不应因此重试 export。 */
|
|
35
|
+
readonly supportsCompactCount = false;
|
|
34
36
|
/** Optional raw-export capture override (testing / alt transport). */
|
|
35
37
|
private readonly captureExportOverride?;
|
|
36
38
|
/** cwd → 列表缓存(TTL 由 cacheTtlMs 决定)。 */
|
|
@@ -119,6 +119,12 @@ function lastTurnStartIndex(messages) {
|
|
|
119
119
|
}
|
|
120
120
|
if (lastSample < 0)
|
|
121
121
|
return -1;
|
|
122
|
+
// 末条采样之后又出现 user:这是新 turn 的起点,但还没有采样(空 turn)。
|
|
123
|
+
// 返回该 user 下标,让 flow 汇总范围为空 → 本 run = 0,而不是上一轮。
|
|
124
|
+
for (let i = lastSample + 1; i < messages.length; i++) {
|
|
125
|
+
if (messages[i]?.type === 'user')
|
|
126
|
+
return i;
|
|
127
|
+
}
|
|
122
128
|
for (let i = lastSample - 1; i >= 0; i--) {
|
|
123
129
|
if (messages[i]?.type === 'idle')
|
|
124
130
|
return i;
|
|
@@ -162,6 +168,8 @@ function withCumulativeUsage(usage, cum) {
|
|
|
162
168
|
export class OpencodeSessionReader {
|
|
163
169
|
binary;
|
|
164
170
|
cacheTtlMs;
|
|
171
|
+
/** opencode export 没有 compactCount;compact 收尾不应因此重试 export。 */
|
|
172
|
+
supportsCompactCount = false;
|
|
165
173
|
/** Optional raw-export capture override (testing / alt transport). */
|
|
166
174
|
captureExportOverride;
|
|
167
175
|
/** cwd → 列表缓存(TTL 由 cacheTtlMs 决定)。 */
|
|
@@ -557,8 +565,9 @@ export class OpencodeSessionReader {
|
|
|
557
565
|
totalTokens: sample.total,
|
|
558
566
|
};
|
|
559
567
|
}
|
|
560
|
-
// turnStart
|
|
561
|
-
//
|
|
568
|
+
// turnStart 可能落在末条采样之后(新 turn 尚无采样)→ turn 全 0;latest
|
|
569
|
+
// 仍是上一轮末条采样,contextLength 保留水位。turnStart 小于最后一个采样
|
|
570
|
+
// 下标时,flow 覆盖末条值,只有 contextLength 保留末条。
|
|
562
571
|
if (latest) {
|
|
563
572
|
latest.inputTokens = turn.input;
|
|
564
573
|
latest.outputTokens = turn.output;
|
|
@@ -157,6 +157,11 @@ function findSessionFile(sessionId, cwd, sessionsDir) {
|
|
|
157
157
|
*/
|
|
158
158
|
function piScalarScan(filePath) {
|
|
159
159
|
const acc = new UsageAccumulator();
|
|
160
|
+
// 末个真人 turn 的 flow:user 消息开启新 turn,assistant 用量同时进两个
|
|
161
|
+
// accumulator。带工具往返的 turn 有多个 assistant 消息(一次模型请求一条),
|
|
162
|
+
// 只取末条会低报整轮消耗。
|
|
163
|
+
let turnAcc = new UsageAccumulator();
|
|
164
|
+
let sawUser = false;
|
|
160
165
|
let tailOffset = -1;
|
|
161
166
|
let lastUserText;
|
|
162
167
|
scanJsonlOnce(filePath, (rawObj, line, offset) => {
|
|
@@ -168,18 +173,22 @@ function piScalarScan(filePath) {
|
|
|
168
173
|
// usage aggregation (assistant only)
|
|
169
174
|
if (msg.role === 'assistant' && msg.usage) {
|
|
170
175
|
const u = msg.usage;
|
|
171
|
-
|
|
176
|
+
const record = {
|
|
172
177
|
input: u.input ?? 0,
|
|
173
178
|
output: u.output ?? 0,
|
|
174
179
|
cacheRead: u.cacheRead ?? 0,
|
|
175
180
|
cacheCreation: u.cacheWrite ?? 0,
|
|
176
181
|
total: u.totalTokens ?? 0,
|
|
177
|
-
}
|
|
182
|
+
};
|
|
183
|
+
acc.add(record);
|
|
184
|
+
turnAcc.add(record);
|
|
178
185
|
}
|
|
179
186
|
// last user message offset + displayTitle. Record the byte offset
|
|
180
187
|
// where the tail should start: the beginning of the NEXT line after
|
|
181
188
|
// this user message (see tailOffsetAfter).
|
|
182
189
|
if (msg.role === 'user') {
|
|
190
|
+
sawUser = true;
|
|
191
|
+
turnAcc = new UsageAccumulator();
|
|
183
192
|
tailOffset = tailOffsetAfter(line, offset);
|
|
184
193
|
const text = extractPiText(msg.content);
|
|
185
194
|
// Compress skill-injection bodies so the "最近输入" label shows what
|
|
@@ -195,7 +204,7 @@ function piScalarScan(filePath) {
|
|
|
195
204
|
});
|
|
196
205
|
return {
|
|
197
206
|
tailOffset,
|
|
198
|
-
usage: buildPiUsage(acc),
|
|
207
|
+
usage: buildPiUsage(acc, turnAcc, sawUser),
|
|
199
208
|
// Whitespace-normalized single-line display title (no length truncation —
|
|
200
209
|
// the consumer truncates). Normalization collapses the skill-injection
|
|
201
210
|
// newlines so the title stays on one line.
|
|
@@ -203,27 +212,38 @@ function piScalarScan(filePath) {
|
|
|
203
212
|
};
|
|
204
213
|
}
|
|
205
214
|
/**
|
|
206
|
-
* Build the AgentSessionUsage
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
215
|
+
* Build the AgentSessionUsage: flow (input/output/cache/total) is the sum of
|
|
216
|
+
* every assistant message in the LAST real-user turn; contextLength is the
|
|
217
|
+
* last assistant message's window occupancy; cumulative is the whole session.
|
|
218
|
+
* With no real-user turn (legacy/truncated file), flow falls back to the last
|
|
219
|
+
* assistant sample, matching dsh/codex's no-turn-marker fallback.
|
|
211
220
|
* Returns undefined when no usage data was observed.
|
|
212
221
|
*/
|
|
213
|
-
function buildPiUsage(acc) {
|
|
222
|
+
function buildPiUsage(acc, turnAcc, sawUser) {
|
|
214
223
|
const t = acc.totals;
|
|
215
|
-
if (t.input + t.output === 0) {
|
|
224
|
+
if (t.input + t.output + t.cacheRead + t.cacheCreation === 0) {
|
|
216
225
|
return undefined;
|
|
217
226
|
}
|
|
218
227
|
const l = acc.last;
|
|
228
|
+
// 没有真人 user prompt(旧数据/截断文件)→ 无法划分 turn,回退末条采样,
|
|
229
|
+
// 与 dsh/codex「无 turn 标记退回末条」一致;不能把整份文件当成一轮。
|
|
230
|
+
const f = sawUser
|
|
231
|
+
? turnAcc.totals
|
|
232
|
+
: {
|
|
233
|
+
input: l.input,
|
|
234
|
+
output: l.output,
|
|
235
|
+
cacheRead: l.cacheRead,
|
|
236
|
+
cacheCreation: l.cacheCreation,
|
|
237
|
+
total: l.total,
|
|
238
|
+
};
|
|
219
239
|
return {
|
|
220
|
-
inputTokens:
|
|
221
|
-
outputTokens:
|
|
240
|
+
inputTokens: f.input,
|
|
241
|
+
outputTokens: f.output,
|
|
222
242
|
contextLength: contextWindowOccupancy(l),
|
|
223
243
|
compactCount: acc.compactCount > 0 ? acc.compactCount : undefined,
|
|
224
|
-
cacheReadTokens:
|
|
225
|
-
cacheCreationTokens:
|
|
226
|
-
totalTokens:
|
|
244
|
+
cacheReadTokens: f.cacheRead > 0 ? f.cacheRead : 0,
|
|
245
|
+
cacheCreationTokens: f.cacheCreation > 0 ? f.cacheCreation : 0,
|
|
246
|
+
totalTokens: f.total > 0 ? f.total : undefined,
|
|
227
247
|
// Cumulative (session-wide): sum of ALL runs in the jsonl file.
|
|
228
248
|
...cumulativeUsageFields(t),
|
|
229
249
|
};
|
|
@@ -10,4 +10,6 @@ export declare class SessionReaderRegistry {
|
|
|
10
10
|
private readonly readers;
|
|
11
11
|
register(kind: AgentKind, reader: AgentSessionReader): void;
|
|
12
12
|
get(kind: AgentKind): AgentSessionReader;
|
|
13
|
+
/** 可选能力探测用:不抛错,交给调用方决定降级行为。 */
|
|
14
|
+
tryGet(kind: AgentKind): AgentSessionReader | undefined;
|
|
13
15
|
}
|
package/dist/session/registry.js
CHANGED
|
@@ -175,4 +175,10 @@ export interface AgentSessionReader {
|
|
|
175
175
|
* 未实现 = 该 agent 没有这个概念,调用方按「不在途」处理。
|
|
176
176
|
*/
|
|
177
177
|
readCompactionState?(sessionId: string, cwd: string): CompactionState;
|
|
178
|
+
/**
|
|
179
|
+
* `readSessionContent` 是否产出 `compactCount`。opencode 的 export 没有
|
|
180
|
+
* 压缩计数;compact 收尾的重试不能把「不支持」误判成「还没落盘」。
|
|
181
|
+
* 未声明 = 支持(保持既有 reader 行为)。
|
|
182
|
+
*/
|
|
183
|
+
supportsCompactCount?: boolean;
|
|
178
184
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lark-remote",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.16",
|
|
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",
|