mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
fix: auto-supersede low-quality compaction memories
This commit is contained in:
+43
-1
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user