feat(web): prime group sessions with member roster
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
### Highlights
|
||||
- **Thinking directives & state:** `/t|/think|/thinking <level>` (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 <level>` (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 `[🛠️ <tool-name> <arg>]` messages (now streamed as they happen), and new sessions surface a `🧭 New session: <id>` 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).
|
||||
|
||||
@@ -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 "<subject>". 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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string>();
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user