lark-remote 0.1.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 +52 -0
- package/LICENSE +21 -0
- package/README.en.md +162 -0
- package/README.md +161 -0
- package/dist/binder.d.ts +57 -0
- package/dist/binder.js +105 -0
- package/dist/bridge/index.d.ts +338 -0
- package/dist/bridge/index.js +1281 -0
- package/dist/bridge/queue-manager.d.ts +277 -0
- package/dist/bridge/queue-manager.js +729 -0
- package/dist/card/bash-card-session.d.ts +78 -0
- package/dist/card/bash-card-session.js +174 -0
- package/dist/card/bash-renderer.d.ts +24 -0
- package/dist/card/bash-renderer.js +187 -0
- package/dist/card/card-budget.d.ts +31 -0
- package/dist/card/card-budget.js +280 -0
- package/dist/card/card-session.d.ts +83 -0
- package/dist/card/card-session.js +185 -0
- package/dist/card/card-shared.d.ts +28 -0
- package/dist/card/card-shared.js +99 -0
- package/dist/card/collapsible.d.ts +26 -0
- package/dist/card/collapsible.js +87 -0
- package/dist/card/run-card-session.d.ts +78 -0
- package/dist/card/run-card-session.js +165 -0
- package/dist/card/run-renderer.d.ts +66 -0
- package/dist/card/run-renderer.js +841 -0
- package/dist/card/run-state.d.ts +103 -0
- package/dist/card/run-state.js +313 -0
- package/dist/card/text-truncate.d.ts +56 -0
- package/dist/card/text-truncate.js +184 -0
- package/dist/card/time.d.ts +1 -0
- package/dist/card/time.js +14 -0
- package/dist/card/tool-render.d.ts +25 -0
- package/dist/card/tool-render.js +176 -0
- package/dist/cli.d.ts +28 -0
- package/dist/cli.js +89 -0
- package/dist/common/guards.d.ts +37 -0
- package/dist/common/guards.js +65 -0
- package/dist/config/codex-config.d.ts +113 -0
- package/dist/config/codex-config.js +428 -0
- package/dist/config/dir.d.ts +17 -0
- package/dist/config/dir.js +81 -0
- package/dist/config/index.d.ts +219 -0
- package/dist/config/index.js +388 -0
- package/dist/config/kimi-config.d.ts +33 -0
- package/dist/config/kimi-config.js +111 -0
- package/dist/config/opencode-config.d.ts +20 -0
- package/dist/config/opencode-config.js +124 -0
- package/dist/config/pi-config.d.ts +49 -0
- package/dist/config/pi-config.js +151 -0
- package/dist/config/wizard.d.ts +12 -0
- package/dist/config/wizard.js +84 -0
- package/dist/connector/file-limits.d.ts +7 -0
- package/dist/connector/file-limits.js +8 -0
- package/dist/connector/index.d.ts +86 -0
- package/dist/connector/index.js +340 -0
- package/dist/error-classification.d.ts +15 -0
- package/dist/error-classification.js +24 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +580 -0
- package/dist/instance-lock.d.ts +30 -0
- package/dist/instance-lock.js +184 -0
- package/dist/logger/index.d.ts +57 -0
- package/dist/logger/index.js +178 -0
- package/dist/order/index.d.ts +27 -0
- package/dist/order/index.js +90 -0
- package/dist/persistence/atomic-write.d.ts +10 -0
- package/dist/persistence/atomic-write.js +65 -0
- package/dist/persistence/load-json-file.d.ts +9 -0
- package/dist/persistence/load-json-file.js +23 -0
- package/dist/restart.d.ts +24 -0
- package/dist/restart.js +73 -0
- package/dist/router/card-helpers.d.ts +28 -0
- package/dist/router/card-helpers.js +65 -0
- package/dist/router/config/claude.d.ts +14 -0
- package/dist/router/config/claude.js +72 -0
- package/dist/router/config/codex.d.ts +22 -0
- package/dist/router/config/codex.js +100 -0
- package/dist/router/config/common/render.d.ts +16 -0
- package/dist/router/config/common/render.js +158 -0
- package/dist/router/config/index.d.ts +18 -0
- package/dist/router/config/index.js +38 -0
- package/dist/router/config/kimi.d.ts +21 -0
- package/dist/router/config/kimi.js +63 -0
- package/dist/router/config/opencode.d.ts +12 -0
- package/dist/router/config/opencode.js +44 -0
- package/dist/router/config/pi.d.ts +12 -0
- package/dist/router/config/pi.js +50 -0
- package/dist/router/config/types.d.ts +43 -0
- package/dist/router/config/types.js +8 -0
- package/dist/router/index.d.ts +318 -0
- package/dist/router/index.js +2715 -0
- package/dist/router/order-exec-dispatch.d.ts +30 -0
- package/dist/router/order-exec-dispatch.js +48 -0
- package/dist/router/utils.d.ts +44 -0
- package/dist/router/utils.js +116 -0
- package/dist/runner/bash/index.d.ts +4 -0
- package/dist/runner/bash/index.js +5 -0
- package/dist/runner/bash/runner.d.ts +55 -0
- package/dist/runner/bash/runner.js +213 -0
- package/dist/runner/claude/index.d.ts +4 -0
- package/dist/runner/claude/index.js +5 -0
- package/dist/runner/claude/runner.d.ts +29 -0
- package/dist/runner/claude/runner.js +83 -0
- package/dist/runner/codex/argv.d.ts +33 -0
- package/dist/runner/codex/argv.js +40 -0
- package/dist/runner/codex/index.d.ts +1 -0
- package/dist/runner/codex/index.js +2 -0
- package/dist/runner/codex/jsonl.d.ts +37 -0
- package/dist/runner/codex/jsonl.js +221 -0
- package/dist/runner/codex/runner.d.ts +65 -0
- package/dist/runner/codex/runner.js +124 -0
- package/dist/runner/common/exec-translator.d.ts +73 -0
- package/dist/runner/common/exec-translator.js +105 -0
- package/dist/runner/common/jsonl-stream.d.ts +39 -0
- package/dist/runner/common/jsonl-stream.js +203 -0
- package/dist/runner/common/process-stopper.d.ts +28 -0
- package/dist/runner/common/process-stopper.js +79 -0
- package/dist/runner/common/runner-utils.d.ts +21 -0
- package/dist/runner/common/runner-utils.js +65 -0
- package/dist/runner/common/spawn-heartbeat.d.ts +33 -0
- package/dist/runner/common/spawn-heartbeat.js +51 -0
- package/dist/runner/common/spawning-runner.d.ts +321 -0
- package/dist/runner/common/spawning-runner.js +634 -0
- package/dist/runner/common/usage.d.ts +31 -0
- package/dist/runner/common/usage.js +29 -0
- package/dist/runner/index.d.ts +19 -0
- package/dist/runner/index.js +24 -0
- package/dist/runner/kimi/index.d.ts +1 -0
- package/dist/runner/kimi/index.js +2 -0
- package/dist/runner/kimi/jsonl.d.ts +28 -0
- package/dist/runner/kimi/jsonl.js +143 -0
- package/dist/runner/kimi/runner.d.ts +50 -0
- package/dist/runner/kimi/runner.js +105 -0
- package/dist/runner/opencode/argv.d.ts +32 -0
- package/dist/runner/opencode/argv.js +35 -0
- package/dist/runner/opencode/index.d.ts +1 -0
- package/dist/runner/opencode/index.js +2 -0
- package/dist/runner/opencode/jsonl.d.ts +68 -0
- package/dist/runner/opencode/jsonl.js +295 -0
- package/dist/runner/opencode/runner.d.ts +63 -0
- package/dist/runner/opencode/runner.js +93 -0
- package/dist/runner/pi/index.d.ts +1 -0
- package/dist/runner/pi/index.js +2 -0
- package/dist/runner/pi/jsonl.d.ts +128 -0
- package/dist/runner/pi/jsonl.js +249 -0
- package/dist/runner/pi/runner.d.ts +37 -0
- package/dist/runner/pi/runner.js +76 -0
- package/dist/runner/registry.d.ts +54 -0
- package/dist/runner/registry.js +75 -0
- package/dist/runner/resolve-agent-choices.d.ts +7 -0
- package/dist/runner/resolve-agent-choices.js +76 -0
- package/dist/runner/sync-agent-choices.d.ts +6 -0
- package/dist/runner/sync-agent-choices.js +73 -0
- package/dist/runner/types.d.ts +250 -0
- package/dist/runner/types.js +8 -0
- package/dist/session/claude/index.d.ts +1 -0
- package/dist/session/claude/index.js +2 -0
- package/dist/session/claude/session-reader.d.ts +34 -0
- package/dist/session/claude/session-reader.js +46 -0
- package/dist/session/claude/sessions.d.ts +53 -0
- package/dist/session/claude/sessions.js +528 -0
- package/dist/session/codex/index.d.ts +1 -0
- package/dist/session/codex/index.js +2 -0
- package/dist/session/codex/rollout-reader.d.ts +88 -0
- package/dist/session/codex/rollout-reader.js +473 -0
- package/dist/session/codex/sessions.d.ts +21 -0
- package/dist/session/codex/sessions.js +40 -0
- package/dist/session/common/catch-up.d.ts +12 -0
- package/dist/session/common/catch-up.js +31 -0
- package/dist/session/common/constants.d.ts +11 -0
- package/dist/session/common/constants.js +12 -0
- package/dist/session/common/content-blocks.d.ts +37 -0
- package/dist/session/common/content-blocks.js +68 -0
- package/dist/session/common/jsonl.d.ts +77 -0
- package/dist/session/common/jsonl.js +351 -0
- package/dist/session/common/usage-accumulator.d.ts +58 -0
- package/dist/session/common/usage-accumulator.js +72 -0
- package/dist/session/index.d.ts +7 -0
- package/dist/session/index.js +11 -0
- package/dist/session/kimi/index.d.ts +1 -0
- package/dist/session/kimi/index.js +2 -0
- package/dist/session/kimi/sessions.d.ts +81 -0
- package/dist/session/kimi/sessions.js +527 -0
- package/dist/session/opencode/index.d.ts +1 -0
- package/dist/session/opencode/index.js +2 -0
- package/dist/session/opencode/sessions.d.ts +81 -0
- package/dist/session/opencode/sessions.js +388 -0
- package/dist/session/pi/index.d.ts +1 -0
- package/dist/session/pi/index.js +2 -0
- package/dist/session/pi/sessions.d.ts +27 -0
- package/dist/session/pi/sessions.js +399 -0
- package/dist/session/registry.d.ts +12 -0
- package/dist/session/registry.js +20 -0
- package/dist/session/session-store.d.ts +86 -0
- package/dist/session/session-store.js +450 -0
- package/dist/startup-contact.d.ts +18 -0
- package/dist/startup-contact.js +51 -0
- package/dist/workspace/index.d.ts +19 -0
- package/dist/workspace/index.js +53 -0
- package/package.json +93 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
|
|
2
|
+
export interface KimiSessionState {
|
|
3
|
+
version?: number;
|
|
4
|
+
createdAt: string | number;
|
|
5
|
+
updatedAt: string | number;
|
|
6
|
+
title?: string;
|
|
7
|
+
isCustomTitle?: boolean;
|
|
8
|
+
workDir?: string;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
lastPrompt?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Detect schema version from state.json content */
|
|
13
|
+
export declare function detectSchemaVersion(state: KimiSessionState): 1 | 2;
|
|
14
|
+
/**
|
|
15
|
+
* Extract the working directory from state.json, version-aware.
|
|
16
|
+
* v1 uses `workDir`, v2 uses `cwd`. Returns undefined when neither field is present.
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractWorkDir(state: KimiSessionState): string | undefined;
|
|
19
|
+
/** Cwd guard result: verified / failed / unverifiable */
|
|
20
|
+
export type CwdGuardResult = 'verified' | 'failed' | 'unverifiable';
|
|
21
|
+
/**
|
|
22
|
+
* Three-state cwd guard. Distinguishes:
|
|
23
|
+
* - verified: session's workDir matches realCwd
|
|
24
|
+
* - failed: session has a workDir but it doesn't match realCwd
|
|
25
|
+
* - unverifiable: session has no workDir field (cannot verify)
|
|
26
|
+
*
|
|
27
|
+
* Unverifiable is a fail-open state — the caller should allow access and log a warning.
|
|
28
|
+
*/
|
|
29
|
+
export declare function checkCwdGuard(state: KimiSessionState, realCwd: string): CwdGuardResult;
|
|
30
|
+
/**
|
|
31
|
+
* Kimi Session Reader
|
|
32
|
+
* Reads session history from ~/.kimi-code/
|
|
33
|
+
*/
|
|
34
|
+
export declare class KimiSessionReader implements AgentSessionReader {
|
|
35
|
+
private readonly kimiDir;
|
|
36
|
+
constructor(kimiDir?: string);
|
|
37
|
+
/**
|
|
38
|
+
* Scan the session index and return all parsed entries.
|
|
39
|
+
* Returns an empty array when the index file doesn't exist or is unreadable.
|
|
40
|
+
*/
|
|
41
|
+
private scanSessionIndex;
|
|
42
|
+
/**
|
|
43
|
+
* Find a single index entry by sessionId, or undefined if not found.
|
|
44
|
+
*/
|
|
45
|
+
private findSessionIndexEntry;
|
|
46
|
+
/**
|
|
47
|
+
* Convenience wrapper: find sessionDir from index, or null if not found.
|
|
48
|
+
*/
|
|
49
|
+
private findSessionDirFromIndex;
|
|
50
|
+
/**
|
|
51
|
+
* Extract the last turn.prompt text from a wire.jsonl file.
|
|
52
|
+
* Used as a fallback summary source when state.json has no title/lastPrompt
|
|
53
|
+
* (v2 sessions). Scans the tail of the file for efficiency.
|
|
54
|
+
* Returns undefined if no turn.prompt is found.
|
|
55
|
+
*/
|
|
56
|
+
private extractLastPromptFromWire;
|
|
57
|
+
/**
|
|
58
|
+
* List all sessions for a given working directory
|
|
59
|
+
*/
|
|
60
|
+
listSessions(cwd: string, opts?: {
|
|
61
|
+
limit?: number;
|
|
62
|
+
offset?: number;
|
|
63
|
+
}): {
|
|
64
|
+
sessions: AgentSession[];
|
|
65
|
+
total: number;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Get the newest session for a given working directory
|
|
69
|
+
*/
|
|
70
|
+
getNewestSession(cwd: string): AgentSession | null;
|
|
71
|
+
/**
|
|
72
|
+
* Read session content (events + usage)
|
|
73
|
+
*/
|
|
74
|
+
readSessionContent(sessionId: string, cwd: string, opts?: {
|
|
75
|
+
maxEvents?: number;
|
|
76
|
+
}): SessionContent;
|
|
77
|
+
/**
|
|
78
|
+
* Check if a session is currently active
|
|
79
|
+
*/
|
|
80
|
+
isSessionActive(sessionId: string, _cwd: string): boolean;
|
|
81
|
+
}
|
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { readJsonlLines as readJsonlLinesShared, readLastNJsonlLines } from '../common/jsonl.js';
|
|
5
|
+
import { UsageAccumulator } from '../common/usage-accumulator.js';
|
|
6
|
+
import { getLogger } from '../../logger/index.js';
|
|
7
|
+
import { STALE_MS } from '../common/constants.js';
|
|
8
|
+
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../card/text-truncate.js';
|
|
9
|
+
/** Default Kimi config directory */
|
|
10
|
+
function defaultKimiDir() {
|
|
11
|
+
return path.join(os.homedir(), '.kimi-code');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Max lines scanned backwards when looking for the last loop event.
|
|
15
|
+
* Bounds the tail scan so huge wire files are not fully parsed.
|
|
16
|
+
*/
|
|
17
|
+
const LOOP_EVENT_SCAN_LIMIT = 20;
|
|
18
|
+
/**
|
|
19
|
+
* Determine whether the last context.append_loop_event in a wire.jsonl is a
|
|
20
|
+
* step.end (turn completed). kimi writes usage.record AFTER step.end at turn
|
|
21
|
+
* completion, so judging by the raw last line never sees the step.end — scan
|
|
22
|
+
* backwards from the tail, skipping non-loop entries (usage.record etc.),
|
|
23
|
+
* until the last loop event is found. Returns false when no loop event is
|
|
24
|
+
* found within the scan window (conservative: caller keeps the session
|
|
25
|
+
* considered active).
|
|
26
|
+
*/
|
|
27
|
+
function lastLoopEventIsStepEnd(lines) {
|
|
28
|
+
for (let i = lines.length - 1, scanned = 0; i >= 0 && scanned < LOOP_EVENT_SCAN_LIMIT; i--, scanned++) {
|
|
29
|
+
try {
|
|
30
|
+
const entry = JSON.parse(lines[i]);
|
|
31
|
+
if (entry.type !== 'context.append_loop_event') {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const loopEvent = entry;
|
|
35
|
+
return loopEvent.event.type === 'step.end';
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Skip malformed lines and keep scanning backwards
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
/** Cache for JSONL file contents to avoid repeated full reads */
|
|
45
|
+
const jsonlCache = new Map();
|
|
46
|
+
const CACHE_TTL_MS = 5000; // 5 second cache TTL
|
|
47
|
+
/** Upper bound on cached files; evict the least-recently-used entry past this. */
|
|
48
|
+
const CACHE_MAX_ENTRIES = 32;
|
|
49
|
+
/**
|
|
50
|
+
* Read lines from a JSONL file with caching.
|
|
51
|
+
* Delegates to the shared readJsonlLines for the actual file read,
|
|
52
|
+
* but wraps it with a TTL-based mtime cache to avoid redundant I/O.
|
|
53
|
+
*/
|
|
54
|
+
function readJsonlLines(filePath) {
|
|
55
|
+
try {
|
|
56
|
+
const stat = fs.statSync(filePath);
|
|
57
|
+
const mtime = stat.mtimeMs;
|
|
58
|
+
const cached = jsonlCache.get(filePath);
|
|
59
|
+
// P1-17: TTL must be measured from the cache-write time (cachedAt), not
|
|
60
|
+
// the FILE mtime — a file modified an hour ago would otherwise never hit
|
|
61
|
+
// the cache, making every call re-read and re-write the same entry.
|
|
62
|
+
if (cached && mtime === cached.mtime && Date.now() - cached.cachedAt < CACHE_TTL_MS) {
|
|
63
|
+
// LRU refresh on hit: re-insert so recently-used entries survive eviction.
|
|
64
|
+
jsonlCache.delete(filePath);
|
|
65
|
+
jsonlCache.set(filePath, cached);
|
|
66
|
+
return cached.lines;
|
|
67
|
+
}
|
|
68
|
+
const lines = readJsonlLinesShared(filePath);
|
|
69
|
+
jsonlCache.set(filePath, { mtime, cachedAt: Date.now(), lines });
|
|
70
|
+
// P1-17: bound the cache. Map iteration is insertion order; after the LRU
|
|
71
|
+
// refresh above the least-recently-used entry is the first key.
|
|
72
|
+
if (jsonlCache.size > CACHE_MAX_ENTRIES) {
|
|
73
|
+
const oldest = jsonlCache.keys().next().value;
|
|
74
|
+
if (oldest !== undefined)
|
|
75
|
+
jsonlCache.delete(oldest);
|
|
76
|
+
}
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Detect schema version from state.json content */
|
|
84
|
+
export function detectSchemaVersion(state) {
|
|
85
|
+
if (state.version === 2)
|
|
86
|
+
return 2;
|
|
87
|
+
// Heuristic: v2 has `cwd` field and no `workDir`
|
|
88
|
+
if (state.cwd !== undefined && state.workDir === undefined)
|
|
89
|
+
return 2;
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Extract the working directory from state.json, version-aware.
|
|
94
|
+
* v1 uses `workDir`, v2 uses `cwd`. Returns undefined when neither field is present.
|
|
95
|
+
*/
|
|
96
|
+
export function extractWorkDir(state) {
|
|
97
|
+
const version = detectSchemaVersion(state);
|
|
98
|
+
if (version === 2)
|
|
99
|
+
return state.cwd;
|
|
100
|
+
return state.workDir;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Three-state cwd guard. Distinguishes:
|
|
104
|
+
* - verified: session's workDir matches realCwd
|
|
105
|
+
* - failed: session has a workDir but it doesn't match realCwd
|
|
106
|
+
* - unverifiable: session has no workDir field (cannot verify)
|
|
107
|
+
*
|
|
108
|
+
* Unverifiable is a fail-open state — the caller should allow access and log a warning.
|
|
109
|
+
*/
|
|
110
|
+
export function checkCwdGuard(state, realCwd) {
|
|
111
|
+
const sessionWorkDir = extractWorkDir(state);
|
|
112
|
+
if (sessionWorkDir === undefined)
|
|
113
|
+
return 'unverifiable';
|
|
114
|
+
if (sessionWorkDir === realCwd)
|
|
115
|
+
return 'verified';
|
|
116
|
+
return 'failed';
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Kimi Session Reader
|
|
120
|
+
* Reads session history from ~/.kimi-code/
|
|
121
|
+
*/
|
|
122
|
+
export class KimiSessionReader {
|
|
123
|
+
kimiDir;
|
|
124
|
+
constructor(kimiDir) {
|
|
125
|
+
this.kimiDir = kimiDir ?? defaultKimiDir();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Scan the session index and return all parsed entries.
|
|
129
|
+
* Returns an empty array when the index file doesn't exist or is unreadable.
|
|
130
|
+
*/
|
|
131
|
+
scanSessionIndex() {
|
|
132
|
+
const indexPath = path.join(this.kimiDir, 'session_index.jsonl');
|
|
133
|
+
if (!fs.existsSync(indexPath))
|
|
134
|
+
return [];
|
|
135
|
+
const entries = [];
|
|
136
|
+
for (const line of readJsonlLines(indexPath)) {
|
|
137
|
+
try {
|
|
138
|
+
entries.push(JSON.parse(line));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return entries;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Find a single index entry by sessionId, or undefined if not found.
|
|
148
|
+
*/
|
|
149
|
+
findSessionIndexEntry(sessionId) {
|
|
150
|
+
return this.scanSessionIndex().find((e) => e.sessionId === sessionId);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Convenience wrapper: find sessionDir from index, or null if not found.
|
|
154
|
+
*/
|
|
155
|
+
findSessionDirFromIndex(sessionId) {
|
|
156
|
+
return this.findSessionIndexEntry(sessionId)?.sessionDir ?? null;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Extract the last turn.prompt text from a wire.jsonl file.
|
|
160
|
+
* Used as a fallback summary source when state.json has no title/lastPrompt
|
|
161
|
+
* (v2 sessions). Scans the tail of the file for efficiency.
|
|
162
|
+
* Returns undefined if no turn.prompt is found.
|
|
163
|
+
*/
|
|
164
|
+
extractLastPromptFromWire(wirePath) {
|
|
165
|
+
try {
|
|
166
|
+
if (!fs.existsSync(wirePath))
|
|
167
|
+
return undefined;
|
|
168
|
+
// Scan backwards from the tail — turn.prompt is typically near the end
|
|
169
|
+
// of a completed turn (before content.part / step.end / usage.record).
|
|
170
|
+
const lines = readLastNJsonlLines(wirePath, 50);
|
|
171
|
+
let lastPrompt;
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
try {
|
|
174
|
+
const entry = JSON.parse(line);
|
|
175
|
+
if (entry.type === 'turn.prompt') {
|
|
176
|
+
const prompt = entry;
|
|
177
|
+
if (prompt.input && prompt.input.length > 0) {
|
|
178
|
+
lastPrompt = prompt.input[0].text;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return lastPrompt?.substring(0, 200);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* List all sessions for a given working directory
|
|
194
|
+
*/
|
|
195
|
+
listSessions(cwd, opts) {
|
|
196
|
+
try {
|
|
197
|
+
const realCwd = fs.realpathSync(cwd);
|
|
198
|
+
const sessions = [];
|
|
199
|
+
for (const entry of this.scanSessionIndex()) {
|
|
200
|
+
// Index workDir is v1-only; v2 sessions may have empty/missing workDir.
|
|
201
|
+
// When index workDir is absent, fall back to state.json's cwd field.
|
|
202
|
+
let entryWorkDir = entry.workDir;
|
|
203
|
+
if (!entryWorkDir) {
|
|
204
|
+
const statePath = path.join(entry.sessionDir, 'state.json');
|
|
205
|
+
try {
|
|
206
|
+
if (fs.existsSync(statePath)) {
|
|
207
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
208
|
+
entryWorkDir = extractWorkDir(state);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Unreadable state — cannot determine cwd, skip
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (entryWorkDir !== realCwd) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const statePath = path.join(entry.sessionDir, 'state.json');
|
|
219
|
+
let summary = 'New Session';
|
|
220
|
+
if (fs.existsSync(statePath)) {
|
|
221
|
+
try {
|
|
222
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
223
|
+
// v1: state.title / state.lastPrompt; v2: these fields may be absent
|
|
224
|
+
summary = state.title || state.lastPrompt || '';
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Corrupt state.json — fall through to wire.jsonl extraction
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// v2 state.json omits title/lastPrompt: extract from wire.jsonl's last
|
|
231
|
+
// turn.prompt instead. Only scan a small tail to find it — the summary
|
|
232
|
+
// only needs the most recent user prompt.
|
|
233
|
+
if (!summary) {
|
|
234
|
+
const wirePath = path.join(entry.sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
235
|
+
summary = this.extractLastPromptFromWire(wirePath) || 'New Session';
|
|
236
|
+
}
|
|
237
|
+
// Get mtime. P1-16: the session DIRECTORY mtime only changes when
|
|
238
|
+
// entries are created/deleted/renamed inside it — kimi appending to
|
|
239
|
+
// agents/main/wire.jsonl during a run does NOT touch it, so dir-mtime
|
|
240
|
+
// sorting lags real activity by minutes. Use the wire.jsonl mtime
|
|
241
|
+
// (the file kimi keeps writing while the session is active) and fall
|
|
242
|
+
// back to the dir mtime when the wire file is missing (e.g. a session
|
|
243
|
+
// that never produced output).
|
|
244
|
+
const wirePath = path.join(entry.sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
245
|
+
let mtimeMs;
|
|
246
|
+
try {
|
|
247
|
+
mtimeMs = fs.statSync(wirePath).mtimeMs;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
mtimeMs = fs.statSync(entry.sessionDir).mtimeMs;
|
|
251
|
+
}
|
|
252
|
+
sessions.push({
|
|
253
|
+
sessionId: entry.sessionId,
|
|
254
|
+
summary: summary.substring(0, 200), // Truncate long titles
|
|
255
|
+
mtime: mtimeMs,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
// Sort by mtime descending; same-mtime ties use a deterministic
|
|
259
|
+
// secondary key so ordering is stable across scans.
|
|
260
|
+
sessions.sort((a, b) => b.mtime - a.mtime || a.sessionId.localeCompare(b.sessionId));
|
|
261
|
+
const total = sessions.length;
|
|
262
|
+
const offset = Math.max(0, opts?.offset ?? 0);
|
|
263
|
+
// Default limit 20, aligned with the other agent session readers
|
|
264
|
+
// (claude/codex/opencode/pi); no opts -> return up to 20 sessions.
|
|
265
|
+
const limit = opts?.limit ?? 20;
|
|
266
|
+
return {
|
|
267
|
+
sessions: sessions.slice(offset, offset + limit),
|
|
268
|
+
total,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
getLogger().error(`[kimi-session-reader] listSessions error: ${error}`);
|
|
273
|
+
return { sessions: [], total: 0 };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Get the newest session for a given working directory
|
|
278
|
+
*/
|
|
279
|
+
getNewestSession(cwd) {
|
|
280
|
+
return this.listSessions(cwd, { limit: 1 }).sessions[0] ?? null;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Read session content (events + usage)
|
|
284
|
+
*/
|
|
285
|
+
readSessionContent(sessionId, cwd, opts) {
|
|
286
|
+
// Find session directory from index
|
|
287
|
+
const sessionDir = this.findSessionDirFromIndex(sessionId);
|
|
288
|
+
if (!sessionDir) {
|
|
289
|
+
return { events: [] };
|
|
290
|
+
}
|
|
291
|
+
const statePath = path.join(sessionDir, 'state.json');
|
|
292
|
+
try {
|
|
293
|
+
if (fs.existsSync(statePath)) {
|
|
294
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
295
|
+
const realCwd = fs.realpathSync(cwd);
|
|
296
|
+
const guardResult = checkCwdGuard(state, realCwd);
|
|
297
|
+
if (guardResult === 'failed') {
|
|
298
|
+
return { events: [] };
|
|
299
|
+
}
|
|
300
|
+
if (guardResult === 'unverifiable') {
|
|
301
|
+
getLogger().warn(`[kimi-session-reader] state.json has no workDir/cwd field for session ${sessionId}, skipping cwd guard`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// Unverifiable — skip the check
|
|
307
|
+
}
|
|
308
|
+
const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
309
|
+
if (!fs.existsSync(wirePath)) {
|
|
310
|
+
return { events: [] };
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const lines = Array.from(readJsonlLines(wirePath));
|
|
314
|
+
const maxEvents = opts?.maxEvents ?? Number.MAX_SAFE_INTEGER;
|
|
315
|
+
const events = [];
|
|
316
|
+
let displayTitle;
|
|
317
|
+
const acc = new UsageAccumulator();
|
|
318
|
+
// contextLength is only from the main wire's last usage.record
|
|
319
|
+
// (subagent context is an independent window). Tracked separately
|
|
320
|
+
// from the accumulator's `last` (which could be a subagent record).
|
|
321
|
+
let mainContextLength = 0;
|
|
322
|
+
// Distinguish "no usage.record in any wire file" (no data → usage:
|
|
323
|
+
// undefined, downstream omits the token block) from "records exist but
|
|
324
|
+
// are all zero / malformed" (real data → aggregate with ?? 0 and return
|
|
325
|
+
// the zeros as-is). A malformed record (usage.record line with missing
|
|
326
|
+
// usage object/fields) still counts as data.
|
|
327
|
+
let hasUsageRecord = false;
|
|
328
|
+
// ccusage-style session-wide aggregation via UsageAccumulator: sum
|
|
329
|
+
// every usage.record's per-component tokens; contextLength is the
|
|
330
|
+
// current context window occupancy (not summable) — keep only the
|
|
331
|
+
// last record's value, and only from the main wire (subagent context
|
|
332
|
+
// is an independent window). Aggregation covers the whole wire.jsonl
|
|
333
|
+
// and is independent of the maxEvents display slice
|
|
334
|
+
// (usage = the session's overall token cost).
|
|
335
|
+
const accumulateUsage = (u, updateContextLength) => {
|
|
336
|
+
hasUsageRecord = true;
|
|
337
|
+
// Malformed records (missing usage object or missing fields) must
|
|
338
|
+
// not poison the accumulators with NaN: count missing fields as 0.
|
|
339
|
+
const inputOther = u?.inputOther ?? 0;
|
|
340
|
+
const output = u?.output ?? 0;
|
|
341
|
+
const inputCacheRead = u?.inputCacheRead ?? 0;
|
|
342
|
+
const inputCacheCreation = u?.inputCacheCreation ?? 0;
|
|
343
|
+
acc.add({
|
|
344
|
+
input: inputOther,
|
|
345
|
+
output,
|
|
346
|
+
cacheRead: inputCacheRead,
|
|
347
|
+
cacheCreation: inputCacheCreation,
|
|
348
|
+
});
|
|
349
|
+
if (updateContextLength) {
|
|
350
|
+
// review P2-8 unified contract: input + cacheRead + cacheCreation
|
|
351
|
+
// (excludes output — generated, not part of the input window).
|
|
352
|
+
mainContextLength = inputOther + inputCacheRead + inputCacheCreation;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
356
|
+
const line = lines[lineIndex];
|
|
357
|
+
try {
|
|
358
|
+
const entry = JSON.parse(line);
|
|
359
|
+
// Extract usage from usage.record.
|
|
360
|
+
if (entry.type === 'usage.record') {
|
|
361
|
+
const usageEntry = entry;
|
|
362
|
+
accumulateUsage(usageEntry.usage, true);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
// P2-7: displayTitle + display events now scan ALL lines (not a
|
|
366
|
+
// line-slice), then `events.slice(-maxEvents)` keeps the LAST N
|
|
367
|
+
// EVENTS. Previously `lines.slice(-maxEvents)` cut raw lines, but
|
|
368
|
+
// usage.record / step.begin / turn.prompt lines produce no display
|
|
369
|
+
// event, so "last 5 lines" could yield only 1~2 visible events —
|
|
370
|
+
// and `slice(-0)` returned the full array. displayTitle takes the
|
|
371
|
+
// last turn.prompt across the whole session (the most recent user
|
|
372
|
+
// prompt), which is the correct catch-up title.
|
|
373
|
+
// Extract display title from turn.prompt
|
|
374
|
+
if (entry.type === 'turn.prompt') {
|
|
375
|
+
const prompt = entry;
|
|
376
|
+
if (prompt.input && prompt.input.length > 0) {
|
|
377
|
+
displayTitle = prompt.input[0].text.substring(0, 100);
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
// Convert loop events to content events
|
|
382
|
+
if (entry.type === 'context.append_loop_event') {
|
|
383
|
+
const loopEvent = entry;
|
|
384
|
+
const evt = loopEvent.event;
|
|
385
|
+
if (evt.type === 'content.part' && evt.part) {
|
|
386
|
+
if (evt.part.type === 'think') {
|
|
387
|
+
// Skip thinking blocks for display
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
else if (evt.part.type === 'text' && evt.part.text) {
|
|
391
|
+
events.push({
|
|
392
|
+
type: 'text',
|
|
393
|
+
content: evt.part.text,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else if (evt.type === 'tool.call' && evt.name) {
|
|
398
|
+
// 对齐 claude content-blocks 的 200 字符截断:大 args(整段
|
|
399
|
+
// 文件内容写入、长 prompt)原文进事件会把 resume 卡顶到
|
|
400
|
+
// 28KB 预算之外(2026-08-08 extreme_fallback 故障的推手之一)。
|
|
401
|
+
let inputStr = JSON.stringify(evt.args ?? {});
|
|
402
|
+
if (inputStr.length > 200) {
|
|
403
|
+
inputStr = inputStr.replace(/\n/g, ' ').slice(0, 197) + '...';
|
|
404
|
+
}
|
|
405
|
+
events.push({
|
|
406
|
+
type: 'tool_use',
|
|
407
|
+
content: `${evt.name}(${inputStr})`,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
else if (evt.type === 'tool.result') {
|
|
411
|
+
// result is { output }, { output, note }, or { isError, output }
|
|
412
|
+
// 对齐 opencode reader 的 L2 预折叠:单条 tool_result 上限
|
|
413
|
+
// TOOL_RESULT_MAX_BYTES,病理级输出(如 51KB 文件读取)不会
|
|
414
|
+
// 撑爆 resume 卡,也约束 events[] 内存占用。
|
|
415
|
+
const rawOut = evt.result?.output ?? `[tool result for ${evt.toolCallId}]`;
|
|
416
|
+
events.push({
|
|
417
|
+
type: 'tool_result',
|
|
418
|
+
content: truncateUtf8(rawOut, TOOL_RESULT_MAX_BYTES),
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Subagent wire files (agents/<name>/wire.jsonl, e.g. Task-derived
|
|
428
|
+
// agent-0..N) belong to the same session and the same billing entity:
|
|
429
|
+
// their token cost must be summed into the session-wide usage.
|
|
430
|
+
// contextLength stays main-only (subagent context is an independent
|
|
431
|
+
// window). Display events/title also stay main-only. Any unreadable
|
|
432
|
+
// directory or missing/corrupt wire file is skipped silently.
|
|
433
|
+
const agentsDir = path.join(sessionDir, 'agents');
|
|
434
|
+
try {
|
|
435
|
+
for (const dirent of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
436
|
+
if (!dirent.isDirectory() || dirent.name === 'main') {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const subWirePath = path.join(agentsDir, dirent.name, 'wire.jsonl');
|
|
440
|
+
if (!fs.existsSync(subWirePath)) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
for (const subLine of readJsonlLines(subWirePath)) {
|
|
444
|
+
try {
|
|
445
|
+
const subEntry = JSON.parse(subLine);
|
|
446
|
+
if (subEntry.type === 'usage.record') {
|
|
447
|
+
const usageEntry = subEntry;
|
|
448
|
+
accumulateUsage(usageEntry.usage, false);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// agents dir unreadable — fall back to main-only usage
|
|
459
|
+
}
|
|
460
|
+
// Build the final AgentSessionUsage from accumulator totals + main contextLength.
|
|
461
|
+
const t = acc.totals;
|
|
462
|
+
// Kimi semantics: preserve 0 values (not undefined) for cache fields to match original
|
|
463
|
+
const totalUsage = {
|
|
464
|
+
inputTokens: t.input,
|
|
465
|
+
outputTokens: t.output,
|
|
466
|
+
contextLength: mainContextLength,
|
|
467
|
+
cacheReadTokens: t.cacheRead > 0 ? t.cacheRead : 0,
|
|
468
|
+
cacheCreationTokens: t.cacheCreation > 0 ? t.cacheCreation : 0,
|
|
469
|
+
totalTokens: t.input + t.output + t.cacheRead + t.cacheCreation,
|
|
470
|
+
// Cumulative (session-wide): kimi sums all usage.records; mirrors
|
|
471
|
+
// inputTokens/outputTokens which are already session totals.
|
|
472
|
+
cumulativeTotalTokens: t.input + t.output + t.cacheRead + t.cacheCreation,
|
|
473
|
+
cumulativeInputTokens: t.input,
|
|
474
|
+
cumulativeOutputTokens: t.output,
|
|
475
|
+
cumulativeCacheReadTokens: t.cacheRead > 0 ? t.cacheRead : 0,
|
|
476
|
+
cumulativeCacheCreationTokens: t.cacheCreation > 0 ? t.cacheCreation : 0,
|
|
477
|
+
};
|
|
478
|
+
return {
|
|
479
|
+
events: maxEvents <= 0 ? [] : events.slice(-maxEvents),
|
|
480
|
+
usage: hasUsageRecord ? totalUsage : undefined,
|
|
481
|
+
displayTitle,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
getLogger().error(`[kimi-session-reader] readSessionContent error: ${error}`);
|
|
486
|
+
return { events: [] };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Check if a session is currently active
|
|
491
|
+
*/
|
|
492
|
+
isSessionActive(sessionId, _cwd) {
|
|
493
|
+
const sessionDir = this.findSessionDirFromIndex(sessionId);
|
|
494
|
+
if (!sessionDir) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
498
|
+
if (!fs.existsSync(wirePath)) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
const stat = fs.statSync(wirePath);
|
|
503
|
+
const now = Date.now();
|
|
504
|
+
// Check mtime freshness
|
|
505
|
+
if (now - stat.mtimeMs > STALE_MS) {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
// Read the tail to check for active indicators. kimi appends
|
|
509
|
+
// usage.record after step.end at turn completion, so the last loop
|
|
510
|
+
// event (not the last line) decides whether the session is complete.
|
|
511
|
+
// P2-5: `lastLoopEventIsStepEnd` only scans backwards up to
|
|
512
|
+
// LOOP_EVENT_SCAN_LIMIT lines, so we read only that many trailing lines
|
|
513
|
+
// via the tail-only helper instead of full-slurping the whole wire log
|
|
514
|
+
// (which can be multi-MB) into a string[].
|
|
515
|
+
const lines = readLastNJsonlLines(wirePath, LOOP_EVENT_SCAN_LIMIT);
|
|
516
|
+
if (lines.length === 0) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
// Consider the session inactive when its last loop event is step.end
|
|
520
|
+
return !lastLoopEventIsStepEnd(lines);
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OpencodeSessionReader } from './sessions.js';
|