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:
Ralph Chang
2026-04-27 10:02:18 +08:00
parent 2437a9dc71
commit 4309cb855f
2 changed files with 37 additions and 12 deletions
+14 -8
View File
@@ -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.