mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-06-02 06:19:36 +02:00
fix: P0c/P0d architect review corrections
P0c fixes:
- Chinese file count regex now accepts 個/个 between number and 文件
- Admin PIN short reference (<20 chars) passes via config value allowlist
- Phase snapshot uses semantic window (.{0,20}) instead of absolute position
P0d fixes:
- Feedback key split: 500 error and port issue remain separate entries
- extractEntityKey avoids over-merging unrelated plugin configs
- chooseBetterMemory supports supersession mode (newer beats longer)
- Sort comparator now includes source priority as secondary tie-breaker
New regression tests (11 total):
- Real Admin PIN short reference passes
- Real Chinese 37 個文件 snapshot rejected
- Real pathology Phase 1-4 snapshot rejected
- Feedback 500 vs port entries not collapsed
- Unrelated plugin configs not collapsed
- Supersession prefers newer shorter over older longer
67/67 tests pass.
This commit is contained in:
+12
-9
@@ -193,8 +193,12 @@ function shouldAcceptWorkspaceMemoryCandidate(entry: {
|
||||
}): boolean {
|
||||
const text = entry.text.trim();
|
||||
|
||||
// Too short
|
||||
if (text.length < 20) return false;
|
||||
// Too short (with type-specific allowlist for stable config values)
|
||||
if (entry.type === "reference" && /\b(?:admin\s+)?pin\s|scrypt|n=\d+|r=\d+|p=\d+/i.test(text)) {
|
||||
// Stable config values can be short — allow below generic min length
|
||||
} else if (text.length < 20) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Git history / commit hash
|
||||
if (/\b[0-9a-f]{7,40}\b/.test(text)) return false;
|
||||
@@ -219,16 +223,15 @@ function shouldAcceptWorkspaceMemoryCandidate(entry: {
|
||||
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;
|
||||
}
|
||||
if (/\b\d+\s*(?:個|个)?\s*(?:files?|文件)/i.test(text)) return false;
|
||||
// Reject "Phase N completed" using semantic window (within 20 chars either direction)
|
||||
if (/\bphase\s*\d+(?:\s*[-–]\s*\d+)?\b.{0,20}\b(?:completed|done|finished)\b/i.test(text)) return false;
|
||||
if (/\b(?:completed|done|finished)\b.{0,20}\bphase\s*\d+(?:\s*[-–]\s*\d+)?\b/i.test(text)) return false;
|
||||
if (/已完成.{0,20}Phase\s*\d+(?:\s*[-–]\s*\d+)?/i.test(text)) return false;
|
||||
if (/Phase\s*\d+(?:\s*[-–]\s*\d+)?.{0,20}已完成/i.test(text)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
+19
-4
@@ -145,7 +145,11 @@ function isPrunableByAge(entry: LongTermMemoryEntry, now: number): boolean {
|
||||
}
|
||||
|
||||
/** Choose better memory when identity/topic keys conflict */
|
||||
function chooseBetterMemory(a: LongTermMemoryEntry, b: LongTermMemoryEntry): LongTermMemoryEntry {
|
||||
function chooseBetterMemory(
|
||||
a: LongTermMemoryEntry,
|
||||
b: LongTermMemoryEntry,
|
||||
mode: "entity" | "supersession" = "entity",
|
||||
): LongTermMemoryEntry {
|
||||
// Source priority: explicit > manual > compaction
|
||||
if (sourcePriority(a.source) !== sourcePriority(b.source)) {
|
||||
return sourcePriority(a.source) > sourcePriority(b.source) ? a : b;
|
||||
@@ -154,11 +158,20 @@ function chooseBetterMemory(a: LongTermMemoryEntry, b: LongTermMemoryEntry): Lon
|
||||
if (a.confidence !== b.confidence) {
|
||||
return a.confidence > b.confidence ? a : b;
|
||||
}
|
||||
// Prefer longer (more specific) text
|
||||
// For entity dedup: longer (more specific) beats shorter
|
||||
// For supersession: newer beats older (and thus longer is not preferred)
|
||||
if (mode === "supersession") {
|
||||
// Newer wins for same-topic supersession
|
||||
if (new Date(a.createdAt).getTime() !== new Date(b.createdAt).getTime()) {
|
||||
return new Date(a.createdAt) > new Date(b.createdAt) ? a : b;
|
||||
}
|
||||
return a.text.length > b.text.length ? a : b;
|
||||
}
|
||||
// Entity mode: longer text means more specific
|
||||
if (Math.abs(a.text.length - b.text.length) > 10) {
|
||||
return a.text.length > b.text.length ? a : b;
|
||||
}
|
||||
// Freshness tie-breaker: newer wins
|
||||
// Freshness tie-breaker
|
||||
return new Date(a.createdAt) > new Date(b.createdAt) ? a : b;
|
||||
}
|
||||
|
||||
@@ -200,7 +213,7 @@ export function enforceLongTermLimits(entries: LongTermMemoryEntry[]): LongTermM
|
||||
const existing = decisionDeduped.get(key);
|
||||
if (!existing) {
|
||||
decisionDeduped.set(key, entry);
|
||||
} else if (chooseBetterMemory(entry, existing) === entry) {
|
||||
} else if (chooseBetterMemory(entry, existing, "supersession") === entry) {
|
||||
decisionDeduped.set(key, entry);
|
||||
}
|
||||
}
|
||||
@@ -217,6 +230,8 @@ export function enforceLongTermLimits(entries: LongTermMemoryEntry[]): LongTermM
|
||||
const pA = priorityWithFreshness(a);
|
||||
const pB = priorityWithFreshness(b);
|
||||
if (pB !== pA) return pB - pA;
|
||||
const sourceDiff = sourcePriority(b.source) - sourcePriority(a.source);
|
||||
if (sourceDiff !== 0) return sourceDiff;
|
||||
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
||||
})
|
||||
.slice(0, LONG_TERM_LIMITS.maxEntries);
|
||||
|
||||
@@ -221,7 +221,7 @@ Memory candidates:
|
||||
test("parseWorkspaceMemoryCandidates accepts bracketless candidate format", () => {
|
||||
const summary = `
|
||||
Memory candidates:
|
||||
- project pathology-playground 後端健康改進計劃已完成 Phase 1-4
|
||||
- project Backend health improvements organized into phased milestones
|
||||
- reference Scrypt 參數必須是 N=16384, r=8, p=1
|
||||
- feedback 端口 9473 可能被舊進程佔用,需殺掉後重啟
|
||||
- decision Use output.prompt to replace the default compaction template
|
||||
@@ -316,16 +316,48 @@ Memory candidates:
|
||||
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
|
||||
test("parseWorkspaceMemoryCandidates accepts short Admin PIN reference entry", () => {
|
||||
// Real Admin PIN is <20 chars — should pass via config value allowlist
|
||||
const summary = `
|
||||
Memory candidates:
|
||||
- reference Scrypt 參數必須是 N=16384, r=8, p=1,必須嚴格遵守
|
||||
- reference Admin PIN 456123 是系統管理員的預設登入密碼
|
||||
- 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"]);
|
||||
assert.equal(items.length, 1, "Short config reference should pass via allowlist");
|
||||
assert.equal(items[0].type, "reference");
|
||||
});
|
||||
|
||||
test("parseWorkspaceMemoryCandidates accepts Scrypt config reference", () => {
|
||||
// Scrypt parameters with numbers should pass
|
||||
const summary = `
|
||||
Memory candidates:
|
||||
- reference Scrypt 參數必須是 N=16384, r=8, p=1
|
||||
`;
|
||||
|
||||
const items = parseWorkspaceMemoryCandidates(summary);
|
||||
assert.equal(items.length, 1, "Scrypt config values should pass");
|
||||
assert.equal(items[0].type, "reference");
|
||||
});
|
||||
|
||||
test("parseWorkspaceMemoryCandidates rejects Chinese file count snapshot", () => {
|
||||
// Real Chinese file count with counter word 個
|
||||
const summary = `
|
||||
Memory candidates:
|
||||
- project USB 同步:37 個文件(bundles, server, frontend, tests, docs)
|
||||
`;
|
||||
|
||||
const items = parseWorkspaceMemoryCandidates(summary);
|
||||
assert.equal(items.length, 0, "Chinese file count with 個 should be rejected");
|
||||
});
|
||||
|
||||
test("parseWorkspaceMemoryCandidates rejects real phase snapshot mid-description", () => {
|
||||
// Real phase snapshot where Phase appears deep in the string
|
||||
const summary = `
|
||||
Memory candidates:
|
||||
- project pathology-playground 後端健康改進計劃已完成 Phase 1-4
|
||||
`;
|
||||
|
||||
const items = parseWorkspaceMemoryCandidates(summary);
|
||||
assert.equal(items.length, 0, "Phase snapshot mid-description should still be rejected");
|
||||
});
|
||||
@@ -346,4 +346,38 @@ test("enforceLongTermLimits priority: freshness used as tie-breaker among same p
|
||||
const kept = enforceLongTermLimits([older, newer]);
|
||||
assert.equal(kept.length, 1);
|
||||
assert.equal(kept[0].id, "newer", "Newer entry should win as tie-breaker");
|
||||
});
|
||||
|
||||
test("enforceLongTermLimits feedback: 500 error and port issue are NOT collapsed", () => {
|
||||
// Distinct feedback entries should remain separate
|
||||
const entries = [
|
||||
agedEntry("f1", "瀏覽器登入出現 500 internal_error,代碼邏輯正確但原因不明", "feedback", { daysAgo: 0 }),
|
||||
agedEntry("f2", "端口 9473 可能被舊進程佔用,需殺掉後重啟", "feedback", { daysAgo: 0 }),
|
||||
];
|
||||
|
||||
const kept = enforceLongTermLimits(entries);
|
||||
const feedbackEntries = kept.filter(e => e.type === "feedback");
|
||||
assert.equal(feedbackEntries.length, 2, "Distinct feedback items should not collapse");
|
||||
});
|
||||
|
||||
test("enforceLongTermLimits config: unrelated plugin configs are NOT collapsed", () => {
|
||||
const entries = [
|
||||
agedEntry("c1", "OpenCode plugin config: .opencode-agenthub/current/xdg/opencode/opencode.json in workspace", "reference", { daysAgo: 0 }),
|
||||
agedEntry("c2", "Vite plugin config location: vite.config.ts at project root", "reference", { daysAgo: 0 }),
|
||||
];
|
||||
|
||||
const kept = enforceLongTermLimits(entries);
|
||||
const refEntries = kept.filter(e => e.type === "reference");
|
||||
assert.equal(refEntries.length, 2, "Unrelated plugin configs should remain separate");
|
||||
});
|
||||
|
||||
test("enforceLongTermLimits supersession: newer shorter decision beats older longer one", () => {
|
||||
// Same topic, same source, same confidence — newer wins even if shorter
|
||||
const older = agedEntry("d1", "Parser supports 3 formats: HTML comment, Markdown section, legacy XML with backward compatibility", "decision", { daysAgo: 5 });
|
||||
const newer = agedEntry("d2", "Parser supports 4 formats", "decision", { daysAgo: 0 });
|
||||
|
||||
const kept = enforceLongTermLimits([older, newer]);
|
||||
const decisions = kept.filter(e => e.type === "decision" && /parser.*format/i.test(e.text));
|
||||
assert.equal(decisions.length, 1, "Newer shorter decision should supersede older longer one");
|
||||
assert.ok(decisions[0].text.includes("4 formats"), "Kept entry should be the newer 4-formats");
|
||||
});
|
||||
Reference in New Issue
Block a user