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.
Files changed (201) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +162 -0
  4. package/README.md +161 -0
  5. package/dist/binder.d.ts +57 -0
  6. package/dist/binder.js +105 -0
  7. package/dist/bridge/index.d.ts +338 -0
  8. package/dist/bridge/index.js +1281 -0
  9. package/dist/bridge/queue-manager.d.ts +277 -0
  10. package/dist/bridge/queue-manager.js +729 -0
  11. package/dist/card/bash-card-session.d.ts +78 -0
  12. package/dist/card/bash-card-session.js +174 -0
  13. package/dist/card/bash-renderer.d.ts +24 -0
  14. package/dist/card/bash-renderer.js +187 -0
  15. package/dist/card/card-budget.d.ts +31 -0
  16. package/dist/card/card-budget.js +280 -0
  17. package/dist/card/card-session.d.ts +83 -0
  18. package/dist/card/card-session.js +185 -0
  19. package/dist/card/card-shared.d.ts +28 -0
  20. package/dist/card/card-shared.js +99 -0
  21. package/dist/card/collapsible.d.ts +26 -0
  22. package/dist/card/collapsible.js +87 -0
  23. package/dist/card/run-card-session.d.ts +78 -0
  24. package/dist/card/run-card-session.js +165 -0
  25. package/dist/card/run-renderer.d.ts +66 -0
  26. package/dist/card/run-renderer.js +841 -0
  27. package/dist/card/run-state.d.ts +103 -0
  28. package/dist/card/run-state.js +313 -0
  29. package/dist/card/text-truncate.d.ts +56 -0
  30. package/dist/card/text-truncate.js +184 -0
  31. package/dist/card/time.d.ts +1 -0
  32. package/dist/card/time.js +14 -0
  33. package/dist/card/tool-render.d.ts +25 -0
  34. package/dist/card/tool-render.js +176 -0
  35. package/dist/cli.d.ts +28 -0
  36. package/dist/cli.js +89 -0
  37. package/dist/common/guards.d.ts +37 -0
  38. package/dist/common/guards.js +65 -0
  39. package/dist/config/codex-config.d.ts +113 -0
  40. package/dist/config/codex-config.js +428 -0
  41. package/dist/config/dir.d.ts +17 -0
  42. package/dist/config/dir.js +81 -0
  43. package/dist/config/index.d.ts +219 -0
  44. package/dist/config/index.js +388 -0
  45. package/dist/config/kimi-config.d.ts +33 -0
  46. package/dist/config/kimi-config.js +111 -0
  47. package/dist/config/opencode-config.d.ts +20 -0
  48. package/dist/config/opencode-config.js +124 -0
  49. package/dist/config/pi-config.d.ts +49 -0
  50. package/dist/config/pi-config.js +151 -0
  51. package/dist/config/wizard.d.ts +12 -0
  52. package/dist/config/wizard.js +84 -0
  53. package/dist/connector/file-limits.d.ts +7 -0
  54. package/dist/connector/file-limits.js +8 -0
  55. package/dist/connector/index.d.ts +86 -0
  56. package/dist/connector/index.js +340 -0
  57. package/dist/error-classification.d.ts +15 -0
  58. package/dist/error-classification.js +24 -0
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +580 -0
  61. package/dist/instance-lock.d.ts +30 -0
  62. package/dist/instance-lock.js +184 -0
  63. package/dist/logger/index.d.ts +57 -0
  64. package/dist/logger/index.js +178 -0
  65. package/dist/order/index.d.ts +27 -0
  66. package/dist/order/index.js +90 -0
  67. package/dist/persistence/atomic-write.d.ts +10 -0
  68. package/dist/persistence/atomic-write.js +65 -0
  69. package/dist/persistence/load-json-file.d.ts +9 -0
  70. package/dist/persistence/load-json-file.js +23 -0
  71. package/dist/restart.d.ts +24 -0
  72. package/dist/restart.js +73 -0
  73. package/dist/router/card-helpers.d.ts +28 -0
  74. package/dist/router/card-helpers.js +65 -0
  75. package/dist/router/config/claude.d.ts +14 -0
  76. package/dist/router/config/claude.js +72 -0
  77. package/dist/router/config/codex.d.ts +22 -0
  78. package/dist/router/config/codex.js +100 -0
  79. package/dist/router/config/common/render.d.ts +16 -0
  80. package/dist/router/config/common/render.js +158 -0
  81. package/dist/router/config/index.d.ts +18 -0
  82. package/dist/router/config/index.js +38 -0
  83. package/dist/router/config/kimi.d.ts +21 -0
  84. package/dist/router/config/kimi.js +63 -0
  85. package/dist/router/config/opencode.d.ts +12 -0
  86. package/dist/router/config/opencode.js +44 -0
  87. package/dist/router/config/pi.d.ts +12 -0
  88. package/dist/router/config/pi.js +50 -0
  89. package/dist/router/config/types.d.ts +43 -0
  90. package/dist/router/config/types.js +8 -0
  91. package/dist/router/index.d.ts +318 -0
  92. package/dist/router/index.js +2715 -0
  93. package/dist/router/order-exec-dispatch.d.ts +30 -0
  94. package/dist/router/order-exec-dispatch.js +48 -0
  95. package/dist/router/utils.d.ts +44 -0
  96. package/dist/router/utils.js +116 -0
  97. package/dist/runner/bash/index.d.ts +4 -0
  98. package/dist/runner/bash/index.js +5 -0
  99. package/dist/runner/bash/runner.d.ts +55 -0
  100. package/dist/runner/bash/runner.js +213 -0
  101. package/dist/runner/claude/index.d.ts +4 -0
  102. package/dist/runner/claude/index.js +5 -0
  103. package/dist/runner/claude/runner.d.ts +29 -0
  104. package/dist/runner/claude/runner.js +83 -0
  105. package/dist/runner/codex/argv.d.ts +33 -0
  106. package/dist/runner/codex/argv.js +40 -0
  107. package/dist/runner/codex/index.d.ts +1 -0
  108. package/dist/runner/codex/index.js +2 -0
  109. package/dist/runner/codex/jsonl.d.ts +37 -0
  110. package/dist/runner/codex/jsonl.js +221 -0
  111. package/dist/runner/codex/runner.d.ts +65 -0
  112. package/dist/runner/codex/runner.js +124 -0
  113. package/dist/runner/common/exec-translator.d.ts +73 -0
  114. package/dist/runner/common/exec-translator.js +105 -0
  115. package/dist/runner/common/jsonl-stream.d.ts +39 -0
  116. package/dist/runner/common/jsonl-stream.js +203 -0
  117. package/dist/runner/common/process-stopper.d.ts +28 -0
  118. package/dist/runner/common/process-stopper.js +79 -0
  119. package/dist/runner/common/runner-utils.d.ts +21 -0
  120. package/dist/runner/common/runner-utils.js +65 -0
  121. package/dist/runner/common/spawn-heartbeat.d.ts +33 -0
  122. package/dist/runner/common/spawn-heartbeat.js +51 -0
  123. package/dist/runner/common/spawning-runner.d.ts +321 -0
  124. package/dist/runner/common/spawning-runner.js +634 -0
  125. package/dist/runner/common/usage.d.ts +31 -0
  126. package/dist/runner/common/usage.js +29 -0
  127. package/dist/runner/index.d.ts +19 -0
  128. package/dist/runner/index.js +24 -0
  129. package/dist/runner/kimi/index.d.ts +1 -0
  130. package/dist/runner/kimi/index.js +2 -0
  131. package/dist/runner/kimi/jsonl.d.ts +28 -0
  132. package/dist/runner/kimi/jsonl.js +143 -0
  133. package/dist/runner/kimi/runner.d.ts +50 -0
  134. package/dist/runner/kimi/runner.js +105 -0
  135. package/dist/runner/opencode/argv.d.ts +32 -0
  136. package/dist/runner/opencode/argv.js +35 -0
  137. package/dist/runner/opencode/index.d.ts +1 -0
  138. package/dist/runner/opencode/index.js +2 -0
  139. package/dist/runner/opencode/jsonl.d.ts +68 -0
  140. package/dist/runner/opencode/jsonl.js +295 -0
  141. package/dist/runner/opencode/runner.d.ts +63 -0
  142. package/dist/runner/opencode/runner.js +93 -0
  143. package/dist/runner/pi/index.d.ts +1 -0
  144. package/dist/runner/pi/index.js +2 -0
  145. package/dist/runner/pi/jsonl.d.ts +128 -0
  146. package/dist/runner/pi/jsonl.js +249 -0
  147. package/dist/runner/pi/runner.d.ts +37 -0
  148. package/dist/runner/pi/runner.js +76 -0
  149. package/dist/runner/registry.d.ts +54 -0
  150. package/dist/runner/registry.js +75 -0
  151. package/dist/runner/resolve-agent-choices.d.ts +7 -0
  152. package/dist/runner/resolve-agent-choices.js +76 -0
  153. package/dist/runner/sync-agent-choices.d.ts +6 -0
  154. package/dist/runner/sync-agent-choices.js +73 -0
  155. package/dist/runner/types.d.ts +250 -0
  156. package/dist/runner/types.js +8 -0
  157. package/dist/session/claude/index.d.ts +1 -0
  158. package/dist/session/claude/index.js +2 -0
  159. package/dist/session/claude/session-reader.d.ts +34 -0
  160. package/dist/session/claude/session-reader.js +46 -0
  161. package/dist/session/claude/sessions.d.ts +53 -0
  162. package/dist/session/claude/sessions.js +528 -0
  163. package/dist/session/codex/index.d.ts +1 -0
  164. package/dist/session/codex/index.js +2 -0
  165. package/dist/session/codex/rollout-reader.d.ts +88 -0
  166. package/dist/session/codex/rollout-reader.js +473 -0
  167. package/dist/session/codex/sessions.d.ts +21 -0
  168. package/dist/session/codex/sessions.js +40 -0
  169. package/dist/session/common/catch-up.d.ts +12 -0
  170. package/dist/session/common/catch-up.js +31 -0
  171. package/dist/session/common/constants.d.ts +11 -0
  172. package/dist/session/common/constants.js +12 -0
  173. package/dist/session/common/content-blocks.d.ts +37 -0
  174. package/dist/session/common/content-blocks.js +68 -0
  175. package/dist/session/common/jsonl.d.ts +77 -0
  176. package/dist/session/common/jsonl.js +351 -0
  177. package/dist/session/common/usage-accumulator.d.ts +58 -0
  178. package/dist/session/common/usage-accumulator.js +72 -0
  179. package/dist/session/index.d.ts +7 -0
  180. package/dist/session/index.js +11 -0
  181. package/dist/session/kimi/index.d.ts +1 -0
  182. package/dist/session/kimi/index.js +2 -0
  183. package/dist/session/kimi/sessions.d.ts +81 -0
  184. package/dist/session/kimi/sessions.js +527 -0
  185. package/dist/session/opencode/index.d.ts +1 -0
  186. package/dist/session/opencode/index.js +2 -0
  187. package/dist/session/opencode/sessions.d.ts +81 -0
  188. package/dist/session/opencode/sessions.js +388 -0
  189. package/dist/session/pi/index.d.ts +1 -0
  190. package/dist/session/pi/index.js +2 -0
  191. package/dist/session/pi/sessions.d.ts +27 -0
  192. package/dist/session/pi/sessions.js +399 -0
  193. package/dist/session/registry.d.ts +12 -0
  194. package/dist/session/registry.js +20 -0
  195. package/dist/session/session-store.d.ts +86 -0
  196. package/dist/session/session-store.js +450 -0
  197. package/dist/startup-contact.d.ts +18 -0
  198. package/dist/startup-contact.js +51 -0
  199. package/dist/workspace/index.d.ts +19 -0
  200. package/dist/workspace/index.js +53 -0
  201. 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';
@@ -0,0 +1,2 @@
1
+ export { OpencodeSessionReader } from './sessions.js';
2
+ //# sourceMappingURL=index.js.map