From 2f5a32201dfb680b9baa1e6ade776c58e508dc91 Mon Sep 17 00:00:00 2001 From: jlin53882 Date: Mon, 13 Apr 2026 02:20:17 +0800 Subject: [PATCH] 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) --- src/reflection-item-store.ts | 6 ++++ src/reflection-store.ts | 66 ++++++++++++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/reflection-item-store.ts b/src/reflection-item-store.ts index 36c4ac5e..31893e9a 100644 --- a/src/reflection-item-store.ts +++ b/src/reflection-item-store.ts @@ -23,6 +23,12 @@ export interface ReflectionItemMetadata { baseWeight: number; quality: number; sourceReflectionPath?: string; + /** Unix timestamp when the item was marked resolved. Undefined = unresolved. */ + resolvedAt?: number; + /** Agent ID that marked this item resolved. */ + resolvedBy?: string; + /** Optional note explaining why the item was resolved. */ + resolutionNote?: string; } export interface ReflectionItemPayload { diff --git a/src/reflection-store.ts b/src/reflection-store.ts index 38da5ce7..bf96691e 100644 --- a/src/reflection-store.ts +++ b/src/reflection-store.ts @@ -252,8 +252,70 @@ export function loadAgentReflectionSlicesFromEntries(params: LoadReflectionSlice const itemRows = reflectionRows.filter(({ metadata }) => metadata.type === "memory-reflection-item"); const legacyRows = reflectionRows.filter(({ metadata }) => metadata.type === "memory-reflection"); - const invariantCandidates = buildInvariantCandidates(itemRows, legacyRows); - const derivedCandidates = buildDerivedCandidates(itemRows, legacyRows); + // [P1] Filter out resolved items — passive suppression for #447 + // resolvedAt === undefined means unresolved (default) + const unresolvedItemRows = itemRows.filter(({ metadata }) => metadata.resolvedAt === undefined); + const resolvedItemRows = itemRows.filter(({ metadata }) => metadata.resolvedAt !== undefined); + + const hasItemRows = itemRows.length > 0; + const hasLegacyRows = legacyRows.length > 0; + + // Collect normalized text of resolved items so we can detect whether legacy + // rows are pure duplicates of already-resolved content. + const resolvedInvariantTexts = new Set( + resolvedItemRows + .filter(({ metadata }) => metadata.itemKind === "invariant") + .flatMap(({ entry }) => sanitizeInjectableReflectionLines([entry.text])) + .map((line) => normalizeReflectionLineForAggregation(line)) + ); + const resolvedDerivedTexts = new Set( + resolvedItemRows + .filter(({ metadata }) => metadata.itemKind === "derived") + .flatMap(({ entry }) => sanitizeInjectableReflectionLines([entry.text])) + .map((line) => normalizeReflectionLineForAggregation(line)) + ); + + // Check whether legacy rows add any content not already covered by resolved items. + const legacyHasUniqueInvariant = legacyRows.some(({ metadata }) => + toStringArray(metadata.invariants).some( + (line) => !resolvedInvariantTexts.has(normalizeReflectionLineForAggregation(line)) + ) + ); + const legacyHasUniqueDerived = legacyRows.some(({ metadata }) => + toStringArray(metadata.derived).some( + (line) => !resolvedDerivedTexts.has(normalizeReflectionLineForAggregation(line)) + ) + ); + + // Suppress when: + // 1) there were item rows, all are resolved, and there are no legacy rows, OR + // 2) there were item rows, all are resolved, legacy rows exist BUT all of their + // content duplicates already-resolved items (prevents legacy fallback from + // reviving just-resolved advice — the P1 bug fixed here). + const shouldSuppress = + hasItemRows && + unresolvedItemRows.length === 0 && + (!hasLegacyRows || (!legacyHasUniqueInvariant && !legacyHasUniqueDerived)); + if (shouldSuppress) { + return { invariants: [], derived: [] }; + } + + // [P2] Per-section legacy filtering: only pass legacy rows that have unique + // content for this specific section. Prevents resolved items in section A from being + // revived when section B has unique legacy content (cross-section legacy fallback bug). + const invariantLegacyRows = legacyRows.filter(({ metadata }) => + toStringArray(metadata.invariants).some( + (line) => !resolvedInvariantTexts.has(normalizeReflectionLineForAggregation(line)) + ) + ); + const derivedLegacyRows = legacyRows.filter(({ metadata }) => + toStringArray(metadata.derived).some( + (line) => !resolvedDerivedTexts.has(normalizeReflectionLineForAggregation(line)) + ) + ); + + const invariantCandidates = buildInvariantCandidates(unresolvedItemRows, invariantLegacyRows); + const derivedCandidates = buildDerivedCandidates(unresolvedItemRows, derivedLegacyRows); const invariants = rankReflectionLines(invariantCandidates, { now,