lark-remote 0.5.13 → 0.5.15
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 +31 -0
- package/dist/agent-kinds.d.ts +27 -0
- package/dist/agent-kinds.js +38 -0
- package/dist/bridge/approval-actions.d.ts +95 -0
- package/dist/bridge/approval-actions.js +104 -0
- package/dist/bridge/approval-coordinator.d.ts +13 -0
- package/dist/bridge/approval-coordinator.js +18 -0
- package/dist/bridge/final-usage.d.ts +8 -17
- package/dist/bridge/final-usage.js +1 -18
- package/dist/bridge/index.d.ts +46 -39
- package/dist/bridge/index.js +269 -240
- package/dist/bridge/queue-manager.d.ts +25 -1
- package/dist/bridge/queue-manager.js +179 -160
- package/dist/bridge/run-approval.d.ts +2 -1
- package/dist/bridge/run-usage.d.ts +21 -30
- package/dist/bridge/run-usage.js +47 -51
- package/dist/bridge/session-writeback.d.ts +3 -2
- package/dist/card/approval-render.d.ts +3 -2
- package/dist/card/approval-render.js +3 -2
- package/dist/{router → card}/card-helpers.d.ts +7 -7
- package/dist/{router → card}/card-helpers.js +8 -8
- package/dist/card/card-session.d.ts +4 -2
- package/dist/card/card-session.js +8 -6
- package/dist/card/card-shared.d.ts +1 -1
- package/dist/card/card-shared.js +1 -1
- package/dist/card/final-answer-overflow.d.ts +0 -6
- package/dist/card/final-answer-overflow.js +1 -1
- package/dist/card/run-card-session.d.ts +2 -2
- package/dist/card/run-card-session.js +2 -2
- package/dist/card/run-renderer.js +29 -30
- package/dist/card/run-state.d.ts +28 -50
- package/dist/card/run-state.js +23 -0
- package/dist/card/usage-format.d.ts +30 -0
- package/dist/{router → card}/usage-format.js +3 -10
- package/dist/clone.js +6 -1
- package/dist/config/index.d.ts +16 -20
- package/dist/config/index.js +0 -16
- package/dist/config/pi-config.js +6 -4
- package/dist/connector/index.d.ts +2 -4
- package/dist/connector/index.js +7 -11
- package/dist/{runner/dsh → dsh}/auth.js +1 -1
- package/dist/dsh/protocol.d.ts +9 -0
- package/dist/dsh/protocol.js +61 -0
- package/dist/index.js +10 -5
- package/dist/model-catalog.d.ts +14 -0
- package/dist/model-catalog.js +25 -0
- package/dist/platform/path.d.ts +9 -0
- package/dist/platform/path.js +18 -0
- package/dist/router/active-run-usage.d.ts +4 -0
- package/dist/router/active-run-usage.js +12 -0
- package/dist/router/commands/ls.js +1 -1
- package/dist/router/commands/order.js +1 -1
- package/dist/router/commands/resume.d.ts +5 -4
- package/dist/router/commands/resume.js +4 -4
- package/dist/router/commands/ws.js +1 -1
- package/dist/router/config/claude.js +2 -1
- package/dist/router/config/common/render.d.ts +5 -1
- package/dist/router/config/common/render.js +6 -13
- package/dist/router/config/dsh.d.ts +1 -1
- package/dist/router/config/index.d.ts +1 -1
- package/dist/router/config/types.d.ts +11 -3
- package/dist/router/index.d.ts +41 -11
- package/dist/router/index.js +169 -233
- package/dist/runner/agent-choices-common.d.ts +1 -1
- package/dist/runner/claude/runner.d.ts +20 -2
- package/dist/runner/claude/runner.js +2 -15
- package/dist/runner/codex/app-server/runner.d.ts +3 -1
- package/dist/runner/codex/app-server/translator.d.ts +11 -0
- package/dist/runner/codex/app-server/translator.js +56 -149
- package/dist/runner/common/acp/base-acp-runner.d.ts +19 -1
- 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/jsonrpc/transport.js +17 -7
- package/dist/runner/dsh/client.d.ts +1 -4
- package/dist/runner/dsh/client.js +7 -55
- package/dist/runner/dsh/daemon.js +9 -2
- package/dist/runner/dsh/exit-cancel.js +1 -1
- package/dist/runner/dsh/mux-stream.js +21 -2
- package/dist/runner/dsh/runner.d.ts +21 -1
- package/dist/runner/dsh/runner.js +97 -78
- package/dist/runner/dsh/translator.d.ts +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +22 -1
- package/dist/runner/kimi/acp/runner.js +83 -60
- package/dist/runner/opencode/acp/error-monitor.d.ts +11 -0
- package/dist/runner/opencode/acp/error-monitor.js +31 -2
- package/dist/runner/opencode/acp/runner.d.ts +14 -1
- package/dist/runner/opencode/acp/runner.js +13 -2
- package/dist/runner/pi/rpc/runner.d.ts +2 -1
- package/dist/runner/probe.d.ts +1 -1
- package/dist/runner/probe.js +10 -9
- package/dist/runner/registry.d.ts +2 -1
- package/dist/runner/types.d.ts +20 -154
- package/dist/runner/types.js +4 -24
- package/dist/session/claude/session-index.d.ts +11 -4
- package/dist/session/claude/session-index.js +30 -8
- package/dist/session/claude/session-reader.d.ts +1 -1
- package/dist/session/claude/sessions.d.ts +2 -2
- package/dist/session/claude/sessions.js +41 -26
- package/dist/session/codex/rollout-reader.d.ts +1 -1
- package/dist/session/codex/rollout-reader.js +14 -13
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +4 -2
- package/dist/session/common/jsonl.js +7 -1
- package/dist/session/common/pagination.d.ts +2 -0
- package/dist/session/common/pagination.js +1 -1
- package/dist/session/common/recency.d.ts +6 -0
- package/dist/session/common/recency.js +7 -5
- package/dist/session/common/two-pass.d.ts +1 -1
- package/dist/session/common/usage-accumulator.d.ts +1 -1
- package/dist/session/dsh/sessions.d.ts +1 -1
- package/dist/session/dsh/sessions.js +37 -12
- package/dist/session/kimi/sessions.d.ts +1 -1
- package/dist/session/kimi/sessions.js +72 -27
- package/dist/session/opencode/sessions.d.ts +1 -1
- package/dist/session/opencode/sessions.js +164 -81
- package/dist/session/pi/sessions.d.ts +1 -1
- package/dist/session/pi/sessions.js +20 -7
- package/dist/session/registry.d.ts +2 -1
- package/dist/session/session-store.d.ts +1 -2
- package/dist/session/session-store.js +14 -7
- package/dist/session-contract.d.ts +178 -0
- package/dist/session-contract.js +9 -0
- package/package.json +1 -1
- package/dist/router/usage-format.d.ts +0 -60
- /package/dist/{runner/dsh → dsh}/auth.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/home.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/home.js +0 -0
- /package/dist/{runner/dsh → dsh}/types.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/types.js +0 -0
|
@@ -5,9 +5,9 @@ import { readJsonlLines as readJsonlLinesShared, readLastNJsonlLines } from '../
|
|
|
5
5
|
import { UsageAccumulator, cumulativeUsageFields } from '../common/usage-accumulator.js';
|
|
6
6
|
import { getLogger } from '../../logger/index.js';
|
|
7
7
|
import { isStale } from '../common/constants.js';
|
|
8
|
-
import { samePath } from '../../platform/path.js';
|
|
8
|
+
import { safeRealpath, samePath } from '../../platform/path.js';
|
|
9
9
|
import { capEvents, paginate } from '../common/pagination.js';
|
|
10
|
-
import { sortByRecencyDesc } from '../common/recency.js';
|
|
10
|
+
import { newestOf, sortByRecencyDesc } from '../common/recency.js';
|
|
11
11
|
import { TtlCache } from '../../common/ttl-cache.js';
|
|
12
12
|
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
13
13
|
import { truncateToolInput } from '../common/content-blocks.js';
|
|
@@ -88,7 +88,12 @@ function readJsonlLines(filePath) {
|
|
|
88
88
|
jsonlCache.set(filePath, { mtime, lines });
|
|
89
89
|
return lines;
|
|
90
90
|
}
|
|
91
|
-
catch {
|
|
91
|
+
catch (err) {
|
|
92
|
+
// ENOENT = 会话文件在扫描间隙被清理(正常);其余 errno 留痕,否则该会话
|
|
93
|
+
// 静默从 /resume 消失。
|
|
94
|
+
if (err.code !== 'ENOENT') {
|
|
95
|
+
getLogger().warn(`[kimi-session-reader] cannot read ${filePath}: ${err?.message ?? String(err)}`);
|
|
96
|
+
}
|
|
92
97
|
return [];
|
|
93
98
|
}
|
|
94
99
|
}
|
|
@@ -201,7 +206,13 @@ export class KimiSessionReader {
|
|
|
201
206
|
*/
|
|
202
207
|
listSessions(cwd, opts) {
|
|
203
208
|
try {
|
|
204
|
-
|
|
209
|
+
// 归一失败(陈旧/无权限 cwd)回退原样输入,让逐条 samePath 比较去决定
|
|
210
|
+
// 空态——一次 realpathSync 抛错不得把整张列表清掉(与 opencode 同口径)。
|
|
211
|
+
const realCwd = safeRealpath(cwd, (err) => {
|
|
212
|
+
if (err.code === 'ENOENT')
|
|
213
|
+
return;
|
|
214
|
+
getLogger().debug(`[kimi-session-reader] realpath ${cwd} failed, using the path as given: ${err?.message ?? String(err)}`);
|
|
215
|
+
});
|
|
205
216
|
const sessions = [];
|
|
206
217
|
for (const entry of this.scanSessionIndex()) {
|
|
207
218
|
// state.json 一次读取共用(cwd 兜底 + summary),I/O 减半。
|
|
@@ -258,7 +269,16 @@ export class KimiSessionReader {
|
|
|
258
269
|
mtimeMs = fs.statSync(wirePath).mtimeMs;
|
|
259
270
|
}
|
|
260
271
|
catch {
|
|
261
|
-
|
|
272
|
+
try {
|
|
273
|
+
mtimeMs = fs.statSync(entry.sessionDir).mtimeMs;
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
if (err.code !== 'ENOENT') {
|
|
277
|
+
getLogger().warn(`[kimi-session-reader] cannot stat ${entry.sessionDir}: ${err?.message ?? String(err)}`);
|
|
278
|
+
}
|
|
279
|
+
// 会话目录已不存在(或被清理):跳过该条,别让它拖垮整张列表。
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
262
282
|
}
|
|
263
283
|
sessions.push({
|
|
264
284
|
sessionId: entry.sessionId,
|
|
@@ -276,15 +296,22 @@ export class KimiSessionReader {
|
|
|
276
296
|
};
|
|
277
297
|
}
|
|
278
298
|
catch (error) {
|
|
279
|
-
|
|
280
|
-
|
|
299
|
+
// ENOENT(cwd 已删除):陈旧 cwd 保持优雅空态。
|
|
300
|
+
// 其余失败:error 日志 + 上抛,router 显示「列表失败」——静默 [] 会让
|
|
301
|
+
// 用户误以为没有 session,与真空不可区分(与 opencode 同口径)。
|
|
302
|
+
if (error.code === 'ENOENT') {
|
|
303
|
+
return { sessions: [], total: 0 };
|
|
304
|
+
}
|
|
305
|
+
getLogger().error(`[kimi-session-reader] listSessions failed: ${error}`);
|
|
306
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
307
|
+
throw new Error(`kimi session list 读取失败: ${msg}`, { cause: error });
|
|
281
308
|
}
|
|
282
309
|
}
|
|
283
310
|
/**
|
|
284
311
|
* Get the newest session for a given working directory
|
|
285
312
|
*/
|
|
286
313
|
getNewestSession(cwd) {
|
|
287
|
-
return this
|
|
314
|
+
return newestOf(this, cwd);
|
|
288
315
|
}
|
|
289
316
|
/**
|
|
290
317
|
* Read session content (events + usage)
|
|
@@ -320,7 +347,15 @@ export class KimiSessionReader {
|
|
|
320
347
|
// is an independent window). Aggregation covers the whole wire.jsonl
|
|
321
348
|
// and is independent of the maxEvents display slice
|
|
322
349
|
// (usage = the session's overall token cost).
|
|
323
|
-
|
|
350
|
+
//
|
|
351
|
+
// 本 run 的 flow 字段是**末个 turn**(最后一个 turn.prompt 之后)的记录之和:
|
|
352
|
+
// kimi 每次模型调用写一条 usage.record,带工具往返的 turn 会有多条。只取
|
|
353
|
+
// 末条会低报,取会话总量(旧实现)又会把历史所有 run 的消耗算进这一轮。
|
|
354
|
+
// 子 agent 的 wire 没有 turn 标记,只能进会话累计(无法归属到某一轮);
|
|
355
|
+
// 整份 wire 都没有 turn.prompt(旧数据)时沿用「整份文件 = 一个 turn」。
|
|
356
|
+
let turnAcc = null;
|
|
357
|
+
let lastMainUsage;
|
|
358
|
+
const accumulateUsage = (u, scope) => {
|
|
324
359
|
hasUsageRecord = true;
|
|
325
360
|
// Malformed records (missing usage object or missing fields) must
|
|
326
361
|
// not poison the accumulators with NaN: count missing fields as 0.
|
|
@@ -328,18 +363,21 @@ export class KimiSessionReader {
|
|
|
328
363
|
const output = u?.output ?? 0;
|
|
329
364
|
const inputCacheRead = u?.inputCacheRead ?? 0;
|
|
330
365
|
const inputCacheCreation = u?.inputCacheCreation ?? 0;
|
|
331
|
-
|
|
366
|
+
const record = {
|
|
332
367
|
input: inputOther,
|
|
333
368
|
output,
|
|
334
369
|
cacheRead: inputCacheRead,
|
|
335
370
|
cacheCreation: inputCacheCreation,
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
371
|
+
};
|
|
372
|
+
acc.add(record);
|
|
373
|
+
if (scope !== 'main')
|
|
374
|
+
return;
|
|
375
|
+
lastMainUsage = record;
|
|
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;
|
|
343
381
|
};
|
|
344
382
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
345
383
|
const line = lines[lineIndex];
|
|
@@ -347,7 +385,7 @@ export class KimiSessionReader {
|
|
|
347
385
|
const entry = JSON.parse(line);
|
|
348
386
|
// Extract usage from usage.record.
|
|
349
387
|
if (isUsageRecord(entry)) {
|
|
350
|
-
accumulateUsage(entry.usage,
|
|
388
|
+
accumulateUsage(entry.usage, 'main');
|
|
351
389
|
continue;
|
|
352
390
|
}
|
|
353
391
|
// §6.3: Count compaction events and track pre-compact context length.
|
|
@@ -374,6 +412,8 @@ export class KimiSessionReader {
|
|
|
374
412
|
// prompt), which is the correct catch-up title.
|
|
375
413
|
// Extract display title from turn.prompt
|
|
376
414
|
if (isTurnPrompt(entry)) {
|
|
415
|
+
// turn.prompt 是 turn 的起点:本 turn 的 flow 从这里重新累计。
|
|
416
|
+
turnAcc = new UsageAccumulator();
|
|
377
417
|
if (entry.input && entry.input.length > 0) {
|
|
378
418
|
displayTitle = entry.input[0].text;
|
|
379
419
|
}
|
|
@@ -443,7 +483,7 @@ export class KimiSessionReader {
|
|
|
443
483
|
try {
|
|
444
484
|
const subEntry = JSON.parse(subLine);
|
|
445
485
|
if (isUsageRecord(subEntry)) {
|
|
446
|
-
accumulateUsage(subEntry.usage,
|
|
486
|
+
accumulateUsage(subEntry.usage, 'subagent');
|
|
447
487
|
}
|
|
448
488
|
}
|
|
449
489
|
catch {
|
|
@@ -455,18 +495,23 @@ export class KimiSessionReader {
|
|
|
455
495
|
catch {
|
|
456
496
|
// agents dir unreadable — fall back to main-only usage
|
|
457
497
|
}
|
|
458
|
-
// Build the final AgentSessionUsage from
|
|
498
|
+
// Build the final AgentSessionUsage from the last turn's flow + session
|
|
499
|
+
// cumulative + main contextLength.
|
|
459
500
|
const t = acc.totals;
|
|
501
|
+
// 有 turn.prompt 时本 run = 末个 turn 的 flow(末 turn 还没有记录时退回
|
|
502
|
+
// 末条主 wire 记录);整份 wire 都没有 turn.prompt(旧数据/截断)时无从
|
|
503
|
+
// 分段,沿用「整份文件 = 一个 turn」的旧口径(含子 agent wire)。
|
|
504
|
+
const flow = turnAcc ? (turnAcc.count > 0 ? turnAcc.totals : (lastMainUsage ?? t)) : t;
|
|
460
505
|
// Kimi semantics: preserve 0 values (not undefined) for cache fields to match original
|
|
461
506
|
const totalUsage = {
|
|
462
|
-
inputTokens:
|
|
463
|
-
outputTokens:
|
|
507
|
+
inputTokens: flow.input,
|
|
508
|
+
outputTokens: flow.output,
|
|
464
509
|
contextLength: mainContextLength,
|
|
465
|
-
cacheReadTokens:
|
|
466
|
-
cacheCreationTokens:
|
|
467
|
-
totalTokens:
|
|
468
|
-
// Cumulative (session-wide):
|
|
469
|
-
//
|
|
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.
|
|
470
515
|
...cumulativeUsageFields(t),
|
|
471
516
|
// §6.3: compaction stats from wire.jsonl
|
|
472
517
|
compactCount,
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - isSessionActive: use `updated` timestamp with 1 hour stale window.
|
|
16
16
|
* - Simple memory cache with TTL.
|
|
17
17
|
*/
|
|
18
|
-
import type { AgentSession, AgentSessionReader, SessionContent } from '../../
|
|
18
|
+
import type { AgentSession, AgentSessionReader, SessionContent } from '../../session-contract.js';
|
|
19
19
|
interface OpencodeSessionReaderOptions {
|
|
20
20
|
/** Path to the opencode binary. Defaults to `opencode`. */
|
|
21
21
|
binary?: string;
|
|
@@ -22,9 +22,9 @@ import os from 'node:os';
|
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
24
24
|
import { isStale } from '../common/constants.js';
|
|
25
|
-
import { samePath } from '../../platform/path.js';
|
|
25
|
+
import { safeRealpath, samePath } from '../../platform/path.js';
|
|
26
26
|
import { paginate, capEvents } from '../common/pagination.js';
|
|
27
|
-
import { sortByRecencyDesc } from '../common/recency.js';
|
|
27
|
+
import { newestOf, sortByRecencyDesc } from '../common/recency.js';
|
|
28
28
|
import { TtlCache } from '../../common/ttl-cache.js';
|
|
29
29
|
import { getLogger } from '../../logger/index.js';
|
|
30
30
|
/** v2 CLI defaults `session list` to 100 entries; keep the reader's historical
|
|
@@ -81,6 +81,84 @@ 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
|
+
for (let i = lastSample - 1; i >= 0; i--) {
|
|
123
|
+
if (messages[i]?.type === 'idle')
|
|
124
|
+
return i;
|
|
125
|
+
}
|
|
126
|
+
for (let i = lastSample - 1; i >= 0; i--) {
|
|
127
|
+
const type = messages[i]?.type;
|
|
128
|
+
if (type === 'user' || type === 'synthetic' || type === 'system')
|
|
129
|
+
return i;
|
|
130
|
+
}
|
|
131
|
+
return -1;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 把会话级累计 token 落到 usage 上(v1 export 与 v2 export 两条读取路径共用)。
|
|
135
|
+
*
|
|
136
|
+
* 已有 usage 就地补累计字段;没有 usage 但累计非零(例如只有 cache 计数)时,
|
|
137
|
+
* 构造一条只带累计的,避免"会话有消耗却显示无 usage"。
|
|
138
|
+
*/
|
|
139
|
+
function withCumulativeUsage(usage, cum) {
|
|
140
|
+
if (usage) {
|
|
141
|
+
usage.cumulativeTotalTokens = cum.total;
|
|
142
|
+
usage.cumulativeInputTokens = cum.input;
|
|
143
|
+
usage.cumulativeOutputTokens = cum.output;
|
|
144
|
+
usage.cumulativeCacheReadTokens = cum.cacheRead;
|
|
145
|
+
usage.cumulativeCacheCreationTokens = cum.cacheWrite;
|
|
146
|
+
return usage;
|
|
147
|
+
}
|
|
148
|
+
if (!(cum.input || cum.output || cum.total || cum.cacheRead || cum.cacheWrite)) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
inputTokens: 0,
|
|
153
|
+
outputTokens: 0,
|
|
154
|
+
contextLength: 0,
|
|
155
|
+
cumulativeTotalTokens: cum.total,
|
|
156
|
+
cumulativeInputTokens: cum.input,
|
|
157
|
+
cumulativeOutputTokens: cum.output,
|
|
158
|
+
cumulativeCacheReadTokens: cum.cacheRead,
|
|
159
|
+
cumulativeCacheCreationTokens: cum.cacheWrite,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
84
162
|
export class OpencodeSessionReader {
|
|
85
163
|
binary;
|
|
86
164
|
cacheTtlMs;
|
|
@@ -124,7 +202,7 @@ export class OpencodeSessionReader {
|
|
|
124
202
|
* Get the newest session in a given cwd.
|
|
125
203
|
*/
|
|
126
204
|
getNewestSession(cwd) {
|
|
127
|
-
return this
|
|
205
|
+
return newestOf(this, cwd);
|
|
128
206
|
}
|
|
129
207
|
/**
|
|
130
208
|
* Read session content: extract catch-up tail (last user message + everything after).
|
|
@@ -167,6 +245,8 @@ export class OpencodeSessionReader {
|
|
|
167
245
|
}
|
|
168
246
|
const events = [];
|
|
169
247
|
let usage;
|
|
248
|
+
/** 本 turn 的 flow 累计:末条 user 消息之后的全部 step-finish。 */
|
|
249
|
+
let turn = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, total: 0 };
|
|
170
250
|
let aiTitle;
|
|
171
251
|
let displayTitle;
|
|
172
252
|
// Find the last user message position (for catch-up tail).
|
|
@@ -233,28 +313,34 @@ export class OpencodeSessionReader {
|
|
|
233
313
|
}
|
|
234
314
|
}
|
|
235
315
|
}
|
|
236
|
-
// Track usage from step-finish parts
|
|
316
|
+
// Track usage from step-finish parts. v1 没有 idle 标记,turn 边界就是
|
|
317
|
+
// 上面那段"最后一条 user 消息";同一 turn 可能有多个 step-finish
|
|
318
|
+
// (每次模型请求一条),flow 要全加起来,只留末条会把多步 run 低报。
|
|
237
319
|
for (const part of msg.parts) {
|
|
238
320
|
if (part.type === 'step-finish' && part.tokens) {
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
// step-finish reports all-zero tokens; trusting it would wipe out
|
|
242
|
-
// the run's real per-turn usage (contextLength/input/output/total
|
|
243
|
-
// all render 0). Skip zero-total steps so per-turn usage reflects
|
|
244
|
-
// the last step that actually consumed tokens.
|
|
321
|
+
// Skip zero-total steps(模型在 reasoning 中被截断)——它们没有消耗,
|
|
322
|
+
// 也不该把本次请求当成一条真实采样。
|
|
245
323
|
// ccusage-aligned: cache.write -> cacheCreationTokens.
|
|
246
324
|
// contextLength = input + output + cacheRead + cacheCreation —
|
|
247
325
|
// the unified context-window occupancy contract across all readers
|
|
248
326
|
// (§9.21:末轮 output 已进入历史、下一 prompt 全量携带,与 compact
|
|
249
327
|
// postTokens 水位同语义).
|
|
250
328
|
if ((part.tokens.total ?? 0) !== 0) {
|
|
329
|
+
const input = part.tokens.input ?? 0;
|
|
330
|
+
const output = part.tokens.output ?? 0;
|
|
331
|
+
const cacheRead = part.tokens.cache?.read ?? 0;
|
|
332
|
+
const cacheWrite = part.tokens.cache?.write ?? 0;
|
|
333
|
+
turn = {
|
|
334
|
+
input: turn.input + input,
|
|
335
|
+
output: turn.output + output,
|
|
336
|
+
cacheRead: turn.cacheRead + cacheRead,
|
|
337
|
+
cacheCreation: turn.cacheCreation + cacheWrite,
|
|
338
|
+
total: turn.total + part.tokens.total,
|
|
339
|
+
};
|
|
251
340
|
usage = {
|
|
252
|
-
inputTokens:
|
|
253
|
-
outputTokens:
|
|
254
|
-
contextLength:
|
|
255
|
-
(part.tokens.output ?? 0) +
|
|
256
|
-
(part.tokens.cache?.read ?? 0) +
|
|
257
|
-
(part.tokens.cache?.write ?? 0),
|
|
341
|
+
inputTokens: input,
|
|
342
|
+
outputTokens: output,
|
|
343
|
+
contextLength: input + output + cacheRead + cacheWrite,
|
|
258
344
|
cacheReadTokens: part.tokens.cache?.read,
|
|
259
345
|
cacheCreationTokens: part.tokens.cache?.write,
|
|
260
346
|
totalTokens: part.tokens.total,
|
|
@@ -263,6 +349,14 @@ export class OpencodeSessionReader {
|
|
|
263
349
|
}
|
|
264
350
|
}
|
|
265
351
|
}
|
|
352
|
+
// flow 覆盖末条采样值,contextLength 保留末条(水位不求和)。
|
|
353
|
+
if (usage) {
|
|
354
|
+
usage.inputTokens = turn.input;
|
|
355
|
+
usage.outputTokens = turn.output;
|
|
356
|
+
usage.cacheReadTokens = turn.cacheRead;
|
|
357
|
+
usage.cacheCreationTokens = turn.cacheCreation;
|
|
358
|
+
usage.totalTokens = turn.total;
|
|
359
|
+
}
|
|
266
360
|
// Cumulative (session-wide): sum input/output/total across ALL step-finish
|
|
267
361
|
// parts in the whole session. The catch-up loop above only scans from
|
|
268
362
|
// the last user message (= last run) for per-turn usage; cumulative
|
|
@@ -283,25 +377,13 @@ export class OpencodeSessionReader {
|
|
|
283
377
|
}
|
|
284
378
|
}
|
|
285
379
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
else if (cumInput || cumOutput || cumTotal || cumCacheRead || cumCacheWrite) {
|
|
294
|
-
usage = {
|
|
295
|
-
inputTokens: 0,
|
|
296
|
-
outputTokens: 0,
|
|
297
|
-
contextLength: 0,
|
|
298
|
-
cumulativeTotalTokens: cumTotal,
|
|
299
|
-
cumulativeInputTokens: cumInput,
|
|
300
|
-
cumulativeOutputTokens: cumOutput,
|
|
301
|
-
cumulativeCacheReadTokens: cumCacheRead,
|
|
302
|
-
cumulativeCacheCreationTokens: cumCacheWrite,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
380
|
+
usage = withCumulativeUsage(usage, {
|
|
381
|
+
total: cumTotal,
|
|
382
|
+
input: cumInput,
|
|
383
|
+
output: cumOutput,
|
|
384
|
+
cacheRead: cumCacheRead,
|
|
385
|
+
cacheWrite: cumCacheWrite,
|
|
386
|
+
});
|
|
305
387
|
// Use info.title as aiTitle
|
|
306
388
|
if (data.info.title) {
|
|
307
389
|
aiTitle = data.info.title;
|
|
@@ -444,51 +526,53 @@ export class OpencodeSessionReader {
|
|
|
444
526
|
let cumTotal = 0;
|
|
445
527
|
let cumCacheRead = 0;
|
|
446
528
|
let cumCacheWrite = 0;
|
|
447
|
-
|
|
448
|
-
|
|
529
|
+
// 本 turn(= 本 run)的 flow:末个 turn 内每次模型请求之和。contextLength
|
|
530
|
+
// 仍取末次请求的窗口水位(水位不可求和)。只取末条会把多步 run 低报
|
|
531
|
+
// (实测一条 25 请求的 run:input 20,040 显示成 543、output 10,358 显示成 1,250)。
|
|
532
|
+
const turnStart = lastTurnStartIndex(messages);
|
|
533
|
+
const turn = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, total: 0 };
|
|
534
|
+
for (let i = 0; i < messages.length; i++) {
|
|
535
|
+
const sample = usageSample(messages[i]);
|
|
536
|
+
if (!sample)
|
|
449
537
|
continue;
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (total !== 0) {
|
|
462
|
-
latest = {
|
|
463
|
-
inputTokens: tokens.input,
|
|
464
|
-
outputTokens: tokens.output,
|
|
465
|
-
// §9.21 口径:含末轮 output(与 postTokens 水位同语义)。
|
|
466
|
-
contextLength: tokens.input + tokens.output + cacheRead + cacheWrite,
|
|
467
|
-
cacheReadTokens: cacheRead,
|
|
468
|
-
cacheCreationTokens: cacheWrite,
|
|
469
|
-
totalTokens: total,
|
|
470
|
-
};
|
|
538
|
+
cumInput += sample.input;
|
|
539
|
+
cumOutput += sample.output;
|
|
540
|
+
cumTotal += sample.total;
|
|
541
|
+
cumCacheRead += sample.cacheRead;
|
|
542
|
+
cumCacheWrite += sample.cacheCreation;
|
|
543
|
+
if (i > turnStart) {
|
|
544
|
+
turn.input += sample.input;
|
|
545
|
+
turn.output += sample.output;
|
|
546
|
+
turn.cacheRead += sample.cacheRead;
|
|
547
|
+
turn.cacheCreation += sample.cacheCreation;
|
|
548
|
+
turn.total += sample.total;
|
|
471
549
|
}
|
|
472
|
-
}
|
|
473
|
-
if (latest) {
|
|
474
|
-
latest.cumulativeTotalTokens = cumTotal;
|
|
475
|
-
latest.cumulativeInputTokens = cumInput;
|
|
476
|
-
latest.cumulativeOutputTokens = cumOutput;
|
|
477
|
-
latest.cumulativeCacheReadTokens = cumCacheRead;
|
|
478
|
-
latest.cumulativeCacheCreationTokens = cumCacheWrite;
|
|
479
|
-
}
|
|
480
|
-
else if (cumInput || cumOutput || cumTotal || cumCacheRead || cumCacheWrite) {
|
|
481
550
|
latest = {
|
|
482
|
-
inputTokens:
|
|
483
|
-
outputTokens:
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
cumulativeCacheCreationTokens: cumCacheWrite,
|
|
551
|
+
inputTokens: sample.input,
|
|
552
|
+
outputTokens: sample.output,
|
|
553
|
+
// §9.21 口径:含末轮 output(与 postTokens 水位同语义)。
|
|
554
|
+
contextLength: sample.input + sample.output + sample.cacheRead + sample.cacheCreation,
|
|
555
|
+
cacheReadTokens: sample.cacheRead,
|
|
556
|
+
cacheCreationTokens: sample.cacheCreation,
|
|
557
|
+
totalTokens: sample.total,
|
|
490
558
|
};
|
|
491
559
|
}
|
|
560
|
+
// turnStart 严格小于最后一个采样下标,所以 latest 存在时 turn 至少含一条
|
|
561
|
+
// 采样:flow 覆盖末条值,只有 contextLength 保留末条。
|
|
562
|
+
if (latest) {
|
|
563
|
+
latest.inputTokens = turn.input;
|
|
564
|
+
latest.outputTokens = turn.output;
|
|
565
|
+
latest.cacheReadTokens = turn.cacheRead;
|
|
566
|
+
latest.cacheCreationTokens = turn.cacheCreation;
|
|
567
|
+
latest.totalTokens = turn.total;
|
|
568
|
+
}
|
|
569
|
+
latest = withCumulativeUsage(latest, {
|
|
570
|
+
total: cumTotal,
|
|
571
|
+
input: cumInput,
|
|
572
|
+
output: cumOutput,
|
|
573
|
+
cacheRead: cumCacheRead,
|
|
574
|
+
cacheWrite: cumCacheWrite,
|
|
575
|
+
});
|
|
492
576
|
return {
|
|
493
577
|
events: capEvents(events, opts?.maxEvents),
|
|
494
578
|
usage: latest,
|
|
@@ -518,12 +602,11 @@ export class OpencodeSessionReader {
|
|
|
518
602
|
// --- Private helpers ---
|
|
519
603
|
/** realpath 归一化(public:测试直接调用,替代 as unknown as)。 */
|
|
520
604
|
realpath(cwd) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
605
|
+
return safeRealpath(cwd, (err) => {
|
|
606
|
+
if (err.code === 'ENOENT')
|
|
607
|
+
return;
|
|
608
|
+
getLogger().debug(`[opencode-session-reader] realpath ${cwd} failed, using the path as given: ${err?.message ?? String(err)}`);
|
|
609
|
+
});
|
|
527
610
|
}
|
|
528
611
|
fetchSessionList(cwd) {
|
|
529
612
|
const realCwd = this.realpath(cwd);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ContentBlockMapping } from '../common/content-blocks.js';
|
|
2
|
-
import type { AgentSession, AgentSessionReader, SessionContent } from '../../
|
|
2
|
+
import type { AgentSession, AgentSessionReader, SessionContent } from '../../session-contract.js';
|
|
3
3
|
/** Pi-specific field-name mapping for content block extraction. */
|
|
4
4
|
export declare const PI_MAPPING: ContentBlockMapping;
|
|
5
5
|
/**
|
|
@@ -8,7 +8,7 @@ import { UsageAccumulator, contextWindowOccupancy, cumulativeUsageFields, } from
|
|
|
8
8
|
import { isStale } from '../common/constants.js';
|
|
9
9
|
import { encodeProjectDirName, samePath } from '../../platform/path.js';
|
|
10
10
|
import { paginate } from '../common/pagination.js';
|
|
11
|
-
import { sortByRecencyDesc } from '../common/recency.js';
|
|
11
|
+
import { newestOf, sortByRecencyDesc } from '../common/recency.js';
|
|
12
12
|
import { TtlCache } from '../../common/ttl-cache.js';
|
|
13
13
|
import { getLogger } from '../../logger/index.js';
|
|
14
14
|
function defaultPiDir() {
|
|
@@ -135,8 +135,11 @@ function findSessionFile(sessionId, cwd, sessionsDir) {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
catch {
|
|
139
|
-
// dir
|
|
138
|
+
catch (err) {
|
|
139
|
+
// dir 不存在(陈旧 cwd / 该 cwd 从没有会话)是正常空态;其余留痕。
|
|
140
|
+
if (err.code !== 'ENOENT') {
|
|
141
|
+
getLogger().debug(`[pi-session-reader] cannot read ${dir}: ${err?.message ?? String(err)}`);
|
|
142
|
+
}
|
|
140
143
|
}
|
|
141
144
|
return null;
|
|
142
145
|
}
|
|
@@ -282,7 +285,7 @@ export class PiSessionReader {
|
|
|
282
285
|
};
|
|
283
286
|
}
|
|
284
287
|
getNewestSession(cwd) {
|
|
285
|
-
return this
|
|
288
|
+
return newestOf(this, cwd);
|
|
286
289
|
}
|
|
287
290
|
readSessionContent(sessionId, cwd, opts) {
|
|
288
291
|
let filePath = null;
|
|
@@ -319,7 +322,10 @@ export class PiSessionReader {
|
|
|
319
322
|
const st = fs.statSync(filePath);
|
|
320
323
|
return !isStale(st.mtimeMs);
|
|
321
324
|
}
|
|
322
|
-
catch {
|
|
325
|
+
catch (err) {
|
|
326
|
+
if (err.code !== 'ENOENT') {
|
|
327
|
+
getLogger().debug(`[pi-session-reader] stat ${filePath} failed: ${err?.message ?? String(err)}`);
|
|
328
|
+
}
|
|
323
329
|
return false;
|
|
324
330
|
}
|
|
325
331
|
}
|
|
@@ -335,8 +341,15 @@ export class PiSessionReader {
|
|
|
335
341
|
try {
|
|
336
342
|
files = fs.readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
|
|
337
343
|
}
|
|
338
|
-
catch {
|
|
339
|
-
|
|
344
|
+
catch (err) {
|
|
345
|
+
// ENOENT:该 cwd 还没有会话目录(正常空态)。其余失败上抛,由 router
|
|
346
|
+
// 显示「列表失败」——静默 [] 与真空不可区分(与 opencode 同口径)。
|
|
347
|
+
if (err.code === 'ENOENT') {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
getLogger().error(`[pi-session-reader] listSessions failed: ${err}`);
|
|
351
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
352
|
+
throw new Error(`pi session list 读取失败: ${msg}`, { cause: err });
|
|
340
353
|
}
|
|
341
354
|
const sessions = [];
|
|
342
355
|
for (const f of files) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AgentKind
|
|
1
|
+
import type { AgentKind } from '../agent-kinds.js';
|
|
2
|
+
import type { AgentSessionReader } from '../session-contract.js';
|
|
2
3
|
/**
|
|
3
4
|
* Registry of `AgentSessionReader` instances, keyed by `AgentKind`. Mirrors
|
|
4
5
|
* `AgentRegistry` but for session-history reads. `index.ts` registers one
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentKind } from '../
|
|
1
|
+
import type { AgentKind } from '../agent-kinds.js';
|
|
2
2
|
interface SessionEntry {
|
|
3
3
|
/** Session IDs per agent kind (Map for in-memory, plain object for persistence). */
|
|
4
4
|
sessions: Map<AgentKind, string>;
|
|
@@ -58,7 +58,6 @@ export declare class SessionStore {
|
|
|
58
58
|
*/
|
|
59
59
|
private mutateEntry;
|
|
60
60
|
get(userId: string): SessionEntry | undefined;
|
|
61
|
-
set(userId: string, entry: SessionEntry): void;
|
|
62
61
|
delete(userId: string): void;
|
|
63
62
|
/** Clear sessionId for a specific agent, keeping cwd and other agent sessions */
|
|
64
63
|
clearSessionId(userId: string, agent?: AgentKind, opts?: {
|
|
@@ -85,10 +85,6 @@ export class SessionStore {
|
|
|
85
85
|
get(userId) {
|
|
86
86
|
return this.sessions.get(userId);
|
|
87
87
|
}
|
|
88
|
-
set(userId, entry) {
|
|
89
|
-
this.sessions.set(userId, entry);
|
|
90
|
-
this.autoPersist();
|
|
91
|
-
}
|
|
92
88
|
delete(userId) {
|
|
93
89
|
this.sessions.delete(userId);
|
|
94
90
|
this.autoPersist();
|
|
@@ -170,7 +166,7 @@ export class SessionStore {
|
|
|
170
166
|
/** Get sessionId for a specific agent, or undefined if no session or sessionId is empty */
|
|
171
167
|
getSessionId(userId, agent = 'claude') {
|
|
172
168
|
const entry = this.sessions.get(userId);
|
|
173
|
-
if (!entry
|
|
169
|
+
if (!entry)
|
|
174
170
|
return undefined;
|
|
175
171
|
const sessionId = entry.sessions.get(agent);
|
|
176
172
|
if (!sessionId)
|
|
@@ -284,8 +280,19 @@ export class SessionStore {
|
|
|
284
280
|
}
|
|
285
281
|
}
|
|
286
282
|
}
|
|
287
|
-
catch {
|
|
288
|
-
getLogger().warn(
|
|
283
|
+
catch (err) {
|
|
284
|
+
getLogger().warn(`[session] persisted session store corrupted (${this.persistPath ?? '(memory)'}), ignoring:`, err);
|
|
285
|
+
// 改名保留现场:原文件继续原地躺着会让下次启动重复告警,也丢掉排障证据。
|
|
286
|
+
if (this.persistPath) {
|
|
287
|
+
const brokenPath = `${this.persistPath}.corrupt-${Date.now()}`;
|
|
288
|
+
try {
|
|
289
|
+
fs.renameSync(this.persistPath, brokenPath);
|
|
290
|
+
getLogger().warn(`[session] moved corrupt store to ${brokenPath}`);
|
|
291
|
+
}
|
|
292
|
+
catch (renameErr) {
|
|
293
|
+
getLogger().warn('[session] failed to preserve corrupt store:', renameErr);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
289
296
|
}
|
|
290
297
|
}
|
|
291
298
|
autoPersist() {
|