Skip to content

Commit 700c309

Browse files
committed
fix(memory-core): mark REM-considered staged entries
1 parent e57e406 commit 700c309

3 files changed

Lines changed: 81 additions & 3 deletions

File tree

extensions/memory-core/src/dreaming-phases.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
readLightStagedKeys,
3232
readShortTermRecallEntries,
3333
recordDreamingPhaseSignals,
34+
recordRemConsideredPhaseSignals,
3435
recordShortTermRecalls,
3536
type ShortTermRecallEntry,
3637
} from "./short-term-promotion.js";
@@ -1738,8 +1739,9 @@ async function runRemDreaming(params: {
17381739
workspaceDir: params.workspaceDir,
17391740
nowMs,
17401741
});
1741-
const entries =
1742-
lightKeys.size > 0 ? allEntries.filter((entry) => lightKeys.has(entry.key)) : allEntries;
1742+
const stagedEntries =
1743+
lightKeys.size > 0 ? allEntries.filter((entry) => lightKeys.has(entry.key)) : [];
1744+
const entries = stagedEntries.length > 0 ? stagedEntries : allEntries;
17431745
const preview = previewRemDreaming({
17441746
entries,
17451747
limit: params.config.limit,
@@ -1753,6 +1755,13 @@ async function runRemDreaming(params: {
17531755
timezone: params.config.timezone,
17541756
storage: params.config.storage,
17551757
});
1758+
if (stagedEntries.length > 0) {
1759+
await recordRemConsideredPhaseSignals({
1760+
workspaceDir: params.workspaceDir,
1761+
keys: stagedEntries.map((entry) => entry.key),
1762+
nowMs,
1763+
});
1764+
}
17561765
await recordDreamingPhaseSignals({
17571766
workspaceDir: params.workspaceDir,
17581767
phase: "rem",

extensions/memory-core/src/short-term-promotion.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
recordGroundedShortTermCandidates,
1515
rankShortTermPromotionCandidates,
1616
recordDreamingPhaseSignals,
17+
recordRemConsideredPhaseSignals,
1718
recordShortTermRecalls,
1819
readLightStagedKeys,
1920
removeGroundedShortTermCandidates,
@@ -557,6 +558,16 @@ describe("short-term promotion", () => {
557558
await expect(readLightStagedKeys({ workspaceDir, nowMs: nowMs + 120_000 })).resolves.toEqual(
558559
new Set([pendingKey]),
559560
);
561+
562+
await recordRemConsideredPhaseSignals({
563+
workspaceDir,
564+
keys: [pendingKey],
565+
nowMs: nowMs + 180_000,
566+
});
567+
568+
await expect(readLightStagedKeys({ workspaceDir, nowMs: nowMs + 240_000 })).resolves.toEqual(
569+
new Set(),
570+
);
560571
});
561572
});
562573

extensions/memory-core/src/short-term-promotion.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ type ShortTermPhaseSignalEntry = {
9696
remHits: number;
9797
lastLightAt?: string;
9898
lastRemAt?: string;
99+
lastRemConsideredAt?: string;
99100
};
100101

101102
type ShortTermPhaseSignalStore = {
@@ -831,12 +832,17 @@ function normalizePhaseSignalStore(raw: unknown, nowIso: string): ShortTermPhase
831832
typeof entry.lastRemAt === "string" && entry.lastRemAt.trim().length > 0
832833
? entry.lastRemAt
833834
: undefined;
835+
const lastRemConsideredAt =
836+
typeof entry.lastRemConsideredAt === "string" && entry.lastRemConsideredAt.trim().length > 0
837+
? entry.lastRemConsideredAt
838+
: undefined;
834839
entries[key] = {
835840
key,
836841
lightHits,
837842
remHits,
838843
...(lastLightAt ? { lastLightAt } : {}),
839844
...(lastRemAt ? { lastRemAt } : {}),
845+
...(lastRemConsideredAt ? { lastRemConsideredAt } : {}),
840846
};
841847
}
842848
return {
@@ -1222,6 +1228,53 @@ export async function recordDreamingPhaseSignals(params: {
12221228
});
12231229
}
12241230

1231+
export async function recordRemConsideredPhaseSignals(params: {
1232+
workspaceDir?: string;
1233+
keys: string[];
1234+
nowMs?: number;
1235+
}): Promise<void> {
1236+
const workspaceDir = params.workspaceDir?.trim();
1237+
if (!workspaceDir) {
1238+
return;
1239+
}
1240+
const keys = [...new Set(params.keys.map((key) => key.trim()).filter(Boolean))];
1241+
if (keys.length === 0) {
1242+
return;
1243+
}
1244+
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
1245+
const nowIso = new Date(nowMs).toISOString();
1246+
1247+
await withShortTermLock(workspaceDir, async () => {
1248+
const [store, phaseSignals] = await Promise.all([
1249+
readStore(workspaceDir, nowIso),
1250+
readPhaseSignalStore(workspaceDir, nowIso),
1251+
]);
1252+
const knownKeys = new Set(Object.keys(store.entries));
1253+
1254+
for (const key of keys) {
1255+
if (!knownKeys.has(key)) {
1256+
continue;
1257+
}
1258+
const entry = phaseSignals.entries[key] ?? {
1259+
key,
1260+
lightHits: 0,
1261+
remHits: 0,
1262+
};
1263+
entry.lastRemConsideredAt = nowIso;
1264+
phaseSignals.entries[key] = entry;
1265+
}
1266+
1267+
for (const [key, entry] of Object.entries(phaseSignals.entries)) {
1268+
if (!knownKeys.has(key) || (entry.lightHits <= 0 && entry.remHits <= 0)) {
1269+
delete phaseSignals.entries[key];
1270+
}
1271+
}
1272+
1273+
phaseSignals.updatedAt = nowIso;
1274+
await writePhaseSignalStore(workspaceDir, phaseSignals);
1275+
});
1276+
}
1277+
12251278
export async function readLightStagedKeys(params: {
12261279
workspaceDir: string;
12271280
nowMs?: number;
@@ -1240,8 +1293,13 @@ export async function readLightStagedKeys(params: {
12401293
}
12411294
const lastLightMs = Date.parse(entry.lastLightAt ?? "");
12421295
const lastRemMs = Date.parse(entry.lastRemAt ?? "");
1296+
const lastRemConsideredMs = Date.parse(entry.lastRemConsideredAt ?? "");
1297+
const lastConsumedMs = Math.max(
1298+
Number.isFinite(lastRemMs) ? lastRemMs : Number.NEGATIVE_INFINITY,
1299+
Number.isFinite(lastRemConsideredMs) ? lastRemConsideredMs : Number.NEGATIVE_INFINITY,
1300+
);
12431301
const hasPendingLightSignal = Number.isFinite(lastLightMs)
1244-
? !Number.isFinite(lastRemMs) || lastLightMs > lastRemMs
1302+
? lastLightMs > lastConsumedMs
12451303
: !entry.lastRemAt;
12461304
if (hasPendingLightSignal) {
12471305
keys.add(key);

0 commit comments

Comments
 (0)