Skip to content

Commit 7ddbcdd

Browse files
committed
fix(dreaming): filter already-emitted entries in light phase to prevent verbatim repeats (fixes #72096)
1 parent 94833b2 commit 7ddbcdd

2 files changed

Lines changed: 41 additions & 1 deletion

File tree

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { textSimilarity as snippetSimilarity } from "./memory/tokenize.js";
4141
import {
4242
filterLiveShortTermRecallEntries,
4343
readLightStagedKeys,
44+
readPhaseSignalEntries,
4445
readShortTermRecallEntries,
4546
recordDreamingPhaseSignals,
4647
recordRemConsideredPhaseSignals,
@@ -1703,8 +1704,29 @@ async function runLightDreaming(params: {
17031704
lookbackDays: params.config.lookbackDays,
17041705
}),
17051706
});
1707+
// Filter entries already emitted in a prior light-phase cycle and not
1708+
// recalled again since. This prevents the work-summary block from
1709+
// repeating verbatim across consecutive cycles when the same entries
1710+
// remain within the lookback window. See #72096.
1711+
const phaseSignals = await readPhaseSignalEntries({
1712+
workspaceDir: params.workspaceDir,
1713+
nowMs,
1714+
});
1715+
const freshEntries = recentEntries.filter((entry) => {
1716+
const signal = phaseSignals[entry.key];
1717+
if (!signal?.lastLightAt) {
1718+
return true; // never emitted → include
1719+
}
1720+
const lastLightMs = Date.parse(signal.lastLightAt);
1721+
const lastRecalledMs = Date.parse(entry.lastRecalledAt);
1722+
if (!Number.isFinite(lastLightMs) || !Number.isFinite(lastRecalledMs)) {
1723+
return true; // malformed timestamps → include conservatively
1724+
}
1725+
// Include only if the entry was recalled again after the last emission.
1726+
return lastRecalledMs > lastLightMs;
1727+
});
17061728
const rankedEntries = dedupeEntries(
1707-
recentEntries.toSorted((a, b) => {
1729+
freshEntries.toSorted((a, b) => {
17081730
const byTime = Date.parse(b.lastRecalledAt) - Date.parse(a.lastRecalledAt);
17091731
if (byTime !== 0) {
17101732
return byTime;

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1775,6 +1775,24 @@ export async function readLightStagedKeys(params: {
17751775
return keys;
17761776
}
17771777

1778+
/**
1779+
* Read the phase signal entries for a workspace so callers can inspect
1780+
* per-entry `lastLightAt` / `lastRemAt` timestamps for deduplication.
1781+
*/
1782+
export async function readPhaseSignalEntries(params: {
1783+
workspaceDir: string;
1784+
nowMs?: number;
1785+
}): Promise<Record<string, ShortTermPhaseSignalEntry>> {
1786+
const workspaceDir = params.workspaceDir?.trim();
1787+
if (!workspaceDir) {
1788+
return {};
1789+
}
1790+
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
1791+
const nowIso = resolveMemoryCoreTimestamp(nowMs);
1792+
const store = await readPhaseSignalStore(workspaceDir, nowIso);
1793+
return store.entries;
1794+
}
1795+
17781796
export async function rankShortTermPromotionCandidates(
17791797
options: RankShortTermPromotionOptions,
17801798
): Promise<PromotionCandidate[]> {

0 commit comments

Comments
 (0)