fix: owner scope in global unowned promotion

Problem: clearPendingMemories() and recordPromotionRejections() would
incorrectly clear or mutate owned entries during global unowned promotion.

Fixes:
1. clearPendingMemories() now respects owner/unowned scope:
   - global clearUnowned only clears unowned same-key entries
   - owned same-key entries are preserved
   - explicit global clear-all-by-key fallback still works

2. recordPromotionRejections() now has includeUnownedOnly option:
   - global unowned rejection only increments/exhausts unowned entries
   - owned same-key entries are preserved

3. Added regression tests:
   - global unowned clear keeps owned same-key entries
   - global unowned rejection only exhausts unowned same-key entries

Tests: 182 pass, 0 fail
This commit is contained in:
Ralph Chang
2026-04-28 12:27:46 +08:00
parent 222bae28de
commit a762e863d1
3 changed files with 86 additions and 6 deletions
+14 -5
View File
@@ -169,10 +169,18 @@ export async function clearPendingMemories(
store.entries = store.entries.filter(entry => {
if (!keys.has(memoryKey(entry))) return true;
if (!options.ownerSessionID) return false;
if (entry.pendingOwnerSessionID === options.ownerSessionID) return false;
if (options.clearUnowned && !entry.pendingOwnerSessionID) return false;
return true;
if (options.ownerSessionID) {
if (entry.pendingOwnerSessionID === options.ownerSessionID) return false;
if (options.clearUnowned && !entry.pendingOwnerSessionID) return false;
return true;
}
if (options.clearUnowned) {
return Boolean(entry.pendingOwnerSessionID);
}
return false;
});
return store;
});
@@ -182,7 +190,7 @@ export async function recordPromotionRejections(
root: string,
keys: Set<string>,
reason: string,
options: { ownerSessionID?: string } = {},
options: { ownerSessionID?: string; includeUnownedOnly?: boolean } = {},
): Promise<Set<string>> {
const exhausted = new Set<string>();
if (keys.size === 0) return exhausted;
@@ -195,6 +203,7 @@ export async function recordPromotionRejections(
const key = memoryKey(entry);
if (!keys.has(key)) return entry;
if (options.ownerSessionID && entry.pendingOwnerSessionID !== options.ownerSessionID) return entry;
if (!options.ownerSessionID && options.includeUnownedOnly && entry.pendingOwnerSessionID) return entry;
const promotionAttempts = (entry.promotionAttempts ?? 0) + 1;
const max = entry.source === "manual"
+4 -1
View File
@@ -347,7 +347,10 @@ export const MemoryV2Plugin: Plugin = async (input) => {
directory,
accounting.retryableRejectedKeys,
"rejected_capacity",
{ ownerSessionID: sessionID },
{
ownerSessionID: sessionID,
includeUnownedOnly: !sessionID,
},
);
const sessionRemovalKeys = new Set([