




















@@ -1126,6 +1126,79 @@ describe("short-term promotion", () => {
11261126});
11271127});
112811281129+it("does not re-append promoted candidates whose marker key path contains spaces", async () => {
1130+await withTempWorkspace(async (workspaceDir) => {
1131+await writeDailyMemoryNoteInSubdir(workspaceDir, "project alpha", "2026-04-01", [
1132+"alpha",
1133+"The project alpha gateway should stay loopback-only on port 18789.",
1134+]);
1135+await recordShortTermRecalls({
1136+ workspaceDir,
1137+query: "project alpha gateway",
1138+results: [
1139+{
1140+path: "memory/project alpha/2026-04-01.md",
1141+startLine: 2,
1142+endLine: 2,
1143+score: 0.95,
1144+snippet: "The project alpha gateway should stay loopback-only on port 18789.",
1145+source: "memory",
1146+},
1147+],
1148+});
1149+1150+const ranked = await rankShortTermPromotionCandidates({
1151+ workspaceDir,
1152+minScore: 0,
1153+minRecallCount: 0,
1154+minUniqueQueries: 0,
1155+});
1156+expect(ranked.map((candidate) => candidate.key)).toContain(
1157+"memory:memory/project alpha/2026-04-01.md:2:2",
1158+);
1159+1160+const firstApply = await applyShortTermPromotions({
1161+ workspaceDir,
1162+candidates: ranked,
1163+minScore: 0,
1164+minRecallCount: 0,
1165+minUniqueQueries: 0,
1166+});
1167+expect(firstApply.applied).toBe(1);
1168+expect(firstApply.appended).toBe(1);
1169+1170+const storePath = resolveShortTermRecallStorePath(workspaceDir);
1171+const rawStore = JSON.parse(await fs.readFile(storePath, "utf-8")) as {
1172+entries: Record<string, { promotedAt?: string }>;
1173+};
1174+for (const entry of Object.values(rawStore.entries)) {
1175+delete entry.promotedAt;
1176+}
1177+await fs.writeFile(storePath, `${JSON.stringify(rawStore, null, 2)}\n`, "utf-8");
1178+1179+const secondApply = await applyShortTermPromotions({
1180+ workspaceDir,
1181+candidates: ranked,
1182+minScore: 0,
1183+minRecallCount: 0,
1184+minUniqueQueries: 0,
1185+});
1186+expect(secondApply.applied).toBe(1);
1187+expect(secondApply.appended).toBe(0);
1188+expect(secondApply.reconciledExisting).toBe(1);
1189+1190+const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
1191+expect(memoryText).toContain(
1192+"<!-- openclaw-memory-promotion:memory:memory/project alpha/2026-04-01.md:2:2 -->",
1193+);
1194+expect(memoryText.match(/openclaw-memory-promotion:/g)?.length).toBe(1);
1195+expect(
1196+memoryText.match(/The project alpha gateway should stay loopback-only on port 18789\./g)
1197+?.length,
1198+).toBe(1);
1199+});
1200+});
1201+11291202it("filters out candidates older than maxAgeDays during ranking", async () => {
11301203await withTempWorkspace(async (workspaceDir) => {
11311204await recordShortTermRecalls({
@@ -1643,6 +1716,60 @@ describe("short-term promotion", () => {
16431716});
16441717});
164517181719+it("keeps promoted MEMORY.md entries compact while preserving provenance", async () => {
1720+await withTempWorkspace(async (workspaceDir) => {
1721+const longDailyEntry = [
1722+"HanJammer reviewed the dashboard state and asked for durable memory hygiene.",
1723+"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.",
1724+"A curated long-term memory entry should preserve the stable conclusion without hauling the whole daily journal line into bootstrap context.",
1725+"Extra filler keeps this source entry long enough to prove promotion output is bounded before it reaches the root memory file.",
1726+].join(" ");
1727+await writeDailyMemoryNote(workspaceDir, "2026-04-01", [longDailyEntry]);
1728+await recordShortTermRecalls({
1729+ workspaceDir,
1730+query: "memory hygiene",
1731+results: [
1732+{
1733+path: "memory/2026-04-01.md",
1734+startLine: 1,
1735+endLine: 1,
1736+score: 0.92,
1737+snippet: longDailyEntry,
1738+source: "memory",
1739+},
1740+],
1741+});
1742+1743+const ranked = await rankShortTermPromotionCandidates({
1744+ workspaceDir,
1745+minScore: 0,
1746+minRecallCount: 0,
1747+minUniqueQueries: 0,
1748+});
1749+const applied = await applyShortTermPromotions({
1750+ workspaceDir,
1751+candidates: ranked,
1752+minScore: 0,
1753+minRecallCount: 0,
1754+minUniqueQueries: 0,
1755+maxPromotedSnippetTokens: 55,
1756+});
1757+1758+expect(applied.applied).toBe(1);
1759+const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
1760+const promotedLine = memoryText
1761+.split("\n")
1762+.find((line) => line.startsWith("- HanJammer reviewed the dashboard state"));
1763+expect(promotedLine).toBeDefined();
1764+expect(promotedLine?.length).toBeLessThan(340);
1765+expect(promotedLine).toContain("...");
1766+expect(promotedLine).toMatch(
1767+/\[score=0\.\d{3} recalls=1 avg=0\.\d{3} source=memory\/2026-04-01\.md:1-1\]/,
1768+);
1769+expect(memoryText).toMatch(/<!-- openclaw-memory-promotion:[^\n]+ -->/);
1770+});
1771+});
1772+16461773it("does not re-append candidates that were promoted in a prior run", async () => {
16471774await withTempWorkspace(async (workspaceDir) => {
16481775await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。