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 8b21325469
commit 1847f63480
3 changed files with 86 additions and 6 deletions
+68
View File
@@ -283,6 +283,35 @@ describe("pending journal retention", () => {
assert.deepEqual(loaded.entries.map(entry => entry.pendingOwnerSessionID), ["session-b"]);
});
it("global unowned clear keeps owned entries with the same key", async () => {
const now = new Date().toISOString();
const unowned: LongTermMemoryEntry = {
id: "clear-unowned",
type: "feedback",
text: "Prefer scoped cleanup.",
source: "explicit",
confidence: 1,
status: "active",
createdAt: now,
updatedAt: now,
};
const owned: LongTermMemoryEntry = {
...unowned,
id: "clear-owned",
pendingOwnerSessionID: "session-owned",
};
await appendPendingMemories(testDir, [unowned, owned]);
await clearPendingMemories(testDir, new Set([memoryKey(unowned)]), {
clearUnowned: true,
});
const loaded = await loadPendingJournal(testDir);
assert.deepEqual(loaded.entries.map(entry => entry.id), ["clear-owned"]);
assert.equal(loaded.entries[0].pendingOwnerSessionID, "session-owned");
});
it("retains same-key pending entries owned by different sessions", async () => {
const now = new Date().toISOString();
await appendPendingMemories(testDir, [
@@ -369,6 +398,45 @@ describe("pending journal retention", () => {
assert.deepEqual(loaded.entries.map(entry => entry.pendingOwnerSessionID), ["session-b"]);
});
it("global unowned rejection exhausts only unowned entries with the same key", async () => {
const now = new Date().toISOString();
const unowned: LongTermMemoryEntry = {
id: "reject-unowned",
type: "reference",
text: "Capacity rejected unowned reference.",
source: "explicit",
confidence: 0.1,
status: "active",
createdAt: now,
updatedAt: now,
promotionAttempts: PROMOTION_RETRY_LIMITS.maxExplicitAttempts - 1,
};
const owned: LongTermMemoryEntry = {
...unowned,
id: "reject-owned",
pendingOwnerSessionID: "session-owned",
promotionAttempts: undefined,
};
await appendPendingMemories(testDir, [unowned, owned]);
const exhausted = await recordPromotionRejections(
testDir,
new Set([memoryKey(unowned)]),
"rejected_capacity",
{ includeUnownedOnly: true },
);
assert.deepEqual([...exhausted], [memoryKey(unowned)]);
const loaded = await loadPendingJournal(testDir);
assert.deepEqual(loaded.entries.map(entry => entry.id), ["reject-owned"]);
assert.equal(
loaded.entries[0].promotionAttempts,
undefined,
"owned same-key entry must not be mutated by global unowned rejection",
);
assert.equal(loaded.entries[0].lastPromotionFailureReason, undefined);
});
it("drops invalid timestamp entries for every source as corruption safety", async () => {
await savePendingJournal(testDir, {
version: 1,