Skip to content
Merged
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
21 changes: 21 additions & 0 deletions src/memory/extract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
'<background-task-result label="PKM research" outcome="success" dispatchId="d1">',
'Agentic RAG is the dominant pattern for personal knowledge management agents.',
'</background-task-result>',
].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
Expand Down
28 changes: 24 additions & 4 deletions src/memory/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
/<background-task-result\b[^>]*>[\s\S]*?<\/background-task-result>/g

/**
* A `<background-task-result>` 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[],
Expand All @@ -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.',
Expand Down