






















@@ -10,11 +10,17 @@ import {
1010resolveMemoryRemDreamingConfig,
1111} from "openclaw/plugin-sdk/memory-core-host-status";
1212import { describe, expect, it, vi } from "vitest";
13-import { __testing, runDreamingSweepPhases } from "./dreaming-phases.js";
13+import {
14+__testing,
15+filterRecallEntriesWithinLookback,
16+runDreamingSweepPhases,
17+} from "./dreaming-phases.js";
18+import { previewRemHarness } from "./rem-harness.js";
1419import {
1520rankShortTermPromotionCandidates,
1621recordShortTermRecalls,
1722resolveShortTermPhaseSignalStorePath,
23+type ShortTermRecallEntry,
1824} from "./short-term-promotion.js";
1925import { createMemoryCoreTestHarness } from "./test-helpers.js";
2026@@ -2523,3 +2529,179 @@ describe("memory-core dreaming phases", () => {
25232529expect(after2[0]?.dailyCount).toBe(2);
25242530});
25252531});
2532+2533+describe("filterRecallEntriesWithinLookback", () => {
2534+const NOW_MS = new Date("2026-04-15T12:00:00.000Z").getTime();
2535+const LOOKBACK_DAYS = 3;
2536+const STALE_LAST_RECALLED_AT = new Date("2026-03-01T00:00:00.000Z").toISOString();
2537+const FRESH_RECALL_DAY = "2026-04-14";
2538+2539+function makeEntry(
2540+overrides: Partial<ShortTermRecallEntry> & Pick<ShortTermRecallEntry, "key">,
2541+): ShortTermRecallEntry {
2542+return {
2543+key: overrides.key,
2544+path: overrides.path ?? "src/example.ts",
2545+startLine: overrides.startLine ?? 1,
2546+endLine: overrides.endLine ?? 10,
2547+source: "memory",
2548+snippet: overrides.snippet ?? "example snippet",
2549+recallCount: overrides.recallCount ?? 1,
2550+dailyCount: overrides.dailyCount ?? 0,
2551+groundedCount: overrides.groundedCount ?? 0,
2552+totalScore: overrides.totalScore ?? 1,
2553+maxScore: overrides.maxScore ?? 1,
2554+firstRecalledAt: overrides.firstRecalledAt ?? STALE_LAST_RECALLED_AT,
2555+lastRecalledAt: overrides.lastRecalledAt ?? STALE_LAST_RECALLED_AT,
2556+queryHashes: overrides.queryHashes ?? [],
2557+recallDays: overrides.recallDays ?? [],
2558+conceptTags: overrides.conceptTags ?? [],
2559+ ...(overrides.claimHash !== undefined ? { claimHash: overrides.claimHash } : {}),
2560+ ...(overrides.promotedAt !== undefined ? { promotedAt: overrides.promotedAt } : {}),
2561+};
2562+}
2563+2564+it("keeps entries with stale lastRecalledAt when recallDays has a recent day", () => {
2565+const entry = makeEntry({
2566+key: "stale-last-recalled-fresh-day",
2567+lastRecalledAt: STALE_LAST_RECALLED_AT,
2568+recallDays: [FRESH_RECALL_DAY],
2569+});
2570+const result = filterRecallEntriesWithinLookback({
2571+entries: [entry],
2572+nowMs: NOW_MS,
2573+lookbackDays: LOOKBACK_DAYS,
2574+});
2575+expect(result).toHaveLength(1);
2576+expect(result[0]?.key).toBe("stale-last-recalled-fresh-day");
2577+});
2578+2579+it("keeps entries with unparseable lastRecalledAt when recallDays has a recent day", () => {
2580+const entry = makeEntry({
2581+key: "bad-last-recalled-fresh-day",
2582+lastRecalledAt: "not-a-date",
2583+recallDays: [FRESH_RECALL_DAY],
2584+});
2585+const result = filterRecallEntriesWithinLookback({
2586+entries: [entry],
2587+nowMs: NOW_MS,
2588+lookbackDays: LOOKBACK_DAYS,
2589+});
2590+expect(result).toHaveLength(1);
2591+expect(result[0]?.key).toBe("bad-last-recalled-fresh-day");
2592+});
2593+2594+it("drops entries whose lastRecalledAt and recallDays are both outside the window", () => {
2595+const entry = makeEntry({
2596+key: "stale-everything",
2597+lastRecalledAt: STALE_LAST_RECALLED_AT,
2598+recallDays: ["2026-03-02"],
2599+});
2600+const result = filterRecallEntriesWithinLookback({
2601+entries: [entry],
2602+nowMs: NOW_MS,
2603+lookbackDays: LOOKBACK_DAYS,
2604+});
2605+expect(result).toHaveLength(0);
2606+});
2607+2608+it("keeps entries with a recent lastRecalledAt even when recallDays is empty", () => {
2609+const entry = makeEntry({
2610+key: "fresh-last-recalled-no-days",
2611+lastRecalledAt: new Date("2026-04-14T00:00:00.000Z").toISOString(),
2612+recallDays: [],
2613+});
2614+const result = filterRecallEntriesWithinLookback({
2615+entries: [entry],
2616+nowMs: NOW_MS,
2617+lookbackDays: LOOKBACK_DAYS,
2618+});
2619+expect(result).toHaveLength(1);
2620+expect(result[0]?.key).toBe("fresh-last-recalled-no-days");
2621+});
2622+});
2623+2624+describe("previewRemHarness", () => {
2625+it("ignores daily-named directories when collecting grounded inputs", async () => {
2626+const workspaceDir = await createDreamingWorkspace();
2627+const memoryDir = path.join(workspaceDir, "memory");
2628+await fs.mkdir(path.join(memoryDir, "2026-04-14.md"), { recursive: true });
2629+await fs.writeFile(path.join(memoryDir, "2026-04-15.md"), "# Day\n\nWorked on REM.\n", "utf-8");
2630+2631+const preview = await previewRemHarness({
2632+ workspaceDir,
2633+grounded: true,
2634+pluginConfig: {
2635+dreaming: {
2636+enabled: true,
2637+phases: {
2638+rem: { enabled: true, limit: 10 },
2639+},
2640+},
2641+},
2642+});
2643+2644+expect(preview.groundedInputPaths.map((entry) => path.basename(entry))).toEqual([
2645+"2026-04-15.md",
2646+]);
2647+expect(preview.grounded?.scannedFiles).toBe(1);
2648+});
2649+2650+it("keeps grounded preview null when no grounded inputs exist", async () => {
2651+const workspaceDir = await createDreamingWorkspace();
2652+2653+const preview = await previewRemHarness({
2654+ workspaceDir,
2655+grounded: true,
2656+pluginConfig: {
2657+dreaming: {
2658+enabled: true,
2659+phases: {
2660+rem: { enabled: true, limit: 10 },
2661+},
2662+},
2663+},
2664+});
2665+2666+expect(preview.groundedInputPaths).toEqual([]);
2667+expect(preview.grounded).toBeNull();
2668+});
2669+2670+it("skips REM preview when rem.limit=0 while still ranking deep candidates", async () => {
2671+const workspaceDir = await createDreamingWorkspace();
2672+const nowMs = new Date("2026-04-15T12:00:00.000Z").getTime();
2673+await recordShortTermRecalls({
2674+ workspaceDir,
2675+query: "outdoor plans",
2676+ nowMs,
2677+results: [
2678+{
2679+path: "memory/2026-04-14.md",
2680+startLine: 1,
2681+endLine: 1,
2682+score: 0.92,
2683+snippet: "Always check weather before suggesting outdoor plans.",
2684+source: "memory",
2685+},
2686+],
2687+});
2688+2689+const preview = await previewRemHarness({
2690+ workspaceDir,
2691+ nowMs,
2692+pluginConfig: {
2693+dreaming: {
2694+enabled: true,
2695+phases: {
2696+rem: { enabled: true, limit: 0 },
2697+},
2698+},
2699+},
2700+});
2701+2702+expect(preview.remSkipped).toBe(true);
2703+expect(preview.rem.candidateTruths).toEqual([]);
2704+expect(preview.rem.bodyLines).toEqual([]);
2705+expect(preview.deep.candidates[0]?.snippet).toContain("Always check weather");
2706+});
2707+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。