From 942b42709159aa652ec3b348dc4f70684bef007b Mon Sep 17 00:00:00 2001 From: ARIA Date: Wed, 20 May 2026 22:06:14 +0000 Subject: [PATCH 1/3] ARIA self-improvement: filter newsletter senders from active-threads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drift audit 2026-05-20 flagged that the "Active threads" section of the working-memory prompt was being polluted by promotional/automated streams (currently the AutoScout24 "Nieuwe matches voor je Zoekopdracht" newsletter was the *only* active thread shown). Real signal-to-noise on that section had dropped to 0%. Defense in depth: - working-memory.ts: introduce isNewsletterParticipant() and reject new threads whose sender/chat matches noreply / no-reply / notifications. / newsletter / savedsearches / mailings. / updates@ / bounce, or known one-way notification domains (autoscout24, schoolkassa, rdw, anwb notifications). Also sweep any pre-existing newsletter threads on every update tick — fixes the currently-stuck AutoScout24 entry. - brain-prompt.ts: filter active threads at render time using the same helper, so even if a newsletter slips past the write-time guard via another path, the prompt stays clean. Intent-summary: Newsletter and automation senders were being promoted to "active conversation threads" in the working-memory prompt, crowding out real conversations Gillis is in. Intent-tokens: newsletter, noise, active-threads, prompt-pollution, working-memory, automation-sender, filter Co-Authored-By: Claude Opus 4.7 --- backend/brain-prompt.ts | 13 +++++++-- backend/memory/working-memory.ts | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/backend/brain-prompt.ts b/backend/brain-prompt.ts index f62fd95f..3235a0a2 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"); 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 From 75a812935acf54b8d60bdaba571d18a6bfb45754 Mon Sep 17 00:00:00 2001 From: ARIA Date: Sat, 23 May 2026 10:07:47 +0000 Subject: [PATCH 2/3] ARIA self-improvement: skip commitment extraction on sa_moltbook sub-agent run summaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reflect-tick commitment-review surface was mining intra-run scratch text from sa_moltbook sub-agent run summaries/details (e.g. "I'll reply to the 6 highest-signal comments", "let me write a helper that handles verification") and surfacing them as personal commitments needing follow-through. Those phrases were sub-agent self-narration about actions it already executed within that same run — not promises to a human channel. Fix in buildCommitmentsBlock (backend/brain-prompt.ts): the Moltbook activity coming from getRecentMoltbookActivity() is sourced entirely from sub-agent run summary/details fields, so stop running extractAndClassifyCommitments() over it. Still show the activity for context, but explicitly label the section as "already executed, NOT personal commitments" and add an action-line note telling reflect not to treat those phrases as promises. extractAndClassifyCommitments() is still applied to recentOutgoingActivity (whatsapp DMs, email, brain messages to human channels), where the audience is actually a human and commitment language is meaningful. Intent-summary: phantom commitments were being surfaced from sub-agent intra-run narrative text because the commitment extractor did not distinguish sub-agent task transcripts from real human-channel messages. Intent-tokens: subagent, commitment, attribution, phantom, moltbook, transcript, narration Co-Authored-By: Claude Opus 4.7 --- backend/brain-prompt.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/brain-prompt.ts b/backend/brain-prompt.ts index 3235a0a2..2d3fc8e9 100644 --- a/backend/brain-prompt.ts +++ b/backend/brain-prompt.ts @@ -904,13 +904,14 @@ 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")}`); + sections.push(`Moltbook posts/comments (sa_moltbook sub-agent run summaries — already executed, NOT personal commitments):\n${recentMoltbookActivity.map((text, i) => ` ${i + 1}. ${text.slice(0, 300)}`).join("\n")}`); } // General outgoing activity (WhatsApp, email, brain messages) — grouped by conversation @@ -946,6 +947,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. `; } From 9aa4c8e5f83c284a4ef5c08447da4626ffcd2814 Mon Sep 17 00:00:00 2001 From: ARIA Date: Tue, 26 May 2026 22:05:18 +0000 Subject: [PATCH 3/3] ARIA self-improvement: cap sa_moltbook run summaries to 3 most recent, 150 chars The commitment-review block in buildCommitmentsBlock() listed all ~10 sa_moltbook sub-agent run summaries at 300 chars each. These are explicitly context-only / non-actionable (per 75a8129), so the full list wastes ~3KB of reflect-prompt budget every cycle without changing any decision. Slice to the 3 most recent entries and truncate each to 150 chars. Non-actionable labeling is unchanged; display volume only. Intent-summary: Non-actionable sub-agent run summaries flooded the reflect prompt, crowding out decision-relevant context with redundant noise. Intent-tokens: prompt, noise, truncation, moltbook, reflect, context, verbosity Co-Authored-By: Claude Opus 4.7 --- backend/brain-prompt.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/brain-prompt.ts b/backend/brain-prompt.ts index 2d3fc8e9..ddc7be41 100644 --- a/backend/brain-prompt.ts +++ b/backend/brain-prompt.ts @@ -911,7 +911,11 @@ function buildCommitmentsBlock( // 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) { - sections.push(`Moltbook posts/comments (sa_moltbook sub-agent run summaries — already executed, NOT personal commitments):\n${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