mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
fix: promotion accounting, sessionID extraction, and strengthened regression tests
Architecture review fixes: - Promotion accounting: only clear pending memories that survived workspace memory normalization/cap limits. Use retainedKeys from the returned normalized store instead of attemptedKeys. - Shared sessionID extraction: add sessionIDFromEventProperties() helper and use it in both session.compacted and session.deleted, fixing the previous gap where session.deleted only read info.id. - Strengthen compaction refresh test: seed workspace memory before first transform so firstSystem1 is non-empty, then assert refreshed system[1] preserves existing entries AND contains promoted memories.
This commit is contained in:
+14
-8
@@ -231,8 +231,7 @@ export const MemoryV2Plugin: Plugin = async (input) => {
|
||||
];
|
||||
if (pending.length === 0) return;
|
||||
|
||||
const promotedKeys = new Set<string>();
|
||||
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.
|
||||
|
||||
+23
-4
@@ -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]");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user