proxy_session_recover
Rebuild session indexes from records to restore proxy sessions after crash or corruption.
Instructions
Rebuild session indexes from records after crash/corruption.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| session_id | No | Recover only this session (default: recover all sessions) |
Implementation Reference
- src/tools/sessions.ts:359-375 (handler)Tool registration and handler for `proxy_session_recover` in `src/tools/sessions.ts`.
server.tool( "proxy_session_recover", "Rebuild session indexes from records after crash/corruption.", { session_id: z.string().optional().describe("Recover only this session (default: recover all sessions)"), }, async ({ session_id }) => { try { const result = await proxyManager.recoverSession(session_id); return { content: [{ type: "text", text: JSON.stringify({ status: "success", ...result }) }], }; } catch (e) { return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: toError(e) }) }] }; } }, ); - src/session-store.ts:689-770 (handler)Actual implementation of `recoverSession` logic in `SessionStore` within `src/session-store.ts`.
async recoverSession(sessionId?: string): Promise<{ recovered: Array<{ sessionId: string; exchanges: number; droppedTailBytes: number }>; }> { const ids = sessionId ? [sessionId] : (await this.listSessions()).map((s) => s.id); const recovered: Array<{ sessionId: string; exchanges: number; droppedTailBytes: number }> = []; for (const id of ids) { const sessionDir = path.join(this.rootDir, id); const recordsPath = path.join(sessionDir, RECORDS_FILENAME); const indexPath = path.join(sessionDir, INDEX_FILENAME); const manifestPath = path.join(sessionDir, MANIFEST_FILENAME); let manifest: SessionManifest; try { manifest = await this.getSession(id); } catch { continue; } const buffer = await fs.readFile(recordsPath); let cursor = 0; let seq = 0; let validBytes = 0; const indexLines: string[] = []; while (cursor < buffer.length) { const nl = buffer.indexOf(0x0a, cursor); const end = nl === -1 ? buffer.length : nl; const lineBuf = buffer.subarray(cursor, end); const lineBytes = (nl === -1 ? end - cursor : (end - cursor + 1)); if (lineBuf.length === 0) { cursor = nl === -1 ? end : end + 1; validBytes += lineBytes; continue; } try { const parsed = JSON.parse(lineBuf.toString("utf8")) as PersistedExchangeRecord; seq++; const record = { ...parsed, seq, }; const idx = this.toIndexEntry(record, { recordOffset: validBytes, recordLineBytes: lineBytes }); indexLines.push(JSON.stringify(idx)); validBytes += lineBytes; cursor = nl === -1 ? end : end + 1; } catch { break; } } if (validBytes < buffer.length) { await fs.truncate(recordsPath, validBytes); } await fs.writeFile(indexPath, `${indexLines.join("\n")}${indexLines.length > 0 ? "\n" : ""}`); manifest.exchangeCount = indexLines.length; manifest.lastSequence = indexLines.length; manifest.bytesWritten = validBytes + Buffer.byteLength(`${indexLines.join("\n")}${indexLines.length > 0 ? "\n" : ""}`); manifest.recoveryState = validBytes < buffer.length ? "recovered" : "clean"; manifest.lastError = validBytes < buffer.length ? "Recovered from truncated tail." : null; manifest.lastFlushAt = Date.now(); if (manifest.status === "active") { manifest.status = "stopped"; manifest.endedAt = Date.now(); } await this.writeManifest(manifestPath, manifest); recovered.push({ sessionId: id, exchanges: indexLines.length, droppedTailBytes: Math.max(0, buffer.length - validBytes), }); } return { recovered }; }