import type { AgentIdentity, JsonRpcId, JsonRpcMessage } from "./protocol"; export type SessionInfo = { /** `legacy`: a 2025-era Mcp-Session-Id session; `listen`: one 2026-era subscriptions/listen stream. */ kind: "legacy" | "listen"; userId: string; tokenId: string; agent: AgentIdentity; protocolVersion: string; /** JSON text of the client capabilities declared at initialize. */ clientCapabilities: string; createdAt: number; subscriptionId: JsonRpcId | null; resourceUris: string[]; repos: string[]; /** subscribe_events filters by repo id; resources/subscribe is tracked separately in resourceUris. */ toolEvents: Record; }; const MAX_LOG = 500; const MAX_EVENT_BYTES = 16 * 1024; /** * The replay log behind `Last-Event-ID`: every server-to-client message for the session's standalone * stream is persisted before it is written, so a reconnect (or a wake from hibernation) resumes exactly * where the client left off. */ export class SessionLog { constructor(private readonly sql: SqlStorage) { // v2 records which repository each event is about, so losing access to one only drops that repository's backlog. sql.exec("DROP TABLE IF EXISTS session_log"); sql.exec( "CREATE TABLE IF NOT EXISTS session_log_v2 (seq INTEGER PRIMARY KEY AUTOINCREMENT, repo_id TEXT, data TEXT NOT NULL)", ); } append(message: JsonRpcMessage, repoId: string | null): { id: string; data: string } | null { const data = JSON.stringify(message); if (data.length > MAX_EVENT_BYTES) return null; const seq = this.sql .exec<{ seq: number }>("INSERT INTO session_log_v2 (repo_id, data) VALUES (?, ?) RETURNING seq", repoId, data) .one().seq; this.sql.exec("DELETE FROM session_log_v2 WHERE seq <= ?", seq - MAX_LOG); return { id: eventId(seq), data }; } /** Drops the backlog about one repository (access to it was revoked or must be re-checked). */ clearRepo(repoId: string): void { this.sql.exec("DELETE FROM session_log_v2 WHERE repo_id = ?", repoId); } since(lastEventId: string | null): Array<{ id: string; data: string }> { const after = parseEventId(lastEventId); if (after === null) return []; return this.sql .exec<{ seq: number; data: string }>("SELECT seq, data FROM session_log_v2 WHERE seq > ? ORDER BY seq", after) .toArray() .map((row) => ({ id: eventId(row.seq), data: row.data })); } clear(): void { this.sql.exec("DELETE FROM session_log_v2"); } } /** Event ids name their stream (`get`, the standalone stream) so a Last-Event-ID can't cross streams. */ function eventId(seq: number): string { return `get-${seq}`; } function parseEventId(raw: string | null): number | null { const match = raw ? /^get-(\d+)$/.exec(raw.trim()) : null; return match ? Number(match[1]) : null; } export function sseFrame(data: string, id?: string): string { return `${id ? `id: ${id}\n` : ""}event: message\ndata: ${data}\n\n`; }