mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
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:
+14
-5
@@ -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
@@ -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([
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user