* Extract title from codex JSONL format. * Codex has no `summary` type — find the first user message that isn't the environment_context.
(filePath: string)
| 270 | * Codex has no `summary` type — find the first user message that isn't the environment_context. |
| 271 | */ |
| 272 | protected async extractTitle(filePath: string): Promise<string | null> { |
| 273 | return new Promise((resolve) => { |
| 274 | let firstUserMessage: string | null = null; |
| 275 | let resolved = false; |
| 276 | |
| 277 | const rl = createInterface({input: createReadStream(filePath), crlfDelay: Infinity}); |
| 278 | |
| 279 | rl.on('line', (line) => { |
| 280 | if (resolved || !line.trim()) return; |
| 281 | try { |
| 282 | const entry = JSON.parse(line) as SessionEntry & {payload?: Record<string, unknown>}; |
| 283 | if ( |
| 284 | entry.type === 'response_item' && |
| 285 | (entry.payload as any)?.type === 'message' && |
| 286 | (entry.payload as any)?.role === 'user' |
| 287 | ) { |
| 288 | const content = (entry.payload as any)?.content; |
| 289 | if (Array.isArray(content)) { |
| 290 | for (const part of content) { |
| 291 | if (part.type === 'input_text' && typeof part.text === 'string') { |
| 292 | const text = part.text.trim(); |
| 293 | // Skip environment context injected by codex |
| 294 | if (!text.startsWith('<environment_context>')) { |
| 295 | firstUserMessage = text.slice(0, 120); |
| 296 | resolved = true; |
| 297 | rl.close(); |
| 298 | return; |
| 299 | } |
| 300 | } |
| 301 | } |
| 302 | } |
| 303 | } |
| 304 | } catch { /* skip */ } |
| 305 | }); |
| 306 | |
| 307 | rl.on('close', () => resolve(firstUserMessage)); |
| 308 | rl.on('error', () => resolve(null)); |
| 309 | }); |
| 310 | } |
| 311 | |
| 312 | async scanSessions(options: ScanOptions = {}): Promise<ScannedSession[]> { |
| 313 | const results: ScannedSession[] = []; |