mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
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:
+19
-3
@@ -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
@@ -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:",
|
||||
"",
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
Reference in New Issue
Block a user