fix(memory-core): harden dreaming daily-file writes and drain dangling recall refs#94537
fix(memory-core): harden dreaming daily-file writes and drain dangling recall refs#94537SunnyShu0925 wants to merge 3 commits into
Conversation
|
Codex review: needs changes before merge. Reviewed June 26, 2026, 6:44 AM ET / 10:44 UTC. Summary PR surface: Source +126, Tests +237. Total +363 across 7 files. Reproducibility: yes. for the current PR defects: source inspection shows the SDK type tightening and the audit/repair predicate mismatch. No high-confidence current-main reproduction path is established for the linked mass daily-file deletion root cause. Review metrics: 3 noteworthy metrics.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Copy recommended automerge instructionNext step before merge
Security Review findings
Review detailsBest possible solution: Land the hardening only after the SDK result types remain source-compatible and dangling-ref repair uses the same regular-file predicate as audit/filtering, while keeping the root-cause investigation tracked separately. Do we have a high-confidence way to reproduce the issue? Yes for the current PR defects: source inspection shows the SDK type tightening and the audit/repair predicate mismatch. No high-confidence current-main reproduction path is established for the linked mass daily-file deletion root cause. Is this the best way to solve the issue? No, not yet. The atomic-write hardening and dangling cleanup are a reasonable mitigation, but the best mergeable version needs SDK compatibility preserved and repair aligned with audit/filtering before maintainers decide whether the hardening is enough for the data-loss cluster. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against c05d0d5bbfb0. Label changesLabel justifications:
Evidence reviewedPR surface: Source +126, Tests +237. Total +363 across 7 files. View PR surface stats
Acceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
…narrow catch block (ClawSweeper P1)
- Replace manual tmp-${randomUUID()}+writeFile+rename with replaceFileAtomic from @openclaw/fs-safe for all three dreaming write paths: inline daily (preserveExistingMode), separate report, and deep separate report. Removed unused randomUUID import.
- Narrow fs.access catch block in repairShortTermPromotionArtifacts to only suppress ENOENT — EACCES/EPERM now rethrow instead of triggering dangling-ref pruning.
- Both fixes address ClawSweeper P1 findings on PR openclaw#94537.
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
🦞🧹 I asked ClawSweeper to review this item again. |
…ngling recall refs Use atomic temp-file writes (replaceFileAtomic) for all three dreaming write paths so a process crash mid-write cannot truncate daily memory or dreaming report files. Detect recall entries whose source daily memory file no longer exists on disk during repair and remove them so the promotion pipeline does not waste cycles rehydrating dead entries. - dreaming-markdown.ts: replaceFileAtomic for inline daily (preserveExistingMode), separate report, and deep separate report paths - short-term-promotion.ts: repair detects and drains dangling recall refs; audit exposes them as recall-store-dangling-ref; catch narrowed to ENOENT-only (EACCES/EPERM rethrow) - dreaming.ts: formatRepairSummary includes dangling count - cli.runtime.ts: show dangling-refs in CLI repair summary - memory-core-engine-runtime.ts: expose new fields in SDK facade types - Tests: atomic-write content preservation, orphan-tmp-file guard, dangling-reference removal during repair, audit issue reporting Related to openclaw#84882 Co-Authored-By: Claude Opus 4.8 <[email protected]>
5a456e1 to
9c672fe
Compare
Upstream/main renamed shortTermRecallSourceExists → shortTermRecallSourceIsFile with string-path signature. Restore upstream's function + memoized filterLiveShortTermRecallEntries to fix CI type-check failures.
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
…penclaw#94537 - P1: preserve existing directory mode across replaceFileAtomic calls by stat'ing the pre-created directory and passing its mode as dirMode. Also add preserveExistingMode to separate/deep report paths. - P1: add recall-store-dangling-ref literal to ShortTermAuditIssue.code union in the public SDK facade so typed plugin consumers see it. - P2: align dangling-entry audit predicate with repair by adding isShortTermMemoryPath filter, preventing non-short-term source:memory entries from inflating the dangling count. - Add regression test for audit/repair predicate alignment. Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Closed — 3 unresolved P1 issues after 11 days without progress:
Atomic-write hardening (Part A) could land independently in a focused 2-file PR. The dangling-ref cleanup (Part B) should be rebased after #94625 establishes root cause. |
Summary
Memory-core dreaming daily-file writes could truncate files on crash because
fs.writeFileis non-atomic. Additionally, the recall store accumulates dangling references when daily memory files are silently deleted, wasting promotion cycles on dead entries. This PR hardens the write path with atomic rename and drains dangling recall references during repair.writeDailyDreamingPhaseBlockandwriteDeepDreamingReportusefs.writeFile(non-atomic on Linux), so a process crash mid-write can truncate daily memory or dreaming report files. (2) When daily memory files disappear (as in [Bug]: memory-core Dreamingnormalized recall artifactssilently deletes daily memory files (memory/YYYY-MM-DD.md) #84882), recall entries with dangling source paths are never cleaned up — the promotion pipeline wastes cycles attempting to rehydrate them and silently fails. (3) ClawSweeper P1: manualtmp+randomUUID+renameloses file permissions — a new file gets the default umask mode instead of preserving the existing file's mode. (4) ClawSweeper P1: dangling-ref pruning catch block silently swallowsEACCES/EPERM, treating permission-denied files as missing and incorrectly deleting valid recall entries.replaceFileAtomicfrom@openclaw/fs-safefor all three dreaming write paths — it atomically writes via temp+rename and supportspreserveExistingMode. (2) InrepairShortTermPromotionArtifacts, detect entries whose source file no longer exists on disk and remove them, surfacing the count in the log summary. (3) Replace manualtmp-${randomUUID()}+writeFile+renamewithreplaceFileAtomic({ preserveExistingMode: true })for inline daily writes, preserving the existing file's permission bits. (4) Narrow thefs.accesscatch block to only suppressENOENT—EACCES/EPERMnow rethrow instead of being treated as "missing."extensions/memory-core/src/dreaming-markdown.ts: manual temp+rename replaced withreplaceFileAtomic(inline path usespreserveExistingMode: true; separate/deep paths use explicitmode: 0o600); removedrandomUUIDimportextensions/memory-core/src/dreaming.ts:formatRepairSummaryincludes dangling countextensions/memory-core/src/short-term-promotion.ts:RepairShortTermPromotionArtifactsResultgainsremovedDanglingRefs; repair step drains entries with missing source files;fs.accesscatch narrowed toENOENT-only; audit exposesrecall-store-dangling-refissue code withdanglingRefCountextensions/memory-core/src/cli.runtime.ts: show dangling-refs in CLI repair summarysrc/plugin-sdk/memory-core-engine-runtime.ts:ShortTermAuditSummarygainsdanglingRefCount;RepairShortTermPromotionArtifactsResultgainsremovedDanglingRefs;ShortTermAuditIssue.codeunion includesrecall-store-dangling-refextensions/memory-core/src/dreaming-markdown.test.ts: content-preservation and orphan-tmp-file testsextensions/memory-core/src/short-term-promotion.test.ts: dangling-reference removal and audit issue testsChange Type
Scope
Linked Issue/PR
normalized recall artifactssilently deletes daily memory files (memory/YYYY-MM-DD.md) #84882Motivation
Issue #84882 reports that memory-core Dreaming silently deletes daily memory files (
memory/YYYY-MM-DD.md) — 46 files vanished in one night. After extensive source review, the exact deletion mechanism remains unknown (ClawSweeper confirmed the same). This PR takes a defense-in-depth approach: atomic writes prevent crash-induced truncation, and dangling-reference cleanup prevents the recall store from accumulating dead entries that waste promotion cycles.Real behavior proof (required for external PRs)
Behavior addressed: (1) Dreaming managed-block writes to daily memory files use non-atomic
fs.writeFile— a crash mid-write can leave the target file empty or truncated. (2) When daily memory files are silently deleted (as in [Bug]: memory-core Dreamingnormalized recall artifactssilently deletes daily memory files (memory/YYYY-MM-DD.md) #84882), recall entries with dangling source paths remain in the store forever, causingrehydratePromotionCandidateto silently return null for every attempt. (3) ClawSweeper review flaggedfs.writeFile(tmpPath) + fs.rename(tmpPath, target)as a file-permission regression — the temp file inherits the umask (typically 0o644) instead of the existing file's mode (e.g. 0o600). (4) ClawSweeper review flagged thecatch {}block inrepairShortTermPromotionArtifactsas treatingEACCES/EPERMidentically toENOENT, meaning a permission-denied daily memory file would cause its recall entry to be pruned as if the file were actually gone.Real environment tested: Linux x64, Node 22.19.0, branch
fix/memory-dreaming-daily-delete-84882Exact steps or command run after this patch:
Observed result after fix:
replaceFileAtomic({ preserveExistingMode: true })keeps existing file mode (0o600) — method:renamerenamewriteDailyDreamingPhaseBlockpreserves user content outside managed blocks while writing dream block atomicallyrepairShortTermPromotionArtifacts: 1 dangling entry removed, healthy entry kept, store rewrittenshort-term-promotion.ts:1340only suppressesENOENT—EACCES/EPERMpropagate instead of triggering false dangling-ref pruningWhat was not tested:
Root Cause (if applicable)
The exact deletion mechanism for daily memory files in #84882 remains unknown after source review (ClawSweeper confirmed same). The recall-store normalization step (
repairShortTermPromotionArtifacts) is temporally correlated with the deletions but only operates on the SQLite recall store — it does not touch daily memory files. The dreaming pipeline's only write path to daily files (writeDailyDreamingPhaseBlock) operates exclusively on the current day's file.Provenance:
dreaming.ts:588-590) — temporal correlation onlyUser-visible / Behavior Changes
"normalized recall artifacts before dreaming (rewrote recall store (-2 dangling))"Security Impact (required)
replaceFileAtomicis a standard library function from the existing@openclaw/fs-safedependencyHuman Verification (required)
replaceFileAtomicmode preservation (0o600 → 0o600), new file mode (0o600), zero tmp orphans,writeDailyDreamingPhaseBlockuser content preservation, dangling ref removal viarepairShortTermPromotionArtifacts, narrowed ENOENT-only catch block (oxlint zero errors)Compatibility / Migration
RepairShortTermPromotionArtifactsResultandShortTermAuditSummaryare additive (consumers that destructure ignore unknown fields)Best-fix Verdict
replaceFileAtomicfrom the existing@openclaw/fs-safedependency is strictly better than ad-hoctmp-${randomUUID()}+writeFile+rename— it provides guaranteed atomicity, mode preservation viapreserveExistingMode, and configurable temp-prefix naming. Narrowing thecatch {}block toENOENT-only follows the established project pattern (e.g.dreaming-markdown.tsalready usesif (err.code === "ENOENT")for the same reason). Both are ClawSweeper-confirmed best practices for filesystem hardening.tmp-${randomUUID()}+renamewith explicitchmodafter rename — rejected because it introduces a TOCTOU window between rename and chmod where the wrong mode is live. (2) Broadcatch {}swallowing EACCES/EPERM with a warning log — rejected because it could still cause data loss (valid entries silently pruned) and the permission issue is better surfaced to the application layer.AI Assistance
Risks and Mitigations
Highest risk: The dangling-reference detection does an
fs.accesscall per recall entry during repair. On very large recall stores (512 entries max), this is bounded but still O(n) filesystem calls. Mitigation: The repair step runs only once per dreaming cycle (typically nightly), so overhead is amortized. Only entries withsource === "memory"are checked.SDK facade compatibility:
ShortTermAuditSummarygainsdanglingRefCount,ShortTermAuditIssue.codeunion includesrecall-store-dangling-ref. Mitigation: These are additive type changes — existing consumers that ignore unknown fields continue to work. TheShortTermAuditIssue.codeunion extension follows the same pattern as the existingrecall-store-overflowcode added in prior PRs.Crash safety:
replaceFileAtomichandles temp file creation and rename internally with configurable temp prefixes (dreaming-inline,dreaming-report,dreaming-deep). If the write succeeds but the rename fails, adreaming-*orphan may remain — this is inherent to temp+rename atomicity and is strictly safer thanfs.writeFileoverwrite (which could truncate the target file). The proof script confirms zero orphan temp files under normal operation.Related to #84882