Fix dreaming replay, repair polluted artifacts, and gate wiki tabs#65138
Conversation
🔒 Aisle Security AnalysisWe found 3 potential security issue(s) in this PR:
1. 🟠 Symlink traversal in dreaming artifact repair archive destination allows arbitrary file move
Description
If an attacker can write within Vulnerable flow:
Vulnerable code: await fs.mkdir(params.archiveDir, { recursive: true });
const destination = path.join(params.archiveDir, `${baseName}.${randomUUID()}`);
await fs.rename(params.targetPath, destination);RecommendationHarden archive destination creation against symlink traversal:
Example approach: async function assertNoSymlink(p: string) {
const st = await fs.lstat(p);
if (st.isSymbolicLink()) throw new Error(`Refusing symlink path component: ${p}`);
}
const workspaceReal = await fs.realpath(workspaceDir);
const repairRoot = path.join(workspaceDir, ".openclaw-repair");
// Ensure existing components are not symlinks
for (const part of [repairRoot, path.join(repairRoot, "dreaming")]) {
try { await assertNoSymlink(part); } catch (e: any) {
if (e?.code !== "ENOENT") throw e;
}
}
await fs.mkdir(path.join(repairRoot, "dreaming"), { recursive: true });
const archiveDir = path.join(repairRoot, "dreaming", buildArchiveTimestamp(new Date()));
await fs.mkdir(archiveDir, { recursive: false });
const archiveRealParent = await fs.realpath(path.dirname(archiveDir));
if (!archiveRealParent.startsWith(workspaceReal + path.sep)) {
throw new Error("Archive directory escapes workspace");
}Additionally consider creating the archive directory with 2. 🟡 Arbitrary file write via unvalidated workspaceDir in dreaming narrative file updates
DescriptionThe DREAMS diary update helpers write to a path derived from
If any consumer can call these exported APIs with attacker-controlled RecommendationValidate and normalize At minimum, require an absolute path and function requireAbsoluteWorkspaceDir(raw: string): string {
const trimmed = raw.trim();
if (!trimmed) throw new Error("workspaceDir is required");
if (!path.isAbsolute(trimmed)) throw new Error("workspaceDir must be an absolute path");
return path.resolve(trimmed);
}
async function updateDreamsFile<T>(params: { workspaceDir: string; ... }): Promise<T> {
const workspaceDir = requireAbsoluteWorkspaceDir(params.workspaceDir);
const dreamsPath = await resolveDreamsPath(workspaceDir);
...
}If there is a known allowed workspace root, additionally enforce containment (e.g., 3. 🔵 TOCTOU race in dreaming artifact archiver allows swapping path between lstat() and rename()
DescriptionThe dreaming repair archiver checks a path with
Vulnerable code: const kind = await ensureArchivablePath(params.targetPath);
...
await fs.rename(params.targetPath, destination);RecommendationAvoid path-based check-then-use flows for filesystem operations on attacker-modifiable directories. Mitigations (pick what is feasible in your environment):
const st1 = await fs.lstat(targetPath);
if (st1.isSymbolicLink()) throw new Error("Refusing symlink");
// ... compute destination ...
const st2 = await fs.lstat(targetPath);
if (st2.ino !== st1.ino || st2.dev !== st1.dev) {
throw new Error("Artifact changed during archival; aborting");
}
await fs.rename(targetPath, destination);
Also consider documenting that the workspace must not be writable by untrusted users/processes during repair operations. Analyzed PR: #65138 at commit Last updated on: 2026-04-12T05:09:47Z |
Greptile SummaryThis PR addresses three related dreaming subsystem issues: (1) heartbeat-triggered dreaming system events were peeked but never consumed, causing a single dreaming run to replay every 30 minutes — fixed by calling One minor write-efficiency concern in Confidence Score: 5/5Safe to merge; all remaining findings are P2 style suggestions that do not affect correctness. The heartbeat drain fix is correct and well-guarded. Self-ingestion prevention is logically sound. Repair/dedupe flows follow existing patterns. The only issue flagged (unnecessary rewrite in dedupe) causes a harmless mtime update with identical content — not a data integrity problem. extensions/memory-core/src/dreaming-narrative.ts — write condition in Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/memory-core/src/dreaming-narrative.ts
Line: 507-512
Comment:
**Unnecessary rewrite when no duplicates found**
The condition `removed > 0 || existing.length > 0` causes `writeDreamsFileAtomic` to run even when zero duplicates were removed, as long as the diary has any content. The roundtrip through `splitDiaryBlocks` → `joinDiaryBlocks` is lossless for well-formed diaries, so the byte content is preserved, but the file's mtime changes needlessly on every no-op `dedupeDiary` call. This can trigger spurious cache invalidations in downstream ingestion trackers that key on mtime. The guard should be `removed > 0`.
```suggestion
if (removed > 0) {
await fs.mkdir(path.dirname(dreamsPath), { recursive: true });
await writeDreamsFileAtomic(
dreamsPath,
replaceDiaryContent(ensured, joinDiaryBlocks(keptBlocks)),
);
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Fix dreaming replay and recovery flows" | Re-trigger Greptile |
|
Found 1 test failure on Blacksmith runners: Failure
|
|
Merged current Also fixed the review-driven follow-ups that landed after the original dreaming changes:
The merge from
The visible Greptile/Aisle comments are stale in a few places relative to the current head. Latest branch head is |
…penclaw#65138) * fix(active-memory): preserve parent channel context for recall runs * fix(active-memory): keep recall runs on the resolved channel * fix(active-memory): prefer resolved recall channel over wrapper hints * fix(active-memory): trust explicit recall channel hints * fix(active-memory): rank recall channel fallbacks by trust * Fix dreaming replay and recovery flows * fix: prevent dreaming event loss and diary write races * chore: add changelog entry for memory fixes * fix: harden dreaming repair and diary writes * fix: harden dreaming artifact archive naming
…penclaw#65138) * fix(active-memory): preserve parent channel context for recall runs * fix(active-memory): keep recall runs on the resolved channel * fix(active-memory): prefer resolved recall channel over wrapper hints * fix(active-memory): trust explicit recall channel hints * fix(active-memory): rank recall channel fallbacks by trust * Fix dreaming replay and recovery flows * fix: prevent dreaming event loss and diary write races * chore: add changelog entry for memory fixes * fix: harden dreaming repair and diary writes * fix: harden dreaming artifact archive naming
|
Related work from PRtags group Title: Open PR candidate: Dream Diary duplicate/raw fallback cleanup
|
…penclaw#65138) * fix(active-memory): preserve parent channel context for recall runs * fix(active-memory): keep recall runs on the resolved channel * fix(active-memory): prefer resolved recall channel over wrapper hints * fix(active-memory): trust explicit recall channel hints * fix(active-memory): rank recall channel fallbacks by trust * Fix dreaming replay and recovery flows * fix: prevent dreaming event loss and diary write races * chore: add changelog entry for memory fixes * fix: harden dreaming repair and diary writes * fix: harden dreaming artifact archive naming
…penclaw#65138) * fix(active-memory): preserve parent channel context for recall runs * fix(active-memory): keep recall runs on the resolved channel * fix(active-memory): prefer resolved recall channel over wrapper hints * fix(active-memory): trust explicit recall channel hints * fix(active-memory): rank recall channel fallbacks by trust * Fix dreaming replay and recovery flows * fix: prevent dreaming event loss and diary write races * chore: add changelog entry for memory fixes * fix: harden dreaming repair and diary writes * fix: harden dreaming artifact archive naming
…penclaw#65138) * fix(active-memory): preserve parent channel context for recall runs * fix(active-memory): keep recall runs on the resolved channel * fix(active-memory): prefer resolved recall channel over wrapper hints * fix(active-memory): trust explicit recall channel hints * fix(active-memory): rank recall channel fallbacks by trust * Fix dreaming replay and recovery flows * fix: prevent dreaming event loss and diary write races * chore: add changelog entry for memory fixes * fix: harden dreaming repair and diary writes * fix: harden dreaming artifact archive naming
…penclaw#65138) * fix(active-memory): preserve parent channel context for recall runs * fix(active-memory): keep recall runs on the resolved channel * fix(active-memory): prefer resolved recall channel over wrapper hints * fix(active-memory): trust explicit recall channel hints * fix(active-memory): rank recall channel fallbacks by trust * Fix dreaming replay and recovery flows * fix: prevent dreaming event loss and diary write races * chore: add changelog entry for memory fixes * fix: harden dreaming repair and diary writes * fix: harden dreaming artifact archive naming
…penclaw#65138) * fix(active-memory): preserve parent channel context for recall runs * fix(active-memory): keep recall runs on the resolved channel * fix(active-memory): prefer resolved recall channel over wrapper hints * fix(active-memory): trust explicit recall channel hints * fix(active-memory): rank recall channel fallbacks by trust * Fix dreaming replay and recovery flows * fix: prevent dreaming event loss and diary write races * chore: add changelog entry for memory fixes * fix: harden dreaming repair and diary writes * fix: harden dreaming artifact archive naming
…penclaw#65138) * fix(active-memory): preserve parent channel context for recall runs * fix(active-memory): keep recall runs on the resolved channel * fix(active-memory): prefer resolved recall channel over wrapper hints * fix(active-memory): trust explicit recall channel hints * fix(active-memory): rank recall channel fallbacks by trust * Fix dreaming replay and recovery flows * fix: prevent dreaming event loss and diary write races * chore: add changelog entry for memory fixes * fix: harden dreaming repair and diary writes * fix: harden dreaming artifact archive naming
What This PR Does
This PR fixes two user-visible memory problems and wires the recovery flow through the product surfaces that already expose dreaming state.
1. Active memory now keeps recall on the right channel
The branch fixes channel selection for active-memory recall runs so recall follows the resolved session/channel context instead of drifting to a weaker wrapper/provider hint.
Before this change, the recall path could flatten these inputs incorrectly:
Now the selection is ordered by trust and keeps parent-session context when it exists.
flowchart TD A["Explicit channel hint"] --> B S["Session store: lastChannel/channel"] --> B O["Session origin provider"] --> B W["Wrapper/provider fallback"] --> B B["Resolve recall run channel context"] --> C["Recall runs on the resolved channel"]In practice, this means recall jobs stay attached to the channel the conversation actually belongs to instead of hopping to a weaker inferred provider value.
2. Managed dreaming no longer replays the same event and spiral-loops on itself
The branch fixes the main dreaming failure mode that produced the half-hourly spam.
There were two separate problems:
Before:
flowchart LR A["Managed dreaming event queued once"] --> B["Heartbeat inspects event"] B --> C["Dreaming run writes diary + narrative transcript"] C --> D["Event not fully consumed or later replayed"] C --> E["Narrative transcript re-enters dream corpus"] D --> B E --> CAfter:
flowchart LR A["Managed dreaming event queued once"] --> B["Heartbeat inspects exact queued entries"] B --> C["Successful dreaming run"] C --> D["Consume only inspected entries"] C --> E["Skip dreaming-generated transcripts during corpus ingestion"] D --> F["Later events remain queued for later heartbeats"] E --> G["No self-ingestion loop"]Concretely, the runtime changes are:
src/infra/heartbeat-runner.tsnow consumes inspected system events on success and on theno-tasks-dueearly-return path.src/infra/system-events.tsnow removes only the exact inspected queued entries instead of draining the whole session queue.src/memory-host-sdk/host/session-files.tsandpackages/memory-host-sdk/src/host/session-files.tsmark dreaming-generated transcripts, andextensions/memory-core/src/dreaming-phases.tsskips them during corpus ingestion.This closes both the replay bug and the “dreaming about its own dreams” feedback loop.
3. Dreaming repair and diary cleanup are now explicit product actions
Once the runtime was fixed, the next problem was cleanup of already-polluted derived state. This PR adds one shared repair backend in
memory-coreand exposes it consistently.flowchart TD A["Polluted dream artifacts"] --> B["memory-core repair API"] B --> C["CLI: openclaw memory status --fix"] B --> D["CLI: openclaw doctor --fix"] B --> E["Control UI: Repair Dream Cache"] B --> F["Control UI: Dedupe Diary"]The repair model is intentionally conservative:
Repair Dream Cachearchives and resets derived dream artifacts such asmemory/.dreams/session-corpusandmemory/.dreams/session-ingestion.json.Dedupe DiaryrewritesDREAMS.md, but only removes exact duplicate diary blocks.DREAMS.mdis not silently deleted by cache repair.The dedupe rule is intentionally narrow:
That means we remove only exact same-timestamp / same-body duplicates, not fuzzy “similar” entries.
4. Diary rewrites are now serialized
The branch also fixes a write race on
DREAMS.md.Before, append/dedupe/backfill paths each did their own read-modify-write cycle. If two paths ran together, one could overwrite the other.
Now all diary mutations go through one locked update path in
extensions/memory-core/src/dreaming-narrative.ts.sequenceDiagram participant A as Append participant B as Dedupe participant L as Per-file async lock participant F as DREAMS.md A->>L: acquire A->>F: read/update/write A-->>L: release B->>L: acquire B->>F: read/update/write B-->>L: releaseThis preserves the existing behavior while preventing lost updates inside a single process.
5. Dreams UI now explains wiki-backed tabs instead of failing opaquely
Imported InsightsandMemory Palaceare provided by the bundledmemory-wikiplugin, but the UI previously exposed those subtabs unconditionally.This PR keeps the tabs visible, but when
memory-wikiis disabled it shows a clear enablement state instead of a vague failure:memory-wikiplugins.entries.memory-wiki.enabled = trueOpen ConfigIt also adds better action feedback in
Dreams -> Advancedfor repair/dedupe:Copy archive pathbuttonRuntime Flow Summary
flowchart TD A["Conversation/session state"] --> B["Active-memory recall resolves strongest channel"] A --> C["Dreaming heartbeat consumes exact queued events once"] C --> D["Dream corpus built from normal session material"] D --> E["Dreaming-generated transcripts filtered out"] E --> F["Narrative diary writes serialized to DREAMS.md"] F --> G["Repair + dedupe available in CLI, doctor, and UI"] G --> H["Wiki tabs gated behind memory-wiki enablement"]Main Files To Look At
extensions/active-memory/index.tssrc/infra/heartbeat-runner.tssrc/infra/system-events.tssrc/memory-host-sdk/host/session-files.tspackages/memory-host-sdk/src/host/session-files.tsextensions/memory-core/src/dreaming-phases.tsextensions/memory-core/src/dreaming-repair.tsextensions/memory-core/src/dreaming-narrative.tssrc/commands/doctor-memory-search.tssrc/gateway/server-methods/doctor.tsui/src/ui/controllers/dreaming.tsui/src/ui/views/dreaming.tsTesting
pnpm vitest run --project infra src/infra/system-events.test.ts src/infra/heartbeat-runner.ghost-reminder.test.ts src/infra/heartbeat-runner.isolated-key-stability.test.tsnode scripts/run-vitest.mjs run --config vitest.config.ts extensions/memory-core/src/dreaming-repair.test.ts extensions/memory-core/src/dreaming-narrative.test.tspnpm vitest run --project extensions extensions/active-memory/config.test.ts extensions/active-memory/index.test.ts -t "modelFallback"pnpm vitest run src/memory-host-sdk/host/session-files.test.tspnpm vitest run extensions/memory-core/src/dreaming-phases.test.tspnpm vitest run extensions/memory-core/src/cli.test.tspnpm vitest run src/commands/doctor-memory-search.test.tspnpm vitest run src/gateway/server-methods/doctor.test.tspnpm vitest run ui/src/ui/controllers/dreaming.test.tspnpm vitest run ui/src/ui/app-settings.test.tspnpm vitest run ui/src/ui/views/dreaming.test.ts