Dreaming: simplify sweep flow and add diary surface

This commit is contained in:
Vignesh Natarajan
2026-04-05 17:16:49 -07:00
parent 02f2a66dff
commit 61e61ccc18
44 changed files with 4375 additions and 1470 deletions

View File

@@ -34,6 +34,7 @@ describe("memory dreaming host helpers", () => {
pluginConfig: {
dreaming: {
enabled: true,
frequency: "0 */4 * * *",
timezone: "Europe/London",
storage: {
mode: "both",
@@ -41,7 +42,6 @@ describe("memory dreaming host helpers", () => {
},
phases: {
deep: {
cron: "0 */4 * * *",
limit: "5",
minScore: "0.9",
minRecallCount: "4",
@@ -54,8 +54,8 @@ describe("memory dreaming host helpers", () => {
},
});
expect(resolved.mode).toBe("core");
expect(resolved.enabled).toBe(true);
expect(resolved.frequency).toBe("0 3 * * *");
expect(resolved.timezone).toBe("Europe/London");
expect(resolved.storage).toEqual({
mode: "both",
@@ -86,18 +86,34 @@ describe("memory dreaming host helpers", () => {
cfg,
});
expect(resolved.mode).toBe("off");
expect(resolved.enabled).toBe(false);
expect(resolved.frequency).toBe("0 3 * * *");
expect(resolved.timezone).toBe("America/Los_Angeles");
expect(resolved.phases.deep).toMatchObject({
cron: "0 3 * * *",
limit: 10,
minScore: 0.75,
minScore: 0.8,
recencyHalfLifeDays: 14,
maxAgeDays: 30,
});
});
it("applies top-level dreaming frequency across all phases", () => {
const resolved = resolveMemoryDreamingConfig({
pluginConfig: {
dreaming: {
enabled: true,
frequency: "15 */8 * * *",
},
},
});
expect(resolved.frequency).toBe("15 */8 * * *");
expect(resolved.phases.light.cron).toBe("15 */8 * * *");
expect(resolved.phases.deep.cron).toBe("15 */8 * * *");
expect(resolved.phases.rem.cron).toBe("15 */8 * * *");
});
it("dedupes shared workspaces and skips agents without memory search", () => {
resolveMemorySearchConfig.mockImplementation((_cfg: OpenClawConfig, agentId: string) =>
agentId === "beta" ? null : { enabled: true },

View File

@@ -8,8 +8,7 @@ export const DEFAULT_MEMORY_DREAMING_TIMEZONE = undefined;
export const DEFAULT_MEMORY_DREAMING_VERBOSE_LOGGING = false;
export const DEFAULT_MEMORY_DREAMING_STORAGE_MODE = "inline";
export const DEFAULT_MEMORY_DREAMING_SEPARATE_REPORTS = false;
export const DEFAULT_MEMORY_DREAMING_MODE = "off";
export const DEFAULT_MEMORY_DREAMING_PRESET = "core";
export const DEFAULT_MEMORY_DREAMING_FREQUENCY = "0 3 * * *";
export const DEFAULT_MEMORY_LIGHT_DREAMING_CRON_EXPR = "0 */6 * * *";
export const DEFAULT_MEMORY_LIGHT_DREAMING_LOOKBACK_DAYS = 2;
@@ -18,9 +17,9 @@ export const DEFAULT_MEMORY_LIGHT_DREAMING_DEDUPE_SIMILARITY = 0.9;
export const DEFAULT_MEMORY_DEEP_DREAMING_CRON_EXPR = "0 3 * * *";
export const DEFAULT_MEMORY_DEEP_DREAMING_LIMIT = 10;
export const DEFAULT_MEMORY_DEEP_DREAMING_MIN_SCORE = 0.75;
export const DEFAULT_MEMORY_DEEP_DREAMING_MIN_SCORE = 0.8;
export const DEFAULT_MEMORY_DEEP_DREAMING_MIN_RECALL_COUNT = 3;
export const DEFAULT_MEMORY_DEEP_DREAMING_MIN_UNIQUE_QUERIES = 2;
export const DEFAULT_MEMORY_DEEP_DREAMING_MIN_UNIQUE_QUERIES = 3;
export const DEFAULT_MEMORY_DEEP_DREAMING_RECENCY_HALF_LIFE_DAYS = 14;
export const DEFAULT_MEMORY_DEEP_DREAMING_MAX_AGE_DAYS = 30;
@@ -44,8 +43,6 @@ export type MemoryDreamingSpeed = "fast" | "balanced" | "slow";
export type MemoryDreamingThinking = "low" | "medium" | "high";
export type MemoryDreamingBudget = "cheap" | "medium" | "expensive";
export type MemoryDreamingStorageMode = "inline" | "separate" | "both";
export type MemoryDreamingPreset = "core" | "deep" | "rem";
export type MemoryDreamingMode = MemoryDreamingPreset | "off";
export type MemoryLightDreamingSource = "daily" | "sessions" | "recall";
export type MemoryDeepDreamingSource = "daily" | "memory" | "sessions" | "logs" | "recall";
@@ -112,8 +109,8 @@ export type MemoryRemDreamingConfig = {
export type MemoryDreamingPhaseName = "light" | "deep" | "rem";
export type MemoryDreamingConfig = {
mode: MemoryDreamingMode;
enabled: boolean;
frequency: string;
timezone?: string;
verboseLogging: boolean;
storage: MemoryDreamingStorageConfig;
@@ -146,47 +143,6 @@ const DEFAULT_MEMORY_DEEP_DREAMING_SOURCES: MemoryDeepDreamingSource[] = [
];
const DEFAULT_MEMORY_REM_DREAMING_SOURCES: MemoryRemDreamingSource[] = ["memory", "daily", "deep"];
const MEMORY_DREAMING_PRESET_DEFAULTS: Record<
MemoryDreamingPreset,
{
cron: string;
limit: number;
minScore: number;
minRecallCount: number;
minUniqueQueries: number;
recencyHalfLifeDays: number;
maxAgeDays: number;
}
> = {
core: {
cron: DEFAULT_MEMORY_DEEP_DREAMING_CRON_EXPR,
limit: DEFAULT_MEMORY_DEEP_DREAMING_LIMIT,
minScore: DEFAULT_MEMORY_DEEP_DREAMING_MIN_SCORE,
minRecallCount: DEFAULT_MEMORY_DEEP_DREAMING_MIN_RECALL_COUNT,
minUniqueQueries: DEFAULT_MEMORY_DEEP_DREAMING_MIN_UNIQUE_QUERIES,
recencyHalfLifeDays: DEFAULT_MEMORY_DEEP_DREAMING_RECENCY_HALF_LIFE_DAYS,
maxAgeDays: DEFAULT_MEMORY_DEEP_DREAMING_MAX_AGE_DAYS,
},
deep: {
cron: "0 */12 * * *",
limit: DEFAULT_MEMORY_DEEP_DREAMING_LIMIT,
minScore: 0.8,
minRecallCount: 3,
minUniqueQueries: 3,
recencyHalfLifeDays: DEFAULT_MEMORY_DEEP_DREAMING_RECENCY_HALF_LIFE_DAYS,
maxAgeDays: DEFAULT_MEMORY_DEEP_DREAMING_MAX_AGE_DAYS,
},
rem: {
cron: "0 */6 * * *",
limit: DEFAULT_MEMORY_DEEP_DREAMING_LIMIT,
minScore: 0.85,
minRecallCount: 4,
minUniqueQueries: 3,
recencyHalfLifeDays: DEFAULT_MEMORY_DEEP_DREAMING_RECENCY_HALF_LIFE_DAYS,
maxAgeDays: DEFAULT_MEMORY_DEEP_DREAMING_MAX_AGE_DAYS,
},
};
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
@@ -288,23 +244,6 @@ function normalizeStringArray<T extends string>(
return normalized.length > 0 ? normalized : [...fallback];
}
function parseMemoryDreamingMode(value: unknown): MemoryDreamingMode | undefined {
const normalized = normalizeTrimmedString(value)?.toLowerCase();
if (
normalized === "off" ||
normalized === "core" ||
normalized === "deep" ||
normalized === "rem"
) {
return normalized;
}
return undefined;
}
export function normalizeMemoryDreamingMode(value: unknown): MemoryDreamingMode {
return parseMemoryDreamingMode(value) ?? DEFAULT_MEMORY_DREAMING_MODE;
}
function normalizeStorageMode(value: unknown): MemoryDreamingStorageMode {
const normalized = normalizeTrimmedString(value)?.toLowerCase();
if (normalized === "inline" || normalized === "separate" || normalized === "both") {
@@ -391,13 +330,8 @@ export function resolveMemoryDreamingConfig(params: {
cfg?: OpenClawConfig;
}): MemoryDreamingConfig {
const dreaming = asRecord(params.pluginConfig?.dreaming);
const explicitMode = parseMemoryDreamingMode(dreaming?.mode);
const legacyEnabled = normalizeBoolean(dreaming?.enabled, DEFAULT_MEMORY_DREAMING_ENABLED);
const mode: MemoryDreamingMode =
explicitMode ?? (legacyEnabled ? DEFAULT_MEMORY_DREAMING_PRESET : DEFAULT_MEMORY_DREAMING_MODE);
const enabled = mode !== "off";
const preset: MemoryDreamingPreset = mode === "off" ? DEFAULT_MEMORY_DREAMING_PRESET : mode;
const presetDefaults = MEMORY_DREAMING_PRESET_DEFAULTS[preset];
const frequency =
normalizeTrimmedString(dreaming?.frequency) ?? DEFAULT_MEMORY_DREAMING_FREQUENCY;
const timezone =
normalizeTrimmedString(dreaming?.timezone) ??
normalizeTrimmedString(params.cfg?.agents?.defaults?.userTimezone) ??
@@ -405,7 +339,6 @@ export function resolveMemoryDreamingConfig(params: {
const storage = asRecord(dreaming?.storage);
const execution = asRecord(dreaming?.execution);
const phases = asRecord(dreaming?.phases);
const frequencyAlias = normalizeTrimmedString(dreaming?.frequency);
const defaultExecution = resolveExecutionConfig(execution?.defaults, {
speed: DEFAULT_MEMORY_DREAMING_SPEED,
@@ -417,39 +350,11 @@ export function resolveMemoryDreamingConfig(params: {
const deep = asRecord(phases?.deep);
const rem = asRecord(phases?.rem);
const deepRecovery = asRecord(deep?.recovery);
const maxAgeDays =
normalizeOptionalPositiveInt(dreaming?.maxAgeDays) ??
normalizeOptionalPositiveInt(deep?.maxAgeDays) ??
presetDefaults.maxAgeDays;
const deepCron =
normalizeTrimmedString(dreaming?.cron) ??
frequencyAlias ??
normalizeTrimmedString(deep?.cron) ??
presetDefaults.cron;
const deepLimit = normalizeNonNegativeInt(
dreaming?.limit,
normalizeNonNegativeInt(deep?.limit, presetDefaults.limit),
);
const deepMinScore = normalizeScore(
dreaming?.minScore,
normalizeScore(deep?.minScore, presetDefaults.minScore),
);
const deepMinRecallCount = normalizeNonNegativeInt(
dreaming?.minRecallCount,
normalizeNonNegativeInt(deep?.minRecallCount, presetDefaults.minRecallCount),
);
const deepMinUniqueQueries = normalizeNonNegativeInt(
dreaming?.minUniqueQueries,
normalizeNonNegativeInt(deep?.minUniqueQueries, presetDefaults.minUniqueQueries),
);
const deepRecencyHalfLifeDays = normalizeNonNegativeInt(
dreaming?.recencyHalfLifeDays,
normalizeNonNegativeInt(deep?.recencyHalfLifeDays, presetDefaults.recencyHalfLifeDays),
);
const maxAgeDays = normalizeOptionalPositiveInt(deep?.maxAgeDays);
return {
mode,
enabled,
enabled: normalizeBoolean(dreaming?.enabled, DEFAULT_MEMORY_DREAMING_ENABLED),
frequency,
...(timezone ? { timezone } : {}),
verboseLogging: normalizeBoolean(
dreaming?.verboseLogging,
@@ -467,10 +372,8 @@ export function resolveMemoryDreamingConfig(params: {
},
phases: {
light: {
// Phased dreaming was experimental and too noisy. Keep light sleep off
// unless we intentionally bring it back behind a separate effort.
enabled: false,
cron: normalizeTrimmedString(light?.cron) ?? DEFAULT_MEMORY_LIGHT_DREAMING_CRON_EXPR,
enabled: normalizeBoolean(light?.enabled, true),
cron: frequency,
lookbackDays: normalizeNonNegativeInt(
light?.lookbackDays,
DEFAULT_MEMORY_LIGHT_DREAMING_LOOKBACK_DAYS,
@@ -493,14 +396,27 @@ export function resolveMemoryDreamingConfig(params: {
}),
},
deep: {
enabled,
cron: deepCron,
limit: deepLimit,
minScore: deepMinScore,
minRecallCount: deepMinRecallCount,
minUniqueQueries: deepMinUniqueQueries,
recencyHalfLifeDays: deepRecencyHalfLifeDays,
...(typeof maxAgeDays === "number" ? { maxAgeDays } : {}),
enabled: normalizeBoolean(deep?.enabled, true),
cron: frequency,
limit: normalizeNonNegativeInt(deep?.limit, DEFAULT_MEMORY_DEEP_DREAMING_LIMIT),
minScore: normalizeScore(deep?.minScore, DEFAULT_MEMORY_DEEP_DREAMING_MIN_SCORE),
minRecallCount: normalizeNonNegativeInt(
deep?.minRecallCount,
DEFAULT_MEMORY_DEEP_DREAMING_MIN_RECALL_COUNT,
),
minUniqueQueries: normalizeNonNegativeInt(
deep?.minUniqueQueries,
DEFAULT_MEMORY_DEEP_DREAMING_MIN_UNIQUE_QUERIES,
),
recencyHalfLifeDays: normalizeNonNegativeInt(
deep?.recencyHalfLifeDays,
DEFAULT_MEMORY_DEEP_DREAMING_RECENCY_HALF_LIFE_DAYS,
),
...(typeof maxAgeDays === "number"
? { maxAgeDays }
: typeof DEFAULT_MEMORY_DEEP_DREAMING_MAX_AGE_DAYS === "number"
? { maxAgeDays: DEFAULT_MEMORY_DEEP_DREAMING_MAX_AGE_DAYS }
: {}),
sources: normalizeStringArray(
deep?.sources,
["daily", "memory", "sessions", "logs", "recall"] as const,
@@ -540,10 +456,8 @@ export function resolveMemoryDreamingConfig(params: {
}),
},
rem: {
// REM reflections are currently disabled by default to keep dreaming
// predictable and focused on durable promotion modes.
enabled: false,
cron: normalizeTrimmedString(rem?.cron) ?? DEFAULT_MEMORY_REM_DREAMING_CRON_EXPR,
enabled: normalizeBoolean(rem?.enabled, true),
cron: frequency,
lookbackDays: normalizeNonNegativeInt(
rem?.lookbackDays,
DEFAULT_MEMORY_REM_DREAMING_LOOKBACK_DAYS,