feat: add consolidation accounting for workspace memory promotion

P0 implementation with four waves:

Wave 1: Dedup with accounting
- Add dedupeLongTermEntriesWithAccounting()
- Classify exact duplicate, identity duplicate, topic duplicate

Wave 2: Normalization with accounting
- Add normalizeWorkspaceMemoryWithAccounting()
- Chain redaction → migration → enforceLongTermLimitsWithAccounting

Wave 3: Promotion accounting integration
- Update accountPendingPromotions() to use new accounting API
- Add supersededKeys to classification
- Distinguish promoted / absorbed / superseded / rejected

Wave 4: Integration tests
- End-to-end tests covering full pipeline

Bug fixes:
- Fix active vs superseded boundary (superseded entries no longer block promotion)
- Remove unused rejected_duplicate_lower_quality type
- Defer pending journal safety cap (TODO added)

Tests: 135 passing (up from 115)
This commit is contained in:
Ralph Chang
2026-04-27 16:45:55 +08:00
parent 1c748f3ee2
commit 3cc6dff7ae
9 changed files with 900 additions and 56 deletions
+9 -3
View File
@@ -34,6 +34,7 @@ import {
import {
loadWorkspaceMemory,
updateWorkspaceMemory,
updateWorkspaceMemoryWithAccounting,
renderWorkspaceMemory,
} from "./workspace-memory.ts";
import {
@@ -250,9 +251,13 @@ export const MemoryV2Plugin: Plugin = async (input) => {
let beforeEntries: Awaited<ReturnType<typeof loadWorkspaceMemory>>["entries"] = [];
const updatedWorkspaceMemory = await updateWorkspaceMemory(directory, workspaceMemory => {
const updateResult = await updateWorkspaceMemoryWithAccounting(directory, workspaceMemory => {
beforeEntries = [...workspaceMemory.entries];
const existingKeys = new Set(workspaceMemory.entries.map(memory => memoryKey(memory)));
const existingKeys = new Set(
workspaceMemory.entries
.filter(memory => memory.status !== "superseded")
.map(memory => memoryKey(memory)),
);
for (const memory of pending) {
const key = memoryKey(memory);
@@ -268,7 +273,8 @@ export const MemoryV2Plugin: Plugin = async (input) => {
const accounting = accountPendingPromotions({
pending,
before: beforeEntries,
after: updatedWorkspaceMemory.entries,
after: updateResult.store.entries,
events: updateResult.events,
});
if (sessionID) {