Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/reflection-item-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
66 changes: 64 additions & 2 deletions src/reflection-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
)
Comment on lines +306 to +309
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude resolved legacy lines before fallback ranking

When unresolvedItemRows is empty, this row-level filter lets any legacy row through if it has at least one unique invariant, but buildInvariantCandidates later emits all invariant lines from that row. In the common case where one legacy row contains both a resolved line and a still-unique line, the resolved line is revived again, which defeats the resolved-item suppression this patch is adding (same pattern exists for derived rows below).

Useful? React with 👍 / 👎.

);
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,
Expand Down
Loading