feat(web): prime group sessions with member roster
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
### Highlights
|
### 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`.
|
- **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 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 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.
|
- **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).
|
- **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]`.
|
- 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.
|
- 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.
|
- 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)
|
## 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:
|
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
|
isFirstTurnInSession && sessionCfg?.sessionIntro
|
||||||
? applyTemplate(sessionCfg.sessionIntro, sessionCtx)
|
? 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
|
const bodyPrefix = reply?.bodyPrefix
|
||||||
? applyTemplate(reply.bodyPrefix, sessionCtx)
|
? applyTemplate(reply.bodyPrefix, sessionCtx)
|
||||||
: "";
|
: "";
|
||||||
@@ -430,6 +449,9 @@ export async function getReplyFromConfig(
|
|||||||
if (sessionIntro) {
|
if (sessionIntro) {
|
||||||
prefixedBodyBase = `${sessionIntro}\n\n${prefixedBodyBase}`;
|
prefixedBodyBase = `${sessionIntro}\n\n${prefixedBodyBase}`;
|
||||||
}
|
}
|
||||||
|
if (groupIntro) {
|
||||||
|
prefixedBodyBase = `${groupIntro}\n\n${prefixedBodyBase}`;
|
||||||
|
}
|
||||||
if (abortedHint) {
|
if (abortedHint) {
|
||||||
prefixedBodyBase = `${abortedHint}\n\n${prefixedBodyBase}`;
|
prefixedBodyBase = `${abortedHint}\n\n${prefixedBodyBase}`;
|
||||||
if (sessionEntry && sessionStore && sessionKey) {
|
if (sessionEntry && sessionStore && sessionKey) {
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ export type MsgContext = {
|
|||||||
MediaUrl?: string;
|
MediaUrl?: string;
|
||||||
MediaType?: string;
|
MediaType?: string;
|
||||||
Transcript?: string;
|
Transcript?: string;
|
||||||
|
ChatType?: string;
|
||||||
|
GroupSubject?: string;
|
||||||
|
GroupMembers?: string;
|
||||||
|
SenderName?: string;
|
||||||
|
SenderE164?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TemplateContext = MsgContext & {
|
export type TemplateContext = MsgContext & {
|
||||||
|
|||||||
@@ -766,6 +766,11 @@ export async function monitorWebProvider(
|
|||||||
MediaPath: latest.mediaPath,
|
MediaPath: latest.mediaPath,
|
||||||
MediaUrl: latest.mediaUrl,
|
MediaUrl: latest.mediaUrl,
|
||||||
MediaType: latest.mediaType,
|
MediaType: latest.mediaType,
|
||||||
|
ChatType: latest.chatType,
|
||||||
|
GroupSubject: latest.groupSubject,
|
||||||
|
GroupMembers: latest.groupParticipants?.join(", "),
|
||||||
|
SenderName: latest.senderName,
|
||||||
|
SenderE164: latest.senderE164,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onReplyStart: latest.sendComposing,
|
onReplyStart: latest.sendComposing,
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export type WebInboundMessage = {
|
|||||||
senderJid?: string;
|
senderJid?: string;
|
||||||
senderE164?: string;
|
senderE164?: string;
|
||||||
senderName?: string;
|
senderName?: string;
|
||||||
|
groupSubject?: string;
|
||||||
|
groupParticipants?: string[];
|
||||||
mentionedJids?: string[];
|
mentionedJids?: string[];
|
||||||
selfJid?: string | null;
|
selfJid?: string | null;
|
||||||
selfE164?: string | null;
|
selfE164?: string | null;
|
||||||
@@ -73,6 +75,33 @@ export async function monitorWebInbox(options: {
|
|||||||
const selfJid = sock.user?.id;
|
const selfJid = sock.user?.id;
|
||||||
const selfE164 = selfJid ? jidToE164(selfJid) : null;
|
const selfE164 = selfJid ? jidToE164(selfJid) : null;
|
||||||
const seen = new Set<string>();
|
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) => {
|
sock.ev.on("messages.upsert", async (upsert) => {
|
||||||
if (upsert.type !== "notify") return;
|
if (upsert.type !== "notify") return;
|
||||||
@@ -109,6 +138,13 @@ export async function monitorWebInbox(options: {
|
|||||||
const from = group ? remoteJid : jidToE164(remoteJid);
|
const from = group ? remoteJid : jidToE164(remoteJid);
|
||||||
// Skip if we still can't resolve an id to key conversation
|
// Skip if we still can't resolve an id to key conversation
|
||||||
if (!from) continue;
|
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
|
// Filter unauthorized senders early to prevent wasted processing
|
||||||
// and potential session corruption from Bad MAC errors
|
// and potential session corruption from Bad MAC errors
|
||||||
@@ -197,6 +233,8 @@ export async function monitorWebInbox(options: {
|
|||||||
senderJid: participantJid,
|
senderJid: participantJid,
|
||||||
senderE164: senderE164 ?? undefined,
|
senderE164: senderE164 ?? undefined,
|
||||||
senderName,
|
senderName,
|
||||||
|
groupSubject,
|
||||||
|
groupParticipants,
|
||||||
mentionedJids: mentionedJids ?? undefined,
|
mentionedJids: mentionedJids ?? undefined,
|
||||||
selfJid,
|
selfJid,
|
||||||
selfE164,
|
selfE164,
|
||||||
|
|||||||
Reference in New Issue
Block a user