* Internal implementation of appendSessionLog with retry logic * Retries on transient errors (network, 5xx, 429). On 409, adopts the server's * last UUID and retries (handles stale state from killed process's in-flight * requests). Fails immediately on 401.
( sessionId: string, entry: TranscriptMessage, url: string, headers: Record<string, string>, )
| 61 | * requests). Fails immediately on 401. |
| 62 | */ |
| 63 | async function appendSessionLogImpl( |
| 64 | sessionId: string, |
| 65 | entry: TranscriptMessage, |
| 66 | url: string, |
| 67 | headers: Record<string, string>, |
| 68 | ): Promise<boolean> { |
| 69 | for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { |
| 70 | try { |
| 71 | const lastUuid = lastUuidMap.get(sessionId) |
| 72 | const requestHeaders = { ...headers } |
| 73 | if (lastUuid) { |
| 74 | requestHeaders['Last-Uuid'] = lastUuid |
| 75 | } |
| 76 | |
| 77 | const response = await axios.put(url, entry, { |
| 78 | headers: requestHeaders, |
| 79 | validateStatus: status => status < 500, |
| 80 | }) |
| 81 | |
| 82 | if (response.status === 200 || response.status === 201) { |
| 83 | lastUuidMap.set(sessionId, entry.uuid) |
| 84 | logForDebugging( |
| 85 | `Successfully persisted session log entry for session ${sessionId}`, |
| 86 | ) |
| 87 | return true |
| 88 | } |
| 89 | |
| 90 | if (response.status === 409) { |
| 91 | // Check if our entry was actually stored (server returned 409 but entry exists) |
| 92 | // This handles the scenario where entry was stored but client received an error |
| 93 | // response, causing lastUuidMap to be stale |
| 94 | const serverLastUuid = response.headers['x-last-uuid'] |
| 95 | if (serverLastUuid === entry.uuid) { |
| 96 | // Our entry IS the last entry on server - it was stored successfully previously |
| 97 | lastUuidMap.set(sessionId, entry.uuid) |
| 98 | logForDebugging( |
| 99 | `Session entry ${entry.uuid} already present on server, recovering from stale state`, |
| 100 | ) |
| 101 | logForDiagnosticsNoPII('info', 'session_persist_recovered_from_409') |
| 102 | return true |
| 103 | } |
| 104 | |
| 105 | // Another writer (e.g. in-flight request from a killed process) |
| 106 | // advanced the server's chain. Try to adopt the server's last UUID |
| 107 | // from the response header, or re-fetch the session to discover it. |
| 108 | if (serverLastUuid) { |
| 109 | lastUuidMap.set(sessionId, serverLastUuid as UUID) |
| 110 | logForDebugging( |
| 111 | `Session 409: adopting server lastUuid=${serverLastUuid} from header, retrying entry ${entry.uuid}`, |
| 112 | ) |
| 113 | } else { |
| 114 | // Server didn't return x-last-uuid (e.g. v1 endpoint). Re-fetch |
| 115 | // the session to discover the current head of the append chain. |
| 116 | const logs = await fetchSessionLogsFromUrl(sessionId, url, headers) |
| 117 | const adoptedUuid = findLastUuid(logs) |
| 118 | if (adoptedUuid) { |
| 119 | lastUuidMap.set(sessionId, adoptedUuid) |
| 120 | logForDebugging( |
no test coverage detected