this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Support old Codex session format (pre-October 2025)

- codex-detector: Extract cwd from environment_context message for old format
- codex-reader: Handle old format session meta without type field
- codex-reader: Handle top-level function_call entries with embedded apply_patch
- codex-reader: Skip timestamp tracking for entries without timestamps

Old format differences:
- First line: {id, timestamp, git} instead of {type:'session_meta', payload:{...}}
- No timestamps on individual entries
- apply_patch embedded in shell command heredoc

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

alice 8cab66cd f0aada1b

+103 -6
+33 -2
src/core/codex-detector.ts
··· 16 16 } 17 17 18 18 /** 19 - * Extract metadata from the first line (session_meta) of a Codex session file 19 + * Extract metadata from a Codex session file 20 + * Supports both new format (session_meta) and old format (pre-October 2025) 20 21 */ 21 22 function extractCodexSessionMeta( 22 23 filePath: string 23 24 ): { cwd: string; sessionId: string; gitBranch: string } | null { 24 25 try { 25 26 const content = readFileSync(filePath, 'utf-8'); 26 - const firstLine = content.split('\n')[0]; 27 + const lines = content.split('\n'); 28 + const firstLine = lines[0]; 27 29 if (!firstLine) return null; 28 30 29 31 const entry = JSON.parse(firstLine); 32 + 33 + // New format: type === 'session_meta' with payload.cwd 30 34 if (entry.type === 'session_meta' && entry.payload?.cwd) { 31 35 return { 32 36 cwd: entry.payload.cwd, 33 37 sessionId: entry.payload.id || '', 34 38 gitBranch: entry.payload.git?.branch || '', 35 39 }; 40 + } 41 + 42 + // Old format: id at top level, cwd in environment_context message 43 + if (entry.id && !entry.type) { 44 + const sessionId = entry.id; 45 + const gitBranch = entry.git?.branch || ''; 46 + 47 + // Search first few lines for environment_context with cwd 48 + for (let i = 0; i < Math.min(lines.length, 10); i++) { 49 + const line = lines[i]; 50 + if (!line) continue; 51 + try { 52 + const msg = JSON.parse(line); 53 + if (msg.type === 'message' && msg.content) { 54 + for (const block of msg.content) { 55 + if (block.type === 'input_text' && block.text?.includes('Current working directory:')) { 56 + const match = block.text.match(/Current working directory: ([^\n\\]+)/); 57 + if (match) { 58 + return { cwd: match[1], sessionId, gitBranch }; 59 + } 60 + } 61 + } 62 + } 63 + } catch { 64 + // Skip malformed lines 65 + } 66 + } 36 67 } 37 68 } catch { 38 69 // Invalid JSON or missing fields
+70 -4
src/core/codex-reader.ts
··· 176 176 const filesChanged = new Set<string>(); 177 177 178 178 for await (const entry of parseCodexJSONLStream(filePath)) { 179 - // Track timestamps 180 - if (!startTime || entry.timestamp < startTime) startTime = entry.timestamp; 181 - if (!endTime || entry.timestamp > endTime) endTime = entry.timestamp; 179 + // Track timestamps (skip entries without timestamps - common in old format) 180 + if (entry.timestamp) { 181 + if (!startTime || entry.timestamp < startTime) startTime = entry.timestamp; 182 + if (!endTime || entry.timestamp > endTime) endTime = entry.timestamp; 183 + } 182 184 183 - // Handle session_meta (first line) 185 + // Handle session_meta (first line) - new format 184 186 if (entry.type === 'session_meta') { 185 187 const meta = entry.payload as CodexSessionMeta; 186 188 sessionId = meta.id || ''; 187 189 gitBranch = meta.git?.branch || ''; 190 + continue; 191 + } 192 + 193 + // Handle old format first line (pre-October 2025): {id, timestamp, git, ...} without type 194 + const rawEntry = entry as Record<string, unknown>; 195 + if (rawEntry.id && !rawEntry.type && rawEntry.git) { 196 + sessionId = rawEntry.id as string; 197 + gitBranch = (rawEntry.git as Record<string, unknown>)?.branch as string || ''; 188 198 continue; 189 199 } 190 200 ··· 259 269 text, 260 270 toolUses: [], 261 271 }); 272 + } 273 + } 274 + } 275 + 276 + // Handle old format: top-level function_call (pre-October 2025) 277 + // Old format: {"type":"function_call","name":"shell","arguments":"{\"command\":[\"bash\",\"-lc\",\"apply_patch...\"]}"} 278 + if (entry.type === 'function_call' && (entry as Record<string, unknown>).name) { 279 + const oldEntry = entry as Record<string, unknown>; 280 + const name = oldEntry.name as string; 281 + const argsStr = oldEntry.arguments as string; 282 + 283 + // Check if this is a shell command containing apply_patch 284 + if (name === 'shell' && argsStr) { 285 + try { 286 + const args = JSON.parse(argsStr); 287 + const command = args.command; 288 + if (Array.isArray(command) && command.length >= 3) { 289 + const shellCmd = command[2] as string; 290 + if (shellCmd?.includes('apply_patch')) { 291 + // Extract the patch content from the heredoc 292 + const patchMatch = shellCmd.match(/apply_patch\s*<<\s*['"]?PATCH['"]?\n([\s\S]*?)\n\s*PATCH/); 293 + if (patchMatch) { 294 + toolCalls['Edit'] = (toolCalls['Edit'] || 0) + 1; 295 + const files = extractFilesFromPatch(patchMatch[1]); 296 + files.forEach((f) => filesChanged.add(f)); 297 + 298 + assistantMessages++; 299 + messages.push({ 300 + type: 'assistant', 301 + timestamp: (oldEntry.timestamp as string) || '', 302 + text: '', 303 + toolUses: [{ 304 + name: 'Edit', 305 + input: `apply_patch: ${files.join(', ') || 'file changes'}`, 306 + rawInput: oldEntry, 307 + }], 308 + }); 309 + } 310 + } else { 311 + // Regular shell command 312 + toolCalls['Bash'] = (toolCalls['Bash'] || 0) + 1; 313 + assistantMessages++; 314 + messages.push({ 315 + type: 'assistant', 316 + timestamp: (oldEntry.timestamp as string) || '', 317 + text: '', 318 + toolUses: [{ 319 + name: 'Bash', 320 + input: shellCmd?.substring(0, 100) || 'shell command', 321 + rawInput: oldEntry, 322 + }], 323 + }); 324 + } 325 + } 326 + } catch { 327 + // Invalid JSON in arguments 262 328 } 263 329 } 264 330 }