diff --git a/CHANGELOG.md b/CHANGELOG.md index 30bfa6bd4d7..eb368f46356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Memory-core/dreaming: add a supported `dreaming.model` knob for Dream Diary narrative subagents, wired through phase config and the existing plugin subagent model-override trust gate. Refs #65963. Thanks @esqandil and @mjamiv. - Memory-core/dreaming: treat request-scoped narrative fallback as expected, skip session cleanup when no subagent run was created, and remove duplicate phase-level cleanup so fallback no longer emits warning noise. Fixes #67152. Thanks @jsompis. - Agents/exec: apply configured `tools.exec.timeoutSec` to background and `yieldMs` commands when no per-call timeout is set, preventing auto-backgrounded commands from running indefinitely. Fixes #67600; supersedes #67603. Thanks @dlmpx and @kagura-agent. - Config/doctor: stop masking unknown-key validation diagnostics such as `agents.defaults.llm`, and have `openclaw doctor --fix` remove the retired `agents.defaults.llm` timeout block. Thanks @aidiffuser. diff --git a/docs/concepts/dreaming.md b/docs/concepts/dreaming.md index 53fd5d5f516..341c6a001d8 100644 --- a/docs/concepts/dreaming.md +++ b/docs/concepts/dreaming.md @@ -72,7 +72,7 @@ Dreaming can ingest redacted session transcripts into the dreaming corpus. When ## Dream Diary -Dreaming also keeps a narrative **Dream Diary** in `DREAMS.md`. After each phase has enough material, `memory-core` runs a best-effort background subagent turn (using the default runtime model) and appends a short diary entry. +Dreaming also keeps a narrative **Dream Diary** in `DREAMS.md`. After each phase has enough material, `memory-core` runs a best-effort background subagent turn and appends a short diary entry. It uses the default runtime model unless `dreaming.model` is configured. This diary is for human reading in the Dreams UI, not a promotion source. Dreaming-generated diary/report artifacts are excluded from short-term promotion. Only grounded memory snippets are eligible to promote into `MEMORY.md`. @@ -112,9 +112,10 @@ When enabled, `memory-core` auto-manages one cron job for a full dreaming sweep. Default cadence behavior: -| Setting | Default | -| -------------------- | ----------- | -| `dreaming.frequency` | `0 3 * * *` | +| Setting | Default | +| -------------------- | ------------- | +| `dreaming.frequency` | `0 3 * * *` | +| `dreaming.model` | default model | ## Quick start @@ -210,6 +211,13 @@ All settings live under `plugins.entries.memory-core.config.dreaming`. Cron cadence for the full dreaming sweep. + + Optional Dream Diary subagent model override. Use a canonical `provider/model` value when also setting a subagent `allowedModels` allowlist. + + + +`dreaming.model` requires `plugins.entries.memory-core.subagent.allowModelOverride: true`. To restrict it, also set `plugins.entries.memory-core.subagent.allowedModels`. + Phase policy, thresholds, and storage behavior are internal implementation details (not user-facing config). See [Memory configuration reference](/reference/memory-config#dreaming) for the full key list. diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 8ebd9aad399..c4c3f996df5 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -182,6 +182,7 @@ See [MCP](/cli/mcp#openclaw-as-an-mcp-client-registry) and - `plugins.entries.memory-core.config.dreaming`: memory dreaming settings. See [Dreaming](/concepts/dreaming) for phases and thresholds. - `enabled`: master dreaming switch (default `false`). - `frequency`: cron cadence for each full dreaming sweep (`"0 3 * * *"` by default). + - `model`: optional Dream Diary subagent model override. Requires `plugins.entries.memory-core.subagent.allowModelOverride: true`; pair with `allowedModels` to restrict targets. - phase policy and thresholds are implementation details (not user-facing config keys). - Full memory config lives in [Memory configuration reference](/reference/memory-config): - `agents.defaults.memorySearch.*` diff --git a/docs/reference/memory-config.md b/docs/reference/memory-config.md index 6b865379692..65d96f9fd96 100644 --- a/docs/reference/memory-config.md +++ b/docs/reference/memory-config.md @@ -511,10 +511,11 @@ For conceptual behavior and slash commands, see [Dreaming](/concepts/dreaming). ### User settings -| Key | Type | Default | Description | -| ----------- | --------- | ----------- | ------------------------------------------------- | -| `enabled` | `boolean` | `false` | Enable or disable dreaming entirely | -| `frequency` | `string` | `0 3 * * *` | Optional cron cadence for the full dreaming sweep | +| Key | Type | Default | Description | +| ----------- | --------- | ------------- | ------------------------------------------------- | +| `enabled` | `boolean` | `false` | Enable or disable dreaming entirely | +| `frequency` | `string` | `0 3 * * *` | Optional cron cadence for the full dreaming sweep | +| `model` | `string` | default model | Optional Dream Diary subagent model override | ### Example @@ -523,10 +524,15 @@ For conceptual behavior and slash commands, see [Dreaming](/concepts/dreaming). plugins: { entries: { "memory-core": { + subagent: { + allowModelOverride: true, + allowedModels: ["anthropic/claude-sonnet-4-6"], + }, config: { dreaming: { enabled: true, frequency: "0 3 * * *", + model: "anthropic/claude-sonnet-4-6", }, }, }, @@ -538,6 +544,7 @@ For conceptual behavior and slash commands, see [Dreaming](/concepts/dreaming). - Dreaming writes machine state to `memory/.dreams/`. - Dreaming writes human-readable narrative output to `DREAMS.md` (or existing `dreams.md`). +- `dreaming.model` uses the existing plugin subagent trust gate; set `plugins.entries.memory-core.subagent.allowModelOverride: true` before enabling it. - The light/deep/REM phase policy and thresholds are internal behavior, not user-facing config. diff --git a/extensions/memory-core/openclaw.plugin.json b/extensions/memory-core/openclaw.plugin.json index ce345331a88..bd7b39f3ea0 100644 --- a/extensions/memory-core/openclaw.plugin.json +++ b/extensions/memory-core/openclaw.plugin.json @@ -16,6 +16,11 @@ "label": "Dreaming Frequency", "placeholder": "0 3 * * *", "help": "Optional cron cadence for the full dreaming sweep (light, REM, then deep)." + }, + "dreaming.model": { + "label": "Dreaming Model", + "placeholder": "anthropic/claude-sonnet-4-6", + "help": "Optional provider/model override for Dream Diary narrative subagent runs. Requires plugins.entries.memory-core.subagent.allowModelOverride." } }, "configSchema": { @@ -32,6 +37,9 @@ "frequency": { "type": "string" }, + "model": { + "type": "string" + }, "timezone": { "type": "string" }, @@ -51,6 +59,21 @@ } } }, + "execution": { + "type": "object", + "additionalProperties": false, + "properties": { + "defaults": { + "type": "object", + "additionalProperties": false, + "properties": { + "model": { + "type": "string" + } + } + } + } + }, "phases": { "type": "object", "additionalProperties": false, @@ -74,6 +97,15 @@ "type": "number", "minimum": 0, "maximum": 1 + }, + "execution": { + "type": "object", + "additionalProperties": false, + "properties": { + "model": { + "type": "string" + } + } } } }, @@ -108,6 +140,15 @@ "maxAgeDays": { "type": "integer", "minimum": 1 + }, + "execution": { + "type": "object", + "additionalProperties": false, + "properties": { + "model": { + "type": "string" + } + } } } }, @@ -130,6 +171,15 @@ "type": "number", "minimum": 0, "maximum": 1 + }, + "execution": { + "type": "object", + "additionalProperties": false, + "properties": { + "model": { + "type": "string" + } + } } } } diff --git a/extensions/memory-core/src/dreaming-narrative.test.ts b/extensions/memory-core/src/dreaming-narrative.test.ts index 02a5db7ec20..47cf87aa8de 100644 --- a/extensions/memory-core/src/dreaming-narrative.test.ts +++ b/extensions/memory-core/src/dreaming-narrative.test.ts @@ -596,6 +596,7 @@ describe("generateAndAppendDreamNarrative", () => { }, nowMs, timezone: "UTC", + model: "anthropic/claude-sonnet-4-6", logger, }); @@ -606,6 +607,7 @@ describe("generateAndAppendDreamNarrative", () => { lane: `dreaming-narrative:${expectedSessionKey}`, lightContext: true, deliver: false, + model: "anthropic/claude-sonnet-4-6", }); expect(subagent.waitForRun).toHaveBeenCalledOnce(); expect(subagent.deleteSession).toHaveBeenCalledOnce(); diff --git a/extensions/memory-core/src/dreaming-narrative.ts b/extensions/memory-core/src/dreaming-narrative.ts index 36d17b4da17..10b639fc309 100644 --- a/extensions/memory-core/src/dreaming-narrative.ts +++ b/extensions/memory-core/src/dreaming-narrative.ts @@ -26,6 +26,7 @@ type SubagentSurface = { idempotencyKey: string; sessionKey: string; message: string; + model?: string; extraSystemPrompt?: string; lane?: string; lightContext?: boolean; @@ -147,6 +148,7 @@ async function startNarrativeRunOrFallback(params: { workspaceDir: string; nowMs: number; timezone?: string; + model?: string; logger: Logger; }): Promise { try { @@ -154,6 +156,7 @@ async function startNarrativeRunOrFallback(params: { idempotencyKey: params.sessionKey, sessionKey: params.sessionKey, message: params.message, + ...(params.model ? { model: params.model } : {}), extraSystemPrompt: NARRATIVE_SYSTEM_PROMPT, lane: `dreaming-narrative:${params.sessionKey}`, lightContext: true, @@ -846,6 +849,7 @@ export async function generateAndAppendDreamNarrative(params: { data: NarrativePhaseData; nowMs?: number; timezone?: string; + model?: string; logger: Logger; }): Promise { const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now(); @@ -871,6 +875,7 @@ export async function generateAndAppendDreamNarrative(params: { workspaceDir: params.workspaceDir, nowMs, timezone: params.timezone, + model: params.model, logger: params.logger, }); if (!runId) { diff --git a/extensions/memory-core/src/dreaming-phases.test.ts b/extensions/memory-core/src/dreaming-phases.test.ts index 12faa007bbf..72c79b382a2 100644 --- a/extensions/memory-core/src/dreaming-phases.test.ts +++ b/extensions/memory-core/src/dreaming-phases.test.ts @@ -118,7 +118,7 @@ function createHarness( } function createMockNarrativeSubagent(response = "The archive hummed softly.") { - const run = vi.fn(async (_params: { sessionKey: string; message: string }) => ({ + const run = vi.fn(async (_params: { sessionKey: string; message: string; model?: string }) => ({ runId: "dream-run-1", })); const waitForRun = vi.fn(async () => ({ status: "ok" })); @@ -2321,7 +2321,33 @@ describe("memory-core dreaming phases", () => { it("passes staged light-dreaming snippets into the narrative pipeline", async () => { const workspaceDir = await createDreamingWorkspace(); const subagent = createMockNarrativeSubagent("The backup plan glowed like cold storage."); - const { beforeAgentReply } = createHarness(LIGHT_DREAMING_TEST_CONFIG, workspaceDir, subagent); + const { beforeAgentReply } = createHarness( + { + plugins: { + entries: { + "memory-core": { + config: { + dreaming: { + enabled: true, + timezone: "UTC", + model: "anthropic/claude-sonnet-4-6", + storage: { mode: "inline", separateReports: false }, + phases: { + light: { + enabled: true, + limit: 20, + lookbackDays: 2, + }, + }, + }, + }, + }, + }, + }, + }, + workspaceDir, + subagent, + ); await withDreamingTestClock(async () => { await writeDailyNote(workspaceDir, [ @@ -2338,6 +2364,7 @@ describe("memory-core dreaming phases", () => { const firstRun = subagent.run.mock.calls[0]?.[0]; expect(firstRun?.message).toContain("Move backups to S3 Glacier."); expect(firstRun?.message).toContain("Keep retention at 365 days."); + expect(firstRun?.model).toBe("anthropic/claude-sonnet-4-6"); await expect(fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8")).resolves.toContain( "The backup plan glowed like cold storage.", ); @@ -2354,12 +2381,20 @@ describe("memory-core dreaming phases", () => { config: { dreaming: { enabled: true, + execution: { + defaults: { + model: "openai/gpt-5.4", + }, + }, phases: { rem: { enabled: true, limit: 10, lookbackDays: 7, minPatternStrength: 0, + execution: { + model: "xai/grok-4.1-fast", + }, }, }, }, @@ -2392,6 +2427,7 @@ describe("memory-core dreaming phases", () => { const firstRun = subagent.run.mock.calls[0]?.[0]; expect(firstRun?.message).toContain("Move backups to S3 Glacier."); expect(firstRun?.message).toContain("Keep retention at 365 days."); + expect(firstRun?.model).toBe("xai/grok-4.1-fast"); await expect(fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8")).resolves.toContain( "The traces braided themselves into a map.", ); diff --git a/extensions/memory-core/src/dreaming-phases.ts b/extensions/memory-core/src/dreaming-phases.ts index 3215e559fb7..8ed7ced68c8 100644 --- a/extensions/memory-core/src/dreaming-phases.ts +++ b/extensions/memory-core/src/dreaming-phases.ts @@ -34,6 +34,7 @@ type DreamingHostConfig = unknown; type DreamingPhaseStorageConfig = { timezone?: string; storage: { mode: "inline" | "separate" | "both"; separateReports: boolean }; + execution?: { model?: string }; }; type LightDreamingConfig = DreamingPhaseStorageConfig & { enabled: boolean; @@ -1580,6 +1581,7 @@ async function runLightDreaming(params: { data, nowMs, timezone: params.config.timezone, + model: params.config.execution?.model, logger: params.logger, }).catch(() => undefined); }); @@ -1590,6 +1592,7 @@ async function runLightDreaming(params: { data, nowMs, timezone: params.config.timezone, + model: params.config.execution?.model, logger: params.logger, }); } @@ -1676,6 +1679,7 @@ async function runRemDreaming(params: { data, nowMs, timezone: params.config.timezone, + model: params.config.execution?.model, logger: params.logger, }).catch(() => undefined); }); @@ -1686,6 +1690,7 @@ async function runRemDreaming(params: { data, nowMs, timezone: params.config.timezone, + model: params.config.execution?.model, logger: params.logger, }); } diff --git a/extensions/memory-core/src/dreaming.test.ts b/extensions/memory-core/src/dreaming.test.ts index 2f23f081d4d..89ac9452884 100644 --- a/extensions/memory-core/src/dreaming.test.ts +++ b/extensions/memory-core/src/dreaming.test.ts @@ -220,6 +220,7 @@ describe("short-term dreaming config", () => { timezone: "UTC", verboseLogging: true, frequency: "5 1 * * *", + model: "anthropic/claude-haiku-4-5", phases: { deep: { limit: 7, @@ -248,6 +249,9 @@ describe("short-term dreaming config", () => { mode: "separate", separateReports: false, }, + execution: { + model: "anthropic/claude-haiku-4-5", + }, }); }); @@ -1880,7 +1884,7 @@ describe("short-term dreaming trigger", () => { }); const subagent = { - run: vi.fn(async () => ({ runId: "narrative-run-1" })), + run: vi.fn(async (_params: { model?: string }) => ({ runId: "narrative-run-1" })), waitForRun: vi.fn(async () => ({ status: "ok" })), getSessionMessages: vi.fn(async () => ({ messages: [{ role: "assistant", content: "A diary entry." }], @@ -1901,6 +1905,9 @@ describe("short-term dreaming trigger", () => { minUniqueQueries: 0, recencyHalfLifeDays: constants.DEFAULT_DREAMING_RECENCY_HALF_LIFE_DAYS, verboseLogging: false, + execution: { + model: "anthropic/claude-sonnet-4-6", + }, }, logger, subagent, @@ -1908,6 +1915,9 @@ describe("short-term dreaming trigger", () => { expect(result?.handled).toBe(true); expect(subagent.run).toHaveBeenCalled(); + expect(subagent.run.mock.calls[0]?.[0]).toMatchObject({ + model: "anthropic/claude-sonnet-4-6", + }); const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8"); expect(memoryText).toContain("Move backups to S3 Glacier."); await vi.waitFor(async () => { diff --git a/extensions/memory-core/src/dreaming.ts b/extensions/memory-core/src/dreaming.ts index 982cd64c86a..de1eb27cf74 100644 --- a/extensions/memory-core/src/dreaming.ts +++ b/extensions/memory-core/src/dreaming.ts @@ -116,6 +116,9 @@ export type ShortTermPromotionDreamingConfig = { mode: "inline" | "separate" | "both"; separateReports: boolean; }; + execution?: { + model?: string; + }; }; type ReconcileResult = @@ -391,6 +394,7 @@ export function resolveShortTermPromotionDreamingConfig(params: { ...(typeof resolved.maxAgeDays === "number" ? { maxAgeDays: resolved.maxAgeDays } : {}), verboseLogging: resolved.verboseLogging, storage: resolved.storage, + ...(resolved.execution.model ? { execution: { model: resolved.execution.model } } : {}), }; } @@ -633,6 +637,7 @@ export async function runShortTermDreamingPromotionIfTriggered(params: { data, nowMs: sweepNowMs, timezone: params.config.timezone, + model: params.config.execution?.model, logger: params.logger, }).catch(() => undefined); }); @@ -643,6 +648,7 @@ export async function runShortTermDreamingPromotionIfTriggered(params: { data, nowMs: sweepNowMs, timezone: params.config.timezone, + model: params.config.execution?.model, logger: params.logger, }); } diff --git a/src/memory-host-sdk/dreaming.test.ts b/src/memory-host-sdk/dreaming.test.ts index 37410e96885..d00f97fbde3 100644 --- a/src/memory-host-sdk/dreaming.test.ts +++ b/src/memory-host-sdk/dreaming.test.ts @@ -28,6 +28,7 @@ describe("memory dreaming host helpers", () => { enabled: true, frequency: "0 */4 * * *", timezone: "Europe/London", + model: " anthropic/claude-sonnet-4-6 ", storage: { mode: "both", separateReports: true, @@ -49,6 +50,10 @@ describe("memory dreaming host helpers", () => { expect(resolved.enabled).toBe(true); expect(resolved.frequency).toBe("0 */4 * * *"); expect(resolved.timezone).toBe("Europe/London"); + expect(resolved.execution.defaults.model).toBe("anthropic/claude-sonnet-4-6"); + expect(resolved.phases.light.execution.model).toBe("anthropic/claude-sonnet-4-6"); + expect(resolved.phases.deep.execution.model).toBe("anthropic/claude-sonnet-4-6"); + expect(resolved.phases.rem.execution.model).toBe("anthropic/claude-sonnet-4-6"); expect(resolved.storage).toEqual({ mode: "both", separateReports: true, @@ -64,6 +69,33 @@ describe("memory dreaming host helpers", () => { }); }); + it("lets execution defaults and phase execution override the top-level dreaming model", () => { + const resolved = resolveMemoryDreamingConfig({ + pluginConfig: { + dreaming: { + model: "anthropic/claude-haiku-4-5", + execution: { + defaults: { + model: "openai/gpt-5.4", + }, + }, + phases: { + rem: { + execution: { + model: "xai/grok-4.1-fast", + }, + }, + }, + }, + }, + }); + + expect(resolved.execution.defaults.model).toBe("openai/gpt-5.4"); + expect(resolved.phases.light.execution.model).toBe("openai/gpt-5.4"); + expect(resolved.phases.deep.execution.model).toBe("openai/gpt-5.4"); + expect(resolved.phases.rem.execution.model).toBe("xai/grok-4.1-fast"); + }); + it("falls back to cfg timezone and deep defaults", () => { const cfg = { agents: { diff --git a/src/memory-host-sdk/dreaming.ts b/src/memory-host-sdk/dreaming.ts index aea9e95f783..fb09b941c25 100644 --- a/src/memory-host-sdk/dreaming.ts +++ b/src/memory-host-sdk/dreaming.ts @@ -291,14 +291,13 @@ function resolveExecutionConfig( typeof temperatureRaw === "number" && Number.isFinite(temperatureRaw) && temperatureRaw >= 0 ? Math.min(2, temperatureRaw) : undefined; + const model = normalizeTrimmedString(record?.model) ?? fallback.model; return { speed: normalizeSpeed(record?.speed) ?? fallback.speed, thinking: normalizeThinking(record?.thinking) ?? fallback.thinking, budget: normalizeBudget(record?.budget) ?? fallback.budget, - ...(normalizeTrimmedString(record?.model) - ? { model: normalizeTrimmedString(record?.model) } - : {}), + ...(model ? { model } : {}), ...(typeof maxOutputTokens === "number" ? { maxOutputTokens } : {}), ...(typeof temperature === "number" ? { temperature } : {}), ...(typeof timeoutMs === "number" ? { timeoutMs } : {}), @@ -359,11 +358,13 @@ export function resolveMemoryDreamingConfig(params: { const storage = asNullableRecord(dreaming?.storage); const execution = asNullableRecord(dreaming?.execution); const phases = asNullableRecord(dreaming?.phases); + const topLevelModel = normalizeTrimmedString(dreaming?.model); const defaultExecution = resolveExecutionConfig(execution?.defaults, { speed: DEFAULT_MEMORY_DREAMING_SPEED, thinking: DEFAULT_MEMORY_DREAMING_THINKING, budget: DEFAULT_MEMORY_DREAMING_BUDGET, + ...(topLevelModel ? { model: topLevelModel } : {}), }); const light = asNullableRecord(phases?.light);