lark-remote 0.5.19 → 0.5.21
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 +29 -0
- package/dist/bridge/approval-actions.d.ts +1 -1
- package/dist/bridge/approval-actions.js +1 -1
- package/dist/bridge/bash-run.d.ts +71 -0
- package/dist/bridge/bash-run.js +217 -0
- package/dist/bridge/compact.d.ts +155 -0
- package/dist/bridge/compact.js +450 -0
- package/dist/bridge/final-usage.js +4 -4
- package/dist/bridge/inbound-media.js +72 -46
- package/dist/bridge/index.d.ts +106 -153
- package/dist/bridge/index.js +400 -1000
- package/dist/bridge/job-watch.d.ts +68 -0
- package/dist/bridge/job-watch.js +146 -0
- package/dist/bridge/queue-manager.d.ts +3 -3
- package/dist/bridge/queue-manager.js +10 -29
- package/dist/card/approval-render.d.ts +2 -2
- package/dist/card/approval-render.js +141 -130
- package/dist/card/card-budget.js +123 -98
- package/dist/card/card-helpers.d.ts +3 -3
- package/dist/card/card-helpers.js +6 -6
- package/dist/card/final-answer-file.js +1 -1
- package/dist/card/run-renderer.d.ts +3 -3
- package/dist/card/run-renderer.js +114 -159
- package/dist/card/run-state.d.ts +1 -15
- package/dist/card/run-state.js +358 -292
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +21 -17
- package/dist/card/usage-format.d.ts +22 -7
- package/dist/card/usage-format.js +119 -100
- package/dist/cli.js +15 -3
- package/dist/clone.js +2 -2
- package/dist/common/text-width.js +36 -24
- package/dist/common/tool-summary.js +132 -90
- package/dist/config/codex-config.js +130 -106
- package/dist/config/index.d.ts +16 -2
- package/dist/config/index.js +23 -8
- package/dist/connector/index.js +6 -6
- package/dist/error-classification.js +3 -2
- package/dist/inbound/placeholder.d.ts +1 -1
- package/dist/inbound/placeholder.js +1 -1
- package/dist/index.js +387 -281
- package/dist/logger/index.js +6 -6
- package/dist/message-handlers.d.ts +0 -5
- package/dist/message-handlers.js +254 -237
- package/dist/model-catalog.d.ts +2 -2
- package/dist/model-catalog.js +3 -8
- package/dist/platform/identity.js +1 -1
- package/dist/platform/path.d.ts +1 -1
- package/dist/platform/path.js +1 -1
- package/dist/platform/sleep-blocker.js +1 -1
- package/dist/platform/terminator-win32.js +2 -1
- package/dist/router/commands/ls.d.ts +3 -0
- package/dist/router/commands/ls.js +256 -175
- package/dist/router/commands/order.js +184 -159
- package/dist/router/commands/resume.js +190 -152
- package/dist/router/commands/ws.js +219 -174
- package/dist/router/config/dsh.d.ts +4 -8
- package/dist/router/config/dsh.js +3 -10
- package/dist/router/config/kimi.js +1 -1
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/config-diff.js +1 -1
- package/dist/router/index.d.ts +56 -11
- package/dist/router/index.js +401 -391
- package/dist/runner/bash/runner.js +2 -4
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +6 -4
- package/dist/runner/codex/app-server/runner.d.ts +9 -2
- package/dist/runner/codex/app-server/runner.js +30 -25
- package/dist/runner/codex/app-server/translator.d.ts +2 -5
- package/dist/runner/codex/app-server/translator.js +291 -238
- package/dist/runner/common/acp/acp-approval.d.ts +1 -1
- package/dist/runner/common/acp/acp-approval.js +1 -1
- package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
- package/dist/runner/common/acp/base-acp-runner.js +26 -65
- package/dist/runner/common/acp/base-translator.d.ts +12 -2
- package/dist/runner/common/acp/base-translator.js +50 -26
- package/dist/runner/common/acp/protocol-types.d.ts +1 -1
- package/dist/runner/common/acp/protocol-types.js +2 -2
- package/dist/runner/common/connection-based-runner.d.ts +32 -1
- package/dist/runner/common/connection-based-runner.js +32 -2
- package/dist/runner/common/jsonl-stream.js +154 -187
- package/dist/runner/common/jsonrpc/client.d.ts +29 -0
- package/dist/runner/common/jsonrpc/client.js +43 -12
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
- package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
- package/dist/runner/common/spawning-runner.d.ts +6 -1
- package/dist/runner/common/spawning-runner.js +9 -4
- package/dist/runner/common/turn-started.d.ts +8 -0
- package/dist/runner/common/turn-started.js +11 -0
- package/dist/runner/dsh/approval.js +84 -51
- package/dist/runner/dsh/catalog-probe.d.ts +17 -0
- package/dist/runner/dsh/catalog-probe.js +10 -0
- package/dist/runner/dsh/client.d.ts +7 -11
- package/dist/runner/dsh/client.js +356 -289
- package/dist/runner/dsh/exit-cancel.d.ts +2 -2
- package/dist/runner/dsh/exit-cancel.js +2 -2
- package/dist/runner/dsh/inflight.d.ts +1 -1
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/mux-stream.js +28 -8
- package/dist/runner/dsh/runner.d.ts +44 -4
- package/dist/runner/dsh/runner.js +296 -199
- package/dist/runner/dsh/translator.d.ts +1 -31
- package/dist/runner/dsh/translator.js +180 -178
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +1 -2
- package/dist/runner/kimi/acp/runner.js +82 -46
- package/dist/runner/kimi/acp/translator.d.ts +1 -1
- package/dist/runner/kimi/acp/translator.js +3 -4
- package/dist/runner/opencode/acp/error-monitor.js +5 -5
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +4 -4
- package/dist/runner/pi/rpc/client.js +4 -13
- package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
- package/dist/runner/pi/rpc/protocol-types.js +1 -1
- package/dist/runner/pi/rpc/runner.d.ts +3 -0
- package/dist/runner/pi/rpc/runner.js +27 -42
- package/dist/runner/pi/rpc/translator.js +61 -60
- package/dist/runner/types.d.ts +2 -2
- package/dist/runner/types.js +1 -1
- package/dist/session/claude/sessions.d.ts +45 -0
- package/dist/session/claude/sessions.js +160 -122
- package/dist/session/codex/rollout-reader.js +181 -182
- package/dist/session/common/throw-session-list-error.d.ts +10 -0
- package/dist/session/common/throw-session-list-error.js +16 -0
- package/dist/session/dsh/sessions.js +291 -235
- package/dist/session/kimi/sessions.d.ts +40 -8
- package/dist/session/kimi/sessions.js +373 -301
- package/dist/session/opencode/sessions.d.ts +142 -1
- package/dist/session/opencode/sessions.js +378 -321
- package/dist/session/pi/sessions.js +4 -5
- package/dist/session/session-store.d.ts +9 -1
- package/dist/session/session-store.js +99 -99
- package/dist/update/version-check.js +2 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.js +1 -1
- package/package.json +1 -1
|
@@ -22,6 +22,7 @@ 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 { throwSessionListError } from '../common/throw-session-list-error.js';
|
|
25
26
|
import { safeRealpath, samePath } from '../../platform/path.js';
|
|
26
27
|
import { paginate, capEvents } from '../common/pagination.js';
|
|
27
28
|
import { newestOf, sortByRecencyDesc } from '../common/recency.js';
|
|
@@ -165,6 +166,349 @@ function withCumulativeUsage(usage, cum) {
|
|
|
165
166
|
cumulativeCacheCreationTokens: cum.cacheWrite,
|
|
166
167
|
};
|
|
167
168
|
}
|
|
169
|
+
function emptyFlow() {
|
|
170
|
+
return { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, total: 0 };
|
|
171
|
+
}
|
|
172
|
+
/** 末条 user 消息下标(catch-up tail 起点);无 user 消息 = -1。 */
|
|
173
|
+
function lastV1UserIndex(messages) {
|
|
174
|
+
let lastUserIndex = -1;
|
|
175
|
+
for (let i = 0; i < messages.length; i++) {
|
|
176
|
+
if (messages[i].info.role === 'user') {
|
|
177
|
+
lastUserIndex = i;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return lastUserIndex;
|
|
181
|
+
}
|
|
182
|
+
/** cumulative:全会话逐字段求和,含零 total 的退化 step(字段 0 不改变和)。 */
|
|
183
|
+
function addV1Cumulative(tokens, cum) {
|
|
184
|
+
cum.input += tokens.input ?? 0;
|
|
185
|
+
cum.output += tokens.output ?? 0;
|
|
186
|
+
cum.total += tokens.total ?? 0;
|
|
187
|
+
cum.cacheRead += tokens.cache?.read ?? 0;
|
|
188
|
+
cum.cacheCreation += tokens.cache?.write ?? 0;
|
|
189
|
+
}
|
|
190
|
+
/** 单次非零采样:累加本 run flow 并返回 latest 快照。 */
|
|
191
|
+
function addV1TurnSample(tokens, turn) {
|
|
192
|
+
// ccusage-aligned: cache.write -> cacheCreationTokens.
|
|
193
|
+
// contextLength = input + output + cacheRead + cacheCreation —
|
|
194
|
+
// the unified context-window occupancy contract across all readers
|
|
195
|
+
// (§9.21:末轮 output 已进入历史、下一 prompt 全量携带,与 compact
|
|
196
|
+
// postTokens 水位同语义).
|
|
197
|
+
const input = tokens.input ?? 0;
|
|
198
|
+
const output = tokens.output ?? 0;
|
|
199
|
+
const cacheRead = tokens.cache?.read ?? 0;
|
|
200
|
+
const cacheWrite = tokens.cache?.write ?? 0;
|
|
201
|
+
turn.input += input;
|
|
202
|
+
turn.output += output;
|
|
203
|
+
turn.cacheRead += cacheRead;
|
|
204
|
+
turn.cacheCreation += cacheWrite;
|
|
205
|
+
turn.total += tokens.total;
|
|
206
|
+
return {
|
|
207
|
+
inputTokens: input,
|
|
208
|
+
outputTokens: output,
|
|
209
|
+
contextLength: input + output + cacheRead + cacheWrite,
|
|
210
|
+
cacheReadTokens: tokens.cache?.read,
|
|
211
|
+
cacheCreationTokens: tokens.cache?.write,
|
|
212
|
+
totalTokens: tokens.total,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* 单条 step-finish part:cum 对全会话逐字段求和(含零 total 退化 step),
|
|
217
|
+
* inTail 且非零 total 时把本 run flow 累加到 turn 并返回新的 latest 采样。
|
|
218
|
+
*/
|
|
219
|
+
function accumulateV1StepPart(part, inTail, turn, cum) {
|
|
220
|
+
if (part.type !== 'step-finish' || !part.tokens)
|
|
221
|
+
return undefined;
|
|
222
|
+
addV1Cumulative(part.tokens, cum);
|
|
223
|
+
if (!inTail)
|
|
224
|
+
return undefined;
|
|
225
|
+
// Skip zero-total steps(模型在 reasoning 中被截断)——它们没有消耗,
|
|
226
|
+
// 也不该把本次请求当成一条真实采样。
|
|
227
|
+
if ((part.tokens.total ?? 0) === 0)
|
|
228
|
+
return undefined;
|
|
229
|
+
return addV1TurnSample(part.tokens, turn);
|
|
230
|
+
}
|
|
231
|
+
/** 一条 user 消息里的首个 text part(tail displayTitle 首选)。 */
|
|
232
|
+
function firstV1TextPart(parts) {
|
|
233
|
+
for (const part of parts) {
|
|
234
|
+
if (part.type === 'text' && part.text)
|
|
235
|
+
return part.text;
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
export function scanV1Messages(messages) {
|
|
240
|
+
// Find the last user message position (for catch-up tail).
|
|
241
|
+
// messages are already parsed objects — scan directly.
|
|
242
|
+
const lastUserIndex = lastV1UserIndex(messages);
|
|
243
|
+
// Process messages from last user message onwards (catch-up tail)
|
|
244
|
+
const startIndex = lastUserIndex >= 0 ? lastUserIndex : 0;
|
|
245
|
+
const turn = emptyFlow();
|
|
246
|
+
const cum = emptyFlow();
|
|
247
|
+
let latest;
|
|
248
|
+
let tailDisplayTitle;
|
|
249
|
+
for (let i = 0; i < messages.length; i++) {
|
|
250
|
+
const inTail = i >= startIndex;
|
|
251
|
+
const msg = messages[i];
|
|
252
|
+
// Track usage from step-finish parts. v1 没有 idle 标记,turn 边界就是
|
|
253
|
+
// 末条 user 消息;同一 turn 可能有多个 step-finish(每次模型请求一条),
|
|
254
|
+
// flow 要全加起来,只留末条会把多步 run 低报。
|
|
255
|
+
for (const part of msg.parts) {
|
|
256
|
+
const stepLatest = accumulateV1StepPart(part, inTail, turn, cum);
|
|
257
|
+
if (stepLatest)
|
|
258
|
+
latest = stepLatest;
|
|
259
|
+
}
|
|
260
|
+
// First user text after last user message is displayTitle
|
|
261
|
+
if (inTail && !tailDisplayTitle && msg.info.role === 'user') {
|
|
262
|
+
tailDisplayTitle = firstV1TextPart(msg.parts);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { lastUserIndex, tailDisplayTitle, latest, turn, cum };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* v1 catch-up tail 装配(纯函数):把 [startIndex, 末尾) 的消息映射为展示
|
|
269
|
+
* 事件(text / thinking / tool_use / tool_result)。tool 在同一 part 上先发
|
|
270
|
+
* use(有 callID+input)再发 result(有 callID+output)。
|
|
271
|
+
*/
|
|
272
|
+
function assembleV1TailEvents(messages, startIndex) {
|
|
273
|
+
const events = [];
|
|
274
|
+
for (let i = startIndex; i < messages.length; i++) {
|
|
275
|
+
const msg = messages[i];
|
|
276
|
+
const timestamp = msg.info.time?.created
|
|
277
|
+
? new Date(msg.info.time.created).toISOString()
|
|
278
|
+
: undefined;
|
|
279
|
+
for (const part of msg.parts) {
|
|
280
|
+
if (part.type === 'text') {
|
|
281
|
+
events.push({
|
|
282
|
+
type: 'text',
|
|
283
|
+
content: part.text ?? '',
|
|
284
|
+
timestamp,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
else if (part.type === 'reasoning') {
|
|
288
|
+
events.push({
|
|
289
|
+
type: 'thinking',
|
|
290
|
+
content: part.text ?? '',
|
|
291
|
+
timestamp,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
else if (part.type === 'tool') {
|
|
295
|
+
// Tool use
|
|
296
|
+
if (part.callID && part.state?.input) {
|
|
297
|
+
events.push({
|
|
298
|
+
type: 'tool_use',
|
|
299
|
+
content: JSON.stringify({
|
|
300
|
+
id: part.callID,
|
|
301
|
+
name: part.tool,
|
|
302
|
+
input: part.state.input,
|
|
303
|
+
}),
|
|
304
|
+
timestamp,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
// Tool result (completed or error)
|
|
308
|
+
if (part.callID && part.state?.output) {
|
|
309
|
+
const isError = part.state.status === 'error';
|
|
310
|
+
const rawOut = isError
|
|
311
|
+
? (part.state.error ?? part.state.output)
|
|
312
|
+
: (part.state.output ?? '');
|
|
313
|
+
// pre-fold pathological tool_results (e.g. a 500KB file
|
|
314
|
+
// listing) via the shared truncateUtf8 primitive, reusing the
|
|
315
|
+
// same …(已截断) suffix as the live renderer / card budget.
|
|
316
|
+
events.push({
|
|
317
|
+
type: 'tool_result',
|
|
318
|
+
content: truncateUtf8(rawOut, TOOL_RESULT_MAX_BYTES),
|
|
319
|
+
timestamp,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return events;
|
|
326
|
+
}
|
|
327
|
+
/** 全会话首条 user 文本(displayTitle 兜底;纯函数)。 */
|
|
328
|
+
function firstV1UserText(messages) {
|
|
329
|
+
for (const msg of messages) {
|
|
330
|
+
if (msg.info.role === 'user') {
|
|
331
|
+
for (const part of msg.parts) {
|
|
332
|
+
if (part.type === 'text' && part.text) {
|
|
333
|
+
return part.text;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* usage 装配(v1/v2 共用,同 readSessionContent 内的既有共享粒度):
|
|
342
|
+
* flow 覆盖末条采样值(contextLength 保留末条——水位不求和),再落会话累计。
|
|
343
|
+
*/
|
|
344
|
+
function finalizeTurnUsage(latest, turn, cum) {
|
|
345
|
+
if (latest) {
|
|
346
|
+
latest.inputTokens = turn.input;
|
|
347
|
+
latest.outputTokens = turn.output;
|
|
348
|
+
latest.cacheReadTokens = turn.cacheRead;
|
|
349
|
+
latest.cacheCreationTokens = turn.cacheCreation;
|
|
350
|
+
latest.totalTokens = turn.total;
|
|
351
|
+
}
|
|
352
|
+
return withCumulativeUsage(latest, {
|
|
353
|
+
total: cum.total,
|
|
354
|
+
input: cum.input,
|
|
355
|
+
output: cum.output,
|
|
356
|
+
cacheRead: cum.cacheRead,
|
|
357
|
+
cacheWrite: cum.cacheCreation,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* v2 扫描累积器(纯函数):usageSample 已滤掉零 total 的退化消息;turn 边界
|
|
362
|
+
* 由 lastTurnStartIndex 判定(idle / user / synthetic / system,见其注释)。
|
|
363
|
+
*
|
|
364
|
+
* 产生:OpencodeV2UsageScan。依赖:入参 messages。不改任何外部状态。
|
|
365
|
+
*/
|
|
366
|
+
export function scanV2Usage(messages) {
|
|
367
|
+
const turn = emptyFlow();
|
|
368
|
+
const cum = emptyFlow();
|
|
369
|
+
let latest;
|
|
370
|
+
// 本 turn(= 本 run)的 flow:末个 turn 内每次模型请求之和。contextLength
|
|
371
|
+
// 仍取末次请求的窗口水位(水位不可求和)。只取末条会把多步 run 低报
|
|
372
|
+
// (实测一条 25 请求的 run:input 20,040 显示成 543、output 10,358 显示成 1,250)。
|
|
373
|
+
const turnStart = lastTurnStartIndex(messages);
|
|
374
|
+
for (let i = 0; i < messages.length; i++) {
|
|
375
|
+
const sample = usageSample(messages[i]);
|
|
376
|
+
if (!sample)
|
|
377
|
+
continue;
|
|
378
|
+
cum.input += sample.input;
|
|
379
|
+
cum.output += sample.output;
|
|
380
|
+
cum.total += sample.total;
|
|
381
|
+
cum.cacheRead += sample.cacheRead;
|
|
382
|
+
cum.cacheCreation += sample.cacheCreation;
|
|
383
|
+
if (i > turnStart) {
|
|
384
|
+
turn.input += sample.input;
|
|
385
|
+
turn.output += sample.output;
|
|
386
|
+
turn.cacheRead += sample.cacheRead;
|
|
387
|
+
turn.cacheCreation += sample.cacheCreation;
|
|
388
|
+
turn.total += sample.total;
|
|
389
|
+
}
|
|
390
|
+
latest = {
|
|
391
|
+
inputTokens: sample.input,
|
|
392
|
+
outputTokens: sample.output,
|
|
393
|
+
// §9.21 口径:含末轮 output(与 postTokens 水位同语义)。
|
|
394
|
+
contextLength: sample.input + sample.output + sample.cacheRead + sample.cacheCreation,
|
|
395
|
+
cacheReadTokens: sample.cacheRead,
|
|
396
|
+
cacheCreationTokens: sample.cacheCreation,
|
|
397
|
+
totalTokens: sample.total,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return { latest, turn, cum };
|
|
401
|
+
}
|
|
402
|
+
function v2UserHandler(msg, timestamp, state) {
|
|
403
|
+
if (typeof msg.text === 'string' && msg.text.length > 0) {
|
|
404
|
+
if (!state.displayTitle)
|
|
405
|
+
state.displayTitle = msg.text;
|
|
406
|
+
state.events.push({ type: 'text', content: msg.text, timestamp });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function pushV2ToolPart(part, timestamp, state) {
|
|
410
|
+
const toolId = part.id;
|
|
411
|
+
const toolName = part.name ?? '';
|
|
412
|
+
const input = parseToolInput(part.state?.input);
|
|
413
|
+
if (toolId) {
|
|
414
|
+
state.events.push({
|
|
415
|
+
type: 'tool_use',
|
|
416
|
+
content: JSON.stringify({ id: toolId, name: toolName, input }),
|
|
417
|
+
timestamp,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
const output = v2ToolOutput(part.state);
|
|
421
|
+
if (toolId && output !== undefined) {
|
|
422
|
+
state.events.push({
|
|
423
|
+
type: 'tool_result',
|
|
424
|
+
content: truncateUtf8(output, TOOL_RESULT_MAX_BYTES),
|
|
425
|
+
timestamp,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function v2AssistantHandler(msg, timestamp, state) {
|
|
430
|
+
for (const part of Array.isArray(msg.content) ? msg.content : []) {
|
|
431
|
+
if (part?.type === 'text' && typeof part.text === 'string') {
|
|
432
|
+
state.events.push({ type: 'text', content: part.text, timestamp });
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (part?.type === 'reasoning' && typeof part.text === 'string') {
|
|
436
|
+
state.events.push({ type: 'thinking', content: part.text, timestamp });
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (part?.type === 'tool')
|
|
440
|
+
pushV2ToolPart(part, timestamp, state);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/** User-invoked shell messages are a first-class message type in v2. */
|
|
444
|
+
function v2ShellHandler(msg, timestamp, state) {
|
|
445
|
+
if (typeof msg.command !== 'string')
|
|
446
|
+
return;
|
|
447
|
+
state.events.push({
|
|
448
|
+
type: 'tool_use',
|
|
449
|
+
content: JSON.stringify({
|
|
450
|
+
id: msg.callID ?? msg.id ?? 'shell',
|
|
451
|
+
name: 'bash',
|
|
452
|
+
input: { command: msg.command },
|
|
453
|
+
}),
|
|
454
|
+
timestamp,
|
|
455
|
+
});
|
|
456
|
+
if (typeof msg.output === 'string') {
|
|
457
|
+
state.events.push({
|
|
458
|
+
type: 'tool_result',
|
|
459
|
+
content: truncateUtf8(msg.output, TOOL_RESULT_MAX_BYTES),
|
|
460
|
+
timestamp,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/** 只保留 compact summary;system/synthetic/切换类控制消息是噪声。 */
|
|
465
|
+
function v2CompactionHandler(msg, timestamp, state) {
|
|
466
|
+
if (typeof msg.summary === 'string' && msg.summary) {
|
|
467
|
+
state.events.push({ type: 'text', content: msg.summary, timestamp });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* 消息类型 → 命名 handler。用 Map 而非对象字面量 / 长串 if:
|
|
472
|
+
* `constructor` / `toString` 这类外来 type 会命中 Object.prototype。
|
|
473
|
+
*/
|
|
474
|
+
const V2_TAIL_HANDLERS = new Map([
|
|
475
|
+
['user', v2UserHandler],
|
|
476
|
+
['assistant', v2AssistantHandler],
|
|
477
|
+
['shell', v2ShellHandler],
|
|
478
|
+
['compaction', v2CompactionHandler],
|
|
479
|
+
]);
|
|
480
|
+
/** 全会话首条 user 文本(tail 无 user 文本时的 displayTitle 兜底)。 */
|
|
481
|
+
function firstV2UserText(messages) {
|
|
482
|
+
for (const msg of messages) {
|
|
483
|
+
if (msg?.type === 'user' && typeof msg.text === 'string' && msg.text.length > 0) {
|
|
484
|
+
return msg.text;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
function assembleV2TailEvents(messages) {
|
|
490
|
+
const state = { events: [] };
|
|
491
|
+
let lastUserIndex = -1;
|
|
492
|
+
for (let i = 0; i < messages.length; i++) {
|
|
493
|
+
if (messages[i]?.type === 'user')
|
|
494
|
+
lastUserIndex = i;
|
|
495
|
+
}
|
|
496
|
+
const startIndex = lastUserIndex >= 0 ? lastUserIndex : 0;
|
|
497
|
+
for (let i = startIndex; i < messages.length; i++) {
|
|
498
|
+
const msg = messages[i];
|
|
499
|
+
if (!msg || typeof msg.type !== 'string')
|
|
500
|
+
continue;
|
|
501
|
+
const timestamp = msg.time?.created ? new Date(msg.time.created).toISOString() : undefined;
|
|
502
|
+
const handler = V2_TAIL_HANDLERS.get(msg.type);
|
|
503
|
+
if (handler)
|
|
504
|
+
handler(msg, timestamp, state);
|
|
505
|
+
}
|
|
506
|
+
// If the last user message had no text (e.g. attachment-only), fall back to
|
|
507
|
+
// the first human text anywhere in the session for the card title.
|
|
508
|
+
if (!state.displayTitle)
|
|
509
|
+
state.displayTitle = firstV2UserText(messages);
|
|
510
|
+
return { events: state.events, displayTitle: state.displayTitle };
|
|
511
|
+
}
|
|
168
512
|
export class OpencodeSessionReader {
|
|
169
513
|
binary;
|
|
170
514
|
cacheTtlMs;
|
|
@@ -214,6 +558,9 @@ export class OpencodeSessionReader {
|
|
|
214
558
|
}
|
|
215
559
|
/**
|
|
216
560
|
* Read session content: extract catch-up tail (last user message + everything after).
|
|
561
|
+
*
|
|
562
|
+
* 编排:定位会话文件(captureExport + parse)→ directory 精确匹配(与
|
|
563
|
+
* cwd-scoped list 同口径)→ 按 export 版本分发到 v1/v2 装配。
|
|
217
564
|
*/
|
|
218
565
|
readSessionContent(sessionId, cwd, opts) {
|
|
219
566
|
const realCwd = this.realpath(cwd);
|
|
@@ -251,342 +598,54 @@ export class OpencodeSessionReader {
|
|
|
251
598
|
if (isV2Export(data)) {
|
|
252
599
|
return this.readV2SessionContent(data, opts);
|
|
253
600
|
}
|
|
254
|
-
|
|
255
|
-
let usage;
|
|
256
|
-
/** 本 turn 的 flow 累计:末条 user 消息之后的全部 step-finish。 */
|
|
257
|
-
let turn = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, total: 0 };
|
|
258
|
-
let aiTitle;
|
|
259
|
-
let displayTitle;
|
|
260
|
-
// Find the last user message position (for catch-up tail).
|
|
261
|
-
// messages are already parsed objects — scan directly.
|
|
262
|
-
let lastUserIndex = -1;
|
|
263
|
-
for (let i = 0; i < data.messages.length; i++) {
|
|
264
|
-
if (data.messages[i].info.role === 'user') {
|
|
265
|
-
lastUserIndex = i;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
// Process messages from last user message onwards (catch-up tail)
|
|
269
|
-
const startIndex = lastUserIndex >= 0 ? lastUserIndex : 0;
|
|
270
|
-
for (let i = startIndex; i < data.messages.length; i++) {
|
|
271
|
-
const msg = data.messages[i];
|
|
272
|
-
const timestamp = msg.info.time?.created
|
|
273
|
-
? new Date(msg.info.time.created).toISOString()
|
|
274
|
-
: undefined;
|
|
275
|
-
for (const part of msg.parts) {
|
|
276
|
-
if (part.type === 'text') {
|
|
277
|
-
// First user text after last user message is displayTitle
|
|
278
|
-
if (msg.info.role === 'user' && part.text && !displayTitle) {
|
|
279
|
-
displayTitle = part.text;
|
|
280
|
-
}
|
|
281
|
-
events.push({
|
|
282
|
-
type: 'text',
|
|
283
|
-
content: part.text ?? '',
|
|
284
|
-
timestamp,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
else if (part.type === 'reasoning') {
|
|
288
|
-
events.push({
|
|
289
|
-
type: 'thinking',
|
|
290
|
-
content: part.text ?? '',
|
|
291
|
-
timestamp,
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
else if (part.type === 'tool') {
|
|
295
|
-
// Tool use
|
|
296
|
-
if (part.callID && part.state?.input) {
|
|
297
|
-
events.push({
|
|
298
|
-
type: 'tool_use',
|
|
299
|
-
content: JSON.stringify({
|
|
300
|
-
id: part.callID,
|
|
301
|
-
name: part.tool,
|
|
302
|
-
input: part.state.input,
|
|
303
|
-
}),
|
|
304
|
-
timestamp,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
// Tool result (completed or error)
|
|
308
|
-
if (part.callID && part.state?.output) {
|
|
309
|
-
const isError = part.state.status === 'error';
|
|
310
|
-
const rawOut = isError
|
|
311
|
-
? (part.state.error ?? part.state.output)
|
|
312
|
-
: (part.state.output ?? '');
|
|
313
|
-
// pre-fold pathological tool_results (e.g. a 500KB file
|
|
314
|
-
// listing) via the shared truncateUtf8 primitive, reusing the
|
|
315
|
-
// same …(已截断) suffix as the live renderer / card budget.
|
|
316
|
-
events.push({
|
|
317
|
-
type: 'tool_result',
|
|
318
|
-
content: truncateUtf8(rawOut, TOOL_RESULT_MAX_BYTES),
|
|
319
|
-
timestamp,
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
// Track usage from step-finish parts. v1 没有 idle 标记,turn 边界就是
|
|
325
|
-
// 上面那段"最后一条 user 消息";同一 turn 可能有多个 step-finish
|
|
326
|
-
// (每次模型请求一条),flow 要全加起来,只留末条会把多步 run 低报。
|
|
327
|
-
for (const part of msg.parts) {
|
|
328
|
-
if (part.type === 'step-finish' && part.tokens) {
|
|
329
|
-
// Skip zero-total steps(模型在 reasoning 中被截断)——它们没有消耗,
|
|
330
|
-
// 也不该把本次请求当成一条真实采样。
|
|
331
|
-
// ccusage-aligned: cache.write -> cacheCreationTokens.
|
|
332
|
-
// contextLength = input + output + cacheRead + cacheCreation —
|
|
333
|
-
// the unified context-window occupancy contract across all readers
|
|
334
|
-
// (§9.21:末轮 output 已进入历史、下一 prompt 全量携带,与 compact
|
|
335
|
-
// postTokens 水位同语义).
|
|
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
|
-
};
|
|
348
|
-
usage = {
|
|
349
|
-
inputTokens: input,
|
|
350
|
-
outputTokens: output,
|
|
351
|
-
contextLength: input + output + cacheRead + cacheWrite,
|
|
352
|
-
cacheReadTokens: part.tokens.cache?.read,
|
|
353
|
-
cacheCreationTokens: part.tokens.cache?.write,
|
|
354
|
-
totalTokens: part.tokens.total,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
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
|
-
}
|
|
368
|
-
// Cumulative (session-wide): sum input/output/total across ALL step-finish
|
|
369
|
-
// parts in the whole session. The catch-up loop above only scans from
|
|
370
|
-
// the last user message (= last run) for per-turn usage; cumulative
|
|
371
|
-
// needs a full scan so multi-run sessions report the true total.
|
|
372
|
-
let cumInput = 0;
|
|
373
|
-
let cumOutput = 0;
|
|
374
|
-
let cumTotal = 0;
|
|
375
|
-
let cumCacheRead = 0;
|
|
376
|
-
let cumCacheWrite = 0;
|
|
377
|
-
for (const msg of data.messages) {
|
|
378
|
-
for (const part of msg.parts) {
|
|
379
|
-
if (part.type === 'step-finish' && part.tokens) {
|
|
380
|
-
cumInput += part.tokens.input ?? 0;
|
|
381
|
-
cumOutput += part.tokens.output ?? 0;
|
|
382
|
-
cumTotal += part.tokens.total ?? 0;
|
|
383
|
-
cumCacheRead += part.tokens.cache?.read ?? 0;
|
|
384
|
-
cumCacheWrite += part.tokens.cache?.write ?? 0;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
usage = withCumulativeUsage(usage, {
|
|
389
|
-
total: cumTotal,
|
|
390
|
-
input: cumInput,
|
|
391
|
-
output: cumOutput,
|
|
392
|
-
cacheRead: cumCacheRead,
|
|
393
|
-
cacheWrite: cumCacheWrite,
|
|
394
|
-
});
|
|
395
|
-
// Use info.title as aiTitle
|
|
396
|
-
if (data.info.title) {
|
|
397
|
-
aiTitle = data.info.title;
|
|
398
|
-
}
|
|
399
|
-
// If no displayTitle yet, use first user text as fallback
|
|
400
|
-
if (!displayTitle) {
|
|
401
|
-
for (const msg of data.messages) {
|
|
402
|
-
if (msg.info.role === 'user') {
|
|
403
|
-
for (const part of msg.parts) {
|
|
404
|
-
if (part.type === 'text' && part.text) {
|
|
405
|
-
displayTitle = part.text;
|
|
406
|
-
break;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
if (displayTitle)
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
// Apply maxEvents cap: keep the LAST N events (most recent), matching
|
|
415
|
-
// CodexSessionReader's slice(-maxEvents). maxEvents limits the catch-up
|
|
416
|
-
// tail so auto-resume cards don't load the entire session.
|
|
417
|
-
const cappedEvents = capEvents(events, opts?.maxEvents);
|
|
418
|
-
return {
|
|
419
|
-
events: cappedEvents,
|
|
420
|
-
usage,
|
|
421
|
-
// opencode run doesn't have background tasks
|
|
422
|
-
aiTitle,
|
|
423
|
-
displayTitle,
|
|
424
|
-
};
|
|
601
|
+
return this.readV1SessionContent(data, opts);
|
|
425
602
|
}
|
|
426
603
|
catch (err) {
|
|
427
604
|
getLogger().warn(`[opencode-session-reader] readSessionContent build failed: ${err}`);
|
|
428
605
|
return { events: [] };
|
|
429
606
|
}
|
|
430
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* v1 export(`{info, parts}` 信封 + step-finish 用量 part)的 catch-up tail
|
|
610
|
+
* 装配:扫描累积器(scanV1Messages)出 tail 位置与本 run/累计 usage,事件
|
|
611
|
+
* 装配(assembleV1TailEvents)出展示流,这里只做组装。
|
|
612
|
+
*/
|
|
613
|
+
readV1SessionContent(data, opts) {
|
|
614
|
+
const scan = scanV1Messages(data.messages);
|
|
615
|
+
const startIndex = scan.lastUserIndex >= 0 ? scan.lastUserIndex : 0;
|
|
616
|
+
return {
|
|
617
|
+
// Apply maxEvents cap: keep the LAST N events (most recent), matching
|
|
618
|
+
// CodexSessionReader's slice(-maxEvents). maxEvents limits the catch-up
|
|
619
|
+
// tail so auto-resume cards don't load the entire session.
|
|
620
|
+
events: capEvents(assembleV1TailEvents(data.messages, startIndex), opts?.maxEvents),
|
|
621
|
+
usage: finalizeTurnUsage(scan.latest, scan.turn, scan.cum),
|
|
622
|
+
// opencode run doesn't have background tasks
|
|
623
|
+
aiTitle: data.info.title || undefined,
|
|
624
|
+
// tail 无 user 文本时回退全会话首条 user 文本
|
|
625
|
+
displayTitle: scan.tailDisplayTitle ?? firstV1UserText(data.messages),
|
|
626
|
+
};
|
|
627
|
+
}
|
|
431
628
|
/**
|
|
432
629
|
* v2 export: messages are flat (`type` + `text`/`content`) and assistant
|
|
433
630
|
* usage lives on the message instead of a `step-finish` part. Keep this
|
|
434
631
|
* mapping isolated from the v1 parser so v1 behaviour stays unchanged.
|
|
632
|
+
*
|
|
633
|
+
* turnStart 可能落在末条采样之后(新 turn 尚无采样)→ turn 全 0;latest
|
|
634
|
+
* 仍是上一轮末条采样,contextLength 保留水位。turnStart 小于最后一个采样
|
|
635
|
+
* 下标时,flow 覆盖末条值,只有 contextLength 保留末条(finalizeTurnUsage)。
|
|
435
636
|
*/
|
|
436
637
|
readV2SessionContent(data, opts) {
|
|
437
638
|
const messages = Array.isArray(data.messages) ? data.messages : [];
|
|
438
|
-
const events = [];
|
|
439
639
|
const aiTitle = typeof data.info.title === 'string' && data.info.title.length > 0
|
|
440
640
|
? data.info.title
|
|
441
641
|
: undefined;
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
for (let i = 0; i < messages.length; i++) {
|
|
445
|
-
if (messages[i]?.type === 'user')
|
|
446
|
-
lastUserIndex = i;
|
|
447
|
-
}
|
|
448
|
-
const startIndex = lastUserIndex >= 0 ? lastUserIndex : 0;
|
|
449
|
-
for (let i = startIndex; i < messages.length; i++) {
|
|
450
|
-
const msg = messages[i];
|
|
451
|
-
if (!msg || typeof msg.type !== 'string')
|
|
452
|
-
continue;
|
|
453
|
-
const timestamp = msg.time?.created ? new Date(msg.time.created).toISOString() : undefined;
|
|
454
|
-
if (msg.type === 'user') {
|
|
455
|
-
if (typeof msg.text === 'string' && msg.text.length > 0) {
|
|
456
|
-
if (!displayTitle)
|
|
457
|
-
displayTitle = msg.text;
|
|
458
|
-
events.push({ type: 'text', content: msg.text, timestamp });
|
|
459
|
-
}
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
-
if (msg.type === 'assistant') {
|
|
463
|
-
for (const part of Array.isArray(msg.content) ? msg.content : []) {
|
|
464
|
-
if (part?.type === 'text' && typeof part.text === 'string') {
|
|
465
|
-
events.push({ type: 'text', content: part.text, timestamp });
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
if (part?.type === 'reasoning' && typeof part.text === 'string') {
|
|
469
|
-
events.push({ type: 'thinking', content: part.text, timestamp });
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
|
-
if (part?.type === 'tool') {
|
|
473
|
-
const toolId = part.id;
|
|
474
|
-
const toolName = part.name ?? '';
|
|
475
|
-
const input = parseToolInput(part.state?.input);
|
|
476
|
-
if (toolId) {
|
|
477
|
-
events.push({
|
|
478
|
-
type: 'tool_use',
|
|
479
|
-
content: JSON.stringify({ id: toolId, name: toolName, input }),
|
|
480
|
-
timestamp,
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
const output = v2ToolOutput(part.state);
|
|
484
|
-
if (toolId && output !== undefined) {
|
|
485
|
-
events.push({
|
|
486
|
-
type: 'tool_result',
|
|
487
|
-
content: truncateUtf8(output, TOOL_RESULT_MAX_BYTES),
|
|
488
|
-
timestamp,
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
// User-invoked shell messages are a first-class message type in v2.
|
|
496
|
-
if (msg.type === 'shell' && typeof msg.command === 'string') {
|
|
497
|
-
events.push({
|
|
498
|
-
type: 'tool_use',
|
|
499
|
-
content: JSON.stringify({
|
|
500
|
-
id: msg.callID ?? msg.id ?? 'shell',
|
|
501
|
-
name: 'bash',
|
|
502
|
-
input: { command: msg.command },
|
|
503
|
-
}),
|
|
504
|
-
timestamp,
|
|
505
|
-
});
|
|
506
|
-
if (typeof msg.output === 'string') {
|
|
507
|
-
events.push({
|
|
508
|
-
type: 'tool_result',
|
|
509
|
-
content: truncateUtf8(msg.output, TOOL_RESULT_MAX_BYTES),
|
|
510
|
-
timestamp,
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
// Keep the compact summary visible in the catch-up tail; other control
|
|
516
|
-
// messages (system/synthetic/agent-switched/model-switched) are noise.
|
|
517
|
-
if (msg.type === 'compaction' && typeof msg.summary === 'string' && msg.summary) {
|
|
518
|
-
events.push({ type: 'text', content: msg.summary, timestamp });
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
// If the last user message had no text (e.g. attachment-only), fall back to
|
|
522
|
-
// the first human text anywhere in the session for the card title.
|
|
523
|
-
if (!displayTitle) {
|
|
524
|
-
for (const msg of messages) {
|
|
525
|
-
if (msg?.type === 'user' && typeof msg.text === 'string' && msg.text.length > 0) {
|
|
526
|
-
displayTitle = msg.text;
|
|
527
|
-
break;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
let latest;
|
|
532
|
-
let cumInput = 0;
|
|
533
|
-
let cumOutput = 0;
|
|
534
|
-
let cumTotal = 0;
|
|
535
|
-
let cumCacheRead = 0;
|
|
536
|
-
let cumCacheWrite = 0;
|
|
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)
|
|
545
|
-
continue;
|
|
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;
|
|
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;
|
|
577
|
-
}
|
|
578
|
-
latest = withCumulativeUsage(latest, {
|
|
579
|
-
total: cumTotal,
|
|
580
|
-
input: cumInput,
|
|
581
|
-
output: cumOutput,
|
|
582
|
-
cacheRead: cumCacheRead,
|
|
583
|
-
cacheWrite: cumCacheWrite,
|
|
584
|
-
});
|
|
642
|
+
const tail = assembleV2TailEvents(messages);
|
|
643
|
+
const usage = scanV2Usage(messages);
|
|
585
644
|
return {
|
|
586
|
-
events: capEvents(events, opts?.maxEvents),
|
|
587
|
-
usage: latest,
|
|
645
|
+
events: capEvents(tail.events, opts?.maxEvents),
|
|
646
|
+
usage: finalizeTurnUsage(usage.latest, usage.turn, usage.cum),
|
|
588
647
|
aiTitle,
|
|
589
|
-
displayTitle,
|
|
648
|
+
displayTitle: tail.displayTitle,
|
|
590
649
|
};
|
|
591
650
|
}
|
|
592
651
|
/**
|
|
@@ -699,9 +758,7 @@ export class OpencodeSessionReader {
|
|
|
699
758
|
if (err.code === 'ENOENT') {
|
|
700
759
|
return [];
|
|
701
760
|
}
|
|
702
|
-
|
|
703
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
704
|
-
throw new Error(`opencode session list 读取失败: ${msg}`, { cause: err });
|
|
761
|
+
throwSessionListError('opencode', err);
|
|
705
762
|
}
|
|
706
763
|
}
|
|
707
764
|
/**
|