fix: parser accepts bracketless format, rejects project snapshots, adds durable-content prompt

P0a: Parser now accepts both - [type] text and - type text formats
P0b: Prompt adds durable-content guidance to avoid session-specific snapshots
P0c: Parser quality gate rejects exact test counts, file counts, phase progress
- Only rejects phase progress when it appears early in the string (snapshot)
- Stable config values with numbers (Admin PIN, Scrypt) still pass
- Adds 7 new tests covering bracketless parsing and snapshot rejection
This commit is contained in:
Ralph Chang
2026-04-26 16:28:55 +08:00
parent ca71c20a8f
commit f9acfd6136
3 changed files with 140 additions and 5 deletions
+19 -3
View File
@@ -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
+9 -2
View File
@@ -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:",
"",
+112
View File
@@ -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"]);
});