lark-remote 0.1.12 → 0.2.0
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 +37 -0
- package/README.en.md +2 -6
- package/README.md +3 -7
- package/dist/bridge/final-usage.d.ts +24 -0
- package/dist/bridge/final-usage.js +55 -0
- package/dist/bridge/inbound-media.d.ts +0 -3
- package/dist/bridge/inbound-media.js +0 -1
- package/dist/bridge/index.d.ts +7 -7
- package/dist/bridge/index.js +100 -133
- package/dist/bridge/queue-manager.js +4 -3
- package/dist/card/approval-render.js +39 -94
- package/dist/card/bash-renderer.js +12 -21
- package/dist/card/card-shared.d.ts +14 -5
- package/dist/card/card-shared.js +23 -34
- package/dist/card/run-renderer.d.ts +0 -22
- package/dist/card/run-renderer.js +206 -153
- package/dist/card/run-state.d.ts +29 -1
- package/dist/card/run-state.js +172 -84
- package/dist/card/text-truncate.d.ts +0 -9
- package/dist/card/text-truncate.js +0 -9
- package/dist/card/tool-render.d.ts +4 -1
- package/dist/card/tool-render.js +76 -32
- package/dist/cli.js +2 -2
- package/dist/common/fs.d.ts +14 -0
- package/dist/common/fs.js +57 -3
- package/dist/common/truncate.d.ts +0 -6
- package/dist/common/truncate.js +1 -1
- package/dist/common/ttl-cache.d.ts +24 -0
- package/dist/common/ttl-cache.js +53 -0
- package/dist/config/codex-config.js +14 -11
- package/dist/config/index.d.ts +0 -8
- package/dist/config/index.js +0 -16
- package/dist/config/kimi-config.d.ts +1 -1
- package/dist/config/kimi-config.js +2 -1
- package/dist/config/opencode-config.d.ts +1 -1
- package/dist/config/opencode-config.js +16 -10
- package/dist/config/qr.d.ts +36 -0
- package/dist/config/qr.js +124 -0
- package/dist/config/wizard.js +69 -27
- package/dist/connector/index.d.ts +3 -2
- package/dist/connector/index.js +5 -3
- package/dist/index.js +14 -41
- package/dist/instance-lock.js +7 -0
- package/dist/persistence/atomic-write.d.ts +2 -0
- package/dist/persistence/atomic-write.js +20 -5
- package/dist/persistence/load-json-file.d.ts +1 -2
- package/dist/persistence/load-json-file.js +2 -3
- package/dist/platform/agent-stopper.d.ts +22 -0
- package/dist/platform/agent-stopper.js +19 -0
- package/dist/platform/command.d.ts +11 -0
- package/dist/platform/command.js +107 -0
- package/dist/platform/identity.d.ts +33 -0
- package/dist/platform/identity.js +187 -0
- package/dist/platform/path.d.ts +24 -0
- package/dist/platform/path.js +110 -0
- package/dist/platform/select.d.ts +9 -0
- package/dist/platform/select.js +12 -0
- package/dist/platform/shell.d.ts +51 -0
- package/dist/platform/shell.js +75 -0
- package/dist/platform/spawn.d.ts +41 -0
- package/dist/platform/spawn.js +77 -0
- package/dist/platform/terminator-posix.d.ts +38 -0
- package/dist/platform/terminator-posix.js +103 -0
- package/dist/platform/terminator-win32.d.ts +21 -0
- package/dist/platform/terminator-win32.js +110 -0
- package/dist/platform/terminator.d.ts +44 -0
- package/dist/platform/terminator.js +24 -0
- package/dist/platform/types.d.ts +23 -0
- package/dist/platform/types.js +5 -0
- package/dist/restart.js +4 -2
- package/dist/router/card-helpers.d.ts +33 -6
- package/dist/router/card-helpers.js +67 -11
- package/dist/router/config/common/render.d.ts +1 -1
- package/dist/router/config/common/render.js +1 -1
- package/dist/router/config-diff.d.ts +4 -0
- package/dist/router/config-diff.js +70 -0
- package/dist/router/index.d.ts +73 -6
- package/dist/router/index.js +663 -590
- package/dist/router/utils.d.ts +6 -0
- package/dist/router/utils.js +12 -0
- package/dist/runner/bash/runner.d.ts +9 -2
- package/dist/runner/bash/runner.js +45 -20
- package/dist/runner/claude/session.d.ts +4 -4
- package/dist/runner/claude/session.js +18 -9
- package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
- package/dist/runner/codex/app-server/runner.d.ts +1 -16
- package/dist/runner/codex/app-server/runner.js +2 -2
- package/dist/runner/codex/app-server/translator.d.ts +18 -0
- package/dist/runner/codex/app-server/translator.js +146 -0
- package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
- package/dist/runner/common/acp/base-acp-runner.js +248 -0
- package/dist/runner/common/acp/base-translator.d.ts +141 -0
- package/dist/runner/common/acp/base-translator.js +271 -0
- package/dist/runner/common/acp/protocol-helpers.d.ts +70 -0
- package/dist/runner/common/acp/protocol-helpers.js +82 -0
- package/dist/runner/common/connection-based-runner.d.ts +0 -2
- package/dist/runner/common/connection-based-runner.js +1 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
- package/dist/runner/common/jsonrpc/transport.js +25 -5
- package/dist/runner/common/process-stopper.d.ts +4 -26
- package/dist/runner/common/process-stopper.js +4 -76
- package/dist/runner/common/runner-utils.d.ts +0 -5
- package/dist/runner/common/runner-utils.js +0 -7
- package/dist/runner/common/spawning-runner.d.ts +42 -132
- package/dist/runner/common/spawning-runner.js +52 -271
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/dsh/index.d.ts +0 -2
- package/dist/runner/dsh/index.js +0 -2
- package/dist/runner/dsh/runner.d.ts +0 -2
- package/dist/runner/dsh/runner.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +63 -53
- package/dist/runner/kimi/acp/runner.js +189 -334
- package/dist/runner/kimi/acp/translator.d.ts +18 -101
- package/dist/runner/kimi/acp/translator.js +61 -255
- package/dist/runner/opencode/acp/runner.d.ts +22 -46
- package/dist/runner/opencode/acp/runner.js +74 -297
- package/dist/runner/opencode/acp/translator.d.ts +28 -104
- package/dist/runner/opencode/acp/translator.js +54 -249
- package/dist/runner/pi/rpc/index.d.ts +0 -2
- package/dist/runner/pi/rpc/index.js +0 -2
- package/dist/runner/pi/rpc/translator.d.ts +3 -0
- package/dist/runner/pi/rpc/translator.js +47 -9
- package/dist/runner/probe.d.ts +8 -11
- package/dist/runner/probe.js +17 -21
- package/dist/runner/registry.d.ts +4 -4
- package/dist/runner/registry.js +4 -4
- package/dist/runner/resolve-agent-choices.js +6 -2
- package/dist/runner/sync-agent-choices.js +1 -1
- package/dist/runner/types.d.ts +38 -1
- package/dist/session/claude/session-index.js +3 -1
- package/dist/session/claude/sessions.js +37 -64
- package/dist/session/codex/rollout-reader.d.ts +10 -14
- package/dist/session/codex/rollout-reader.js +126 -48
- package/dist/session/common/constants.d.ts +7 -0
- package/dist/session/common/constants.js +9 -0
- package/dist/session/common/pagination.d.ts +0 -2
- package/dist/session/common/pagination.js +1 -1
- package/dist/session/common/recency.d.ts +7 -0
- package/dist/session/common/recency.js +10 -0
- package/dist/session/common/two-pass.d.ts +22 -0
- package/dist/session/common/two-pass.js +70 -0
- package/dist/session/common/usage-accumulator.d.ts +14 -0
- package/dist/session/common/usage-accumulator.js +15 -0
- package/dist/session/dsh/sessions.js +15 -11
- package/dist/session/kimi/sessions.d.ts +24 -4
- package/dist/session/kimi/sessions.js +99 -103
- package/dist/session/opencode/sessions.d.ts +2 -14
- package/dist/session/opencode/sessions.js +35 -15
- package/dist/session/pi/sessions.js +64 -95
- package/dist/update/index.d.ts +2 -3
- package/dist/update/index.js +1 -2
- package/dist/update/install.d.ts +2 -1
- package/dist/update/install.js +58 -9
- package/dist/update/version-check.d.ts +5 -0
- package/dist/update/version-check.js +9 -0
- package/package.json +9 -7
- package/dist/update/startup-hint.d.ts +0 -5
- package/dist/update/startup-hint.js +0 -11
|
@@ -2,10 +2,13 @@ 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 { UsageAccumulator } from '../common/usage-accumulator.js';
|
|
5
|
+
import { UsageAccumulator, cumulativeUsageFields } from '../common/usage-accumulator.js';
|
|
6
6
|
import { getLogger } from '../../logger/index.js';
|
|
7
|
-
import {
|
|
7
|
+
import { isStale } from '../common/constants.js';
|
|
8
|
+
import { samePath } from '../../platform/path.js';
|
|
8
9
|
import { capEvents, paginate } from '../common/pagination.js';
|
|
10
|
+
import { sortByRecencyDesc } from '../common/recency.js';
|
|
11
|
+
import { TtlCache } from '../../common/ttl-cache.js';
|
|
9
12
|
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
10
13
|
import { truncateToolInput } from '../common/content-blocks.js';
|
|
11
14
|
/** Default Kimi config directory */
|
|
@@ -25,6 +28,16 @@ function isTurnPrompt(entry) {
|
|
|
25
28
|
function isCompactionComplete(entry) {
|
|
26
29
|
return entry.type === 'full_compaction.complete' || entry.type === 'context.apply_compaction';
|
|
27
30
|
}
|
|
31
|
+
/** full_compaction.begin:压缩开始(手动 /compact 或自动触发都写)。 */
|
|
32
|
+
function isCompactionBegin(entry) {
|
|
33
|
+
return entry.type === 'full_compaction.begin';
|
|
34
|
+
}
|
|
35
|
+
/** 压缩终态:完成(complete/apply)或失败/取消(cancel)共用 cancel 记录。 */
|
|
36
|
+
function isCompactionTerminal(entry) {
|
|
37
|
+
return (entry.type === 'full_compaction.complete' ||
|
|
38
|
+
entry.type === 'context.apply_compaction' ||
|
|
39
|
+
entry.type === 'full_compaction.cancel');
|
|
40
|
+
}
|
|
28
41
|
/**
|
|
29
42
|
* Max lines scanned backwards when looking for the last loop event.
|
|
30
43
|
* Bounds the tail scan so huge wire files are not fully parsed.
|
|
@@ -55,10 +68,7 @@ function lastLoopEventIsStepEnd(lines) {
|
|
|
55
68
|
return false;
|
|
56
69
|
}
|
|
57
70
|
/** Cache for JSONL file contents to avoid repeated full reads */
|
|
58
|
-
const jsonlCache = new
|
|
59
|
-
const CACHE_TTL_MS = 5000; // 5 second cache TTL
|
|
60
|
-
/** Upper bound on cached files; evict the least-recently-used entry past this. */
|
|
61
|
-
const CACHE_MAX_ENTRIES = 32;
|
|
71
|
+
const jsonlCache = new TtlCache(5_000, 32);
|
|
62
72
|
/**
|
|
63
73
|
* Read lines from a JSONL file with caching.
|
|
64
74
|
* Delegates to the shared readJsonlLines for the actual file read,
|
|
@@ -68,25 +78,14 @@ function readJsonlLines(filePath) {
|
|
|
68
78
|
try {
|
|
69
79
|
const stat = fs.statSync(filePath);
|
|
70
80
|
const mtime = stat.mtimeMs;
|
|
81
|
+
// P1-17: TTL 由 TtlCache 按缓存写入时间判定(不是文件 mtime),命中即 LRU 刷新;
|
|
82
|
+
// mtime 变化视为失效(文件被改写后不得返回旧行)。
|
|
71
83
|
const cached = jsonlCache.get(filePath);
|
|
72
|
-
|
|
73
|
-
// the FILE mtime — a file modified an hour ago would otherwise never hit
|
|
74
|
-
// the cache, making every call re-read and re-write the same entry.
|
|
75
|
-
if (cached && mtime === cached.mtime && Date.now() - cached.cachedAt < CACHE_TTL_MS) {
|
|
76
|
-
// LRU refresh on hit: re-insert so recently-used entries survive eviction.
|
|
77
|
-
jsonlCache.delete(filePath);
|
|
78
|
-
jsonlCache.set(filePath, cached);
|
|
84
|
+
if (cached && mtime === cached.mtime) {
|
|
79
85
|
return cached.lines;
|
|
80
86
|
}
|
|
81
87
|
const lines = readJsonlLinesShared(filePath);
|
|
82
|
-
jsonlCache.set(filePath, { mtime,
|
|
83
|
-
// P1-17: bound the cache. Map iteration is insertion order; after the LRU
|
|
84
|
-
// refresh above the least-recently-used entry is the first key.
|
|
85
|
-
if (jsonlCache.size > CACHE_MAX_ENTRIES) {
|
|
86
|
-
const oldest = jsonlCache.keys().next().value;
|
|
87
|
-
if (oldest !== undefined)
|
|
88
|
-
jsonlCache.delete(oldest);
|
|
89
|
-
}
|
|
88
|
+
jsonlCache.set(filePath, { mtime, lines });
|
|
90
89
|
return lines;
|
|
91
90
|
}
|
|
92
91
|
catch {
|
|
@@ -126,7 +125,7 @@ export function checkCwdGuard(state, realCwd) {
|
|
|
126
125
|
const sessionWorkDir = extractWorkDir(state);
|
|
127
126
|
if (sessionWorkDir === undefined)
|
|
128
127
|
return 'unverifiable';
|
|
129
|
-
if (sessionWorkDir
|
|
128
|
+
if (samePath(sessionWorkDir, realCwd))
|
|
130
129
|
return 'verified';
|
|
131
130
|
return 'failed';
|
|
132
131
|
}
|
|
@@ -211,35 +210,36 @@ export class KimiSessionReader {
|
|
|
211
210
|
const realCwd = fs.realpathSync(cwd);
|
|
212
211
|
const sessions = [];
|
|
213
212
|
for (const entry of this.scanSessionIndex()) {
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
213
|
+
// W2.7:state.json 一次读取共用(cwd 兜底 + summary),I/O 减半。
|
|
214
|
+
// 两次 catch 语义保留:解析失败 = 无法判 cwd → 跳过(continue);
|
|
215
|
+
// 解析成功但字段缺失 = summary 落 wire.jsonl 提取。
|
|
216
|
+
const statePath = path.join(entry.sessionDir, 'state.json');
|
|
217
|
+
let state = null;
|
|
218
|
+
let stateParsed = false;
|
|
219
|
+
if (fs.existsSync(statePath)) {
|
|
219
220
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
entryWorkDir = extractWorkDir(state);
|
|
223
|
-
}
|
|
221
|
+
state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
222
|
+
stateParsed = true;
|
|
224
223
|
}
|
|
225
224
|
catch {
|
|
226
|
-
//
|
|
225
|
+
// Corrupt state.json — cannot determine cwd → skip below
|
|
227
226
|
}
|
|
228
227
|
}
|
|
228
|
+
// Index workDir is v1-only; v2 sessions may have empty/missing workDir.
|
|
229
|
+
// When index workDir is absent, fall back to state.json's cwd field.
|
|
230
|
+
let entryWorkDir = entry.workDir;
|
|
231
|
+
if (!entryWorkDir) {
|
|
232
|
+
if (stateParsed && state)
|
|
233
|
+
entryWorkDir = extractWorkDir(state);
|
|
234
|
+
// Unreadable state — cannot determine cwd, entryWorkDir stays undefined
|
|
235
|
+
}
|
|
229
236
|
if (entryWorkDir !== realCwd) {
|
|
230
237
|
continue;
|
|
231
238
|
}
|
|
232
|
-
const statePath = path.join(entry.sessionDir, 'state.json');
|
|
233
239
|
let summary = 'New Session';
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
// v1: state.title / state.lastPrompt; v2: these fields may be absent
|
|
238
|
-
summary = state.title || state.lastPrompt || '';
|
|
239
|
-
}
|
|
240
|
-
catch {
|
|
241
|
-
// Corrupt state.json — fall through to wire.jsonl extraction
|
|
242
|
-
}
|
|
240
|
+
if (stateParsed && state) {
|
|
241
|
+
// v1: state.title / state.lastPrompt; v2: these fields may be absent
|
|
242
|
+
summary = state.title || state.lastPrompt || '';
|
|
243
243
|
}
|
|
244
244
|
// v2 state.json omits title/lastPrompt: extract from wire.jsonl's last
|
|
245
245
|
// turn.prompt instead. Only scan a small tail to find it — the summary
|
|
@@ -271,7 +271,7 @@ export class KimiSessionReader {
|
|
|
271
271
|
}
|
|
272
272
|
// Sort by mtime descending; same-mtime ties use a deterministic
|
|
273
273
|
// secondary key so ordering is stable across scans.
|
|
274
|
-
sessions
|
|
274
|
+
sortByRecencyDesc(sessions, (s) => s.mtime, (s) => s.sessionId);
|
|
275
275
|
const { items, total } = paginate(sessions, opts ?? {});
|
|
276
276
|
return {
|
|
277
277
|
sessions: items,
|
|
@@ -469,11 +469,7 @@ export class KimiSessionReader {
|
|
|
469
469
|
totalTokens: t.input + t.output + t.cacheRead + t.cacheCreation,
|
|
470
470
|
// Cumulative (session-wide): kimi sums all usage.records; mirrors
|
|
471
471
|
// inputTokens/outputTokens which are already session totals.
|
|
472
|
-
|
|
473
|
-
cumulativeInputTokens: t.input,
|
|
474
|
-
cumulativeOutputTokens: t.output,
|
|
475
|
-
cumulativeCacheReadTokens: t.cacheRead > 0 ? t.cacheRead : 0,
|
|
476
|
-
cumulativeCacheCreationTokens: t.cacheCreation > 0 ? t.cacheCreation : 0,
|
|
472
|
+
...cumulativeUsageFields(t),
|
|
477
473
|
// §6.3: compaction stats from wire.jsonl
|
|
478
474
|
compactCount,
|
|
479
475
|
...(compactPreContextLength !== undefined ? { compactPreContextLength } : {}),
|
|
@@ -490,11 +486,12 @@ export class KimiSessionReader {
|
|
|
490
486
|
}
|
|
491
487
|
}
|
|
492
488
|
/**
|
|
493
|
-
* Read the compaction records (
|
|
494
|
-
*
|
|
489
|
+
* Read the compaction records (full_compaction.begin / complete / cancel /
|
|
490
|
+
* context.apply_compaction) from a session's wire.jsonl.
|
|
495
491
|
*
|
|
496
492
|
* Same cwd guard as readSessionContent. Used by KimiAcpRunner.runCompact
|
|
497
|
-
*
|
|
493
|
+
* to wait for the background compaction terminal before ending the run, and
|
|
494
|
+
* by readCompactionState to detect an in-flight compaction.
|
|
498
495
|
*/
|
|
499
496
|
readCompactionRecords(sessionId, cwd) {
|
|
500
497
|
const wirePath = this.resolveSessionWirePath(sessionId, cwd);
|
|
@@ -506,9 +503,10 @@ export class KimiSessionReader {
|
|
|
506
503
|
for (const line of readJsonlLines(wirePath)) {
|
|
507
504
|
try {
|
|
508
505
|
const entry = JSON.parse(line);
|
|
509
|
-
if (
|
|
506
|
+
if (isCompactionBegin(entry) || isCompactionTerminal(entry)) {
|
|
510
507
|
records.push({
|
|
511
508
|
type: entry.type,
|
|
509
|
+
...(entry.source !== undefined ? { source: entry.source } : {}),
|
|
512
510
|
...(entry.compactedCount !== undefined
|
|
513
511
|
? { compactedCount: entry.compactedCount }
|
|
514
512
|
: {}),
|
|
@@ -530,11 +528,34 @@ export class KimiSessionReader {
|
|
|
530
528
|
return records;
|
|
531
529
|
}
|
|
532
530
|
/**
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
531
|
+
* 由 begin/terminal 记录重建压缩状态机:最后一次 begin 无更新的 terminal
|
|
532
|
+
* 记录 = 在途(kimi-compact-wait-redesign §5.1)。崩溃墓碑由 kimi resume
|
|
533
|
+
* 时补写 full_compaction.cancel,天然闭合。
|
|
536
534
|
*/
|
|
537
|
-
|
|
535
|
+
readCompactionState(sessionId, cwd) {
|
|
536
|
+
const records = [...this.readCompactionRecords(sessionId, cwd)].sort((a, b) => a.time - b.time);
|
|
537
|
+
let pendingBeginAt;
|
|
538
|
+
for (const record of records) {
|
|
539
|
+
if (isCompactionBegin(record)) {
|
|
540
|
+
pendingBeginAt = record.time;
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
// 终态(complete/apply/cancel)闭合当前在途压缩。
|
|
544
|
+
pendingBeginAt = undefined;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
inFlight: pendingBeginAt !== undefined,
|
|
549
|
+
...(pendingBeginAt !== undefined ? { inFlightSince: pendingBeginAt } : {}),
|
|
550
|
+
records,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* W2.5 单源:resolveSessionWirePath 与 isSessionActive 共用的 cwd 守卫
|
|
555
|
+
* (realpathSync → state.json workDir/cwd 三态 → index workDir 兜底 →
|
|
556
|
+
* fail-closed)。返回 sessionDir;会话未知或守卫不过返回 null。
|
|
557
|
+
*/
|
|
558
|
+
verifySessionCwd(sessionId, cwd) {
|
|
538
559
|
// Find session directory from index (also retrieve index entry for
|
|
539
560
|
// fallback cwd guard when state.json is missing or unparseable)
|
|
540
561
|
const indexEntry = this.findSessionIndexEntry(sessionId);
|
|
@@ -542,7 +563,6 @@ export class KimiSessionReader {
|
|
|
542
563
|
if (!sessionDir) {
|
|
543
564
|
return null;
|
|
544
565
|
}
|
|
545
|
-
const statePath = path.join(sessionDir, 'state.json');
|
|
546
566
|
let realCwd;
|
|
547
567
|
try {
|
|
548
568
|
realCwd = fs.realpathSync(cwd);
|
|
@@ -552,6 +572,7 @@ export class KimiSessionReader {
|
|
|
552
572
|
return null;
|
|
553
573
|
}
|
|
554
574
|
let cwdGuardPassed = false;
|
|
575
|
+
const statePath = path.join(sessionDir, 'state.json');
|
|
555
576
|
try {
|
|
556
577
|
if (fs.existsSync(statePath)) {
|
|
557
578
|
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
@@ -559,13 +580,12 @@ export class KimiSessionReader {
|
|
|
559
580
|
if (guardResult === 'failed') {
|
|
560
581
|
return null;
|
|
561
582
|
}
|
|
562
|
-
if (guardResult === '
|
|
563
|
-
|
|
564
|
-
// Fall through to index-based guard below
|
|
583
|
+
if (guardResult === 'verified') {
|
|
584
|
+
cwdGuardPassed = true;
|
|
565
585
|
}
|
|
566
586
|
else {
|
|
567
|
-
|
|
568
|
-
|
|
587
|
+
getLogger().warn(`[kimi-session-reader] state.json has no workDir/cwd field for session ${sessionId}, falling back to index workDir`);
|
|
588
|
+
// Fall through to index-based guard below
|
|
569
589
|
}
|
|
570
590
|
}
|
|
571
591
|
}
|
|
@@ -591,7 +611,18 @@ export class KimiSessionReader {
|
|
|
591
611
|
return null;
|
|
592
612
|
}
|
|
593
613
|
}
|
|
594
|
-
|
|
614
|
+
return { sessionDir };
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Resolve the main wire.jsonl path for a session after passing the cwd
|
|
618
|
+
* guard (state.json + session index fallback). Returns null when the
|
|
619
|
+
* session is unknown, fails the guard, or has no wire file.
|
|
620
|
+
*/
|
|
621
|
+
resolveSessionWirePath(sessionId, cwd) {
|
|
622
|
+
const verified = this.verifySessionCwd(sessionId, cwd);
|
|
623
|
+
if (!verified)
|
|
624
|
+
return null;
|
|
625
|
+
const wirePath = path.join(verified.sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
595
626
|
if (!fs.existsSync(wirePath)) {
|
|
596
627
|
return null;
|
|
597
628
|
}
|
|
@@ -601,46 +632,11 @@ export class KimiSessionReader {
|
|
|
601
632
|
* Check if a session is currently active
|
|
602
633
|
*/
|
|
603
634
|
isSessionActive(sessionId, cwd) {
|
|
604
|
-
|
|
605
|
-
const
|
|
606
|
-
if (!
|
|
635
|
+
// W2.5:cwd 守卫与 resolveSessionWirePath 共用 verifySessionCwd(单源)。
|
|
636
|
+
const verified = this.verifySessionCwd(sessionId, cwd);
|
|
637
|
+
if (!verified)
|
|
607
638
|
return false;
|
|
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
|
-
}
|
|
643
|
-
const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
639
|
+
const wirePath = path.join(verified.sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
644
640
|
if (!fs.existsSync(wirePath)) {
|
|
645
641
|
return false;
|
|
646
642
|
}
|
|
@@ -648,7 +644,7 @@ export class KimiSessionReader {
|
|
|
648
644
|
const stat = fs.statSync(wirePath);
|
|
649
645
|
const now = Date.now();
|
|
650
646
|
// Check mtime freshness
|
|
651
|
-
if (
|
|
647
|
+
if (isStale(stat.mtimeMs, now)) {
|
|
652
648
|
return false;
|
|
653
649
|
}
|
|
654
650
|
// Read the tail to check for active indicators. kimi appends
|
|
@@ -24,25 +24,13 @@ 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
|
-
}
|
|
35
27
|
export declare class OpencodeSessionReader implements AgentSessionReader {
|
|
36
28
|
private readonly binary;
|
|
37
29
|
private readonly cacheTtlMs;
|
|
38
30
|
/** Optional raw-export capture override (testing / alt transport). */
|
|
39
31
|
private readonly captureExportOverride?;
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
listCache: Map<string, {
|
|
43
|
-
ts: number;
|
|
44
|
-
data: OpencodeSessionListEntry[];
|
|
45
|
-
}>;
|
|
32
|
+
/** cwd → 列表缓存(TTL 由 cacheTtlMs 决定)。 */
|
|
33
|
+
private listCache;
|
|
46
34
|
constructor(opts?: OpencodeSessionReaderOptions);
|
|
47
35
|
/**
|
|
48
36
|
* List sessions in a given cwd, using directory field for matching.
|
|
@@ -13,26 +13,29 @@
|
|
|
13
13
|
* - isSessionActive: use `updated` timestamp with 1 hour stale window.
|
|
14
14
|
* - Simple memory cache with TTL.
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
16
|
+
import { spawnProcessSync } from '../../platform/spawn.js';
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import { silentlyUnlink } from '../../common/fs.js';
|
|
19
19
|
import os from 'node:os';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
22
22
|
import { getLogger } from '../../logger/index.js';
|
|
23
|
-
import {
|
|
23
|
+
import { isStale } from '../common/constants.js';
|
|
24
|
+
import { samePath } from '../../platform/path.js';
|
|
24
25
|
import { paginate, capEvents } from '../common/pagination.js';
|
|
26
|
+
import { sortByRecencyDesc } from '../common/recency.js';
|
|
27
|
+
import { TtlCache } from '../../common/ttl-cache.js';
|
|
25
28
|
export class OpencodeSessionReader {
|
|
26
29
|
binary;
|
|
27
30
|
cacheTtlMs;
|
|
28
31
|
/** Optional raw-export capture override (testing / alt transport). */
|
|
29
32
|
captureExportOverride;
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
listCache = new Map();
|
|
33
|
+
/** cwd → 列表缓存(TTL 由 cacheTtlMs 决定)。 */
|
|
34
|
+
listCache;
|
|
33
35
|
constructor(opts = {}) {
|
|
34
36
|
this.binary = 'opencode';
|
|
35
37
|
this.cacheTtlMs = opts.cacheTtlMs ?? 10_000;
|
|
38
|
+
this.listCache = new TtlCache(this.cacheTtlMs);
|
|
36
39
|
this.captureExportOverride = opts.captureExport;
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
@@ -44,10 +47,10 @@ export class OpencodeSessionReader {
|
|
|
44
47
|
const realCwd = this.realpath(cwd);
|
|
45
48
|
const entries = this.fetchSessionList(realCwd);
|
|
46
49
|
// Filter by directory === realpath(cwd)
|
|
47
|
-
const filtered = entries.filter((e) => e.directory
|
|
50
|
+
const filtered = entries.filter((e) => samePath(e.directory, realCwd));
|
|
48
51
|
// Sort by updated descending; same-updated ties use id as a deterministic
|
|
49
52
|
// secondary key so CLI/list-cache rebuilds never reorder the page.
|
|
50
|
-
filtered
|
|
53
|
+
sortByRecencyDesc(filtered, (s) => s.updated, (s) => s.id);
|
|
51
54
|
// Compute total over the full sorted set, then paginate.
|
|
52
55
|
const { items, total } = paginate(filtered, opts ?? {});
|
|
53
56
|
return {
|
|
@@ -279,11 +282,11 @@ export class OpencodeSessionReader {
|
|
|
279
282
|
const realCwd = this.realpath(cwd);
|
|
280
283
|
try {
|
|
281
284
|
const entries = this.fetchSessionList(realCwd);
|
|
282
|
-
const entry = entries.find((e) => e.id === sessionId && e.directory
|
|
285
|
+
const entry = entries.find((e) => e.id === sessionId && samePath(e.directory, realCwd));
|
|
283
286
|
if (!entry)
|
|
284
287
|
return false;
|
|
285
288
|
// Check if updated within STALE_MS
|
|
286
|
-
return
|
|
289
|
+
return !isStale(entry.updated);
|
|
287
290
|
}
|
|
288
291
|
catch {
|
|
289
292
|
return false;
|
|
@@ -302,12 +305,16 @@ export class OpencodeSessionReader {
|
|
|
302
305
|
fetchSessionList(cwd) {
|
|
303
306
|
const realCwd = this.realpath(cwd);
|
|
304
307
|
const cached = this.listCache.get(realCwd);
|
|
305
|
-
if (cached
|
|
306
|
-
return cached
|
|
308
|
+
if (cached) {
|
|
309
|
+
return cached;
|
|
307
310
|
}
|
|
308
311
|
try {
|
|
312
|
+
// spawnProcessSync(cross-spawn)而非 execFileSync:opencode 是 npm 安装
|
|
313
|
+
// 的 .cmd 垫片,win32 上 execFileSync 不做 PATHEXT 解析会直接 ENOENT,
|
|
314
|
+
// 会话列表永远为空;cross-spawn 走与 runner spawn 相同的垫片执行路径
|
|
315
|
+
// (posix 为 child_process.spawnSync 直通,行为不变)。
|
|
309
316
|
// Pass cwd to execFileSync so opencode CLI returns sessions for the correct directory
|
|
310
|
-
const
|
|
317
|
+
const res = spawnProcessSync(this.binary, ['session', 'list', '--format', 'json'], {
|
|
311
318
|
encoding: 'utf-8',
|
|
312
319
|
timeout: 10000,
|
|
313
320
|
// P1-15: default maxBuffer is 1MiB — a large session list (>1MiB JSON)
|
|
@@ -315,14 +322,26 @@ export class OpencodeSessionReader {
|
|
|
315
322
|
// user sees "no sessions" even though the CLI returned healthy data.
|
|
316
323
|
maxBuffer: 64 * 1024 * 1024,
|
|
317
324
|
cwd: realCwd, // Key fix: pass user cwd to CLI
|
|
318
|
-
|
|
325
|
+
windowsHide: true, // opencode 是 npm .cmd 垫片:不隐藏会闪 cmd.exe 控制台
|
|
326
|
+
});
|
|
327
|
+
if (res.error) {
|
|
328
|
+
const enoent = res.error.code === 'ENOENT';
|
|
329
|
+
if (enoent)
|
|
330
|
+
return [];
|
|
331
|
+
throw res.error;
|
|
332
|
+
}
|
|
333
|
+
if (res.status !== 0) {
|
|
334
|
+
const stderr = String(res.stderr ?? '').trim();
|
|
335
|
+
throw new Error(`exit ${res.status}${stderr ? `: ${stderr}` : ''}`);
|
|
336
|
+
}
|
|
337
|
+
const output = String(res.stdout ?? '').trim();
|
|
319
338
|
// CLI returns empty string when no sessions exist - treat as empty array
|
|
320
339
|
if (!output) {
|
|
321
340
|
return [];
|
|
322
341
|
}
|
|
323
342
|
const data = JSON.parse(output);
|
|
324
343
|
// Update cache keyed by cwd
|
|
325
|
-
this.listCache.set(realCwd,
|
|
344
|
+
this.listCache.set(realCwd, data);
|
|
326
345
|
return data;
|
|
327
346
|
}
|
|
328
347
|
catch (err) {
|
|
@@ -363,7 +382,7 @@ export class OpencodeSessionReader {
|
|
|
363
382
|
let fd;
|
|
364
383
|
try {
|
|
365
384
|
fd = fs.openSync(tmp, 'w');
|
|
366
|
-
|
|
385
|
+
spawnProcessSync(this.binary, ['export', sessionId], {
|
|
367
386
|
encoding: 'utf-8',
|
|
368
387
|
timeout: 30000,
|
|
369
388
|
stdio: ['ignore', fd, 'pipe'],
|
|
@@ -371,6 +390,7 @@ export class OpencodeSessionReader {
|
|
|
371
390
|
// stderr) would throw ENOBUFS and lose the export. Keep the default
|
|
372
391
|
// stdout file-fd transport unaffected by raising the pipe cap.
|
|
373
392
|
maxBuffer: 64 * 1024 * 1024,
|
|
393
|
+
windowsHide: true,
|
|
374
394
|
});
|
|
375
395
|
return fs.readFileSync(tmp, 'utf-8');
|
|
376
396
|
}
|