lark-remote 0.5.14 → 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 +32 -0
- package/dist/bridge/approval-coordinator.d.ts +13 -0
- package/dist/bridge/approval-coordinator.js +18 -0
- package/dist/bridge/index.js +87 -24
- package/dist/bridge/run-usage.d.ts +5 -0
- package/dist/bridge/run-usage.js +6 -1
- package/dist/card/run-renderer.js +22 -8
- package/dist/router/index.js +4 -7
- package/dist/runner/common/acp/base-acp-runner.d.ts +16 -0
- package/dist/runner/common/acp/base-acp-runner.js +37 -3
- package/dist/runner/common/connection-based-runner.d.ts +3 -1
- package/dist/runner/common/connection-based-runner.js +4 -0
- package/dist/runner/dsh/client.js +5 -0
- package/dist/runner/dsh/runner.js +22 -6
- 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 +13 -0
- package/dist/runner/opencode/acp/runner.js +17 -2
- 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 +37 -19
- package/dist/session/opencode/sessions.d.ts +2 -0
- package/dist/session/opencode/sessions.js +124 -36
- 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
|
@@ -482,10 +482,17 @@ export class DshRunner {
|
|
|
482
482
|
timeoutMs: GOAL_PROBE_TIMEOUT_MS,
|
|
483
483
|
});
|
|
484
484
|
const lastGoal = seedGoal.known ? seedGoal.goal : undefined;
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
485
|
+
if (seedGoal.known) {
|
|
486
|
+
// 只有确实存在 goal 才产事件:没有 goal 的会话保持事件序列不变(少一次
|
|
487
|
+
// 渲染,也不让"读不到"变成一张多出来的卡片事件)。
|
|
488
|
+
if (seedGoal.goal)
|
|
489
|
+
yield goalStateEvent(seedGoal.goal, true);
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
// 读不到 ≠ 没有目标。必须显式把 unknown 交给卡片,否则收尾只能按
|
|
493
|
+
// 「没有 goal」写成「已完成」——正是 GoalStateEvent 契约禁止的推断。
|
|
494
|
+
yield goalStateEvent(null, false);
|
|
495
|
+
}
|
|
489
496
|
try {
|
|
490
497
|
const submitted = await this.submitPrompt(sessionId, opts.cwd, message, abort.signal);
|
|
491
498
|
promptRequestId = submitted.promptRequestId;
|
|
@@ -497,7 +504,7 @@ export class DshRunner {
|
|
|
497
504
|
return;
|
|
498
505
|
}
|
|
499
506
|
try {
|
|
500
|
-
for await (const ev of this.runTurn(sessionId, opts.cwd, submitted.prePromptSeq, promptRequestId, abort, { last: lastGoal })) {
|
|
507
|
+
for await (const ev of this.runTurn(sessionId, opts.cwd, submitted.prePromptSeq, promptRequestId, abort, { last: lastGoal, known: seedGoal.known })) {
|
|
501
508
|
yield ev;
|
|
502
509
|
if (ev.type === 'result') {
|
|
503
510
|
terminalEmitted = true;
|
|
@@ -734,7 +741,16 @@ export class DshRunner {
|
|
|
734
741
|
signal,
|
|
735
742
|
timeoutMs: GOAL_PROBE_TIMEOUT_MS,
|
|
736
743
|
});
|
|
737
|
-
if (finalGoal.known
|
|
744
|
+
if (!finalGoal.known) {
|
|
745
|
+
// 收尾读不到:seed 已报过 unknown 就不重复发,否则把状态降级
|
|
746
|
+
// 交给卡片(卡片据此不再宣布完成)。
|
|
747
|
+
if (goal.known) {
|
|
748
|
+
goal.known = false;
|
|
749
|
+
yield goalStateEvent(null, false);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
else if (!goal.known || !sameGoal(finalGoal.goal, goal.last)) {
|
|
753
|
+
goal.known = true;
|
|
738
754
|
goal.last = finalGoal.goal;
|
|
739
755
|
yield goalStateEvent(finalGoal.goal, true);
|
|
740
756
|
}
|
|
@@ -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. */
|
|
@@ -91,9 +93,20 @@ export declare class OpencodeAcpRunner extends BaseAcpRunner<OpencodeAcpTranslat
|
|
|
91
93
|
* starting or still activating location plugins when the first session is
|
|
92
94
|
* created; both surface as a JSON-RPC internal error. Retry a few times
|
|
93
95
|
* before failing the turn.
|
|
96
|
+
*
|
|
97
|
+
* 单次 attempt 自身还要等 opencode 侧超时(~10s),所以真实窗口 ≈ Σdelay +
|
|
98
|
+
* 每次的等待。2026-10-30 现场:daemon 刚重启,`/api/model` 因模型目录拉取
|
|
99
|
+
* 超时连续 503 约 7 分钟,旧退避 0.5/1.5/3s(合计 ~45s)全落在这段里,4 次
|
|
100
|
+
* 重试无一成功。现在铺到 ~75s + 每次超时 ≈ 2 分钟,覆盖一次冷启动窗口,
|
|
101
|
+
* 又不至于让真故障的用户干等太久。
|
|
94
102
|
*/
|
|
95
103
|
protected sessionSetupRetryDelaysMs(): number[];
|
|
96
104
|
protected isRetryableSessionSetupError(err: unknown): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* 用户可见文案。这里刻意**不**建议 `opencode service restart`:卡住的是
|
|
107
|
+
* opencode 的模型目录/位置服务冷启动,重启只会清掉刚热好的状态、重新回到
|
|
108
|
+
* 同一个窗口(2026-10-30 现场:照提示重启两次仍然失败,等几分钟自己好了)。
|
|
109
|
+
*/
|
|
97
110
|
protected enrichSessionSetupError(err: unknown): Error;
|
|
98
111
|
getStatusInfo(): AgentStatusInfo;
|
|
99
112
|
/**
|
|
@@ -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. */
|
|
@@ -173,20 +177,31 @@ export class OpencodeAcpRunner extends BaseAcpRunner {
|
|
|
173
177
|
* starting or still activating location plugins when the first session is
|
|
174
178
|
* created; both surface as a JSON-RPC internal error. Retry a few times
|
|
175
179
|
* before failing the turn.
|
|
180
|
+
*
|
|
181
|
+
* 单次 attempt 自身还要等 opencode 侧超时(~10s),所以真实窗口 ≈ Σdelay +
|
|
182
|
+
* 每次的等待。2026-10-30 现场:daemon 刚重启,`/api/model` 因模型目录拉取
|
|
183
|
+
* 超时连续 503 约 7 分钟,旧退避 0.5/1.5/3s(合计 ~45s)全落在这段里,4 次
|
|
184
|
+
* 重试无一成功。现在铺到 ~75s + 每次超时 ≈ 2 分钟,覆盖一次冷启动窗口,
|
|
185
|
+
* 又不至于让真故障的用户干等太久。
|
|
176
186
|
*/
|
|
177
187
|
sessionSetupRetryDelaysMs() {
|
|
178
|
-
return [500,
|
|
188
|
+
return [500, 2000, 8000, 20000, 45000];
|
|
179
189
|
}
|
|
180
190
|
isRetryableSessionSetupError(err) {
|
|
181
191
|
const message = err instanceof Error ? err.message : String(err);
|
|
182
192
|
return /ServiceUnavailableError|Location plugins did not finish activating|Internal service failure|ECONNREFUSED|fetch failed/i.test(message);
|
|
183
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* 用户可见文案。这里刻意**不**建议 `opencode service restart`:卡住的是
|
|
196
|
+
* opencode 的模型目录/位置服务冷启动,重启只会清掉刚热好的状态、重新回到
|
|
197
|
+
* 同一个窗口(2026-10-30 现场:照提示重启两次仍然失败,等几分钟自己好了)。
|
|
198
|
+
*/
|
|
184
199
|
enrichSessionSetupError(err) {
|
|
185
200
|
if (!this.isRetryableSessionSetupError(err)) {
|
|
186
201
|
return super.enrichSessionSetupError(err);
|
|
187
202
|
}
|
|
188
203
|
const message = err instanceof Error ? err.message : String(err);
|
|
189
|
-
return new Error(`opencode
|
|
204
|
+
return new Error(`opencode 后台服务尚未就绪(${message})。这是 opencode 冷启动 / 模型目录加载期间的临时状态:等 1–2 分钟重发即可,重启服务只会重新进入同一窗口。`, { cause: err });
|
|
190
205
|
}
|
|
191
206
|
getStatusInfo() {
|
|
192
207
|
return {
|
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,
|
|
@@ -347,7 +347,16 @@ export class KimiSessionReader {
|
|
|
347
347
|
// is an independent window). Aggregation covers the whole wire.jsonl
|
|
348
348
|
// and is independent of the maxEvents display slice
|
|
349
349
|
// (usage = the session's overall token cost).
|
|
350
|
-
|
|
350
|
+
//
|
|
351
|
+
// 本 run 的 flow 字段是**末个 turn**(最后一个 turn.prompt 之后)的记录之和:
|
|
352
|
+
// kimi 每次模型调用写一条 usage.record,带工具往返的 turn 会有多条。只取
|
|
353
|
+
// 末条会低报,取会话总量(旧实现)又会把历史所有 run 的消耗算进这一轮。
|
|
354
|
+
// 子 agent 的 wire 没有 turn 标记,只能进会话累计(无法归属到某一轮);
|
|
355
|
+
// 整份 wire 都没有 turn.prompt(旧数据)时沿用「整份文件 = 一个 turn」。
|
|
356
|
+
// 末个 turn 尚无 usage.record 时 flow = 0(当前契约是「末个 turn」,不是
|
|
357
|
+
// 「末个非空 turn」)——不能退回上一轮的记录。
|
|
358
|
+
let turnAcc = null;
|
|
359
|
+
const accumulateUsage = (u, scope) => {
|
|
351
360
|
hasUsageRecord = true;
|
|
352
361
|
// Malformed records (missing usage object or missing fields) must
|
|
353
362
|
// not poison the accumulators with NaN: count missing fields as 0.
|
|
@@ -355,18 +364,20 @@ export class KimiSessionReader {
|
|
|
355
364
|
const output = u?.output ?? 0;
|
|
356
365
|
const inputCacheRead = u?.inputCacheRead ?? 0;
|
|
357
366
|
const inputCacheCreation = u?.inputCacheCreation ?? 0;
|
|
358
|
-
|
|
367
|
+
const record = {
|
|
359
368
|
input: inputOther,
|
|
360
369
|
output,
|
|
361
370
|
cacheRead: inputCacheRead,
|
|
362
371
|
cacheCreation: inputCacheCreation,
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
372
|
+
};
|
|
373
|
+
acc.add(record);
|
|
374
|
+
if (scope !== 'main')
|
|
375
|
+
return;
|
|
376
|
+
turnAcc?.add(record);
|
|
377
|
+
// unified contract (§9.21): input + output + cacheRead + cacheCreation.
|
|
378
|
+
// 末轮 output 已进入历史、下一 prompt 全量携带——与 compact postTokens
|
|
379
|
+
// 水位同语义(此前漏加 output 会系统性低估长收尾 turn)。
|
|
380
|
+
mainContextLength = inputOther + output + inputCacheRead + inputCacheCreation;
|
|
370
381
|
};
|
|
371
382
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
372
383
|
const line = lines[lineIndex];
|
|
@@ -374,7 +385,7 @@ export class KimiSessionReader {
|
|
|
374
385
|
const entry = JSON.parse(line);
|
|
375
386
|
// Extract usage from usage.record.
|
|
376
387
|
if (isUsageRecord(entry)) {
|
|
377
|
-
accumulateUsage(entry.usage,
|
|
388
|
+
accumulateUsage(entry.usage, 'main');
|
|
378
389
|
continue;
|
|
379
390
|
}
|
|
380
391
|
// §6.3: Count compaction events and track pre-compact context length.
|
|
@@ -401,6 +412,8 @@ export class KimiSessionReader {
|
|
|
401
412
|
// prompt), which is the correct catch-up title.
|
|
402
413
|
// Extract display title from turn.prompt
|
|
403
414
|
if (isTurnPrompt(entry)) {
|
|
415
|
+
// turn.prompt 是 turn 的起点:本 turn 的 flow 从这里重新累计。
|
|
416
|
+
turnAcc = new UsageAccumulator();
|
|
404
417
|
if (entry.input && entry.input.length > 0) {
|
|
405
418
|
displayTitle = entry.input[0].text;
|
|
406
419
|
}
|
|
@@ -470,7 +483,7 @@ export class KimiSessionReader {
|
|
|
470
483
|
try {
|
|
471
484
|
const subEntry = JSON.parse(subLine);
|
|
472
485
|
if (isUsageRecord(subEntry)) {
|
|
473
|
-
accumulateUsage(subEntry.usage,
|
|
486
|
+
accumulateUsage(subEntry.usage, 'subagent');
|
|
474
487
|
}
|
|
475
488
|
}
|
|
476
489
|
catch {
|
|
@@ -482,18 +495,23 @@ export class KimiSessionReader {
|
|
|
482
495
|
catch {
|
|
483
496
|
// agents dir unreadable — fall back to main-only usage
|
|
484
497
|
}
|
|
485
|
-
// Build the final AgentSessionUsage from
|
|
498
|
+
// Build the final AgentSessionUsage from the last turn's flow + session
|
|
499
|
+
// cumulative + main contextLength.
|
|
486
500
|
const t = acc.totals;
|
|
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;
|
|
487
505
|
// Kimi semantics: preserve 0 values (not undefined) for cache fields to match original
|
|
488
506
|
const totalUsage = {
|
|
489
|
-
inputTokens:
|
|
490
|
-
outputTokens:
|
|
507
|
+
inputTokens: flow.input,
|
|
508
|
+
outputTokens: flow.output,
|
|
491
509
|
contextLength: mainContextLength,
|
|
492
|
-
cacheReadTokens:
|
|
493
|
-
cacheCreationTokens:
|
|
494
|
-
totalTokens:
|
|
495
|
-
// Cumulative (session-wide):
|
|
496
|
-
//
|
|
510
|
+
cacheReadTokens: flow.cacheRead > 0 ? flow.cacheRead : 0,
|
|
511
|
+
cacheCreationTokens: flow.cacheCreation > 0 ? flow.cacheCreation : 0,
|
|
512
|
+
totalTokens: flow.input + flow.output + flow.cacheRead + flow.cacheCreation,
|
|
513
|
+
// Cumulative (session-wide): sums every usage.record across all turns
|
|
514
|
+
// (subagent wires included) — the "累计" line, never the per-run value.
|
|
497
515
|
...cumulativeUsageFields(t),
|
|
498
516
|
// §6.3: compaction stats from wire.jsonl
|
|
499
517
|
compactCount,
|
|
@@ -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 决定)。 */
|
|
@@ -81,6 +81,61 @@ function v2ToolOutput(state) {
|
|
|
81
81
|
return state.result;
|
|
82
82
|
return undefined;
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* v2 assistant 消息 → 用量采样。total 为 0 的退化消息(模型在 reasoning 中被
|
|
86
|
+
* 截断)不算采样:信任它会抹掉整轮真实用量。
|
|
87
|
+
*/
|
|
88
|
+
function usageSample(msg) {
|
|
89
|
+
if (msg?.type !== 'assistant' || !msg.tokens)
|
|
90
|
+
return undefined;
|
|
91
|
+
const input = msg.tokens.input ?? 0;
|
|
92
|
+
const output = msg.tokens.output ?? 0;
|
|
93
|
+
const cacheRead = msg.tokens.cache?.read ?? 0;
|
|
94
|
+
const cacheCreation = msg.tokens.cache?.write ?? 0;
|
|
95
|
+
const total = input + output + (msg.tokens.reasoning ?? 0) + cacheRead + cacheCreation;
|
|
96
|
+
if (total === 0)
|
|
97
|
+
return undefined;
|
|
98
|
+
return { input, output, cacheRead, cacheCreation, total };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 末个 turn 的起点(其后才是"本 run"的采样)。
|
|
102
|
+
*
|
|
103
|
+
* `idle` 是 opencode 自带的 turn 结束标记(真实 export 里每个 idle 前一条是
|
|
104
|
+
* assistant、后一条是 user/synthetic/system)。取「最后一个其后仍有采样的 idle」:
|
|
105
|
+
* - 文件以 idle 收尾(/resume 常态)时,末个 idle 之后没有采样,边界自动落到
|
|
106
|
+
* 再上一个 idle,即最后那个完整 turn;
|
|
107
|
+
* - 文件停在 turn 中间(run 收尾读盘)时,末个 idle 就是当前 turn 的起点。
|
|
108
|
+
*
|
|
109
|
+
* 老数据没有 idle:退回最后一个 turn 起跑标记(user/synthetic/system),与
|
|
110
|
+
* codex/dsh「无 turn 标记则退回末条采样」的既有兜底一致。
|
|
111
|
+
*/
|
|
112
|
+
function lastTurnStartIndex(messages) {
|
|
113
|
+
let lastSample = -1;
|
|
114
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
115
|
+
if (usageSample(messages[i]) !== undefined) {
|
|
116
|
+
lastSample = i;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (lastSample < 0)
|
|
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
|
+
}
|
|
128
|
+
for (let i = lastSample - 1; i >= 0; i--) {
|
|
129
|
+
if (messages[i]?.type === 'idle')
|
|
130
|
+
return i;
|
|
131
|
+
}
|
|
132
|
+
for (let i = lastSample - 1; i >= 0; i--) {
|
|
133
|
+
const type = messages[i]?.type;
|
|
134
|
+
if (type === 'user' || type === 'synthetic' || type === 'system')
|
|
135
|
+
return i;
|
|
136
|
+
}
|
|
137
|
+
return -1;
|
|
138
|
+
}
|
|
84
139
|
/**
|
|
85
140
|
* 把会话级累计 token 落到 usage 上(v1 export 与 v2 export 两条读取路径共用)。
|
|
86
141
|
*
|
|
@@ -113,6 +168,8 @@ function withCumulativeUsage(usage, cum) {
|
|
|
113
168
|
export class OpencodeSessionReader {
|
|
114
169
|
binary;
|
|
115
170
|
cacheTtlMs;
|
|
171
|
+
/** opencode export 没有 compactCount;compact 收尾不应因此重试 export。 */
|
|
172
|
+
supportsCompactCount = false;
|
|
116
173
|
/** Optional raw-export capture override (testing / alt transport). */
|
|
117
174
|
captureExportOverride;
|
|
118
175
|
/** cwd → 列表缓存(TTL 由 cacheTtlMs 决定)。 */
|
|
@@ -196,6 +253,8 @@ export class OpencodeSessionReader {
|
|
|
196
253
|
}
|
|
197
254
|
const events = [];
|
|
198
255
|
let usage;
|
|
256
|
+
/** 本 turn 的 flow 累计:末条 user 消息之后的全部 step-finish。 */
|
|
257
|
+
let turn = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, total: 0 };
|
|
199
258
|
let aiTitle;
|
|
200
259
|
let displayTitle;
|
|
201
260
|
// Find the last user message position (for catch-up tail).
|
|
@@ -262,28 +321,34 @@ export class OpencodeSessionReader {
|
|
|
262
321
|
}
|
|
263
322
|
}
|
|
264
323
|
}
|
|
265
|
-
// Track usage from step-finish parts
|
|
324
|
+
// Track usage from step-finish parts. v1 没有 idle 标记,turn 边界就是
|
|
325
|
+
// 上面那段"最后一条 user 消息";同一 turn 可能有多个 step-finish
|
|
326
|
+
// (每次模型请求一条),flow 要全加起来,只留末条会把多步 run 低报。
|
|
266
327
|
for (const part of msg.parts) {
|
|
267
328
|
if (part.type === 'step-finish' && part.tokens) {
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// step-finish reports all-zero tokens; trusting it would wipe out
|
|
271
|
-
// the run's real per-turn usage (contextLength/input/output/total
|
|
272
|
-
// all render 0). Skip zero-total steps so per-turn usage reflects
|
|
273
|
-
// the last step that actually consumed tokens.
|
|
329
|
+
// Skip zero-total steps(模型在 reasoning 中被截断)——它们没有消耗,
|
|
330
|
+
// 也不该把本次请求当成一条真实采样。
|
|
274
331
|
// ccusage-aligned: cache.write -> cacheCreationTokens.
|
|
275
332
|
// contextLength = input + output + cacheRead + cacheCreation —
|
|
276
333
|
// the unified context-window occupancy contract across all readers
|
|
277
334
|
// (§9.21:末轮 output 已进入历史、下一 prompt 全量携带,与 compact
|
|
278
335
|
// postTokens 水位同语义).
|
|
279
336
|
if ((part.tokens.total ?? 0) !== 0) {
|
|
337
|
+
const input = part.tokens.input ?? 0;
|
|
338
|
+
const output = part.tokens.output ?? 0;
|
|
339
|
+
const cacheRead = part.tokens.cache?.read ?? 0;
|
|
340
|
+
const cacheWrite = part.tokens.cache?.write ?? 0;
|
|
341
|
+
turn = {
|
|
342
|
+
input: turn.input + input,
|
|
343
|
+
output: turn.output + output,
|
|
344
|
+
cacheRead: turn.cacheRead + cacheRead,
|
|
345
|
+
cacheCreation: turn.cacheCreation + cacheWrite,
|
|
346
|
+
total: turn.total + part.tokens.total,
|
|
347
|
+
};
|
|
280
348
|
usage = {
|
|
281
|
-
inputTokens:
|
|
282
|
-
outputTokens:
|
|
283
|
-
contextLength:
|
|
284
|
-
(part.tokens.output ?? 0) +
|
|
285
|
-
(part.tokens.cache?.read ?? 0) +
|
|
286
|
-
(part.tokens.cache?.write ?? 0),
|
|
349
|
+
inputTokens: input,
|
|
350
|
+
outputTokens: output,
|
|
351
|
+
contextLength: input + output + cacheRead + cacheWrite,
|
|
287
352
|
cacheReadTokens: part.tokens.cache?.read,
|
|
288
353
|
cacheCreationTokens: part.tokens.cache?.write,
|
|
289
354
|
totalTokens: part.tokens.total,
|
|
@@ -292,6 +357,14 @@ export class OpencodeSessionReader {
|
|
|
292
357
|
}
|
|
293
358
|
}
|
|
294
359
|
}
|
|
360
|
+
// flow 覆盖末条采样值,contextLength 保留末条(水位不求和)。
|
|
361
|
+
if (usage) {
|
|
362
|
+
usage.inputTokens = turn.input;
|
|
363
|
+
usage.outputTokens = turn.output;
|
|
364
|
+
usage.cacheReadTokens = turn.cacheRead;
|
|
365
|
+
usage.cacheCreationTokens = turn.cacheCreation;
|
|
366
|
+
usage.totalTokens = turn.total;
|
|
367
|
+
}
|
|
295
368
|
// Cumulative (session-wide): sum input/output/total across ALL step-finish
|
|
296
369
|
// parts in the whole session. The catch-up loop above only scans from
|
|
297
370
|
// the last user message (= last run) for per-turn usage; cumulative
|
|
@@ -461,31 +534,46 @@ export class OpencodeSessionReader {
|
|
|
461
534
|
let cumTotal = 0;
|
|
462
535
|
let cumCacheRead = 0;
|
|
463
536
|
let cumCacheWrite = 0;
|
|
464
|
-
|
|
465
|
-
|
|
537
|
+
// 本 turn(= 本 run)的 flow:末个 turn 内每次模型请求之和。contextLength
|
|
538
|
+
// 仍取末次请求的窗口水位(水位不可求和)。只取末条会把多步 run 低报
|
|
539
|
+
// (实测一条 25 请求的 run:input 20,040 显示成 543、output 10,358 显示成 1,250)。
|
|
540
|
+
const turnStart = lastTurnStartIndex(messages);
|
|
541
|
+
const turn = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, total: 0 };
|
|
542
|
+
for (let i = 0; i < messages.length; i++) {
|
|
543
|
+
const sample = usageSample(messages[i]);
|
|
544
|
+
if (!sample)
|
|
466
545
|
continue;
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (total !== 0) {
|
|
479
|
-
latest = {
|
|
480
|
-
inputTokens: tokens.input,
|
|
481
|
-
outputTokens: tokens.output,
|
|
482
|
-
// §9.21 口径:含末轮 output(与 postTokens 水位同语义)。
|
|
483
|
-
contextLength: tokens.input + tokens.output + cacheRead + cacheWrite,
|
|
484
|
-
cacheReadTokens: cacheRead,
|
|
485
|
-
cacheCreationTokens: cacheWrite,
|
|
486
|
-
totalTokens: total,
|
|
487
|
-
};
|
|
546
|
+
cumInput += sample.input;
|
|
547
|
+
cumOutput += sample.output;
|
|
548
|
+
cumTotal += sample.total;
|
|
549
|
+
cumCacheRead += sample.cacheRead;
|
|
550
|
+
cumCacheWrite += sample.cacheCreation;
|
|
551
|
+
if (i > turnStart) {
|
|
552
|
+
turn.input += sample.input;
|
|
553
|
+
turn.output += sample.output;
|
|
554
|
+
turn.cacheRead += sample.cacheRead;
|
|
555
|
+
turn.cacheCreation += sample.cacheCreation;
|
|
556
|
+
turn.total += sample.total;
|
|
488
557
|
}
|
|
558
|
+
latest = {
|
|
559
|
+
inputTokens: sample.input,
|
|
560
|
+
outputTokens: sample.output,
|
|
561
|
+
// §9.21 口径:含末轮 output(与 postTokens 水位同语义)。
|
|
562
|
+
contextLength: sample.input + sample.output + sample.cacheRead + sample.cacheCreation,
|
|
563
|
+
cacheReadTokens: sample.cacheRead,
|
|
564
|
+
cacheCreationTokens: sample.cacheCreation,
|
|
565
|
+
totalTokens: sample.total,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
// turnStart 可能落在末条采样之后(新 turn 尚无采样)→ turn 全 0;latest
|
|
569
|
+
// 仍是上一轮末条采样,contextLength 保留水位。turnStart 小于最后一个采样
|
|
570
|
+
// 下标时,flow 覆盖末条值,只有 contextLength 保留末条。
|
|
571
|
+
if (latest) {
|
|
572
|
+
latest.inputTokens = turn.input;
|
|
573
|
+
latest.outputTokens = turn.output;
|
|
574
|
+
latest.cacheReadTokens = turn.cacheRead;
|
|
575
|
+
latest.cacheCreationTokens = turn.cacheCreation;
|
|
576
|
+
latest.totalTokens = turn.total;
|
|
489
577
|
}
|
|
490
578
|
latest = withCumulativeUsage(latest, {
|
|
491
579
|
total: cumTotal,
|