mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
aaa4016ae8
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).
324 lines
12 KiB
TypeScript
324 lines
12 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { existsSync } from "node:fs";
|
|
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
import { dirname, join } from "node:path";
|
|
import { realpath } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import {
|
|
EVIDENCE_LOG_LIMITS,
|
|
appendEvidenceEvent,
|
|
appendEvidenceEvents,
|
|
queryEvidenceEvents,
|
|
summarizeMemoryEvidence,
|
|
traceMemoryLifecycle,
|
|
type EvidenceEventInput,
|
|
type EvidenceEventV1,
|
|
} from "../src/evidence-log.ts";
|
|
import { workspaceEvidenceLogPath, workspaceKey } from "../src/paths.ts";
|
|
|
|
async function tempRoot(): Promise<string> {
|
|
return mkdtemp(join(tmpdir(), "opencode-evidence-log-"));
|
|
}
|
|
|
|
function eventInput(overrides: Partial<EvidenceEventInput> = {}): EvidenceEventInput {
|
|
return {
|
|
type: "promotion_promoted",
|
|
phase: "promotion",
|
|
outcome: "promoted",
|
|
reasonCodes: ["new_workspace_entry"],
|
|
memory: { memoryId: "mem-a", type: "decision", source: "explicit", status: "active" },
|
|
textPreview: "Use npm test before release",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function readLog(root: string): Promise<string> {
|
|
return readFile(await workspaceEvidenceLogPath(root), "utf8");
|
|
}
|
|
|
|
function privacyHash(value: string): string {
|
|
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
}
|
|
|
|
async function workspaceRootHash(root: string): Promise<string> {
|
|
const resolved = await realpath(root).catch(() => root);
|
|
return privacyHash(resolved);
|
|
}
|
|
|
|
function manualEvent(rootKey: string, rootHash: string, id: string, createdAt: string): EvidenceEventV1 {
|
|
return {
|
|
version: 1,
|
|
eventId: id,
|
|
createdAt,
|
|
workspaceKey: rootKey,
|
|
workspaceRootHash: rootHash,
|
|
type: "render_selected",
|
|
phase: "render",
|
|
outcome: "rendered",
|
|
memory: { memoryId: id, type: "decision" },
|
|
reasonCodes: ["within_caps", "within_char_budget"],
|
|
};
|
|
}
|
|
|
|
test("appendEvidenceEvent writes versioned JSONL with workspace hashes", async () => {
|
|
const root = await tempRoot();
|
|
try {
|
|
const event = await appendEvidenceEvent(root, eventInput({ sessionHash: "session-1", messageHash: "message-1" }));
|
|
const raw = await readLog(root);
|
|
const stored = JSON.parse(raw.trim()) as EvidenceEventV1;
|
|
|
|
assert.equal(stored.version, 1);
|
|
assert.match(stored.eventId, /^evt_\d+_[a-z0-9]{8}$/);
|
|
assert.equal(stored.eventId, event.eventId);
|
|
assert.equal(stored.workspaceKey, await workspaceKey(root));
|
|
assert.equal(stored.workspaceRootHash, await workspaceRootHash(root));
|
|
assert.equal(stored.sessionHash, privacyHash("session-1"));
|
|
assert.equal(stored.messageHash, privacyHash("message-1"));
|
|
assert.equal(stored.type, "promotion_promoted");
|
|
assert.equal(stored.outcome, "promoted");
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("appendEvidenceEvent redacts text previews before writing", async () => {
|
|
const root = await tempRoot();
|
|
try {
|
|
await appendEvidenceEvent(root, eventInput({
|
|
type: "extraction_candidate_rejected",
|
|
phase: "extraction",
|
|
outcome: "rejected",
|
|
reasonCodes: ["raw_error"],
|
|
textPreview: "password: sushi\nAdmin PIN 是 456123\nBearer abc.def.ghi\nThis candidate is rejected and should be short",
|
|
details: {
|
|
note: "password: sushi Admin PIN 是 456123 Bearer abc.def.ghi",
|
|
},
|
|
}));
|
|
|
|
const raw = await readLog(root);
|
|
const stored = JSON.parse(raw.trim()) as EvidenceEventV1;
|
|
assert.ok(!raw.includes("sushi"));
|
|
assert.ok(!raw.includes("456123"));
|
|
assert.ok(!raw.includes("abc.def.ghi"));
|
|
assert.ok(stored.textPreview?.includes("[REDACTED]"));
|
|
assert.ok((stored.textPreview?.length ?? 0) <= 80);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("queryEvidenceEvents filters by type outcome and memory id", async () => {
|
|
const root = await tempRoot();
|
|
try {
|
|
await appendEvidenceEvents(root, [
|
|
eventInput({ type: "promotion_promoted", outcome: "promoted", memory: { memoryId: "mem-a" } }),
|
|
eventInput({ type: "render_omitted", phase: "render", outcome: "omitted", reasonCodes: ["type_cap"], memory: { memoryId: "mem-a" } }),
|
|
eventInput({ type: "render_omitted", phase: "render", outcome: "omitted", reasonCodes: ["global_cap"], memory: { memoryId: "mem-b" } }),
|
|
]);
|
|
|
|
const result = await queryEvidenceEvents(root, {
|
|
types: ["render_omitted"],
|
|
outcomes: ["omitted"],
|
|
memoryId: "mem-a",
|
|
});
|
|
|
|
assert.equal(result.length, 1);
|
|
assert.equal(result[0].type, "render_omitted");
|
|
assert.equal(result[0].memory?.memoryId, "mem-a");
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("memory_removed_capacity event round-trips through append and query", async () => {
|
|
const root = await tempRoot();
|
|
try {
|
|
await appendEvidenceEvent(root, eventInput({
|
|
type: "memory_removed_capacity",
|
|
phase: "storage",
|
|
outcome: "removed",
|
|
reasonCodes: ["global_cap"],
|
|
memory: { memoryId: "removed-memory", type: "reference", source: "compaction", status: "active" },
|
|
relations: [{ role: "removed", memory: { memoryId: "removed-memory", type: "reference", source: "compaction", status: "active" } }],
|
|
details: {
|
|
type: "reference",
|
|
globalCap: 28,
|
|
retentionClock: Date.UTC(2026, 4, 1),
|
|
createdAt: "2026-05-01T00:00:00.000Z",
|
|
source: "compaction",
|
|
},
|
|
}));
|
|
|
|
const result = await queryEvidenceEvents(root, {
|
|
types: ["memory_removed_capacity"],
|
|
phases: ["storage"],
|
|
outcomes: ["removed"],
|
|
memoryId: "removed-memory",
|
|
});
|
|
|
|
assert.equal(result.length, 1);
|
|
assert.equal(result[0].type, "memory_removed_capacity");
|
|
assert.equal(result[0].phase, "storage");
|
|
assert.equal(result[0].outcome, "removed");
|
|
assert.deepEqual(result[0].details, {
|
|
type: "reference",
|
|
globalCap: 28,
|
|
retentionClock: Date.UTC(2026, 4, 1),
|
|
createdAt: "2026-05-01T00:00:00.000Z",
|
|
source: "compaction",
|
|
});
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
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 {
|
|
const events = await appendEvidenceEvents(root, [
|
|
eventInput({ memory: { memoryId: "oldest" } }),
|
|
eventInput({ memory: { memoryId: "middle" } }),
|
|
eventInput({ memory: { memoryId: "newest" } }),
|
|
]);
|
|
|
|
const result = await queryEvidenceEvents(root, { newestFirst: true, limit: 2 });
|
|
|
|
assert.deepEqual(result.map(event => event.eventId), [events[2].eventId, events[1].eventId]);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("appendEvidenceEvent returns a record when appendFile fails", async () => {
|
|
const root = await tempRoot();
|
|
try {
|
|
const path = await workspaceEvidenceLogPath(root);
|
|
await mkdir(path, { recursive: true });
|
|
|
|
const event = await appendEvidenceEvent(root, eventInput());
|
|
|
|
assert.equal(event.version, 1);
|
|
assert.match(event.eventId, /^evt_\d+_[a-z0-9]{8}$/);
|
|
assert.equal(event.workspaceKey, await workspaceKey(root));
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("evidence log pruning drops old events, caps count, and quarantines invalid lines", async () => {
|
|
const root = await tempRoot();
|
|
try {
|
|
const path = await workspaceEvidenceLogPath(root);
|
|
await mkdir(dirname(path), { recursive: true });
|
|
const rootKey = await workspaceKey(root);
|
|
const rootHash = await workspaceRootHash(root);
|
|
const old = manualEvent(rootKey, rootHash, "old-event", new Date(Date.now() - 91 * 24 * 60 * 60 * 1000).toISOString());
|
|
const recentEvents = Array.from({ length: EVIDENCE_LOG_LIMITS.maxEventsPerWorkspace + 1 }, (_, i) =>
|
|
manualEvent(rootKey, rootHash, `recent-${i}`, new Date(Date.now() - (EVIDENCE_LOG_LIMITS.maxEventsPerWorkspace - i) * 1000).toISOString())
|
|
);
|
|
await writeFile(path, [
|
|
JSON.stringify(old),
|
|
"{not valid json",
|
|
...recentEvents.map(event => JSON.stringify(event)),
|
|
].join("\n") + "\n", "utf8");
|
|
|
|
const appended = await appendEvidenceEvents(root, Array.from({ length: EVIDENCE_LOG_LIMITS.pruneEveryAppendCount }, (_, i) =>
|
|
eventInput({ memory: { memoryId: `appended-${i}` }, reasonCodes: ["new_workspace_entry"] })
|
|
));
|
|
|
|
const events = await queryEvidenceEvents(root);
|
|
const memoryIds = new Set(events.map(event => event.memory?.memoryId));
|
|
const files = await readdir(dirname(path));
|
|
|
|
assert.ok(events.length <= EVIDENCE_LOG_LIMITS.maxEventsPerWorkspace);
|
|
assert.equal(memoryIds.has("old-event"), false);
|
|
assert.equal(memoryIds.has("recent-0"), false, "oldest events over the count cap should be pruned");
|
|
assert.equal(memoryIds.has(appended.at(-1)?.memory?.memoryId), true);
|
|
assert.ok(files.some(file => file.startsWith("events.jsonl.corrupt-lines-")));
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("memory evidence summary and lifecycle trace derive latest status", async () => {
|
|
const root = await tempRoot();
|
|
try {
|
|
await appendEvidenceEvents(root, [
|
|
eventInput({ type: "extraction_candidate_accepted", phase: "extraction", outcome: "accepted", reasonCodes: ["quality_gate_passed"], memory: { memoryId: "mem-life", memoryKeyHash: "raw-key" } }),
|
|
eventInput({ type: "promotion_promoted", phase: "promotion", outcome: "promoted", reasonCodes: ["new_workspace_entry"], memory: { memoryId: "mem-life", memoryKeyHash: "raw-key" } }),
|
|
eventInput({ type: "memory_reinforced", phase: "reinforcement", outcome: "reinforced", reasonCodes: ["duplicate_exact"], relations: [{ role: "reinforced", memory: { memoryId: "mem-life", memoryKeyHash: "raw-key" } }] }),
|
|
eventInput({ type: "render_selected", phase: "render", outcome: "rendered", reasonCodes: ["within_caps", "within_char_budget"], memory: { memoryId: "mem-life", memoryKeyHash: "raw-key" } }),
|
|
]);
|
|
|
|
const summary = await summarizeMemoryEvidence(root, { memoryId: "mem-life" });
|
|
const trace = await traceMemoryLifecycle(root, { memoryId: "mem-life" });
|
|
|
|
assert.equal(summary.latestOutcome, "rendered");
|
|
assert.equal(summary.latestRenderStatus, "rendered");
|
|
assert.ok(summary.reasonCodes.includes("duplicate_exact"));
|
|
assert.equal(trace.currentStatus, "rendered");
|
|
assert.ok(trace.acceptedBy);
|
|
assert.ok(trace.promotedBy);
|
|
assert.equal(trace.reinforcedBy.length, 1);
|
|
assert.equal(trace.latestRender?.type, "render_selected");
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("evidence relation roles reject sublimation placeholders at compile-time surface", () => {
|
|
const allowedRoles: Array<NonNullable<EvidenceEventInput["relations"]>[number]["role"]> = [
|
|
"candidate",
|
|
"pending",
|
|
"promoted",
|
|
"retained",
|
|
"absorbed",
|
|
"superseded",
|
|
"superseded_by",
|
|
"reinforced",
|
|
"reinforced_by",
|
|
"rendered",
|
|
"omitted",
|
|
"removed",
|
|
];
|
|
|
|
assert.equal(allowedRoles.includes("candidate"), true);
|
|
});
|