Skip to content

Commit 4c1022c

Browse files
authored
feat(memory-core): add dreaming promotion with weighted recall thresholds (#60569)
* memory-core: add dreaming promotion flow with weighted thresholds * docs(memory): mark dreaming as experimental * memory-core: address dreaming promotion review feedback * memory-core: harden short-term promotion concurrency * acpx: make abort-process test timer-independent * memory-core: simplify dreaming config with mode presets * memory-core: add /dreaming command and tighten recall tracking * ui: add Dreams tab with sleeping lobster animation Adds a new Dreams tab to the gateway UI under the Agent group. The tab is gated behind the memory-core dreaming config — it only appears in the sidebar when dreaming.mode is not 'off'. Features: - Sleeping vector lobster with breathing animation - Floating Z's, twinkling starfield, moon glow - Rotating dream phrase bubble (17 whimsical phrases) - Memory stats bar (short-term, long-term, promoted) - Active/idle visual states - 14 unit tests * plugins: fix --json stdout pollution from hook runner log The hook runner initialization message was using log.info() which writes to stdout via console.log, breaking JSON.parse() in the Docker smoke test for 'openclaw plugins list --json'. Downgrade to log.debug() so it only appears when debugging is enabled. * ui: keep Dreams tab visible when dreaming is off * tests: fix contracts and stabilize extension shards * memory-core: harden dreaming recall persistence and locking * fix: stabilize dreaming PR gates (#60569) (thanks @vignesh07) * test: fix rebase drift in telegram and plugin guards
1 parent 2687a49 commit 4c1022c

34 files changed

Lines changed: 3843 additions & 45 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
2020
- Plugins/browser seams: split browser and WhatsApp plugin-sdk seams into narrower browser, approval-auth, and target-helper facades so hot paths and owner tests avoid broader runtime fan-out. (#60376) Thanks @shakkernerd.
2121
- Tests/runtime: trim local unit-test import/runtime fan-out across browser, WhatsApp, cron, task, and reply flows so owner suites start faster with lower shared-worker overhead while preserving the same focused behavior coverage. (#60249) Thanks @shakkernerd.
2222
- Tests/secrets runtime: restore split secrets suite cache and env isolation cleanup so broader runs do not leak stale plugin or provider snapshot state. (#60395) Thanks @shakkernerd.
23+
- Memory/dreaming (experimental): add opt-in weighted short-term recall promotion to `MEMORY.md`, managed dreaming modes (`off|core|rem|deep`), and a `/dreaming` command plus Dreams UI so durable memory promotion can run on background cadence without manual scheduling. (#60569) Thanks @vignesh07.
2324

2425
### Fixes
2526

docs/cli/memory.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
---
2-
summary: "CLI reference for `openclaw memory` (status/index/search)"
2+
summary: "CLI reference for `openclaw memory` (status/index/search/promote)"
33
read_when:
44
- You want to index or search semantic memory
55
- You’re debugging memory availability or indexing
6+
- You want to promote recalled short-term memory into `MEMORY.md`
67
title: "memory"
78
---
89

@@ -24,6 +25,9 @@ openclaw memory status --deep
2425
openclaw memory index --force
2526
openclaw memory search "meeting notes"
2627
openclaw memory search --query "deployment" --max-results 20
28+
openclaw memory promote --limit 10 --min-score 0.75
29+
openclaw memory promote --apply
30+
openclaw memory promote --json --min-recall-count 0 --min-unique-queries 0
2731
openclaw memory status --json
2832
openclaw memory status --deep --index
2933
openclaw memory status --deep --index --verbose
@@ -58,9 +62,60 @@ openclaw memory index --agent main --verbose
5862
- `--min-score <n>`: filter out low-score matches.
5963
- `--json`: print JSON results.
6064

65+
`memory promote`:
66+
67+
- Ranks short-term candidates from `memory/YYYY-MM-DD.md` using weighted recall signals (`frequency`, `relevance`, `query diversity`, `recency`).
68+
- Uses recall events captured when `memory_search` returns daily-memory hits.
69+
- Optional auto-dreaming mode: when `plugins.entries.memory-core.config.dreaming.mode` is `core`, `deep`, or `rem`, `memory-core` auto-manages a cron job that triggers promotion in the background (no manual `openclaw cron add` required).
70+
- `--agent <id>`: scope to a single agent (default: the default agent).
71+
- `--limit <n>`: max candidates to return/apply.
72+
- `--min-score <n>`: minimum weighted promotion score.
73+
- `--min-recall-count <n>`: minimum recall count required for a candidate.
74+
- `--min-unique-queries <n>`: minimum distinct query count required for a candidate.
75+
- `--apply`: append selected candidates into `MEMORY.md` and mark them promoted.
76+
- `--include-promoted`: include already promoted candidates in output.
77+
- `--json`: print JSON output.
78+
79+
## Dreaming (experimental)
80+
81+
Dreaming is the overnight reflection pass for memory. It is called "dreaming" because the system revisits what was recalled during the day and decides what is worth keeping long-term.
82+
83+
- It is opt-in and disabled by default.
84+
- Enable it with `plugins.entries.memory-core.config.dreaming.mode`.
85+
- You can toggle modes from chat with `/dreaming off|core|rem|deep`. Run `/dreaming` (or `/dreaming options`) to see what each mode does.
86+
- When enabled, `memory-core` automatically creates and maintains a managed cron job.
87+
- Set `dreaming.limit` to `0` if you want dreaming enabled but automatic promotion effectively paused.
88+
- Ranking uses weighted signals: recall frequency, retrieval relevance, query diversity, and temporal recency (recent recalls decay over time).
89+
- Promotion into `MEMORY.md` only happens when quality thresholds are met, so long-term memory stays high signal instead of collecting one-off details.
90+
91+
Default mode presets:
92+
93+
- `core`: daily at `0 3 * * *`, `minScore=0.75`, `minRecallCount=3`, `minUniqueQueries=2`
94+
- `deep`: every 12 hours (`0 */12 * * *`), `minScore=0.8`, `minRecallCount=3`, `minUniqueQueries=3`
95+
- `rem`: every 6 hours (`0 */6 * * *`), `minScore=0.85`, `minRecallCount=4`, `minUniqueQueries=3`
96+
97+
Example:
98+
99+
```json
100+
{
101+
"plugins": {
102+
"entries": {
103+
"memory-core": {
104+
"config": {
105+
"dreaming": {
106+
"mode": "core"
107+
}
108+
}
109+
}
110+
}
111+
}
112+
}
113+
```
114+
61115
Notes:
62116

63117
- `memory index --verbose` prints per-phase details (provider, model, sources, batch activity).
64118
- `memory status` includes any extra paths configured via `memorySearch.extraPaths`.
65119
- If effectively active memory remote API key fields are configured as SecretRefs, the command resolves those values from the active gateway snapshot. If gateway is unavailable, the command fails fast.
66120
- Gateway version skew note: this command path requires a gateway that supports `secrets.resolve`; older gateways return an unknown-method error.
121+
- Dreaming cadence defaults to each mode's preset schedule. Override cadence with `plugins.entries.memory-core.config.dreaming.frequency` as a cron expression (for example `0 3 * * *`) and fine-tune with `timezone`, `limit`, `minScore`, `minRecallCount`, and `minUniqueQueries`.

extensions/acpx/src/runtime-internals/process.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -403,9 +403,7 @@ describe("spawnAndCollect", () => {
403403
{ signal: controller.signal },
404404
);
405405

406-
setTimeout(() => {
407-
controller.abort();
408-
}, 10);
406+
controller.abort();
409407

410408
const result = await resultPromise;
411409
expect(result.error?.name).toBe("AbortError");

extensions/feishu/src/monitor.webhook.test-helpers.ts

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { vi } from "vitest";
44
import type { ClawdbotConfig } from "../runtime-api.js";
55
import type { monitorFeishuProvider } from "./monitor.js";
66

7+
const WEBHOOK_READY_MAX_ATTEMPTS = 200;
8+
const WEBHOOK_READY_RETRY_DELAY_MS = 50;
9+
const WEBHOOK_MONITOR_START_MAX_ATTEMPTS = 4;
10+
711
export async function getFreePort(): Promise<number> {
812
const server = createServer();
913
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
@@ -16,7 +20,7 @@ export async function getFreePort(): Promise<number> {
1620
}
1721

1822
async function waitUntilServerReady(url: string): Promise<void> {
19-
for (let i = 0; i < 50; i += 1) {
23+
for (let i = 0; i < WEBHOOK_READY_MAX_ATTEMPTS; i += 1) {
2024
try {
2125
const response = await fetch(url, { method: "GET" });
2226
if (response.status >= 200 && response.status < 500) {
@@ -25,7 +29,7 @@ async function waitUntilServerReady(url: string): Promise<void> {
2529
} catch {
2630
// retry
2731
}
28-
await new Promise((resolve) => setTimeout(resolve, 20));
32+
await new Promise((resolve) => setTimeout(resolve, WEBHOOK_READY_RETRY_DELAY_MS));
2933
}
3034
throw new Error(`server did not start: ${url}`);
3135
}
@@ -69,30 +73,44 @@ export async function withRunningWebhookMonitor(
6973
monitor: typeof monitorFeishuProvider,
7074
run: (url: string) => Promise<void>,
7175
) {
72-
const port = await getFreePort();
73-
const cfg = buildWebhookConfig({
74-
accountId: params.accountId,
75-
path: params.path,
76-
port,
77-
encryptKey: params.encryptKey,
78-
verificationToken: params.verificationToken,
79-
});
80-
81-
const abortController = new AbortController();
82-
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
83-
const monitorPromise = monitor({
84-
config: cfg,
85-
runtime,
86-
abortSignal: abortController.signal,
87-
});
76+
let startupError: unknown;
77+
for (let attempt = 1; attempt <= WEBHOOK_MONITOR_START_MAX_ATTEMPTS; attempt += 1) {
78+
const port = await getFreePort();
79+
const cfg = buildWebhookConfig({
80+
accountId: params.accountId,
81+
path: params.path,
82+
port,
83+
encryptKey: params.encryptKey,
84+
verificationToken: params.verificationToken,
85+
});
8886

89-
const url = `http://127.0.0.1:${port}${params.path}`;
90-
await waitUntilServerReady(url);
87+
const abortController = new AbortController();
88+
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
89+
const monitorPromise = monitor({
90+
config: cfg,
91+
runtime,
92+
abortSignal: abortController.signal,
93+
accountId: params.accountId,
94+
});
9195

92-
try {
93-
await run(url);
94-
} finally {
95-
abortController.abort();
96-
await monitorPromise;
96+
const url = `http://127.0.0.1:${port}${params.path}`;
97+
try {
98+
await waitUntilServerReady(url);
99+
try {
100+
await run(url);
101+
} finally {
102+
abortController.abort();
103+
await monitorPromise.catch(() => undefined);
104+
}
105+
return;
106+
} catch (error) {
107+
startupError = error;
108+
abortController.abort();
109+
await monitorPromise.catch(() => undefined);
110+
if (attempt < WEBHOOK_MONITOR_START_MAX_ATTEMPTS) {
111+
await new Promise((resolve) => setTimeout(resolve, attempt * WEBHOOK_READY_RETRY_DELAY_MS));
112+
}
113+
}
97114
}
115+
throw startupError instanceof Error ? startupError : new Error("failed to start webhook monitor");
98116
}

extensions/memory-core/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
22
import { registerMemoryCli } from "./src/cli.js";
3+
import { registerDreamingCommand } from "./src/dreaming-command.js";
4+
import { registerShortTermPromotionDreaming } from "./src/dreaming.js";
35
import {
46
buildMemoryFlushPlan,
57
DEFAULT_MEMORY_FLUSH_FORCE_TRANSCRIPT_BYTES,
@@ -25,6 +27,8 @@ export default definePluginEntry({
2527
kind: "memory",
2628
register(api) {
2729
registerBuiltInMemoryEmbeddingProviders(api);
30+
registerShortTermPromotionDreaming(api);
31+
registerDreamingCommand(api);
2832
api.registerMemoryPromptSection(buildPromptSection);
2933
api.registerMemoryFlushPlan(buildMemoryFlushPlan);
3034
api.registerMemoryRuntime(memoryRuntime);
Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,81 @@
11
{
22
"id": "memory-core",
33
"kind": "memory",
4+
"uiHints": {
5+
"dreaming.mode": {
6+
"label": "Dreaming Mode",
7+
"placeholder": "core",
8+
"help": "Select dreaming mode: off, core, deep, or rem."
9+
},
10+
"dreaming.frequency": {
11+
"label": "Dreaming Frequency",
12+
"placeholder": "0 3 * * *",
13+
"help": "Optional cron cadence override for managed dreaming runs."
14+
},
15+
"dreaming.timezone": {
16+
"label": "Dreaming Timezone",
17+
"placeholder": "America/Los_Angeles",
18+
"help": "IANA timezone for the managed dreaming cron schedule."
19+
},
20+
"dreaming.limit": {
21+
"label": "Promotion Limit",
22+
"placeholder": "10",
23+
"help": "Maximum short-term candidates promoted per dreaming run (set to 0 to skip promotions)."
24+
},
25+
"dreaming.minScore": {
26+
"label": "Promotion Min Score",
27+
"placeholder": "0.75",
28+
"help": "Minimum weighted rank required for automatic promotion."
29+
},
30+
"dreaming.minRecallCount": {
31+
"label": "Promotion Min Recalls",
32+
"placeholder": "3",
33+
"help": "Minimum recall count required for automatic promotion."
34+
},
35+
"dreaming.minUniqueQueries": {
36+
"label": "Promotion Min Queries",
37+
"placeholder": "2",
38+
"help": "Minimum unique query count required for automatic promotion."
39+
}
40+
},
441
"configSchema": {
542
"type": "object",
643
"additionalProperties": false,
7-
"properties": {}
44+
"properties": {
45+
"dreaming": {
46+
"type": "object",
47+
"additionalProperties": false,
48+
"properties": {
49+
"mode": {
50+
"type": "string",
51+
"enum": ["off", "core", "deep", "rem"]
52+
},
53+
"frequency": {
54+
"type": "string"
55+
},
56+
"timezone": {
57+
"type": "string"
58+
},
59+
"limit": {
60+
"type": "number",
61+
"minimum": 0
62+
},
63+
"minScore": {
64+
"type": "number",
65+
"minimum": 0,
66+
"maximum": 1
67+
},
68+
"minRecallCount": {
69+
"type": "number",
70+
"minimum": 0
71+
},
72+
"minUniqueQueries": {
73+
"type": "number",
74+
"minimum": 0
75+
}
76+
},
77+
"required": ["mode"]
78+
}
79+
}
880
}
981
}

0 commit comments

Comments
 (0)