From b55ac994ea47c49698d213f8dc6f1d0b412d3a05 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 3 Dec 2025 13:33:32 +0000 Subject: [PATCH] feat(web): prime group sessions with member roster --- CHANGELOG.md | 1 + docs/group-messages.md | 1 + src/auto-reply/reply.ts | 22 +++++++++++++++++++++ src/auto-reply/templating.ts | 5 +++++ src/web/auto-reply.ts | 5 +++++ src/web/inbound.ts | 38 ++++++++++++++++++++++++++++++++++++ 6 files changed, 72 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07d0882aa..a291a270f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Highlights - **Thinking directives & state:** `/t|/think|/thinking ` (aliases off|minimal|low|medium|high|max/highest). Inline applies to that message; directive-only message pins the level for the session; `/think:off` clears. Resolution: inline > session override > `inbound.reply.thinkingDefault` > off. Pi/Tau get `--thinking ` (except off); other agents append cue words (`think` → `think hard` → `think harder` → `ultrathink`). Heartbeat probe uses `HEARTBEAT /think:high`. - **Group chats (web provider):** New `inbound.groupChat` config (requireMention, mentionPatterns, historyLimit). Warelay now listens to WhatsApp groups, only replies when mentioned, injects recent group history into the prompt, and keeps group sessions separate from personal chats; heartbeats are skipped for group threads. +- **Group session primer:** The first turn of a group session now tells the agent it is in a WhatsApp group and lists known members/subject so it can address the right speaker. - **Verbose directives + session hints:** `/v|/verbose on|full|off` mirrors thinking: inline > session > config default. Directive-only replies with an acknowledgement; invalid levels return a hint. When enabled, tool results from JSON-emitting agents (Pi/Tau, etc.) are forwarded as metadata-only `[🛠️ ]` messages (now streamed as they happen), and new sessions surface a `🧭 New session: ` hint. - **Verbose tool coalescing:** successive tool results of the same tool within ~1s are batched into one `[🛠️ tool] arg1, arg2` message to reduce WhatsApp noise. - **Directive confirmations:** Directive-only messages now reply with an acknowledgement (`Thinking level set to high.` / `Thinking disabled.`) and reject unknown levels with a helpful hint (state is unchanged). diff --git a/docs/group-messages.md b/docs/group-messages.md index 9ff624a06..b9ee13359 100644 --- a/docs/group-messages.md +++ b/docs/group-messages.md @@ -9,6 +9,7 @@ Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that - Context injection: last N (default 50) group messages are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`. - Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so Tau/Claude know who is speaking. - Ephemeral/view-once: we unwrap those before extracting text/mentions, so pings inside them still trigger. +- New session primer: on the first turn of a group session we now prepend a short blurb to the model like `You are replying inside the WhatsApp group "". Group members: +44..., +43..., … Address the specific sender noted in the message context.` If metadata isn’t available we still tell the agent it’s a group chat. ## Config for Clawd UK (+447511247203) Add a `groupChat` block to `~/.warelay/warelay.json` so display-name pings work even when WhatsApp strips the visual `@` in the text body: diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index 20a69ae17..30cd7afb4 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -413,6 +413,25 @@ export async function getReplyFromConfig( isFirstTurnInSession && sessionCfg?.sessionIntro ? applyTemplate(sessionCfg.sessionIntro, sessionCtx) : ""; + const groupIntro = + isFirstTurnInSession && sessionCtx.ChatType === "group" + ? (() => { + const subject = sessionCtx.GroupSubject?.trim(); + const members = sessionCtx.GroupMembers?.trim(); + const subjectLine = subject + ? `You are replying inside the WhatsApp group "${subject}".` + : "You are replying inside a WhatsApp group chat."; + const membersLine = members + ? `Group members: ${members}.` + : undefined; + return [subjectLine, membersLine] + .filter(Boolean) + .join(" ") + .concat( + " Address the specific sender noted in the message context.", + ); + })() + : ""; const bodyPrefix = reply?.bodyPrefix ? applyTemplate(reply.bodyPrefix, sessionCtx) : ""; @@ -430,6 +449,9 @@ export async function getReplyFromConfig( if (sessionIntro) { prefixedBodyBase = `${sessionIntro}\n\n${prefixedBodyBase}`; } + if (groupIntro) { + prefixedBodyBase = `${groupIntro}\n\n${prefixedBodyBase}`; + } if (abortedHint) { prefixedBodyBase = `${abortedHint}\n\n${prefixedBodyBase}`; if (sessionEntry && sessionStore && sessionKey) { diff --git a/src/auto-reply/templating.ts b/src/auto-reply/templating.ts index 434f651fb..8d0b487ed 100644 --- a/src/auto-reply/templating.ts +++ b/src/auto-reply/templating.ts @@ -7,6 +7,11 @@ export type MsgContext = { MediaUrl?: string; MediaType?: string; Transcript?: string; + ChatType?: string; + GroupSubject?: string; + GroupMembers?: string; + SenderName?: string; + SenderE164?: string; }; export type TemplateContext = MsgContext & { diff --git a/src/web/auto-reply.ts b/src/web/auto-reply.ts index bbbbfcf50..06f691994 100644 --- a/src/web/auto-reply.ts +++ b/src/web/auto-reply.ts @@ -766,6 +766,11 @@ export async function monitorWebProvider( MediaPath: latest.mediaPath, MediaUrl: latest.mediaUrl, MediaType: latest.mediaType, + ChatType: latest.chatType, + GroupSubject: latest.groupSubject, + GroupMembers: latest.groupParticipants?.join(", "), + SenderName: latest.senderName, + SenderE164: latest.senderE164, }, { onReplyStart: latest.sendComposing, diff --git a/src/web/inbound.ts b/src/web/inbound.ts index b64831218..cd822fb89 100644 --- a/src/web/inbound.ts +++ b/src/web/inbound.ts @@ -39,6 +39,8 @@ export type WebInboundMessage = { senderJid?: string; senderE164?: string; senderName?: string; + groupSubject?: string; + groupParticipants?: string[]; mentionedJids?: string[]; selfJid?: string | null; selfE164?: string | null; @@ -73,6 +75,33 @@ export async function monitorWebInbox(options: { const selfJid = sock.user?.id; const selfE164 = selfJid ? jidToE164(selfJid) : null; const seen = new Set(); + const groupMetaCache = new Map< + string, + { subject?: string; participants?: string[]; expires: number } + >(); + const GROUP_META_TTL_MS = 5 * 60 * 1000; // 5 minutes + + const getGroupMeta = async (jid: string) => { + const cached = groupMetaCache.get(jid); + if (cached && cached.expires > Date.now()) return cached; + try { + const meta = await sock.groupMetadata(jid); + const participants = + meta.participants + ?.map((p) => jidToE164(p.id) ?? p.id) + .filter(Boolean) ?? []; + const entry = { + subject: meta.subject, + participants, + expires: Date.now() + GROUP_META_TTL_MS, + }; + groupMetaCache.set(jid, entry); + return entry; + } catch (err) { + logVerbose(`Failed to fetch group metadata for ${jid}: ${String(err)}`); + return { expires: Date.now() + GROUP_META_TTL_MS }; + } + }; sock.ev.on("messages.upsert", async (upsert) => { if (upsert.type !== "notify") return; @@ -109,6 +138,13 @@ export async function monitorWebInbox(options: { const from = group ? remoteJid : jidToE164(remoteJid); // Skip if we still can't resolve an id to key conversation if (!from) continue; + let groupSubject: string | undefined; + let groupParticipants: string[] | undefined; + if (group) { + const meta = await getGroupMeta(remoteJid); + groupSubject = meta.subject; + groupParticipants = meta.participants; + } // Filter unauthorized senders early to prevent wasted processing // and potential session corruption from Bad MAC errors @@ -197,6 +233,8 @@ export async function monitorWebInbox(options: { senderJid: participantJid, senderE164: senderE164 ?? undefined, senderName, + groupSubject, + groupParticipants, mentionedJids: mentionedJids ?? undefined, selfJid, selfE164,