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,473 @@
1
+ /**
2
+ * Codex Rollout File Reader.
3
+ *
4
+ * Reads Codex session history from rollout JSONL files in `~/.codex/sessions/`.
5
+ *
6
+ * Rollout file format (appendix C in design doc):
7
+ * ```
8
+ * {"timestamp":"...","type":"session_meta","payload":{"session_id":"...","cwd":"...","originator":"lark-remote",...}}
9
+ * {"timestamp":"...","type":"event_msg","payload":{"type":"task_started","turn_id":"..."}}
10
+ * {"timestamp":"...","type":"response_item","payload":{"type":"message","role":"user|developer","content":[...]}}
11
+ * ```
12
+ *
13
+ * Note: This handles the **rollout file format** (historical logging), NOT the
14
+ * `codex exec --json` stdout format. Those are two different JSONL formats
15
+ * (see design doc appendix B vs C).
16
+ */
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { readJsonlLines, findJsonlLine } from '../common/jsonl.js';
20
+ import { getLogger } from '../../logger/index.js';
21
+ import { isRecord, stringValue } from '../../common/guards.js';
22
+ import { resolveCodexHome } from '../../config/codex-config.js';
23
+ /** Field-wise difference (total - previous) for cumulative->incremental. */
24
+ function subtractRawUsage(a, b) {
25
+ return {
26
+ input_tokens: a.input_tokens - b.input_tokens,
27
+ cached_input_tokens: a.cached_input_tokens - b.cached_input_tokens,
28
+ output_tokens: a.output_tokens - b.output_tokens,
29
+ total_tokens: a.total_tokens - b.total_tokens,
30
+ };
31
+ }
32
+ export function readCodexRollout(filePath) {
33
+ if (!fs.existsSync(filePath)) {
34
+ return null;
35
+ }
36
+ try {
37
+ const lines = readJsonlLines(filePath);
38
+ const events = [];
39
+ let sessionMeta = null;
40
+ // Real user input is identified by a paired `event_msg` whose
41
+ // payload.type === "user_message" - codex emits this ONLY for text the
42
+ // human actually typed. Injected scaffolding (AGENTS.md project rules,
43
+ // <environment_context>, permissions) is also written as `role:"user"`
44
+ // response_items but has NO user_message event, so it must be excluded
45
+ // from displayTitle/recap/summary. Regression 2026-07-13: the first
46
+ // `role:user` message is the injected AGENTS.md, mistakenly shown as
47
+ // "最近输入".
48
+ const realUserMessages = [];
49
+ // token_count tracking: codex emits cumulative `total_token_usage` and an
50
+ // incremental `last_token_usage`. We want the LAST turn's usage (matches
51
+ // pi/opencode /resume "last turn" display semantics). When
52
+ // last_token_usage is absent we derive it as total - previous_total.
53
+ let lastRawUsage;
54
+ // Final total_token_usage (cumulative across the whole session) for the
55
+ // Run card's "累计" display. Codex emits this alongside last_token_usage.
56
+ let lastTotalUsage;
57
+ let previousTotals;
58
+ for (const line of lines) {
59
+ const trimmed = line.trim();
60
+ if (!trimmed)
61
+ continue;
62
+ try {
63
+ const parsed = JSON.parse(trimmed);
64
+ if (!parsed || typeof parsed.type !== 'string')
65
+ continue;
66
+ if (parsed.type === 'session_meta' && parsed.payload) {
67
+ sessionMeta = parsed.payload;
68
+ }
69
+ else if (parsed.type === 'event_msg' && parsed.payload) {
70
+ // user_message events carry the authoritative human-typed text.
71
+ const payload = parsed.payload;
72
+ if (payload.type === 'user_message') {
73
+ const text = stringValue(payload.message);
74
+ if (text) {
75
+ realUserMessages.push(text.slice(0, 200));
76
+ }
77
+ }
78
+ else if (payload.type === 'token_count') {
79
+ const info = payload.info;
80
+ const last = info?.last_token_usage;
81
+ const total = info?.total_token_usage;
82
+ let raw;
83
+ if (last) {
84
+ raw = last;
85
+ }
86
+ else if (total && previousTotals) {
87
+ raw = subtractRawUsage(total, previousTotals);
88
+ }
89
+ else {
90
+ raw = total;
91
+ }
92
+ if (total) {
93
+ previousTotals = total;
94
+ lastTotalUsage = total;
95
+ }
96
+ if (raw && (raw.input_tokens || raw.cached_input_tokens || raw.output_tokens)) {
97
+ lastRawUsage = raw;
98
+ }
99
+ }
100
+ }
101
+ else if (parsed.type === 'response_item' && parsed.payload) {
102
+ const payload = parsed.payload;
103
+ if (payload.type === 'message' && Array.isArray(payload.content)) {
104
+ // Extract text content for display
105
+ const messages = extractMessageContent(payload);
106
+ for (const msg of messages) {
107
+ events.push({
108
+ type: msg.role === 'user' ? 'user' : 'assistant',
109
+ content: msg.text,
110
+ timestamp: parsed.timestamp,
111
+ });
112
+ }
113
+ }
114
+ }
115
+ }
116
+ catch {
117
+ // Skip malformed lines
118
+ continue;
119
+ }
120
+ }
121
+ const firstUserMessage = realUserMessages[0] ?? '';
122
+ const lastRealUserMessage = realUserMessages[realUserMessages.length - 1] ?? '';
123
+ if (!sessionMeta) {
124
+ return null;
125
+ }
126
+ const sessionId = stringValue(sessionMeta.session_id) ?? stringValue(sessionMeta.id) ?? '';
127
+ const cwd = stringValue(sessionMeta.cwd) ?? '';
128
+ if (!sessionId) {
129
+ return null;
130
+ }
131
+ // Use file mtime as updatedAtMs, or parse timestamp from session_meta
132
+ const stats = fs.statSync(filePath);
133
+ const createdAtMs = sessionMeta.timestamp
134
+ ? new Date(String(sessionMeta.timestamp)).getTime()
135
+ : stats.birthtimeMs;
136
+ const updatedAtMs = stats.mtimeMs;
137
+ // Build ccusage-aligned usage from the last turn's raw tokens.
138
+ // input = raw - cached (codex reports cached input separately), cache
139
+ // creation is never reported by codex, reasoning is a subset of output
140
+ // (display-only, never added to total).
141
+ // contextLength = input_tokens (= (input_tokens-cached) + cached + 0),
142
+ // i.e. input + cacheRead + cacheCreation — the unified context-window
143
+ // occupancy contract across all five readers (review P2-8, excludes
144
+ // output/reasoning).
145
+ let usage;
146
+ if (lastRawUsage) {
147
+ const r = lastRawUsage;
148
+ const cached = Math.min(r.cached_input_tokens, r.input_tokens);
149
+ usage = {
150
+ inputTokens: r.input_tokens - cached,
151
+ outputTokens: r.output_tokens,
152
+ contextLength: r.input_tokens,
153
+ cacheReadTokens: cached,
154
+ cacheCreationTokens: 0,
155
+ totalTokens: r.total_tokens,
156
+ };
157
+ // Cumulative from the final total_token_usage (non-cached input +
158
+ // output), summed across all turns in the session.
159
+ if (lastTotalUsage) {
160
+ const cumCached = Math.min(lastTotalUsage.cached_input_tokens, lastTotalUsage.input_tokens);
161
+ usage.cumulativeInputTokens = lastTotalUsage.input_tokens - cumCached;
162
+ usage.cumulativeOutputTokens = lastTotalUsage.output_tokens;
163
+ usage.cumulativeTotalTokens = lastTotalUsage.total_tokens;
164
+ usage.cumulativeCacheReadTokens = cumCached;
165
+ usage.cumulativeCacheCreationTokens = 0; // Codex 不报告 cache creation
166
+ }
167
+ }
168
+ return {
169
+ threadId: sessionId,
170
+ cwd,
171
+ firstUserMessage: firstUserMessage || '(no user message)',
172
+ lastRealUserMessage,
173
+ createdAtMs,
174
+ updatedAtMs,
175
+ events,
176
+ usage,
177
+ };
178
+ }
179
+ catch (err) {
180
+ getLogger().warn(`[codex-rollout-reader] failed to read ${filePath}: ${err}`);
181
+ return null;
182
+ }
183
+ }
184
+ /**
185
+ * List Codex sessions from rollout files.
186
+ *
187
+ * Returns `{ entries, total }`: `total` is the size of the full cwd-matched
188
+ * set before pagination; `entries` are the newest `limit` entries by mtime.
189
+ *
190
+ * A global order over the full set is established first (plan §1.4: no early
191
+ * termination before ordering) using the lightweight session index — full
192
+ * walk + first-line session_meta parse + stat mtime, 5s TTL cache. Only the
193
+ * returned page is fully parsed for summary/usage (plan §2.2).
194
+ */
195
+ export function listCodexRollouts(opts = {}) {
196
+ const codexHome = resolveCodexHome(opts.codexHome);
197
+ const limit = opts.limit ?? 20;
198
+ const offset = Math.max(0, opts.offset ?? 0);
199
+ const filterCwd = opts.cwd;
200
+ // Full index: every rollout file's session_meta + mtime, no early break.
201
+ const index = getSessionIndex(codexHome);
202
+ const matched = [];
203
+ for (const entry of index.values()) {
204
+ // Subagent thread rollouts are part of a thread tree, not standalone
205
+ // sessions: exclude them explicitly (plan §2.1) so a pure-subagent
206
+ // session (main file missing) never pollutes the list or total.
207
+ if (entry.isSubagent)
208
+ continue;
209
+ if (filterCwd && entry.cwd !== filterCwd)
210
+ continue;
211
+ matched.push(entry);
212
+ }
213
+ // Establish the global order by mtime desc, then slice the page.
214
+ // Same-mtime ties use filePath as a deterministic secondary key so index
215
+ // rebuilds / walk order changes never reorder the page.
216
+ matched.sort((a, b) => b.mtimeMs - a.mtimeMs || a.filePath.localeCompare(b.filePath));
217
+ const total = matched.length;
218
+ const page = matched.slice(offset, offset + limit);
219
+ // Full-parse only the page being returned (summary/usage).
220
+ const entries = [];
221
+ for (const item of page) {
222
+ const entry = readCodexRollout(item.filePath);
223
+ if (entry)
224
+ entries.push(entry);
225
+ }
226
+ return { entries, total };
227
+ }
228
+ /**
229
+ * Read the full content of a specific session by its threadId.
230
+ *
231
+ * P2-4: Uses session index for O(1) file lookup instead of walking the
232
+ * entire directory tree and fully parsing every file. Only the matching
233
+ * file is fully parsed.
234
+ *
235
+ * `maxEvents` (optional): when set, only the LAST `maxEvents` events are
236
+ * returned (most recent activity). This matches the auto-resume use case
237
+ * ("show recent history") and keeps the card payload small. Unlike claude,
238
+ * codex events are already at message granularity (no multi-block split),
239
+ * so a hard `slice(-maxEvents)` is sufficient — no soft-cap complication.
240
+ */
241
+ export function readCodexSessionContent(sessionId, opts = {}) {
242
+ const codexHome = resolveCodexHome(opts.codexHome);
243
+ // P2-4: Use session index for direct file lookup
244
+ let index = getSessionIndex(codexHome);
245
+ let entry = index.get(sessionId);
246
+ // P3-2: On miss, the index may be stale (a rollout file created after the
247
+ // cache was built but within the 5s TTL). Force a refresh and recheck so a
248
+ // brand-new session is found without waiting for TTL expiry.
249
+ if (!entry) {
250
+ index = getSessionIndex(codexHome, true);
251
+ entry = index.get(sessionId);
252
+ }
253
+ if (!entry) {
254
+ return { events: [] };
255
+ }
256
+ const rollout = readCodexRollout(entry.filePath);
257
+ if (!rollout || rollout.threadId !== sessionId) {
258
+ return { events: [] };
259
+ }
260
+ // Apply maxEvents cap: keep the LAST N events (most recent).
261
+ // See function docstring for rationale.
262
+ // P2-7: maxEvents <= 0 returns [] — guards the `slice(-0) === slice(0)`
263
+ // full-array trap (no caller passes 0 today, but the contract must hold).
264
+ const events = opts.maxEvents !== undefined && opts.maxEvents > 0 && rollout.events.length > opts.maxEvents
265
+ ? rollout.events.slice(-opts.maxEvents)
266
+ : opts.maxEvents !== undefined && opts.maxEvents <= 0
267
+ ? []
268
+ : rollout.events;
269
+ return {
270
+ events,
271
+ // displayTitle = LAST real user message (matches the
272
+ // "最近输入" label). recap is undefined: codex has no
273
+ // compact-summary concept, so never fake one from a user
274
+ // message (was: firstUserMessage = injected AGENTS.md).
275
+ displayTitle: rollout.lastRealUserMessage
276
+ ? rollout.lastRealUserMessage.slice(0, 50)
277
+ : undefined,
278
+ recap: undefined,
279
+ usage: rollout.usage,
280
+ };
281
+ }
282
+ /**
283
+ * Check if a session is still active (recently updated).
284
+ *
285
+ * P2-4: Uses session index for direct file lookup, then checks mtime
286
+ * via statSync — no full rollout parse required. This reduces the
287
+ * cost from "walk entire directory + full-parse every file" to a single
288
+ * statSync call for the matching file.
289
+ */
290
+ export function isCodexSessionActive(sessionId, opts = {}) {
291
+ const codexHome = resolveCodexHome(opts.codexHome);
292
+ const threshold = opts.activeThresholdMs ?? 10 * 60 * 1000; // 10 minutes default
293
+ // P2-4: Use session index for direct file lookup + mtime check
294
+ let index = getSessionIndex(codexHome);
295
+ let entry = index.get(sessionId);
296
+ // P3-2: On miss, refresh the index in case the session file was created
297
+ // after the cache was built (within the 5s TTL). See readCodexSessionContent.
298
+ if (!entry) {
299
+ index = getSessionIndex(codexHome, true);
300
+ entry = index.get(sessionId);
301
+ }
302
+ if (!entry) {
303
+ return false;
304
+ }
305
+ // Subagent thread rollouts are never "active sessions": their mtime
306
+ // reflects thread-tree activity, not the parent session's liveness
307
+ // (plan §2.1). A pure-subagent session must report inactive.
308
+ if (entry.isSubagent) {
309
+ return false;
310
+ }
311
+ // Re-stat to get fresh mtime (index may be up to INDEX_TTL_MS stale)
312
+ try {
313
+ const stat = fs.statSync(entry.filePath);
314
+ return Date.now() - stat.mtimeMs < threshold;
315
+ }
316
+ catch {
317
+ return false;
318
+ }
319
+ }
320
+ /**
321
+ * Walk the YYYY/MM/DD directory tree under `sessionsDir` and return all
322
+ * rollout-*.jsonl file paths. Silently skips non-directory entries and
323
+ * malformed names; returns an empty array if `sessionsDir` doesn't exist
324
+ * or is unreadable.
325
+ */
326
+ function walkRolloutFiles(sessionsDir) {
327
+ if (!fs.existsSync(sessionsDir))
328
+ return [];
329
+ const result = [];
330
+ try {
331
+ const years = fs.readdirSync(sessionsDir);
332
+ for (const year of years) {
333
+ if (!/^\d{4}$/.test(year))
334
+ continue;
335
+ const yearPath = path.join(sessionsDir, year);
336
+ if (!fs.statSync(yearPath).isDirectory())
337
+ continue;
338
+ const months = fs.readdirSync(yearPath);
339
+ for (const month of months) {
340
+ if (!/^\d{2}$/.test(month))
341
+ continue;
342
+ const monthPath = path.join(yearPath, month);
343
+ if (!fs.statSync(monthPath).isDirectory())
344
+ continue;
345
+ const days = fs.readdirSync(monthPath);
346
+ for (const day of days) {
347
+ if (!/^\d{2}$/.test(day))
348
+ continue;
349
+ const dayPath = path.join(monthPath, day);
350
+ if (!fs.statSync(dayPath).isDirectory())
351
+ continue;
352
+ const files = fs
353
+ .readdirSync(dayPath)
354
+ .filter((f) => f.startsWith('rollout-') && f.endsWith('.jsonl'));
355
+ for (const file of files) {
356
+ result.push(path.join(dayPath, file));
357
+ }
358
+ }
359
+ }
360
+ }
361
+ }
362
+ catch (err) {
363
+ getLogger().warn(`[codex-rollout-reader] failed to walk sessions dir: ${err}`);
364
+ }
365
+ return result;
366
+ }
367
+ const INDEX_TTL_MS = 5_000; // 5 seconds — covers a single /resume flow
368
+ const sessionIndexCache = new Map();
369
+ /**
370
+ * Build or return cached session index for a given resolved codexHome.
371
+ * The index maps sessionId → {filePath, cwd, mtimeMs}.
372
+ *
373
+ * @param resolvedCodexHome Already-resolved codex home path (from resolveCodexHome).
374
+ * @param forceRefresh When true, bypass the TTL cache and rebuild. Used
375
+ * on an index MISS (readCodexSessionContent /
376
+ * isCodexSessionActive) so a rollout file created
377
+ * AFTER the cache was built — but still within the
378
+ * 5s TTL — is found without waiting for expiry.
379
+ *
380
+ * Cache shape note: `sessionIndexCache` is module-level and keyed by the
381
+ * resolved codexHome string. In production `CodexSessionReader` resolves
382
+ * codexHome once in its constructor and reuses it for every call, so a single
383
+ * bridge instance produces exactly one cache entry — the Map does NOT grow
384
+ * unbounded. The TTL refreshes the value on each rebuild; entries are never
385
+ * evicted, but there is effectively just one.
386
+ */
387
+ function getSessionIndex(resolvedCodexHome, forceRefresh = false) {
388
+ const cacheKey = resolvedCodexHome;
389
+ const cached = sessionIndexCache.get(cacheKey);
390
+ if (!forceRefresh && cached && Date.now() - cached.builtAt < INDEX_TTL_MS) {
391
+ return cached.index;
392
+ }
393
+ const sessionsDir = path.join(resolvedCodexHome, 'sessions');
394
+ const files = walkRolloutFiles(sessionsDir);
395
+ const index = new Map();
396
+ for (const filePath of files) {
397
+ // Use findJsonlLine to extract session_meta without full file parse.
398
+ // session_meta is always the first line of a rollout file.
399
+ const metaLine = findJsonlLine(filePath, (line) => {
400
+ try {
401
+ const obj = JSON.parse(line);
402
+ return obj.type === 'session_meta';
403
+ }
404
+ catch {
405
+ return false;
406
+ }
407
+ });
408
+ if (!metaLine)
409
+ continue;
410
+ try {
411
+ const meta = JSON.parse(metaLine);
412
+ const sessionId = meta.payload?.session_id;
413
+ if (!sessionId)
414
+ continue;
415
+ let mtimeMs;
416
+ try {
417
+ mtimeMs = fs.statSync(filePath).mtimeMs;
418
+ }
419
+ catch {
420
+ continue;
421
+ }
422
+ // Codex multi-agent threads: subagent rollout files share the parent
423
+ // session's session_id but carry thread_source === 'subagent' (or a
424
+ // source.subagent marker). Keep only the main-thread file for a given
425
+ // session id; among files of the same kind pick the newest mtime, with
426
+ // filePath as a deterministic tie-breaker — resolution never depends on
427
+ // fs.readdirSync order (APFS hash order is not filename order).
428
+ const isSubagent = meta.payload?.thread_source === 'subagent' || meta.payload?.source?.subagent !== undefined;
429
+ const candidate = {
430
+ filePath,
431
+ cwd: meta.payload?.cwd ?? '',
432
+ mtimeMs,
433
+ isSubagent,
434
+ };
435
+ const existing = index.get(sessionId);
436
+ if (!existing ||
437
+ (existing.isSubagent && !isSubagent) ||
438
+ (existing.isSubagent === isSubagent && mtimeMs > existing.mtimeMs) ||
439
+ (existing.isSubagent === isSubagent &&
440
+ mtimeMs === existing.mtimeMs &&
441
+ filePath.localeCompare(existing.filePath) < 0)) {
442
+ index.set(sessionId, candidate);
443
+ }
444
+ }
445
+ catch {
446
+ continue;
447
+ }
448
+ }
449
+ sessionIndexCache.set(cacheKey, { index, builtAt: Date.now() });
450
+ return index;
451
+ }
452
+ /** Clear the session index cache. Exposed for tests. */
453
+ export function clearSessionIndexCache() {
454
+ sessionIndexCache.clear();
455
+ }
456
+ function extractMessageContent(payload) {
457
+ const result = [];
458
+ const content = payload.content;
459
+ if (!Array.isArray(content)) {
460
+ return result;
461
+ }
462
+ const role = stringValue(payload.role) ?? 'assistant';
463
+ for (const item of content) {
464
+ if (!isRecord(item))
465
+ continue;
466
+ const text = stringValue(item.text) ?? stringValue(item.input_text);
467
+ if (text) {
468
+ result.push({ role, text });
469
+ }
470
+ }
471
+ return result;
472
+ }
473
+ //# sourceMappingURL=rollout-reader.js.map
@@ -0,0 +1,21 @@
1
+ import type { AgentSession, SessionContent, AgentSessionReader } from '../../runner/index.js';
2
+ interface CodexSessionReaderOptions {
3
+ codexHome?: string;
4
+ }
5
+ export declare class CodexSessionReader implements AgentSessionReader {
6
+ private readonly codexHome;
7
+ constructor(opts?: CodexSessionReaderOptions);
8
+ listSessions(cwd: string, opts?: {
9
+ limit?: number;
10
+ offset?: number;
11
+ }): {
12
+ sessions: AgentSession[];
13
+ total: number;
14
+ };
15
+ getNewestSession(cwd: string): AgentSession | null;
16
+ readSessionContent(sessionId: string, _cwd: string, opts?: {
17
+ maxEvents?: number;
18
+ }): SessionContent;
19
+ isSessionActive(sessionId: string, _cwd: string): boolean;
20
+ }
21
+ export {};
@@ -0,0 +1,40 @@
1
+ import { resolveCodexHome } from '../../config/codex-config.js';
2
+ import { listCodexRollouts, readCodexSessionContent, isCodexSessionActive, } from './rollout-reader.js';
3
+ export class CodexSessionReader {
4
+ codexHome;
5
+ constructor(opts = {}) {
6
+ this.codexHome = resolveCodexHome(opts.codexHome);
7
+ }
8
+ listSessions(cwd, opts) {
9
+ const result = listCodexRollouts({
10
+ codexHome: this.codexHome,
11
+ cwd,
12
+ limit: opts?.limit ?? 20,
13
+ offset: opts?.offset,
14
+ });
15
+ return {
16
+ sessions: result.entries.map((entry) => ({
17
+ sessionId: entry.threadId,
18
+ // '(no user message)' 是 reader 的占位符,不是真实输入;给空串由
19
+ // router 的占位符集合统一过滤。`|| entry.cwd` 永不达(rollout-reader
20
+ // 已保证 firstUserMessage 非空字符串),去掉以免把 cwd 误当摘要。
21
+ summary: entry.firstUserMessage === '(no user message)' ? '' : entry.firstUserMessage,
22
+ mtime: entry.updatedAtMs,
23
+ })),
24
+ total: result.total,
25
+ };
26
+ }
27
+ getNewestSession(cwd) {
28
+ return this.listSessions(cwd, { limit: 1 }).sessions[0] ?? null;
29
+ }
30
+ readSessionContent(sessionId, _cwd, opts) {
31
+ return readCodexSessionContent(sessionId, {
32
+ codexHome: this.codexHome,
33
+ maxEvents: opts?.maxEvents,
34
+ });
35
+ }
36
+ isSessionActive(sessionId, _cwd) {
37
+ return isCodexSessionActive(sessionId, { codexHome: this.codexHome });
38
+ }
39
+ }
40
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Find the index of the last line in a JSONL line array whose parsed object
3
+ * satisfies the given predicate.
4
+ *
5
+ * Invalid JSON, empty, and whitespace-only lines are silently skipped.
6
+ *
7
+ * @param lines Array of raw JSONL line strings.
8
+ * @param isUserMessage Predicate that receives the parsed object and returns
9
+ * true for lines considered "user" messages.
10
+ * @returns The array index of the last matching line, or -1 if none match.
11
+ */
12
+ export declare function findLastUserIndex(lines: string[], isUserMessage: (obj: Record<string, unknown>) => boolean): number;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Find the index of the last line in a JSONL line array whose parsed object
3
+ * satisfies the given predicate.
4
+ *
5
+ * Invalid JSON, empty, and whitespace-only lines are silently skipped.
6
+ *
7
+ * @param lines Array of raw JSONL line strings.
8
+ * @param isUserMessage Predicate that receives the parsed object and returns
9
+ * true for lines considered "user" messages.
10
+ * @returns The array index of the last matching line, or -1 if none match.
11
+ */
12
+ export function findLastUserIndex(lines, isUserMessage) {
13
+ let lastIdx = -1;
14
+ for (let i = 0; i < lines.length; i++) {
15
+ const line = lines[i];
16
+ if (!line.trim())
17
+ continue;
18
+ let obj;
19
+ try {
20
+ obj = JSON.parse(line);
21
+ }
22
+ catch {
23
+ continue;
24
+ }
25
+ if (isUserMessage(obj)) {
26
+ lastIdx = i;
27
+ }
28
+ }
29
+ return lastIdx;
30
+ }
31
+ //# sourceMappingURL=catch-up.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Session-related constants shared across agent session readers.
3
+ */
4
+ /**
5
+ * Threshold (in milliseconds) after which a session is considered inactive.
6
+ *
7
+ * Set to 1 hour — well above the typical 15-minute idle watchdog, so a
8
+ * legitimately paused session is not prematurely marked stale. A file whose
9
+ * mtime is older than this threshold cannot be from a running process.
10
+ */
11
+ export declare const STALE_MS: number;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Session-related constants shared across agent session readers.
3
+ */
4
+ /**
5
+ * Threshold (in milliseconds) after which a session is considered inactive.
6
+ *
7
+ * Set to 1 hour — well above the typical 15-minute idle watchdog, so a
8
+ * legitimately paused session is not prematurely marked stale. A file whose
9
+ * mtime is older than this threshold cannot be from a running process.
10
+ */
11
+ export const STALE_MS = 60 * 60 * 1000;
12
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Parameterized content-block extraction shared across session readers.
3
+ *
4
+ * Different agents (Claude, pi, …) use different field names for tool_use /
5
+ * tool_result blocks. The {@link ContentBlockMapping} type captures those
6
+ * differences so a single implementation can serve all readers.
7
+ */
8
+ /** A single extracted content block for rendering. */
9
+ interface ContentBlock {
10
+ type: 'text' | 'thinking' | 'tool_use' | 'tool_result';
11
+ content: string;
12
+ }
13
+ /** Maps agent-specific field names to the generic extraction logic. */
14
+ export interface ContentBlockMapping {
15
+ /** The `type` value that identifies a tool-use block (e.g. 'tool_use' or 'toolCall'). */
16
+ toolUseType: string;
17
+ /** The `type` value that identifies a tool-result block (e.g. 'tool_result' or 'toolResult'). */
18
+ toolResultType: string;
19
+ /** Field name holding the tool input/arguments (e.g. 'input' or 'arguments'). */
20
+ toolInputField: string;
21
+ /** Field name holding the error flag (e.g. 'is_error' or 'isError'). */
22
+ toolErrorField: string;
23
+ }
24
+ /**
25
+ * Extract content blocks from a message content array.
26
+ *
27
+ * Handles text, thinking, tool_use, and tool_result blocks. Blank text and
28
+ * whitespace-only tool_result content are skipped. Tool input is serialized
29
+ * to JSON and truncated at 200 characters.
30
+ *
31
+ * @param content The raw `content` value from a message (array expected;
32
+ * non-array values return an empty array).
33
+ * @param mapping Agent-specific field-name mapping.
34
+ * @returns Array of {@link ContentBlock} objects.
35
+ */
36
+ export declare function extractContentBlocks(content: unknown, mapping: ContentBlockMapping): ContentBlock[];
37
+ export {};