mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-17 12:31:38 +00:00
3747 lines
122 KiB
TypeScript
3747 lines
122 KiB
TypeScript
// Memory Core tests cover short term promotion plugin behavior.
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { expectDefined } from "@openclaw/normalization-core";
|
|
import type { OpenKeyedStoreOptions } from "openclaw/plugin-sdk/plugin-state-runtime";
|
|
import { createPluginStateKeyedStoreForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime";
|
|
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("openclaw/plugin-sdk/memory-host-events", () => ({
|
|
appendMemoryHostEvent: vi.fn(async () => {}),
|
|
}));
|
|
|
|
import {
|
|
configureMemoryCoreDreamingState,
|
|
configureMemoryCoreDreamingStateForTests,
|
|
resetMemoryCoreDreamingStateForTests,
|
|
SHORT_TERM_PHASE_SIGNAL_NAMESPACE,
|
|
} from "./dreaming-state.js";
|
|
import {
|
|
applyShortTermPromotions,
|
|
auditShortTermPromotionArtifacts,
|
|
filterLiveShortTermRecallEntries,
|
|
isShortTermMemoryPath,
|
|
loadShortTermPromotionDreamingStats,
|
|
recordGroundedShortTermCandidates,
|
|
rankShortTermPromotionCandidates,
|
|
recordDreamingPhaseSignals,
|
|
recordRemConsideredPhaseSignals,
|
|
recordShortTermRecalls,
|
|
readLightStagedKeys,
|
|
removeGroundedShortTermCandidates,
|
|
repairShortTermPromotionArtifacts,
|
|
testing,
|
|
} from "./short-term-promotion.js";
|
|
|
|
describe("short-term promotion", () => {
|
|
let fixtureRoot = "";
|
|
let caseId = 0;
|
|
|
|
beforeAll(async () => {
|
|
await configureMemoryCoreDreamingStateForTests();
|
|
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "memory-promote-"));
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (!fixtureRoot) {
|
|
return;
|
|
}
|
|
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
|
resetMemoryCoreDreamingStateForTests();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
async function withTempWorkspace(run: (workspaceDir: string) => Promise<void>) {
|
|
const workspaceDir = path.join(fixtureRoot, `case-${caseId++}`);
|
|
await fs.mkdir(path.join(workspaceDir, "memory", ".dreams"), { recursive: true });
|
|
await run(workspaceDir);
|
|
}
|
|
|
|
async function writeDailyMemoryNote(
|
|
workspaceDir: string,
|
|
date: string,
|
|
lines: string[],
|
|
): Promise<string> {
|
|
const notePath = path.join(workspaceDir, "memory", `${date}.md`);
|
|
await fs.writeFile(notePath, `${lines.join("\n")}\n`, "utf-8");
|
|
return notePath;
|
|
}
|
|
|
|
async function writeDailyMemoryNoteInSubdir(
|
|
workspaceDir: string,
|
|
subdir: string,
|
|
date: string,
|
|
lines: string[],
|
|
): Promise<string> {
|
|
const dir = path.join(workspaceDir, "memory", subdir);
|
|
await fs.mkdir(dir, { recursive: true });
|
|
const notePath = path.join(dir, `${date}.md`);
|
|
await fs.writeFile(notePath, `${lines.join("\n")}\n`, "utf-8");
|
|
return notePath;
|
|
}
|
|
|
|
function requireCandidateKey(
|
|
candidate: { key?: string } | null | undefined,
|
|
label: string,
|
|
): string {
|
|
if (!candidate?.key) {
|
|
throw new Error(`expected ${label} candidate key`);
|
|
}
|
|
return candidate.key;
|
|
}
|
|
|
|
function requirePromotedAt(
|
|
candidate: { promotedAt?: string } | null | undefined,
|
|
label: string,
|
|
): string {
|
|
if (typeof candidate?.promotedAt !== "string" || candidate.promotedAt.length === 0) {
|
|
throw new Error(`expected ${label} promotedAt timestamp`);
|
|
}
|
|
return candidate.promotedAt;
|
|
}
|
|
|
|
async function readRecallStoreEntries(workspaceDir: string): Promise<
|
|
Record<
|
|
string,
|
|
{
|
|
claimHash?: unknown;
|
|
firstRecalledAt?: unknown;
|
|
lastRecalledAt?: unknown;
|
|
dailyCount?: unknown;
|
|
recallCount?: unknown;
|
|
snippet?: unknown;
|
|
totalScore?: unknown;
|
|
}
|
|
>
|
|
> {
|
|
return await testing
|
|
.readRecallStore(workspaceDir, new Date().toISOString())
|
|
.then((store) => store.entries);
|
|
}
|
|
|
|
function readEntrySnippet(entry: { snippet?: unknown }): string {
|
|
return typeof entry.snippet === "string" ? entry.snippet : "";
|
|
}
|
|
|
|
async function expectEnoent(promise: Promise<unknown>): Promise<void> {
|
|
await expect(promise).rejects.toHaveProperty("code", "ENOENT");
|
|
}
|
|
|
|
it("detects short-term daily memory paths", () => {
|
|
expect(isShortTermMemoryPath("memory/2026-04-03.md")).toBe(true);
|
|
expect(isShortTermMemoryPath("2026-04-03.md")).toBe(true);
|
|
expect(isShortTermMemoryPath("memory/.dreams/session-corpus/2026-04-03.txt")).toBe(true);
|
|
expect(isShortTermMemoryPath("notes/2026-04-03.md")).toBe(false);
|
|
expect(isShortTermMemoryPath("MEMORY.md")).toBe(false);
|
|
expect(isShortTermMemoryPath("memory/network.md")).toBe(false);
|
|
expect(isShortTermMemoryPath("memory/daily/2026-04-03.md")).toBe(true);
|
|
expect(isShortTermMemoryPath("memory/daily notes/2026-04-03.md")).toBe(true);
|
|
expect(isShortTermMemoryPath("memory/日记/2026-04-03.md")).toBe(true);
|
|
expect(isShortTermMemoryPath("memory/notes/2026-04-03.md")).toBe(true);
|
|
expect(isShortTermMemoryPath("memory/nested/deep/2026-04-03.md")).toBe(true);
|
|
expect(isShortTermMemoryPath("memory/dreaming/2026-04-03.md")).toBe(false);
|
|
expect(isShortTermMemoryPath("memory/dreaming/deep/2026-04-03.md")).toBe(false);
|
|
expect(isShortTermMemoryPath("../../vault/memory/dreaming/deep/2026-04-03.md")).toBe(false);
|
|
expect(isShortTermMemoryPath("notes/daily/2026-04-03.md")).toBe(false);
|
|
});
|
|
|
|
it("records short-term recall for notes stored in a memory/ subdirectory", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const notePath = await writeDailyMemoryNoteInSubdir(workspaceDir, "daily", "2026-04-03", [
|
|
"Subdirectory recall integration test note.",
|
|
]);
|
|
const relativePath = path.relative(workspaceDir, notePath).replaceAll("\\", "/");
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "test query",
|
|
results: [
|
|
{
|
|
path: relativePath,
|
|
source: "memory",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: "Subdirectory recall integration test note.",
|
|
},
|
|
],
|
|
});
|
|
const store = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
|
expect(Object.keys(store.entries).length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
it("deduplicates source-file checks within a recall batch", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const notePath = await writeDailyMemoryNote(workspaceDir, "2026-04-03", [
|
|
"Deduplicated source check note.",
|
|
]);
|
|
const relativePath = path.relative(workspaceDir, notePath).replaceAll("\\", "/");
|
|
const entry = {
|
|
key: "duplicate-source",
|
|
path: relativePath,
|
|
startLine: 1,
|
|
endLine: 1,
|
|
source: "memory" as const,
|
|
snippet: "Deduplicated source check note.",
|
|
recallCount: 1,
|
|
dailyCount: 1,
|
|
groundedCount: 0,
|
|
totalScore: 0.9,
|
|
maxScore: 0.9,
|
|
firstRecalledAt: "2026-04-03T00:00:00.000Z",
|
|
lastRecalledAt: "2026-04-03T00:00:00.000Z",
|
|
queryHashes: ["query"],
|
|
recallDays: ["2026-04-03"],
|
|
conceptTags: [],
|
|
};
|
|
const statSpy = vi.spyOn(fs, "stat");
|
|
|
|
const live = await filterLiveShortTermRecallEntries({
|
|
workspaceDir,
|
|
entries: [entry, { ...entry, key: "duplicate-source-2" }],
|
|
});
|
|
|
|
expect(live).toHaveLength(2);
|
|
expect(statSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
it("falls back when the injected recall timestamp is outside Date range", async () => {
|
|
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 4, 30, 12, 0, 0));
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const notePath = await writeDailyMemoryNote(workspaceDir, "2026-05-30", [
|
|
"Bounded recall timestamp note.",
|
|
]);
|
|
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "bounded recall",
|
|
nowMs: 8_640_000_000_000_001,
|
|
results: [
|
|
{
|
|
path: path.relative(workspaceDir, notePath).replaceAll("\\", "/"),
|
|
source: "memory",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: "Bounded recall timestamp note.",
|
|
},
|
|
],
|
|
});
|
|
|
|
const [entry] = Object.values(await readRecallStoreEntries(workspaceDir));
|
|
expect(entry?.firstRecalledAt).toBe("2026-05-30T12:00:00.000Z");
|
|
expect(entry?.lastRecalledAt).toBe("2026-05-30T12:00:00.000Z");
|
|
});
|
|
});
|
|
|
|
it("records short-term recall for notes stored in spaced and Unicode memory subdirectories", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const spacedPath = await writeDailyMemoryNoteInSubdir(
|
|
workspaceDir,
|
|
"daily notes",
|
|
"2026-04-03",
|
|
["Spaced subdirectory recall integration test note."],
|
|
);
|
|
const unicodePath = await writeDailyMemoryNoteInSubdir(workspaceDir, "日记", "2026-04-04", [
|
|
"Unicode subdirectory recall integration test note.",
|
|
]);
|
|
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "nested subdir query",
|
|
results: [
|
|
{
|
|
path: path.relative(workspaceDir, spacedPath).replaceAll("\\", "/"),
|
|
source: "memory",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: "Spaced subdirectory recall integration test note.",
|
|
},
|
|
{
|
|
path: path.relative(workspaceDir, unicodePath).replaceAll("\\", "/"),
|
|
source: "memory",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.85,
|
|
snippet: "Unicode subdirectory recall integration test note.",
|
|
},
|
|
],
|
|
});
|
|
|
|
const raw = JSON.stringify(
|
|
await testing.readRecallStore(workspaceDir, new Date().toISOString()),
|
|
);
|
|
expect(raw).toContain("memory/daily notes/2026-04-03.md");
|
|
expect(raw).toContain("memory/日记/2026-04-04.md");
|
|
});
|
|
});
|
|
|
|
it("caps short-term recall store entries and snippets during normal recording", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const maxEntries = testing.SHORT_TERM_RECALL_MAX_ENTRIES;
|
|
const maxSnippetChars = testing.SHORT_TERM_RECALL_MAX_SNIPPET_CHARS;
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "bounded recall",
|
|
results: Array.from({ length: maxEntries + 5 }, (_, index) => ({
|
|
path: "memory/2026-04-03.md",
|
|
source: "memory" as const,
|
|
startLine: index + 1,
|
|
endLine: index + 1,
|
|
score: 0.1 + index / (maxEntries + 5),
|
|
snippet: `Recall entry ${index} ${"x".repeat(maxSnippetChars + 100)}`,
|
|
})),
|
|
});
|
|
|
|
const entries = Object.values(await readRecallStoreEntries(workspaceDir));
|
|
expect(entries).toHaveLength(maxEntries);
|
|
expect(entries.every((entry) => readEntrySnippet(entry).length <= maxSnippetChars)).toBe(
|
|
true,
|
|
);
|
|
expect(entries.some((entry) => readEntrySnippet(entry).startsWith("Recall entry 0 "))).toBe(
|
|
false,
|
|
);
|
|
expect(
|
|
entries.some((entry) =>
|
|
readEntrySnippet(entry).startsWith(`Recall entry ${maxEntries + 4} `),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
it("keeps long-snippet claim identity stable while storing capped snippets", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const maxSnippetChars = testing.SHORT_TERM_RECALL_MAX_SNIPPET_CHARS;
|
|
const longSnippet = `Stable claim identity ${"x".repeat(maxSnippetChars + 100)}`;
|
|
const claimHash = testing.buildClaimHash(longSnippet);
|
|
|
|
await recordGroundedShortTermCandidates({
|
|
workspaceDir,
|
|
query: "__dreaming_grounded_backfill__",
|
|
items: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
snippet: longSnippet,
|
|
score: 0.9,
|
|
query: "__dreaming_grounded_backfill__:candidate",
|
|
signalCount: 1,
|
|
dayBucket: "2026-04-03",
|
|
},
|
|
],
|
|
nowMs: Date.parse("2026-04-03T10:00:00.000Z"),
|
|
});
|
|
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "stable claim",
|
|
nowMs: Date.parse("2026-04-03T11:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
source: "memory",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.8,
|
|
snippet: longSnippet,
|
|
},
|
|
],
|
|
});
|
|
|
|
const entries = Object.entries(await readRecallStoreEntries(workspaceDir));
|
|
expect(entries).toHaveLength(1);
|
|
const [key, entry] = expectDefined(entries[0], "stable claim recall entry");
|
|
expect(key.endsWith(`:${claimHash}`)).toBe(true);
|
|
expect(entry.claimHash).toBe(claimHash);
|
|
expect(entry.recallCount).toBe(1);
|
|
expect(readEntrySnippet(entry).length).toBeLessThanOrEqual(maxSnippetChars);
|
|
});
|
|
});
|
|
|
|
it("ignores dream report paths when recording short-term recalls", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "dream recall",
|
|
results: [
|
|
{
|
|
path: "memory/dreaming/deep/2026-04-03.md",
|
|
source: "memory",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: "Auto-generated dream report should not seed promotions.",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(await readRecallStoreEntries(workspaceDir)).toEqual({});
|
|
});
|
|
});
|
|
|
|
it("ignores prefixed dream report paths when recording short-term recalls", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "prefixed dream recall",
|
|
results: [
|
|
{
|
|
path: "../../vault/memory/dreaming/deep/2026-04-03.md",
|
|
source: "memory",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: "External dream report should not seed promotions.",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(await readRecallStoreEntries(workspaceDir)).toEqual({});
|
|
});
|
|
});
|
|
|
|
it("ignores contaminated dreaming snippets when recording short-term recalls", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "action preference",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
source: "memory",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.92,
|
|
snippet:
|
|
"Candidate: Default to action. confidence: 0.76 evidence: memory/.dreams/session-corpus/2026-04-08.txt:1-1 recalls: 3 status: staged",
|
|
},
|
|
],
|
|
});
|
|
|
|
const store = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
|
expect(store.version).toBe(1);
|
|
expect(store.entries).toEqual({});
|
|
});
|
|
});
|
|
|
|
it("ignores bullet-prefixed dreaming snippets when recording short-term recalls", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "action preference",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
source: "memory",
|
|
startLine: 1,
|
|
endLine: 5,
|
|
score: 0.92,
|
|
snippet: [
|
|
"- Candidate: Default to action.",
|
|
" - confidence: 0.76",
|
|
" - evidence: memory/.dreams/session-corpus/2026-04-08.txt:1-1",
|
|
" - recalls: 3",
|
|
" - status: staged",
|
|
].join("\n"),
|
|
},
|
|
],
|
|
});
|
|
|
|
const store = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
|
expect(store.version).toBe(1);
|
|
expect(store.entries).toEqual({});
|
|
});
|
|
});
|
|
|
|
it("ignores raw session and transcript snippets when recording short-term recalls", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "session recap",
|
|
results: [
|
|
{
|
|
path: "memory/2026-06-18.md",
|
|
source: "memory",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.92,
|
|
snippet:
|
|
"Session: 2026-06-18 10:37:05 EDT; Session Key: agent:cody:discord:channel:1502199757592989836; Session ID: 6d52b6a2-a2e1-4839-a69a-a532b9090a6d; Source: discord",
|
|
},
|
|
{
|
|
path: "memory/2026-06-18.md",
|
|
source: "memory",
|
|
startLine: 2,
|
|
endLine: 2,
|
|
score: 0.91,
|
|
snippet: "Conversation Summary: assistant: Traced all three. No changes made.",
|
|
},
|
|
{
|
|
path: "memory/2026-06-18.md",
|
|
source: "memory",
|
|
startLine: 3,
|
|
endLine: 3,
|
|
score: 0.9,
|
|
snippet:
|
|
"user: Save important context from this session to the daily memory file. STRICT RULES: 1. The file MUST be named exactly memory/2026-06-18.md",
|
|
},
|
|
],
|
|
});
|
|
|
|
const store = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
|
expect(store.version).toBe(1);
|
|
expect(store.entries).toEqual({});
|
|
});
|
|
});
|
|
|
|
it("ignores already-promoted score metadata snippets when recording short-term recalls", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "promotion metadata",
|
|
results: [
|
|
{
|
|
path: "memory/2026-06-18.md",
|
|
source: "memory",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.94,
|
|
snippet:
|
|
"2026-06-13 09:20 America/New_York - Polycore PR #112 re-review... [score=0.837 recalls=0 avg=0.620 source=memory/2026-06-13.md:10-12]",
|
|
},
|
|
],
|
|
});
|
|
|
|
const store = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
|
expect(store.version).toBe(1);
|
|
expect(store.entries).toEqual({});
|
|
});
|
|
});
|
|
|
|
it("keeps ordinary snippets that only quote dreaming prompt markers", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "debug note",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
source: "memory",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.75,
|
|
snippet:
|
|
"Debug note: quote Write a dream diary entry from these memory fragments for docs, but do not use dreaming-narrative-like labels in production.",
|
|
},
|
|
],
|
|
});
|
|
|
|
const store = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
|
const entries = Object.values(store.entries);
|
|
expect(entries).toHaveLength(1);
|
|
expect(entries[0]?.snippet).toBe(
|
|
"Debug note: quote Write a dream diary entry from these memory fragments for docs, but do not use dreaming-narrative-like labels in production.",
|
|
);
|
|
});
|
|
});
|
|
|
|
it("records recalls and ranks candidates with weighted scores", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "router",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-02.md",
|
|
startLine: 3,
|
|
endLine: 5,
|
|
score: 0.9,
|
|
snippet: "Configured VLAN 10 on Omada router",
|
|
source: "memory",
|
|
},
|
|
{
|
|
path: "MEMORY.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.99,
|
|
snippet: "Long-term note",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "iot vlan",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-02.md",
|
|
startLine: 3,
|
|
endLine: 5,
|
|
score: 0.8,
|
|
snippet: "Configured VLAN 10 on Omada router",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
expect(ranked).toHaveLength(1);
|
|
expect(ranked[0]?.path).toBe("memory/2026-04-02.md");
|
|
expect(ranked[0]?.recallCount).toBe(2);
|
|
expect(ranked[0]?.uniqueQueries).toBe(2);
|
|
expect(ranked[0]?.score).toBeGreaterThan(0);
|
|
expect(ranked[0]?.conceptTags).toContain("router");
|
|
expect(ranked[0]?.components.conceptual).toBeGreaterThan(0);
|
|
|
|
const raw = JSON.stringify(
|
|
await testing.readRecallStore(workspaceDir, new Date().toISOString()),
|
|
);
|
|
expect(raw).toContain("memory/2026-04-02.md");
|
|
expect(raw).not.toContain("Long-term note");
|
|
});
|
|
});
|
|
|
|
it("serializes concurrent recall writes so counts are not lost", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await Promise.all(
|
|
Array.from({ length: 8 }, (_, index) =>
|
|
recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: `backup-${index % 4}`,
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.9,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
);
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(ranked).toHaveLength(1);
|
|
expect(ranked[0]?.recallCount).toBe(8);
|
|
expect(ranked[0]?.uniqueQueries).toBe(4);
|
|
});
|
|
});
|
|
|
|
it("keeps duplicate daily signals from refreshing recall freshness", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "__dreaming_daily__:2026-04-03",
|
|
signalType: "daily",
|
|
dedupeByQueryPerDay: true,
|
|
dayBucket: "2026-04-05",
|
|
nowMs: Date.parse("2026-04-05T10:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.62,
|
|
snippet: "Added primary issue extraction for pain notifications.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "__dreaming_daily__:2026-04-03",
|
|
signalType: "daily",
|
|
dedupeByQueryPerDay: true,
|
|
dayBucket: "2026-04-05",
|
|
nowMs: Date.parse("2026-04-05T11:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.62,
|
|
snippet: "Added primary issue extraction for pain notifications.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const [entry] = Object.values(await readRecallStoreEntries(workspaceDir));
|
|
expect(entry?.dailyCount).toBe(1);
|
|
expect(entry?.lastRecalledAt).toBe("2026-04-05T10:00:00.000Z");
|
|
});
|
|
});
|
|
|
|
it("uses default thresholds for promotion", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "glacier",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.96,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({ workspaceDir });
|
|
expect(ranked).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
it("lets repeated dreaming-only daily signals clear the default promotion gates", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const queryDays = ["2026-04-01", "2026-04-02", "2026-04-03"];
|
|
let candidateKey;
|
|
|
|
for (const [index, day] of queryDays.entries()) {
|
|
const nowMs = Date.parse(`${day}T10:00:00.000Z`);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: `__dreaming_daily__:${day}`,
|
|
signalType: "daily",
|
|
dedupeByQueryPerDay: true,
|
|
dayBucket: day,
|
|
nowMs,
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.62,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs,
|
|
});
|
|
candidateKey = requireCandidateKey(ranked[0], "ranked daily");
|
|
|
|
await recordDreamingPhaseSignals({
|
|
workspaceDir,
|
|
phase: "light",
|
|
keys: [candidateKey],
|
|
nowMs,
|
|
});
|
|
await recordDreamingPhaseSignals({
|
|
workspaceDir,
|
|
phase: "rem",
|
|
keys: [candidateKey],
|
|
nowMs: nowMs + 60_000,
|
|
});
|
|
|
|
if (index < 2) {
|
|
const beforeThreshold = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
nowMs,
|
|
});
|
|
expect(beforeThreshold).toHaveLength(0);
|
|
}
|
|
}
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
nowMs: Date.parse("2026-04-03T10:01:00.000Z"),
|
|
});
|
|
|
|
expect(ranked).toHaveLength(1);
|
|
expect(ranked[0]?.recallCount).toBe(0);
|
|
expect(ranked[0]?.dailyCount).toBe(3);
|
|
expect(ranked[0]?.uniqueQueries).toBe(3);
|
|
expect(ranked[0]?.recallDays).toEqual(queryDays);
|
|
expect(ranked[0]?.score).toBeGreaterThanOrEqual(0.75);
|
|
});
|
|
});
|
|
|
|
it("reads only light-staged keys that have not already gone through REM", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const nowMs = Date.parse("2026-04-05T10:00:00.000Z");
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "phase pipeline",
|
|
nowMs,
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
{
|
|
path: "memory/2026-04-02.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.91,
|
|
snippet: "Document the Ollama setup.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs,
|
|
});
|
|
const staleKey = requireCandidateKey(
|
|
ranked.find((entry) => entry.path === "memory/2026-04-01.md"),
|
|
"stale candidate",
|
|
);
|
|
const pendingKey = requireCandidateKey(
|
|
ranked.find((entry) => entry.path === "memory/2026-04-02.md"),
|
|
"pending candidate",
|
|
);
|
|
|
|
await recordDreamingPhaseSignals({
|
|
workspaceDir,
|
|
phase: "light",
|
|
keys: [staleKey],
|
|
nowMs: nowMs - 60_000,
|
|
});
|
|
await recordDreamingPhaseSignals({
|
|
workspaceDir,
|
|
phase: "rem",
|
|
keys: [staleKey],
|
|
nowMs,
|
|
});
|
|
await recordDreamingPhaseSignals({
|
|
workspaceDir,
|
|
phase: "light",
|
|
keys: [pendingKey],
|
|
nowMs: nowMs + 60_000,
|
|
});
|
|
|
|
await expect(readLightStagedKeys({ workspaceDir, nowMs: nowMs + 120_000 })).resolves.toEqual(
|
|
new Set([pendingKey]),
|
|
);
|
|
|
|
await recordRemConsideredPhaseSignals({
|
|
workspaceDir,
|
|
keys: [pendingKey],
|
|
nowMs: nowMs + 180_000,
|
|
});
|
|
|
|
await expect(readLightStagedKeys({ workspaceDir, nowMs: nowMs + 240_000 })).resolves.toEqual(
|
|
new Set(),
|
|
);
|
|
});
|
|
});
|
|
|
|
it("lets grounded durable evidence satisfy default deep thresholds", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-03", [
|
|
'Always use "Happy Together" calendar for flights and reservations.',
|
|
]);
|
|
|
|
await recordGroundedShortTermCandidates({
|
|
workspaceDir,
|
|
query: "__dreaming_grounded_backfill__",
|
|
items: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
snippet: 'Always use "Happy Together" calendar for flights and reservations.',
|
|
score: 0.92,
|
|
query: "__dreaming_grounded_backfill__:lasting-update",
|
|
signalCount: 2,
|
|
dayBucket: "2026-04-03",
|
|
},
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
snippet: 'Always use "Happy Together" calendar for flights and reservations.',
|
|
score: 0.82,
|
|
query: "__dreaming_grounded_backfill__:candidate",
|
|
signalCount: 1,
|
|
dayBucket: "2026-04-03",
|
|
},
|
|
],
|
|
dedupeByQueryPerDay: true,
|
|
nowMs: Date.parse("2026-04-03T10:00:00.000Z"),
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
nowMs: Date.parse("2026-04-03T10:00:00.000Z"),
|
|
});
|
|
|
|
expect(ranked).toHaveLength(1);
|
|
expect(ranked[0]?.groundedCount).toBe(3);
|
|
expect(ranked[0]?.uniqueQueries).toBe(2);
|
|
expect(ranked[0]?.avgScore).toBeGreaterThan(0.85);
|
|
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
nowMs: Date.parse("2026-04-03T10:00:00.000Z"),
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
const memory = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memory).toContain('Always use "Happy Together" calendar');
|
|
});
|
|
});
|
|
|
|
it("removes grounded-only staged entries without deleting mixed live entries", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-03", [
|
|
"Grounded only rule.",
|
|
"Live recall-backed rule.",
|
|
]);
|
|
|
|
await recordGroundedShortTermCandidates({
|
|
workspaceDir,
|
|
query: "__dreaming_grounded_backfill__",
|
|
items: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
snippet: "Grounded only rule.",
|
|
score: 0.92,
|
|
query: "__dreaming_grounded_backfill__:lasting-update",
|
|
signalCount: 2,
|
|
dayBucket: "2026-04-03",
|
|
},
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 2,
|
|
endLine: 2,
|
|
snippet: "Live recall-backed rule.",
|
|
score: 0.92,
|
|
query: "__dreaming_grounded_backfill__:lasting-update",
|
|
signalCount: 2,
|
|
dayBucket: "2026-04-03",
|
|
},
|
|
],
|
|
dedupeByQueryPerDay: true,
|
|
});
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "live recall",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 2,
|
|
endLine: 2,
|
|
score: 0.87,
|
|
snippet: "Live recall-backed rule.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await removeGroundedShortTermCandidates({ workspaceDir });
|
|
expect(result.removed).toBe(1);
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(ranked).toHaveLength(1);
|
|
expect(ranked[0]?.snippet).toContain("Live recall-backed rule");
|
|
expect(ranked[0]?.groundedCount).toBe(2);
|
|
expect(ranked[0]?.recallCount).toBe(1);
|
|
});
|
|
});
|
|
|
|
it("rewards spaced recalls as consolidation instead of only raw count", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "router",
|
|
nowMs: Date.parse("2026-04-01T10:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.9,
|
|
snippet: "Configured router VLAN 10 and IoT segment.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "iot segment",
|
|
nowMs: Date.parse("2026-04-04T10:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.88,
|
|
snippet: "Configured router VLAN 10 and IoT segment.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-05T10:00:00.000Z"),
|
|
});
|
|
|
|
expect(ranked).toHaveLength(1);
|
|
expect(ranked[0]?.recallDays).toEqual(["2026-04-01", "2026-04-04"]);
|
|
expect(ranked[0]?.components.consolidation).toBeGreaterThan(0.4);
|
|
});
|
|
});
|
|
|
|
it("lets recency half-life tune the temporal score", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "glacier retention",
|
|
nowMs: Date.parse("2026-04-01T10:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.92,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const slowerDecay = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-15T10:00:00.000Z"),
|
|
recencyHalfLifeDays: 14,
|
|
});
|
|
const fasterDecay = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-15T10:00:00.000Z"),
|
|
recencyHalfLifeDays: 7,
|
|
});
|
|
|
|
expect(slowerDecay).toHaveLength(1);
|
|
expect(fasterDecay).toHaveLength(1);
|
|
expect(slowerDecay[0]?.components.recency).toBeCloseTo(0.5, 3);
|
|
expect(fasterDecay[0]?.components.recency).toBeCloseTo(0.25, 3);
|
|
const slowerResult = expectDefined(slowerDecay[0], "slower decay result");
|
|
const fasterResult = expectDefined(fasterDecay[0], "faster decay result");
|
|
expect(slowerResult.score).toBeGreaterThan(fasterResult.score);
|
|
});
|
|
});
|
|
|
|
it("boosts deep ranking when light/rem phase signals reinforce a candidate", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const nowMs = Date.parse("2026-04-05T10:00:00.000Z");
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "router setup",
|
|
nowMs,
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.75,
|
|
snippet: "Router VLAN baseline noted.",
|
|
source: "memory",
|
|
},
|
|
{
|
|
path: "memory/2026-04-02.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.75,
|
|
snippet: "Backup policy for router snapshots.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "router backup",
|
|
nowMs,
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.75,
|
|
snippet: "Router VLAN baseline noted.",
|
|
source: "memory",
|
|
},
|
|
{
|
|
path: "memory/2026-04-02.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.75,
|
|
snippet: "Backup policy for router snapshots.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const baseline = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs,
|
|
});
|
|
expect(baseline).toHaveLength(2);
|
|
expect(baseline[0]?.path).toBe("memory/2026-04-01.md");
|
|
|
|
const boostedKey = requireCandidateKey(
|
|
baseline.find((entry) => entry.path === "memory/2026-04-02.md"),
|
|
"boosted baseline",
|
|
);
|
|
await recordDreamingPhaseSignals({
|
|
workspaceDir,
|
|
phase: "light",
|
|
keys: [boostedKey],
|
|
nowMs,
|
|
});
|
|
await recordDreamingPhaseSignals({
|
|
workspaceDir,
|
|
phase: "rem",
|
|
keys: [boostedKey],
|
|
nowMs,
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs,
|
|
});
|
|
expect(ranked[0]?.path).toBe("memory/2026-04-02.md");
|
|
const boostedResult = expectDefined(ranked[0], "boosted phase-signal result");
|
|
const baselineResult = expectDefined(ranked[1], "baseline phase-signal result");
|
|
expect(boostedResult.score).toBeGreaterThan(baselineResult.score);
|
|
|
|
const phaseStore = await testing.readPhaseSignalStore(
|
|
workspaceDir,
|
|
new Date(nowMs).toISOString(),
|
|
);
|
|
expect(phaseStore.entries[boostedKey]?.lightHits).toBe(1);
|
|
expect(phaseStore.entries[boostedKey]?.remHits).toBe(1);
|
|
});
|
|
});
|
|
|
|
it("weights fresh phase signals more than stale ones", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "glacier cadence",
|
|
nowMs: Date.parse("2026-04-01T10:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "backup lifecycle",
|
|
nowMs: Date.parse("2026-04-01T12:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const rankedBaseline = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-05T10:00:00.000Z"),
|
|
});
|
|
const key = requireCandidateKey(rankedBaseline[0], "ranked baseline");
|
|
|
|
await recordDreamingPhaseSignals({
|
|
workspaceDir,
|
|
phase: "rem",
|
|
keys: [key],
|
|
nowMs: Date.parse("2026-02-01T10:00:00.000Z"),
|
|
});
|
|
const staleSignalRank = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-05T10:00:00.000Z"),
|
|
});
|
|
await recordDreamingPhaseSignals({
|
|
workspaceDir,
|
|
phase: "rem",
|
|
keys: [key],
|
|
nowMs: Date.parse("2026-04-05T10:00:00.000Z"),
|
|
});
|
|
const freshSignalRank = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-05T10:00:00.000Z"),
|
|
});
|
|
|
|
expect(staleSignalRank).toHaveLength(1);
|
|
expect(freshSignalRank).toHaveLength(1);
|
|
const freshResult = expectDefined(freshSignalRank[0], "fresh phase-signal result");
|
|
const staleResult = expectDefined(staleSignalRank[0], "stale phase-signal result");
|
|
expect(freshResult.score).toBeGreaterThan(staleResult.score);
|
|
});
|
|
});
|
|
|
|
it("updates existing phase-signal rows without dropping prior signal counts", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "glacier cadence",
|
|
nowMs: Date.parse("2026-04-01T10:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-05T10:00:00.000Z"),
|
|
});
|
|
const key = ranked[0]?.key;
|
|
expect(key).toBeTruthy();
|
|
if (!key) {
|
|
throw new Error("expected ranked candidate key");
|
|
}
|
|
|
|
await testing.writeRawPhaseSignalStore(workspaceDir, {
|
|
version: 1,
|
|
updatedAt: "2026-04-01T10:00:00.000Z",
|
|
entries: {
|
|
[key]: {
|
|
key,
|
|
lightHits: 2,
|
|
remHits: 1,
|
|
lastLightAt: "2026-04-01T10:00:00.000Z",
|
|
lastRemAt: "2026-04-02T10:00:00.000Z",
|
|
},
|
|
},
|
|
});
|
|
|
|
await recordDreamingPhaseSignals({
|
|
workspaceDir,
|
|
phase: "rem",
|
|
keys: [key],
|
|
nowMs: Date.parse("2026-04-05T10:00:00.000Z"),
|
|
});
|
|
|
|
const phaseStore = await testing.readPhaseSignalStore(
|
|
workspaceDir,
|
|
"2026-04-05T10:00:00.000Z",
|
|
);
|
|
expect(phaseStore.entries[key]?.lightHits).toBe(2);
|
|
expect(phaseStore.entries[key]?.remHits).toBe(2);
|
|
});
|
|
});
|
|
|
|
it("keeps recall stats when phase-signal state cannot be read", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const nowMs = Date.parse("2026-04-05T10:00:00.000Z");
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "glacier cadence",
|
|
nowMs,
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const env = { ...process.env };
|
|
configureMemoryCoreDreamingState(<T>(options: OpenKeyedStoreOptions) => {
|
|
if (options.namespace === SHORT_TERM_PHASE_SIGNAL_NAMESPACE) {
|
|
throw new Error("phase state unavailable");
|
|
}
|
|
return createPluginStateKeyedStoreForTests<T>("memory-core", { ...options, env });
|
|
});
|
|
try {
|
|
const stats = await loadShortTermPromotionDreamingStats({ workspaceDir, nowMs });
|
|
expect(stats.shortTermCount).toBe(1);
|
|
expect(stats.recallSignalCount).toBe(1);
|
|
expect(stats.phaseSignalCount).toBe(0);
|
|
expect(stats.phaseSignalError).toContain("phase state unavailable");
|
|
} finally {
|
|
await configureMemoryCoreDreamingStateForTests();
|
|
}
|
|
});
|
|
});
|
|
|
|
it("reconciles existing promotion markers instead of appending duplicates", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
|
|
"line 1",
|
|
"line 2",
|
|
"The gateway should stay loopback-only on port 18789.",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "gateway loopback",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 3,
|
|
endLine: 3,
|
|
score: 0.95,
|
|
snippet: "The gateway should stay loopback-only on port 18789.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const firstApply = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(firstApply.applied).toBe(1);
|
|
expect(firstApply.appended).toBe(1);
|
|
expect(firstApply.reconciledExisting).toBe(0);
|
|
|
|
const rawStore = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
|
for (const entry of Object.values(rawStore.entries)) {
|
|
delete entry.promotedAt;
|
|
}
|
|
await testing.writeRawRecallStore(workspaceDir, rawStore);
|
|
|
|
const secondApply = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(secondApply.applied).toBe(1);
|
|
expect(secondApply.appended).toBe(0);
|
|
expect(secondApply.reconciledExisting).toBe(1);
|
|
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText.match(/openclaw-memory-promotion:/g)?.length).toBe(1);
|
|
expect(
|
|
memoryText.match(/The gateway should stay loopback-only on port 18789\./g)?.length,
|
|
).toBe(1);
|
|
});
|
|
});
|
|
|
|
it("does not re-append promoted candidates whose marker key path contains spaces", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNoteInSubdir(workspaceDir, "project alpha", "2026-04-01", [
|
|
"alpha",
|
|
"The project alpha gateway should stay loopback-only on port 18789.",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "project alpha gateway",
|
|
results: [
|
|
{
|
|
path: "memory/project alpha/2026-04-01.md",
|
|
startLine: 2,
|
|
endLine: 2,
|
|
score: 0.95,
|
|
snippet: "The project alpha gateway should stay loopback-only on port 18789.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(ranked.map((candidate) => candidate.key)).toContain(
|
|
"memory:memory/project alpha/2026-04-01.md:2:2",
|
|
);
|
|
|
|
const firstApply = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(firstApply.applied).toBe(1);
|
|
expect(firstApply.appended).toBe(1);
|
|
|
|
const rawStore = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
|
for (const entry of Object.values(rawStore.entries)) {
|
|
delete entry.promotedAt;
|
|
}
|
|
await testing.writeRawRecallStore(workspaceDir, rawStore);
|
|
|
|
const secondApply = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(secondApply.applied).toBe(1);
|
|
expect(secondApply.appended).toBe(0);
|
|
expect(secondApply.reconciledExisting).toBe(1);
|
|
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain(
|
|
"<!-- openclaw-memory-promotion:memory:memory/project alpha/2026-04-01.md:2:2 -->",
|
|
);
|
|
expect(memoryText.match(/openclaw-memory-promotion:/g)?.length).toBe(1);
|
|
expect(
|
|
memoryText.match(/The project alpha gateway should stay loopback-only on port 18789\./g)
|
|
?.length,
|
|
).toBe(1);
|
|
});
|
|
});
|
|
|
|
it("filters out candidates older than maxAgeDays during ranking", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "old note",
|
|
nowMs: Date.parse("2026-04-01T10:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.92,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-15T10:00:00.000Z"),
|
|
maxAgeDays: 7,
|
|
});
|
|
|
|
expect(ranked).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
it("treats negative threshold overrides as invalid and keeps defaults", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "glacier",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.96,
|
|
snippet: "Move backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: -1,
|
|
minRecallCount: -1,
|
|
minUniqueQueries: -1,
|
|
});
|
|
expect(ranked).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
it("enforces default thresholds during apply even when candidates are passed directly", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: [
|
|
{
|
|
key: "memory:memory/2026-04-03.md:1:2",
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
source: "memory",
|
|
snippet: "Move backups to S3 Glacier.",
|
|
recallCount: 1,
|
|
signalCount: 1,
|
|
avgScore: 0.95,
|
|
maxScore: 0.95,
|
|
uniqueQueries: 1,
|
|
firstRecalledAt: new Date().toISOString(),
|
|
lastRecalledAt: new Date().toISOString(),
|
|
ageDays: 0,
|
|
score: 0.95,
|
|
recallDays: [new Date().toISOString().slice(0, 10)],
|
|
conceptTags: ["glacier", "backups"],
|
|
components: {
|
|
frequency: 0.2,
|
|
relevance: 0.95,
|
|
diversity: 0.2,
|
|
recency: 1,
|
|
consolidation: 0.2,
|
|
conceptual: 0.4,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(applied.applied).toBe(0);
|
|
});
|
|
});
|
|
|
|
it("does not rank contaminated dreaming snippets from an existing short-term store", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await testing.writeRawRecallStore(workspaceDir, {
|
|
version: 1,
|
|
updatedAt: "2026-04-04T00:00:00.000Z",
|
|
entries: {
|
|
contaminated: {
|
|
key: "contaminated",
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
source: "memory",
|
|
snippet:
|
|
"Reflections: Theme: assistant. confidence: 1.00 evidence: memory/.dreams/session-corpus/2026-04-08.txt:2-2 recalls: 4 status: staged",
|
|
recallCount: 4,
|
|
dailyCount: 0,
|
|
groundedCount: 0,
|
|
totalScore: 3.6,
|
|
maxScore: 0.95,
|
|
firstRecalledAt: "2026-04-03T00:00:00.000Z",
|
|
lastRecalledAt: "2026-04-04T00:00:00.000Z",
|
|
queryHashes: ["a", "b"],
|
|
recallDays: ["2026-04-03", "2026-04-04"],
|
|
conceptTags: ["assistant"],
|
|
},
|
|
},
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
expect(ranked).toStrictEqual([]);
|
|
});
|
|
});
|
|
|
|
it("treats diff-prefixed dreaming snippets as contaminated", () => {
|
|
expect(
|
|
testing.isContaminatedDreamingSnippet(
|
|
"@@ -1,1 - Candidate: Default to action. confidence: 0.76 evidence: memory/.dreams/session-corpus/2026-04-08.txt:1-1 recalls: 3 status: staged",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("treats bracket-prefixed dreaming snippets as contaminated", () => {
|
|
expect(
|
|
testing.isContaminatedDreamingSnippet(
|
|
"([ Candidate: Default to action. confidence: 0.76 evidence: memory/.dreams/session-corpus/2026-04-08.txt:1-1 recalls: 3 status: staged",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("does not treat ordinary candidate notes with daily-memory evidence as contaminated", () => {
|
|
expect(
|
|
testing.isContaminatedDreamingSnippet(
|
|
"Candidate: move backups weekly. confidence: 0.76 evidence: memory/2026-04-08.md:1-1",
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("treats transcript-style dreaming prompt echoes as contaminated", () => {
|
|
expect(
|
|
testing.isContaminatedDreamingSnippet(
|
|
"[main/dreaming-narrative-light.jsonl#L1] User: Write a dream diary entry from these memory fragments:",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("treats snippets with metadata prefix before the Candidate marker as contaminated", () => {
|
|
expect(
|
|
testing.isContaminatedDreamingSnippet(
|
|
"- - status: staged - Candidate: User: [cron:26fb656d] run thing - confidence: 0.00 - evidence: memory/.dreams/session-corpus/2026-04-12.txt:25-25 - recalls: 0 - status: staged",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("treats snippets with confidence prefix before the Candidate marker as contaminated", () => {
|
|
expect(
|
|
testing.isContaminatedDreamingSnippet(
|
|
"confidence: 0.58 - Candidate: Assistant: Mason shipped the enforcement pass. - evidence: memory/.dreams/session-corpus/2026-04-11.txt:167-167 - recalls: 0 - status: staged",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("treats raw session metadata snippets as contaminated", () => {
|
|
expect(
|
|
testing.isContaminatedDreamingSnippet(
|
|
"Session: 2026-06-18 10:37:05 EDT; Session Key: agent:cody:discord:channel:1502199757592989836; Session ID: 6d52b6a2-a2e1-4839-a69a-a532b9090a6d; Source: discord",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("treats raw conversation summaries as contaminated", () => {
|
|
expect(
|
|
testing.isContaminatedDreamingSnippet(
|
|
"Conversation Summary: assistant: Traced all three. No changes made.",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("treats raw transcript turns as contaminated", () => {
|
|
expect(
|
|
testing.isContaminatedDreamingSnippet(
|
|
"user: Save important context from this session to the daily memory file. STRICT RULES: 1. The file MUST be named exactly memory/2026-06-18.md",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("treats promotion score metadata as contaminated", () => {
|
|
expect(
|
|
testing.isContaminatedDreamingSnippet(
|
|
"Polycore PR #112 re-review... [score=0.837 recalls=0 avg=0.620 source=memory/2026-06-13.md:10-12]",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("treats legacy and signals= promotion annotations as contaminated", () => {
|
|
expect(
|
|
testing.isContaminatedDreamingSnippet(
|
|
"Legacy annotation [score=0.837 recalls=0 avg=0.620 source=memory/2026-06-13.md:10-12]",
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
testing.isContaminatedDreamingSnippet(
|
|
"New annotation [score=0.837 signals=7 recalls=0 avg=0.620 source=memory/2026-06-13.md:10-12]",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("does not treat prose that mentions the word Candidate as contaminated", () => {
|
|
expect(
|
|
testing.isContaminatedDreamingSnippet(
|
|
"The Candidate profile for Josh Rhoden shows he runs SEU's network admin team; stack is Cisco plus Meraki.",
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
describe("lineRangeOverlapsDreamingFence", () => {
|
|
it("returns true when the range falls inside a Light Sleep fence", () => {
|
|
const lines = [
|
|
"# Daily note",
|
|
"## Notes",
|
|
"Real durable content.",
|
|
"## Light Sleep",
|
|
"<!-- openclaw:dreaming:light:start -->",
|
|
"- Candidate: some staged dream content",
|
|
"<!-- openclaw:dreaming:light:end -->",
|
|
"## After",
|
|
"More real content.",
|
|
];
|
|
// Line 6 (1-indexed) sits between the fence markers.
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 6, 6)).toBe(true);
|
|
});
|
|
|
|
it("returns false when the range sits entirely outside any dreaming fence", () => {
|
|
const lines = [
|
|
"# Daily note",
|
|
"Real durable content.",
|
|
"<!-- openclaw:dreaming:rem:start -->",
|
|
"staged dream content",
|
|
"<!-- openclaw:dreaming:rem:end -->",
|
|
"More real content.",
|
|
];
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 2, 2)).toBe(false);
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 6, 6)).toBe(false);
|
|
});
|
|
|
|
it("returns true when the range straddles a fence boundary", () => {
|
|
const lines = [
|
|
"real line 1",
|
|
"<!-- openclaw:dreaming:diary:start -->",
|
|
"dream line",
|
|
"<!-- openclaw:dreaming:diary:end -->",
|
|
"real line 5",
|
|
];
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 2, 4)).toBe(true);
|
|
});
|
|
|
|
it("recovers after a fence end so later real content is not flagged", () => {
|
|
const lines = [
|
|
"<!-- openclaw:dreaming:light:start -->",
|
|
"dream",
|
|
"<!-- openclaw:dreaming:light:end -->",
|
|
"real line 4",
|
|
"<!-- openclaw:dreaming:rem:start -->",
|
|
"more dream",
|
|
"<!-- openclaw:dreaming:rem:end -->",
|
|
"real line 8",
|
|
];
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 4, 4)).toBe(false);
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 8, 8)).toBe(false);
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 6, 6)).toBe(true);
|
|
});
|
|
|
|
// Marker lines themselves carry managed-block content. A relocated range
|
|
// that includes a `<!-- openclaw:dreaming:*:start/end -->` marker would
|
|
// build its snippet from raw lines that contain that marker text, leaking
|
|
// it into MEMORY.md alongside any adjacent fenced content captured by the
|
|
// same window. The guard treats marker lines as inside-fence so those
|
|
// ranges are rejected. (#80613)
|
|
it("returns true when the range ends on a Light Sleep start marker", () => {
|
|
const lines = [
|
|
"## Plan",
|
|
"- Plan switches use exRule, not abConfig",
|
|
"",
|
|
"## Light Sleep",
|
|
"<!-- openclaw:dreaming:light:start -->",
|
|
"- Candidate: staged dream",
|
|
"<!-- openclaw:dreaming:light:end -->",
|
|
];
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 2, 5)).toBe(true);
|
|
});
|
|
|
|
it("returns true when the range begins on a Light Sleep end marker", () => {
|
|
const lines = [
|
|
"<!-- openclaw:dreaming:light:start -->",
|
|
"- Candidate: staged dream",
|
|
"<!-- openclaw:dreaming:light:end -->",
|
|
"- normal durable bullet",
|
|
];
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 3, 4)).toBe(true);
|
|
});
|
|
|
|
it("returns true when the range covers only a marker line", () => {
|
|
const lines = [
|
|
"<!-- openclaw:dreaming:light:start -->",
|
|
"- Candidate: staged dream",
|
|
"<!-- openclaw:dreaming:light:end -->",
|
|
];
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 1, 1)).toBe(true);
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 3, 3)).toBe(true);
|
|
});
|
|
|
|
it("returns true for REM marker single-line ranges even with no body between markers", () => {
|
|
const lines = [
|
|
"real line 1",
|
|
"<!-- openclaw:dreaming:rem:start -->",
|
|
"<!-- openclaw:dreaming:rem:end -->",
|
|
"real line 4",
|
|
];
|
|
// No content between the markers, but the marker text itself must not
|
|
// ride along into a promoted snippet.
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 2, 2)).toBe(true);
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 3, 3)).toBe(true);
|
|
// Real-content single lines remain unflagged.
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 1, 1)).toBe(false);
|
|
expect(testing.lineRangeOverlapsDreamingFence(lines, 4, 4)).toBe(false);
|
|
});
|
|
});
|
|
|
|
it("does not promote rehydrated candidates whose relocated range covers a managed dreaming fence marker line (#80613)", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
// Daily note: human content + a managed Light Sleep block. The relevant
|
|
// surface is the marker lines (5 and 8), not the fenced content between
|
|
// them. The existing fence-overlap guard already blocks ranges between
|
|
// the markers; this test exercises the residual edge case where the
|
|
// relocated range covers a marker line itself.
|
|
await writeDailyMemoryNote(workspaceDir, "2026-05-18", [
|
|
"## Plan", // 1
|
|
"- Plan switches use exRule, not abConfig", // 2
|
|
"", // 3
|
|
"## Light Sleep", // 4
|
|
"<!-- openclaw:dreaming:light:start -->", // 5
|
|
"- Candidate: staged dream", // 6
|
|
" - confidence: 0.95", // 7
|
|
"<!-- openclaw:dreaming:light:end -->", // 8
|
|
]);
|
|
|
|
// Stored recall snippet equals the marker text exactly, so relocate's
|
|
// exact-match path resolves to (5, 5) with the marker as its snippet.
|
|
// The contamination predicate does not flag bare marker text (no
|
|
// Candidate/Reflections + confidence + evidence + status: staged +
|
|
// recalls signature), so the only line of defense is the fence-overlap
|
|
// guard. Pre-patch the guard returns false for a marker-only range and
|
|
// the marker text leaks into MEMORY.md; post-patch the range is rejected.
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "marker-line edge case",
|
|
results: [
|
|
{
|
|
path: "memory/2026-05-18.md",
|
|
startLine: 5,
|
|
endLine: 5,
|
|
score: 0.94,
|
|
snippet: "<!-- openclaw:dreaming:light:start -->",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
expect(applied.applied).toBe(0);
|
|
const memoryText = await fs
|
|
.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8")
|
|
.catch(() => "");
|
|
expect(memoryText).not.toContain("Promoted From Short-Term Memory");
|
|
expect(memoryText).not.toMatch(/openclaw:dreaming/i);
|
|
});
|
|
});
|
|
|
|
it("refuses to promote rehydrated candidates that land inside a managed dreaming fence", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const dailyPath = await writeDailyMemoryNote(workspaceDir, "2026-04-18", [
|
|
"# 2026-04-18",
|
|
"",
|
|
"## Notes",
|
|
"Legitimate durable observation about backups.",
|
|
"",
|
|
"## Light Sleep",
|
|
"<!-- openclaw:dreaming:light:start -->",
|
|
"- Candidate: staged dream scratchwork",
|
|
"<!-- openclaw:dreaming:light:end -->",
|
|
]);
|
|
expect(dailyPath).toBeTruthy();
|
|
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
candidates: [
|
|
{
|
|
key: "memory:memory/2026-04-18.md:8:8",
|
|
path: "memory/2026-04-18.md",
|
|
startLine: 8,
|
|
endLine: 8,
|
|
source: "memory",
|
|
snippet: "- Candidate: staged dream scratchwork",
|
|
recallCount: 3,
|
|
signalCount: 3,
|
|
avgScore: 0.9,
|
|
maxScore: 0.9,
|
|
uniqueQueries: 2,
|
|
firstRecalledAt: "2026-04-17T00:00:00.000Z",
|
|
lastRecalledAt: "2026-04-18T00:00:00.000Z",
|
|
ageDays: 1,
|
|
score: 0.9,
|
|
recallDays: ["2026-04-17", "2026-04-18"],
|
|
conceptTags: ["dream"],
|
|
components: {
|
|
frequency: 1,
|
|
relevance: 0,
|
|
diversity: 1,
|
|
recency: 1,
|
|
consolidation: 0,
|
|
conceptual: 0,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(applied.applied).toBe(0);
|
|
const memoryText = await fs
|
|
.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8")
|
|
.catch(() => "");
|
|
expect(memoryText).not.toContain("Promoted From Short-Term Memory");
|
|
expect(memoryText).not.toContain("staged dream scratchwork");
|
|
});
|
|
});
|
|
|
|
it("skips direct candidates that exceed maxAgeDays during apply", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
maxAgeDays: 7,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
candidates: [
|
|
{
|
|
key: "memory:memory/2026-04-01.md:1:1",
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
source: "memory",
|
|
snippet: "Expired short-term note.",
|
|
recallCount: 3,
|
|
signalCount: 3,
|
|
avgScore: 0.95,
|
|
maxScore: 0.95,
|
|
uniqueQueries: 2,
|
|
firstRecalledAt: "2026-04-01T00:00:00.000Z",
|
|
lastRecalledAt: "2026-04-02T00:00:00.000Z",
|
|
ageDays: 10,
|
|
score: 0.95,
|
|
recallDays: ["2026-04-01", "2026-04-02"],
|
|
conceptTags: ["expired"],
|
|
components: {
|
|
frequency: 1,
|
|
relevance: 1,
|
|
diversity: 1,
|
|
recency: 1,
|
|
consolidation: 1,
|
|
conceptual: 1,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(applied.applied).toBe(0);
|
|
await expectEnoent(fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8"));
|
|
});
|
|
});
|
|
|
|
it("does not append contaminated dreaming snippets during direct apply", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
candidates: [
|
|
{
|
|
key: "memory:memory/2026-04-03.md:1:1",
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
source: "memory",
|
|
snippet:
|
|
"Candidate: Default to action. confidence: 0.76 evidence: memory/.dreams/session-corpus/2026-04-08.txt:1-1 recalls: 3 status: staged",
|
|
recallCount: 4,
|
|
signalCount: 4,
|
|
avgScore: 0.97,
|
|
maxScore: 0.97,
|
|
uniqueQueries: 2,
|
|
firstRecalledAt: "2026-04-03T00:00:00.000Z",
|
|
lastRecalledAt: "2026-04-04T00:00:00.000Z",
|
|
ageDays: 0,
|
|
score: 0.99,
|
|
recallDays: ["2026-04-03", "2026-04-04"],
|
|
conceptTags: ["assistant"],
|
|
components: {
|
|
frequency: 1,
|
|
relevance: 1,
|
|
diversity: 1,
|
|
recency: 1,
|
|
consolidation: 1,
|
|
conceptual: 1,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(applied.applied).toBe(0);
|
|
await expectEnoent(fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8"));
|
|
});
|
|
});
|
|
|
|
it("applies promotion candidates to MEMORY.md and marks them promoted", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
|
|
"alpha",
|
|
"beta",
|
|
"gamma",
|
|
"delta",
|
|
"epsilon",
|
|
"zeta",
|
|
"eta",
|
|
"theta",
|
|
"iota",
|
|
"Gateway binds loopback and port 18789",
|
|
"Keep gateway on localhost only",
|
|
"Document healthcheck endpoint",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "gateway host",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 10,
|
|
endLine: 12,
|
|
score: 0.92,
|
|
snippet: "Gateway binds loopback and port 18789",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(applied.applied).toBe(1);
|
|
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain("Promoted From Short-Term Memory");
|
|
expect(memoryText).toContain("memory/2026-04-01.md:10-10");
|
|
|
|
const rankedAfter = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(rankedAfter).toHaveLength(0);
|
|
|
|
const rankedIncludingPromoted = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
includePromoted: true,
|
|
});
|
|
expect(rankedIncludingPromoted).toHaveLength(1);
|
|
expect(requirePromotedAt(rankedIncludingPromoted[0], "promoted candidate")).toMatch(
|
|
/^\d{4}-\d{2}-\d{2}T/,
|
|
);
|
|
});
|
|
});
|
|
|
|
it("does not double-prefix promoted snippets that are already markdown bullets", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
|
|
"alpha",
|
|
"- Gateway binds loopback and port 18789",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "gateway host",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 2,
|
|
endLine: 2,
|
|
score: 0.92,
|
|
snippet: "- Gateway binds loopback and port 18789",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain("- Gateway binds loopback and port 18789");
|
|
expect(memoryText).not.toContain("- - Gateway binds loopback and port 18789");
|
|
});
|
|
});
|
|
|
|
it("keeps promoted MEMORY.md entries compact while preserving provenance", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const longDailyEntry = [
|
|
"HanJammer reviewed the dashboard state and asked for durable memory hygiene.",
|
|
"The raw daily note also included implementation chatter, transient timings, repeated troubleshooting detail, and operational narration that should not be copied wholesale into MEMORY.md.",
|
|
"A curated long-term memory entry should preserve the stable conclusion without hauling the whole daily journal line into bootstrap context.",
|
|
"Extra filler keeps this source entry long enough to prove promotion output is bounded before it reaches the root memory file.",
|
|
].join(" ");
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [longDailyEntry]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "memory hygiene",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.92,
|
|
snippet: longDailyEntry,
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
maxPromotedSnippetTokens: 55,
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
const promotedLine = memoryText
|
|
.split("\n")
|
|
.find((line) => line.startsWith("- HanJammer reviewed the dashboard state"));
|
|
expect(promotedLine).toBeDefined();
|
|
expect(promotedLine?.length).toBeLessThan(340);
|
|
expect(promotedLine).toContain("...");
|
|
expect(promotedLine).toMatch(
|
|
/\[score=0\.\d{3} signals=1 recalls=1 avg=0\.\d{3} source=memory\/2026-04-01\.md:1-1\]/,
|
|
);
|
|
expect(memoryText).toMatch(/<!-- openclaw-memory-promotion:[^\n]+ -->/);
|
|
});
|
|
});
|
|
|
|
it("does not re-append candidates that were promoted in a prior run", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
|
|
"alpha",
|
|
"beta",
|
|
"gamma",
|
|
"delta",
|
|
"epsilon",
|
|
"zeta",
|
|
"eta",
|
|
"theta",
|
|
"iota",
|
|
"Gateway binds loopback and port 18789",
|
|
"Keep gateway on localhost only",
|
|
"Document healthcheck endpoint",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "gateway host",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 10,
|
|
endLine: 12,
|
|
score: 0.92,
|
|
snippet: "Gateway binds loopback and port 18789",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const first = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(first.applied).toBe(1);
|
|
|
|
const second = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(second.applied).toBe(0);
|
|
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
const sectionCount = memoryText.match(/Promoted From Short-Term Memory/g)?.length ?? 0;
|
|
expect(sectionCount).toBe(1);
|
|
});
|
|
});
|
|
|
|
it("rehydrates moved snippets from the live daily note before promotion", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
|
|
"intro",
|
|
"summary",
|
|
"Moved backups to S3 Glacier.",
|
|
"Keep cold storage retention at 365 days.",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "glacier",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.94,
|
|
snippet: "Moved backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.startLine).toBe(3);
|
|
expect(applied.appliedCandidates[0]?.endLine).toBe(3);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain("memory/2026-04-01.md:3-3");
|
|
});
|
|
});
|
|
|
|
it("rehydrates daily-ingested heading-prefixed list snippets from the live note", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-05-28", [
|
|
"# 2026-05-28",
|
|
"",
|
|
"## 模型切换 (16:23)",
|
|
"- **需求**: 用户想使用小米 Mimo 模型作为默认",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "__dreaming_daily__:2026-05-28",
|
|
signalType: "daily",
|
|
dedupeByQueryPerDay: true,
|
|
dayBucket: "2026-05-28",
|
|
results: [
|
|
{
|
|
path: "memory/2026-05-28.md",
|
|
startLine: 4,
|
|
endLine: 4,
|
|
score: 0.91,
|
|
snippet: "模型切换 (16:23): **需求**: 用户想使用小米 Mimo 模型作为默认",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.startLine).toBe(4);
|
|
expect(applied.appliedCandidates[0]?.endLine).toBe(4);
|
|
expect(applied.appliedCandidates[0]?.snippet).toBe(
|
|
"模型切换 (16:23): **需求**: 用户想使用小米 Mimo 模型作为默认",
|
|
);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain("memory/2026-05-28.md:4-4");
|
|
expect(memoryText).toContain("模型切换 (16:23): **需求**");
|
|
});
|
|
});
|
|
|
|
it("rehydrates daily-ingested multi-line list snippets from the full live note range", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-05-28", [
|
|
"# 2026-05-28",
|
|
"",
|
|
"## 模型切换 (16:23)",
|
|
"- **需求**: 用户想使用小米 Mimo 模型作为默认",
|
|
"- **偏好**: 保持低成本默认路由",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "__dreaming_daily__:2026-05-28",
|
|
signalType: "daily",
|
|
dedupeByQueryPerDay: true,
|
|
dayBucket: "2026-05-28",
|
|
results: [
|
|
{
|
|
path: "memory/2026-05-28.md",
|
|
startLine: 4,
|
|
endLine: 5,
|
|
score: 0.91,
|
|
snippet:
|
|
"模型切换 (16:23): **需求**: 用户想使用小米 Mimo 模型作为默认; **偏好**: 保持低成本默认路由",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.startLine).toBe(4);
|
|
expect(applied.appliedCandidates[0]?.endLine).toBe(5);
|
|
expect(applied.appliedCandidates[0]?.snippet).toBe(
|
|
"模型切换 (16:23): **需求**: 用户想使用小米 Mimo 模型作为默认; **偏好**: 保持低成本默认路由",
|
|
);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain("memory/2026-05-28.md:4-5");
|
|
expect(memoryText).toContain("模型切换 (16:23): **需求**");
|
|
expect(memoryText).toContain("**偏好**: 保持低成本默认路由");
|
|
});
|
|
});
|
|
|
|
it("rebuilds heading context from the live note during list rehydration", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-05-28", [
|
|
"# 2026-05-28",
|
|
"",
|
|
"## New model routing (16:23)",
|
|
"- Keep Xiaomi Mimo as the low-cost default.",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "__dreaming_daily__:2026-05-28",
|
|
signalType: "daily",
|
|
dedupeByQueryPerDay: true,
|
|
dayBucket: "2026-05-28",
|
|
results: [
|
|
{
|
|
path: "memory/2026-05-28.md",
|
|
startLine: 4,
|
|
endLine: 4,
|
|
score: 0.91,
|
|
snippet: "Old model routing: Keep Xiaomi Mimo as the low-cost default.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.snippet).toBe(
|
|
"New model routing (16:23): Keep Xiaomi Mimo as the low-cost default.",
|
|
);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain("New model routing (16:23)");
|
|
expect(memoryText).not.toContain("Old model routing");
|
|
});
|
|
});
|
|
|
|
it("does not rehydrate heading-prefixed list snippets without a live body", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-05-28", [
|
|
"# 2026-05-28",
|
|
"",
|
|
"## Model routing",
|
|
"",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "__dreaming_daily__:2026-05-28",
|
|
signalType: "daily",
|
|
dedupeByQueryPerDay: true,
|
|
dayBucket: "2026-05-28",
|
|
results: [
|
|
{
|
|
path: "memory/2026-05-28.md",
|
|
startLine: 4,
|
|
endLine: 4,
|
|
score: 0.91,
|
|
snippet: "Model routing: Keep Xiaomi Mimo as the low-cost default.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(applied.applied).toBe(0);
|
|
});
|
|
});
|
|
|
|
it("does not add heading context to ordinary list-item rehydration", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-05-28", [
|
|
"# 2026-05-28",
|
|
"",
|
|
"## Model routing",
|
|
"- Keep Xiaomi Mimo as the low-cost default.",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "__dreaming_daily__:2026-05-28",
|
|
signalType: "daily",
|
|
dedupeByQueryPerDay: true,
|
|
dayBucket: "2026-05-28",
|
|
results: [
|
|
{
|
|
path: "memory/2026-05-28.md",
|
|
startLine: 4,
|
|
endLine: 4,
|
|
score: 0.91,
|
|
snippet: "Keep Xiaomi Mimo as the low-cost default.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.snippet).toBe(
|
|
"Keep Xiaomi Mimo as the low-cost default.",
|
|
);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).not.toContain("Model routing: Keep Xiaomi");
|
|
});
|
|
});
|
|
|
|
it("rehydrates capped heading-prefixed list snippets from the live note", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const longBody = `Keep Xiaomi Mimo as the low-cost default ${"route ".repeat(80)}`.trim();
|
|
await writeDailyMemoryNote(workspaceDir, "2026-05-28", [
|
|
"# 2026-05-28",
|
|
"",
|
|
"## Long model routing",
|
|
`- ${longBody}`,
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "__dreaming_daily__:2026-05-28",
|
|
signalType: "daily",
|
|
dedupeByQueryPerDay: true,
|
|
dayBucket: "2026-05-28",
|
|
results: [
|
|
{
|
|
path: "memory/2026-05-28.md",
|
|
startLine: 4,
|
|
endLine: 4,
|
|
score: 0.91,
|
|
snippet: `Long model routing: ${longBody}`.slice(0, 280).replace(/\s+/g, " ").trim(),
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.startLine).toBe(4);
|
|
expect(applied.appliedCandidates[0]?.endLine).toBe(4);
|
|
expect(applied.appliedCandidates[0]?.snippet).toContain(
|
|
"Long model routing: Keep Xiaomi Mimo",
|
|
);
|
|
});
|
|
});
|
|
|
|
it("rehydrates capped heading-prefixed list snippets after the heading changes", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const longBody = `Keep Xiaomi Mimo as the low-cost default ${"route ".repeat(80)}`.trim();
|
|
await writeDailyMemoryNote(workspaceDir, "2026-05-28", [
|
|
"# 2026-05-28",
|
|
"",
|
|
"## New model routing",
|
|
`- ${longBody}`,
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "__dreaming_daily__:2026-05-28",
|
|
signalType: "daily",
|
|
dedupeByQueryPerDay: true,
|
|
dayBucket: "2026-05-28",
|
|
results: [
|
|
{
|
|
path: "memory/2026-05-28.md",
|
|
startLine: 4,
|
|
endLine: 4,
|
|
score: 0.91,
|
|
snippet: `Old model routing: ${longBody}`.slice(0, 280).replace(/\s+/g, " ").trim(),
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.snippet).toContain(
|
|
"New model routing: Keep Xiaomi Mimo",
|
|
);
|
|
expect(applied.appliedCandidates[0]?.snippet).not.toContain("Old model routing");
|
|
});
|
|
});
|
|
|
|
it("keeps renamed heading fallback bound to colon-prefixed list bodies", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-05-28", [
|
|
"# 2026-05-28",
|
|
"",
|
|
"## Nearby shortcut",
|
|
"- use Mimo",
|
|
"",
|
|
"## New model routing",
|
|
"- **需求**: use Mimo",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "__dreaming_daily__:2026-05-28",
|
|
signalType: "daily",
|
|
dedupeByQueryPerDay: true,
|
|
dayBucket: "2026-05-28",
|
|
results: [
|
|
{
|
|
path: "memory/2026-05-28.md",
|
|
startLine: 7,
|
|
endLine: 7,
|
|
score: 0.91,
|
|
snippet: "Old model routing: **需求**: use Mimo",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.startLine).toBe(7);
|
|
expect(applied.appliedCandidates[0]?.endLine).toBe(7);
|
|
expect(applied.appliedCandidates[0]?.snippet).toBe("New model routing: **需求**: use Mimo");
|
|
expect(applied.appliedCandidates[0]?.snippet).not.toContain("Nearby shortcut");
|
|
});
|
|
});
|
|
|
|
it("preserves the full range for capped heading-prefixed multi-line list snippets", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const maxDailySnippetChars = 280;
|
|
const firstListItem =
|
|
`Keep Xiaomi Mimo as the low-cost default ${"route ".repeat(12)}`.trim();
|
|
const secondListItem =
|
|
`Preserve the fallback routing note when the ingestion cap cuts this chunk ${"tail ".repeat(
|
|
42,
|
|
)}`.trim();
|
|
const fullIngestedSnippet = `Long model routing: ${firstListItem}; ${secondListItem}`
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
const ingestedSnippet = fullIngestedSnippet
|
|
.slice(0, maxDailySnippetChars)
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
expect(ingestedSnippet.length).toBeLessThan(fullIngestedSnippet.length);
|
|
|
|
await writeDailyMemoryNote(workspaceDir, "2026-05-28", [
|
|
"# 2026-05-28",
|
|
"",
|
|
"## Long model routing",
|
|
`- ${firstListItem}`,
|
|
`- ${secondListItem}`,
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "__dreaming_daily__:2026-05-28",
|
|
signalType: "daily",
|
|
dedupeByQueryPerDay: true,
|
|
dayBucket: "2026-05-28",
|
|
results: [
|
|
{
|
|
path: "memory/2026-05-28.md",
|
|
startLine: 4,
|
|
endLine: 5,
|
|
score: 0.91,
|
|
snippet: ingestedSnippet,
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.startLine).toBe(4);
|
|
expect(applied.appliedCandidates[0]?.endLine).toBe(5);
|
|
expect(applied.appliedCandidates[0]?.snippet).toContain(firstListItem);
|
|
expect(applied.appliedCandidates[0]?.snippet).toContain(secondListItem);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain("memory/2026-05-28.md:4-5");
|
|
expect(memoryText).toContain(secondListItem);
|
|
});
|
|
});
|
|
|
|
it("does not reintroduce generic daily headings during list rehydration", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-05-28", [
|
|
"# 2026-05-28",
|
|
"",
|
|
"## Model routing",
|
|
"- Keep Xiaomi Mimo as the low-cost default.",
|
|
"",
|
|
"## Morning",
|
|
"- Reviewed travel timing before the workshop.",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "__dreaming_daily__:2026-05-28",
|
|
signalType: "daily",
|
|
dedupeByQueryPerDay: true,
|
|
dayBucket: "2026-05-28",
|
|
results: [
|
|
{
|
|
path: "memory/2026-05-28.md",
|
|
startLine: 7,
|
|
endLine: 7,
|
|
score: 0.91,
|
|
snippet: "Reviewed travel timing before the workshop.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.snippet).toBe(
|
|
"Reviewed travel timing before the workshop.",
|
|
);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).not.toContain("Morning:");
|
|
expect(memoryText).not.toContain("Model routing: Reviewed travel timing");
|
|
});
|
|
});
|
|
|
|
it("does not reintroduce managed dreaming headings during list rehydration", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-05-28", [
|
|
"# 2026-05-28",
|
|
"",
|
|
"## Light Sleep",
|
|
"<!-- openclaw:dreaming:light:start -->",
|
|
"- Candidate: scratch reflection",
|
|
"<!-- openclaw:dreaming:light:end -->",
|
|
"- Reviewed travel timing before the workshop.",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "__dreaming_daily__:2026-05-28",
|
|
signalType: "daily",
|
|
dedupeByQueryPerDay: true,
|
|
dayBucket: "2026-05-28",
|
|
results: [
|
|
{
|
|
path: "memory/2026-05-28.md",
|
|
startLine: 7,
|
|
endLine: 7,
|
|
score: 0.91,
|
|
snippet: "Reviewed travel timing before the workshop.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-05-31T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.snippet).toBe(
|
|
"Reviewed travel timing before the workshop.",
|
|
);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).not.toContain("Light Sleep:");
|
|
});
|
|
});
|
|
|
|
it("keeps rehydrated promotion snippets capped in the recall store", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const maxSnippetChars = testing.SHORT_TERM_RECALL_MAX_SNIPPET_CHARS;
|
|
const longSnippet = `Moved backup policy ${"x".repeat(maxSnippetChars + 100)}`;
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", ["intro", longSnippet]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "backup policy",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.94,
|
|
snippet: longSnippet,
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const candidateKey = requireCandidateKey(ranked[0], "long rehydrated");
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.snippet.length).toBeGreaterThan(maxSnippetChars);
|
|
const entries = await readRecallStoreEntries(workspaceDir);
|
|
const storedSnippet = readEntrySnippet(entries[candidateKey] ?? {});
|
|
expect(storedSnippet.length).toBeLessThanOrEqual(maxSnippetChars);
|
|
expect(storedSnippet).toBe(applied.appliedCandidates[0]?.snippet.slice(0, maxSnippetChars));
|
|
});
|
|
});
|
|
|
|
it("prefers the nearest matching snippet when the same text appears multiple times", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
|
|
"header",
|
|
"Repeat backup note.",
|
|
"gap",
|
|
"gap",
|
|
"gap",
|
|
"gap",
|
|
"gap",
|
|
"gap",
|
|
"Repeat backup note.",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "backup repeat",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 8,
|
|
endLine: 9,
|
|
score: 0.9,
|
|
snippet: "Repeat backup note.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.appliedCandidates[0]?.startLine).toBe(9);
|
|
expect(applied.appliedCandidates[0]?.endLine).toBe(10);
|
|
});
|
|
});
|
|
|
|
it("rehydrates legacy basename-only short-term paths from the memory directory", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", ["Legacy basename path note."]);
|
|
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: [
|
|
{
|
|
key: "memory:2026-04-01.md:1:1",
|
|
path: "2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
source: "memory",
|
|
snippet: "Legacy basename path note.",
|
|
recallCount: 2,
|
|
signalCount: 2,
|
|
avgScore: 0.9,
|
|
maxScore: 0.95,
|
|
uniqueQueries: 2,
|
|
firstRecalledAt: "2026-04-01T00:00:00.000Z",
|
|
lastRecalledAt: "2026-04-02T00:00:00.000Z",
|
|
ageDays: 0,
|
|
score: 0.9,
|
|
recallDays: ["2026-04-01", "2026-04-02"],
|
|
conceptTags: ["legacy", "note"],
|
|
components: {
|
|
frequency: 0.3,
|
|
relevance: 0.9,
|
|
diversity: 0.4,
|
|
recency: 1,
|
|
consolidation: 0.5,
|
|
conceptual: 0.3,
|
|
},
|
|
},
|
|
],
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain("source=2026-04-01.md:1-1");
|
|
});
|
|
});
|
|
|
|
it("skips promotion when the live daily note no longer contains the snippet", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", ["Different note content now."]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "glacier",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.94,
|
|
snippet: "Moved backups to S3 Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
expect(applied.applied).toBe(0);
|
|
await expectEnoent(fs.access(path.join(workspaceDir, "MEMORY.md")));
|
|
});
|
|
});
|
|
|
|
it("uses dreaming timezone for recall-day bucketing and promotion headers", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
|
|
"Cross-midnight router maintenance window.",
|
|
]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "router window",
|
|
nowMs: Date.parse("2026-04-01T23:30:00.000Z"),
|
|
timezone: "America/Los_Angeles",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: "Cross-midnight router maintenance window.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
expect(ranked[0]?.recallDays).toEqual(["2026-04-01"]);
|
|
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-02T06:30:00.000Z"),
|
|
timezone: "America/Los_Angeles",
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain("Promoted From Short-Term Memory (2026-04-01)");
|
|
});
|
|
});
|
|
|
|
it("audits and repairs invalid store metadata plus stale locks", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await testing.writeRawRecallStore(workspaceDir, {
|
|
version: 1,
|
|
updatedAt: "2026-04-04T00:00:00.000Z",
|
|
entries: {
|
|
good: {
|
|
key: "good",
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
source: "memory",
|
|
snippet: "Gateway host uses qmd vector search for router notes.",
|
|
recallCount: 2,
|
|
totalScore: 1.8,
|
|
maxScore: 0.95,
|
|
firstRecalledAt: "2026-04-01T00:00:00.000Z",
|
|
lastRecalledAt: "2026-04-04T00:00:00.000Z",
|
|
queryHashes: ["a", "b"],
|
|
},
|
|
bad: {
|
|
path: "",
|
|
},
|
|
},
|
|
});
|
|
await testing.writeShortTermLock(workspaceDir, {
|
|
owner: "999999:0",
|
|
acquiredAt: Date.now() - 120_000,
|
|
});
|
|
|
|
const auditBefore = await auditShortTermPromotionArtifacts({ workspaceDir });
|
|
expect(auditBefore.invalidEntryCount).toBe(1);
|
|
expect(auditBefore.issues.map((issue) => issue.code)).toStrictEqual([
|
|
"recall-store-invalid",
|
|
"recall-lock-stale",
|
|
]);
|
|
|
|
const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
|
|
expect(repair.changed).toBe(true);
|
|
expect(repair.rewroteStore).toBe(true);
|
|
expect(repair.removedStaleLock).toBe(true);
|
|
|
|
const auditAfter = await auditShortTermPromotionArtifacts({ workspaceDir });
|
|
expect(auditAfter.invalidEntryCount).toBe(0);
|
|
expect(auditAfter.issues.map((issue) => issue.code)).not.toContain("recall-lock-stale");
|
|
|
|
const repairedRaw = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
|
expect(repairedRaw.entries.good?.conceptTags).toContain("router");
|
|
expect(repairedRaw.entries.good?.recallDays).toEqual(["2026-04-04"]);
|
|
});
|
|
});
|
|
|
|
it("audits and repairs oversized recall stores", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const maxEntries = testing.SHORT_TERM_RECALL_MAX_ENTRIES;
|
|
const maxSnippetChars = testing.SHORT_TERM_RECALL_MAX_SNIPPET_CHARS;
|
|
await testing.writeRawRecallStore(workspaceDir, {
|
|
version: 1,
|
|
updatedAt: "2026-04-04T00:00:00.000Z",
|
|
entries: Object.fromEntries(
|
|
Array.from({ length: maxEntries + 3 }, (_, index) => [
|
|
`entry-${index}`,
|
|
{
|
|
key: `entry-${index}`,
|
|
path: "memory/2026-04-01.md",
|
|
startLine: index + 1,
|
|
endLine: index + 1,
|
|
source: "memory",
|
|
snippet: `Oversized recall ${index} ${"x".repeat(maxSnippetChars + 100)}`,
|
|
recallCount: 1,
|
|
dailyCount: 0,
|
|
groundedCount: 0,
|
|
totalScore: index,
|
|
maxScore: 0.75,
|
|
firstRecalledAt: "2026-04-01T00:00:00.000Z",
|
|
lastRecalledAt: new Date(
|
|
Date.parse("2026-04-01T00:00:00.000Z") + index,
|
|
).toISOString(),
|
|
queryHashes: [`q-${index}`],
|
|
recallDays: ["2026-04-01"],
|
|
conceptTags: [],
|
|
},
|
|
]),
|
|
),
|
|
});
|
|
|
|
const auditBefore = await auditShortTermPromotionArtifacts({ workspaceDir });
|
|
expect(auditBefore.entryCount).toBe(maxEntries + 3);
|
|
expect(auditBefore.issues.map((issue) => issue.code)).toContain("recall-store-over-limit");
|
|
|
|
const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
|
|
|
|
expect(repair.changed).toBe(true);
|
|
expect(repair.rewroteStore).toBe(true);
|
|
expect(repair.removedOverflowEntries).toBe(3);
|
|
|
|
const entries = Object.values(await readRecallStoreEntries(workspaceDir));
|
|
expect(entries).toHaveLength(maxEntries);
|
|
expect(entries.every((entry) => readEntrySnippet(entry).length <= maxSnippetChars)).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
entries.some((entry) => readEntrySnippet(entry).startsWith("Oversized recall 0 ")),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
it("uses score tie-breakers when retention timestamps are invalid", () => {
|
|
const entry = {
|
|
key: "lower-score",
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
source: "memory" as const,
|
|
snippet: "Invalid timestamp recall",
|
|
recallCount: 1,
|
|
dailyCount: 0,
|
|
groundedCount: 0,
|
|
totalScore: 1,
|
|
maxScore: 0.75,
|
|
firstRecalledAt: "not-a-date",
|
|
lastRecalledAt: "not-a-date",
|
|
queryHashes: ["q"],
|
|
recallDays: ["2026-04-01"],
|
|
conceptTags: [],
|
|
};
|
|
const higherScoreEntry = { ...entry, key: "higher-score", totalScore: 2 };
|
|
|
|
expect([entry, higherScoreEntry].toSorted(testing.compareShortTermRecallRetention)).toEqual([
|
|
higherScoreEntry,
|
|
entry,
|
|
]);
|
|
});
|
|
|
|
it("rejects long contaminated legacy recall entries before truncating snippets", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const maxSnippetChars = testing.SHORT_TERM_RECALL_MAX_SNIPPET_CHARS;
|
|
await testing.writeRawRecallStore(workspaceDir, {
|
|
version: 1,
|
|
updatedAt: "2026-04-04T00:00:00.000Z",
|
|
entries: {
|
|
contaminated: {
|
|
key: "contaminated",
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
source: "memory",
|
|
snippet: `Candidate: ${"x".repeat(maxSnippetChars + 100)} confidence: 9 evidence: memory/.dreams/session-corpus/2026-04-01.txt status: staged recalls: 1`,
|
|
recallCount: 1,
|
|
dailyCount: 0,
|
|
groundedCount: 0,
|
|
totalScore: 1,
|
|
maxScore: 0.75,
|
|
firstRecalledAt: "2026-04-01T00:00:00.000Z",
|
|
lastRecalledAt: "2026-04-01T00:00:00.000Z",
|
|
queryHashes: ["q"],
|
|
recallDays: ["2026-04-01"],
|
|
conceptTags: [],
|
|
},
|
|
},
|
|
});
|
|
|
|
const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
|
|
|
|
expect(repair.changed).toBe(true);
|
|
expect(repair.removedInvalidEntries).toBe(1);
|
|
expect(await readRecallStoreEntries(workspaceDir)).toEqual({});
|
|
});
|
|
});
|
|
|
|
it("leaves empty recall stores normalized without rewriting", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
|
|
|
|
expect(repair.changed).toBe(false);
|
|
expect(repair.rewroteStore).toBe(false);
|
|
const store = await testing.readRecallStore(workspaceDir, new Date().toISOString());
|
|
expect(store.version).toBe(1);
|
|
expect(store.entries).toEqual({});
|
|
});
|
|
});
|
|
|
|
it("does not rewrite an already normalized healthy recall store", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const snippet = "Gateway host uses qmd vector search for router notes.";
|
|
const raw = {
|
|
version: 1,
|
|
updatedAt: "2026-04-04T00:00:00.000Z",
|
|
entries: {
|
|
good: {
|
|
key: "good",
|
|
path: "memory/2026-04-01.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
source: "memory",
|
|
snippet,
|
|
recallCount: 2,
|
|
dailyCount: 0,
|
|
groundedCount: 0,
|
|
totalScore: 1.8,
|
|
maxScore: 0.95,
|
|
firstRecalledAt: "2026-04-01T00:00:00.000Z",
|
|
lastRecalledAt: "2026-04-04T00:00:00.000Z",
|
|
queryHashes: ["a", "b"],
|
|
recallDays: ["2026-04-04"],
|
|
conceptTags: testing.deriveConceptTags({
|
|
path: "memory/2026-04-01.md",
|
|
snippet,
|
|
}),
|
|
},
|
|
},
|
|
};
|
|
await testing.writeRawRecallStore(workspaceDir, raw);
|
|
|
|
const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
|
|
|
|
expect(repair.changed).toBe(false);
|
|
expect(repair.rewroteStore).toBe(false);
|
|
expect(await testing.readRecallStore(workspaceDir, new Date().toISOString())).toEqual(raw);
|
|
});
|
|
});
|
|
|
|
it("waits for an active short-term lock before repairing", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await testing.writeRawRecallStore(workspaceDir, {
|
|
version: 1,
|
|
updatedAt: "2026-04-04T00:00:00.000Z",
|
|
entries: {
|
|
bad: {
|
|
path: "",
|
|
},
|
|
},
|
|
});
|
|
await testing.writeShortTermLock(workspaceDir, {
|
|
owner: `${process.pid}:${Date.now()}`,
|
|
acquiredAt: Date.now(),
|
|
});
|
|
|
|
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
try {
|
|
let settled = false;
|
|
const repairPromise = repairShortTermPromotionArtifacts({ workspaceDir }).then((result) => {
|
|
settled = true;
|
|
return result;
|
|
});
|
|
|
|
await vi.advanceTimersByTimeAsync(41);
|
|
expect(settled).toBe(false);
|
|
|
|
await testing.deleteShortTermLock(workspaceDir);
|
|
await vi.advanceTimersByTimeAsync(40);
|
|
const repair = await repairPromise;
|
|
|
|
expect(repair.changed).toBe(true);
|
|
expect(repair.rewroteStore).toBe(true);
|
|
expect(repair.removedInvalidEntries).toBe(1);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
});
|
|
|
|
it("reports stale sqlite locks as repairable audit issues", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await testing.writeShortTermLock(workspaceDir, {
|
|
owner: "999999:0",
|
|
acquiredAt: Date.now() - 120_000,
|
|
});
|
|
const audit = await auditShortTermPromotionArtifacts({ workspaceDir });
|
|
expect(audit.issues.find((issue) => issue.code === "recall-lock-stale")).toStrictEqual({
|
|
severity: "warn",
|
|
code: "recall-lock-stale",
|
|
message: "Short-term promotion lock appears stale.",
|
|
fixable: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
it("reports concept tag script coverage for multilingual recalls", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "routeur glacier",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.93,
|
|
snippet: "Configuration du routeur et sauvegarde Glacier.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "router cjk",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-04.md",
|
|
startLine: 1,
|
|
endLine: 2,
|
|
score: 0.95,
|
|
snippet: "障害対応ルーター設定とバックアップ確認。",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const audit = await auditShortTermPromotionArtifacts({ workspaceDir });
|
|
expect(audit.conceptTaggedEntryCount).toBe(2);
|
|
expect(audit.conceptTagScripts).toEqual({
|
|
latinEntryCount: 1,
|
|
cjkEntryCount: 1,
|
|
mixedEntryCount: 0,
|
|
otherEntryCount: 0,
|
|
});
|
|
});
|
|
});
|
|
|
|
it("extracts stable concept tags from snippets and paths", () => {
|
|
expect(
|
|
testing.deriveConceptTags({
|
|
path: "memory/2026-04-03.md",
|
|
snippet: "Move backups to S3 Glacier and sync QMD router notes.",
|
|
}),
|
|
// "s3" is a protected-glossary term; it now surfaces as a standalone token past the
|
|
// per-script min-length gate (the longer terms still match as substrings).
|
|
).toStrictEqual(["backup", "backups", "glacier", "qmd", "router", "s3", "sync"]);
|
|
});
|
|
|
|
it("extracts multilingual concept tags across latin and cjk snippets", () => {
|
|
expect(
|
|
testing.deriveConceptTags({
|
|
path: "memory/2026-04-03.md",
|
|
snippet: "Configuración du routeur et sauvegarde Glacier.",
|
|
}),
|
|
).toStrictEqual(["glacier", "sauvegarde", "routeur", "configuración"]);
|
|
expect(
|
|
testing.deriveConceptTags({
|
|
path: "memory/2026-04-03.md",
|
|
snippet: "障害対応ルーター設定とバックアップ確認。路由器备份与网关同步。",
|
|
}),
|
|
).toStrictEqual([
|
|
"バックアップ",
|
|
"ルーター",
|
|
"障害対応",
|
|
"路由器",
|
|
"备份",
|
|
"网关",
|
|
"障害",
|
|
"対応",
|
|
]);
|
|
});
|
|
|
|
describe("MEMORY.md budget compaction (#73691)", () => {
|
|
it("drops the oldest promoted section before write when memoryFileMaxChars would be exceeded", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
// Source daily note that the candidate references (rehydrate reads it).
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-29", [
|
|
"Notes",
|
|
"",
|
|
"Rotate the staging Postgres credentials before next deploy.",
|
|
]);
|
|
|
|
// Seed an oversized MEMORY.md with two pre-existing promotion sections.
|
|
const memoryPath = path.join(workspaceDir, "MEMORY.md");
|
|
const filler = "x".repeat(600);
|
|
const seeded = [
|
|
"# Long-Term Memory",
|
|
"",
|
|
"## Promoted From Short-Term Memory (2026-04-10)",
|
|
"<!-- openclaw-memory-promotion:legacy-old -->",
|
|
`- ${filler}`,
|
|
"",
|
|
"## Promoted From Short-Term Memory (2026-04-20)",
|
|
"<!-- openclaw-memory-promotion:legacy-newer -->",
|
|
`- ${filler}`,
|
|
"",
|
|
].join("\n");
|
|
await fs.writeFile(memoryPath, seeded, "utf-8");
|
|
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "rotate creds",
|
|
nowMs: Date.parse("2026-04-29T10:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-29.md",
|
|
startLine: 3,
|
|
endLine: 3,
|
|
score: 0.96,
|
|
snippet: "Rotate the staging Postgres credentials before next deploy.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs: Date.parse("2026-04-29T10:00:00.000Z"),
|
|
memoryFileMaxChars: 1_400,
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
expect(applied.compactedSections).toBeGreaterThan(0);
|
|
expect(applied.compactedDates).toContain("2026-04-10");
|
|
|
|
const memoryText = await fs.readFile(memoryPath, "utf-8");
|
|
expect(memoryText).not.toContain("(2026-04-10)");
|
|
expect(memoryText).not.toContain("legacy-old");
|
|
// Newer pre-existing section + the freshly-written one survive.
|
|
expect(memoryText).toContain("Rotate the staging Postgres credentials");
|
|
});
|
|
});
|
|
|
|
it("leaves MEMORY.md untouched when total stays within memoryFileMaxChars", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-29", [
|
|
"Notes",
|
|
"",
|
|
"A short snippet that fits comfortably.",
|
|
]);
|
|
|
|
const memoryPath = path.join(workspaceDir, "MEMORY.md");
|
|
const seeded = "# Long-Term Memory\n\nSome small existing content.\n";
|
|
await fs.writeFile(memoryPath, seeded, "utf-8");
|
|
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "tiny note",
|
|
nowMs: Date.parse("2026-04-29T10:00:00.000Z"),
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-29.md",
|
|
startLine: 3,
|
|
endLine: 3,
|
|
score: 0.92,
|
|
snippet: "A short snippet that fits comfortably.",
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
memoryFileMaxChars: 10_000,
|
|
});
|
|
|
|
expect(applied.compactedSections).toBe(0);
|
|
expect(applied.compactedDates).toEqual([]);
|
|
const memoryText = await fs.readFile(memoryPath, "utf-8");
|
|
expect(memoryText).toContain("Some small existing content.");
|
|
});
|
|
});
|
|
});
|
|
it("shows signalCount instead of just recallCount in promotion annotations", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const nowMs = Date.parse("2026-05-28T10:00:00.000Z");
|
|
const snippet = "Entry with dailyCount signals but zero recallCount.";
|
|
await writeDailyMemoryNote(workspaceDir, "2026-05-28", [snippet]);
|
|
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "test signal count display",
|
|
nowMs,
|
|
results: [
|
|
{
|
|
path: "memory/2026-05-28.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.85,
|
|
snippet,
|
|
source: "memory",
|
|
},
|
|
],
|
|
});
|
|
|
|
const store = await testing.readRecallStore(workspaceDir, new Date(nowMs).toISOString());
|
|
const entryKey = expectDefined(Object.keys(store.entries)[0], "signal-count recall key");
|
|
const entry = expectDefined(store.entries[entryKey], "signal-count recall entry");
|
|
entry.dailyCount = 6;
|
|
entry.recallCount = 0;
|
|
entry.groundedCount = 1;
|
|
await testing.writeRawRecallStore(workspaceDir, store);
|
|
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs,
|
|
});
|
|
|
|
expect(ranked.length).toBe(1);
|
|
const rankedResult = expectDefined(ranked[0], "signal-count ranking result");
|
|
expect(rankedResult.recallCount).toBe(0);
|
|
expect(rankedResult.dailyCount).toBe(6);
|
|
expect(rankedResult.groundedCount).toBe(1);
|
|
expect(rankedResult.signalCount).toBe(7);
|
|
|
|
const applied = await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
nowMs,
|
|
});
|
|
|
|
expect(applied.applied).toBe(1);
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
|
|
expect(memoryText).toContain("signals=7");
|
|
expect(memoryText).toContain("recalls=0");
|
|
expect(memoryText).not.toMatch(/recalls=7/);
|
|
});
|
|
});
|
|
|
|
describe("UTF-16 snippet bounds", () => {
|
|
it("stores a complete-code-point short-term recall snippet", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const prefix = "y".repeat(testing.SHORT_TERM_RECALL_MAX_SNIPPET_CHARS - 1);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "utf16 recall",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
source: "memory",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet: `${prefix}🚀tail`,
|
|
},
|
|
],
|
|
});
|
|
|
|
const entries = Object.values(await readRecallStoreEntries(workspaceDir));
|
|
expect(entries).toHaveLength(1);
|
|
const entry = expectDefined(entries[0], "UTF-16 recall entry");
|
|
expect(readEntrySnippet(entry)).toBe(prefix);
|
|
});
|
|
});
|
|
|
|
it("writes a complete-code-point promoted MEMORY.md snippet", async () => {
|
|
await withTempWorkspace(async (workspaceDir) => {
|
|
const prefix = "a".repeat(7);
|
|
const snippet = `${prefix}🚀tail`;
|
|
await writeDailyMemoryNote(workspaceDir, "2026-04-03", [snippet]);
|
|
await recordShortTermRecalls({
|
|
workspaceDir,
|
|
query: "utf16 promotion",
|
|
results: [
|
|
{
|
|
path: "memory/2026-04-03.md",
|
|
source: "memory",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 0.9,
|
|
snippet,
|
|
},
|
|
],
|
|
});
|
|
const ranked = await rankShortTermPromotionCandidates({
|
|
workspaceDir,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
});
|
|
|
|
await applyShortTermPromotions({
|
|
workspaceDir,
|
|
candidates: ranked,
|
|
minScore: 0,
|
|
minRecallCount: 0,
|
|
minUniqueQueries: 0,
|
|
maxPromotedSnippetTokens: 2,
|
|
});
|
|
|
|
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
|
|
expect(memoryText).toContain(`- ${prefix}... [`);
|
|
expect(memoryText).not.toContain("🚀");
|
|
});
|
|
});
|
|
});
|
|
});
|