lark-remote 0.1.6 → 0.1.7
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 +23 -1
- package/README.en.md +9 -1
- package/README.md +14 -2
- package/dist/bridge/approval-coordinator.d.ts +57 -34
- package/dist/bridge/approval-coordinator.js +191 -40
- package/dist/bridge/inbound-media.d.ts +62 -0
- package/dist/bridge/inbound-media.js +282 -0
- package/dist/bridge/index.d.ts +78 -7
- package/dist/bridge/index.js +202 -79
- package/dist/bridge/queue-manager.d.ts +2 -1
- package/dist/bridge/queue-manager.js +1 -0
- package/dist/card/approval-render.js +152 -36
- package/dist/card/bash-card-session.d.ts +2 -24
- package/dist/card/bash-card-session.js +6 -73
- package/dist/card/bash-renderer.js +2 -3
- package/dist/card/card-budget.js +29 -75
- package/dist/card/card-session.d.ts +48 -0
- package/dist/card/card-session.js +86 -0
- package/dist/card/card-shared.d.ts +1 -12
- package/dist/card/card-shared.js +1 -26
- package/dist/card/run-card-session.d.ts +2 -55
- package/dist/card/run-card-session.js +6 -104
- package/dist/card/run-renderer.js +99 -232
- package/dist/card/run-state.d.ts +13 -0
- package/dist/card/run-state.js +25 -16
- package/dist/card/text-truncate.d.ts +13 -12
- package/dist/card/text-truncate.js +13 -63
- package/dist/common/fs.d.ts +8 -0
- package/dist/common/fs.js +17 -0
- package/dist/common/guards.d.ts +0 -20
- package/dist/common/guards.js +0 -43
- package/dist/common/sleep.d.ts +2 -0
- package/dist/common/sleep.js +5 -0
- package/dist/common/truncate.d.ts +39 -0
- package/dist/common/truncate.js +92 -0
- package/dist/config/codex-config.d.ts +1 -1
- package/dist/config/codex-config.js +1 -1
- package/dist/config/index.d.ts +88 -23
- package/dist/config/index.js +105 -34
- package/dist/connector/index.d.ts +69 -2
- package/dist/connector/index.js +107 -1
- package/dist/index.js +123 -105
- package/dist/instance-lock.js +4 -12
- package/dist/order/alias-resolve.d.ts +13 -0
- package/dist/order/alias-resolve.js +22 -0
- package/dist/order/alias-store.d.ts +38 -0
- package/dist/order/alias-store.js +106 -0
- package/dist/persistence/atomic-write.d.ts +8 -2
- package/dist/persistence/atomic-write.js +23 -7
- package/dist/restart.js +2 -1
- package/dist/router/card-helpers.d.ts +63 -0
- package/dist/router/card-helpers.js +146 -0
- package/dist/router/config/claude.js +25 -1
- package/dist/router/config/codex.js +29 -50
- package/dist/router/config/common/model-patch.d.ts +22 -0
- package/dist/router/config/common/model-patch.js +26 -0
- package/dist/router/config/common/render.js +5 -4
- package/dist/router/config/index.d.ts +12 -0
- package/dist/router/config/index.js +25 -1
- package/dist/router/config/kimi.d.ts +4 -1
- package/dist/router/config/kimi.js +27 -0
- package/dist/router/config/opencode.js +18 -5
- package/dist/router/config/pi.js +4 -5
- package/dist/router/config/types.d.ts +11 -2
- package/dist/router/index.d.ts +56 -21
- package/dist/router/index.js +373 -548
- package/dist/router/utils.d.ts +2 -0
- package/dist/router/utils.js +4 -0
- package/dist/runner/agent-choices-common.d.ts +16 -0
- package/dist/runner/agent-choices-common.js +31 -0
- package/dist/runner/claude/runner.d.ts +47 -10
- package/dist/runner/claude/runner.js +117 -46
- package/dist/runner/claude/session.d.ts +166 -0
- package/dist/runner/claude/session.js +749 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +31 -39
- package/dist/runner/codex/app-server/protocol-types.js +1 -12
- package/dist/runner/codex/app-server/runner.d.ts +42 -59
- package/dist/runner/codex/app-server/runner.js +160 -337
- package/dist/runner/codex/app-server/translator.d.ts +1 -21
- package/dist/runner/codex/app-server/translator.js +0 -18
- package/dist/runner/codex/index.d.ts +0 -2
- package/dist/runner/codex/index.js +0 -2
- package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
- package/dist/runner/common/acp/protocol-helpers.js +21 -0
- package/dist/runner/common/acp/protocol-types.d.ts +238 -0
- package/dist/runner/common/acp/protocol-types.js +75 -0
- package/dist/runner/common/connection-based-runner.d.ts +111 -0
- package/dist/runner/common/connection-based-runner.js +261 -0
- package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
- package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
- package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
- package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
- package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
- package/dist/runner/common/jsonrpc/types.d.ts +32 -0
- package/dist/runner/common/jsonrpc/types.js +9 -0
- package/dist/runner/common/process-stopper.d.ts +2 -1
- package/dist/runner/common/process-stopper.js +1 -0
- package/dist/runner/common/runner-utils.d.ts +6 -13
- package/dist/runner/common/runner-utils.js +6 -47
- package/dist/runner/common/spawning-runner.d.ts +24 -70
- package/dist/runner/common/spawning-runner.js +98 -152
- package/dist/runner/index.d.ts +4 -4
- package/dist/runner/index.js +4 -4
- package/dist/runner/kimi/acp/runner.d.ts +106 -0
- package/dist/runner/kimi/acp/runner.js +448 -0
- package/dist/runner/kimi/acp/translator.d.ts +124 -0
- package/dist/runner/kimi/acp/translator.js +336 -0
- package/dist/runner/kimi/index.d.ts +1 -1
- package/dist/runner/kimi/index.js +1 -1
- package/dist/runner/opencode/acp/index.d.ts +1 -0
- package/dist/runner/opencode/acp/index.js +2 -0
- package/dist/runner/opencode/acp/runner.d.ts +121 -0
- package/dist/runner/opencode/acp/runner.js +429 -0
- package/dist/runner/opencode/acp/translator.d.ts +119 -0
- package/dist/runner/opencode/acp/translator.js +345 -0
- package/dist/runner/opencode/index.d.ts +1 -1
- package/dist/runner/opencode/index.js +1 -1
- package/dist/runner/pi/index.d.ts +1 -1
- package/dist/runner/pi/index.js +1 -1
- package/dist/runner/pi/rpc/client.d.ts +52 -0
- package/dist/runner/pi/rpc/client.js +114 -0
- package/dist/runner/pi/rpc/index.d.ts +4 -0
- package/dist/runner/pi/rpc/index.js +4 -0
- package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
- package/dist/runner/pi/rpc/protocol-types.js +21 -0
- package/dist/runner/pi/rpc/runner.d.ts +80 -0
- package/dist/runner/pi/rpc/runner.js +199 -0
- package/dist/runner/pi/rpc/translator.d.ts +55 -0
- package/dist/runner/pi/rpc/translator.js +258 -0
- package/dist/runner/registry.d.ts +7 -8
- package/dist/runner/registry.js +3 -4
- package/dist/runner/resolve-agent-choices.js +18 -59
- package/dist/runner/sync-agent-choices.js +16 -67
- package/dist/runner/types.d.ts +43 -13
- package/dist/session/claude/session-index.js +2 -3
- package/dist/session/claude/session-reader.js +3 -6
- package/dist/session/claude/sessions.js +6 -13
- package/dist/session/codex/rollout-reader.d.ts +2 -2
- package/dist/session/codex/rollout-reader.js +14 -17
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +2 -2
- package/dist/session/common/content-blocks.d.ts +7 -0
- package/dist/session/common/content-blocks.js +13 -9
- package/dist/session/common/jsonl.d.ts +8 -25
- package/dist/session/common/jsonl.js +126 -261
- package/dist/session/common/pagination.d.ts +28 -0
- package/dist/session/common/pagination.js +33 -0
- package/dist/session/kimi/sessions.d.ts +34 -1
- package/dist/session/kimi/sessions.js +208 -98
- package/dist/session/opencode/sessions.d.ts +17 -3
- package/dist/session/opencode/sessions.js +29 -29
- package/dist/session/pi/sessions.d.ts +5 -1
- package/dist/session/pi/sessions.js +12 -16
- package/dist/session/session-store.d.ts +11 -1
- package/dist/session/session-store.js +75 -178
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/install.d.ts +20 -2
- package/dist/update/install.js +51 -7
- package/package.json +1 -1
- package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
- package/dist/runner/codex/argv.d.ts +0 -33
- package/dist/runner/codex/argv.js +0 -40
- package/dist/runner/codex/factory.d.ts +0 -29
- package/dist/runner/codex/factory.js +0 -39
- package/dist/runner/codex/jsonl.d.ts +0 -37
- package/dist/runner/codex/jsonl.js +0 -221
- package/dist/runner/codex/runner.d.ts +0 -66
- package/dist/runner/codex/runner.js +0 -129
- package/dist/runner/common/exec-translator.d.ts +0 -73
- package/dist/runner/common/exec-translator.js +0 -105
- package/dist/runner/kimi/jsonl.d.ts +0 -28
- package/dist/runner/kimi/jsonl.js +0 -143
- package/dist/runner/kimi/runner.d.ts +0 -49
- package/dist/runner/kimi/runner.js +0 -105
- package/dist/runner/opencode/argv.d.ts +0 -32
- package/dist/runner/opencode/argv.js +0 -35
- package/dist/runner/opencode/jsonl.d.ts +0 -68
- package/dist/runner/opencode/jsonl.js +0 -295
- package/dist/runner/opencode/runner.d.ts +0 -61
- package/dist/runner/opencode/runner.js +0 -93
- package/dist/runner/pi/jsonl.d.ts +0 -128
- package/dist/runner/pi/jsonl.js +0 -249
- package/dist/runner/pi/runner.d.ts +0 -36
- package/dist/runner/pi/runner.js +0 -76
- package/dist/session/common/catch-up.d.ts +0 -12
- package/dist/session/common/catch-up.js +0 -31
|
@@ -5,11 +5,26 @@ import { readJsonlLines as readJsonlLinesShared, readLastNJsonlLines } from '../
|
|
|
5
5
|
import { UsageAccumulator } from '../common/usage-accumulator.js';
|
|
6
6
|
import { getLogger } from '../../logger/index.js';
|
|
7
7
|
import { STALE_MS } from '../common/constants.js';
|
|
8
|
-
import {
|
|
8
|
+
import { capEvents, paginate } from '../common/pagination.js';
|
|
9
|
+
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
10
|
+
import { truncateToolInput } from '../common/content-blocks.js';
|
|
9
11
|
/** Default Kimi config directory */
|
|
10
12
|
function defaultKimiDir() {
|
|
11
13
|
return path.join(os.homedir(), '.kimi-code');
|
|
12
14
|
}
|
|
15
|
+
/** union 含 catch-all 成员,普通 `===` 判窄不生效,用显式守卫收窄(替代 as unknown as)。 */
|
|
16
|
+
function isAppendLoopEvent(entry) {
|
|
17
|
+
return entry.type === 'context.append_loop_event';
|
|
18
|
+
}
|
|
19
|
+
function isUsageRecord(entry) {
|
|
20
|
+
return entry.type === 'usage.record';
|
|
21
|
+
}
|
|
22
|
+
function isTurnPrompt(entry) {
|
|
23
|
+
return entry.type === 'turn.prompt';
|
|
24
|
+
}
|
|
25
|
+
function isCompactionComplete(entry) {
|
|
26
|
+
return entry.type === 'full_compaction.complete' || entry.type === 'context.apply_compaction';
|
|
27
|
+
}
|
|
13
28
|
/**
|
|
14
29
|
* Max lines scanned backwards when looking for the last loop event.
|
|
15
30
|
* Bounds the tail scan so huge wire files are not fully parsed.
|
|
@@ -28,11 +43,9 @@ function lastLoopEventIsStepEnd(lines) {
|
|
|
28
43
|
for (let i = lines.length - 1, scanned = 0; i >= 0 && scanned < LOOP_EVENT_SCAN_LIMIT; i--, scanned++) {
|
|
29
44
|
try {
|
|
30
45
|
const entry = JSON.parse(lines[i]);
|
|
31
|
-
if (entry
|
|
46
|
+
if (!isAppendLoopEvent(entry))
|
|
32
47
|
continue;
|
|
33
|
-
|
|
34
|
-
const loopEvent = entry;
|
|
35
|
-
return loopEvent.event.type === 'step.end';
|
|
48
|
+
return entry.event.type === 'step.end';
|
|
36
49
|
}
|
|
37
50
|
catch {
|
|
38
51
|
// Skip malformed lines and keep scanning backwards
|
|
@@ -174,10 +187,9 @@ export class KimiSessionReader {
|
|
|
174
187
|
for (const line of lines) {
|
|
175
188
|
try {
|
|
176
189
|
const entry = JSON.parse(line);
|
|
177
|
-
if (entry
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
lastPrompt = prompt.input[0].text;
|
|
190
|
+
if (isTurnPrompt(entry)) {
|
|
191
|
+
if (entry.input && entry.input.length > 0) {
|
|
192
|
+
lastPrompt = entry.input[0].text;
|
|
181
193
|
}
|
|
182
194
|
}
|
|
183
195
|
}
|
|
@@ -253,20 +265,16 @@ export class KimiSessionReader {
|
|
|
253
265
|
}
|
|
254
266
|
sessions.push({
|
|
255
267
|
sessionId: entry.sessionId,
|
|
256
|
-
summary
|
|
268
|
+
summary,
|
|
257
269
|
mtime: mtimeMs,
|
|
258
270
|
});
|
|
259
271
|
}
|
|
260
272
|
// Sort by mtime descending; same-mtime ties use a deterministic
|
|
261
273
|
// secondary key so ordering is stable across scans.
|
|
262
274
|
sessions.sort((a, b) => b.mtime - a.mtime || a.sessionId.localeCompare(b.sessionId));
|
|
263
|
-
const total = sessions
|
|
264
|
-
const offset = Math.max(0, opts?.offset ?? 0);
|
|
265
|
-
// Default limit 20, aligned with the other agent session readers
|
|
266
|
-
// (claude/codex/opencode/pi); no opts -> return up to 20 sessions.
|
|
267
|
-
const limit = opts?.limit ?? 20;
|
|
275
|
+
const { items, total } = paginate(sessions, opts ?? {});
|
|
268
276
|
return {
|
|
269
|
-
sessions:
|
|
277
|
+
sessions: items,
|
|
270
278
|
total,
|
|
271
279
|
};
|
|
272
280
|
}
|
|
@@ -285,64 +293,8 @@ export class KimiSessionReader {
|
|
|
285
293
|
* Read session content (events + usage)
|
|
286
294
|
*/
|
|
287
295
|
readSessionContent(sessionId, cwd, opts) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const indexEntry = this.findSessionIndexEntry(sessionId);
|
|
291
|
-
const sessionDir = indexEntry?.sessionDir ?? null;
|
|
292
|
-
if (!sessionDir) {
|
|
293
|
-
return { events: [] };
|
|
294
|
-
}
|
|
295
|
-
const statePath = path.join(sessionDir, 'state.json');
|
|
296
|
-
let realCwd;
|
|
297
|
-
try {
|
|
298
|
-
realCwd = fs.realpathSync(cwd);
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
getLogger().warn(`[kimi-session-reader] fs.realpathSync failed for cwd=${cwd} (session ${sessionId}), returning empty`);
|
|
302
|
-
return { events: [] };
|
|
303
|
-
}
|
|
304
|
-
let cwdGuardPassed = false;
|
|
305
|
-
try {
|
|
306
|
-
if (fs.existsSync(statePath)) {
|
|
307
|
-
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
308
|
-
const guardResult = checkCwdGuard(state, realCwd);
|
|
309
|
-
if (guardResult === 'failed') {
|
|
310
|
-
return { events: [] };
|
|
311
|
-
}
|
|
312
|
-
if (guardResult === 'unverifiable') {
|
|
313
|
-
getLogger().warn(`[kimi-session-reader] state.json has no workDir/cwd field for session ${sessionId}, falling back to index workDir`);
|
|
314
|
-
// Fall through to index-based guard below
|
|
315
|
-
}
|
|
316
|
-
else {
|
|
317
|
-
// verified — state.json cwd matches
|
|
318
|
-
cwdGuardPassed = true;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
catch {
|
|
323
|
-
// state.json unparseable — fall through to index-based guard
|
|
324
|
-
getLogger().warn(`[kimi-session-reader] state.json parse failed for session ${sessionId}, falling back to index workDir`);
|
|
325
|
-
}
|
|
326
|
-
// Fallback: when state.json is missing, unparseable, or has no workDir/cwd,
|
|
327
|
-
// use the workDir from the session index entry as a secondary guard.
|
|
328
|
-
// This prevents /resume <id> from accessing a session belonging to a
|
|
329
|
-
// different workspace when state.json is unavailable.
|
|
330
|
-
if (!cwdGuardPassed) {
|
|
331
|
-
const indexWorkDir = indexEntry?.workDir;
|
|
332
|
-
if (indexWorkDir && indexWorkDir !== realCwd) {
|
|
333
|
-
return { events: [] };
|
|
334
|
-
}
|
|
335
|
-
// v2 sessions have empty index workDir (v1-only field) and may lack
|
|
336
|
-
// state.json cwd. When no cwd source can verify the session belongs to
|
|
337
|
-
// the requested workspace, fail-closed to prevent cross-workspace access
|
|
338
|
-
// (aligned with claude's fail-closed cwd guard).
|
|
339
|
-
if (!indexWorkDir) {
|
|
340
|
-
getLogger().warn(`[kimi-session-reader] no cwd source (state.json + index) for session ${sessionId}, rejecting (fail-closed)`);
|
|
341
|
-
return { events: [] };
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
345
|
-
if (!fs.existsSync(wirePath)) {
|
|
296
|
+
const wirePath = this.resolveSessionWirePath(sessionId, cwd);
|
|
297
|
+
if (wirePath === null) {
|
|
346
298
|
return { events: [] };
|
|
347
299
|
}
|
|
348
300
|
try {
|
|
@@ -355,6 +307,9 @@ export class KimiSessionReader {
|
|
|
355
307
|
// (subagent context is an independent window). Tracked separately
|
|
356
308
|
// from the accumulator's `last` (which could be a subagent record).
|
|
357
309
|
let mainContextLength = 0;
|
|
310
|
+
// §6.3: Compaction stats — count compact events, track last pre-compact context.
|
|
311
|
+
let compactCount = 0;
|
|
312
|
+
let compactPreContextLength;
|
|
358
313
|
// Distinguish "no usage.record in any wire file" (no data → usage:
|
|
359
314
|
// undefined, downstream omits the token block) from "records exist but
|
|
360
315
|
// are all zero / malformed" (real data → aggregate with ?? 0 and return
|
|
@@ -393,9 +348,22 @@ export class KimiSessionReader {
|
|
|
393
348
|
try {
|
|
394
349
|
const entry = JSON.parse(line);
|
|
395
350
|
// Extract usage from usage.record.
|
|
396
|
-
if (entry
|
|
397
|
-
|
|
398
|
-
|
|
351
|
+
if (isUsageRecord(entry)) {
|
|
352
|
+
accumulateUsage(entry.usage, true);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
// §6.3: Count compaction events and track pre-compact context length.
|
|
356
|
+
// 展示计数只计 context.apply_compaction:一次手动 /compact 会同时写
|
|
357
|
+
// full_compaction.complete + context.apply_compaction 两条(2026-08-16
|
|
358
|
+
// 实弹 wire.jsonl),双计会把一次压缩虚报成 2 次。full_compaction.complete
|
|
359
|
+
// 仅作 R2 runCompact 轮询的完成信号(readCompactionRecords 仍返回两种)。
|
|
360
|
+
if (isCompactionComplete(entry) && entry.type === 'context.apply_compaction') {
|
|
361
|
+
compactCount++;
|
|
362
|
+
// Capture context length before this compaction as the pre-compact
|
|
363
|
+
// watermark (the most recent one is most relevant for display).
|
|
364
|
+
if (entry.tokensBefore !== undefined) {
|
|
365
|
+
compactPreContextLength = entry.tokensBefore;
|
|
366
|
+
}
|
|
399
367
|
continue;
|
|
400
368
|
}
|
|
401
369
|
// P2-7: displayTitle + display events now scan ALL lines (not a
|
|
@@ -407,17 +375,15 @@ export class KimiSessionReader {
|
|
|
407
375
|
// last turn.prompt across the whole session (the most recent user
|
|
408
376
|
// prompt), which is the correct catch-up title.
|
|
409
377
|
// Extract display title from turn.prompt
|
|
410
|
-
if (entry
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
displayTitle = prompt.input[0].text.substring(0, 100);
|
|
378
|
+
if (isTurnPrompt(entry)) {
|
|
379
|
+
if (entry.input && entry.input.length > 0) {
|
|
380
|
+
displayTitle = entry.input[0].text;
|
|
414
381
|
}
|
|
415
382
|
continue;
|
|
416
383
|
}
|
|
417
384
|
// Convert loop events to content events
|
|
418
|
-
if (entry
|
|
419
|
-
const
|
|
420
|
-
const evt = loopEvent.event;
|
|
385
|
+
if (isAppendLoopEvent(entry)) {
|
|
386
|
+
const evt = entry.event;
|
|
421
387
|
if (evt.type === 'content.part' && evt.part) {
|
|
422
388
|
if (evt.part.type === 'think') {
|
|
423
389
|
// Skip thinking blocks for display
|
|
@@ -431,13 +397,10 @@ export class KimiSessionReader {
|
|
|
431
397
|
}
|
|
432
398
|
}
|
|
433
399
|
else if (evt.type === 'tool.call' && evt.name) {
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
if (inputStr.length > 200) {
|
|
439
|
-
inputStr = inputStr.replace(/\n/g, ' ').slice(0, 197) + '...';
|
|
440
|
-
}
|
|
400
|
+
// 复用 content-blocks 的 200 字符截断:大 args(整段文件内容
|
|
401
|
+
// 写入、长 prompt)原文进事件会把 resume 卡顶到 28KB 预算之外
|
|
402
|
+
// (2026-08-08 extreme_fallback 故障的推手之一)。
|
|
403
|
+
const inputStr = truncateToolInput(JSON.stringify(evt.args ?? {}));
|
|
441
404
|
events.push({
|
|
442
405
|
type: 'tool_use',
|
|
443
406
|
content: `${evt.name}(${inputStr})`,
|
|
@@ -466,7 +429,9 @@ export class KimiSessionReader {
|
|
|
466
429
|
// contextLength stays main-only (subagent context is an independent
|
|
467
430
|
// window). Display events/title also stay main-only. Any unreadable
|
|
468
431
|
// directory or missing/corrupt wire file is skipped silently.
|
|
469
|
-
|
|
432
|
+
// wirePath = <sessionDir>/agents/main/wire.jsonl → agents dir is two
|
|
433
|
+
// levels up (siblings of main hold subagent wires).
|
|
434
|
+
const agentsDir = path.dirname(path.dirname(wirePath));
|
|
470
435
|
try {
|
|
471
436
|
for (const dirent of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
472
437
|
if (!dirent.isDirectory() || dirent.name === 'main') {
|
|
@@ -479,9 +444,8 @@ export class KimiSessionReader {
|
|
|
479
444
|
for (const subLine of readJsonlLines(subWirePath)) {
|
|
480
445
|
try {
|
|
481
446
|
const subEntry = JSON.parse(subLine);
|
|
482
|
-
if (subEntry
|
|
483
|
-
|
|
484
|
-
accumulateUsage(usageEntry.usage, false);
|
|
447
|
+
if (isUsageRecord(subEntry)) {
|
|
448
|
+
accumulateUsage(subEntry.usage, false);
|
|
485
449
|
}
|
|
486
450
|
}
|
|
487
451
|
catch {
|
|
@@ -510,9 +474,12 @@ export class KimiSessionReader {
|
|
|
510
474
|
cumulativeOutputTokens: t.output,
|
|
511
475
|
cumulativeCacheReadTokens: t.cacheRead > 0 ? t.cacheRead : 0,
|
|
512
476
|
cumulativeCacheCreationTokens: t.cacheCreation > 0 ? t.cacheCreation : 0,
|
|
477
|
+
// §6.3: compaction stats from wire.jsonl
|
|
478
|
+
compactCount,
|
|
479
|
+
...(compactPreContextLength !== undefined ? { compactPreContextLength } : {}),
|
|
513
480
|
};
|
|
514
481
|
return {
|
|
515
|
-
events:
|
|
482
|
+
events: capEvents(events, maxEvents),
|
|
516
483
|
usage: hasUsageRecord ? totalUsage : undefined,
|
|
517
484
|
displayTitle,
|
|
518
485
|
};
|
|
@@ -522,14 +489,157 @@ export class KimiSessionReader {
|
|
|
522
489
|
return { events: [] };
|
|
523
490
|
}
|
|
524
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* Read the compaction records (context.apply_compaction /
|
|
494
|
+
* full_compaction.complete) from a session's wire.jsonl.
|
|
495
|
+
*
|
|
496
|
+
* Same cwd guard as readSessionContent. Used by KimiAcpRunner.runCompact
|
|
497
|
+
* (R2) to wait for the background compaction to land before ending the run.
|
|
498
|
+
*/
|
|
499
|
+
readCompactionRecords(sessionId, cwd) {
|
|
500
|
+
const wirePath = this.resolveSessionWirePath(sessionId, cwd);
|
|
501
|
+
if (wirePath === null) {
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
504
|
+
const records = [];
|
|
505
|
+
try {
|
|
506
|
+
for (const line of readJsonlLines(wirePath)) {
|
|
507
|
+
try {
|
|
508
|
+
const entry = JSON.parse(line);
|
|
509
|
+
if (isCompactionComplete(entry)) {
|
|
510
|
+
records.push({
|
|
511
|
+
type: entry.type,
|
|
512
|
+
...(entry.compactedCount !== undefined
|
|
513
|
+
? { compactedCount: entry.compactedCount }
|
|
514
|
+
: {}),
|
|
515
|
+
...(entry.tokensBefore !== undefined ? { tokensBefore: entry.tokensBefore } : {}),
|
|
516
|
+
...(entry.tokensAfter !== undefined ? { tokensAfter: entry.tokensAfter } : {}),
|
|
517
|
+
time: entry.time,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
getLogger().error(`[kimi-session-reader] readCompactionRecords error: ${error}`);
|
|
528
|
+
return [];
|
|
529
|
+
}
|
|
530
|
+
return records;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Resolve the main wire.jsonl path for a session after passing the cwd
|
|
534
|
+
* guard (state.json + session index fallback). Returns null when the
|
|
535
|
+
* session is unknown, fails the guard, or has no wire file.
|
|
536
|
+
*/
|
|
537
|
+
resolveSessionWirePath(sessionId, cwd) {
|
|
538
|
+
// Find session directory from index (also retrieve index entry for
|
|
539
|
+
// fallback cwd guard when state.json is missing or unparseable)
|
|
540
|
+
const indexEntry = this.findSessionIndexEntry(sessionId);
|
|
541
|
+
const sessionDir = indexEntry?.sessionDir ?? null;
|
|
542
|
+
if (!sessionDir) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
const statePath = path.join(sessionDir, 'state.json');
|
|
546
|
+
let realCwd;
|
|
547
|
+
try {
|
|
548
|
+
realCwd = fs.realpathSync(cwd);
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
getLogger().warn(`[kimi-session-reader] fs.realpathSync failed for cwd=${cwd} (session ${sessionId}), returning empty`);
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
let cwdGuardPassed = false;
|
|
555
|
+
try {
|
|
556
|
+
if (fs.existsSync(statePath)) {
|
|
557
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
558
|
+
const guardResult = checkCwdGuard(state, realCwd);
|
|
559
|
+
if (guardResult === 'failed') {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
if (guardResult === 'unverifiable') {
|
|
563
|
+
getLogger().warn(`[kimi-session-reader] state.json has no workDir/cwd field for session ${sessionId}, falling back to index workDir`);
|
|
564
|
+
// Fall through to index-based guard below
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
// verified — state.json cwd matches
|
|
568
|
+
cwdGuardPassed = true;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
// state.json unparseable — fall through to index-based guard
|
|
574
|
+
getLogger().warn(`[kimi-session-reader] state.json parse failed for session ${sessionId}, falling back to index workDir`);
|
|
575
|
+
}
|
|
576
|
+
// Fallback: when state.json is missing, unparseable, or has no workDir/cwd,
|
|
577
|
+
// use the workDir from the session index entry as a secondary guard.
|
|
578
|
+
// This prevents /resume <id> from accessing a session belonging to a
|
|
579
|
+
// different workspace when state.json is unavailable.
|
|
580
|
+
if (!cwdGuardPassed) {
|
|
581
|
+
const indexWorkDir = indexEntry?.workDir;
|
|
582
|
+
if (indexWorkDir && indexWorkDir !== realCwd) {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
// v2 sessions have empty index workDir (v1-only field) and may lack
|
|
586
|
+
// state.json cwd. When no cwd source can verify the session belongs to
|
|
587
|
+
// the requested workspace, fail-closed to prevent cross-workspace access
|
|
588
|
+
// (aligned with claude's fail-closed cwd guard).
|
|
589
|
+
if (!indexWorkDir) {
|
|
590
|
+
getLogger().warn(`[kimi-session-reader] no cwd source (state.json + index) for session ${sessionId}, rejecting (fail-closed)`);
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
595
|
+
if (!fs.existsSync(wirePath)) {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
return wirePath;
|
|
599
|
+
}
|
|
525
600
|
/**
|
|
526
601
|
* Check if a session is currently active
|
|
527
602
|
*/
|
|
528
|
-
isSessionActive(sessionId,
|
|
529
|
-
const
|
|
603
|
+
isSessionActive(sessionId, cwd) {
|
|
604
|
+
const indexEntry = this.findSessionIndexEntry(sessionId);
|
|
605
|
+
const sessionDir = indexEntry?.sessionDir;
|
|
530
606
|
if (!sessionDir) {
|
|
531
607
|
return false;
|
|
532
608
|
}
|
|
609
|
+
// Cwd guard: the session's working directory must match the requested
|
|
610
|
+
// cwd (align with claude/pi/opencode). Mirrors readSessionContent's
|
|
611
|
+
// fail-closed cwd check (state.json workDir/cwd, then index workDir).
|
|
612
|
+
let realCwd;
|
|
613
|
+
try {
|
|
614
|
+
realCwd = fs.realpathSync(cwd);
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
let cwdGuardPassed = false;
|
|
620
|
+
const statePath = path.join(sessionDir, 'state.json');
|
|
621
|
+
try {
|
|
622
|
+
if (fs.existsSync(statePath)) {
|
|
623
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
624
|
+
const guardResult = checkCwdGuard(state, realCwd);
|
|
625
|
+
if (guardResult === 'failed')
|
|
626
|
+
return false;
|
|
627
|
+
if (guardResult === 'verified')
|
|
628
|
+
cwdGuardPassed = true;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
// state.json unparseable — fall through to index-based guard
|
|
633
|
+
}
|
|
634
|
+
if (!cwdGuardPassed) {
|
|
635
|
+
const indexWorkDir = indexEntry?.workDir;
|
|
636
|
+
if (indexWorkDir && indexWorkDir !== realCwd)
|
|
637
|
+
return false;
|
|
638
|
+
// v2 sessions have empty index workDir (v1-only field) and may lack
|
|
639
|
+
// state.json cwd — fail-closed to prevent cross-workspace access.
|
|
640
|
+
if (!indexWorkDir)
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
533
643
|
const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
534
644
|
if (!fs.existsSync(wirePath)) {
|
|
535
645
|
return false;
|
|
@@ -24,13 +24,25 @@ interface OpencodeSessionReaderOptions {
|
|
|
24
24
|
*/
|
|
25
25
|
captureExport?: (sessionId: string) => string;
|
|
26
26
|
}
|
|
27
|
+
interface OpencodeSessionListEntry {
|
|
28
|
+
id: string;
|
|
29
|
+
title: string;
|
|
30
|
+
updated: number;
|
|
31
|
+
created: number;
|
|
32
|
+
projectId: string;
|
|
33
|
+
directory: string;
|
|
34
|
+
}
|
|
27
35
|
export declare class OpencodeSessionReader implements AgentSessionReader {
|
|
28
36
|
private readonly binary;
|
|
29
37
|
private readonly cacheTtlMs;
|
|
30
38
|
/** Optional raw-export capture override (testing / alt transport). */
|
|
31
39
|
private readonly captureExportOverride?;
|
|
32
40
|
/** Cache keyed by cwd - each cwd has its own cache entry */
|
|
33
|
-
|
|
41
|
+
/** cwd → 列表缓存(public:测试直接断言缓存形状,替代 as unknown as)。 */
|
|
42
|
+
listCache: Map<string, {
|
|
43
|
+
ts: number;
|
|
44
|
+
data: OpencodeSessionListEntry[];
|
|
45
|
+
}>;
|
|
34
46
|
constructor(opts?: OpencodeSessionReaderOptions);
|
|
35
47
|
/**
|
|
36
48
|
* List sessions in a given cwd, using directory field for matching.
|
|
@@ -59,14 +71,16 @@ export declare class OpencodeSessionReader implements AgentSessionReader {
|
|
|
59
71
|
* Passes cwd to fetchSessionList so it returns sessions for the correct directory.
|
|
60
72
|
*/
|
|
61
73
|
isSessionActive(sessionId: string, cwd: string): boolean;
|
|
62
|
-
|
|
74
|
+
/** realpath 归一化(public:测试直接调用,替代 as unknown as)。 */
|
|
75
|
+
realpath(cwd: string): string;
|
|
63
76
|
private fetchSessionList;
|
|
64
77
|
/**
|
|
65
78
|
* Capture raw `opencode export <sessionId>` output (JSON string). Injectable
|
|
66
79
|
* via the constructor; the default routes stdout to a temp file fd to bypass
|
|
67
80
|
* opencode's ~128KB pipe truncation on large sessions (L1).
|
|
68
81
|
*/
|
|
69
|
-
|
|
82
|
+
/** 导出会话内容(public:测试直接调用,替代 as unknown as)。 */
|
|
83
|
+
captureExport(sessionId: string): string;
|
|
70
84
|
/**
|
|
71
85
|
* L1 default transport: run `opencode export` with stdout pointed at a
|
|
72
86
|
* regular-file fd (NOT a pipe). opencode truncates piped stdout at ~128KB for
|
|
@@ -15,17 +15,20 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { execFileSync } from 'node:child_process';
|
|
17
17
|
import fs from 'node:fs';
|
|
18
|
+
import { silentlyUnlink } from '../../common/fs.js';
|
|
18
19
|
import os from 'node:os';
|
|
19
20
|
import path from 'node:path';
|
|
20
|
-
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../
|
|
21
|
+
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
21
22
|
import { getLogger } from '../../logger/index.js';
|
|
22
23
|
import { STALE_MS } from '../common/constants.js';
|
|
24
|
+
import { paginate, capEvents } from '../common/pagination.js';
|
|
23
25
|
export class OpencodeSessionReader {
|
|
24
26
|
binary;
|
|
25
27
|
cacheTtlMs;
|
|
26
28
|
/** Optional raw-export capture override (testing / alt transport). */
|
|
27
29
|
captureExportOverride;
|
|
28
30
|
/** Cache keyed by cwd - each cwd has its own cache entry */
|
|
31
|
+
/** cwd → 列表缓存(public:测试直接断言缓存形状,替代 as unknown as)。 */
|
|
29
32
|
listCache = new Map();
|
|
30
33
|
constructor(opts = {}) {
|
|
31
34
|
this.binary = 'opencode';
|
|
@@ -46,12 +49,9 @@ export class OpencodeSessionReader {
|
|
|
46
49
|
// secondary key so CLI/list-cache rebuilds never reorder the page.
|
|
47
50
|
filtered.sort((a, b) => b.updated - a.updated || a.id.localeCompare(b.id));
|
|
48
51
|
// Compute total over the full sorted set, then paginate.
|
|
49
|
-
const total = filtered
|
|
50
|
-
const offset = Math.max(0, opts?.offset ?? 0);
|
|
51
|
-
const limit = opts?.limit;
|
|
52
|
-
const page = limit === undefined ? filtered.slice(offset) : filtered.slice(offset, offset + limit);
|
|
52
|
+
const { items, total } = paginate(filtered, opts ?? {});
|
|
53
53
|
return {
|
|
54
|
-
sessions:
|
|
54
|
+
sessions: items.map((e) => ({
|
|
55
55
|
sessionId: e.id,
|
|
56
56
|
// CLI title 字段形状不保证恒为 string(null/缺字段时 router 的
|
|
57
57
|
// s.summary.trim() 会 TypeError)——非 string 给空串(Review R3 P3-3)。
|
|
@@ -105,8 +105,7 @@ export class OpencodeSessionReader {
|
|
|
105
105
|
let aiTitle;
|
|
106
106
|
let displayTitle;
|
|
107
107
|
// Find the last user message position (for catch-up tail).
|
|
108
|
-
// messages are already parsed objects — scan directly
|
|
109
|
-
// stringify→findLastUserIndex→parse round-trip (review P2-36).
|
|
108
|
+
// messages are already parsed objects — scan directly.
|
|
110
109
|
let lastUserIndex = -1;
|
|
111
110
|
for (let i = 0; i < data.messages.length; i++) {
|
|
112
111
|
if (data.messages[i].info.role === 'user') {
|
|
@@ -172,21 +171,28 @@ export class OpencodeSessionReader {
|
|
|
172
171
|
// Track usage from step-finish parts
|
|
173
172
|
for (const part of msg.parts) {
|
|
174
173
|
if (part.type === 'step-finish' && part.tokens) {
|
|
175
|
-
// Use the last step-finish tokens
|
|
174
|
+
// Use the last NON-EMPTY step-finish tokens. A long autonomous run
|
|
175
|
+
// can end on a degenerate step (model cut off mid-reasoning) whose
|
|
176
|
+
// step-finish reports all-zero tokens; trusting it would wipe out
|
|
177
|
+
// the run's real per-turn usage (contextLength/input/output/total
|
|
178
|
+
// all render 0). Skip zero-total steps so per-turn usage reflects
|
|
179
|
+
// the last step that actually consumed tokens.
|
|
176
180
|
// ccusage-aligned: cache.write -> cacheCreationTokens.
|
|
177
181
|
// contextLength = input + cacheRead + cacheCreation (excludes
|
|
178
182
|
// output/reasoning) — the unified context-window occupancy
|
|
179
183
|
// contract across all five readers (review P2-8).
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
if ((part.tokens.total ?? 0) !== 0) {
|
|
185
|
+
usage = {
|
|
186
|
+
inputTokens: part.tokens.input,
|
|
187
|
+
outputTokens: part.tokens.output,
|
|
188
|
+
contextLength: part.tokens.input +
|
|
189
|
+
(part.tokens.cache?.read ?? 0) +
|
|
190
|
+
(part.tokens.cache?.write ?? 0),
|
|
191
|
+
cacheReadTokens: part.tokens.cache?.read,
|
|
192
|
+
cacheCreationTokens: part.tokens.cache?.write,
|
|
193
|
+
totalTokens: part.tokens.total,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
}
|
|
@@ -251,12 +257,7 @@ export class OpencodeSessionReader {
|
|
|
251
257
|
// Apply maxEvents cap: keep the LAST N events (most recent), matching
|
|
252
258
|
// CodexSessionReader's slice(-maxEvents). maxEvents limits the catch-up
|
|
253
259
|
// tail so auto-resume cards don't load the entire session.
|
|
254
|
-
|
|
255
|
-
const cappedEvents = opts?.maxEvents !== undefined
|
|
256
|
-
? opts.maxEvents <= 0
|
|
257
|
-
? []
|
|
258
|
-
: events.slice(-opts.maxEvents)
|
|
259
|
-
: events;
|
|
260
|
+
const cappedEvents = capEvents(events, opts?.maxEvents);
|
|
260
261
|
return {
|
|
261
262
|
events: cappedEvents,
|
|
262
263
|
usage,
|
|
@@ -289,6 +290,7 @@ export class OpencodeSessionReader {
|
|
|
289
290
|
}
|
|
290
291
|
}
|
|
291
292
|
// --- Private helpers ---
|
|
293
|
+
/** realpath 归一化(public:测试直接调用,替代 as unknown as)。 */
|
|
292
294
|
realpath(cwd) {
|
|
293
295
|
try {
|
|
294
296
|
return fs.realpathSync(cwd);
|
|
@@ -343,6 +345,7 @@ export class OpencodeSessionReader {
|
|
|
343
345
|
* via the constructor; the default routes stdout to a temp file fd to bypass
|
|
344
346
|
* opencode's ~128KB pipe truncation on large sessions (L1).
|
|
345
347
|
*/
|
|
348
|
+
/** 导出会话内容(public:测试直接调用,替代 as unknown as)。 */
|
|
346
349
|
captureExport(sessionId) {
|
|
347
350
|
if (this.captureExportOverride)
|
|
348
351
|
return this.captureExportOverride(sessionId);
|
|
@@ -378,10 +381,7 @@ export class OpencodeSessionReader {
|
|
|
378
381
|
}
|
|
379
382
|
catch { }
|
|
380
383
|
}
|
|
381
|
-
|
|
382
|
-
fs.unlinkSync(tmp);
|
|
383
|
-
}
|
|
384
|
-
catch { }
|
|
384
|
+
silentlyUnlink(tmp);
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
387
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { type ContentBlockMapping } from '../common/content-blocks.js';
|
|
1
2
|
import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
|
|
3
|
+
/** Pi-specific field-name mapping for content block extraction. */
|
|
4
|
+
export declare const PI_MAPPING: ContentBlockMapping;
|
|
2
5
|
/**
|
|
3
6
|
* PiSessionReader: reads pi agent sessions from `~/.pi/agent/`.
|
|
4
7
|
*
|
|
@@ -7,7 +10,8 @@ import type { AgentSession, AgentSessionReader, SessionContent } from '../../run
|
|
|
7
10
|
*/
|
|
8
11
|
export declare class PiSessionReader implements AgentSessionReader {
|
|
9
12
|
private readonly piDir;
|
|
10
|
-
|
|
13
|
+
/** 会话目录(public:测试可重定向到 fixture 目录,替代 as any)。 */
|
|
14
|
+
sessionsDir: string;
|
|
11
15
|
constructor(opts?: {
|
|
12
16
|
piDir?: string;
|
|
13
17
|
});
|