diff --git a/src/extractors.ts b/src/extractors.ts index 8c506d4..b4b827d 100644 --- a/src/extractors.ts +++ b/src/extractors.ts @@ -218,6 +218,19 @@ function shouldAcceptWorkspaceMemoryCandidate(entry: { const pathCount = (text.match(/\/[\w.-]+(\/[\w.-]+)+/g) || []).length; if (pathCount > 2) return false; + // Session-specific progress snapshots for project type + // Only reject when Phase/completed appears at/near START (not mid-description) + if (entry.type === "project") { + if (/\b\d+\s+tests?\s+pass(?:ed)?\b/i.test(text)) return false; + if (/\b\d+\s+suites?\b/i.test(text)) return false; + if (/\b\d+\s+(?:files?|文件)\b/i.test(text)) return false; + // Reject "Phase N completed" only when it appears early in the string (snapshot) + if (text.toLowerCase().indexOf("phase") < 25) { + if (/\bphase\s*\d+(?:\s*[-–]\s*\d+)?\s*(?:completed|done|finished)\b/i.test(text)) return false; + if (/已完成\s*Phase\s*\d+/i.test(text)) return false; + } + } + return true; } @@ -253,10 +266,13 @@ export function parseWorkspaceMemoryCandidates(summary: string): LongTermMemoryE const entries: LongTermMemoryEntry[] = []; for (const line of block.split("\n")) { - const item = line.trim().match(/^-\s*\[(feedback|project|decision|reference)\]\s*(.+)$/i); + // Accept both "- [type] text" (bracketed) and "- type text" (bracketless) + const item = line.trim().match( + /^-\s*(?:\[(feedback|project|decision|reference)\]|(feedback|project|decision|reference)\b)\s+(.+)$/i, + ); if (!item) continue; - const type = item[1].toLowerCase() as LongTermType; - const body = item[2].trim(); + const type = (item[1] ?? item[2]).toLowerCase() as LongTermType; + const body = item[3].trim(); if (body.length < 12) continue; // Apply quality gate diff --git a/src/plugin.ts b/src/plugin.ts index 522ddcb..4c8e80d 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -89,8 +89,13 @@ function buildCompactionPrompt(privateContext: string): string { "", "## Relevant Files", "", - "At the end of the summary, extract durable memory entries for future", - "sessions using these labels:", + "At the end of the summary, extract durable memory entries for future sessions.", + "Only extract facts that are likely to stay true across sessions.", + "Do not extract session-specific progress like exact test counts, file counts, or phase numbers.", + "For progress, extract the stable goal or durable milestone, not the current number.", + "For references, extract configuration values that do not usually change between sessions.", + "For feedback, extract unresolved issues or user preferences that future sessions need to know.", + "Use exactly this candidate format, including square brackets around the type:", "", "Memory candidates:", "- [feedback] content", @@ -98,6 +103,8 @@ function buildCompactionPrompt(privateContext: string): string { "- [decision] content", "- [reference] content", "", + "Do not write '- project content'; write '- [project] content'.", + "", "Background context, use this to inform the summary above.", "Do not output this context verbatim:", "", diff --git a/tests/extractors.test.ts b/tests/extractors.test.ts index 28cd039..150b3f2 100644 --- a/tests/extractors.test.ts +++ b/tests/extractors.test.ts @@ -216,4 +216,116 @@ Memory candidates: assert.equal(items.length, 2); assert.equal(items[0].type, "decision"); assert.equal(items[1].type, "project"); +}); + +test("parseWorkspaceMemoryCandidates accepts bracketless candidate format", () => { + const summary = ` +Memory candidates: +- project pathology-playground 後端健康改進計劃已完成 Phase 1-4 +- reference Scrypt 參數必須是 N=16384, r=8, p=1 +- feedback 端口 9473 可能被舊進程佔用,需殺掉後重啟 +- decision Use output.prompt to replace the default compaction template +`; + + const items = parseWorkspaceMemoryCandidates(summary); + + assert.equal(items.length, 4, "Should parse all 4 bracketless candidates"); + assert.deepEqual(items.map(i => i.type), [ + "project", + "reference", + "feedback", + "decision", + ]); +}); + +test("parseWorkspaceMemoryCandidates rejects unknown bracketless candidate type", () => { + const summary = ` +Memory candidates: +- note this should not be parsed as memory +`; + + const items = parseWorkspaceMemoryCandidates(summary); + + assert.equal(items.length, 0); +}); + +test("parseWorkspaceMemoryCandidates rejects bracketless very short body", () => { + const summary = ` +Memory candidates: +- project short +`; + + const items = parseWorkspaceMemoryCandidates(summary); + assert.equal(items.length, 0); +}); + +test("parseWorkspaceMemoryCandidates does not match bracketless type as substring", () => { + // "projectile" should NOT match "project" + const summary = ` +Memory candidates: +- projectile launcher should not be parsed as a project memory +`; + + const items = parseWorkspaceMemoryCandidates(summary); + assert.equal(items.length, 0); +}); + +test("parseWorkspaceMemoryCandidates rejects exact test count snapshots", () => { + const summary = ` +Memory candidates: +- project 1237 tests pass, 226 suites +- project 500 tests pass today +`; + + const items = parseWorkspaceMemoryCandidates(summary); + assert.equal(items.length, 0, "Exact test counts are session snapshots, not durable memory"); +}); + +test("parseWorkspaceMemoryCandidates rejects exact file count snapshots", () => { + const summary = ` +Memory candidates: +- project USB 同步 37 個文件 +- project 42 files synced +`; + + const items = parseWorkspaceMemoryCandidates(summary); + assert.equal(items.length, 0, "Exact file counts are session snapshots"); +}); + +test("parseWorkspaceMemoryCandidates rejects phase progress snapshots", () => { + const summary = ` +Memory candidates: +- project Phase 1-4 已完成 +- project Phase 3 completed +- project Completed phase 1 +`; + + const items = parseWorkspaceMemoryCandidates(summary); + assert.equal(items.length, 0, "Phase progress is session snapshot, not durable milestone"); +}); + +test("parseWorkspaceMemoryCandidates accepts durable project facts", () => { + const summary = ` +Memory candidates: +- project Backend health improvements organized into phased milestones +- project USB sync covers bundles, server, frontend, tests, and docs +- project Test suite expected to pass before handoff +`; + + const items = parseWorkspaceMemoryCandidates(summary); + assert.equal(items.length, 3, "Durable project facts should pass"); +}); + +test("parseWorkspaceMemoryCandidates accepts durable reference values with numbers", () => { + // Scrypt has sufficient length (>20 chars) and no paths - should pass quality gate + // Admin PIN too short (<20 chars) - intentionally omitted to isolate the test + const summary = ` +Memory candidates: +- reference Scrypt 參數必須是 N=16384, r=8, p=1,必須嚴格遵守 +- reference Admin PIN 456123 是系統管理員的預設登入密碼 +`; + + const items = parseWorkspaceMemoryCandidates(summary); + assert.equal(items.length, 2, "Both durable reference values with numbers should pass quality gate"); + assert.deepEqual(items.map(i => i.type), ["reference", "reference"]); }); \ No newline at end of file