Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589// memory/store.mjs// Local-first encrypted session memory store for Claude/Codex workflows.
import { createHash, randomUUID } from "crypto";import { existsSync } from "fs";import { appendFile, mkdir, readFile, readdir, writeFile } from "fs/promises";import { homedir, hostname } from "os";import { join } from "path";
import { decryptJSON, encryptJSON, resolveMemoryKey } from "./crypto.mjs";import { enqueueRemoteRecord, flushRemoteQueue, getRemoteConfig } from "./remote.mjs";import { redactText } from "./redact.mjs";
function nowIso() { return new Date().toISOString();}
function sanitizeSessionId(sessionId) { if (!sessionId) return ""; return String(sessionId).replace(/[^a-zA-Z0-9._:-]/g, "-");}
function sanitizeDeviceId(deviceId) { if (!deviceId) return ""; return String(deviceId).replace(/[^a-zA-Z0-9._:-]/g, "-");}
function createId(prefix) { return `${prefix}_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;}
function resolveStoreHome() { if (process.env.AGENT_MEMORY_HOME) { return process.env.AGENT_MEMORY_HOME; } return join(homedir(), ".ac-agent-memory");}
function pathsFor(home) { return { home, sessionsDir: join(home, "sessions"), eventsDir: join(home, "events"), checkpointsDir: join(home, "checkpoints"), queueDir: join(home, "queue"), queueFile: join(home, "queue", "outbound.ndjson"), deviceFile: join(home, "device-id"), };}
async function ensureStore(paths) { await mkdir(paths.sessionsDir, { recursive: true }); await mkdir(paths.eventsDir, { recursive: true }); await mkdir(paths.checkpointsDir, { recursive: true }); await mkdir(paths.queueDir, { recursive: true });}
async function readJsonFile(path) { if (!existsSync(path)) return null; try { const data = await readFile(path, "utf8"); return JSON.parse(data); } catch { return null; }}
async function writeJsonFile(path, payload) { await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, "utf8");}
function sessionPath(paths, sessionId) { return join(paths.sessionsDir, `${sanitizeSessionId(sessionId)}.json`);}
function eventsPath(paths, sessionId) { return join(paths.eventsDir, `${sanitizeSessionId(sessionId)}.ndjson`);}
function checkpointsPath(paths, sessionId) { return join(paths.checkpointsDir, `${sanitizeSessionId(sessionId)}.ndjson`);}
async function readNdjson(path) { if (!existsSync(path)) return []; const data = await readFile(path, "utf8"); return data .split("\n") .map((line) => line.trim()) .filter(Boolean) .map((line) => { try { return JSON.parse(line); } catch { return null; } }) .filter(Boolean);}
async function appendNdjson(path, payload) { await appendFile(path, `${JSON.stringify(payload)}\n`, "utf8");}
async function resolveDeviceId(paths) { if (process.env.AGENT_DEVICE_ID) { return sanitizeDeviceId(process.env.AGENT_DEVICE_ID); }
if (existsSync(paths.deviceFile)) { return sanitizeDeviceId((await readFile(paths.deviceFile, "utf8")).trim()); }
const generated = `${hostname()}-${randomUUID().slice(0, 8)}`; await writeFile(paths.deviceFile, `${generated}\n`, "utf8"); return generated;}
async function loadSession(paths, sessionId) { return readJsonFile(sessionPath(paths, sessionId));}
async function saveSession(paths, session) { await writeJsonFile(sessionPath(paths, session.session_id), session);}
export async function createSession(options = {}) { const storeHome = resolveStoreHome(); const paths = pathsFor(storeHome); await ensureStore(paths);
const requestedSessionId = sanitizeSessionId(options.sessionId || options.session_id || ""); const sessionId = requestedSessionId || createId("session");
const existing = await loadSession(paths, sessionId); if (existing) return existing;
const timestamp = nowIso(); const session = { version: 1, session_id: sessionId, title: options.title || "Untitled Session", provider: options.provider || "unknown", project: options.project || "default", created_at: timestamp, updated_at: timestamp, last_seq: 0, last_checkpoint_id: null, remembered_from: options.remembered_from || null, };
await saveSession(paths, session); return session;}
function extractPrimaryText(options) { if (typeof options.text === "string") return options.text; if (typeof options.content === "string") return options.content; if (typeof options.message === "string") return options.message; if (options.content != null) { try { return JSON.stringify(options.content); } catch { return String(options.content); } } return "";}
export async function commitEvent(options = {}) { const storeHome = resolveStoreHome(); const paths = pathsFor(storeHome); await ensureStore(paths);
const key = await resolveMemoryKey(storeHome); const deviceId = await resolveDeviceId(paths);
const sessionId = sanitizeSessionId(options.sessionId || options.session_id || ""); const session = await createSession({ sessionId, provider: options.provider, title: options.title, project: options.project, });
const loadedSession = (await loadSession(paths, session.session_id)) || session; const seq = Number(loadedSession.last_seq || 0) + 1; const when = options.when || nowIso(); const primaryText = extractPrimaryText(options); const redaction = redactText(primaryText);
const payload = { role: options.role || "user", text: primaryText, content: options.content ?? null, tool_calls: options.tool_calls ?? null, source: options.source || "manual", context: options.context || null, metadata: options.metadata || null, };
const eventRecord = { version: 1, kind: "event", session_id: session.session_id, device_id: deviceId, seq, when, provider: options.provider || loadedSession.provider || "unknown", model: options.model || null, role: payload.role, source: payload.source, redacted_preview: redaction.preview, redaction_hits: redaction.findings, ciphertext: encryptJSON(payload, key), };
await appendNdjson(eventsPath(paths, session.session_id), eventRecord);
const updatedSession = { ...loadedSession, provider: eventRecord.provider, project: options.project || loadedSession.project, updated_at: when, last_seq: seq, }; await saveSession(paths, updatedSession);
await enqueueRemoteRecord(paths.queueFile, paths.queueDir, { kind: "event", session_id: session.session_id, payload: eventRecord, });
return { session_id: session.session_id, seq, when, redacted_preview: redaction.preview, redaction_hits: redaction.findings, };}
function buildSummaryFromEvents(events, maxItems = 8) { const previews = events .map((event) => event.redacted_preview) .filter(Boolean) .slice(-1 * Math.max(maxItems * 2, 1));
const unique = []; for (const preview of previews) { if (!unique.includes(preview)) unique.push(preview); }
const selected = unique.slice(-1 * maxItems); if (selected.length === 0) { return "No notable messages captured yet."; }
return selected.map((item, index) => `${index + 1}. ${item}`).join("\n");}
async function listCheckpointsForSession(paths, sessionId) { return readNdjson(checkpointsPath(paths, sessionId));}
export async function createCheckpoint(options = {}) { const sessionId = sanitizeSessionId(options.sessionId || options.session_id || ""); if (!sessionId) { throw new Error("createCheckpoint requires sessionId"); }
const storeHome = resolveStoreHome(); const paths = pathsFor(storeHome); await ensureStore(paths);
const session = await loadSession(paths, sessionId); if (!session) { throw new Error(`session not found: ${sessionId}`); }
const events = await readNdjson(eventsPath(paths, sessionId)); const maxEvents = Number(options.maxEvents || 40); const scoped = events.slice(-1 * Math.max(maxEvents, 1));
const providedSummary = options.summary || ""; const autoSummary = buildSummaryFromEvents(scoped, 8); const summaryInput = providedSummary || autoSummary; const redactedSummary = redactText(summaryInput);
const checkpoint = { version: 1, kind: "checkpoint", checkpoint_id: createId("checkpoint"), session_id: sessionId, when: options.when || nowIso(), reason: options.reason || "manual", based_on_seq: session.last_seq || 0, summary: redactedSummary.redacted, redaction_hits: redactedSummary.findings, };
await appendNdjson(checkpointsPath(paths, sessionId), checkpoint);
const updatedSession = { ...session, updated_at: checkpoint.when, last_checkpoint_id: checkpoint.checkpoint_id, }; await saveSession(paths, updatedSession);
await enqueueRemoteRecord(paths.queueFile, paths.queueDir, { kind: "checkpoint", session_id: sessionId, payload: checkpoint, });
return checkpoint;}
export async function listSessions(options = {}) { const storeHome = resolveStoreHome(); const paths = pathsFor(storeHome); await ensureStore(paths);
const files = await readdir(paths.sessionsDir); const sessions = [];
for (const file of files) { if (!file.endsWith(".json")) continue; const loaded = await readJsonFile(join(paths.sessionsDir, file)); if (loaded) sessions.push(loaded); }
const filtered = sessions.filter((session) => { if (options.project && session.project !== options.project) return false; return true; });
filtered.sort((a, b) => { const left = new Date(a.updated_at || a.created_at || 0).getTime(); const right = new Date(b.updated_at || b.created_at || 0).getTime(); return right - left; });
const limit = Number(options.limit || 50); return filtered.slice(0, Math.max(limit, 1));}
// Pull a readable window around the first match, so a hit shows its context// instead of a whole prompt. Redacted on the way out: the ciphertext holds the// prompt verbatim (secrets included), and a search result travels — to another// machine over ssh, into an agent's context — so it must not carry them.function snippetAround(text, index, span = 160) { const start = Math.max(0, index - Math.floor(span / 2)); const end = Math.min(text.length, start + span); const body = text.slice(start, end).replace(/\s+/g, " ").trim(); return `${start > 0 ? "…" : ""}${redactText(body).redacted}${end < text.length ? "…" : ""}`;}
/// Full-text search across this machine's prompt history.////// Matches against the DECRYPTED event text — the `redacted_preview` on each/// record is lossy (truncated, secrets masked), so searching it alone would/// miss real hits. Decryption needs the local key, which is exactly why the/// fleet search fans out over ssh and lets each machine search its own store:/// no key ever leaves the Mac that owns it. Events that won't decrypt (written/// under a rotated key) fall back to their preview rather than vanishing.export async function searchEvents(options = {}) { const query = String(options.query || "").trim(); if (!query) throw new Error("searchEvents requires a query");
const storeHome = resolveStoreHome(); const paths = pathsFor(storeHome); await ensureStore(paths);
const key = await resolveMemoryKey(storeHome); const deviceId = await resolveDeviceId(paths); const limit = Math.max(Number(options.limit || 20), 1); const since = options.since ? new Date(options.since).getTime() : null; const pattern = options.regex ? new RegExp(query, "i") : null; const needle = query.toLowerCase();
// Sessions carry the project + title; events only point back at them. const sessions = new Map(); for (const session of await listSessions({ limit: Number.MAX_SAFE_INTEGER })) { sessions.set(session.session_id, session); }
const files = (await readdir(paths.eventsDir)).filter((name) => name.endsWith(".ndjson")); const hits = [];
for (const file of files) { const sessionId = file.replace(/\.ndjson$/, ""); const session = sessions.get(sessionId) || null;
if (options.session && sessionId !== sanitizeSessionId(options.session)) continue; if (options.project && session?.project !== options.project) continue;
for (const event of await readNdjson(join(paths.eventsDir, file))) { if (since && new Date(event.when || 0).getTime() < since) continue; if (options.provider && event.provider !== options.provider) continue; if (options.role && event.role !== options.role) continue;
let text = ""; try { const payload = decryptJSON(event.ciphertext, key); if (typeof payload?.text === "string") text = payload.text; } catch { text = ""; // rotated/foreign key — fall through to the preview } const haystack = text || event.redacted_preview || ""; if (!haystack) continue;
const at = pattern ? haystack.search(pattern) : haystack.toLowerCase().indexOf(needle); if (at < 0) continue;
hits.push({ device_id: event.device_id || deviceId, session_id: sessionId, session_title: session?.title || null, project: session?.project || null, provider: event.provider || null, role: event.role || null, source: event.source || null, seq: event.seq, when: event.when, snippet: snippetAround(haystack, at), }); } }
hits.sort((a, b) => new Date(b.when || 0) - new Date(a.when || 0)); return { query, device_id: deviceId, total: hits.length, hits: hits.slice(0, limit) };}
export async function rememberSession(options = {}) { const fromSessionId = sanitizeSessionId(options.fromSessionId || options.from_session_id || ""); if (!fromSessionId) { throw new Error("rememberSession requires fromSessionId"); }
const storeHome = resolveStoreHome(); const paths = pathsFor(storeHome); await ensureStore(paths);
const sourceSession = await loadSession(paths, fromSessionId); if (!sourceSession) { throw new Error(`source session not found: ${fromSessionId}`); }
const checkpoints = await listCheckpointsForSession(paths, fromSessionId); let selectedCheckpoint = null;
if (options.checkpointId) { selectedCheckpoint = checkpoints.find( (checkpoint) => checkpoint.checkpoint_id === options.checkpointId ); }
if (!selectedCheckpoint && checkpoints.length > 0) { selectedCheckpoint = checkpoints[checkpoints.length - 1]; }
const rememberedFrom = { session_id: fromSessionId, checkpoint_id: selectedCheckpoint?.checkpoint_id || null, seq: selectedCheckpoint?.based_on_seq || sourceSession.last_seq || 0, };
const newSession = await createSession({ sessionId: options.sessionId, title: options.title || `Remembered from ${fromSessionId}`, provider: options.provider || sourceSession.provider, project: options.project || sourceSession.project, remembered_from: rememberedFrom, });
if (selectedCheckpoint) { await createCheckpoint({ sessionId: newSession.session_id, reason: "remember-seed", summary: `Remembered from ${fromSessionId} @ seq ${rememberedFrom.seq}\n${selectedCheckpoint.summary}`, }); }
return { session_id: newSession.session_id, remembered_from: rememberedFrom, };}
export async function flushRemote() { const storeHome = resolveStoreHome(); const paths = pathsFor(storeHome); await ensureStore(paths); return flushRemoteQueue(paths.queueFile);}
export async function getSession(sessionId) { const normalizedSessionId = sanitizeSessionId(sessionId); const storeHome = resolveStoreHome(); const paths = pathsFor(storeHome); await ensureStore(paths); return loadSession(paths, normalizedSessionId);}
export async function listSessionCheckpoints(sessionId) { const normalizedSessionId = sanitizeSessionId(sessionId); const storeHome = resolveStoreHome(); const paths = pathsFor(storeHome); await ensureStore(paths); return listCheckpointsForSession(paths, normalizedSessionId);}
export async function setDeviceId(deviceId) { const normalized = sanitizeDeviceId(deviceId); if (!normalized) { throw new Error("setDeviceId requires a non-empty device id"); }
const storeHome = resolveStoreHome(); const paths = pathsFor(storeHome); await ensureStore(paths); await writeFile(paths.deviceFile, `${normalized}\n`, "utf8"); return normalized;}
export async function inspectStore() { const storeHome = resolveStoreHome(); const paths = pathsFor(storeHome); await ensureStore(paths);
const key = await resolveMemoryKey(storeHome); const deviceId = await resolveDeviceId(paths); const sessions = await readdir(paths.sessionsDir); const events = await readdir(paths.eventsDir); const checkpoints = await readdir(paths.checkpointsDir); const remote = getRemoteConfig(); const keyFingerprint = createHash("sha256") .update(key) .digest("hex") .slice(0, 16);
return { home: storeHome, device_id: deviceId, key_source: process.env.AGENT_MEMORY_KEY ? "env" : "file", key_fingerprint: keyFingerprint, sessions_count: sessions.filter((name) => name.endsWith(".json")).length, event_streams_count: events.filter((name) => name.endsWith(".ndjson")).length, checkpoint_streams_count: checkpoints.filter((name) => name.endsWith(".ndjson")).length, remote_enabled: remote.enabled, remote_endpoint: remote.endpoint || null, };}
export async function buildEnvProfile(options = {}) { const storeHome = resolveStoreHome(); const paths = pathsFor(storeHome); await ensureStore(paths);
const key = await resolveMemoryKey(storeHome); const deviceId = options.deviceId ? await setDeviceId(options.deviceId) : await resolveDeviceId(paths);
const lines = []; if (options.includeKey) { lines.push(`AGENT_MEMORY_KEY=base64:${key.toString("base64")}`); }
lines.push(`AGENT_DEVICE_ID=${deviceId}`);
const project = options.project || process.env.AGENT_MEMORY_PROJECT || ""; const provider = options.provider || process.env.AGENT_MEMORY_PROVIDER || ""; const ticket = options.ticket || process.env.AGENT_MEMORY_TICKET || ""; const sessionId = options.sessionId || process.env.AGENT_SESSION_ID || "";
if (project) lines.push(`AGENT_MEMORY_PROJECT=${project}`); if (provider) lines.push(`AGENT_MEMORY_PROVIDER=${provider}`); if (ticket) lines.push(`AGENT_MEMORY_TICKET=${ticket}`); if (sessionId) lines.push(`AGENT_SESSION_ID=${sessionId}`);
return { home: storeHome, lines, };}