Files
openclaw/extensions/memory-core/src/short-term-promotion.test.ts
Mariano e8209e4cf9 Memory/dreaming: feed grounded backfill into short-term promotion (#63370)
Merged via squash.

Prepared head SHA: 5dfe246ef9
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-04-08 23:31:37 +02:00

1390 lines
42 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
vi.mock("openclaw/plugin-sdk/memory-host-events", () => ({
appendMemoryHostEvent: vi.fn(async () => {}),
}));
import {
applyShortTermPromotions,
auditShortTermPromotionArtifacts,
isShortTermMemoryPath,
recordGroundedShortTermCandidates,
rankShortTermPromotionCandidates,
recordDreamingPhaseSignals,
recordShortTermRecalls,
removeGroundedShortTermCandidates,
repairShortTermPromotionArtifacts,
resolveShortTermRecallLockPath,
resolveShortTermPhaseSignalStorePath,
resolveShortTermRecallStorePath,
__testing,
} from "./short-term-promotion.js";
describe("short-term promotion", () => {
let fixtureRoot = "";
let caseId = 0;
beforeAll(async () => {
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "memory-promote-"));
});
afterAll(async () => {
if (!fixtureRoot) {
return;
}
await fs.rm(fixtureRoot, { recursive: true, force: true });
});
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;
}
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);
});
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 storePath = resolveShortTermRecallStorePath(workspaceDir);
const raw = await fs.readFile(storePath, "utf-8");
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("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 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);
expect(slowerDecay[0].score).toBeGreaterThan(fasterDecay[0].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 = baseline.find((entry) => entry.path === "memory/2026-04-02.md")?.key;
expect(boostedKey).toBeTruthy();
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");
expect(ranked[0].score).toBeGreaterThan(ranked[1].score);
const phaseStorePath = resolveShortTermPhaseSignalStorePath(workspaceDir);
const phaseStore = JSON.parse(await fs.readFile(phaseStorePath, "utf-8")) as {
entries: Record<string, { lightHits: number; remHits: number }>;
};
expect(phaseStore.entries[boostedKey!]).toMatchObject({
lightHits: 1,
remHits: 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 = rankedBaseline[0]?.key;
expect(key).toBeTruthy();
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);
expect(freshSignalRank[0].score).toBeGreaterThan(staleSignalRank[0].score);
});
});
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 storePath = resolveShortTermRecallStorePath(workspaceDir);
const rawStore = JSON.parse(await fs.readFile(storePath, "utf-8")) as {
entries: Record<string, { promotedAt?: string }>;
};
for (const entry of Object.values(rawStore.entries)) {
delete entry.promotedAt;
}
await fs.writeFile(storePath, `${JSON.stringify(rawStore, null, 2)}\n`, "utf-8");
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("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,
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("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,
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 expect(
fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8"),
).rejects.toMatchObject({
code: "ENOENT",
});
});
});
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(rankedIncludingPromoted[0]?.promotedAt).toBeTruthy();
});
});
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("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,
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 expect(fs.access(path.join(workspaceDir, "MEMORY.md"))).rejects.toMatchObject({
code: "ENOENT",
});
});
});
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) => {
const storePath = resolveShortTermRecallStorePath(workspaceDir);
await fs.writeFile(
storePath,
JSON.stringify(
{
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: "",
},
},
},
null,
2,
),
"utf-8",
);
const lockPath = path.join(workspaceDir, "memory", ".dreams", "short-term-promotion.lock");
await fs.writeFile(lockPath, "999999:0\n", "utf-8");
const staleMtime = new Date(Date.now() - 120_000);
await fs.utimes(lockPath, staleMtime, staleMtime);
const auditBefore = await auditShortTermPromotionArtifacts({ workspaceDir });
expect(auditBefore.invalidEntryCount).toBe(1);
expect(auditBefore.issues.map((issue) => issue.code)).toEqual(
expect.arrayContaining(["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 = JSON.parse(await fs.readFile(storePath, "utf-8")) as {
entries: Record<string, { conceptTags?: string[]; recallDays?: string[] }>;
};
expect(repairedRaw.entries.good?.conceptTags).toContain("router");
expect(repairedRaw.entries.good?.recallDays).toEqual(["2026-04-04"]);
});
});
it("repairs empty recall-store files without throwing", async () => {
await withTempWorkspace(async (workspaceDir) => {
const storePath = resolveShortTermRecallStorePath(workspaceDir);
await fs.writeFile(storePath, " \n", "utf-8");
const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
expect(repair.changed).toBe(true);
expect(repair.rewroteStore).toBe(true);
expect(JSON.parse(await fs.readFile(storePath, "utf-8"))).toMatchObject({
version: 1,
entries: {},
});
});
});
it("does not rewrite an already normalized healthy recall store", async () => {
await withTempWorkspace(async (workspaceDir) => {
const storePath = resolveShortTermRecallStorePath(workspaceDir);
const snippet = "Gateway host uses qmd vector search for router notes.";
const raw = `${JSON.stringify(
{
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,
}),
},
},
},
null,
2,
)}\n`;
await fs.writeFile(storePath, raw, "utf-8");
const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
expect(repair.changed).toBe(false);
expect(repair.rewroteStore).toBe(false);
const nextRaw = await fs.readFile(storePath, "utf-8");
expect(nextRaw).toBe(raw);
});
});
it("waits for an active short-term lock before repairing", async () => {
await withTempWorkspace(async (workspaceDir) => {
const storePath = resolveShortTermRecallStorePath(workspaceDir);
const lockPath = resolveShortTermRecallLockPath(workspaceDir);
await fs.writeFile(
storePath,
JSON.stringify(
{
version: 1,
updatedAt: "2026-04-04T00:00:00.000Z",
entries: {
bad: {
path: "",
},
},
},
null,
2,
),
"utf-8",
);
await fs.writeFile(lockPath, `${process.pid}:${Date.now()}\n`, "utf-8");
let settled = false;
const repairPromise = repairShortTermPromotionArtifacts({ workspaceDir }).then((result) => {
settled = true;
return result;
});
await new Promise((resolve) => setTimeout(resolve, 41));
expect(settled).toBe(false);
await fs.unlink(lockPath);
const repair = await repairPromise;
expect(repair.changed).toBe(true);
expect(repair.rewroteStore).toBe(true);
expect(repair.removedInvalidEntries).toBe(1);
});
});
it("downgrades lock inspection failures into audit issues", async () => {
await withTempWorkspace(async (workspaceDir) => {
const lockPath = path.join(workspaceDir, "memory", ".dreams", "short-term-promotion.lock");
const stat = vi.spyOn(fs, "stat").mockImplementation(async (target) => {
if (String(target) === lockPath) {
const error = Object.assign(new Error("no access"), { code: "EACCES" });
throw error;
}
return await vi
.importActual<typeof import("node:fs/promises")>("node:fs/promises")
.then((actual) => actual.stat(target));
});
try {
const audit = await auditShortTermPromotionArtifacts({ workspaceDir });
expect(audit.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "recall-lock-unreadable",
fixable: false,
}),
]),
);
} finally {
stat.mockRestore();
}
});
});
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.",
}),
).toEqual(expect.arrayContaining(["glacier", "router", "backups"]));
});
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.",
}),
).toEqual(expect.arrayContaining(["configuración", "routeur", "sauvegarde", "glacier"]));
expect(
__testing.deriveConceptTags({
path: "memory/2026-04-03.md",
snippet: "障害対応ルーター設定とバックアップ確認。路由器备份与网关同步。",
}),
).toEqual(expect.arrayContaining(["障害対応", "ルーター", "バックアップ", "路由器", "备份"]));
});
});