refactor: normalize group session keys

This commit is contained in:
Peter Steinberger
2026-01-02 10:14:58 +01:00
parent 35582cfe8a
commit 9adbf47773
48 changed files with 537 additions and 86 deletions

View File

@@ -220,6 +220,7 @@ describe("trigger handling", () => {
From: "123@g.us",
To: "+2000",
ChatType: "group",
Surface: "whatsapp",
SenderE164: "+2000",
},
{},
@@ -230,7 +231,7 @@ describe("trigger handling", () => {
const store = JSON.parse(
await fs.readFile(cfg.session.store, "utf-8"),
) as Record<string, { groupActivation?: string }>;
expect(store["group:123@g.us"]?.groupActivation).toBe("always");
expect(store["whatsapp:group:123@g.us"]?.groupActivation).toBe("always");
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
});
});
@@ -244,6 +245,7 @@ describe("trigger handling", () => {
From: "123@g.us",
To: "+2000",
ChatType: "group",
Surface: "whatsapp",
SenderE164: "+999",
},
{},
@@ -270,6 +272,7 @@ describe("trigger handling", () => {
From: "123@g.us",
To: "+2000",
ChatType: "group",
Surface: "whatsapp",
SenderE164: "+2000",
GroupSubject: "Test Group",
GroupMembers: "Alice (+1), Bob (+2)",

View File

@@ -29,7 +29,9 @@ import { type ClawdisConfig, loadConfig } from "../config/config.js";
import {
DEFAULT_IDLE_MINUTES,
DEFAULT_RESET_TRIGGERS,
buildGroupDisplayName,
loadSessionStore,
resolveGroupSessionKey,
resolveSessionKey,
resolveSessionTranscriptPath,
resolveStorePath,
@@ -364,9 +366,9 @@ export async function getReplyFromConfig(
let persistedModelOverride: string | undefined;
let persistedProviderOverride: string | undefined;
const groupResolution = resolveGroupSessionKey(ctx);
const isGroup =
typeof ctx.From === "string" &&
(ctx.From.includes("@g.us") || ctx.From.startsWith("group:"));
ctx.ChatType?.trim().toLowerCase() === "group" || Boolean(groupResolution);
const triggerBodyNormalized = stripStructuralPrefixes(ctx.Body ?? "")
.trim()
.toLowerCase();
@@ -399,6 +401,16 @@ export async function getReplyFromConfig(
sessionKey = resolveSessionKey(sessionScope, ctx, mainKey);
sessionStore = loadSessionStore(storePath);
if (
groupResolution?.legacyKey &&
groupResolution.legacyKey !== sessionKey
) {
const legacyEntry = sessionStore[groupResolution.legacyKey];
if (legacyEntry && !sessionStore[sessionKey]) {
sessionStore[sessionKey] = legacyEntry;
delete sessionStore[groupResolution.legacyKey];
}
}
const entry = sessionStore[sessionKey];
const idleMs = idleMinutes * 60_000;
const freshEntry = entry && Date.now() - entry.updatedAt <= idleMs;
@@ -431,7 +443,35 @@ export async function getReplyFromConfig(
modelOverride: persistedModelOverride ?? baseEntry?.modelOverride,
providerOverride: persistedProviderOverride ?? baseEntry?.providerOverride,
queueMode: baseEntry?.queueMode,
displayName: baseEntry?.displayName,
chatType: baseEntry?.chatType,
surface: baseEntry?.surface,
subject: baseEntry?.subject,
room: baseEntry?.room,
space: baseEntry?.space,
};
if (groupResolution?.surface) {
const surface = groupResolution.surface;
const subject = ctx.GroupSubject?.trim();
const isRoomSurface = surface === "discord" || surface === "slack";
const nextRoom =
isRoomSurface && subject && subject.startsWith("#") ? subject : undefined;
const nextSubject = nextRoom ? undefined : subject;
sessionEntry.chatType = groupResolution.chatType ?? "group";
sessionEntry.surface = surface;
if (nextSubject) sessionEntry.subject = nextSubject;
if (nextRoom) sessionEntry.room = nextRoom;
sessionEntry.displayName = buildGroupDisplayName({
surface: sessionEntry.surface,
subject: sessionEntry.subject,
room: sessionEntry.room,
space: sessionEntry.space,
id: groupResolution.id,
key: sessionKey,
});
} else if (!sessionEntry.chatType) {
sessionEntry.chatType = "direct";
}
sessionStore[sessionKey] = sessionEntry;
await saveSessionStore(storePath, sessionStore);
@@ -1038,8 +1078,7 @@ export async function getReplyFromConfig(
// Prepend queued system events (transitions only) and (for new main sessions) a provider snapshot.
// Token efficiency: we filter out periodic/heartbeat noise and keep the lines compact.
const isGroupSession =
typeof ctx.From === "string" &&
(ctx.From.includes("@g.us") || ctx.From.startsWith("group:"));
sessionEntry?.chatType === "group" || sessionEntry?.chatType === "room";
const isMainSession =
!isGroupSession && sessionKey === (sessionCfg?.mainKey ?? "main");
if (isMainSession) {

View File

@@ -63,8 +63,9 @@ describe("buildStatusMessage", () => {
sessionId: "g1",
updatedAt: 0,
groupActivation: "always",
chatType: "group",
},
sessionKey: "group:123@g.us",
sessionKey: "whatsapp:group:123@g.us",
sessionScope: "per-sender",
webLinked: true,
});

View File

@@ -191,7 +191,13 @@ export function buildStatusMessage(args: StatusArgs): string {
.filter(Boolean)
.join(" • ");
const groupActivationLine = args.sessionKey?.startsWith("group:")
const isGroupSession =
entry?.chatType === "group" ||
entry?.chatType === "room" ||
Boolean(args.sessionKey?.includes(":group:")) ||
Boolean(args.sessionKey?.includes(":channel:")) ||
Boolean(args.sessionKey?.startsWith("group:"));
const groupActivationLine = isGroupSession
? `Group activation: ${entry?.groupActivation ?? "mention"}`
: undefined;