diff --git a/src/browser/features/Memory/MemoryBrowser.tsx b/src/browser/features/Memory/MemoryBrowser.tsx index b723f94e58..aa88b05eeb 100644 --- a/src/browser/features/Memory/MemoryBrowser.tsx +++ b/src/browser/features/Memory/MemoryBrowser.tsx @@ -485,6 +485,20 @@ function MemoryFileRow(props: MemoryFileRowProps) { ); } +function uniqueConsolidationSummaryLines( + summaries: ReadonlyArray +): string[] { + const seen = new Set(); + const lines: string[] = []; + for (const summary of summaries) { + const line = summary?.trim(); + if (!line || seen.has(line)) continue; + seen.add(line); + lines.push(line); + } + return lines; +} + function formatConsolidationRecord(record: MemoryConsolidationRecordPayload | null): string { if (record === null) return "never"; const appliedCount = record.ops.filter((op) => op.applied).length; @@ -556,14 +570,15 @@ function ConsolidationFooter(props: { : "never"; const harvestError = status?.latestHarvestRecord?.status === "failed" ? status.latestHarvestRecord.error : undefined; - const summaryTitle = [ + // One consolidation pass can cover workspace, project, and global memory at + // once, so those scope records often share the exact same summary. Keep the + // per-scope status lines below, but avoid repeating identical tooltip text. + const summaryTitle = uniqueConsolidationSummaryLines([ status?.workspaceRecord?.summary, status?.projectRecord?.summary, status?.globalRecord?.summary, status?.latestHarvestRecord?.error, - ] - .filter(Boolean) - .join("\n"); + ]).join("\n"); return (
diff --git a/src/browser/features/RightSidebar/Memory/MemoryTab.test.tsx b/src/browser/features/RightSidebar/Memory/MemoryTab.test.tsx index 35b5bbe531..68a5432f5d 100644 --- a/src/browser/features/RightSidebar/Memory/MemoryTab.test.tsx +++ b/src/browser/features/RightSidebar/Memory/MemoryTab.test.tsx @@ -269,6 +269,39 @@ describe("MemoryTab", () => { expect(getByText(/^Project:/).textContent).not.toContain("manual"); }); + test("deduplicates identical consolidation summaries in the tooltip", async () => { + const sharedRecord = { + ...DEFAULT_CONSOLIDATION_RECORD, + summary: "shared consolidation summary", + }; + fake = createFakeMemoryApi([], { + consolidationStatus: { + workspaceRecord: sharedRecord, + projectRecord: sharedRecord, + globalRecord: sharedRecord, + latestHarvestRecord: null, + projectAvailable: true, + }, + }); + const { findByText } = render(); + + const workspaceLine = await findByText(/^Workspace: .*manual/); + const statusBlock = workspaceLine.parentElement; + expect(statusBlock).not.toBeNull(); + fireEvent.pointerMove(statusBlock!); + + await waitFor(() => { + const tooltipBlocks = Array.from( + document.querySelectorAll(".whitespace-pre-line") + ); + expect(tooltipBlocks.length).toBeGreaterThan(0); + for (const block of tooltipBlocks) { + const matches = block.textContent?.match(/shared consolidation summary/g) ?? []; + expect(matches).toHaveLength(1); + } + }); + }); + test("consolidation summary does not leave a native title tooltip", async () => { fake = createFakeMemoryApi([], { consolidationStatus: { diff --git a/src/node/services/historyService.ts b/src/node/services/historyService.ts index 364d15a45f..8a9f6e6c87 100644 --- a/src/node/services/historyService.ts +++ b/src/node/services/historyService.ts @@ -901,7 +901,7 @@ export class HistoryService { let summary: MuxMessage | undefined; const lowerBound = metadata.previousBoundaryHistorySequence; - await this.iterateForward(workspaceId, (chunk) => { + const iteration = await this.iterateFullHistory(workspaceId, "forward", (chunk) => { for (const message of chunk) { const sequence = message.metadata?.historySequence; if (!isNonNegativeInteger(sequence)) continue; @@ -921,6 +921,9 @@ export class HistoryService { messages.push(message); } }); + if (!iteration.success) { + return Err(`Failed to read compaction epoch messages: ${iteration.error}`); + } if (summary === undefined) { return Err(`Compaction summary not found: ${metadata.summaryMessageId}`);