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
27 changes: 19 additions & 8 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 Expand Up @@ -895,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
Expand Down Expand Up @@ -937,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.
`;
}

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