Skip to content

Commit 4705750

Browse files
committed
fix(dreaming): filter already-emitted entries in light phase to prevent verbatim repeats (fixes #72096)
Light-phase dreaming re-summarized the same short-term recall entries every cycle because it never checked whether an entry was already emitted in a prior cycle. This produced byte-for-byte identical work-summary blocks in DREAMS.md across consecutive cycles. Read the phase signal store before building the light-phase body and skip entries whose is at or after their — meaning they were already emitted and have not been recalled again since. Entries without a prior (first cycle) or with a newer (re-recalled since last emission) are kept.
1 parent 7f6df80 commit 4705750

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
@@ -40,6 +40,7 @@ import { textSimilarity as snippetSimilarity } from "./memory/tokenize.js";
4040
import {
4141
filterLiveShortTermRecallEntries,
4242
readLightStagedKeys,
43+
readPhaseSignalEntries,
4344
readShortTermRecallEntries,
4445
recordDreamingPhaseSignals,
4546
recordRemConsideredPhaseSignals,
@@ -1660,8 +1661,29 @@ async function runLightDreaming(params: {
16601661
lookbackDays: params.config.lookbackDays,
16611662
}),
16621663
});
1664+
// Filter entries already emitted in a prior light-phase cycle and not
1665+
// recalled again since. This prevents the work-summary block from
1666+
// repeating verbatim across consecutive cycles when the same entries
1667+
// remain within the lookback window. See #72096.
1668+
const phaseSignals = await readPhaseSignalEntries({
1669+
workspaceDir: params.workspaceDir,
1670+
nowMs,
1671+
});
1672+
const freshEntries = recentEntries.filter((entry) => {
1673+
const signal = phaseSignals[entry.key];
1674+
if (!signal?.lastLightAt) {
1675+
return true; // never emitted → include
1676+
}
1677+
const lastLightMs = Date.parse(signal.lastLightAt);
1678+
const lastRecalledMs = Date.parse(entry.lastRecalledAt);
1679+
if (!Number.isFinite(lastLightMs) || !Number.isFinite(lastRecalledMs)) {
1680+
return true; // malformed timestamps → include conservatively
1681+
}
1682+
// Include only if the entry was recalled again after the last emission.
1683+
return lastRecalledMs > lastLightMs;
1684+
});
16631685
const entries = dedupeEntries(
1664-
recentEntries
1686+
freshEntries
16651687
.toSorted((a, b) => {
16661688
const byTime = Date.parse(b.lastRecalledAt) - Date.parse(a.lastRecalledAt);
16671689
if (byTime !== 0) {

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)