


















@@ -89,6 +89,28 @@ describe("short-term promotion", () => {
8989return candidate.promotedAt;
9090}
919192+async function readRecallStoreEntries(
93+workspaceDir: string,
94+): Promise<
95+Record<
96+string,
97+{ claimHash?: unknown; recallCount?: unknown; snippet?: unknown; totalScore?: unknown }
98+>
99+> {
100+const raw = await fs.readFile(resolveShortTermRecallStorePath(workspaceDir), "utf-8");
101+const store = JSON.parse(raw) as {
102+entries?: Record<
103+string,
104+{ claimHash?: unknown; recallCount?: unknown; snippet?: unknown; totalScore?: unknown }
105+>;
106+};
107+return store.entries ?? {};
108+}
109+110+function readEntrySnippet(entry: { snippet?: unknown }): string {
111+return typeof entry.snippet === "string" ? entry.snippet : "";
112+}
113+92114async function expectEnoent(promise: Promise<unknown>): Promise<void> {
93115await expect(promise).rejects.toHaveProperty("code", "ENOENT");
94116}
@@ -179,6 +201,89 @@ describe("short-term promotion", () => {
179201});
180202});
181203204+it("caps short-term recall store entries and snippets during normal recording", async () => {
205+await withTempWorkspace(async (workspaceDir) => {
206+const maxEntries = testing.SHORT_TERM_RECALL_MAX_ENTRIES;
207+const maxSnippetChars = testing.SHORT_TERM_RECALL_MAX_SNIPPET_CHARS;
208+await recordShortTermRecalls({
209+ workspaceDir,
210+query: "bounded recall",
211+results: Array.from({ length: maxEntries + 5 }, (_, index) => ({
212+path: "memory/2026-04-03.md",
213+source: "memory" as const,
214+startLine: index + 1,
215+endLine: index + 1,
216+score: 0.1 + index / (maxEntries + 5),
217+snippet: `Recall entry ${index} ${"x".repeat(maxSnippetChars + 100)}`,
218+})),
219+});
220+221+const entries = Object.values(await readRecallStoreEntries(workspaceDir));
222+expect(entries).toHaveLength(maxEntries);
223+expect(entries.every((entry) => readEntrySnippet(entry).length <= maxSnippetChars)).toBe(
224+true,
225+);
226+expect(entries.some((entry) => readEntrySnippet(entry).startsWith("Recall entry 0 "))).toBe(
227+false,
228+);
229+expect(
230+entries.some((entry) =>
231+readEntrySnippet(entry).startsWith(`Recall entry ${maxEntries + 4} `),
232+),
233+).toBe(true);
234+});
235+});
236+237+it("keeps long-snippet claim identity stable while storing capped snippets", async () => {
238+await withTempWorkspace(async (workspaceDir) => {
239+const maxSnippetChars = testing.SHORT_TERM_RECALL_MAX_SNIPPET_CHARS;
240+const longSnippet = `Stable claim identity ${"x".repeat(maxSnippetChars + 100)}`;
241+const claimHash = testing.buildClaimHash(longSnippet);
242+243+await recordGroundedShortTermCandidates({
244+ workspaceDir,
245+query: "__dreaming_grounded_backfill__",
246+items: [
247+{
248+path: "memory/2026-04-03.md",
249+startLine: 1,
250+endLine: 1,
251+snippet: longSnippet,
252+score: 0.9,
253+query: "__dreaming_grounded_backfill__:candidate",
254+signalCount: 1,
255+dayBucket: "2026-04-03",
256+},
257+],
258+nowMs: Date.parse("2026-04-03T10:00:00.000Z"),
259+});
260+261+await recordShortTermRecalls({
262+ workspaceDir,
263+query: "stable claim",
264+nowMs: Date.parse("2026-04-03T11:00:00.000Z"),
265+results: [
266+{
267+path: "memory/2026-04-03.md",
268+source: "memory",
269+startLine: 1,
270+endLine: 1,
271+score: 0.8,
272+snippet: longSnippet,
273+},
274+],
275+});
276+277+const entries = Object.entries(await readRecallStoreEntries(workspaceDir));
278+expect(entries).toHaveLength(1);
279+const [key, entry] = entries[0];
280+expect(key.endsWith(`:${claimHash}`)).toBe(true);
281+expect(entry.claimHash).toBe(claimHash);
282+expect(entry.recallCount).toBe(1);
283+expect(readEntrySnippet(entry).length).toBeLessThanOrEqual(maxSnippetChars);
284+});
285+});
286+182287it("ignores dream report paths when recording short-term recalls", async () => {
183288await withTempWorkspace(async (workspaceDir) => {
184289await recordShortTermRecalls({
@@ -1644,6 +1749,50 @@ describe("short-term promotion", () => {
16441749});
16451750});
164617511752+it("keeps rehydrated promotion snippets capped in the recall store", async () => {
1753+await withTempWorkspace(async (workspaceDir) => {
1754+const maxSnippetChars = testing.SHORT_TERM_RECALL_MAX_SNIPPET_CHARS;
1755+const longSnippet = `Moved backup policy ${"x".repeat(maxSnippetChars + 100)}`;
1756+await writeDailyMemoryNote(workspaceDir, "2026-04-01", ["intro", longSnippet]);
1757+await recordShortTermRecalls({
1758+ workspaceDir,
1759+query: "backup policy",
1760+results: [
1761+{
1762+path: "memory/2026-04-01.md",
1763+startLine: 1,
1764+endLine: 1,
1765+score: 0.94,
1766+snippet: longSnippet,
1767+source: "memory",
1768+},
1769+],
1770+});
1771+1772+const ranked = await rankShortTermPromotionCandidates({
1773+ workspaceDir,
1774+minScore: 0,
1775+minRecallCount: 0,
1776+minUniqueQueries: 0,
1777+});
1778+const candidateKey = requireCandidateKey(ranked[0], "long rehydrated");
1779+const applied = await applyShortTermPromotions({
1780+ workspaceDir,
1781+candidates: ranked,
1782+minScore: 0,
1783+minRecallCount: 0,
1784+minUniqueQueries: 0,
1785+});
1786+1787+expect(applied.applied).toBe(1);
1788+expect(applied.appliedCandidates[0]?.snippet.length).toBeGreaterThan(maxSnippetChars);
1789+const entries = await readRecallStoreEntries(workspaceDir);
1790+const storedSnippet = readEntrySnippet(entries[candidateKey] ?? {});
1791+expect(storedSnippet.length).toBeLessThanOrEqual(maxSnippetChars);
1792+expect(storedSnippet).toBe(applied.appliedCandidates[0]?.snippet.slice(0, maxSnippetChars));
1793+});
1794+});
1795+16471796it("prefers the nearest matching snippet when the same text appears multiple times", async () => {
16481797await withTempWorkspace(async (workspaceDir) => {
16491798await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
@@ -1884,6 +2033,165 @@ describe("short-term promotion", () => {
18842033});
18852034});
188620352036+it("audits and repairs oversized recall stores", async () => {
2037+await withTempWorkspace(async (workspaceDir) => {
2038+const maxEntries = testing.SHORT_TERM_RECALL_MAX_ENTRIES;
2039+const maxSnippetChars = testing.SHORT_TERM_RECALL_MAX_SNIPPET_CHARS;
2040+const storePath = resolveShortTermRecallStorePath(workspaceDir);
2041+await fs.writeFile(
2042+storePath,
2043+`${JSON.stringify(
2044+ {
2045+ version: 1,
2046+ updatedAt: "2026-04-04T00:00:00.000Z",
2047+ entries: Object.fromEntries(
2048+ Array.from({ length: maxEntries + 3 }, (_, index) => [
2049+ `entry-${index}`,
2050+ {
2051+ key: `entry-${index}`,
2052+ path: "memory/2026-04-01.md",
2053+ startLine: index + 1,
2054+ endLine: index + 1,
2055+ source: "memory",
2056+ snippet: `Oversized recall ${index} ${"x".repeat(maxSnippetChars + 100)}`,
2057+ recallCount: 1,
2058+ dailyCount: 0,
2059+ groundedCount: 0,
2060+ totalScore: index,
2061+ maxScore: 0.75,
2062+ firstRecalledAt: "2026-04-01T00:00:00.000Z",
2063+ lastRecalledAt: new Date(
2064+ Date.parse("2026-04-01T00:00:00.000Z") + index,
2065+ ).toISOString(),
2066+ queryHashes: [`q-${index}`],
2067+ recallDays: ["2026-04-01"],
2068+ conceptTags: [],
2069+ },
2070+ ]),
2071+ ),
2072+ },
2073+ null,
2074+ 2,
2075+ )}\n`,
2076+"utf-8",
2077+);
2078+2079+const auditBefore = await auditShortTermPromotionArtifacts({ workspaceDir });
2080+expect(auditBefore.entryCount).toBe(maxEntries + 3);
2081+expect(auditBefore.issues.map((issue) => issue.code)).toContain("recall-store-over-limit");
2082+2083+const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
2084+2085+expect(repair.changed).toBe(true);
2086+expect(repair.rewroteStore).toBe(true);
2087+expect(repair.removedOverflowEntries).toBe(3);
2088+2089+const entries = Object.values(await readRecallStoreEntries(workspaceDir));
2090+expect(entries).toHaveLength(maxEntries);
2091+expect(entries.every((entry) => readEntrySnippet(entry).length <= maxSnippetChars)).toBe(
2092+true,
2093+);
2094+expect(
2095+entries.some((entry) => readEntrySnippet(entry).startsWith("Oversized recall 0 ")),
2096+).toBe(false);
2097+});
2098+});
2099+2100+it("uses score tie-breakers when capping stores with invalid timestamps", async () => {
2101+await withTempWorkspace(async (workspaceDir) => {
2102+const maxEntries = testing.SHORT_TERM_RECALL_MAX_ENTRIES;
2103+const storePath = resolveShortTermRecallStorePath(workspaceDir);
2104+await fs.writeFile(
2105+storePath,
2106+`${JSON.stringify(
2107+ {
2108+ version: 1,
2109+ updatedAt: "2026-04-04T00:00:00.000Z",
2110+ entries: Object.fromEntries(
2111+ Array.from({ length: maxEntries + 3 }, (_, index) => [
2112+ `entry-${index}`,
2113+ {
2114+ key: `entry-${index}`,
2115+ path: "memory/2026-04-01.md",
2116+ startLine: index + 1,
2117+ endLine: index + 1,
2118+ source: "memory",
2119+ snippet: `Invalid timestamp recall ${index}`,
2120+ recallCount: 1,
2121+ dailyCount: 0,
2122+ groundedCount: 0,
2123+ totalScore: index,
2124+ maxScore: 0.75,
2125+ firstRecalledAt: "not-a-date",
2126+ lastRecalledAt: "not-a-date",
2127+ queryHashes: [`q-${index}`],
2128+ recallDays: ["2026-04-01"],
2129+ conceptTags: [],
2130+ },
2131+ ]),
2132+ ),
2133+ },
2134+ null,
2135+ 2,
2136+ )}\n`,
2137+"utf-8",
2138+);
2139+2140+const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
2141+2142+expect(repair.removedOverflowEntries).toBe(3);
2143+const entries = await readRecallStoreEntries(workspaceDir);
2144+expect(Object.keys(entries)).toHaveLength(maxEntries);
2145+expect(entries["entry-0"]).toBeUndefined();
2146+expect(entries[`entry-${maxEntries + 2}`]).toBeDefined();
2147+});
2148+});
2149+2150+it("rejects long contaminated legacy recall entries before truncating snippets", async () => {
2151+await withTempWorkspace(async (workspaceDir) => {
2152+const maxSnippetChars = testing.SHORT_TERM_RECALL_MAX_SNIPPET_CHARS;
2153+const storePath = resolveShortTermRecallStorePath(workspaceDir);
2154+await fs.writeFile(
2155+storePath,
2156+`${JSON.stringify(
2157+ {
2158+ version: 1,
2159+ updatedAt: "2026-04-04T00:00:00.000Z",
2160+ entries: {
2161+ contaminated: {
2162+ key: "contaminated",
2163+ path: "memory/2026-04-01.md",
2164+ startLine: 1,
2165+ endLine: 1,
2166+ source: "memory",
2167+ snippet: `Candidate: ${"x".repeat(maxSnippetChars + 100)} confidence: 9 evidence: memory/.dreams/session-corpus/2026-04-01.txt status: staged recalls: 1`,
2168+ recallCount: 1,
2169+ dailyCount: 0,
2170+ groundedCount: 0,
2171+ totalScore: 1,
2172+ maxScore: 0.75,
2173+ firstRecalledAt: "2026-04-01T00:00:00.000Z",
2174+ lastRecalledAt: "2026-04-01T00:00:00.000Z",
2175+ queryHashes: ["q"],
2176+ recallDays: ["2026-04-01"],
2177+ conceptTags: [],
2178+ },
2179+ },
2180+ },
2181+ null,
2182+ 2,
2183+ )}\n`,
2184+"utf-8",
2185+);
2186+2187+const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
2188+2189+expect(repair.changed).toBe(true);
2190+expect(repair.removedInvalidEntries).toBe(1);
2191+expect(await readRecallStoreEntries(workspaceDir)).toEqual({});
2192+});
2193+});
2194+18872195it("repairs empty recall-store files without throwing", async () => {
18882196await withTempWorkspace(async (workspaceDir) => {
18892197const storePath = resolveShortTermRecallStorePath(workspaceDir);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。