( dir: string, startedAfter: number, )
| 122 | * Polls every 500ms, times out after 10 seconds. |
| 123 | */ |
| 124 | export async function findLatestCodexSession( |
| 125 | dir: string, |
| 126 | startedAfter: number, |
| 127 | ): Promise<{sessionId: string; filePath: string} | null> { |
| 128 | // Allow a small buffer in case the file was created just before our timestamp |
| 129 | const cutoff = startedAfter - 2_000; |
| 130 | const deadline = Date.now() + 10_000; |
| 131 | |
| 132 | // codex records realpath in session_meta.cwd, but the caller passes the |
| 133 | // user-supplied symlinked form (e.g. Railway's `/app` → `/mnt/volume/workspace`). |
| 134 | // Resolve both sides so the match works regardless of which form arrives. |
| 135 | let targetDir = dir; |
| 136 | try { targetDir = realpathSync(dir); } catch { /* dir may not exist yet */ } |
| 137 | |
| 138 | async function scan(): Promise<{sessionId: string; filePath: string} | null> { |
| 139 | for (const {filePath, sessionId} of walkCodexSessions()) { |
| 140 | let stats; |
| 141 | try { |
| 142 | stats = statSync(filePath); |
| 143 | } catch { |
| 144 | continue; |
| 145 | } |
| 146 | |
| 147 | // Stop scanning once files are older than our start time |
| 148 | if (stats.mtimeMs < cutoff) break; |
| 149 | |
| 150 | const first = await readFirstLine(filePath); |
| 151 | if (first?.type === 'session_meta') { |
| 152 | const recordedCwd = (first.payload as any)?.cwd; |
| 153 | if (typeof recordedCwd !== 'string') continue; |
| 154 | let cwd = recordedCwd; |
| 155 | try { cwd = realpathSync(recordedCwd); } catch { /* recorded path may be gone */ } |
| 156 | if (cwd === targetDir || recordedCwd === dir) return {sessionId, filePath}; |
| 157 | } |
| 158 | } |
| 159 | return null; |
| 160 | } |
| 161 | |
| 162 | while (Date.now() < deadline) { |
| 163 | const result = await scan(); |
| 164 | if (result) return result; |
| 165 | await new Promise((resolve) => setTimeout(resolve, 500)); |
| 166 | } |
| 167 | |
| 168 | return null; |
| 169 | } |
| 170 | |
| 171 | export function codexEntriesToTurns(entries: SessionEntry[]): ConversationTurn[] { |
| 172 | const turns: ConversationTurn[] = []; |
no test coverage detected