import { readFileSync, writeFileSync, mkdirSync } from 'node:fs' import { dirname } from 'node:path' import { randomUUID } from 'node:crypto' export interface SessionRecord { openclawSessionId: string claudeSessionUuid: string createdAt: number lastActiveAt: number } /** * Persistent openclaw_session ↔ claude_session mapping. * * Single-writer (the OpenclawPlugin process). File-level on every mutation — * simple and correct for the volume we expect. If it ever matters, swap for * sqlite. */ export class SessionMapping { private records = new Map() private path: string constructor(path: string) { this.path = path this.load() } private load(): void { try { const raw = readFileSync(this.path, 'utf8') const parsed = JSON.parse(raw) as { records?: SessionRecord[] } for (const r of parsed.records ?? []) { this.records.set(r.openclawSessionId, r) } } catch { // File doesn't exist or unreadable — start fresh. } } private flush(): void { mkdirSync(dirname(this.path), { recursive: true }) writeFileSync( this.path, JSON.stringify({ records: [...this.records.values()] }, null, 2), 'utf8', ) } get(openclawSessionId: string): SessionRecord | undefined { return this.records.get(openclawSessionId) } /** Get-or-create. Returns the (possibly freshly assigned) claude_session_uuid. */ ensure(openclawSessionId: string): SessionRecord { const existing = this.records.get(openclawSessionId) if (existing) return existing const now = Date.now() const rec: SessionRecord = { openclawSessionId, claudeSessionUuid: randomUUID(), createdAt: now, lastActiveAt: now, } this.records.set(openclawSessionId, rec) this.flush() return rec } touch(openclawSessionId: string): void { const r = this.records.get(openclawSessionId) if (!r) return r.lastActiveAt = Date.now() this.flush() } forget(openclawSessionId: string): void { if (this.records.delete(openclawSessionId)) this.flush() } all(): SessionRecord[] { return [...this.records.values()] } }