import fs from "node:fs"; import path from "node:path"; export interface StoreIdentity { botId: string; platform: string; } export interface AssistantBinding { chatKey: string; agentId: string; nativeSessionId: string; assistantWorkspace: string; botFingerprint: string; createdAt: number; updatedAt: number; // Refreshed on every assistant turn; drives the idle reset of long-inactive sessions. lastActiveAt?: number; } interface StoreData { version: 3; botId: string; platform: string; bindings: Record; } export class DurableSessionStore { private data: StoreData; private queue: Promise = Promise.resolve(); private lockFd?: fs.promises.FileHandle; private closed = false; constructor(readonly file: string, readonly identity: StoreIdentity) { this.data = emptyData(identity); } async open(): Promise { const directory = path.dirname(this.file); await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 }); const directoryStat = await fs.promises.lstat(directory); if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new Error(`Refusing unsafe state directory: ${directory}`); const directoryMode = directoryStat.mode & 0o777; if (directoryMode !== 0o700) throw new Error(`State directory mode must be 0700, got 0${directoryMode.toString(8)}`); if (typeof process.getuid === "function" && directoryStat.uid !== process.getuid()) throw new Error("State directory is not owned by the current user"); const lockFile = `${this.file}.lock`; try { this.lockFd = await acquireLock(lockFile); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST" || !await removeStaleLock(lockFile)) { if ((error as NodeJS.ErrnoException).code === "EEXIST") throw new Error(`Session state is locked by another gori-agent instance: ${lockFile}`); throw error; } this.lockFd = await acquireLock(lockFile).catch((retryError) => { if ((retryError as NodeJS.ErrnoException).code === "EEXIST") throw new Error(`Session state is locked by another gori-agent instance: ${lockFile}`); throw retryError; }); } try { await this.lockFd.writeFile(`${process.pid}\n`); await this.lockFd.sync(); } catch (error) { await this.releaseLock(); throw error; } try { const raw = await fs.promises.readFile(this.file, "utf8"); const parsed = parseStoreData(JSON.parse(raw) as unknown); if (parsed.botId !== this.identity.botId || parsed.platform !== this.identity.platform) { throw new Error(`state identity mismatch: expected bot '${this.identity.botId}' platform '${this.identity.platform}'`); } this.data = parsed; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") { await this.releaseLock(); throw new Error(`Cannot read session state '${this.file}'; original file was preserved: ${error instanceof Error ? error.message : String(error)}`); } } } getBinding(chatKey: string): AssistantBinding | undefined { const value = this.data.bindings[chatKey]; return value ? structuredClone(value) : undefined; } async setBinding(binding: AssistantBinding): Promise { this.data.bindings[binding.chatKey] = structuredClone(binding); await this.persist(); } async touchBinding(chatKey: string): Promise { const binding = this.data.bindings[chatKey]; if (!binding) return; binding.updatedAt = Date.now(); binding.lastActiveAt = binding.updatedAt; await this.persist(); } async deleteBinding(chatKey: string): Promise { const existing = this.data.bindings[chatKey]; delete this.data.bindings[chatKey]; if (existing) await this.persist(); return existing ? structuredClone(existing) : undefined; } stats(): { bindings: number } { return { bindings: Object.keys(this.data.bindings).length }; } async flush(): Promise { await this.queue; } async close(): Promise { if (this.closed) return; this.closed = true; try { await this.flush(); } finally { await this.releaseLock(); } } private persist(): Promise { if (this.closed) return Promise.reject(new Error("Session store is closed")); const snapshot = JSON.stringify(this.data, null, 2) + "\n"; const operation = this.queue.then(() => atomicWrite(this.file, snapshot)); this.queue = operation.catch(() => undefined); return operation; } private async releaseLock(): Promise { if (!this.lockFd) return; await this.lockFd.close(); this.lockFd = undefined; await fs.promises.unlink(`${this.file}.lock`).catch((error: NodeJS.ErrnoException) => { if (error.code !== "ENOENT") throw error; }); } } export function chatKeyFor(platform: string, chatId: string): string { return `${platform}:${chatId}`; } function emptyData(identity: StoreIdentity): StoreData { return { version: 3, botId: identity.botId, platform: identity.platform, bindings: {} }; } function parseStoreData(value: unknown): StoreData { if (!isRecord(value) || value.version !== 3 || typeof value.botId !== "string" || typeof value.platform !== "string" || !isRecord(value.bindings)) { throw new Error("unsupported state version; Config v3 requires state v3 in a new GORI_AGENT_HOME"); } for (const [chatKey, binding] of Object.entries(value.bindings)) { if (!isRecord(binding) || binding.chatKey !== chatKey || typeof binding.agentId !== "string" || typeof binding.nativeSessionId !== "string" || typeof binding.assistantWorkspace !== "string" || typeof binding.botFingerprint !== "string" || typeof binding.createdAt !== "number" || typeof binding.updatedAt !== "number" || (binding.lastActiveAt !== undefined && typeof binding.lastActiveAt !== "number")) { throw new Error(`invalid state v3 binding '${chatKey}'`); } } return value as unknown as StoreData; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } async function acquireLock(lockFile: string): Promise { return fs.promises.open(lockFile, "wx", 0o600); } async function removeStaleLock(lockFile: string): Promise { let handle: fs.promises.FileHandle | undefined; try { handle = await fs.promises.open(lockFile, "r"); const pid = Number((await handle.readFile("utf8")).trim()); if (!Number.isSafeInteger(pid) || pid <= 1 || processExists(pid)) return false; const opened = await handle.stat(); const current = await fs.promises.lstat(lockFile); if (opened.dev !== current.dev || opened.ino !== current.ino) return false; await fs.promises.unlink(lockFile); return true; } catch { return false; } finally { await handle?.close().catch(() => undefined); } } function processExists(pid: number): boolean { try { process.kill(pid, 0); return true; } catch (error) { return (error as NodeJS.ErrnoException).code === "EPERM"; } } async function atomicWrite(file: string, content: string): Promise { const temp = `${file}.${process.pid}.${Date.now()}.tmp`; let handle: fs.promises.FileHandle | undefined; try { handle = await fs.promises.open(temp, "wx", 0o600); await handle.writeFile(content, "utf8"); await handle.sync(); await handle.close(); handle = undefined; await fs.promises.rename(temp, file); await fs.promises.chmod(file, 0o600); const dir = await fs.promises.open(path.dirname(file), "r"); try { await dir.sync(); } finally { await dir.close(); } } catch (error) { if (handle) await handle.close().catch(() => undefined); await fs.promises.unlink(temp).catch(() => undefined); throw error; } }