Skip to content
Open
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
13 changes: 11 additions & 2 deletions backend/brain-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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");
Expand Down
48 changes: 48 additions & 0 deletions backend/memory/working-memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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],
Expand All @@ -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
Expand Down