lark-remote 0.5.18 → 0.5.20
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-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 +1 -1
- package/dist/bridge/index.d.ts +120 -119
- package/dist/bridge/index.js +257 -695
- 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 +109 -97
- package/dist/card/bash-card-session.js +2 -0
- package/dist/card/card-budget.js +1 -1
- package/dist/card/card-helpers.d.ts +3 -3
- package/dist/card/card-helpers.js +6 -6
- package/dist/card/card-session.d.ts +17 -0
- package/dist/card/card-session.js +35 -0
- package/dist/card/final-answer-file.js +1 -1
- package/dist/card/run-card-session.js +2 -0
- package/dist/card/run-renderer.d.ts +3 -3
- package/dist/card/run-renderer.js +77 -102
- package/dist/card/run-state.d.ts +35 -8
- package/dist/card/run-state.js +117 -90
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +1 -1
- package/dist/card/usage-format.d.ts +12 -0
- package/dist/card/usage-format.js +8 -3
- package/dist/cli.js +15 -3
- package/dist/clone.js +2 -2
- 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 +203 -133
- package/dist/logger/index.js +6 -6
- package/dist/message-handlers.d.ts +0 -5
- package/dist/message-handlers.js +4 -2
- 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/resume.js +2 -20
- 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 +222 -202
- 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 +0 -5
- package/dist/runner/codex/app-server/translator.js +4 -17
- 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 +31 -9
- 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/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 +1 -1
- package/dist/runner/common/spawning-runner.js +1 -1
- 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 +13 -8
- package/dist/runner/dsh/client.js +23 -15
- 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/runner.d.ts +48 -2
- package/dist/runner/dsh/runner.js +288 -199
- package/dist/runner/dsh/translator.d.ts +1 -1
- package/dist/runner/dsh/translator.js +1 -1
- 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 +1 -2
- package/dist/runner/kimi/acp/translator.d.ts +1 -1
- package/dist/runner/kimi/acp/translator.js +3 -4
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +4 -4
- 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/types.d.ts +25 -8
- package/dist/runner/types.js +1 -1
- package/dist/session/claude/sessions.d.ts +45 -0
- package/dist/session/claude/sessions.js +97 -71
- package/dist/session/codex/rollout-reader.js +4 -4
- 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 +11 -5
- package/dist/session/kimi/sessions.d.ts +40 -1
- package/dist/session/kimi/sessions.js +244 -192
- package/dist/session/opencode/sessions.d.ts +153 -1
- package/dist/session/opencode/sessions.js +354 -321
- package/dist/session/pi/sessions.js +4 -5
- package/dist/session/session-store.d.ts +1 -1
- package/dist/session/session-store.js +1 -1
- 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,325 @@ 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
|
+
/**
|
|
173
|
+
* v1 扫描累积器(纯函数):单遍扫过全部 export 消息,产出 catch-up tail 的
|
|
174
|
+
* 位置(末条 user 消息)、本 run flow(tail 内非零 step-finish 求和)、末条
|
|
175
|
+
* 非零采样(contextLength 水位)与全会话累计。
|
|
176
|
+
*
|
|
177
|
+
* 产生:OpencodeV1Scan。依赖:入参 messages。不改任何外部状态。
|
|
178
|
+
*
|
|
179
|
+
* 口径(勿改):cumulative 对**所有** step-finish 逐字段求和、不设零 total
|
|
180
|
+
* 门槛;turn/latest 只认 tail 内非零 total 的 step(模型在 reasoning 中被
|
|
181
|
+
* 截断的退化 step 不算采样)。
|
|
182
|
+
*/
|
|
183
|
+
export function scanV1Messages(messages) {
|
|
184
|
+
// Find the last user message position (for catch-up tail).
|
|
185
|
+
// messages are already parsed objects — scan directly.
|
|
186
|
+
let lastUserIndex = -1;
|
|
187
|
+
for (let i = 0; i < messages.length; i++) {
|
|
188
|
+
if (messages[i].info.role === 'user') {
|
|
189
|
+
lastUserIndex = i;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Process messages from last user message onwards (catch-up tail)
|
|
193
|
+
const startIndex = lastUserIndex >= 0 ? lastUserIndex : 0;
|
|
194
|
+
const turn = emptyFlow();
|
|
195
|
+
const cum = emptyFlow();
|
|
196
|
+
let latest;
|
|
197
|
+
let tailDisplayTitle;
|
|
198
|
+
for (let i = 0; i < messages.length; i++) {
|
|
199
|
+
const inTail = i >= startIndex;
|
|
200
|
+
const msg = messages[i];
|
|
201
|
+
// Track usage from step-finish parts. v1 没有 idle 标记,turn 边界就是
|
|
202
|
+
// 末条 user 消息;同一 turn 可能有多个 step-finish(每次模型请求一条),
|
|
203
|
+
// flow 要全加起来,只留末条会把多步 run 低报。
|
|
204
|
+
for (const part of msg.parts) {
|
|
205
|
+
if (part.type === 'step-finish' && part.tokens) {
|
|
206
|
+
// cumulative:全会话求和,含零 total 的退化 step(字段为 0 时求和不
|
|
207
|
+
// 变,与拆分前行为一致)。
|
|
208
|
+
cum.input += part.tokens.input ?? 0;
|
|
209
|
+
cum.output += part.tokens.output ?? 0;
|
|
210
|
+
cum.total += part.tokens.total ?? 0;
|
|
211
|
+
cum.cacheRead += part.tokens.cache?.read ?? 0;
|
|
212
|
+
cum.cacheCreation += part.tokens.cache?.write ?? 0;
|
|
213
|
+
if (!inTail)
|
|
214
|
+
continue;
|
|
215
|
+
// Skip zero-total steps(模型在 reasoning 中被截断)——它们没有消耗,
|
|
216
|
+
// 也不该把本次请求当成一条真实采样。
|
|
217
|
+
// ccusage-aligned: cache.write -> cacheCreationTokens.
|
|
218
|
+
// contextLength = input + output + cacheRead + cacheCreation —
|
|
219
|
+
// the unified context-window occupancy contract across all readers
|
|
220
|
+
// (§9.21:末轮 output 已进入历史、下一 prompt 全量携带,与 compact
|
|
221
|
+
// postTokens 水位同语义).
|
|
222
|
+
if ((part.tokens.total ?? 0) === 0)
|
|
223
|
+
continue;
|
|
224
|
+
const input = part.tokens.input ?? 0;
|
|
225
|
+
const output = part.tokens.output ?? 0;
|
|
226
|
+
const cacheRead = part.tokens.cache?.read ?? 0;
|
|
227
|
+
const cacheWrite = part.tokens.cache?.write ?? 0;
|
|
228
|
+
turn.input += input;
|
|
229
|
+
turn.output += output;
|
|
230
|
+
turn.cacheRead += cacheRead;
|
|
231
|
+
turn.cacheCreation += cacheWrite;
|
|
232
|
+
turn.total += part.tokens.total;
|
|
233
|
+
latest = {
|
|
234
|
+
inputTokens: input,
|
|
235
|
+
outputTokens: output,
|
|
236
|
+
contextLength: input + output + cacheRead + cacheWrite,
|
|
237
|
+
cacheReadTokens: part.tokens.cache?.read,
|
|
238
|
+
cacheCreationTokens: part.tokens.cache?.write,
|
|
239
|
+
totalTokens: part.tokens.total,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// First user text after last user message is displayTitle
|
|
244
|
+
if (inTail && !tailDisplayTitle && msg.info.role === 'user') {
|
|
245
|
+
for (const part of msg.parts) {
|
|
246
|
+
if (part.type === 'text' && part.text) {
|
|
247
|
+
tailDisplayTitle = part.text;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { lastUserIndex, tailDisplayTitle, latest, turn, cum };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* v1 catch-up tail 装配(纯函数):把 [startIndex, 末尾) 的消息映射为展示
|
|
257
|
+
* 事件(text / thinking / tool_use / tool_result)。tool 在同一 part 上先发
|
|
258
|
+
* use(有 callID+input)再发 result(有 callID+output)。
|
|
259
|
+
*/
|
|
260
|
+
function assembleV1TailEvents(messages, startIndex) {
|
|
261
|
+
const events = [];
|
|
262
|
+
for (let i = startIndex; i < messages.length; i++) {
|
|
263
|
+
const msg = messages[i];
|
|
264
|
+
const timestamp = msg.info.time?.created
|
|
265
|
+
? new Date(msg.info.time.created).toISOString()
|
|
266
|
+
: undefined;
|
|
267
|
+
for (const part of msg.parts) {
|
|
268
|
+
if (part.type === 'text') {
|
|
269
|
+
events.push({
|
|
270
|
+
type: 'text',
|
|
271
|
+
content: part.text ?? '',
|
|
272
|
+
timestamp,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
else if (part.type === 'reasoning') {
|
|
276
|
+
events.push({
|
|
277
|
+
type: 'thinking',
|
|
278
|
+
content: part.text ?? '',
|
|
279
|
+
timestamp,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
else if (part.type === 'tool') {
|
|
283
|
+
// Tool use
|
|
284
|
+
if (part.callID && part.state?.input) {
|
|
285
|
+
events.push({
|
|
286
|
+
type: 'tool_use',
|
|
287
|
+
content: JSON.stringify({
|
|
288
|
+
id: part.callID,
|
|
289
|
+
name: part.tool,
|
|
290
|
+
input: part.state.input,
|
|
291
|
+
}),
|
|
292
|
+
timestamp,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
// Tool result (completed or error)
|
|
296
|
+
if (part.callID && part.state?.output) {
|
|
297
|
+
const isError = part.state.status === 'error';
|
|
298
|
+
const rawOut = isError
|
|
299
|
+
? (part.state.error ?? part.state.output)
|
|
300
|
+
: (part.state.output ?? '');
|
|
301
|
+
// pre-fold pathological tool_results (e.g. a 500KB file
|
|
302
|
+
// listing) via the shared truncateUtf8 primitive, reusing the
|
|
303
|
+
// same …(已截断) suffix as the live renderer / card budget.
|
|
304
|
+
events.push({
|
|
305
|
+
type: 'tool_result',
|
|
306
|
+
content: truncateUtf8(rawOut, TOOL_RESULT_MAX_BYTES),
|
|
307
|
+
timestamp,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return events;
|
|
314
|
+
}
|
|
315
|
+
/** 全会话首条 user 文本(displayTitle 兜底;纯函数)。 */
|
|
316
|
+
function firstV1UserText(messages) {
|
|
317
|
+
for (const msg of messages) {
|
|
318
|
+
if (msg.info.role === 'user') {
|
|
319
|
+
for (const part of msg.parts) {
|
|
320
|
+
if (part.type === 'text' && part.text) {
|
|
321
|
+
return part.text;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* usage 装配(v1/v2 共用,同 readSessionContent 内的既有共享粒度):
|
|
330
|
+
* flow 覆盖末条采样值(contextLength 保留末条——水位不求和),再落会话累计。
|
|
331
|
+
*/
|
|
332
|
+
function finalizeTurnUsage(latest, turn, cum) {
|
|
333
|
+
if (latest) {
|
|
334
|
+
latest.inputTokens = turn.input;
|
|
335
|
+
latest.outputTokens = turn.output;
|
|
336
|
+
latest.cacheReadTokens = turn.cacheRead;
|
|
337
|
+
latest.cacheCreationTokens = turn.cacheCreation;
|
|
338
|
+
latest.totalTokens = turn.total;
|
|
339
|
+
}
|
|
340
|
+
return withCumulativeUsage(latest, {
|
|
341
|
+
total: cum.total,
|
|
342
|
+
input: cum.input,
|
|
343
|
+
output: cum.output,
|
|
344
|
+
cacheRead: cum.cacheRead,
|
|
345
|
+
cacheWrite: cum.cacheCreation,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* v2 扫描累积器(纯函数):usageSample 已滤掉零 total 的退化消息;turn 边界
|
|
350
|
+
* 由 lastTurnStartIndex 判定(idle / user / synthetic / system,见其注释)。
|
|
351
|
+
*
|
|
352
|
+
* 产生:OpencodeV2UsageScan。依赖:入参 messages。不改任何外部状态。
|
|
353
|
+
*/
|
|
354
|
+
export function scanV2Usage(messages) {
|
|
355
|
+
const turn = emptyFlow();
|
|
356
|
+
const cum = emptyFlow();
|
|
357
|
+
let latest;
|
|
358
|
+
// 本 turn(= 本 run)的 flow:末个 turn 内每次模型请求之和。contextLength
|
|
359
|
+
// 仍取末次请求的窗口水位(水位不可求和)。只取末条会把多步 run 低报
|
|
360
|
+
// (实测一条 25 请求的 run:input 20,040 显示成 543、output 10,358 显示成 1,250)。
|
|
361
|
+
const turnStart = lastTurnStartIndex(messages);
|
|
362
|
+
for (let i = 0; i < messages.length; i++) {
|
|
363
|
+
const sample = usageSample(messages[i]);
|
|
364
|
+
if (!sample)
|
|
365
|
+
continue;
|
|
366
|
+
cum.input += sample.input;
|
|
367
|
+
cum.output += sample.output;
|
|
368
|
+
cum.total += sample.total;
|
|
369
|
+
cum.cacheRead += sample.cacheRead;
|
|
370
|
+
cum.cacheCreation += sample.cacheCreation;
|
|
371
|
+
if (i > turnStart) {
|
|
372
|
+
turn.input += sample.input;
|
|
373
|
+
turn.output += sample.output;
|
|
374
|
+
turn.cacheRead += sample.cacheRead;
|
|
375
|
+
turn.cacheCreation += sample.cacheCreation;
|
|
376
|
+
turn.total += sample.total;
|
|
377
|
+
}
|
|
378
|
+
latest = {
|
|
379
|
+
inputTokens: sample.input,
|
|
380
|
+
outputTokens: sample.output,
|
|
381
|
+
// §9.21 口径:含末轮 output(与 postTokens 水位同语义)。
|
|
382
|
+
contextLength: sample.input + sample.output + sample.cacheRead + sample.cacheCreation,
|
|
383
|
+
cacheReadTokens: sample.cacheRead,
|
|
384
|
+
cacheCreationTokens: sample.cacheCreation,
|
|
385
|
+
totalTokens: sample.total,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
return { latest, turn, cum };
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* v2 catch-up tail 装配(纯函数):末条 user 消息起的消息映射为展示事件
|
|
392
|
+
* (user / assistant / shell / compaction);displayTitle 取 tail 内首条
|
|
393
|
+
* user 文本,tail 无 user 文本(如附件-only)时回退全会话首条。
|
|
394
|
+
*/
|
|
395
|
+
function assembleV2TailEvents(messages) {
|
|
396
|
+
const events = [];
|
|
397
|
+
let displayTitle;
|
|
398
|
+
let lastUserIndex = -1;
|
|
399
|
+
for (let i = 0; i < messages.length; i++) {
|
|
400
|
+
if (messages[i]?.type === 'user')
|
|
401
|
+
lastUserIndex = i;
|
|
402
|
+
}
|
|
403
|
+
const startIndex = lastUserIndex >= 0 ? lastUserIndex : 0;
|
|
404
|
+
for (let i = startIndex; i < messages.length; i++) {
|
|
405
|
+
const msg = messages[i];
|
|
406
|
+
if (!msg || typeof msg.type !== 'string')
|
|
407
|
+
continue;
|
|
408
|
+
const timestamp = msg.time?.created ? new Date(msg.time.created).toISOString() : undefined;
|
|
409
|
+
if (msg.type === 'user') {
|
|
410
|
+
if (typeof msg.text === 'string' && msg.text.length > 0) {
|
|
411
|
+
if (!displayTitle)
|
|
412
|
+
displayTitle = msg.text;
|
|
413
|
+
events.push({ type: 'text', content: msg.text, timestamp });
|
|
414
|
+
}
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (msg.type === 'assistant') {
|
|
418
|
+
for (const part of Array.isArray(msg.content) ? msg.content : []) {
|
|
419
|
+
if (part?.type === 'text' && typeof part.text === 'string') {
|
|
420
|
+
events.push({ type: 'text', content: part.text, timestamp });
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (part?.type === 'reasoning' && typeof part.text === 'string') {
|
|
424
|
+
events.push({ type: 'thinking', content: part.text, timestamp });
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (part?.type === 'tool') {
|
|
428
|
+
const toolId = part.id;
|
|
429
|
+
const toolName = part.name ?? '';
|
|
430
|
+
const input = parseToolInput(part.state?.input);
|
|
431
|
+
if (toolId) {
|
|
432
|
+
events.push({
|
|
433
|
+
type: 'tool_use',
|
|
434
|
+
content: JSON.stringify({ id: toolId, name: toolName, input }),
|
|
435
|
+
timestamp,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
const output = v2ToolOutput(part.state);
|
|
439
|
+
if (toolId && output !== undefined) {
|
|
440
|
+
events.push({
|
|
441
|
+
type: 'tool_result',
|
|
442
|
+
content: truncateUtf8(output, TOOL_RESULT_MAX_BYTES),
|
|
443
|
+
timestamp,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
// User-invoked shell messages are a first-class message type in v2.
|
|
451
|
+
if (msg.type === 'shell' && typeof msg.command === 'string') {
|
|
452
|
+
events.push({
|
|
453
|
+
type: 'tool_use',
|
|
454
|
+
content: JSON.stringify({
|
|
455
|
+
id: msg.callID ?? msg.id ?? 'shell',
|
|
456
|
+
name: 'bash',
|
|
457
|
+
input: { command: msg.command },
|
|
458
|
+
}),
|
|
459
|
+
timestamp,
|
|
460
|
+
});
|
|
461
|
+
if (typeof msg.output === 'string') {
|
|
462
|
+
events.push({
|
|
463
|
+
type: 'tool_result',
|
|
464
|
+
content: truncateUtf8(msg.output, TOOL_RESULT_MAX_BYTES),
|
|
465
|
+
timestamp,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
// Keep the compact summary visible in the catch-up tail; other control
|
|
471
|
+
// messages (system/synthetic/agent-switched/model-switched) are noise.
|
|
472
|
+
if (msg.type === 'compaction' && typeof msg.summary === 'string' && msg.summary) {
|
|
473
|
+
events.push({ type: 'text', content: msg.summary, timestamp });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// If the last user message had no text (e.g. attachment-only), fall back to
|
|
477
|
+
// the first human text anywhere in the session for the card title.
|
|
478
|
+
if (!displayTitle) {
|
|
479
|
+
for (const msg of messages) {
|
|
480
|
+
if (msg?.type === 'user' && typeof msg.text === 'string' && msg.text.length > 0) {
|
|
481
|
+
displayTitle = msg.text;
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return { events, displayTitle };
|
|
487
|
+
}
|
|
168
488
|
export class OpencodeSessionReader {
|
|
169
489
|
binary;
|
|
170
490
|
cacheTtlMs;
|
|
@@ -214,6 +534,9 @@ export class OpencodeSessionReader {
|
|
|
214
534
|
}
|
|
215
535
|
/**
|
|
216
536
|
* Read session content: extract catch-up tail (last user message + everything after).
|
|
537
|
+
*
|
|
538
|
+
* 编排:定位会话文件(captureExport + parse)→ directory 精确匹配(与
|
|
539
|
+
* cwd-scoped list 同口径)→ 按 export 版本分发到 v1/v2 装配。
|
|
217
540
|
*/
|
|
218
541
|
readSessionContent(sessionId, cwd, opts) {
|
|
219
542
|
const realCwd = this.realpath(cwd);
|
|
@@ -251,342 +574,54 @@ export class OpencodeSessionReader {
|
|
|
251
574
|
if (isV2Export(data)) {
|
|
252
575
|
return this.readV2SessionContent(data, opts);
|
|
253
576
|
}
|
|
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
|
-
};
|
|
577
|
+
return this.readV1SessionContent(data, opts);
|
|
425
578
|
}
|
|
426
579
|
catch (err) {
|
|
427
580
|
getLogger().warn(`[opencode-session-reader] readSessionContent build failed: ${err}`);
|
|
428
581
|
return { events: [] };
|
|
429
582
|
}
|
|
430
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* v1 export(`{info, parts}` 信封 + step-finish 用量 part)的 catch-up tail
|
|
586
|
+
* 装配:扫描累积器(scanV1Messages)出 tail 位置与本 run/累计 usage,事件
|
|
587
|
+
* 装配(assembleV1TailEvents)出展示流,这里只做组装。
|
|
588
|
+
*/
|
|
589
|
+
readV1SessionContent(data, opts) {
|
|
590
|
+
const scan = scanV1Messages(data.messages);
|
|
591
|
+
const startIndex = scan.lastUserIndex >= 0 ? scan.lastUserIndex : 0;
|
|
592
|
+
return {
|
|
593
|
+
// Apply maxEvents cap: keep the LAST N events (most recent), matching
|
|
594
|
+
// CodexSessionReader's slice(-maxEvents). maxEvents limits the catch-up
|
|
595
|
+
// tail so auto-resume cards don't load the entire session.
|
|
596
|
+
events: capEvents(assembleV1TailEvents(data.messages, startIndex), opts?.maxEvents),
|
|
597
|
+
usage: finalizeTurnUsage(scan.latest, scan.turn, scan.cum),
|
|
598
|
+
// opencode run doesn't have background tasks
|
|
599
|
+
aiTitle: data.info.title || undefined,
|
|
600
|
+
// tail 无 user 文本时回退全会话首条 user 文本
|
|
601
|
+
displayTitle: scan.tailDisplayTitle ?? firstV1UserText(data.messages),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
431
604
|
/**
|
|
432
605
|
* v2 export: messages are flat (`type` + `text`/`content`) and assistant
|
|
433
606
|
* usage lives on the message instead of a `step-finish` part. Keep this
|
|
434
607
|
* mapping isolated from the v1 parser so v1 behaviour stays unchanged.
|
|
608
|
+
*
|
|
609
|
+
* turnStart 可能落在末条采样之后(新 turn 尚无采样)→ turn 全 0;latest
|
|
610
|
+
* 仍是上一轮末条采样,contextLength 保留水位。turnStart 小于最后一个采样
|
|
611
|
+
* 下标时,flow 覆盖末条值,只有 contextLength 保留末条(finalizeTurnUsage)。
|
|
435
612
|
*/
|
|
436
613
|
readV2SessionContent(data, opts) {
|
|
437
614
|
const messages = Array.isArray(data.messages) ? data.messages : [];
|
|
438
|
-
const events = [];
|
|
439
615
|
const aiTitle = typeof data.info.title === 'string' && data.info.title.length > 0
|
|
440
616
|
? data.info.title
|
|
441
617
|
: 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
|
-
});
|
|
618
|
+
const tail = assembleV2TailEvents(messages);
|
|
619
|
+
const usage = scanV2Usage(messages);
|
|
585
620
|
return {
|
|
586
|
-
events: capEvents(events, opts?.maxEvents),
|
|
587
|
-
usage: latest,
|
|
621
|
+
events: capEvents(tail.events, opts?.maxEvents),
|
|
622
|
+
usage: finalizeTurnUsage(usage.latest, usage.turn, usage.cum),
|
|
588
623
|
aiTitle,
|
|
589
|
-
displayTitle,
|
|
624
|
+
displayTitle: tail.displayTitle,
|
|
590
625
|
};
|
|
591
626
|
}
|
|
592
627
|
/**
|
|
@@ -699,9 +734,7 @@ export class OpencodeSessionReader {
|
|
|
699
734
|
if (err.code === 'ENOENT') {
|
|
700
735
|
return [];
|
|
701
736
|
}
|
|
702
|
-
|
|
703
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
704
|
-
throw new Error(`opencode session list 读取失败: ${msg}`, { cause: err });
|
|
737
|
+
throwSessionListError('opencode', err);
|
|
705
738
|
}
|
|
706
739
|
}
|
|
707
740
|
/**
|