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
|
@@ -2,8 +2,10 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { readJsonlLines as readJsonlLinesShared, readLastNJsonlLines } from '../common/jsonl.js';
|
|
5
|
-
import {
|
|
5
|
+
import { cumulativeUsageFields } from '../common/usage-accumulator.js';
|
|
6
|
+
import { throwSessionListError } from '../common/throw-session-list-error.js';
|
|
6
7
|
import { getLogger } from '../../logger/index.js';
|
|
8
|
+
import { logNonEnoentDebug } from '../session-store.js';
|
|
7
9
|
import { isStale } from '../common/constants.js';
|
|
8
10
|
import { safeRealpath, samePath } from '../../platform/path.js';
|
|
9
11
|
import { capEvents, paginate } from '../common/pagination.js';
|
|
@@ -134,6 +136,233 @@ export function checkCwdGuard(state, realCwd) {
|
|
|
134
136
|
return 'verified';
|
|
135
137
|
return 'failed';
|
|
136
138
|
}
|
|
139
|
+
function emptyKimiTotals() {
|
|
140
|
+
return { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* kimi 主 wire usage 扫描累积器(纯函数):单遍解析,产出会话累计、末 turn
|
|
144
|
+
* flow、上下文水位与压缩统计。
|
|
145
|
+
*
|
|
146
|
+
* 产生:KimiWireUsageScan。依赖:入参 lines(已解析前的原始行)。不改任何
|
|
147
|
+
* 外部状态;畸形行跳过,畸形 usage.record 按字段 0 计但记为「有数据」。
|
|
148
|
+
*/
|
|
149
|
+
export function scanKimiUsage(lines) {
|
|
150
|
+
const totals = emptyKimiTotals();
|
|
151
|
+
let turn;
|
|
152
|
+
// contextLength is only from the main wire's last usage.record
|
|
153
|
+
// (subagent context is an independent window).
|
|
154
|
+
let mainContextLength = 0;
|
|
155
|
+
// §6.3: Compaction stats — count compact events, track last pre-compact context.
|
|
156
|
+
let compactCount = 0;
|
|
157
|
+
let compactPreContextLength;
|
|
158
|
+
// Distinguish "no usage.record in any wire file" (no data → usage:
|
|
159
|
+
// undefined, downstream omits the token block) from "records exist but
|
|
160
|
+
// are all zero / malformed" (real data → aggregate with ?? 0 and return
|
|
161
|
+
// the zeros as-is). A malformed record (usage.record line with missing
|
|
162
|
+
// usage object/fields) still counts as data.
|
|
163
|
+
let hasUsageRecord = false;
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
let entry;
|
|
166
|
+
try {
|
|
167
|
+
entry = JSON.parse(line);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (isUsageRecord(entry)) {
|
|
173
|
+
hasUsageRecord = true;
|
|
174
|
+
// Malformed records (missing usage object or missing fields) must
|
|
175
|
+
// not poison the accumulators with NaN: count missing fields as 0.
|
|
176
|
+
const inputOther = entry.usage?.inputOther ?? 0;
|
|
177
|
+
const output = entry.usage?.output ?? 0;
|
|
178
|
+
const inputCacheRead = entry.usage?.inputCacheRead ?? 0;
|
|
179
|
+
const inputCacheCreation = entry.usage?.inputCacheCreation ?? 0;
|
|
180
|
+
totals.input += inputOther;
|
|
181
|
+
totals.output += output;
|
|
182
|
+
totals.cacheRead += inputCacheRead;
|
|
183
|
+
totals.cacheCreation += inputCacheCreation;
|
|
184
|
+
if (turn) {
|
|
185
|
+
turn.input += inputOther;
|
|
186
|
+
turn.output += output;
|
|
187
|
+
turn.cacheRead += inputCacheRead;
|
|
188
|
+
turn.cacheCreation += inputCacheCreation;
|
|
189
|
+
}
|
|
190
|
+
// unified contract (§9.21): input + output + cacheRead + cacheCreation.
|
|
191
|
+
// 末轮 output 已进入历史、下一 prompt 全量携带——与 compact postTokens
|
|
192
|
+
// 水位同语义(此前漏加 output 会系统性低估长收尾 turn)。
|
|
193
|
+
mainContextLength = inputOther + output + inputCacheRead + inputCacheCreation;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// §6.3: Count compaction events and track pre-compact context length.
|
|
197
|
+
// 展示计数只计 context.apply_compaction:一次手动 /compact 会同时写
|
|
198
|
+
// full_compaction.complete + context.apply_compaction 两条(2026-08-16
|
|
199
|
+
// 实弹 wire.jsonl),双计会把一次压缩虚报成 2 次。full_compaction.complete
|
|
200
|
+
// 仅作 runCompact 轮询的完成信号(readCompactionRecords 仍返回两种)。
|
|
201
|
+
if (isCompactionComplete(entry) && entry.type === 'context.apply_compaction') {
|
|
202
|
+
compactCount++;
|
|
203
|
+
// Capture context length before this compaction as the pre-compact
|
|
204
|
+
// watermark (the most recent one is most relevant for display).
|
|
205
|
+
if (entry.tokensBefore !== undefined) {
|
|
206
|
+
compactPreContextLength = entry.tokensBefore;
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
// turn.prompt 是 turn 的起点:本 turn 的 flow 从这里重新累计。
|
|
211
|
+
if (isTurnPrompt(entry)) {
|
|
212
|
+
turn = emptyKimiTotals();
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { totals, turn, mainContextLength, compactCount, compactPreContextLength, hasUsageRecord };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* kimi 子 agent wire usage 扫描(只读 I/O):agents/<name>/wire.jsonl(main
|
|
220
|
+
* 以外的兄弟目录)与主会话同属同一计费实体,token 消耗必须并入会话累计;
|
|
221
|
+
* contextLength/展示事件不取子 agent。目录不可读或文件缺失静默跳过。
|
|
222
|
+
*
|
|
223
|
+
* 产生:KimiSubagentUsageScan。依赖:文件系统(agentsDir 只读)。不改外部状态。
|
|
224
|
+
*/
|
|
225
|
+
function scanKimiSubagentUsage(agentsDir) {
|
|
226
|
+
const totals = emptyKimiTotals();
|
|
227
|
+
let hasUsageRecord = false;
|
|
228
|
+
try {
|
|
229
|
+
for (const dirent of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
230
|
+
if (!dirent.isDirectory() || dirent.name === 'main') {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const subWirePath = path.join(agentsDir, dirent.name, 'wire.jsonl');
|
|
234
|
+
if (!fs.existsSync(subWirePath)) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
for (const subLine of readJsonlLines(subWirePath)) {
|
|
238
|
+
try {
|
|
239
|
+
const subEntry = JSON.parse(subLine);
|
|
240
|
+
if (isUsageRecord(subEntry)) {
|
|
241
|
+
hasUsageRecord = true;
|
|
242
|
+
totals.input += subEntry.usage?.inputOther ?? 0;
|
|
243
|
+
totals.output += subEntry.usage?.output ?? 0;
|
|
244
|
+
totals.cacheRead += subEntry.usage?.inputCacheRead ?? 0;
|
|
245
|
+
totals.cacheCreation += subEntry.usage?.inputCacheCreation ?? 0;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// agents dir unreadable — fall back to main-only usage
|
|
256
|
+
}
|
|
257
|
+
return { totals, hasUsageRecord };
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* kimi usage 装配(tail 装配段,纯函数):末 turn flow + 会话累计(主 wire +
|
|
261
|
+
* 子 agent)+ 主 wire 水位 + 压缩统计 → AgentSessionUsage。
|
|
262
|
+
*
|
|
263
|
+
* 有 turn.prompt 时本 run = 末个 turn 的 flow;末 turn 还没有记录时
|
|
264
|
+
* turn 为全 0。整份 wire 都没有 turn.prompt(旧数据/截断)时无从分段,
|
|
265
|
+
* 沿用「整份文件 = 一个 turn」的旧口径(含子 agent wire)。
|
|
266
|
+
*/
|
|
267
|
+
function assembleKimiUsage(main, sub) {
|
|
268
|
+
const totals = {
|
|
269
|
+
input: main.totals.input + sub.totals.input,
|
|
270
|
+
output: main.totals.output + sub.totals.output,
|
|
271
|
+
cacheRead: main.totals.cacheRead + sub.totals.cacheRead,
|
|
272
|
+
cacheCreation: main.totals.cacheCreation + sub.totals.cacheCreation,
|
|
273
|
+
};
|
|
274
|
+
const flow = main.turn ?? totals;
|
|
275
|
+
// Kimi semantics: preserve 0 values (not undefined) for cache fields to match original
|
|
276
|
+
return {
|
|
277
|
+
inputTokens: flow.input,
|
|
278
|
+
outputTokens: flow.output,
|
|
279
|
+
contextLength: main.mainContextLength,
|
|
280
|
+
cacheReadTokens: flow.cacheRead > 0 ? flow.cacheRead : 0,
|
|
281
|
+
cacheCreationTokens: flow.cacheCreation > 0 ? flow.cacheCreation : 0,
|
|
282
|
+
totalTokens: flow.input + flow.output + flow.cacheRead + flow.cacheCreation,
|
|
283
|
+
// Cumulative (session-wide): sums every usage.record across all turns
|
|
284
|
+
// (subagent wires included) — the "累计" line, never the per-run value.
|
|
285
|
+
...cumulativeUsageFields(totals),
|
|
286
|
+
// §6.3: compaction stats from wire.jsonl
|
|
287
|
+
compactCount: main.compactCount,
|
|
288
|
+
...(main.compactPreContextLength !== undefined
|
|
289
|
+
? { compactPreContextLength: main.compactPreContextLength }
|
|
290
|
+
: {}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* kimi 主 wire 展示装配(纯函数):turn.prompt 提取 displayTitle,loop event
|
|
295
|
+
* 映射为展示事件。tool.call/tool.result 是**独立的 loop event**(对照真实
|
|
296
|
+
* wire.jsonl 验证过的结构,勿合并/改形)。
|
|
297
|
+
*
|
|
298
|
+
* 产生:KimiEventScan。依赖:入参 lines。不改任何外部状态。
|
|
299
|
+
*/
|
|
300
|
+
function scanKimiEvents(lines) {
|
|
301
|
+
const events = [];
|
|
302
|
+
let displayTitle;
|
|
303
|
+
for (const line of lines) {
|
|
304
|
+
let entry;
|
|
305
|
+
try {
|
|
306
|
+
entry = JSON.parse(line);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
// displayTitle + display events now scan ALL lines (not a
|
|
312
|
+
// line-slice), then `events.slice(-maxEvents)` keeps the LAST N
|
|
313
|
+
// EVENTS. Previously `lines.slice(-maxEvents)` cut raw lines, but
|
|
314
|
+
// usage.record / step.begin / turn.prompt lines produce no display
|
|
315
|
+
// event, so "last 5 lines" could yield only 1~2 visible events —
|
|
316
|
+
// and `slice(-0)` returned the full array. displayTitle takes the
|
|
317
|
+
// last turn.prompt across the whole session (the most recent user
|
|
318
|
+
// prompt), which is the correct catch-up title.
|
|
319
|
+
// Extract display title from turn.prompt
|
|
320
|
+
if (isTurnPrompt(entry)) {
|
|
321
|
+
if (entry.input && entry.input.length > 0) {
|
|
322
|
+
displayTitle = entry.input[0].text;
|
|
323
|
+
}
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
// Convert loop events to content events
|
|
327
|
+
if (isAppendLoopEvent(entry)) {
|
|
328
|
+
const evt = entry.event;
|
|
329
|
+
if (evt.type === 'content.part' && evt.part) {
|
|
330
|
+
if (evt.part.type === 'think') {
|
|
331
|
+
// Skip thinking blocks for display
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
else if (evt.part.type === 'text' && evt.part.text) {
|
|
335
|
+
events.push({
|
|
336
|
+
type: 'text',
|
|
337
|
+
content: evt.part.text,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else if (evt.type === 'tool.call' && evt.name) {
|
|
342
|
+
// 复用 content-blocks 的 200 字符截断:大 args(整段文件内容
|
|
343
|
+
// 写入、长 prompt)原文进事件会把 resume 卡顶到 28KB 预算之外
|
|
344
|
+
// (2026-08-08 extreme_fallback 故障的推手之一)。
|
|
345
|
+
const inputStr = truncateToolInput(JSON.stringify(evt.args ?? {}));
|
|
346
|
+
events.push({
|
|
347
|
+
type: 'tool_use',
|
|
348
|
+
content: `${evt.name}(${inputStr})`,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
else if (evt.type === 'tool.result') {
|
|
352
|
+
// result is { output }, { output, note }, or { isError, output }
|
|
353
|
+
// 对齐 opencode reader 的预折叠:单条 tool_result 上限
|
|
354
|
+
// TOOL_RESULT_MAX_BYTES,病理级输出(如 51KB 文件读取)不会
|
|
355
|
+
// 撑爆 resume 卡,也约束 events[] 内存占用。
|
|
356
|
+
const rawOut = evt.result?.output ?? `[tool result for ${evt.toolCallId}]`;
|
|
357
|
+
events.push({
|
|
358
|
+
type: 'tool_result',
|
|
359
|
+
content: truncateUtf8(rawOut, TOOL_RESULT_MAX_BYTES),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return { events, displayTitle };
|
|
365
|
+
}
|
|
137
366
|
/**
|
|
138
367
|
* Kimi Session Reader
|
|
139
368
|
* Reads session history from ~/.kimi-code/
|
|
@@ -168,39 +397,6 @@ export class KimiSessionReader {
|
|
|
168
397
|
findSessionIndexEntry(sessionId) {
|
|
169
398
|
return this.scanSessionIndex().find((e) => e.sessionId === sessionId);
|
|
170
399
|
}
|
|
171
|
-
/**
|
|
172
|
-
* Extract the last turn.prompt text from a wire.jsonl file.
|
|
173
|
-
* Used as a fallback summary source when state.json has no title/lastPrompt
|
|
174
|
-
* (v2 sessions). Scans the tail of the file for efficiency.
|
|
175
|
-
* Returns undefined if no turn.prompt is found.
|
|
176
|
-
*/
|
|
177
|
-
extractLastPromptFromWire(wirePath) {
|
|
178
|
-
try {
|
|
179
|
-
if (!fs.existsSync(wirePath))
|
|
180
|
-
return undefined;
|
|
181
|
-
// Scan backwards from the tail — turn.prompt is typically near the end
|
|
182
|
-
// of a completed turn (before content.part / step.end / usage.record).
|
|
183
|
-
const lines = readLastNJsonlLines(wirePath, 50);
|
|
184
|
-
let lastPrompt;
|
|
185
|
-
for (const line of lines) {
|
|
186
|
-
try {
|
|
187
|
-
const entry = JSON.parse(line);
|
|
188
|
-
if (isTurnPrompt(entry)) {
|
|
189
|
-
if (entry.input && entry.input.length > 0) {
|
|
190
|
-
lastPrompt = entry.input[0].text;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return lastPrompt?.substring(0, 200);
|
|
199
|
-
}
|
|
200
|
-
catch {
|
|
201
|
-
return undefined;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
400
|
/**
|
|
205
401
|
* List all sessions for a given working directory
|
|
206
402
|
*/
|
|
@@ -208,87 +404,16 @@ export class KimiSessionReader {
|
|
|
208
404
|
try {
|
|
209
405
|
// 归一失败(陈旧/无权限 cwd)回退原样输入,让逐条 samePath 比较去决定
|
|
210
406
|
// 空态——一次 realpathSync 抛错不得把整张列表清掉(与 opencode 同口径)。
|
|
211
|
-
const realCwd = safeRealpath(cwd, (
|
|
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
|
-
});
|
|
407
|
+
const realCwd = safeRealpath(cwd, logKimiRealpathFailure.bind(null, cwd));
|
|
216
408
|
const sessions = [];
|
|
217
409
|
for (const entry of this.scanSessionIndex()) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const statePath = path.join(entry.sessionDir, 'state.json');
|
|
222
|
-
let state = null;
|
|
223
|
-
let stateParsed = false;
|
|
224
|
-
if (fs.existsSync(statePath)) {
|
|
225
|
-
try {
|
|
226
|
-
state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
227
|
-
stateParsed = true;
|
|
228
|
-
}
|
|
229
|
-
catch (err) {
|
|
230
|
-
getLogger().debug(`[kimi-session-reader] corrupt state.json skipped session=${entry.sessionId}: ${err?.message ?? String(err)}`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
// Index workDir is v1-only; v2 sessions may have empty/missing workDir.
|
|
234
|
-
// When index workDir is absent, fall back to state.json's cwd field.
|
|
235
|
-
let entryWorkDir = entry.workDir;
|
|
236
|
-
if (!entryWorkDir) {
|
|
237
|
-
if (stateParsed && state)
|
|
238
|
-
entryWorkDir = extractWorkDir(state);
|
|
239
|
-
// Unreadable state — cannot determine cwd, entryWorkDir stays undefined
|
|
240
|
-
}
|
|
241
|
-
// cwd 判定与守卫(checkCwdGuard 的 samePath)同口径:严格字符串比较会
|
|
242
|
-
// 把大小写(darwin)/尾分隔符形态不同的会话从列表里滤掉,而 /resume <id>
|
|
243
|
-
// 的守卫却判等通过——「列表没有、按 id 却能读」。
|
|
244
|
-
if (entryWorkDir === undefined || !samePath(entryWorkDir, realCwd)) {
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
let summary = 'New Session';
|
|
248
|
-
if (stateParsed && state) {
|
|
249
|
-
// v1: state.title / state.lastPrompt; v2: these fields may be absent
|
|
250
|
-
summary = state.title || state.lastPrompt || '';
|
|
251
|
-
}
|
|
252
|
-
// v2 state.json omits title/lastPrompt: extract from wire.jsonl's last
|
|
253
|
-
// turn.prompt instead. Only scan a small tail to find it — the summary
|
|
254
|
-
// only needs the most recent user prompt.
|
|
255
|
-
if (!summary) {
|
|
256
|
-
const wirePath = path.join(entry.sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
257
|
-
summary = this.extractLastPromptFromWire(wirePath) || 'New Session';
|
|
258
|
-
}
|
|
259
|
-
// Get mtime. the session DIRECTORY mtime only changes when
|
|
260
|
-
// entries are created/deleted/renamed inside it — kimi appending to
|
|
261
|
-
// agents/main/wire.jsonl during a run does NOT touch it, so dir-mtime
|
|
262
|
-
// sorting lags real activity by minutes. Use the wire.jsonl mtime
|
|
263
|
-
// (the file kimi keeps writing while the session is active) and fall
|
|
264
|
-
// back to the dir mtime when the wire file is missing (e.g. a session
|
|
265
|
-
// that never produced output).
|
|
266
|
-
const wirePath = path.join(entry.sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
267
|
-
let mtimeMs;
|
|
268
|
-
try {
|
|
269
|
-
mtimeMs = fs.statSync(wirePath).mtimeMs;
|
|
270
|
-
}
|
|
271
|
-
catch {
|
|
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
|
-
}
|
|
282
|
-
}
|
|
283
|
-
sessions.push({
|
|
284
|
-
sessionId: entry.sessionId,
|
|
285
|
-
summary,
|
|
286
|
-
mtime: mtimeMs,
|
|
287
|
-
});
|
|
410
|
+
const row = collectKimiSession(entry, realCwd);
|
|
411
|
+
if (row)
|
|
412
|
+
sessions.push(row);
|
|
288
413
|
}
|
|
289
414
|
// Sort by mtime descending; same-mtime ties use a deterministic
|
|
290
415
|
// secondary key so ordering is stable across scans.
|
|
291
|
-
sortByRecencyDesc(sessions,
|
|
416
|
+
sortByRecencyDesc(sessions, kimiRowMtime, kimiRowId);
|
|
292
417
|
const { items, total } = paginate(sessions, opts ?? {});
|
|
293
418
|
return {
|
|
294
419
|
sessions: items,
|
|
@@ -302,9 +427,7 @@ export class KimiSessionReader {
|
|
|
302
427
|
if (error.code === 'ENOENT') {
|
|
303
428
|
return { sessions: [], total: 0 };
|
|
304
429
|
}
|
|
305
|
-
|
|
306
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
307
|
-
throw new Error(`kimi session list 读取失败: ${msg}`, { cause: error });
|
|
430
|
+
throwSessionListError('kimi', error);
|
|
308
431
|
}
|
|
309
432
|
}
|
|
310
433
|
/**
|
|
@@ -315,6 +438,11 @@ export class KimiSessionReader {
|
|
|
315
438
|
}
|
|
316
439
|
/**
|
|
317
440
|
* Read session content (events + usage)
|
|
441
|
+
*
|
|
442
|
+
* 编排:cwd 守卫定位主 wire(resolveSessionWirePath,与 isSessionActive
|
|
443
|
+
* 共用 verifySessionCwd 单源)→ 主 wire 两遍只读扫描(scanKimiEvents 出
|
|
444
|
+
* 展示流 + displayTitle,scanKimiUsage 出累计/flow/水位/压缩统计)→ 子
|
|
445
|
+
* agent wire 只进 usage 累计 → assembleKimiUsage 组装。
|
|
318
446
|
*/
|
|
319
447
|
readSessionContent(sessionId, cwd, opts) {
|
|
320
448
|
const wirePath = this.resolveSessionWirePath(sessionId, cwd);
|
|
@@ -323,144 +451,8 @@ export class KimiSessionReader {
|
|
|
323
451
|
}
|
|
324
452
|
try {
|
|
325
453
|
const lines = Array.from(readJsonlLines(wirePath));
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
let displayTitle;
|
|
329
|
-
const acc = new UsageAccumulator();
|
|
330
|
-
// contextLength is only from the main wire's last usage.record
|
|
331
|
-
// (subagent context is an independent window). Tracked separately
|
|
332
|
-
// from the accumulator's `last` (which could be a subagent record).
|
|
333
|
-
let mainContextLength = 0;
|
|
334
|
-
// §6.3: Compaction stats — count compact events, track last pre-compact context.
|
|
335
|
-
let compactCount = 0;
|
|
336
|
-
let compactPreContextLength;
|
|
337
|
-
// Distinguish "no usage.record in any wire file" (no data → usage:
|
|
338
|
-
// undefined, downstream omits the token block) from "records exist but
|
|
339
|
-
// are all zero / malformed" (real data → aggregate with ?? 0 and return
|
|
340
|
-
// the zeros as-is). A malformed record (usage.record line with missing
|
|
341
|
-
// usage object/fields) still counts as data.
|
|
342
|
-
let hasUsageRecord = false;
|
|
343
|
-
// ccusage-style session-wide aggregation via UsageAccumulator: sum
|
|
344
|
-
// every usage.record's per-component tokens; contextLength is the
|
|
345
|
-
// current context window occupancy (not summable) — keep only the
|
|
346
|
-
// last record's value, and only from the main wire (subagent context
|
|
347
|
-
// is an independent window). Aggregation covers the whole wire.jsonl
|
|
348
|
-
// and is independent of the maxEvents display slice
|
|
349
|
-
// (usage = the session's overall token cost).
|
|
350
|
-
//
|
|
351
|
-
// 本 run 的 flow 字段是**末个 turn**(最后一个 turn.prompt 之后)的记录之和:
|
|
352
|
-
// kimi 每次模型调用写一条 usage.record,带工具往返的 turn 会有多条。只取
|
|
353
|
-
// 末条会低报,取会话总量(旧实现)又会把历史所有 run 的消耗算进这一轮。
|
|
354
|
-
// 子 agent 的 wire 没有 turn 标记,只能进会话累计(无法归属到某一轮);
|
|
355
|
-
// 整份 wire 都没有 turn.prompt(旧数据)时沿用「整份文件 = 一个 turn」。
|
|
356
|
-
// 末个 turn 尚无 usage.record 时 flow = 0(当前契约是「末个 turn」,不是
|
|
357
|
-
// 「末个非空 turn」)——不能退回上一轮的记录。
|
|
358
|
-
let turnAcc = null;
|
|
359
|
-
const accumulateUsage = (u, scope) => {
|
|
360
|
-
hasUsageRecord = true;
|
|
361
|
-
// Malformed records (missing usage object or missing fields) must
|
|
362
|
-
// not poison the accumulators with NaN: count missing fields as 0.
|
|
363
|
-
const inputOther = u?.inputOther ?? 0;
|
|
364
|
-
const output = u?.output ?? 0;
|
|
365
|
-
const inputCacheRead = u?.inputCacheRead ?? 0;
|
|
366
|
-
const inputCacheCreation = u?.inputCacheCreation ?? 0;
|
|
367
|
-
const record = {
|
|
368
|
-
input: inputOther,
|
|
369
|
-
output,
|
|
370
|
-
cacheRead: inputCacheRead,
|
|
371
|
-
cacheCreation: inputCacheCreation,
|
|
372
|
-
};
|
|
373
|
-
acc.add(record);
|
|
374
|
-
if (scope !== 'main')
|
|
375
|
-
return;
|
|
376
|
-
turnAcc?.add(record);
|
|
377
|
-
// unified contract (§9.21): input + output + cacheRead + cacheCreation.
|
|
378
|
-
// 末轮 output 已进入历史、下一 prompt 全量携带——与 compact postTokens
|
|
379
|
-
// 水位同语义(此前漏加 output 会系统性低估长收尾 turn)。
|
|
380
|
-
mainContextLength = inputOther + output + inputCacheRead + inputCacheCreation;
|
|
381
|
-
};
|
|
382
|
-
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
383
|
-
const line = lines[lineIndex];
|
|
384
|
-
try {
|
|
385
|
-
const entry = JSON.parse(line);
|
|
386
|
-
// Extract usage from usage.record.
|
|
387
|
-
if (isUsageRecord(entry)) {
|
|
388
|
-
accumulateUsage(entry.usage, 'main');
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
// §6.3: Count compaction events and track pre-compact context length.
|
|
392
|
-
// 展示计数只计 context.apply_compaction:一次手动 /compact 会同时写
|
|
393
|
-
// full_compaction.complete + context.apply_compaction 两条(2026-08-16
|
|
394
|
-
// 实弹 wire.jsonl),双计会把一次压缩虚报成 2 次。full_compaction.complete
|
|
395
|
-
// 仅作 runCompact 轮询的完成信号(readCompactionRecords 仍返回两种)。
|
|
396
|
-
if (isCompactionComplete(entry) && entry.type === 'context.apply_compaction') {
|
|
397
|
-
compactCount++;
|
|
398
|
-
// Capture context length before this compaction as the pre-compact
|
|
399
|
-
// watermark (the most recent one is most relevant for display).
|
|
400
|
-
if (entry.tokensBefore !== undefined) {
|
|
401
|
-
compactPreContextLength = entry.tokensBefore;
|
|
402
|
-
}
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
// displayTitle + display events now scan ALL lines (not a
|
|
406
|
-
// line-slice), then `events.slice(-maxEvents)` keeps the LAST N
|
|
407
|
-
// EVENTS. Previously `lines.slice(-maxEvents)` cut raw lines, but
|
|
408
|
-
// usage.record / step.begin / turn.prompt lines produce no display
|
|
409
|
-
// event, so "last 5 lines" could yield only 1~2 visible events —
|
|
410
|
-
// and `slice(-0)` returned the full array. displayTitle takes the
|
|
411
|
-
// last turn.prompt across the whole session (the most recent user
|
|
412
|
-
// prompt), which is the correct catch-up title.
|
|
413
|
-
// Extract display title from turn.prompt
|
|
414
|
-
if (isTurnPrompt(entry)) {
|
|
415
|
-
// turn.prompt 是 turn 的起点:本 turn 的 flow 从这里重新累计。
|
|
416
|
-
turnAcc = new UsageAccumulator();
|
|
417
|
-
if (entry.input && entry.input.length > 0) {
|
|
418
|
-
displayTitle = entry.input[0].text;
|
|
419
|
-
}
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
// Convert loop events to content events
|
|
423
|
-
if (isAppendLoopEvent(entry)) {
|
|
424
|
-
const evt = entry.event;
|
|
425
|
-
if (evt.type === 'content.part' && evt.part) {
|
|
426
|
-
if (evt.part.type === 'think') {
|
|
427
|
-
// Skip thinking blocks for display
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
else if (evt.part.type === 'text' && evt.part.text) {
|
|
431
|
-
events.push({
|
|
432
|
-
type: 'text',
|
|
433
|
-
content: evt.part.text,
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
else if (evt.type === 'tool.call' && evt.name) {
|
|
438
|
-
// 复用 content-blocks 的 200 字符截断:大 args(整段文件内容
|
|
439
|
-
// 写入、长 prompt)原文进事件会把 resume 卡顶到 28KB 预算之外
|
|
440
|
-
// (2026-08-08 extreme_fallback 故障的推手之一)。
|
|
441
|
-
const inputStr = truncateToolInput(JSON.stringify(evt.args ?? {}));
|
|
442
|
-
events.push({
|
|
443
|
-
type: 'tool_use',
|
|
444
|
-
content: `${evt.name}(${inputStr})`,
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
else if (evt.type === 'tool.result') {
|
|
448
|
-
// result is { output }, { output, note }, or { isError, output }
|
|
449
|
-
// 对齐 opencode reader 的预折叠:单条 tool_result 上限
|
|
450
|
-
// TOOL_RESULT_MAX_BYTES,病理级输出(如 51KB 文件读取)不会
|
|
451
|
-
// 撑爆 resume 卡,也约束 events[] 内存占用。
|
|
452
|
-
const rawOut = evt.result?.output ?? `[tool result for ${evt.toolCallId}]`;
|
|
453
|
-
events.push({
|
|
454
|
-
type: 'tool_result',
|
|
455
|
-
content: truncateUtf8(rawOut, TOOL_RESULT_MAX_BYTES),
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
catch {
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
454
|
+
const { events, displayTitle } = scanKimiEvents(lines);
|
|
455
|
+
const mainUsage = scanKimiUsage(lines);
|
|
464
456
|
// Subagent wire files (agents/<name>/wire.jsonl, e.g. Task-derived
|
|
465
457
|
// agent-0..N) belong to the same session and the same billing entity:
|
|
466
458
|
// their token cost must be summed into the session-wide usage.
|
|
@@ -470,56 +462,12 @@ export class KimiSessionReader {
|
|
|
470
462
|
// wirePath = <sessionDir>/agents/main/wire.jsonl → agents dir is two
|
|
471
463
|
// levels up (siblings of main hold subagent wires).
|
|
472
464
|
const agentsDir = path.dirname(path.dirname(wirePath));
|
|
473
|
-
|
|
474
|
-
for (const dirent of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
475
|
-
if (!dirent.isDirectory() || dirent.name === 'main') {
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
|
-
const subWirePath = path.join(agentsDir, dirent.name, 'wire.jsonl');
|
|
479
|
-
if (!fs.existsSync(subWirePath)) {
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
for (const subLine of readJsonlLines(subWirePath)) {
|
|
483
|
-
try {
|
|
484
|
-
const subEntry = JSON.parse(subLine);
|
|
485
|
-
if (isUsageRecord(subEntry)) {
|
|
486
|
-
accumulateUsage(subEntry.usage, 'subagent');
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
catch {
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
catch {
|
|
496
|
-
// agents dir unreadable — fall back to main-only usage
|
|
497
|
-
}
|
|
498
|
-
// Build the final AgentSessionUsage from the last turn's flow + session
|
|
499
|
-
// cumulative + main contextLength.
|
|
500
|
-
const t = acc.totals;
|
|
501
|
-
// 有 turn.prompt 时本 run = 末个 turn 的 flow;末 turn 还没有记录时
|
|
502
|
-
// turnAcc.count = 0 → flow = 0。整份 wire 都没有 turn.prompt(旧数据/
|
|
503
|
-
// 截断)时无从分段,沿用「整份文件 = 一个 turn」的旧口径(含子 agent wire)。
|
|
504
|
-
const flow = turnAcc ? turnAcc.totals : t;
|
|
505
|
-
// Kimi semantics: preserve 0 values (not undefined) for cache fields to match original
|
|
506
|
-
const totalUsage = {
|
|
507
|
-
inputTokens: flow.input,
|
|
508
|
-
outputTokens: flow.output,
|
|
509
|
-
contextLength: mainContextLength,
|
|
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.
|
|
515
|
-
...cumulativeUsageFields(t),
|
|
516
|
-
// §6.3: compaction stats from wire.jsonl
|
|
517
|
-
compactCount,
|
|
518
|
-
...(compactPreContextLength !== undefined ? { compactPreContextLength } : {}),
|
|
519
|
-
};
|
|
465
|
+
const subUsage = scanKimiSubagentUsage(agentsDir);
|
|
520
466
|
return {
|
|
521
|
-
events: capEvents(events, maxEvents),
|
|
522
|
-
usage: hasUsageRecord
|
|
467
|
+
events: capEvents(events, opts?.maxEvents),
|
|
468
|
+
usage: mainUsage.hasUsageRecord || subUsage.hasUsageRecord
|
|
469
|
+
? assembleKimiUsage(mainUsage, subUsage)
|
|
470
|
+
: undefined,
|
|
523
471
|
displayTitle,
|
|
524
472
|
};
|
|
525
473
|
}
|
|
@@ -704,9 +652,133 @@ export class KimiSessionReader {
|
|
|
704
652
|
// Consider the session inactive when its last loop event is step.end
|
|
705
653
|
return !lastLoopEventIsStepEnd(lines);
|
|
706
654
|
}
|
|
707
|
-
catch {
|
|
655
|
+
catch (err) {
|
|
656
|
+
getLogger().debug('[kimi-session-reader] isSessionActive failed:', err);
|
|
708
657
|
return false;
|
|
709
658
|
}
|
|
710
659
|
}
|
|
711
660
|
}
|
|
661
|
+
// --- listSessions 装配原语(模块级命名函数 + 显式传参) ---
|
|
662
|
+
// 纯结构拆分:listSessions 只做 realpath / 遍历 / 排序分页 / 错误降级,
|
|
663
|
+
// 单条会话的 state 读取、cwd 守卫、summary 三级回退、mtime 回退全部下沉到
|
|
664
|
+
// 这些模块级函数。文案、跳过条件、排序键、分页口径逐项保持。
|
|
665
|
+
/** realpath 归一化失败(非 ENOENT)留痕;ENOENT 静默沿用原 cwd。 */
|
|
666
|
+
function logKimiRealpathFailure(cwd, err) {
|
|
667
|
+
logNonEnoentDebug(err, (m) => `[kimi-session-reader] realpath ${cwd} failed, using the path as given: ${m}`);
|
|
668
|
+
}
|
|
669
|
+
/** state.json 一次读取共用(cwd 兜底 + summary),I/O 减半。 */
|
|
670
|
+
function readKimiState(sessionDir, sessionId) {
|
|
671
|
+
const statePath = path.join(sessionDir, 'state.json');
|
|
672
|
+
if (!fs.existsSync(statePath))
|
|
673
|
+
return { state: null, stateParsed: false };
|
|
674
|
+
try {
|
|
675
|
+
return {
|
|
676
|
+
state: JSON.parse(fs.readFileSync(statePath, 'utf-8')),
|
|
677
|
+
stateParsed: true,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
// 解析失败 = 无法判 cwd → 不 continue,留给 index workDir 兜底。
|
|
682
|
+
getLogger().debug(`[kimi-session-reader] corrupt state.json skipped session=${sessionId}: ${err?.message ?? String(err)}`);
|
|
683
|
+
return { state: null, stateParsed: false };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/** index workDir 优先,缺失时回退 state.json 的 workDir/cwd。 */
|
|
687
|
+
function resolveKimiEntryWorkDir(entry, read) {
|
|
688
|
+
let entryWorkDir = entry.workDir;
|
|
689
|
+
if (!entryWorkDir && read.stateParsed && read.state) {
|
|
690
|
+
entryWorkDir = extractWorkDir(read.state);
|
|
691
|
+
}
|
|
692
|
+
return entryWorkDir;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* 从 wire.jsonl 末段提取最后一条 turn.prompt。
|
|
696
|
+
* v2 state.json 省略 title/lastPrompt 时作为 summary 兜底;只扫尾部 50 行。
|
|
697
|
+
*/
|
|
698
|
+
function extractLastPromptFromWire(wirePath) {
|
|
699
|
+
try {
|
|
700
|
+
if (!fs.existsSync(wirePath))
|
|
701
|
+
return undefined;
|
|
702
|
+
// Scan backwards from the tail — turn.prompt is typically near the end
|
|
703
|
+
// of a completed turn (before content.part / step.end / usage.record).
|
|
704
|
+
const lines = readLastNJsonlLines(wirePath, 50);
|
|
705
|
+
let lastPrompt;
|
|
706
|
+
for (const line of lines) {
|
|
707
|
+
try {
|
|
708
|
+
const entry = JSON.parse(line);
|
|
709
|
+
if (isTurnPrompt(entry)) {
|
|
710
|
+
if (entry.input && entry.input.length > 0) {
|
|
711
|
+
lastPrompt = entry.input[0].text;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return lastPrompt?.substring(0, 200);
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
return undefined;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/** summary:state.title / lastPrompt → wire turn.prompt → 'New Session'。 */
|
|
726
|
+
function kimiSessionSummary(sessionDir, read) {
|
|
727
|
+
let summary = 'New Session';
|
|
728
|
+
if (read.stateParsed && read.state) {
|
|
729
|
+
// v1: state.title / state.lastPrompt; v2: these fields may be absent
|
|
730
|
+
summary = read.state.title || read.state.lastPrompt || '';
|
|
731
|
+
}
|
|
732
|
+
if (!summary) {
|
|
733
|
+
const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
734
|
+
summary = extractLastPromptFromWire(wirePath) || 'New Session';
|
|
735
|
+
}
|
|
736
|
+
return summary;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* 排序用 mtime:wire.jsonl 优先(run 期间持续写入),缺失回退会话目录
|
|
740
|
+
* mtime;目录也不可读(非 ENOENT 留痕)返回 null,由调用方跳过该条。
|
|
741
|
+
*/
|
|
742
|
+
function kimiSessionMtime(sessionDir) {
|
|
743
|
+
const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
744
|
+
try {
|
|
745
|
+
return fs.statSync(wirePath).mtimeMs;
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
try {
|
|
749
|
+
return fs.statSync(sessionDir).mtimeMs;
|
|
750
|
+
}
|
|
751
|
+
catch (err) {
|
|
752
|
+
if (err.code !== 'ENOENT') {
|
|
753
|
+
getLogger().warn(`[kimi-session-reader] cannot stat ${sessionDir}: ${err?.message ?? String(err)}`);
|
|
754
|
+
}
|
|
755
|
+
// 会话目录已不存在(或被清理):跳过该条,别让它拖垮整张列表。
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/** 单条 index entry → 列表行;cwd 不匹配 / 目录不可读时返回 null(跳过)。 */
|
|
761
|
+
function collectKimiSession(entry, realCwd) {
|
|
762
|
+
const read = readKimiState(entry.sessionDir, entry.sessionId);
|
|
763
|
+
const entryWorkDir = resolveKimiEntryWorkDir(entry, read);
|
|
764
|
+
if (entryWorkDir === undefined || !samePath(entryWorkDir, realCwd)) {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
const summary = kimiSessionSummary(entry.sessionDir, read);
|
|
768
|
+
const mtimeMs = kimiSessionMtime(entry.sessionDir);
|
|
769
|
+
if (mtimeMs === null)
|
|
770
|
+
return null;
|
|
771
|
+
return {
|
|
772
|
+
sessionId: entry.sessionId,
|
|
773
|
+
summary,
|
|
774
|
+
mtime: mtimeMs,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
/** 排序键(模块级:不把回调决策点算回 listSessions)。 */
|
|
778
|
+
function kimiRowMtime(s) {
|
|
779
|
+
return s.mtime;
|
|
780
|
+
}
|
|
781
|
+
function kimiRowId(s) {
|
|
782
|
+
return s.sessionId;
|
|
783
|
+
}
|
|
712
784
|
//# sourceMappingURL=sessions.js.map
|