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,399 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { findJsonlLine, scanJsonlLines, readJsonlLinesFromOffset } from '../common/jsonl.js';
5
+ import { extractContentBlocks } from '../common/content-blocks.js';
6
+ import { UsageAccumulator } from '../common/usage-accumulator.js';
7
+ import { truncate } from '../../card/card-shared.js';
8
+ import { STALE_MS } from '../common/constants.js';
9
+ function defaultPiDir() {
10
+ return path.join(os.homedir(), '.pi', 'agent');
11
+ }
12
+ /** Encode cwd to pi's directory name format: '--<cwd-with-/->- + '-' --' */
13
+ function projectDirForCwd(cwd, sessionsDir) {
14
+ const encodedCwd = cwd.replace(/^\//, '').replace(/\//g, '-');
15
+ const encoded = `--${encodedCwd}--`;
16
+ return path.join(sessionsDir, encoded);
17
+ }
18
+ /**
19
+ * P1-19: TTL cache for pi directory scans (mirror codex getSessionIndex's
20
+ * 5s TTL). Repeated /resume pages re-scan the whole sessions dir per call.
21
+ */
22
+ const PI_LIST_TTL_MS = 5000;
23
+ /** Upper bound on cached listings; FIFO eviction past this. */
24
+ const PI_LIST_MAX_ENTRIES = 32;
25
+ const piListCache = new Map();
26
+ /** Pi-specific field-name mapping for content block extraction. */
27
+ const PI_MAPPING = {
28
+ toolUseType: 'toolCall',
29
+ toolResultType: 'toolResult',
30
+ toolInputField: 'arguments',
31
+ toolErrorField: 'isError',
32
+ };
33
+ /** Extract text from a pi content array (first text block). */
34
+ function extractPiText(content) {
35
+ if (typeof content === 'string')
36
+ return content.trim() || null;
37
+ if (!Array.isArray(content))
38
+ return null;
39
+ for (const part of content) {
40
+ if (part && typeof part === 'object') {
41
+ const p = part;
42
+ if (p.type === 'text' && typeof p.text === 'string' && p.text.trim()) {
43
+ return p.text.trim();
44
+ }
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ /**
50
+ * Compress a pi skill-injection body to `skill:<name>`.
51
+ *
52
+ * pi wraps an invoked SKILL.md as `<skill name="x" location="...">…</skill>`
53
+ * and injects it through the user channel - often followed by the user's
54
+ * real input on a new line. Without compression the multi-KB body head
55
+ * dominates displayTitle and squeezes out the user's actual words.
56
+ * Replace the whole `<skill>…</skill>` block with `skill:x`, preserving any
57
+ * trailing real text. Falls back to the original text when no skill block
58
+ * is present (or the block is malformed/unclosed). Regression 2026-07-13.
59
+ */
60
+ function compressSkillInjection(text) {
61
+ return text
62
+ .replace(/<skill\s+name="([^"]+)"[^>]*>[\s\S]*?<\/skill>/g, (_m, name) => `skill:${name}`)
63
+ .trim();
64
+ }
65
+ /** Get a one-line summary from a pi session JSONL (first user message text).
66
+ *
67
+ * P2-3 optimization: previously used `readJsonlLines` (full-file slurp)
68
+ * just to get the first user message. Now uses `findJsonlLine` which
69
+ * stops reading as soon as the predicate matches — typically only the
70
+ * first few lines of the file, saving ~99% I/O on large sessions.
71
+ */
72
+ function summarizePiSession(filePath) {
73
+ const line = findJsonlLine(filePath, (l) => {
74
+ try {
75
+ const obj = JSON.parse(l);
76
+ return (obj.type === 'message' &&
77
+ obj.message?.role === 'user' &&
78
+ !!extractPiText(obj.message.content));
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ });
84
+ if (!line)
85
+ return '(无摘要)';
86
+ try {
87
+ const obj = JSON.parse(line);
88
+ return extractPiText(obj.message?.content) ?? '(无摘要)';
89
+ }
90
+ catch {
91
+ return '(无摘要)';
92
+ }
93
+ }
94
+ /** Read cwd from pi session JSONL first line (session event has cwd field). */
95
+ function readCwdFromPiJsonl(filePath) {
96
+ const line = findJsonlLine(filePath, (l) => {
97
+ try {
98
+ const obj = JSON.parse(l);
99
+ return obj.type === 'session' && typeof obj.cwd === 'string' && !!obj.cwd;
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ });
105
+ if (!line)
106
+ return undefined;
107
+ try {
108
+ return JSON.parse(line).cwd;
109
+ }
110
+ catch {
111
+ return undefined;
112
+ }
113
+ }
114
+ /** Find the JSONL file path for a session by scanning the cwd's directory. */
115
+ function findSessionFile(sessionId, cwd, sessionsDir) {
116
+ const dir = projectDirForCwd(cwd, sessionsDir);
117
+ try {
118
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
119
+ for (const f of files) {
120
+ // pi filenames: <timestamp>_<uuid>.jsonl — match by sessionId (uuid) suffix
121
+ if (f.includes(sessionId)) {
122
+ return path.join(dir, f);
123
+ }
124
+ }
125
+ }
126
+ catch {
127
+ // dir doesn't exist
128
+ }
129
+ return null;
130
+ }
131
+ /**
132
+ * P2-6 + P2-5 first-pass scan: stream the file once via `scanJsonlLines`
133
+ * (no `string[]` materialized), parse each line exactly once, and collect
134
+ * all metadata needed by readSessionContent (usage, tailOffset, displayTitle).
135
+ * Does NOT retain parsed objects — the events tail is re-parsed separately
136
+ * by `extractPiEventsFromTail` so memory for both raw line strings and
137
+ * parsed objects is O(tail), not O(whole file).
138
+ *
139
+ * Mirrors claude's scalarScan (P2-2 + P2-5). Preserves the skill-signature
140
+ * compression (`<skill>…</skill>` → `skill:x`) for displayTitle and the
141
+ * "last turn" semantics for usage tokens.
142
+ */
143
+ function piScalarScan(filePath) {
144
+ const acc = new UsageAccumulator();
145
+ let tailOffset = -1;
146
+ let lastUserText;
147
+ scanJsonlLines(filePath, (line, offset) => {
148
+ let obj;
149
+ try {
150
+ obj = JSON.parse(line);
151
+ }
152
+ catch {
153
+ return;
154
+ }
155
+ if (obj.type === 'message') {
156
+ const msg = obj.message;
157
+ if (!msg)
158
+ return;
159
+ // usage aggregation (assistant only)
160
+ if (msg.role === 'assistant' && msg.usage) {
161
+ const u = msg.usage;
162
+ acc.add({
163
+ input: u.input ?? 0,
164
+ output: u.output ?? 0,
165
+ cacheRead: u.cacheRead ?? 0,
166
+ cacheCreation: u.cacheWrite ?? 0,
167
+ total: u.totalTokens ?? 0,
168
+ });
169
+ }
170
+ // last user message offset + displayTitle. Record the byte offset
171
+ // where the tail should start: the beginning of the NEXT line after
172
+ // this user message.
173
+ if (msg.role === 'user') {
174
+ tailOffset = offset + Buffer.byteLength(line, 'utf-8') + 1;
175
+ const text = extractPiText(msg.content);
176
+ // Compress skill-injection bodies so the "最近输入" label shows what
177
+ // the user invoked (skill:x) + any trailing real input, not a
178
+ // truncated SKILL.md head. Regression 2026-07-13.
179
+ if (text)
180
+ lastUserText = compressSkillInjection(text);
181
+ }
182
+ }
183
+ else if (obj.type === 'compaction') {
184
+ acc.bumpCompact();
185
+ }
186
+ });
187
+ return {
188
+ tailOffset,
189
+ usage: buildPiUsage(acc),
190
+ displayTitle: lastUserText
191
+ ? truncate(lastUserText, 200, { normalizeWhitespace: true })
192
+ : undefined,
193
+ };
194
+ }
195
+ /**
196
+ * Build the AgentSessionUsage from an accumulator, preserving pi's
197
+ * "last turn" semantics: inputTokens/outputTokens use the LAST turn's
198
+ * values, contextLength = last turn's context-window occupancy
199
+ * (input + cacheRead + cacheCreation, excludes output — review P2-8 unified
200
+ * contract across all five readers), cumulative = session-wide sum.
201
+ * Returns undefined when no usage data was observed.
202
+ */
203
+ function buildPiUsage(acc) {
204
+ const t = acc.totals;
205
+ if (t.input + t.output === 0) {
206
+ return undefined;
207
+ }
208
+ const l = acc.last;
209
+ const contextLength = l.input + l.cacheRead + l.cacheCreation;
210
+ return {
211
+ inputTokens: l.input,
212
+ outputTokens: l.output,
213
+ contextLength,
214
+ compactCount: acc.compactCount > 0 ? acc.compactCount : undefined,
215
+ cacheReadTokens: l.cacheRead > 0 ? l.cacheRead : 0,
216
+ cacheCreationTokens: l.cacheCreation > 0 ? l.cacheCreation : 0,
217
+ totalTokens: l.total > 0 ? l.total : undefined,
218
+ // Cumulative (session-wide): sum of ALL runs in the jsonl file.
219
+ cumulativeTotalTokens: t.input + t.output + t.cacheRead + t.cacheCreation,
220
+ cumulativeInputTokens: t.input,
221
+ cumulativeOutputTokens: t.output,
222
+ cumulativeCacheReadTokens: t.cacheRead,
223
+ cumulativeCacheCreationTokens: t.cacheCreation,
224
+ };
225
+ }
226
+ /**
227
+ * P2-6 + P2-5 second-pass: extract content-block events from the tail
228
+ * (lines after the last user message), re-parsing only those lines.
229
+ * `tailLines` is already the tail-only slice (read via
230
+ * `readJsonlLinesFromOffset`), so both raw line-string memory and parsed-
231
+ * object memory are O(tail), not O(whole file) — P2-6 + P2-5.
232
+ */
233
+ function extractPiEventsFromTail(tailLines) {
234
+ const events = [];
235
+ for (const line of tailLines) {
236
+ if (!line.trim())
237
+ continue;
238
+ try {
239
+ const obj = JSON.parse(line);
240
+ if (obj.type !== 'message' || !obj.message)
241
+ continue;
242
+ const message = obj.message;
243
+ // Handle error messages: when stopReason is "error", content is empty
244
+ // but errorMessage has details
245
+ if (message.stopReason === 'error' && message.errorMessage) {
246
+ events.push({
247
+ type: 'error',
248
+ content: `❌ ${message.errorMessage}`,
249
+ timestamp: obj.timestamp,
250
+ });
251
+ continue;
252
+ }
253
+ const blocks = extractContentBlocks(message.content, PI_MAPPING);
254
+ const role = message.role ?? 'unknown';
255
+ for (const block of blocks) {
256
+ events.push({ type: block.type, content: block.content, timestamp: obj.timestamp });
257
+ }
258
+ if (blocks.length === 0 && message.role) {
259
+ events.push({ type: role, content: `(${role} event)`, timestamp: obj.timestamp });
260
+ }
261
+ }
262
+ catch {
263
+ /* skip */
264
+ }
265
+ }
266
+ return events;
267
+ }
268
+ /**
269
+ * PiSessionReader: reads pi agent sessions from `~/.pi/agent/`.
270
+ *
271
+ * pi is file-system based (like Claude), so this implements AgentSessionReader
272
+ * directly. The session list comes from directory scanning.
273
+ */
274
+ export class PiSessionReader {
275
+ piDir;
276
+ sessionsDir;
277
+ constructor(opts = {}) {
278
+ this.piDir = opts.piDir ?? defaultPiDir();
279
+ this.sessionsDir = path.join(this.piDir, 'sessions');
280
+ }
281
+ listSessions(cwd, opts) {
282
+ const limit = opts?.limit ?? 20;
283
+ const offset = Math.max(0, opts?.offset ?? 0);
284
+ // SCAN: filesystem is the source of truth for pi sessions.
285
+ const scanned = this.listSessionsByScan(cwd);
286
+ return {
287
+ sessions: scanned.slice(offset, offset + limit),
288
+ total: scanned.length,
289
+ };
290
+ }
291
+ getNewestSession(cwd) {
292
+ return this.listSessions(cwd, { limit: 1 }).sessions[0] ?? null;
293
+ }
294
+ readSessionContent(sessionId, cwd, opts) {
295
+ let filePath = null;
296
+ filePath = findSessionFile(sessionId, cwd, this.sessionsDir);
297
+ if (!filePath) {
298
+ return { events: [] };
299
+ }
300
+ // Verify cwd matches
301
+ const fileCwd = readCwdFromPiJsonl(filePath);
302
+ if (fileCwd && fileCwd !== cwd) {
303
+ return { events: [] };
304
+ }
305
+ // P2-5 + P2-6: First pass STREAMS the file once via `scanJsonlLines`
306
+ // (no `string[]` materialized), parsing each line once to collect
307
+ // scalars (usage, tailOffset, displayTitle) while retaining no parsed
308
+ // objects and no whole-file line array. The tailOffset is the byte
309
+ // offset where the tail begins (start of the line after the last user
310
+ // message). A second pass re-reads ONLY the tail from that offset via
311
+ // `readJsonlLinesFromOffset` and re-parses it for events — O(tail)
312
+ // memory for both raw line strings and parsed objects, instead of
313
+ // O(whole file).
314
+ //
315
+ // Parse ratio: ≈1.0–1.5× line count when a user message exists.
316
+ // When the session has NO user message, tailOffset stays -1 and the
317
+ // tail IS the whole file → scan(N) + tail(N) = 2.0× — the known
318
+ // asymptotic upper bound of the two-phase design.
319
+ const scan = piScalarScan(filePath);
320
+ const tailLines = readJsonlLinesFromOffset(filePath, scan.tailOffset >= 0 ? scan.tailOffset : 0);
321
+ const events = extractPiEventsFromTail(tailLines);
322
+ // Apply maxEvents cap: keep the LAST N events (most recent), matching
323
+ // CodexSessionReader's slice(-maxEvents). maxEvents limits the catch-up
324
+ // tail so auto-resume cards don't load the entire session.
325
+ // P2-7: maxEvents <= 0 returns [] (guards `slice(-0)` full-array trap).
326
+ const cappedEvents = opts?.maxEvents !== undefined
327
+ ? opts.maxEvents <= 0
328
+ ? []
329
+ : events.slice(-opts.maxEvents)
330
+ : events;
331
+ return {
332
+ events: cappedEvents,
333
+ usage: scan.usage,
334
+ displayTitle: scan.displayTitle,
335
+ };
336
+ }
337
+ isSessionActive(sessionId, cwd) {
338
+ const filePath = findSessionFile(sessionId, cwd, this.sessionsDir);
339
+ if (!filePath)
340
+ return false;
341
+ try {
342
+ const st = fs.statSync(filePath);
343
+ return Date.now() - st.mtimeMs < STALE_MS;
344
+ }
345
+ catch {
346
+ return false;
347
+ }
348
+ }
349
+ // --- Private helpers ---
350
+ listSessionsByScan(cwd) {
351
+ const cacheKey = `${this.sessionsDir}\u0000${cwd}`;
352
+ const cached = piListCache.get(cacheKey);
353
+ if (cached && Date.now() - cached.builtAt < PI_LIST_TTL_MS) {
354
+ return cached.sessions;
355
+ }
356
+ const dir = projectDirForCwd(cwd, this.sessionsDir);
357
+ let files;
358
+ try {
359
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
360
+ }
361
+ catch {
362
+ return [];
363
+ }
364
+ const sessions = [];
365
+ for (const f of files) {
366
+ const full = path.join(dir, f);
367
+ try {
368
+ const st = fs.statSync(full);
369
+ const fileCwd = readCwdFromPiJsonl(full);
370
+ if (fileCwd && fileCwd !== cwd)
371
+ continue;
372
+ // Extract sessionId (UUID) from filename: <timestamp>_<uuid>.jsonl
373
+ // The timestamp and uuid are separated by '_', and neither contains '_'
374
+ // (timestamps use '-'/'T'/':', UUIDs use '-'). Take the last '_' segment.
375
+ const stem = f.replace('.jsonl', '');
376
+ const sessionId = stem.includes('_') ? stem.slice(stem.lastIndexOf('_') + 1) : stem;
377
+ sessions.push({
378
+ sessionId,
379
+ summary: truncate(summarizePiSession(full), 60, { normalizeWhitespace: true }),
380
+ mtime: st.mtimeMs,
381
+ });
382
+ }
383
+ catch {
384
+ // skip
385
+ }
386
+ }
387
+ // Same-mtime ties: secondary key keeps the full order deterministic.
388
+ sessions.sort((a, b) => b.mtime - a.mtime || a.sessionId.localeCompare(b.sessionId));
389
+ piListCache.set(cacheKey, { builtAt: Date.now(), sessions });
390
+ // Bound the cache (map iteration order = insertion order).
391
+ if (piListCache.size > PI_LIST_MAX_ENTRIES) {
392
+ const oldest = piListCache.keys().next().value;
393
+ if (oldest !== undefined)
394
+ piListCache.delete(oldest);
395
+ }
396
+ return sessions;
397
+ }
398
+ }
399
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1,12 @@
1
+ import type { AgentKind, AgentSessionReader } from '../runner/index.js';
2
+ /**
3
+ * Registry of `AgentSessionReader` instances, keyed by `AgentKind`. Mirrors
4
+ * `AgentRegistry` but for session-history reads. `index.ts` registers one
5
+ * reader per available agent at startup; router/bridge look up readers here
6
+ * instead of importing `claude-sessions.ts` directly.
7
+ */
8
+ export declare class SessionReaderRegistry {
9
+ private readonly readers;
10
+ register(kind: AgentKind, reader: AgentSessionReader): void;
11
+ get(kind: AgentKind): AgentSessionReader;
12
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Registry of `AgentSessionReader` instances, keyed by `AgentKind`. Mirrors
3
+ * `AgentRegistry` but for session-history reads. `index.ts` registers one
4
+ * reader per available agent at startup; router/bridge look up readers here
5
+ * instead of importing `claude-sessions.ts` directly.
6
+ */
7
+ export class SessionReaderRegistry {
8
+ readers = new Map();
9
+ register(kind, reader) {
10
+ this.readers.set(kind, reader);
11
+ }
12
+ get(kind) {
13
+ const reader = this.readers.get(kind);
14
+ if (!reader) {
15
+ throw new Error(`session reader not registered: ${kind}`);
16
+ }
17
+ return reader;
18
+ }
19
+ }
20
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,86 @@
1
+ import type { AgentKind } from '../runner/index.js';
2
+ interface SessionEntry {
3
+ /** Session IDs per agent kind (Map for in-memory, plain object for persistence). */
4
+ sessions: Map<AgentKind, string>;
5
+ /** Previous session IDs per agent kind - stored when switching away, for potential restore. */
6
+ previousSessions: Map<AgentKind, string>;
7
+ /** Arrival baseline per agent kind - the session id the user was given when
8
+ * they last "arrived" at this agent via config.save switch ('' = cleared arrival). */
9
+ arrivalSessions: Map<AgentKind, string>;
10
+ /** Session actual cwd per agent (from system.init event.cwd; only claude may differ from cwd). */
11
+ sessionCwds: Map<AgentKind, string>;
12
+ cwd: string;
13
+ }
14
+ /**
15
+ * Per-user session store. Persists cwd + per-agent sessions to disk so that
16
+ * the last workspace AND the last-used sessionId per agent are restored after
17
+ * a bridge restart.
18
+ *
19
+ * Persistence strategy:
20
+ * - Persists `cwd`, `sessions` (per-agent sessionId), `previousSessions`
21
+ * (parked restore slots for `/config` agent switching), and `arrivalSessions`
22
+ * (arrival baseline; '' entries are meaningful and must survive restart).
23
+ * - Atomic write (temp + rename) to avoid truncated files on crash
24
+ * - Corrupt files are treated as empty (with a warning)
25
+ *
26
+ * P3.3: sessions is Map<AgentKind, sessionId> (one session per agent), allowing
27
+ * each agent to maintain its own session context when switching between agents.
28
+ */
29
+ export declare class SessionStore {
30
+ private sessions;
31
+ private readonly persistPath?;
32
+ private readonly defaultAgent?;
33
+ /**
34
+ * 进程内会话代际(2026-08-09 /new 被在途 run init 写回撤销事故):
35
+ * 每次「用户意图移动 session 指针」+1。system.init 写回通道不 bump。
36
+ * Bridge 在 run 启动时快照 epoch,init 写回前比较——不一致说明 /new、
37
+ * /cd、/resume 等在 run 在途时移动了指针,该 run 的写回是 stale 的,丢弃。
38
+ * 不持久化:重启后无在途 run,stale 写回不可能发生。
39
+ * 独立 Map 而非 SessionEntry 字段:entry 有 ~10 处整体重构点,字段易漏传。
40
+ */
41
+ private agentEpochs;
42
+ private userEpochs;
43
+ private static epochKey;
44
+ /** 当前代际 = user 级 + agent 级之和;任一级 bump 都会改变总和。 */
45
+ getSessionEpoch(userId: string, agent: AgentKind): number;
46
+ /** 用户意图移动了指定 agent 的 session 指针(/new、/resume、/config 切换)。 */
47
+ bumpSessionEpoch(userId: string, agent: AgentKind): void;
48
+ /** 用户意图移动了全部 agent 的 session 指针(/cd、/ws use 的 setCwd)。 */
49
+ private bumpUserEpoch;
50
+ constructor(persistPath?: string, defaultAgent?: AgentKind);
51
+ get(userId: string): SessionEntry | undefined;
52
+ set(userId: string, entry: SessionEntry): void;
53
+ delete(userId: string): void;
54
+ /** Clear sessionId for a specific agent, keeping cwd and other agent sessions */
55
+ clearSessionId(userId: string, agent?: AgentKind, opts?: {
56
+ clearSessionCwd?: boolean;
57
+ }): void;
58
+ /** Get previous sessionId for a specific agent (stored when switching away) */
59
+ getPreviousSessionId(userId: string, agent: AgentKind): string | undefined;
60
+ /** Set previous sessionId for a specific agent (for potential restore when switching back) */
61
+ setPreviousSessionId(userId: string, agent: AgentKind, sessionId: string): void;
62
+ /** Clear previous sessionId for a specific agent */
63
+ clearPreviousSessionId(userId: string, agent: AgentKind): void;
64
+ /** Get the arrival baseline sessionId for a specific agent ('' and missing → undefined) */
65
+ getArrivalSessionId(userId: string, agent: AgentKind): string | undefined;
66
+ /** Set the arrival baseline for a specific agent ('' explicitly clears the arrival baseline) */
67
+ setArrivalSessionId(userId: string, agent: AgentKind, sessionId: string): void;
68
+ /** Set cwd for a user, clearing all sessionIds (§9.1) */
69
+ setCwd(userId: string, cwd: string): void;
70
+ /** Get cwd for a user, or undefined if no session */
71
+ getCwd(userId: string): string | undefined;
72
+ /** Get sessionId for a specific agent, or undefined if no session or sessionId is empty */
73
+ getSessionId(userId: string, agent?: AgentKind): string | undefined;
74
+ /** Set sessionId for a specific agent, optionally with cwd */
75
+ setSessionId(userId: string, agent: AgentKind, sessionId: string, cwd?: string): void;
76
+ /** Set sessionId for a specific agent and cwd together */
77
+ setSessionIdAndCwd(userId: string, agent: AgentKind, sessionId: string, cwd: string, sessionCwd?: string): void;
78
+ /** Get sessionCwd for a specific agent, or undefined if not set */
79
+ getSessionCwd(userId: string, agent?: AgentKind): string | undefined;
80
+ /** Set sessionId and sessionCwd for a specific agent, without touching cwd */
81
+ setSessionIdAndSessionCwd(userId: string, agent: AgentKind, sessionId: string, sessionCwd: string): void;
82
+ /** Persisted format: only cwd per user (sessionId is process-scoped) */
83
+ private load;
84
+ private autoPersist;
85
+ }
86
+ export {};