Skip to content

Commit 2f5a322

Browse files
committed
fix(reflection): per-section legacy filtering prevents resolved items from being revived (P1+P2)
- Add resolvedAt/resolvedBy/resolutionNote metadata to ReflectionItemMetadata - Filter out resolved items from reflection loading (P1 fix) - Per-section legacy filtering to prevent cross-section revival (P2 fix)
1 parent 0988a46 commit 2f5a322

File tree

2 files changed

+70
-2
lines changed

2 files changed

+70
-2
lines changed

src/reflection-item-store.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export interface ReflectionItemMetadata {
2323
baseWeight: number;
2424
quality: number;
2525
sourceReflectionPath?: string;
26+
/** Unix timestamp when the item was marked resolved. Undefined = unresolved. */
27+
resolvedAt?: number;
28+
/** Agent ID that marked this item resolved. */
29+
resolvedBy?: string;
30+
/** Optional note explaining why the item was resolved. */
31+
resolutionNote?: string;
2632
}
2733

2834
export interface ReflectionItemPayload {

src/reflection-store.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,70 @@ export function loadAgentReflectionSlicesFromEntries(params: LoadReflectionSlice
252252
const itemRows = reflectionRows.filter(({ metadata }) => metadata.type === "memory-reflection-item");
253253
const legacyRows = reflectionRows.filter(({ metadata }) => metadata.type === "memory-reflection");
254254

255-
const invariantCandidates = buildInvariantCandidates(itemRows, legacyRows);
256-
const derivedCandidates = buildDerivedCandidates(itemRows, legacyRows);
255+
// [P1] Filter out resolved items — passive suppression for #447
256+
// resolvedAt === undefined means unresolved (default)
257+
const unresolvedItemRows = itemRows.filter(({ metadata }) => metadata.resolvedAt === undefined);
258+
const resolvedItemRows = itemRows.filter(({ metadata }) => metadata.resolvedAt !== undefined);
259+
260+
const hasItemRows = itemRows.length > 0;
261+
const hasLegacyRows = legacyRows.length > 0;
262+
263+
// Collect normalized text of resolved items so we can detect whether legacy
264+
// rows are pure duplicates of already-resolved content.
265+
const resolvedInvariantTexts = new Set(
266+
resolvedItemRows
267+
.filter(({ metadata }) => metadata.itemKind === "invariant")
268+
.flatMap(({ entry }) => sanitizeInjectableReflectionLines([entry.text]))
269+
.map((line) => normalizeReflectionLineForAggregation(line))
270+
);
271+
const resolvedDerivedTexts = new Set(
272+
resolvedItemRows
273+
.filter(({ metadata }) => metadata.itemKind === "derived")
274+
.flatMap(({ entry }) => sanitizeInjectableReflectionLines([entry.text]))
275+
.map((line) => normalizeReflectionLineForAggregation(line))
276+
);
277+
278+
// Check whether legacy rows add any content not already covered by resolved items.
279+
const legacyHasUniqueInvariant = legacyRows.some(({ metadata }) =>
280+
toStringArray(metadata.invariants).some(
281+
(line) => !resolvedInvariantTexts.has(normalizeReflectionLineForAggregation(line))
282+
)
283+
);
284+
const legacyHasUniqueDerived = legacyRows.some(({ metadata }) =>
285+
toStringArray(metadata.derived).some(
286+
(line) => !resolvedDerivedTexts.has(normalizeReflectionLineForAggregation(line))
287+
)
288+
);
289+
290+
// Suppress when:
291+
// 1) there were item rows, all are resolved, and there are no legacy rows, OR
292+
// 2) there were item rows, all are resolved, legacy rows exist BUT all of their
293+
// content duplicates already-resolved items (prevents legacy fallback from
294+
// reviving just-resolved advice — the P1 bug fixed here).
295+
const shouldSuppress =
296+
hasItemRows &&
297+
unresolvedItemRows.length === 0 &&
298+
(!hasLegacyRows || (!legacyHasUniqueInvariant && !legacyHasUniqueDerived));
299+
if (shouldSuppress) {
300+
return { invariants: [], derived: [] };
301+
}
302+
303+
// [P2] Per-section legacy filtering: only pass legacy rows that have unique
304+
// content for this specific section. Prevents resolved items in section A from being
305+
// revived when section B has unique legacy content (cross-section legacy fallback bug).
306+
const invariantLegacyRows = legacyRows.filter(({ metadata }) =>
307+
toStringArray(metadata.invariants).some(
308+
(line) => !resolvedInvariantTexts.has(normalizeReflectionLineForAggregation(line))
309+
)
310+
);
311+
const derivedLegacyRows = legacyRows.filter(({ metadata }) =>
312+
toStringArray(metadata.derived).some(
313+
(line) => !resolvedDerivedTexts.has(normalizeReflectionLineForAggregation(line))
314+
)
315+
);
316+
317+
const invariantCandidates = buildInvariantCandidates(unresolvedItemRows, invariantLegacyRows);
318+
const derivedCandidates = buildDerivedCandidates(unresolvedItemRows, derivedLegacyRows);
257319

258320
const invariants = rankReflectionLines(invariantCandidates, {
259321
now,

0 commit comments

Comments
 (0)