import fs from "node:fs/promises"; import path from "node:path"; import { formatMemoryDreamingDay, type MemoryDreamingPhaseName, type MemoryDreamingStorageConfig, } from "openclaw/plugin-sdk/memory-core-host-status"; const DAILY_PHASE_HEADINGS: Record, string> = { light: "## Light Sleep", rem: "## REM Sleep", }; const DEEP_PHASE_HEADING = "## Deep Sleep"; const DAILY_PHASE_LABELS: Record, string> = { light: "light", rem: "rem", }; const PRIMARY_DREAMS_FILENAME = "DREAMS.md"; const DREAMS_FILENAME_ALIASES = [PRIMARY_DREAMS_FILENAME, "dreams.md"] as const; function resolvePhaseMarkers(phase: Exclude): { start: string; end: string; } { const label = DAILY_PHASE_LABELS[phase]; return { start: ``, end: ``, }; } function withTrailingNewline(content: string): string { return content.endsWith("\n") ? content : `${content}\n`; } function replaceManagedBlock(params: { original: string; heading: string; startMarker: string; endMarker: string; body: string; }): string { const managedBlock = `${params.heading}\n${params.startMarker}\n${params.body}\n${params.endMarker}`; const existingPattern = new RegExp( `${escapeRegex(params.heading)}\\n${escapeRegex(params.startMarker)}[\\s\\S]*?${escapeRegex(params.endMarker)}`, "m", ); if (existingPattern.test(params.original)) { return params.original.replace(existingPattern, managedBlock); } const trimmed = params.original.trimEnd(); if (trimmed.length === 0) { return `${managedBlock}\n`; } return `${trimmed}\n\n${managedBlock}\n`; } function escapeRegex(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } async function resolveDreamsPath(workspaceDir: string): Promise { for (const candidate of DREAMS_FILENAME_ALIASES) { const target = path.join(workspaceDir, candidate); try { await fs.access(target); return target; } catch (err) { if ((err as NodeJS.ErrnoException)?.code !== "ENOENT") { throw err; } } } return path.join(workspaceDir, PRIMARY_DREAMS_FILENAME); } async function writeInlineDeepDreamingBlock(params: { workspaceDir: string; body: string; }): Promise { const inlinePath = await resolveDreamsPath(params.workspaceDir); await fs.mkdir(path.dirname(inlinePath), { recursive: true }); const original = await fs.readFile(inlinePath, "utf-8").catch((err: unknown) => { if ((err as NodeJS.ErrnoException)?.code === "ENOENT") { return ""; } throw err; }); const updated = replaceManagedBlock({ original, heading: DEEP_PHASE_HEADING, startMarker: "", endMarker: "", body: params.body, }); await fs.writeFile(inlinePath, withTrailingNewline(updated), "utf-8"); return inlinePath; } function resolveSeparateReportPath( workspaceDir: string, phase: MemoryDreamingPhaseName, epochMs: number, timezone?: string, ): string { const isoDay = formatMemoryDreamingDay(epochMs, timezone); return path.join(workspaceDir, "memory", "dreaming", phase, `${isoDay}.md`); } function shouldWriteInline(storage: MemoryDreamingStorageConfig): boolean { return storage.mode === "inline" || storage.mode === "both"; } function shouldWriteSeparate(storage: MemoryDreamingStorageConfig): boolean { return storage.mode === "separate" || storage.mode === "both" || storage.separateReports; } export async function writeDailyDreamingPhaseBlock(params: { workspaceDir: string; phase: Exclude; bodyLines: string[]; nowMs?: number; timezone?: string; storage: MemoryDreamingStorageConfig; }): Promise<{ inlinePath?: string; reportPath?: string }> { const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now(); const body = params.bodyLines.length > 0 ? params.bodyLines.join("\n") : "- No notable updates."; let inlinePath: string | undefined; let reportPath: string | undefined; if (shouldWriteInline(params.storage)) { inlinePath = await resolveDreamsPath(params.workspaceDir); await fs.mkdir(path.dirname(inlinePath), { recursive: true }); const original = await fs.readFile(inlinePath, "utf-8").catch((err: unknown) => { if ((err as NodeJS.ErrnoException)?.code === "ENOENT") { return ""; } throw err; }); const markers = resolvePhaseMarkers(params.phase); const updated = replaceManagedBlock({ original, heading: DAILY_PHASE_HEADINGS[params.phase], startMarker: markers.start, endMarker: markers.end, body, }); await fs.writeFile(inlinePath, withTrailingNewline(updated), "utf-8"); } if (shouldWriteSeparate(params.storage)) { reportPath = resolveSeparateReportPath( params.workspaceDir, params.phase, nowMs, params.timezone, ); await fs.mkdir(path.dirname(reportPath), { recursive: true }); const report = [ `# ${params.phase === "light" ? "Light Sleep" : "REM Sleep"}`, "", body, "", ].join("\n"); await fs.writeFile(reportPath, report, "utf-8"); } return { ...(inlinePath ? { inlinePath } : {}), ...(reportPath ? { reportPath } : {}), }; } export async function writeDeepDreamingReport(params: { workspaceDir: string; bodyLines: string[]; nowMs?: number; timezone?: string; storage: MemoryDreamingStorageConfig; }): Promise { const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now(); const body = params.bodyLines.length > 0 ? params.bodyLines.join("\n") : "- No durable changes."; await writeInlineDeepDreamingBlock({ workspaceDir: params.workspaceDir, body, }); if (!shouldWriteSeparate(params.storage)) { return undefined; } const reportPath = resolveSeparateReportPath(params.workspaceDir, "deep", nowMs, params.timezone); await fs.mkdir(path.dirname(reportPath), { recursive: true }); await fs.writeFile(reportPath, `# Deep Sleep\n\n${body}\n`, "utf-8"); return reportPath; }