diff --git a/src/plugin.ts b/src/plugin.ts index 035ca1e..184d2fb 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -231,8 +231,7 @@ export const MemoryV2Plugin: Plugin = async (input) => { ]; if (pending.length === 0) return; - const promotedKeys = new Set(); - await updateWorkspaceMemory(directory, workspaceMemory => { + const updatedWorkspaceMemory = await updateWorkspaceMemory(directory, workspaceMemory => { const existingKeys = new Set(workspaceMemory.entries.map(memory => memoryKey(memory))); for (const memory of pending) { @@ -241,21 +240,24 @@ export const MemoryV2Plugin: Plugin = async (input) => { workspaceMemory.entries.push(memory); existingKeys.add(key); } - promotedKeys.add(key); } return workspaceMemory; }); + // Only clear pending memories that survived workspace normalization/limits. + // updateWorkspaceMemory() may dedupe, supersede, redact, or cap entries. + const retainedKeys = new Set(updatedWorkspaceMemory.entries.map(memory => memoryKey(memory))); + if (sessionID) { await updateSessionState(directory, sessionID, state => { - state.pendingMemories = state.pendingMemories.filter(memory => !promotedKeys.has(memoryKey(memory))); + state.pendingMemories = state.pendingMemories.filter(memory => !retainedKeys.has(memoryKey(memory))); return state; }); clearFrozenWorkspaceMemoryCache(sessionID); } - await clearPendingMemories(directory, promotedKeys); + await clearPendingMemories(directory, retainedKeys); } function bashExitCode(hookOutput: unknown): number | undefined { @@ -314,6 +316,11 @@ export const MemoryV2Plugin: Plugin = async (input) => { frozenWorkspaceMemoryCache.delete(sessionID); } + function sessionIDFromEventProperties(properties: unknown): string | undefined { + const props = properties as { sessionID?: string; info?: { id?: string } } | undefined; + return props?.sessionID ?? props?.info?.id; + } + return { // Inject workspace memory and hot session state into system prompt "experimental.chat.system.transform": async (hookInput, output) => { @@ -469,8 +476,7 @@ export const MemoryV2Plugin: Plugin = async (input) => { // Handle session events event: async ({ event }) => { if (event.type === "session.compacted") { - const sessionID = (event.properties as { sessionID?: string; info?: { id?: string } })?.sessionID - ?? (event.properties as { info?: { id?: string } })?.info?.id; + const sessionID = sessionIDFromEventProperties(event.properties); if (!sessionID) return; // Sub-agents don't need post-compaction processing @@ -492,7 +498,7 @@ export const MemoryV2Plugin: Plugin = async (input) => { } if (event.type === "session.deleted") { - const sessionID = (event.properties as { info?: { id?: string } })?.info?.id; + const sessionID = sessionIDFromEventProperties(event.properties); if (sessionID) { // Promote pending memories before deleting per-session state. // If promotion fails, leave session state and journal intact. diff --git a/tests/plugin.test.ts b/tests/plugin.test.ts index 86f0de5..afa25f7 100644 --- a/tests/plugin.test.ts +++ b/tests/plugin.test.ts @@ -630,7 +630,22 @@ test("compaction intentionally refreshes frozen system[1] with promoted memories const tmpDir = await mkdtemp(join(tmpdir(), "memory-plugin-test-")); try { - // 1. First transform creates frozen system[1] + // 1. Seed workspace memory, then first transform creates non-empty frozen system[1]. + const now = new Date().toISOString(); + await updateWorkspaceMemory(tmpDir, store => { + store.entries.push({ + id: "mem_old_stable", + type: "project", + text: "Old stable memory.", + source: "compaction", + confidence: 0.9, + status: "active", + createdAt: now, + updatedAt: now, + }); + return store; + }); + const client = mockRootClient(); const plugin = await MemoryV2Plugin({ directory: tmpDir, client }); @@ -640,7 +655,9 @@ test("compaction intentionally refreshes frozen system[1] with promoted memories output1, ); - const firstSystem1 = output1.system[1]; // workspace memory snapshot + const firstSystem1 = output1.system.find((part: string) => part.startsWith("Workspace memory")); + assert.match(firstSystem1 ?? "", /Old stable memory/, + "first transform should create a non-empty frozen system[1]"); // 2. Add pending memory to session state await saveSessionState(tmpDir, { @@ -678,13 +695,15 @@ test("compaction intentionally refreshes frozen system[1] with promoted memories output2, ); - const secondSystem1 = output2.system[1]; + const secondSystem1 = output2.system.find((part: string) => part.startsWith("Workspace memory")); // 5. Assert: system[1] changed (compaction started new cache epoch) assert.notEqual(secondSystem1, firstSystem1, "frozen system[1] should change after compaction (new cache epoch)"); - // 6. Assert: promoted memory is now in system[1] + // 6. Assert: old stable memory is preserved and promoted memory is now in system[1] + assert.match(secondSystem1 ?? "", /Old stable memory/, + "refreshed system[1] should preserve existing workspace memory"); assert.match(secondSystem1 ?? "", /Compaction refreshes frozen snapshot/, "promoted memory should appear in refreshed system[1]");