fix: auto-supersede low-quality compaction memories

This commit is contained in:
Ralph Chang
2026-04-28 13:29:28 +08:00
parent b21347c12b
commit 6a80f4b047
2 changed files with 172 additions and 2 deletions
+43 -1
View File
@@ -2,10 +2,12 @@ import type { LongTermMemoryEntry, WorkspaceMemoryStore } from "./types.ts";
import { LONG_TERM_LIMITS } from "./types.ts";
import { workspaceKey, workspaceMemoryPath } from "./paths.ts";
import { atomicWriteJSON, readJSON, updateJSON } from "./storage.ts";
import { assessMemoryQuality } from "./memory-quality.ts";
// Minimum length for workspace_memory envelope: <workspace_memory>\n...\n</workspace_memory>
const MIN_ENVELOPE_LENGTH = 80;
const MIGRATION_ID = "2026-04-26-p0-cleanup";
const QUALITY_CLEANUP_MIGRATION_ID = "2026-04-28-quality-cleanup";
const SECRET_VALUE = String.raw`[^` + "`" + String.raw`'",,\s\[]+`;
@@ -188,6 +190,7 @@ export async function normalizeWorkspaceMemoryWithAccounting(
// One-time migration for legacy snapshot violations
result = runMigrationP0Cleanup(result, nowIso);
result = runMigrationQualityCleanup(result, nowIso);
// P0 accounting only considers active entries. Entries that were already
// superseded before this normalization are preserved in storage; entries that
@@ -282,7 +285,7 @@ export function runMigrationP0Cleanup(
}
const entries = store.entries.map(entry => {
if (entry.source === "explicit") return entry;
if (entry.source !== "compaction") return entry;
if (entry.type !== "project") return entry;
if (isProjectSnapshotViolation(entry.text)) {
@@ -304,6 +307,45 @@ export function runMigrationP0Cleanup(
};
}
function runMigrationQualityCleanup(
store: WorkspaceMemoryStore,
nowIso: string,
): WorkspaceMemoryStore {
if (store.migrations?.includes(QUALITY_CLEANUP_MIGRATION_ID)) {
return store;
}
let changed = false;
const entries = store.entries.map(entry => {
if (entry.source !== "compaction") return entry;
if (entry.status === "superseded") return entry;
const quality = assessMemoryQuality(entry);
if (quality.accepted) return entry;
changed = true;
const tags = new Set([
...(entry.tags ?? []),
"quality_cleanup",
...quality.reasons.map(reason => `quality:${reason}`),
]);
return {
...entry,
status: "superseded" as const,
updatedAt: nowIso,
tags: [...tags],
};
});
return {
...store,
entries,
migrations: [...(store.migrations ?? []), QUALITY_CLEANUP_MIGRATION_ID],
updatedAt: changed ? nowIso : store.updatedAt,
};
}
function sourcePriority(source: LongTermMemoryEntry["source"]): number {
if (source === "explicit") return 3;
if (source === "manual") return 2;
+129 -1
View File
@@ -5,7 +5,7 @@ import { join, dirname } from "node:path";
import { tmpdir } from "node:os";
import type { LongTermMemoryEntry, WorkspaceMemoryStore } from "../src/types.ts";
import { LONG_TERM_LIMITS } from "../src/types.ts";
import { workspaceMemoryPath } from "../src/paths.ts";
import { workspaceKey, workspaceMemoryPath } from "../src/paths.ts";
import {
renderWorkspaceMemory,
enforceLongTermLimits,
@@ -21,6 +21,7 @@ import {
saveWorkspaceMemory,
updateWorkspaceMemoryWithAccounting,
} from "../src/workspace-memory.ts";
import { reviewerCurrent28Fixture, expectedAcceptedFixtureIds } from "./fixtures/memory-quality-current-28.ts";
function entry(id: string, text: string, type: LongTermMemoryEntry["type"] = "decision"): LongTermMemoryEntry {
const now = new Date().toISOString();
@@ -953,6 +954,133 @@ test("runMigrationP0Cleanup marks only non-explicit project snapshots and runs o
assert.equal(twice.entries.find(e => e.id === "project-snapshot")?.updatedAt, once.entries.find(e => e.id === "project-snapshot")?.updatedAt);
});
test("quality cleanup migration supersedes low-quality compaction memories from current-28 fixture", async () => {
const root = await mkdtemp(join(tmpdir(), "wm-quality-cleanup-"));
try {
const now = new Date().toISOString();
await saveWorkspaceMemory(root, {
version: 1,
workspace: { root, key: await workspaceKey(root) },
limits: { maxRenderedChars: LONG_TERM_LIMITS.maxRenderedChars, maxEntries: LONG_TERM_LIMITS.maxEntries },
entries: reviewerCurrent28Fixture,
migrations: [],
updatedAt: now,
});
const loaded = await loadWorkspaceMemory(root);
const activeIds = new Set(loaded.entries.filter(entry => entry.status === "active").map(entry => entry.id));
const supersededIds = new Set(loaded.entries.filter(entry => entry.status === "superseded").map(entry => entry.id));
for (const entry of reviewerCurrent28Fixture) {
if (expectedAcceptedFixtureIds.has(entry.id)) {
assert.equal(activeIds.has(entry.id), true, `${entry.id} should remain active`);
} else {
assert.equal(supersededIds.has(entry.id), true, `${entry.id} should be superseded`);
}
}
assert.ok(loaded.migrations?.includes("2026-04-28-quality-cleanup"));
} finally {
await rm(root, { recursive: true, force: true });
}
});
test("quality cleanup migration dedupes tags", async () => {
const root = await mkdtemp(join(tmpdir(), "wm-quality-tags-"));
try {
const now = new Date().toISOString();
await saveWorkspaceMemory(root, {
version: 1,
workspace: { root, key: await workspaceKey(root) },
limits: { maxRenderedChars: LONG_TERM_LIMITS.maxRenderedChars, maxEntries: LONG_TERM_LIMITS.maxEntries },
entries: [{
id: "bad_with_tags",
type: "feedback",
text: "Wave 1 completed successfully and all tests passed",
source: "compaction",
confidence: 0.75,
status: "active",
createdAt: now,
updatedAt: now,
tags: ["quality_cleanup", "quality:progress_snapshot"],
}],
migrations: [],
updatedAt: now,
});
const loaded = await loadWorkspaceMemory(root);
const tags = loaded.entries[0].tags ?? [];
assert.equal(tags.filter(tag => tag === "quality_cleanup").length, 1);
assert.equal(tags.filter(tag => tag === "quality:progress_snapshot").length, 1);
} finally {
await rm(root, { recursive: true, force: true });
}
});
test("quality cleanup migration does not supersede explicit memories", async () => {
const root = await mkdtemp(join(tmpdir(), "wm-quality-explicit-"));
try {
const now = new Date().toISOString();
const explicitBadShape = {
id: "explicit_progress_like",
type: "feedback" as const,
text: "Wave 1 completed successfully and all tests passed",
source: "explicit" as const,
confidence: 1,
status: "active" as const,
createdAt: now,
updatedAt: now,
};
await saveWorkspaceMemory(root, {
version: 1,
workspace: { root, key: await workspaceKey(root) },
limits: { maxRenderedChars: LONG_TERM_LIMITS.maxRenderedChars, maxEntries: LONG_TERM_LIMITS.maxEntries },
entries: [explicitBadShape],
migrations: [],
updatedAt: now,
});
const loaded = await loadWorkspaceMemory(root);
assert.equal(loaded.entries[0].status, "active");
assert.equal(loaded.entries[0].source, "explicit");
} finally {
await rm(root, { recursive: true, force: true });
}
});
test("quality cleanup migration does not supersede manual memories", async () => {
const root = await mkdtemp(join(tmpdir(), "wm-quality-manual-"));
try {
const now = new Date().toISOString();
const manualBadShape = {
id: "manual_progress_like",
type: "feedback" as const,
text: "Wave 1 completed successfully and all tests passed",
source: "manual" as const,
confidence: 0.9,
status: "active" as const,
createdAt: now,
updatedAt: now,
};
await saveWorkspaceMemory(root, {
version: 1,
workspace: { root, key: await workspaceKey(root) },
limits: { maxRenderedChars: LONG_TERM_LIMITS.maxRenderedChars, maxEntries: LONG_TERM_LIMITS.maxEntries },
entries: [manualBadShape],
migrations: [],
updatedAt: now,
});
const loaded = await loadWorkspaceMemory(root);
assert.equal(loaded.entries[0].status, "active");
assert.equal(loaded.entries[0].source, "manual");
} finally {
await rm(root, { recursive: true, force: true });
}
});
test("renderWorkspaceMemory excludes superseded entries", () => {
const now = new Date().toISOString();
const store: WorkspaceMemoryStore = {