79 lines · 2.9 KB
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<string, string[]>;
};

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