lark-remote 0.1.2 → 0.1.4
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 +65 -0
- package/README.en.md +2 -5
- package/README.md +1 -4
- package/dist/bridge/index.d.ts +5 -1
- package/dist/bridge/index.js +12 -2
- package/dist/card/card-budget.js +1 -2
- package/dist/card/run-renderer.js +1 -0
- package/dist/card/run-state.d.ts +4 -0
- package/dist/config/codex-config.d.ts +3 -5
- package/dist/config/codex-config.js +7 -7
- package/dist/config/dir.d.ts +1 -0
- package/dist/config/dir.js +4 -0
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -11
- package/dist/connector/index.js +28 -0
- package/dist/index.js +74 -21
- package/dist/router/config/codex.js +8 -12
- package/dist/router/config/index.d.ts +10 -0
- package/dist/router/config/index.js +17 -0
- package/dist/router/index.d.ts +40 -1
- package/dist/router/index.js +514 -48
- package/dist/router/utils.d.ts +3 -0
- package/dist/router/utils.js +15 -3
- package/dist/runner/claude/runner.d.ts +0 -1
- package/dist/runner/claude/runner.js +1 -1
- package/dist/runner/codex/runner.d.ts +0 -2
- package/dist/runner/codex/runner.js +1 -1
- package/dist/runner/common/spawning-runner.d.ts +0 -1
- package/dist/runner/common/spawning-runner.js +2 -4
- package/dist/runner/kimi/runner.d.ts +0 -1
- package/dist/runner/kimi/runner.js +1 -1
- package/dist/runner/opencode/runner.d.ts +0 -2
- package/dist/runner/opencode/runner.js +1 -1
- package/dist/runner/pi/runner.d.ts +0 -1
- package/dist/runner/pi/runner.js +1 -1
- package/dist/runner/probe.d.ts +45 -0
- package/dist/runner/probe.js +87 -0
- package/dist/runner/types.d.ts +3 -0
- package/dist/session/claude/sessions.d.ts +1 -1
- package/dist/session/claude/sessions.js +77 -31
- package/dist/session/codex/rollout-reader.js +9 -0
- package/dist/session/kimi/sessions.d.ts +3 -1
- package/dist/session/kimi/sessions.js +9 -3
- package/dist/session/opencode/sessions.d.ts +0 -2
- package/dist/session/opencode/sessions.js +1 -1
- package/dist/startup-contact.d.ts +4 -2
- package/dist/startup-contact.js +7 -4
- package/dist/workspace/index.d.ts +19 -1
- package/dist/workspace/index.js +51 -5
- package/package.json +39 -4
package/dist/router/utils.d.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import type { ActiveRunSnapshot } from '../bridge/index.js';
|
|
6
6
|
export interface SessionDisplayUsage {
|
|
7
7
|
contextLength?: number;
|
|
8
|
+
/** 当前模型 context window 上限(仅 codex 提供);用于渲染 "Context - X (Y%)"。 */
|
|
9
|
+
contextLimit?: number;
|
|
8
10
|
compactCount?: number;
|
|
9
11
|
cacheReadTokens?: number;
|
|
10
12
|
cacheCreationTokens?: number;
|
|
@@ -18,6 +20,7 @@ export declare function activeRunUsage(activeRun: ActiveRunSnapshot | undefined)
|
|
|
18
20
|
*/
|
|
19
21
|
export declare function formatUsageStats(usage?: {
|
|
20
22
|
contextLength?: number;
|
|
23
|
+
contextLimit?: number;
|
|
21
24
|
compactCount?: number;
|
|
22
25
|
cacheReadTokens?: number;
|
|
23
26
|
cacheCreationTokens?: number;
|
package/dist/router/utils.js
CHANGED
|
@@ -14,10 +14,18 @@ export function activeRunUsage(activeRun) {
|
|
|
14
14
|
}
|
|
15
15
|
// --- Extracted helpers for refactoring ---
|
|
16
16
|
/**
|
|
17
|
-
* Format number to
|
|
18
|
-
* (e.g.,
|
|
17
|
+
* Format number to human-readable unit string.
|
|
18
|
+
* - >= 1M: one decimal, M suffix (e.g., 1200000 → 1.2M, 25000000 → 25M)
|
|
19
|
+
* - >= 1K: rounded to nearest integer K (e.g., 1500 → 2K, 120000 → 120K)
|
|
20
|
+
* - < 1K: as-is (e.g., 500 → "500")
|
|
19
21
|
*/
|
|
20
22
|
function formatTokenK(n) {
|
|
23
|
+
if (n >= 1_000_000) {
|
|
24
|
+
const m = n / 1_000_000;
|
|
25
|
+
const rounded = Math.round(m * 10) / 10;
|
|
26
|
+
// Drop unnecessary ".0" for whole numbers (2.0M → 2M)
|
|
27
|
+
return rounded === Math.floor(rounded) ? `${Math.floor(rounded)}M` : `${rounded}M`;
|
|
28
|
+
}
|
|
21
29
|
if (n >= 1000) {
|
|
22
30
|
const k = Math.round(n / 1000);
|
|
23
31
|
return `${k}K`;
|
|
@@ -40,7 +48,11 @@ export function formatUsageStats(usage, options) {
|
|
|
40
48
|
}
|
|
41
49
|
// 2. Context 长度
|
|
42
50
|
if (usage?.contextLength !== undefined) {
|
|
43
|
-
|
|
51
|
+
const ctxLimit = usage.contextLimit;
|
|
52
|
+
const ctxPercent = ctxLimit !== undefined && ctxLimit > 0
|
|
53
|
+
? ` (${Math.round((usage.contextLength / ctxLimit) * 100)}%)`
|
|
54
|
+
: '';
|
|
55
|
+
lines.push(`Context - ${formatTokenK(usage.contextLength)}${ctxPercent}`);
|
|
44
56
|
}
|
|
45
57
|
// 3. Compact 次数
|
|
46
58
|
if (usage?.compactCount) {
|
|
@@ -19,7 +19,6 @@ export class ClaudeRunner extends SpawningRunner {
|
|
|
19
19
|
defaultSettings;
|
|
20
20
|
constructor(opts) {
|
|
21
21
|
super({
|
|
22
|
-
binary: opts.binary ?? 'claude',
|
|
23
22
|
pidDir: opts.pidDir,
|
|
24
23
|
workspace: opts.workspace,
|
|
25
24
|
stopGraceMs: opts.stopGraceMs,
|
|
@@ -27,6 +26,7 @@ export class ClaudeRunner extends SpawningRunner {
|
|
|
27
26
|
pidFilePrefix: 'claude',
|
|
28
27
|
logTag: 'claude-runner',
|
|
29
28
|
});
|
|
29
|
+
this.binary = 'claude';
|
|
30
30
|
this.defaultModel = opts.model ?? 'claude-opus-4-8';
|
|
31
31
|
this.defaultEffort = opts.effort ?? 'medium';
|
|
32
32
|
this.defaultSettings = opts.settings;
|
|
@@ -26,8 +26,6 @@ import type { AgentKind, AgentSessionReader, AgentEvent, AgentRunner, AgentStatu
|
|
|
26
26
|
import type { SpawnOptions } from '../types.js';
|
|
27
27
|
import { SpawningRunner, type RunnerTranslator } from '../common/spawning-runner.js';
|
|
28
28
|
interface CodexExecRunnerOptions {
|
|
29
|
-
/** Path to codex binary. Default: 'codex' (in PATH). */
|
|
30
|
-
binary?: string;
|
|
31
29
|
/** Model override. Omitted → codex reads from its config.toml. */
|
|
32
30
|
model?: string;
|
|
33
31
|
/** Model provider override. Omitted → codex reads from its config.toml. */
|
|
@@ -34,7 +34,6 @@ export class CodexExecRunner extends SpawningRunner {
|
|
|
34
34
|
reasoningEffort;
|
|
35
35
|
constructor(opts) {
|
|
36
36
|
super({
|
|
37
|
-
binary: opts.binary ?? 'codex',
|
|
38
37
|
pidDir: opts.pidDir,
|
|
39
38
|
workspace: opts.workspace,
|
|
40
39
|
stopGraceMs: opts.stopGraceMs,
|
|
@@ -42,6 +41,7 @@ export class CodexExecRunner extends SpawningRunner {
|
|
|
42
41
|
pidFilePrefix: 'codex',
|
|
43
42
|
logTag: 'codex-exec-runner',
|
|
44
43
|
});
|
|
44
|
+
this.binary = 'codex';
|
|
45
45
|
this.defaultModel = opts.model;
|
|
46
46
|
this.modelProvider = opts.modelProvider;
|
|
47
47
|
this.reasoningEffort = opts.reasoningEffort;
|
|
@@ -111,10 +111,8 @@ export class SpawningRunner {
|
|
|
111
111
|
*/
|
|
112
112
|
stoppedByUser = false;
|
|
113
113
|
constructor(opts) {
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
// class toward one agent). Subclasses pass their own binary.
|
|
117
|
-
this.binary = opts.binary ?? '';
|
|
114
|
+
// Subclasses set this.binary after super()
|
|
115
|
+
this.binary = '';
|
|
118
116
|
this.stopGraceMs = opts.stopGraceMs ?? DEFAULT_STOP_GRACE_MS;
|
|
119
117
|
this.logTag = opts.logTag ?? 'spawning-runner';
|
|
120
118
|
const pidDir = opts.pidDir ?? path.join(os.homedir(), '.lark-remote');
|
|
@@ -2,7 +2,6 @@ import type { ChildProcess } from 'node:child_process';
|
|
|
2
2
|
import { SpawningRunner, type RunnerTranslator } from '../common/spawning-runner.js';
|
|
3
3
|
import type { AgentRunner, AgentSessionReader, SpawnOptions, AgentStatusInfo } from '../types.js';
|
|
4
4
|
interface KimiRunnerConfig {
|
|
5
|
-
binary?: string;
|
|
6
5
|
model?: string;
|
|
7
6
|
thinkingEffort?: string;
|
|
8
7
|
stopGraceMs?: number;
|
|
@@ -32,7 +32,6 @@ export class KimiRunner extends SpawningRunner {
|
|
|
32
32
|
completionTimeoutMs;
|
|
33
33
|
constructor(opts) {
|
|
34
34
|
super({
|
|
35
|
-
binary: opts.binary ?? 'kimi',
|
|
36
35
|
pidDir: opts.pidDir,
|
|
37
36
|
workspace: opts.workspace,
|
|
38
37
|
stopGraceMs: opts.stopGraceMs,
|
|
@@ -40,6 +39,7 @@ export class KimiRunner extends SpawningRunner {
|
|
|
40
39
|
pidFilePrefix: 'kimi',
|
|
41
40
|
logTag: 'kimi-runner',
|
|
42
41
|
});
|
|
42
|
+
this.binary = 'kimi';
|
|
43
43
|
this.defaultModel = opts.model ?? 'kimi-code/k3';
|
|
44
44
|
this.thinkingEffort = opts.thinkingEffort ?? 'max';
|
|
45
45
|
this.completionTimeoutMs = opts.completionTimeoutMs ?? KIMI_TIMEOUT_MS;
|
|
@@ -30,8 +30,6 @@ import type { AgentKind, AgentSessionReader, AgentRunner, AgentStatusInfo } from
|
|
|
30
30
|
import type { SpawnOptions } from '../types.js';
|
|
31
31
|
import { SpawningRunner, type RunnerTranslator } from '../common/spawning-runner.js';
|
|
32
32
|
interface OpencodeExecRunnerOptions {
|
|
33
|
-
/** Path to opencode binary. Default: 'opencode' (in PATH). */
|
|
34
|
-
binary?: string;
|
|
35
33
|
/** Model override in provider/model format (e.g. 'anthropic/claude-sonnet-4-20250514'). Omitted → opencode config default. */
|
|
36
34
|
model?: string;
|
|
37
35
|
/** Grace period (ms) for SIGTERM→SIGKILL. Default: 5000. */
|
|
@@ -35,7 +35,6 @@ export class OpencodeExecRunner extends SpawningRunner {
|
|
|
35
35
|
defaultModel;
|
|
36
36
|
constructor(opts) {
|
|
37
37
|
super({
|
|
38
|
-
binary: opts.binary ?? 'opencode',
|
|
39
38
|
pidDir: opts.pidDir,
|
|
40
39
|
workspace: opts.workspace,
|
|
41
40
|
stopGraceMs: opts.stopGraceMs,
|
|
@@ -43,6 +42,7 @@ export class OpencodeExecRunner extends SpawningRunner {
|
|
|
43
42
|
pidFilePrefix: 'opencode',
|
|
44
43
|
logTag: 'opencode-exec-runner',
|
|
45
44
|
});
|
|
45
|
+
this.binary = 'opencode';
|
|
46
46
|
this.defaultModel = opts.model;
|
|
47
47
|
this.sessionReader = opts.sessionReader;
|
|
48
48
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { AgentRunner, AgentSessionReader, SpawnOptions, AgentStatusInfo } from '../types.js';
|
|
2
2
|
import { SpawningRunner, type RunnerTranslator } from '../common/spawning-runner.js';
|
|
3
3
|
interface PiRunnerConfig {
|
|
4
|
-
binary?: string;
|
|
5
4
|
provider?: string;
|
|
6
5
|
model?: string;
|
|
7
6
|
thinking?: string;
|
package/dist/runner/pi/runner.js
CHANGED
|
@@ -18,7 +18,6 @@ export class PiRunner extends SpawningRunner {
|
|
|
18
18
|
tools;
|
|
19
19
|
constructor(opts) {
|
|
20
20
|
super({
|
|
21
|
-
binary: opts.binary ?? 'pi',
|
|
22
21
|
pidDir: opts.pidDir,
|
|
23
22
|
workspace: opts.workspace,
|
|
24
23
|
stopGraceMs: opts.stopGraceMs,
|
|
@@ -26,6 +25,7 @@ export class PiRunner extends SpawningRunner {
|
|
|
26
25
|
pidFilePrefix: 'pi',
|
|
27
26
|
logTag: 'pi-runner',
|
|
28
27
|
});
|
|
28
|
+
this.binary = 'pi';
|
|
29
29
|
this.provider = opts.provider ?? 'Volcano';
|
|
30
30
|
this.defaultModel = opts.model ?? 'glm-5.2';
|
|
31
31
|
this.thinking = opts.thinking ?? 'medium';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent availability probe — detect whether each CLI binary is installed
|
|
3
|
+
* by checking whether it resolves on PATH via `which`.
|
|
4
|
+
*
|
|
5
|
+
* Why `which` over `<binary> --help`:
|
|
6
|
+
* - `--help` starts a full Node.js process per agent (2–8 s on
|
|
7
|
+
* resource-constrained devices like Raspberry Pi). Under startup load
|
|
8
|
+
* (auto-resume spawns, CPU contention) probes time out and installed
|
|
9
|
+
* agents are falsely reported as unavailable.
|
|
10
|
+
* - `which` is a lightweight PATH lookup (<10 ms), immune to CPU contention.
|
|
11
|
+
* - "Installed but broken" cases surface at run time via SpawningRunner's
|
|
12
|
+
* ENOENT/error handling (src/runner/common/spawning-runner.ts), which
|
|
13
|
+
* shows a friendly error card instead of a startup-time mislabel.
|
|
14
|
+
*
|
|
15
|
+
* Why `which` over `command -v`:
|
|
16
|
+
* - `which` is a standalone executable (e.g. /usr/bin/which on macOS and
|
|
17
|
+
* Debian-based Linux), so no `shell: true` and no Node DEP0190 warning.
|
|
18
|
+
* - Caveat: `which` is not POSIX-guaranteed. macOS, Raspberry Pi OS and
|
|
19
|
+
* other Debian-based distributions ship it by default; if Windows or
|
|
20
|
+
* minimal Alpine containers become supported, prefer a pure-Node
|
|
21
|
+
* PATH + X_OK lookup instead.
|
|
22
|
+
*/
|
|
23
|
+
import type { AgentKind } from './types.js';
|
|
24
|
+
/**
|
|
25
|
+
* Probe a single agent, using cache when fresh.
|
|
26
|
+
* Updates cache on completion.
|
|
27
|
+
*/
|
|
28
|
+
export declare function probeAgentAvailability(kind: AgentKind): Promise<boolean>;
|
|
29
|
+
/**
|
|
30
|
+
* Probe all agents concurrently.
|
|
31
|
+
*
|
|
32
|
+
* Concurrent (not sequential) because `which` is a lightweight PATH lookup
|
|
33
|
+
* (<10 ms) with no CPU contention risk — unlike the previous `--help`
|
|
34
|
+
* probes, which needed to run one-at-a-time to avoid starving each other
|
|
35
|
+
* on resource-constrained devices.
|
|
36
|
+
*/
|
|
37
|
+
export declare function probeAllAgents(): Promise<Map<AgentKind, boolean>>;
|
|
38
|
+
/**
|
|
39
|
+
* Synchronous cache read — returns cached availability if fresh,
|
|
40
|
+
* or `undefined` if no fresh cache entry exists.
|
|
41
|
+
* Used by synchronous code paths (e.g. buildConfigCard) that cannot await.
|
|
42
|
+
*/
|
|
43
|
+
export declare function getCachedAvailability(kind: AgentKind): boolean | undefined;
|
|
44
|
+
/** Clear all cache entries. Exported for test use only (cache auto-expires via TTL). */
|
|
45
|
+
export declare function _clearCacheForTest(): void;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent availability probe — detect whether each CLI binary is installed
|
|
3
|
+
* by checking whether it resolves on PATH via `which`.
|
|
4
|
+
*
|
|
5
|
+
* Why `which` over `<binary> --help`:
|
|
6
|
+
* - `--help` starts a full Node.js process per agent (2–8 s on
|
|
7
|
+
* resource-constrained devices like Raspberry Pi). Under startup load
|
|
8
|
+
* (auto-resume spawns, CPU contention) probes time out and installed
|
|
9
|
+
* agents are falsely reported as unavailable.
|
|
10
|
+
* - `which` is a lightweight PATH lookup (<10 ms), immune to CPU contention.
|
|
11
|
+
* - "Installed but broken" cases surface at run time via SpawningRunner's
|
|
12
|
+
* ENOENT/error handling (src/runner/common/spawning-runner.ts), which
|
|
13
|
+
* shows a friendly error card instead of a startup-time mislabel.
|
|
14
|
+
*
|
|
15
|
+
* Why `which` over `command -v`:
|
|
16
|
+
* - `which` is a standalone executable (e.g. /usr/bin/which on macOS and
|
|
17
|
+
* Debian-based Linux), so no `shell: true` and no Node DEP0190 warning.
|
|
18
|
+
* - Caveat: `which` is not POSIX-guaranteed. macOS, Raspberry Pi OS and
|
|
19
|
+
* other Debian-based distributions ship it by default; if Windows or
|
|
20
|
+
* minimal Alpine containers become supported, prefer a pure-Node
|
|
21
|
+
* PATH + X_OK lookup instead.
|
|
22
|
+
*/
|
|
23
|
+
import { spawn } from 'child_process';
|
|
24
|
+
/** Map from AgentKind to its CLI binary name. */
|
|
25
|
+
const BINARY_MAP = {
|
|
26
|
+
claude: 'claude',
|
|
27
|
+
codex: 'codex',
|
|
28
|
+
opencode: 'opencode',
|
|
29
|
+
pi: 'pi',
|
|
30
|
+
kimi: 'kimi',
|
|
31
|
+
};
|
|
32
|
+
const CACHE_TTL_MS = 5 * 60_000;
|
|
33
|
+
/** Module-level cache, survives across calls within the process. */
|
|
34
|
+
const cache = new Map();
|
|
35
|
+
/**
|
|
36
|
+
* Probe a single agent's availability by spawning `which <binary>`.
|
|
37
|
+
* Exit code 0 = binary on PATH; error / non-zero = unavailable.
|
|
38
|
+
*/
|
|
39
|
+
async function probeOne(kind) {
|
|
40
|
+
const binary = BINARY_MAP[kind];
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const proc = spawn('which', [binary], { stdio: 'ignore' });
|
|
43
|
+
proc.on('error', () => resolve(false));
|
|
44
|
+
proc.on('exit', (code) => resolve(code === 0));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Probe a single agent, using cache when fresh.
|
|
49
|
+
* Updates cache on completion.
|
|
50
|
+
*/
|
|
51
|
+
export async function probeAgentAvailability(kind) {
|
|
52
|
+
const cached = cache.get(kind);
|
|
53
|
+
if (cached && Date.now() - cached.ts < CACHE_TTL_MS)
|
|
54
|
+
return cached.ok;
|
|
55
|
+
const ok = await probeOne(kind);
|
|
56
|
+
cache.set(kind, { ts: Date.now(), ok });
|
|
57
|
+
return ok;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Probe all agents concurrently.
|
|
61
|
+
*
|
|
62
|
+
* Concurrent (not sequential) because `which` is a lightweight PATH lookup
|
|
63
|
+
* (<10 ms) with no CPU contention risk — unlike the previous `--help`
|
|
64
|
+
* probes, which needed to run one-at-a-time to avoid starving each other
|
|
65
|
+
* on resource-constrained devices.
|
|
66
|
+
*/
|
|
67
|
+
export async function probeAllAgents() {
|
|
68
|
+
const kinds = Object.keys(BINARY_MAP);
|
|
69
|
+
const entries = await Promise.all(kinds.map(async (k) => [k, await probeAgentAvailability(k)]));
|
|
70
|
+
return new Map(entries);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Synchronous cache read — returns cached availability if fresh,
|
|
74
|
+
* or `undefined` if no fresh cache entry exists.
|
|
75
|
+
* Used by synchronous code paths (e.g. buildConfigCard) that cannot await.
|
|
76
|
+
*/
|
|
77
|
+
export function getCachedAvailability(kind) {
|
|
78
|
+
const cached = cache.get(kind);
|
|
79
|
+
if (!cached || Date.now() - cached.ts >= CACHE_TTL_MS)
|
|
80
|
+
return undefined;
|
|
81
|
+
return cached.ok;
|
|
82
|
+
}
|
|
83
|
+
/** Clear all cache entries. Exported for test use only (cache auto-expires via TTL). */
|
|
84
|
+
export function _clearCacheForTest() {
|
|
85
|
+
cache.clear();
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=probe.js.map
|
package/dist/runner/types.d.ts
CHANGED
|
@@ -153,6 +153,9 @@ export interface AgentSessionUsage {
|
|
|
153
153
|
inputTokens: number;
|
|
154
154
|
outputTokens: number;
|
|
155
155
|
contextLength: number;
|
|
156
|
+
/** Agent 上报的当前模型 context window 上限(codex token_count.info.model_context_window)。
|
|
157
|
+
* 仅 codex 提供;缺省时卡片只显示绝对量、不显示百分比。 */
|
|
158
|
+
contextLimit?: number;
|
|
156
159
|
/** Claude auto-compact 次数(数 compact_boundary 事件);codex/opencode 无此概念,留 undefined。 */
|
|
157
160
|
compactCount?: number;
|
|
158
161
|
/** 从缓存读取的 token 数(节省的费用)。 */
|
|
@@ -20,7 +20,7 @@ export declare function readCwdFromJsonl(filePath: string): string | undefined;
|
|
|
20
20
|
*
|
|
21
21
|
* We locate the directory via `projectDirForCwd` (lossy but OK for locating
|
|
22
22
|
* a known cwd) and then **verify** each jsonl's cwd field matches. This
|
|
23
|
-
* prevents collision: `
|
|
23
|
+
* prevents collision: `disk_d/foo` and `disk-d/foo` both encode to the same
|
|
24
24
|
* directory name, but only the matching cwd field is kept.
|
|
25
25
|
*/
|
|
26
26
|
export declare function listClaudeSessions(cwd: string, opts?: {
|
|
@@ -12,8 +12,8 @@ import { truncate } from '../../card/card-shared.js';
|
|
|
12
12
|
* Claude encodes the cwd by replacing `/` with `-` (e.g.
|
|
13
13
|
* `/Users/x/proj` -> `-Users-x-proj`). Underscores are also replaced with `-`.
|
|
14
14
|
*
|
|
15
|
-
* Note: this encoding is LOSSY (`
|
|
16
|
-
* `
|
|
15
|
+
* Note: this encoding is LOSSY (`disk_d/foo` and `disk-d/foo` both encode to
|
|
16
|
+
* `disk-d-foo`). It is only safe to use the result to **locate** files; never
|
|
17
17
|
* to **decode** a directory name back to a cwd. Use `readCwdFromJsonl` for
|
|
18
18
|
* cwd (regression 2026-06-21 /resume & /cd 路径错乱).
|
|
19
19
|
*/
|
|
@@ -83,7 +83,7 @@ function defaultProjectsDir() {
|
|
|
83
83
|
* sorted, first match wins; the cwd guard in `readSessionContent` still
|
|
84
84
|
* validates the file afterwards.
|
|
85
85
|
*/
|
|
86
|
-
function findSessionFileInProjects(sessionId, projectsDir) {
|
|
86
|
+
function findSessionFileInProjects(sessionId, projectsDir, cwd) {
|
|
87
87
|
let entries;
|
|
88
88
|
try {
|
|
89
89
|
entries = fs.readdirSync(projectsDir);
|
|
@@ -92,12 +92,27 @@ function findSessionFileInProjects(sessionId, projectsDir) {
|
|
|
92
92
|
return undefined;
|
|
93
93
|
}
|
|
94
94
|
const name = `${sessionId}.jsonl`;
|
|
95
|
+
const candidates = [];
|
|
95
96
|
for (const entry of entries.sort()) {
|
|
96
97
|
const candidate = path.join(projectsDir, entry, name);
|
|
97
|
-
if (fs.existsSync(candidate))
|
|
98
|
-
|
|
98
|
+
if (fs.existsSync(candidate)) {
|
|
99
|
+
candidates.push(candidate);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (candidates.length === 0)
|
|
103
|
+
return undefined;
|
|
104
|
+
if (candidates.length === 1)
|
|
105
|
+
return candidates[0];
|
|
106
|
+
// Multiple copies: prefer the one whose jsonl contains the requested cwd
|
|
107
|
+
// (handles EnterWorktree relocate where the same sessionId exists in
|
|
108
|
+
// multiple project dirs). Fall back to the first sorted candidate.
|
|
109
|
+
if (cwd) {
|
|
110
|
+
for (const candidate of candidates) {
|
|
111
|
+
if (jsonlContainsCwd(candidate, cwd))
|
|
112
|
+
return candidate;
|
|
113
|
+
}
|
|
99
114
|
}
|
|
100
|
-
return
|
|
115
|
+
return candidates[0];
|
|
101
116
|
}
|
|
102
117
|
/**
|
|
103
118
|
* P1-19: TTL cache for full project listings (mirror codex getSessionIndex's
|
|
@@ -125,7 +140,11 @@ function summarizeSession(filePath) {
|
|
|
125
140
|
const line = findJsonlLine(filePath, (l) => {
|
|
126
141
|
try {
|
|
127
142
|
const obj = JSON.parse(l);
|
|
128
|
-
|
|
143
|
+
if (obj.type === 'user' && !!obj.message?.content) {
|
|
144
|
+
const text = extractText(obj.message.content);
|
|
145
|
+
return !!text && !isTaskNotificationText(text);
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
129
148
|
}
|
|
130
149
|
catch {
|
|
131
150
|
return false;
|
|
@@ -148,6 +167,19 @@ const CLAUDE_MAPPING = {
|
|
|
148
167
|
toolInputField: 'input',
|
|
149
168
|
toolErrorField: 'is_error',
|
|
150
169
|
};
|
|
170
|
+
/**
|
|
171
|
+
* Detect Claude CLI sub-agent completion notification text.
|
|
172
|
+
* Claude CLI injects a `type:"user"` record with string content starting
|
|
173
|
+
* with `<task-notification>` when a sub-agent (Agent tool) finishes.
|
|
174
|
+
* These must be excluded from displayTitle/summary — they are machine
|
|
175
|
+
* injections, not real user input. Primary criterion: extracted text
|
|
176
|
+
* starts with `<task-notification` (works for both string and array
|
|
177
|
+
* content since `extractText` is called first). Secondary (optional):
|
|
178
|
+
* `origin.kind === 'task-notification'` (not all CLI versions write it).
|
|
179
|
+
*/
|
|
180
|
+
function isTaskNotificationText(text) {
|
|
181
|
+
return text.trimStart().startsWith('<task-notification');
|
|
182
|
+
}
|
|
151
183
|
/**
|
|
152
184
|
* Extract first text from a user message.
|
|
153
185
|
* Used by readFirstUserMessage.
|
|
@@ -172,7 +204,7 @@ function extractText(content) {
|
|
|
172
204
|
*
|
|
173
205
|
* We locate the directory via `projectDirForCwd` (lossy but OK for locating
|
|
174
206
|
* a known cwd) and then **verify** each jsonl's cwd field matches. This
|
|
175
|
-
* prevents collision: `
|
|
207
|
+
* prevents collision: `disk_d/foo` and `disk-d/foo` both encode to the same
|
|
176
208
|
* directory name, but only the matching cwd field is kept.
|
|
177
209
|
*/
|
|
178
210
|
export function listClaudeSessions(cwd, opts = {}) {
|
|
@@ -236,11 +268,21 @@ function scanDirForSessions(cwd, dir) {
|
|
|
236
268
|
return sessions;
|
|
237
269
|
}
|
|
238
270
|
/**
|
|
239
|
-
* Scan OTHER project subdirectories for jsonl files whose cwd matches
|
|
240
|
-
* requested cwd. Used
|
|
241
|
-
*
|
|
271
|
+
* Scan OTHER project subdirectories for jsonl files whose first cwd matches
|
|
272
|
+
* the requested cwd. Used to find sessions that were MOVED OUT of the primary
|
|
273
|
+
* dir by EnterWorktree (which relocates the transcript to the target worktree's
|
|
274
|
+
* project dir). Such sessions always have firstCwd == queryCwd because
|
|
275
|
+
* EnterWorktree happens after session start.
|
|
276
|
+
*
|
|
277
|
+
* Uses `readCwdFromJsonl` (first cwd only) instead of `jsonlContainsCwd`
|
|
278
|
+
* (full-file scan) because:
|
|
279
|
+
* - Relocated-out sessions always have firstCwd == queryCwd → readCwdFromJsonl
|
|
280
|
+
* is sufficient and ~10× faster (no full-file streaming).
|
|
281
|
+
* - Relocated-IN sessions are found by Phase 1's jsonlContainsCwd in the
|
|
282
|
+
* primary dir — no need for Phase 2 to find them again.
|
|
283
|
+
*
|
|
242
284
|
* Skips the primary dir (already scanned) and any sessionIds already found
|
|
243
|
-
* (avoids duplicates).
|
|
285
|
+
* (avoids duplicates). S2 fix 2026-08-10.
|
|
244
286
|
*/
|
|
245
287
|
function scanRelocatedSessions(cwd, primaryDir, projectsDir, seenIds) {
|
|
246
288
|
let entries;
|
|
@@ -281,13 +323,19 @@ function scanRelocatedSessions(cwd, primaryDir, projectsDir, seenIds) {
|
|
|
281
323
|
continue;
|
|
282
324
|
try {
|
|
283
325
|
const st = fs.statSync(full);
|
|
284
|
-
|
|
326
|
+
// S2: use readCwdFromJsonl (first cwd only) instead of
|
|
327
|
+
// jsonlContainsCwd (full-file scan). Relocated-out sessions
|
|
328
|
+
// always have firstCwd == queryCwd.
|
|
329
|
+
const firstCwd = readCwdFromJsonl(full);
|
|
330
|
+
if (firstCwd === undefined || firstCwd !== cwd)
|
|
285
331
|
continue;
|
|
286
332
|
sessions.push({
|
|
287
333
|
sessionId,
|
|
288
334
|
summary: truncate(summarizeSession(full), 60, { normalizeWhitespace: true }),
|
|
289
335
|
mtime: st.mtimeMs,
|
|
290
336
|
});
|
|
337
|
+
// Prevent duplicates if the same sessionId exists in multiple dirs
|
|
338
|
+
seenIds.add(sessionId);
|
|
291
339
|
}
|
|
292
340
|
catch (err) {
|
|
293
341
|
getLogger().warn(`[session] skip ${f}: ${err.message}`);
|
|
@@ -297,11 +345,14 @@ function scanRelocatedSessions(cwd, primaryDir, projectsDir, seenIds) {
|
|
|
297
345
|
return sessions;
|
|
298
346
|
}
|
|
299
347
|
/** Uncached scan of a claude project dir (the expensive part being cached).
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
348
|
+
* Phase 1 scans the primary project dir using jsonlContainsCwd (any cwd
|
|
349
|
+
* match) to find sessions including relocated-in ones.
|
|
350
|
+
* Phase 2 scans all OTHER project dirs using readCwdFromJsonl (first cwd
|
|
351
|
+
* only) to find relocated-out sessions (always firstCwd == queryCwd).
|
|
352
|
+
* S2 fix 2026-08-10: removed the primarySessions.length === 0 guard so
|
|
353
|
+
* relocated-out sessions are visible even when the primary dir has other
|
|
354
|
+
* sessions. readCwdFromJsonl is ~10× cheaper than jsonlContainsCwd for
|
|
355
|
+
* cross-dir scanning.
|
|
305
356
|
*/
|
|
306
357
|
function scanClaudeSessions(cwd, dir, projectsDir) {
|
|
307
358
|
const sessions = [];
|
|
@@ -309,17 +360,12 @@ function scanClaudeSessions(cwd, dir, projectsDir) {
|
|
|
309
360
|
const primarySessions = scanDirForSessions(cwd, dir);
|
|
310
361
|
sessions.push(...primarySessions);
|
|
311
362
|
// Phase 2: cross-dir fallback for relocated sessions whose transcript
|
|
312
|
-
// was moved away from the primary dir
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
// handled by jsonlContainsCwd on files in the primary dir (the file was
|
|
317
|
-
// moved TO the primary dir's encoded path). Skip to avoid O(all projects)
|
|
318
|
-
// overhead on every cached-miss list call.
|
|
363
|
+
// was moved away from the primary dir by EnterWorktree.
|
|
364
|
+
// Always executed (S2 fix): relocated-out sessions have firstCwd == queryCwd
|
|
365
|
+
// but are invisible to Phase 1 (file moved away). seenIds prevents
|
|
366
|
+
// double-counting sessions already found by Phase 1.
|
|
319
367
|
const effectiveProjectsDir = projectsDir ?? defaultProjectsDir();
|
|
320
|
-
if (
|
|
321
|
-
effectiveProjectsDir &&
|
|
322
|
-
dir.startsWith(effectiveProjectsDir)) {
|
|
368
|
+
if (effectiveProjectsDir && dir.startsWith(effectiveProjectsDir)) {
|
|
323
369
|
const seenIds = new Set(primarySessions.map((s) => s.sessionId));
|
|
324
370
|
const crossDirSessions = scanRelocatedSessions(cwd, dir, effectiveProjectsDir, seenIds);
|
|
325
371
|
sessions.push(...crossDirSessions);
|
|
@@ -385,7 +431,7 @@ export function isClaudeSessionActive(sessionId, cwd, opts = {}) {
|
|
|
385
431
|
let filePath = path.join(dir, `${sessionId}.jsonl`);
|
|
386
432
|
if (!fs.existsSync(filePath)) {
|
|
387
433
|
// EnterWorktree relocate: transcript moved to the new cwd's project dir.
|
|
388
|
-
const relocated = findSessionFileInProjects(sessionId, projectsDir);
|
|
434
|
+
const relocated = findSessionFileInProjects(sessionId, projectsDir, cwd);
|
|
389
435
|
if (!relocated)
|
|
390
436
|
return false;
|
|
391
437
|
filePath = relocated;
|
|
@@ -485,7 +531,7 @@ function scalarScan(filePath) {
|
|
|
485
531
|
content[0]?.type === 'tool_result';
|
|
486
532
|
if (!isToolResult) {
|
|
487
533
|
const userContent = extractText(content);
|
|
488
|
-
if (userContent) {
|
|
534
|
+
if (userContent && !isTaskNotificationText(userContent)) {
|
|
489
535
|
lastUserMessage = userContent;
|
|
490
536
|
}
|
|
491
537
|
}
|
|
@@ -583,7 +629,7 @@ export function readSessionContent(sessionId, cwd, opts = {}) {
|
|
|
583
629
|
let filePath = path.join(dir, `${sessionId}.jsonl`);
|
|
584
630
|
if (!fs.existsSync(filePath)) {
|
|
585
631
|
// EnterWorktree relocate: transcript moved to the new cwd's project dir.
|
|
586
|
-
const relocated = findSessionFileInProjects(sessionId, projectsDir);
|
|
632
|
+
const relocated = findSessionFileInProjects(sessionId, projectsDir, cwd);
|
|
587
633
|
if (!relocated)
|
|
588
634
|
return { events: [] };
|
|
589
635
|
filePath = relocated;
|
|
@@ -51,6 +51,9 @@ export function readCodexRollout(filePath) {
|
|
|
51
51
|
// pi/opencode /resume "last turn" display semantics). When
|
|
52
52
|
// last_token_usage is absent we derive it as total - previous_total.
|
|
53
53
|
let lastRawUsage;
|
|
54
|
+
// Context window limit from the same token_count event as lastRawUsage
|
|
55
|
+
// (codex reports info.model_context_window per turn; absent on old data).
|
|
56
|
+
let lastContextLimit;
|
|
54
57
|
// Final total_token_usage (cumulative across the whole session) for the
|
|
55
58
|
// Run card's "累计" display. Codex emits this alongside last_token_usage.
|
|
56
59
|
let lastTotalUsage;
|
|
@@ -95,6 +98,8 @@ export function readCodexRollout(filePath) {
|
|
|
95
98
|
}
|
|
96
99
|
if (raw && (raw.input_tokens || raw.cached_input_tokens || raw.output_tokens)) {
|
|
97
100
|
lastRawUsage = raw;
|
|
101
|
+
const ctxWindow = info?.model_context_window;
|
|
102
|
+
lastContextLimit = typeof ctxWindow === 'number' ? ctxWindow : undefined;
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
}
|
|
@@ -150,6 +155,7 @@ export function readCodexRollout(filePath) {
|
|
|
150
155
|
inputTokens: r.input_tokens - cached,
|
|
151
156
|
outputTokens: r.output_tokens,
|
|
152
157
|
contextLength: r.input_tokens,
|
|
158
|
+
contextLimit: lastContextLimit,
|
|
153
159
|
cacheReadTokens: cached,
|
|
154
160
|
cacheCreationTokens: 0,
|
|
155
161
|
totalTokens: r.total_tokens,
|
|
@@ -256,6 +262,9 @@ export function readCodexSessionContent(sessionId, opts = {}) {
|
|
|
256
262
|
// Cwd guard: when a cwd is provided, the session's working directory must
|
|
257
263
|
// match. Without this, /resume <id> finds sessions by global ID lookup and
|
|
258
264
|
// allows resuming a session from workspace B while the user is in workspace A.
|
|
265
|
+
// Codex has no relocation (no EnterWorktree equivalent), so a simple equality
|
|
266
|
+
// check suffices — unlike claude which needs jsonlContainsCwd to handle
|
|
267
|
+
// relocated sessions with multiple cwd values.
|
|
259
268
|
if (opts.cwd && entry.cwd !== opts.cwd) {
|
|
260
269
|
return { events: [] };
|
|
261
270
|
}
|
|
@@ -24,7 +24,9 @@ export type CwdGuardResult = 'verified' | 'failed' | 'unverifiable';
|
|
|
24
24
|
* - failed: session has a workDir but it doesn't match realCwd
|
|
25
25
|
* - unverifiable: session has no workDir field (cannot verify)
|
|
26
26
|
*
|
|
27
|
-
* Unverifiable
|
|
27
|
+
* Unverifiable sessions are handled by the caller: when no cwd source can
|
|
28
|
+
* verify the session, it is rejected (fail-closed), aligned with claude's
|
|
29
|
+
* cwd guard semantics.
|
|
28
30
|
*/
|
|
29
31
|
export declare function checkCwdGuard(state: KimiSessionState, realCwd: string): CwdGuardResult;
|
|
30
32
|
/**
|
|
@@ -105,7 +105,9 @@ export function extractWorkDir(state) {
|
|
|
105
105
|
* - failed: session has a workDir but it doesn't match realCwd
|
|
106
106
|
* - unverifiable: session has no workDir field (cannot verify)
|
|
107
107
|
*
|
|
108
|
-
* Unverifiable
|
|
108
|
+
* Unverifiable sessions are handled by the caller: when no cwd source can
|
|
109
|
+
* verify the session, it is rejected (fail-closed), aligned with claude's
|
|
110
|
+
* cwd guard semantics.
|
|
109
111
|
*/
|
|
110
112
|
export function checkCwdGuard(state, realCwd) {
|
|
111
113
|
const sessionWorkDir = extractWorkDir(state);
|
|
@@ -330,9 +332,13 @@ export class KimiSessionReader {
|
|
|
330
332
|
if (indexWorkDir && indexWorkDir !== realCwd) {
|
|
331
333
|
return { events: [] };
|
|
332
334
|
}
|
|
333
|
-
//
|
|
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).
|
|
334
339
|
if (!indexWorkDir) {
|
|
335
|
-
getLogger().warn(`[kimi-session-reader] no cwd source (state.json + index) for session ${sessionId},
|
|
340
|
+
getLogger().warn(`[kimi-session-reader] no cwd source (state.json + index) for session ${sessionId}, rejecting (fail-closed)`);
|
|
341
|
+
return { events: [] };
|
|
336
342
|
}
|
|
337
343
|
}
|
|
338
344
|
const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
|