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:
Ralph Chang
2026-05-02 15:03:34 +08:00
parent f19614565a
commit e0357c572a
7 changed files with 745 additions and 23 deletions
+35
View File
@@ -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 {
+63
View File
@@ -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-"));
+185 -8
View File
@@ -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");