From 2c714ac2e09f7e5fc4aaada0472cbc3eceb802ec Mon Sep 17 00:00:00 2001 From: Josh Lehman Date: Wed, 24 Jun 2026 13:46:26 -0700 Subject: [PATCH] fix(whatsapp): route group activation through session accessor (#96530) --- .../auto-reply/monitor/group-activation.ts | 42 ++++++++++++------- scripts/check-session-accessor-boundary.mjs | 1 + .../check-session-accessor-boundary.test.ts | 1 + 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/extensions/whatsapp/src/auto-reply/monitor/group-activation.ts b/extensions/whatsapp/src/auto-reply/monitor/group-activation.ts index 87d887717eb..8651e500f99 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/group-activation.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/group-activation.ts @@ -1,10 +1,14 @@ // Whatsapp plugin module implements group activation behavior. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/routing"; -import { updateSessionStore } from "openclaw/plugin-sdk/session-store-runtime"; +import { + getSessionEntry, + patchSessionEntry, + resolveStorePath, + type SessionEntry, +} from "openclaw/plugin-sdk/session-store-runtime"; import { resolveWhatsAppLegacyGroupSessionKey } from "../../group-session-key.js"; import { resolveWhatsAppInboundPolicy } from "../../inbound-policy.js"; -import { loadSessionStore, resolveStorePath } from "../config.runtime.js"; import { normalizeGroupActivation } from "./group-activation.runtime.js"; function hasNamedWhatsAppAccounts(cfg: OpenClawConfig) { @@ -28,6 +32,7 @@ function isActivationOnlyEntry( ); } +/** Resolves group activation for a WhatsApp conversation and backfills scoped session metadata. */ export async function resolveGroupActivationFor(params: { cfg: OpenClawConfig; accountId?: string | null; @@ -38,13 +43,15 @@ export async function resolveGroupActivationFor(params: { const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.agentId, }); - const store = loadSessionStore(storePath); + const sessionScope = { storePath, agentId: params.agentId }; const legacySessionKey = resolveWhatsAppLegacyGroupSessionKey({ sessionKey: params.sessionKey, accountId: params.accountId, }); - const legacyEntry = legacySessionKey ? store[legacySessionKey] : undefined; - const scopedEntry = store[params.sessionKey]; + const legacyEntry = legacySessionKey + ? getSessionEntry({ ...sessionScope, sessionKey: legacySessionKey }) + : undefined; + const scopedEntry = getSessionEntry({ ...sessionScope, sessionKey: params.sessionKey }); const normalizedAccountId = normalizeAccountId(params.accountId); const ignoreScopedActivation = normalizedAccountId === DEFAULT_ACCOUNT_ID && @@ -54,15 +61,22 @@ export async function resolveGroupActivationFor(params: { (ignoreScopedActivation ? undefined : scopedEntry?.groupActivation) ?? legacyEntry?.groupActivation; if (activation !== undefined && scopedEntry?.groupActivation === undefined) { - await updateSessionStore(storePath, (nextStore) => { - const nextScopedEntry = nextStore[params.sessionKey]; - if (nextScopedEntry?.groupActivation !== undefined) { - return; - } - nextStore[params.sessionKey] = { - ...nextScopedEntry, - groupActivation: activation, - }; + // Activation-only backfills must not synthesize session ids or activity. + // replaceEntry preserves existing scoped metadata while keeping fallback writes sparse. + await patchSessionEntry({ + ...sessionScope, + sessionKey: params.sessionKey, + fallbackEntry: {} as SessionEntry, + replaceEntry: true, + update: (entry) => { + if (entry.groupActivation !== undefined) { + return null; + } + return { + ...entry, + groupActivation: activation, + }; + }, }); } const requireMention = resolveWhatsAppInboundPolicy({ diff --git a/scripts/check-session-accessor-boundary.mjs b/scripts/check-session-accessor-boundary.mjs index 761b5312b1e..b20959f5d5b 100644 --- a/scripts/check-session-accessor-boundary.mjs +++ b/scripts/check-session-accessor-boundary.mjs @@ -143,6 +143,7 @@ export const migratedBundledPluginSessionAccessorFiles = new Set([ "extensions/telegram/src/bot-message-dispatch.ts", "extensions/telegram/src/bot-native-commands.ts", "extensions/voice-call/src/response-generator.ts", + "extensions/whatsapp/src/auto-reply/monitor/group-activation.ts", ]); export const migratedEmbeddedAgentSessionTargetFiles = new Set([ diff --git a/test/scripts/check-session-accessor-boundary.test.ts b/test/scripts/check-session-accessor-boundary.test.ts index 28eb3d8eca9..c47e49ccda7 100644 --- a/test/scripts/check-session-accessor-boundary.test.ts +++ b/test/scripts/check-session-accessor-boundary.test.ts @@ -98,6 +98,7 @@ describe("session accessor boundary guard", () => { "extensions/telegram/src/bot-message-dispatch.ts", "extensions/telegram/src/bot-native-commands.ts", "extensions/voice-call/src/response-generator.ts", + "extensions/whatsapp/src/auto-reply/monitor/group-activation.ts", ]), ); });