From c84edb4924976cc2182983f869f9498718f42495 Mon Sep 17 00:00:00 2001 From: zouyicheng Date: Wed, 20 May 2026 17:06:33 +0800 Subject: [PATCH] fix(memory): skip background-task-result blocks in the extraction window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 3 from the 2026-05-20 dogfood had three parts; the prompt PR (ce8ef67) fixed transient snapshots and role-name filenames. This closes the third: cross-layer duplication. When a background dispatch completes, its deliverable is injected into the manager's prompt as a block. The worker that produced it has already run its own extraction over its fork transcript (into its role-private L3). The manager's afterEndTurn extraction then sees the same block and re-extracts the finding into the user-level root — and cannot dedup it, because the manager's existing-memories list does not include worker L3 dirs. The dogfood saw one PKM research topic land 3 copies in the user root on top of the worker's own L3 entries. buildExtractPrompt now runs stripBackgroundResultBlocks over the rendered conversation text: each span is replaced with a one-line marker. The manager's own assistant messages — its synthesis and reactions — are untouched and still extracted. Applies to every extraction (manager and worker), so a worker that itself received a sub-dispatch result also won't re-extract it. The remaining same-topic copies across role-private L3 dirs are by design (per-role extraction) and are the curator's job to reconcile — Bug 4's fix restored the curator so that path works again. New test in extract.test.ts. typecheck clean, 1463/1463 pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/memory/extract.test.ts | 21 +++++++++++++++++++++ src/memory/extract.ts | 28 ++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 4 deletions(-) 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.',