mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-06-02 06:19:36 +02:00
feat(reinforcement): compaction prompt wording reuse, migration evidence, and validation baseline
Wave 1 — Compaction prompt improvement: - Add three wording-reuse bullets to buildCompactionPrompt() under CRITICAL MEMORY RULES: do not create rephrased duplicates, reuse existing wording exactly when re-emitting, only emit new memories when the fact is new, materially corrected, or more specific. - This attacks the root cause of zero reinforcement: compaction generating variant text for the same durable fact. Wave 2 — Bug fixes: - Bug #2: Add placeholder comment to superseded_existing branch in decision dedupe (unreachable until v1.5.4 numbered refs). Preserve as const type assertions. - Bug #3: Add memory_migration_superseded evidence event type. Both P0 and quality cleanup migrations now produce evidence events for superseded entries. loadWorkspaceMemory appends migration evidence on first-load migrations only (idempotent via migration IDs). No historical backfill. - Bug #4: Add documentation comment explaining that feedback identity key returns exact key (absorbed_identity currently impossible for feedback). Add test verifying this behavior. Wave 3 — Validation baseline script: - Add scripts/dev/validate-identity-keys.ts: read-only script that scans workspace memory stores, computes exact/identity key collisions, and reports reinforcement statistics. Baseline matches audit: 0 exact collisions, 0 identity collisions, 0 reinforcement events across 123 active memories. Identity extension is gated on measurement: if the prompt change produces measurable reinforcement (reinforcementCount > 0), identity extension may be unnecessary. Decision dedupe stays exact-only (Wave 4 deferred).
This commit is contained in:
@@ -174,6 +174,41 @@ test("memory_removed_capacity event round-trips through append and query", async
|
||||
}
|
||||
});
|
||||
|
||||
test("memory_migration_superseded event round-trips through append and query", async () => {
|
||||
const root = await tempRoot();
|
||||
try {
|
||||
await appendEvidenceEvent(root, eventInput({
|
||||
type: "memory_migration_superseded",
|
||||
phase: "storage",
|
||||
outcome: "superseded",
|
||||
reasonCodes: ["migration:quality_cleanup", "quality:progress_snapshot"],
|
||||
memory: { memoryId: "migrated-memory", type: "project", source: "compaction", status: "superseded" },
|
||||
relations: [{ role: "superseded", memory: { memoryId: "migrated-memory", type: "project", source: "compaction", status: "superseded" } }],
|
||||
details: {
|
||||
migrationId: "2026-04-28-quality-cleanup",
|
||||
type: "project",
|
||||
source: "compaction",
|
||||
},
|
||||
}));
|
||||
|
||||
const result = await queryEvidenceEvents(root, {
|
||||
types: ["memory_migration_superseded"],
|
||||
phases: ["storage"],
|
||||
outcomes: ["superseded"],
|
||||
memoryId: "migrated-memory",
|
||||
});
|
||||
|
||||
assert.equal(result.length, 1);
|
||||
assert.equal(result[0].type, "memory_migration_superseded");
|
||||
assert.equal(result[0].phase, "storage");
|
||||
assert.equal(result[0].outcome, "superseded");
|
||||
assert.ok(result[0].reasonCodes.includes("migration:quality_cleanup"));
|
||||
assert.equal(result[0].memory?.status, "superseded");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("queryEvidenceEvents supports newestFirst and limit", async () => {
|
||||
const root = await tempRoot();
|
||||
try {
|
||||
|
||||
@@ -339,6 +339,69 @@ test("compaction prompt forbids progress and session-internal memory candidates"
|
||||
}
|
||||
});
|
||||
|
||||
test("compaction prompt includes existing-memory wording reuse guidance", async () => {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), "memory-plugin-prompt-"));
|
||||
try {
|
||||
const plugin = await MemoryV2Plugin({ directory: tmpDir, client: mockRootClient() });
|
||||
const output = { prompt: "", context: [] as string[] };
|
||||
|
||||
await (plugin as Record<string, Function>)["experimental.session.compacting"](
|
||||
{ sessionID: "prompt-wording-reuse-session", model: {} },
|
||||
output,
|
||||
);
|
||||
|
||||
assert.match(output.prompt, /Existing workspace memory may already contain durable facts/);
|
||||
assert.match(output.prompt, /do not create a rephrased duplicate/);
|
||||
assert.match(output.prompt, /reuse the existing memory wording exactly whenever possible/);
|
||||
assert.match(output.prompt, /new, materially corrected, or materially more specific/);
|
||||
} finally {
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("compaction prompt does not introduce CRUD memory directives", async () => {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), "memory-plugin-prompt-"));
|
||||
try {
|
||||
const plugin = await MemoryV2Plugin({ directory: tmpDir, client: mockRootClient() });
|
||||
const output = { prompt: "", context: [] as string[] };
|
||||
|
||||
await (plugin as Record<string, Function>)["experimental.session.compacting"](
|
||||
{ sessionID: "prompt-no-crud-session", model: {} },
|
||||
output,
|
||||
);
|
||||
|
||||
assert.doesNotMatch(output.prompt, /\bREPLACE\b/);
|
||||
assert.doesNotMatch(output.prompt, /\bDROP\b/);
|
||||
assert.doesNotMatch(output.prompt, /\bREINFORCE\s+\[M/);
|
||||
} finally {
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("compaction prompt preserves Memory candidates output format", async () => {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), "memory-plugin-prompt-"));
|
||||
try {
|
||||
const plugin = await MemoryV2Plugin({ directory: tmpDir, client: mockRootClient() });
|
||||
const output = { prompt: "", context: [] as string[] };
|
||||
|
||||
await (plugin as Record<string, Function>)["experimental.session.compacting"](
|
||||
{ sessionID: "prompt-format-session", model: {} },
|
||||
output,
|
||||
);
|
||||
|
||||
assert.match(
|
||||
output.prompt,
|
||||
/Format when there ARE durable memories:\nMemory candidates:\n- \[feedback\|decision\|project\|reference\] future-facing durable fact/,
|
||||
);
|
||||
assert.match(
|
||||
output.prompt,
|
||||
/Format when there are NO durable memories:\nMemory candidates:\n\(none\)/,
|
||||
);
|
||||
} finally {
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("compaction hook merges existing output.context from other plugins", async () => {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), "memory-plugin-test-"));
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { tmpdir } from "node:os";
|
||||
import type { LongTermMemoryEntry, WorkspaceMemoryStore } from "../src/types.ts";
|
||||
import { HOT_STATE_LIMITS, LONG_TERM_LIMITS } from "../src/types.ts";
|
||||
import { workspaceKey, workspaceMemoryPath } from "../src/paths.ts";
|
||||
import { queryEvidenceEvents } from "../src/evidence-log.ts";
|
||||
import {
|
||||
renderWorkspaceMemory,
|
||||
accountWorkspaceMemoryRender,
|
||||
@@ -562,6 +563,23 @@ test("dedupeLongTermEntriesWithAccounting reinforces absorbed exact duplicates",
|
||||
));
|
||||
});
|
||||
|
||||
test("dedupeLongTermEntriesWithAccounting decision same-identity variants absorb exact only", () => {
|
||||
const retained = entry("decision-a", "Use pnpm for package management!!!", "decision");
|
||||
const duplicate = entry("decision-b", "use pnpm for package management", "decision");
|
||||
|
||||
assert.notEqual(retained.text, duplicate.text);
|
||||
assert.equal(workspaceMemoryIdentityKey(retained), workspaceMemoryIdentityKey(duplicate));
|
||||
assert.equal(workspaceMemoryExactKey(retained), workspaceMemoryExactKey(duplicate));
|
||||
|
||||
const result = dedupeLongTermEntriesWithAccounting([retained, duplicate]);
|
||||
|
||||
assert.equal(result.kept.length, 1);
|
||||
assert.equal(result.absorbed.length, 1);
|
||||
assert.equal(result.absorbed[0].reason, "absorbed_exact");
|
||||
assert.equal(result.superseded.length, 0);
|
||||
assert.equal(result.absorbed.some(event => event.reason === "superseded_existing"), false);
|
||||
});
|
||||
|
||||
test("dedupeLongTermEntriesWithAccounting emits identity reinforcement evidence", () => {
|
||||
const now = Date.now();
|
||||
const retained: LongTermMemoryEntry = {
|
||||
@@ -999,6 +1017,16 @@ test("workspaceMemoryExactKey uses pending-compatible canonical semantics", () =
|
||||
assert.equal(workspaceMemoryExactKey(entry), "decision:opencode uses npm cache for plugin loading");
|
||||
});
|
||||
|
||||
test("workspaceMemoryIdentityKey returns exact key for feedback entries", () => {
|
||||
const feedback = entry(
|
||||
"feedback-exact-identity",
|
||||
"User prefers references to mention `.opencode/opencode.json` explicitly.",
|
||||
"feedback",
|
||||
);
|
||||
|
||||
assert.equal(workspaceMemoryIdentityKey(feedback), workspaceMemoryExactKey(feedback));
|
||||
});
|
||||
|
||||
test("normalizeWorkspaceMemoryWithAccounting redacts credentials before accounting", async () => {
|
||||
const root = "/repo";
|
||||
const now = new Date().toISOString();
|
||||
@@ -1252,6 +1280,50 @@ test("updateWorkspaceMemoryWithAccounting emits accounting events for persisted
|
||||
}
|
||||
});
|
||||
|
||||
test("updateWorkspaceMemoryWithAccounting includes migration evidence from pre-update normalization", async () => {
|
||||
const sandbox = await mkdtemp(join(tmpdir(), "wm-accounting-migration-update-"));
|
||||
const dataHome = join(sandbox, "xdg-data-home");
|
||||
const root = join(sandbox, "workspace");
|
||||
const previousXdgDataHome = process.env.XDG_DATA_HOME;
|
||||
process.env.XDG_DATA_HOME = dataHome;
|
||||
|
||||
try {
|
||||
await mkdir(root, { recursive: true });
|
||||
const now = "2026-04-26T00:00:00.000Z";
|
||||
const storePath = await workspaceMemoryPath(root);
|
||||
await mkdir(dirname(storePath), { recursive: true });
|
||||
await writeFile(storePath, JSON.stringify({
|
||||
version: 1,
|
||||
workspace: { root, key: await workspaceKey(root) },
|
||||
limits: { maxRenderedChars: LONG_TERM_LIMITS.maxRenderedChars, maxEntries: LONG_TERM_LIMITS.maxEntries },
|
||||
entries: [{
|
||||
id: "update_p0_progress",
|
||||
type: "project",
|
||||
text: "Phase 1-4 completed successfully",
|
||||
source: "compaction",
|
||||
confidence: 0.75,
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}],
|
||||
migrations: ["2026-04-28-quality-cleanup"],
|
||||
updatedAt: now,
|
||||
}, null, 2), "utf8");
|
||||
|
||||
const result = await updateWorkspaceMemoryWithAccounting(root, store => store);
|
||||
const migrationEvidence = result.evidence.filter(event => event.type === "memory_migration_superseded");
|
||||
|
||||
assert.equal(result.store.entries.find(memory => memory.id === "update_p0_progress")?.status, "superseded");
|
||||
assert.equal(migrationEvidence.length, 1);
|
||||
assert.deepEqual(migrationEvidence[0].reasonCodes, ["migration:p0_cleanup"]);
|
||||
assert.equal(migrationEvidence[0].memory?.memoryId, "update_p0_progress");
|
||||
} finally {
|
||||
if (previousXdgDataHome === undefined) delete process.env.XDG_DATA_HOME;
|
||||
else process.env.XDG_DATA_HOME = previousXdgDataHome;
|
||||
await rm(sandbox, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// P0d: identity-key dedup, supersession, staleness
|
||||
// ============================================
|
||||
@@ -1604,7 +1676,7 @@ test("redactCredentials is idempotent and also redacts rationale text", () => {
|
||||
})),
|
||||
},
|
||||
now,
|
||||
);
|
||||
).store;
|
||||
assert.equal(migrated.entries[0].text, "Admin PIN 是 [REDACTED]");
|
||||
assert.equal(migrated.entries[0].rationale, "password: [REDACTED]");
|
||||
});
|
||||
@@ -1663,14 +1735,69 @@ test("runMigrationP0Cleanup marks only non-explicit project snapshots and runs o
|
||||
};
|
||||
|
||||
const once = runMigrationP0Cleanup(store, now);
|
||||
assert.deepEqual(once.migrations, ["2026-04-26-p0-cleanup"]);
|
||||
assert.equal(once.entries.find(e => e.id === "project-snapshot")?.status, "superseded");
|
||||
assert.equal(once.entries.find(e => e.id === "project-explicit")?.status, "active");
|
||||
assert.equal(once.entries.find(e => e.id === "feedback-snapshot-like")?.status, "active");
|
||||
assert.deepEqual(once.store.migrations, ["2026-04-26-p0-cleanup"]);
|
||||
assert.equal(once.store.entries.find(e => e.id === "project-snapshot")?.status, "superseded");
|
||||
assert.equal(once.store.entries.find(e => e.id === "project-explicit")?.status, "active");
|
||||
assert.equal(once.store.entries.find(e => e.id === "feedback-snapshot-like")?.status, "active");
|
||||
assert.equal(once.events.length, 1);
|
||||
assert.equal(once.events[0].type, "memory_migration_superseded");
|
||||
assert.equal(once.events[0].phase, "storage");
|
||||
assert.equal(once.events[0].outcome, "superseded");
|
||||
assert.ok(once.events[0].reasonCodes.includes("migration:p0_cleanup"));
|
||||
assert.equal(once.events[0].memory?.memoryId, "project-snapshot");
|
||||
|
||||
const twice = runMigrationP0Cleanup(once, later);
|
||||
assert.deepEqual(twice.migrations, ["2026-04-26-p0-cleanup"], "migration id should not duplicate");
|
||||
assert.equal(twice.entries.find(e => e.id === "project-snapshot")?.updatedAt, once.entries.find(e => e.id === "project-snapshot")?.updatedAt);
|
||||
const twice = runMigrationP0Cleanup(once.store, later);
|
||||
assert.deepEqual(twice.store.migrations, ["2026-04-26-p0-cleanup"], "migration id should not duplicate");
|
||||
assert.equal(twice.store.entries.find(e => e.id === "project-snapshot")?.updatedAt, once.store.entries.find(e => e.id === "project-snapshot")?.updatedAt);
|
||||
assert.equal(twice.events.length, 0);
|
||||
});
|
||||
|
||||
test("loadWorkspaceMemory appends P0 migration evidence once", async () => {
|
||||
const sandbox = await mkdtemp(join(tmpdir(), "wm-p0-evidence-"));
|
||||
const dataHome = join(sandbox, "xdg-data-home");
|
||||
const root = join(sandbox, "workspace");
|
||||
const previousXdgDataHome = process.env.XDG_DATA_HOME;
|
||||
process.env.XDG_DATA_HOME = dataHome;
|
||||
|
||||
try {
|
||||
await mkdir(root, { recursive: true });
|
||||
const now = "2026-04-26T00:00:00.000Z";
|
||||
const storePath = await workspaceMemoryPath(root);
|
||||
await mkdir(dirname(storePath), { recursive: true });
|
||||
await writeFile(storePath, JSON.stringify({
|
||||
version: 1,
|
||||
workspace: { root, key: await workspaceKey(root) },
|
||||
limits: { maxRenderedChars: LONG_TERM_LIMITS.maxRenderedChars, maxEntries: LONG_TERM_LIMITS.maxEntries },
|
||||
entries: [{
|
||||
id: "p0_progress",
|
||||
type: "project",
|
||||
text: "Phase 1-4 completed successfully",
|
||||
source: "compaction",
|
||||
confidence: 0.75,
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}],
|
||||
migrations: ["2026-04-28-quality-cleanup"],
|
||||
updatedAt: now,
|
||||
}, null, 2), "utf8");
|
||||
|
||||
const firstLoad = await loadWorkspaceMemory(root);
|
||||
await loadWorkspaceMemory(root);
|
||||
const events = await queryEvidenceEvents(root, { types: ["memory_migration_superseded"] });
|
||||
|
||||
assert.equal(firstLoad.entries.find(memory => memory.id === "p0_progress")?.status, "superseded");
|
||||
assert.equal(events.length, 1);
|
||||
assert.equal(events[0].type, "memory_migration_superseded");
|
||||
assert.equal(events[0].phase, "storage");
|
||||
assert.equal(events[0].outcome, "superseded");
|
||||
assert.deepEqual(events[0].reasonCodes, ["migration:p0_cleanup"]);
|
||||
assert.equal(events[0].memory?.memoryId, "p0_progress");
|
||||
} finally {
|
||||
if (previousXdgDataHome === undefined) delete process.env.XDG_DATA_HOME;
|
||||
else process.env.XDG_DATA_HOME = previousXdgDataHome;
|
||||
await rm(sandbox, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("quality cleanup migration preserves soft-only feedback and decision violations", async () => {
|
||||
@@ -1797,6 +1924,56 @@ test("quality cleanup migration writes audit log for hard supersedes", async ()
|
||||
}
|
||||
});
|
||||
|
||||
test("quality cleanup migration appends superseded evidence with hard reasons", async () => {
|
||||
const sandbox = await mkdtemp(join(tmpdir(), "wm-quality-evidence-"));
|
||||
const dataHome = join(sandbox, "xdg-data-home");
|
||||
const root = join(sandbox, "workspace");
|
||||
const previousXdgDataHome = process.env.XDG_DATA_HOME;
|
||||
process.env.XDG_DATA_HOME = dataHome;
|
||||
|
||||
try {
|
||||
await mkdir(root, { recursive: true });
|
||||
const now = "2026-04-28T00:00:00.000Z";
|
||||
const storePath = await workspaceMemoryPath(root);
|
||||
await mkdir(dirname(storePath), { recursive: true });
|
||||
await writeFile(storePath, JSON.stringify({
|
||||
version: 1,
|
||||
workspace: { root, key: await workspaceKey(root) },
|
||||
limits: { maxRenderedChars: LONG_TERM_LIMITS.maxRenderedChars, maxEntries: LONG_TERM_LIMITS.maxEntries },
|
||||
entries: [{
|
||||
id: "quality_progress",
|
||||
type: "project",
|
||||
text: "Test suite: 1237 tests pass, 226 suites",
|
||||
source: "compaction",
|
||||
confidence: 0.75,
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
staleAfterDays: 60,
|
||||
}],
|
||||
migrations: [],
|
||||
updatedAt: now,
|
||||
}, null, 2), "utf8");
|
||||
|
||||
const loaded = await loadWorkspaceMemory(root);
|
||||
const events = await queryEvidenceEvents(root, { types: ["memory_migration_superseded"] });
|
||||
|
||||
assert.equal(loaded.entries.find(memory => memory.id === "quality_progress")?.status, "superseded");
|
||||
assert.equal(events.length, 1);
|
||||
assert.equal(events[0].type, "memory_migration_superseded");
|
||||
assert.equal(events[0].phase, "storage");
|
||||
assert.equal(events[0].outcome, "superseded");
|
||||
assert.ok(events[0].reasonCodes.includes("migration:quality_cleanup"));
|
||||
assert.ok(events[0].reasonCodes.includes("quality:progress_snapshot"));
|
||||
assert.equal(events[0].memory?.memoryId, "quality_progress");
|
||||
assert.equal(events[0].memory?.status, "superseded");
|
||||
} finally {
|
||||
if (previousXdgDataHome === undefined) delete process.env.XDG_DATA_HOME;
|
||||
else process.env.XDG_DATA_HOME = previousXdgDataHome;
|
||||
await rm(sandbox, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("quality cleanup migration aborts supersede when audit log cannot be written", async () => {
|
||||
const sandbox = await mkdtemp(join(tmpdir(), "wm-quality-audit-fail-"));
|
||||
const dataHome = join(sandbox, "xdg-data-home");
|
||||
|
||||
Reference in New Issue
Block a user