diff --git a/backend/brain-prompt.ts b/backend/brain-prompt.ts index f62fd95f..ddc7be41 100644 --- a/backend/brain-prompt.ts +++ b/backend/brain-prompt.ts @@ -2,6 +2,7 @@ import type { Observation } from "./observer.js"; import type { MemoryNode, WorkingMemory } from "./memory/types.js"; import type { MemoryGraph } from "./memory/graph.js"; import { serializeNodesForPrompt, collectRelevantRejectedEdges, formatRejectedEdgesForPrompt } from "./memory/activation.js"; +import { isNewsletterParticipant } from "./memory/working-memory.js"; import { ariaPersonality } from "./aria-identity.js"; import type { CharacterOverride } from "./aria-identity.js"; import { getBrainConfig, getCharacterPreset, getOwnerLocalTime } from "./brain-config.js"; @@ -316,9 +317,17 @@ function formatWorkingMemory(wm: WorkingMemory): string { parts.push(`Follow-ups:\n${fuLines.join("\n")}`); } - // Active conversation threads + // Active conversation threads — filter newsletter/automation participants so + // promotional streams (AutoScout24 saved searches, no-reply notifications, etc.) + // don't crowd out real conversations in the prompt. if (wm.conversationThreads && wm.conversationThreads.length > 0) { - const activeThreads = wm.conversationThreads.filter(t => t.status === "active").slice(0, 5); + const activeThreads = wm.conversationThreads + .filter(t => t.status === "active") + .filter(t => { + const list = Array.isArray(t.participants) ? t.participants : (t.participants ? [t.participants] : []); + return !list.some(p => isNewsletterParticipant(p)); + }) + .slice(0, 5); if (activeThreads.length > 0) { const threadLines = activeThreads.map(t => { const who = Array.isArray(t.participants) ? t.participants.join(", ") : (t.participants || "unknown"); @@ -895,13 +904,18 @@ function buildCommitmentsBlock( ): string { const sections: string[] = []; - // Moltbook-specific section (backwards compat) + // Moltbook-specific section. recentMoltbookActivity is sourced from the + // sa_moltbook sub-agent's run summary/details fields — intra-run scratch + // narrative ("I'll reply to 6 comments", "let me write a helper") that was + // already executed within that sub-agent run. Show the activity for + // context, but do NOT mine commitments from it: those phrases are not + // promises made to a human channel, they are sub-agent self-narration. if (recentMoltbookActivity && recentMoltbookActivity.length > 0) { - const moltbookCommitments = recentMoltbookActivity.flatMap(text => extractAndClassifyCommitments(text)); - const detectedSection = moltbookCommitments.length > 0 - ? `\nDetected commitment language in Moltbook posts:\n${moltbookCommitments.map(c => `- [${c.weight}] "${c.commitment}" (pattern: ${c.pattern})`).join("\n")}\n` - : ""; - sections.push(`Moltbook posts/comments:\n${detectedSection}${recentMoltbookActivity.map((text, i) => ` ${i + 1}. ${text.slice(0, 300)}`).join("\n")}`); + // These are non-actionable context only, so cap display to the 3 most + // recent runs and shorten each summary — showing all ~10 every reflect + // wastes prompt budget without changing decisions. + const moltbookForDisplay = recentMoltbookActivity.slice(0, 3); + sections.push(`Moltbook posts/comments (sa_moltbook sub-agent run summaries — already executed, NOT personal commitments):\n${moltbookForDisplay.map((text, i) => ` ${i + 1}. ${text.slice(0, 150)}`).join("\n")}`); } // General outgoing activity (WhatsApp, email, brain messages) — grouped by conversation @@ -937,6 +951,7 @@ ACTION REQUIRED: 3. For any non-trivial commitment not already tracked, create a goal via goalOps. 4. Trivial commitments (quick lookups/checks) are filtered out automatically. 5. Update progress on existing commitment-sourced goals. +6. Moltbook sub-agent run summaries are shown for context only — do NOT treat phrases like "I'll reply to X comments" or "let me write a helper" inside those summaries as personal commitments. They were already executed inside the sub-agent run. `; } diff --git a/backend/memory/working-memory.ts b/backend/memory/working-memory.ts index 119e58fd..73365859 100644 --- a/backend/memory/working-memory.ts +++ b/backend/memory/working-memory.ts @@ -181,6 +181,46 @@ export function populateTemporalContext(wm: WorkingMemory): void { // ── Conversation Thread Tracking ── +// Newsletter / automation sender patterns. If a participant string matches one +// of these, the thread is treated as one-way noise — never promoted to "active" +// in the prompt, and never written as a fresh thread. Defense in depth: applied +// at both write time (here) and render time (brain-prompt.ts). +const NEWSLETTER_SUBSTRINGS = [ + "noreply", + "no-reply", + "notifications.", + "newsletter", + "savedsearches", + "mailings.", + "updates@", + "bounce", +]; + +const NEWSLETTER_DOMAINS = [ + "autoscout24", + "schoolkassa", + "rdw", + "anwb.nl/notifications", +]; + +export function isNewsletterParticipant(participant: string | undefined | null): boolean { + if (!participant) return false; + const p = participant.toLowerCase(); + for (const sub of NEWSLETTER_SUBSTRINGS) { + if (p.includes(sub)) return true; + } + for (const dom of NEWSLETTER_DOMAINS) { + if (p.includes(dom)) return true; + } + return false; +} + +function threadHasNewsletterParticipant(participants: string[] | string | undefined): boolean { + if (!participants) return false; + const list = Array.isArray(participants) ? participants : [participants]; + return list.some(isNewsletterParticipant); +} + export function updateConversationThreads(wm: WorkingMemory, observations: Observation[]): void { const now = Date.now(); const STALE_THRESHOLD = 48 * 60 * 60 * 1000; // 48 hours @@ -194,6 +234,10 @@ export function updateConversationThreads(wm: WorkingMemory, observations: Obser let thread = wm.conversationThreads.find(t => t.id === key); if (!thread) { + // Reject newsletter/automation senders at write time — they're never real conversations. + if (isNewsletterParticipant(obs.sender) || isNewsletterParticipant(obs.chatName) || isNewsletterParticipant(obs.chatJid)) { + continue; + } thread = { id: key, participants: [obs.sender], @@ -214,6 +258,10 @@ export function updateConversationThreads(wm: WorkingMemory, observations: Obser } } + // Sweep any pre-existing newsletter threads that slipped in during prior ticks + // (before this guard was added, or via an alternative write path). + wm.conversationThreads = wm.conversationThreads.filter(t => !threadHasNewsletterParticipant(t.participants)); + // Thread lifecycle: active → stale (48h) → closed (7d) → removed (14d) const CLOSED_THRESHOLD = 7 * 24 * 60 * 60 * 1000; // 7 days since last message const REMOVE_THRESHOLD = 14 * 24 * 60 * 60 * 1000; // 14 days since last message