diff --git a/src/memory/extract.test.ts b/src/memory/extract.test.ts index df741c13..ea531fb2 100644 --- a/src/memory/extract.test.ts +++ b/src/memory/extract.test.ts @@ -67,6 +67,27 @@ test('buildExtractPrompt invokes MemoryWrite and renders existing memories', () assert.match(prompt, /project-style\.md/) }) +test('buildExtractPrompt strips background-task-result blocks but keeps surrounding turns', () => { + const bgBlock = [ + '', + 'Agentic RAG is the dominant pattern for personal knowledge management agents.', + '', + ].join('\n') + const prompt = buildExtractPrompt( + [ + createUserMessage(bgBlock, null, 10), + createUserMessage('thanks, please also note I prefer metric units', null, 20), + ], + [], + ) + // The worker that produced the bg result already extracted it into its own + // L3; the manager must not re-extract the same finding. + assert.doesNotMatch(prompt, /Agentic RAG is the dominant pattern/) + assert.match(prompt, /background-task result omitted/) + // The manager's own surrounding turn is still in the extraction window. + assert.match(prompt, /prefer metric units/) +}) + test('memoryExtractor system prompt bans JSON-text output (regression guard)', () => { // Output discipline lives in the role's system prompt, not in the // per-call user message (which previously duplicated this rule). The diff --git a/src/memory/extract.ts b/src/memory/extract.ts index 838d7a3f..d2b69eb0 100644 --- a/src/memory/extract.ts +++ b/src/memory/extract.ts @@ -172,6 +172,27 @@ function newestTimestamp(messages: Message[], fallback: number): number { return Math.max(fallback, ...messages.map(message => message.timestamp)) } +const BACKGROUND_RESULT_BLOCK_RE = + /]*>[\s\S]*?<\/background-task-result>/g + +/** + * A `` block is a finished worker's deliverable + * injected into the manager's prompt when a background dispatch completes. + * The worker that produced it already ran its own extraction over its fork + * transcript, so re-extracting the same finding from the manager's window + * would duplicate it into the manager's memory tier (2026-05-20 dogfood + * Bug 3: one PKM research topic landed 3 copies in the user-level root on + * top of the worker's own L3 entries). Replace each block with a one-line + * marker so the manager's surrounding turns still read coherently — the + * manager's own synthesis lives in its assistant messages, which are kept. + */ +export function stripBackgroundResultBlocks(text: string): string { + return text.replace( + BACKGROUND_RESULT_BLOCK_RE, + '[background-task result omitted — already extracted by the worker that produced it]', + ) +} + export function buildExtractPrompt( newMessages: Message[], existingMemories: MemoryEntry[], @@ -183,10 +204,9 @@ export function buildExtractPrompt( .join('\n') : '[none]' - const conversationText = newMessages - .map(message => messageToText(message)) - .join('\n\n') - .slice(0, 100_000) + const conversationText = stripBackgroundResultBlocks( + newMessages.map(message => messageToText(message)).join('\n\n'), + ).slice(0, 100_000) return [ 'Extract durable memories from the conversation segment below by calling the MemoryWrite tool. Follow the workflow and conventions from your system prompt.',