mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-15 19:21:08 +00:00
199 lines
6.2 KiB
TypeScript
199 lines
6.2 KiB
TypeScript
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<Exclude<MemoryDreamingPhaseName, "deep">, string> = {
|
|
light: "## Light Sleep",
|
|
rem: "## REM Sleep",
|
|
};
|
|
const DEEP_PHASE_HEADING = "## Deep Sleep";
|
|
|
|
const DAILY_PHASE_LABELS: Record<Exclude<MemoryDreamingPhaseName, "deep">, 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<MemoryDreamingPhaseName, "deep">): {
|
|
start: string;
|
|
end: string;
|
|
} {
|
|
const label = DAILY_PHASE_LABELS[phase];
|
|
return {
|
|
start: `<!-- openclaw:dreaming:${label}:start -->`,
|
|
end: `<!-- openclaw:dreaming:${label}: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<string> {
|
|
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<string> {
|
|
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: "<!-- openclaw:dreaming:deep:start -->",
|
|
endMarker: "<!-- openclaw:dreaming:deep:end -->",
|
|
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<MemoryDreamingPhaseName, "deep">;
|
|
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<string | undefined> {
|
|
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;
|
|
}
|