mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix(memory-core): vary dream diary recall snippets (#91225)
Prevent repeated first-day Dream Diary narratives by prioritizing fresh recall snippets across the bounded short-term store and adding recent diary context to narrative generation. Keep diary reads best-effort and reject symlink/non-file inputs. Fixes #83830. Thanks @mushuiyu886. Co-authored-by: 杨浩宇0668001029 <yang.haoyu@xydigit.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { createAsyncLock } from "openclaw/plugin-sdk/async-lock-runtime";
|
||||
import { extractErrorCode } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { resolveGlobalMap } from "openclaw/plugin-sdk/global-singleton";
|
||||
import { replaceManagedMarkdownBlock } from "openclaw/plugin-sdk/memory-host-markdown";
|
||||
import { replaceFileAtomic } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { readRegularFile, replaceFileAtomic } from "openclaw/plugin-sdk/security-runtime";
|
||||
|
||||
const DREAMS_FILENAMES = ["DREAMS.md", "dreams.md"] as const;
|
||||
const DEEP_START_MARKER = "<!-- openclaw:dreaming:deep:start -->";
|
||||
@@ -19,7 +19,7 @@ type DreamsFileLockEntry = {
|
||||
|
||||
const dreamsFileLocks = resolveGlobalMap<string, DreamsFileLockEntry>(DREAMS_FILE_LOCKS_KEY);
|
||||
|
||||
async function resolveDreamsPath(workspaceDir: string): Promise<string> {
|
||||
export async function resolveDreamsPath(workspaceDir: string): Promise<string> {
|
||||
for (const name of DREAMS_FILENAMES) {
|
||||
const target = path.join(workspaceDir, name);
|
||||
try {
|
||||
@@ -34,11 +34,27 @@ async function resolveDreamsPath(workspaceDir: string): Promise<string> {
|
||||
return path.join(workspaceDir, DREAMS_FILENAMES[0]);
|
||||
}
|
||||
|
||||
async function readDreamsFile(dreamsPath: string): Promise<string> {
|
||||
function isEmptyDreamsReadError(err: unknown): boolean {
|
||||
const code = extractErrorCode(err);
|
||||
if (
|
||||
code === "ENOENT" ||
|
||||
code === "ENOTDIR" ||
|
||||
code === "not-found" ||
|
||||
code === "not-file" ||
|
||||
code === "path-alias" ||
|
||||
code === "path-mismatch" ||
|
||||
code === "symlink"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return err instanceof Error && err.message === "path must be a regular file";
|
||||
}
|
||||
|
||||
export async function readDreamsFile(dreamsPath: string): Promise<string> {
|
||||
try {
|
||||
return await fs.readFile(dreamsPath, "utf-8");
|
||||
return (await readRegularFile({ filePath: dreamsPath })).buffer.toString("utf-8");
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
|
||||
if (isEmptyDreamsReadError(err)) {
|
||||
return "";
|
||||
}
|
||||
throw err;
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
formatNarrativeDate,
|
||||
formatBackfillDiaryDate,
|
||||
generateAndAppendDreamNarrative,
|
||||
readRecentDreamDiaryEntries,
|
||||
removeBackfillDiaryEntries,
|
||||
runDetachedDreamNarrative,
|
||||
type NarrativePhaseData,
|
||||
@@ -133,6 +134,19 @@ describe("buildNarrativePrompt", () => {
|
||||
expect(prompt).toContain("snippet-11");
|
||||
expect(prompt).not.toContain("snippet-12");
|
||||
});
|
||||
|
||||
it("includes current sweep and recent diary context", () => {
|
||||
const prompt = buildNarrativePrompt({
|
||||
phase: "light",
|
||||
snippets: ["Later workspace routing notes surfaced."],
|
||||
currentDate: "April 6, 2026, 9:00 AM UTC",
|
||||
recentDiaryEntries: ["The first meeting memory already filled the page."],
|
||||
});
|
||||
expect(prompt).toContain("Diary continuity context");
|
||||
expect(prompt).toContain("Current sweep: April 6, 2026, 9:00 AM UTC");
|
||||
expect(prompt).toContain("The first meeting memory already filled the page.");
|
||||
expect(prompt).toContain("do not replay the same first-day framing");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractNarrativeText", () => {
|
||||
@@ -388,6 +402,77 @@ describe("appendNarrativeEntry", () => {
|
||||
expect(secondIdx).toBeLessThan(end);
|
||||
});
|
||||
|
||||
it("reads recent diary entries without timestamps or markers", async () => {
|
||||
const workspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-");
|
||||
await appendNarrativeEntry({
|
||||
workspaceDir,
|
||||
narrative: "The first meeting memory already filled the page.",
|
||||
nowMs: Date.parse("2026-04-04T03:00:00Z"),
|
||||
timezone: "UTC",
|
||||
});
|
||||
await appendNarrativeEntry({
|
||||
workspaceDir,
|
||||
narrative: "A later routing note flickered in the margins.",
|
||||
nowMs: Date.parse("2026-04-05T03:00:00Z"),
|
||||
timezone: "UTC",
|
||||
});
|
||||
|
||||
await expect(readRecentDreamDiaryEntries({ workspaceDir, limit: 1 })).resolves.toEqual([
|
||||
"A later routing note flickered in the margins.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("skips symlinked DREAMS.md when building recent diary context", async () => {
|
||||
const workspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-");
|
||||
const targetPath = path.join(workspaceDir, "target-dreams.md");
|
||||
const dreamsPath = path.join(workspaceDir, "DREAMS.md");
|
||||
const symlinkTargetDiary = "Symlink target diary text must not enter the prompt.";
|
||||
await fs.writeFile(
|
||||
targetPath,
|
||||
[
|
||||
"# Dream Diary",
|
||||
"",
|
||||
"<!-- openclaw:dreaming:diary:start -->",
|
||||
"---",
|
||||
"",
|
||||
"*April 5, 2026, 3:00 AM UTC*",
|
||||
"",
|
||||
symlinkTargetDiary,
|
||||
"",
|
||||
"<!-- openclaw:dreaming:diary:end -->",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.symlink(targetPath, dreamsPath);
|
||||
|
||||
const entries = await readRecentDreamDiaryEntries({ workspaceDir, limit: 3 });
|
||||
expect(entries).toEqual([]);
|
||||
const prompt = buildNarrativePrompt({
|
||||
phase: "light",
|
||||
snippets: ["A fresh routing memory arrived."],
|
||||
recentDiaryEntries: entries,
|
||||
});
|
||||
expect(prompt).not.toContain(symlinkTargetDiary);
|
||||
});
|
||||
|
||||
it("skips non-file DREAMS.md when reading recent diary context", async () => {
|
||||
const workspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-");
|
||||
await fs.mkdir(path.join(workspaceDir, "DREAMS.md"));
|
||||
|
||||
await expect(readRecentDreamDiaryEntries({ workspaceDir, limit: 3 })).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it("treats unreadable DREAMS.md as empty recent diary context", async () => {
|
||||
const workspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-");
|
||||
await fs.writeFile(path.join(workspaceDir, "DREAMS.md"), "unreadable", "utf-8");
|
||||
vi.spyOn(fs, "access").mockRejectedValueOnce(
|
||||
Object.assign(new Error("permission denied"), { code: "EACCES" }),
|
||||
);
|
||||
|
||||
await expect(readRecentDreamDiaryEntries({ workspaceDir, limit: 3 })).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it("prepends diary before existing managed blocks", async () => {
|
||||
const workspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-");
|
||||
const dreamsPath = path.join(workspaceDir, "DREAMS.md");
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
resolveStorePath,
|
||||
updateSessionStore,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { updateDreamsFile } from "./dreaming-dreams-file.js";
|
||||
import { readDreamsFile, resolveDreamsPath, updateDreamsFile } from "./dreaming-dreams-file.js";
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -54,6 +54,8 @@ export type NarrativePhaseData = {
|
||||
themes?: string[];
|
||||
/** Snippets that were promoted to durable memory (deep). */
|
||||
promotions?: string[];
|
||||
currentDate?: string;
|
||||
recentDiaryEntries?: string[];
|
||||
};
|
||||
|
||||
type Logger = {
|
||||
@@ -110,6 +112,8 @@ const SAFE_SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
||||
const DIARY_START_MARKER = "<!-- openclaw:dreaming:diary:start -->";
|
||||
const DIARY_END_MARKER = "<!-- openclaw:dreaming:diary:end -->";
|
||||
const BACKFILL_ENTRY_MARKER = "openclaw:dreaming:backfill-entry";
|
||||
const RECENT_DIARY_CONTEXT_LIMIT = 3;
|
||||
const RECENT_DIARY_CONTEXT_MAX_CHARS = 360;
|
||||
const NARRATIVE_SESSION_LOCKS_KEY = Symbol.for(
|
||||
"openclaw.memoryCore.dreamingNarrative.sessionLocks",
|
||||
);
|
||||
@@ -305,6 +309,27 @@ export function buildNarrativePrompt(data: NarrativePhaseData): string {
|
||||
}
|
||||
}
|
||||
|
||||
const currentDate = data.currentDate?.trim();
|
||||
const recentDiaryEntries = (data.recentDiaryEntries ?? [])
|
||||
.map(clampDiaryContextEntry)
|
||||
.filter((entry) => entry.length > 0)
|
||||
.slice(0, RECENT_DIARY_CONTEXT_LIMIT);
|
||||
if (currentDate || recentDiaryEntries.length > 0) {
|
||||
lines.push("\nDiary continuity context:");
|
||||
if (currentDate) {
|
||||
lines.push(`- Current sweep: ${currentDate}`);
|
||||
}
|
||||
if (recentDiaryEntries.length > 0) {
|
||||
lines.push("- Recent diary entries already written:");
|
||||
for (const entry of recentDiaryEntries) {
|
||||
lines.push(` - ${entry}`);
|
||||
}
|
||||
}
|
||||
lines.push(
|
||||
"- Prefer a fresh angle; do not replay the same first-day framing unless newer fragments change it.",
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
@@ -435,6 +460,78 @@ function splitDiaryBlocks(diaryContent: string): string[] {
|
||||
.filter((block) => block.length > 0);
|
||||
}
|
||||
|
||||
function clampDiaryContextEntry(entry: string): string {
|
||||
const normalized = entry.replace(/\s+/g, " ").trim();
|
||||
if (normalized.length <= RECENT_DIARY_CONTEXT_MAX_CHARS) {
|
||||
return normalized;
|
||||
}
|
||||
return `${normalized.slice(0, RECENT_DIARY_CONTEXT_MAX_CHARS).trimEnd()}...`;
|
||||
}
|
||||
|
||||
function normalizeDiaryBlockBody(block: string): string {
|
||||
const bodyLines: string[] = [];
|
||||
for (const line of block.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("<!--") || trimmed.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith("*") && trimmed.endsWith("*") && trimmed.length > 2) {
|
||||
continue;
|
||||
}
|
||||
bodyLines.push(trimmed);
|
||||
}
|
||||
return clampDiaryContextEntry(bodyLines.join(" "));
|
||||
}
|
||||
|
||||
function isOptionalDiaryContextReadError(err: unknown): boolean {
|
||||
const code = extractErrorCode(err);
|
||||
if (
|
||||
code === "EACCES" ||
|
||||
code === "EPERM" ||
|
||||
code === "ENOENT" ||
|
||||
code === "ENOTDIR" ||
|
||||
code === "not-found" ||
|
||||
code === "not-file" ||
|
||||
code === "path-alias" ||
|
||||
code === "path-mismatch" ||
|
||||
code === "symlink"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return err instanceof Error && err.message === "path must be a regular file";
|
||||
}
|
||||
|
||||
export async function readRecentDreamDiaryEntries(params: {
|
||||
workspaceDir: string;
|
||||
limit?: number;
|
||||
}): Promise<string[]> {
|
||||
const limit = Math.max(0, Math.floor(params.limit ?? RECENT_DIARY_CONTEXT_LIMIT));
|
||||
if (limit === 0) {
|
||||
return [];
|
||||
}
|
||||
let existing: string;
|
||||
try {
|
||||
const dreamsPath = await resolveDreamsPath(params.workspaceDir);
|
||||
existing = await readDreamsFile(dreamsPath);
|
||||
} catch (err) {
|
||||
if (isOptionalDiaryContextReadError(err)) {
|
||||
return [];
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
const startIdx = existing.indexOf(DIARY_START_MARKER);
|
||||
const endIdx = existing.indexOf(DIARY_END_MARKER);
|
||||
if (startIdx < 0 || endIdx < 0 || endIdx < startIdx) {
|
||||
return [];
|
||||
}
|
||||
const inner = existing.slice(startIdx + DIARY_START_MARKER.length, endIdx);
|
||||
return splitDiaryBlocks(inner)
|
||||
.map(normalizeDiaryBlockBody)
|
||||
.filter((entry) => entry.length > 0)
|
||||
.slice(-limit)
|
||||
.toReversed();
|
||||
}
|
||||
|
||||
function normalizeDiaryBlockFingerprint(block: string): string {
|
||||
const lines = block
|
||||
.split("\n")
|
||||
|
||||
@@ -450,6 +450,131 @@ describe("memory-core dreaming phases", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers a fresh light snippet outside the top diary-covered candidates", async () => {
|
||||
const workspaceDir = await createDreamingWorkspace();
|
||||
const stalePath = path.join(workspaceDir, "memory", "2026-04-03.md");
|
||||
const freshPath = path.join(workspaceDir, "memory", "2026-04-04.md");
|
||||
const nowMs = Date.parse("2026-04-05T10:05:00.000Z");
|
||||
const staleSnippets = [
|
||||
"初次见面时,我第一次醒来并认识了主人。",
|
||||
"The first morning began beside a quiet terminal.",
|
||||
"An early config file felt like the first map of home.",
|
||||
"The initial heartbeat made the empty workspace feel awake.",
|
||||
];
|
||||
await fs.writeFile(stalePath, `${staleSnippets.join("\n")}\n`, "utf-8");
|
||||
await fs.writeFile(
|
||||
freshPath,
|
||||
"Later routing notes: queue hydration changed after plugin reload.\n",
|
||||
"utf-8",
|
||||
);
|
||||
for (const [index, snippet] of staleSnippets.entries()) {
|
||||
for (let recall = 0; recall < staleSnippets.length - index; recall += 1) {
|
||||
await recordShortTermRecalls({
|
||||
workspaceDir,
|
||||
query: `first-day-${index}-${recall}`,
|
||||
nowMs,
|
||||
results: [
|
||||
{
|
||||
path: "memory/2026-04-03.md",
|
||||
startLine: index + 1,
|
||||
endLine: index + 1,
|
||||
score: 0.93,
|
||||
snippet,
|
||||
source: "memory",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
await recordShortTermRecalls({
|
||||
workspaceDir,
|
||||
query: "routing queue reload",
|
||||
nowMs,
|
||||
results: [
|
||||
{
|
||||
path: "memory/2026-04-04.md",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
score: 0.91,
|
||||
snippet: "Later routing notes: queue hydration changed after plugin reload.",
|
||||
source: "memory",
|
||||
},
|
||||
],
|
||||
});
|
||||
await fs.writeFile(
|
||||
path.join(workspaceDir, "DREAMS.md"),
|
||||
[
|
||||
"# Dream Diary",
|
||||
"",
|
||||
"<!-- openclaw:dreaming:diary:start -->",
|
||||
...staleSnippets.flatMap((snippet, index) => [
|
||||
"---",
|
||||
"",
|
||||
`*April ${index + 1}, 2026, 10:00 AM UTC*`,
|
||||
"",
|
||||
snippet,
|
||||
"",
|
||||
]),
|
||||
"<!-- openclaw:dreaming:diary:end -->",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
const subagent = createMockNarrativeSubagent("A later routing note finally took the page.");
|
||||
const testConfig: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: workspaceDir,
|
||||
userTimezone: "UTC",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-core": {
|
||||
config: {
|
||||
dreaming: {
|
||||
enabled: true,
|
||||
timezone: "UTC",
|
||||
storage: { mode: "inline", separateReports: false },
|
||||
phases: {
|
||||
light: {
|
||||
enabled: true,
|
||||
limit: 1,
|
||||
lookbackDays: 7,
|
||||
},
|
||||
rem: {
|
||||
enabled: false,
|
||||
limit: 0,
|
||||
lookbackDays: 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
await runDreamingSweepPhases({
|
||||
workspaceDir,
|
||||
cfg: testConfig,
|
||||
pluginConfig: resolveMemoryCorePluginConfig(testConfig),
|
||||
logger,
|
||||
subagent,
|
||||
nowMs,
|
||||
});
|
||||
|
||||
const message = firstNarrativeRun(subagent).message;
|
||||
expect(message).toContain("Later routing notes: queue hydration changed after plugin reload.");
|
||||
expect(message).toContain("Recent diary entries already written");
|
||||
expect(message).not.toContain("\n- 初次见面时,我第一次醒来并认识了主人。");
|
||||
});
|
||||
|
||||
it("triggers light dreaming when the token is embedded in a reminder body", async () => {
|
||||
const workspaceDir = await createDreamingWorkspace();
|
||||
await withDreamingTestClock(async () => {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/strin
|
||||
import { writeDailyDreamingPhaseBlock } from "./dreaming-markdown.js";
|
||||
import {
|
||||
generateAndAppendDreamNarrative,
|
||||
readRecentDreamDiaryEntries,
|
||||
type NarrativePhaseData,
|
||||
runDetachedDreamNarrative,
|
||||
} from "./dreaming-narrative.js";
|
||||
@@ -112,6 +113,8 @@ const SESSION_INGESTION_MIN_MESSAGES_PER_FILE = 12;
|
||||
const SESSION_INGESTION_MAX_TRACKED_MESSAGES_PER_SESSION = 4096;
|
||||
const SESSION_INGESTION_MAX_TRACKED_SCOPES = 2048;
|
||||
const SESSION_CHECKPOINT_TRANSCRIPT_FILENAME_RE = /\.checkpoint\..+\.jsonl$/i;
|
||||
const LIGHT_DIARY_HISTORY_LIMIT = 4;
|
||||
const LIGHT_DIARY_SNIPPET_SIMILARITY_THRESHOLD = 0.35;
|
||||
const GENERIC_DAY_HEADING_RE =
|
||||
/^(?:(?:mon|monday|tue|tues|tuesday|wed|wednesday|thu|thur|thurs|thursday|fri|friday|sat|saturday|sun|sunday)(?:,\s+)?)?(?:(?:jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|sept|september|oct|october|nov|november|dec|december)\s+\d{1,2}(?:st|nd|rd|th)?(?:,\s*\d{4})?|\d{1,2}[/-]\d{1,2}(?:[/-]\d{2,4})?|\d{4}[/-]\d{2}[/-]\d{2})$/i;
|
||||
const MANAGED_DAILY_DREAMING_BLOCKS = [
|
||||
@@ -1476,6 +1479,46 @@ function dedupeEntries(entries: ShortTermRecallEntry[], threshold: number): Shor
|
||||
return deduped;
|
||||
}
|
||||
|
||||
function normalizeDiaryCoverageText(text: string): string {
|
||||
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function isEntryCoveredByRecentDiary(
|
||||
entry: ShortTermRecallEntry,
|
||||
recentDiaryEntries: readonly string[],
|
||||
): boolean {
|
||||
const snippet = normalizeDiaryCoverageText(entry.snippet);
|
||||
if (!snippet) {
|
||||
return false;
|
||||
}
|
||||
return recentDiaryEntries.some((diaryEntry) => {
|
||||
const diaryText = normalizeDiaryCoverageText(diaryEntry);
|
||||
return (
|
||||
diaryText.includes(snippet) ||
|
||||
snippetSimilarity(entry.snippet, diaryEntry) >= LIGHT_DIARY_SNIPPET_SIMILARITY_THRESHOLD
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function prioritizeLightEntriesByDiaryCoverage(
|
||||
entries: ShortTermRecallEntry[],
|
||||
recentDiaryEntries: readonly string[],
|
||||
): ShortTermRecallEntry[] {
|
||||
if (recentDiaryEntries.length === 0) {
|
||||
return entries;
|
||||
}
|
||||
const fresh: ShortTermRecallEntry[] = [];
|
||||
const covered: ShortTermRecallEntry[] = [];
|
||||
for (const entry of entries) {
|
||||
if (isEntryCoveredByRecentDiary(entry, recentDiaryEntries)) {
|
||||
covered.push(entry);
|
||||
} else {
|
||||
fresh.push(entry);
|
||||
}
|
||||
}
|
||||
return [...fresh, ...covered];
|
||||
}
|
||||
|
||||
function buildLightDreamingBody(entries: ShortTermRecallEntry[]): string[] {
|
||||
if (entries.length === 0) {
|
||||
return ["- No notable updates."];
|
||||
@@ -1660,18 +1703,21 @@ async function runLightDreaming(params: {
|
||||
lookbackDays: params.config.lookbackDays,
|
||||
}),
|
||||
});
|
||||
const entries = dedupeEntries(
|
||||
recentEntries
|
||||
.toSorted((a, b) => {
|
||||
const byTime = Date.parse(b.lastRecalledAt) - Date.parse(a.lastRecalledAt);
|
||||
if (byTime !== 0) {
|
||||
return byTime;
|
||||
}
|
||||
return b.recallCount - a.recallCount;
|
||||
})
|
||||
.slice(0, params.config.limit),
|
||||
const rankedEntries = dedupeEntries(
|
||||
recentEntries.toSorted((a, b) => {
|
||||
const byTime = Date.parse(b.lastRecalledAt) - Date.parse(a.lastRecalledAt);
|
||||
if (byTime !== 0) {
|
||||
return byTime;
|
||||
}
|
||||
return b.recallCount - a.recallCount;
|
||||
}),
|
||||
params.config.dedupeSimilarity,
|
||||
);
|
||||
const recentDiaryEntries = await readRecentDreamDiaryEntries({
|
||||
workspaceDir: params.workspaceDir,
|
||||
limit: LIGHT_DIARY_HISTORY_LIMIT,
|
||||
});
|
||||
const entries = prioritizeLightEntriesByDiaryCoverage(rankedEntries, recentDiaryEntries);
|
||||
const capped = entries.slice(0, params.config.limit);
|
||||
const bodyLines = buildLightDreamingBody(capped);
|
||||
await writeDailyDreamingPhaseBlock({
|
||||
@@ -1699,7 +1745,9 @@ async function runLightDreaming(params: {
|
||||
const data: NarrativePhaseData = {
|
||||
phase: "light",
|
||||
snippets: capped.map((e) => e.snippet).filter(Boolean),
|
||||
currentDate: formatMemoryDreamingDay(nowMs, params.config.timezone),
|
||||
...(themes.length > 0 ? { themes } : {}),
|
||||
...(recentDiaryEntries.length > 0 ? { recentDiaryEntries } : {}),
|
||||
};
|
||||
if (params.detachNarratives) {
|
||||
runDetachedDreamNarrative({
|
||||
|
||||
Reference in New Issue
Block a user