fix: persist session origin metadata
This commit is contained in:
@@ -11,6 +11,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Plugins: add the bundled BlueBubbles channel plugin (disabled by default).
|
- Plugins: add the bundled BlueBubbles channel plugin (disabled by default).
|
||||||
- Plugins: migrate bundled messaging extensions to the plugin SDK; resolve plugin-sdk imports in loader.
|
- Plugins: migrate bundled messaging extensions to the plugin SDK; resolve plugin-sdk imports in loader.
|
||||||
- Plugins: migrate the Zalo plugin to the shared plugin SDK runtime.
|
- Plugins: migrate the Zalo plugin to the shared plugin SDK runtime.
|
||||||
|
- Sessions: persist origin metadata for last-route updates so DM/channel/group sessions keep explainers. (#1133) — thanks @adam91holt.
|
||||||
|
|
||||||
## 2026.1.17-5
|
## 2026.1.17-5
|
||||||
|
|
||||||
|
|||||||
@@ -122,3 +122,10 @@ Each session entry records where it came from (best-effort) in `origin`:
|
|||||||
- `from`/`to`: raw routing ids from the inbound envelope
|
- `from`/`to`: raw routing ids from the inbound envelope
|
||||||
- `accountId`: provider account id (when multi-account)
|
- `accountId`: provider account id (when multi-account)
|
||||||
- `threadId`: thread/topic id when the channel supports it
|
- `threadId`: thread/topic id when the channel supports it
|
||||||
|
The origin fields are populated for direct messages, channels, and groups. If a
|
||||||
|
connector only updates delivery routing (for example, to keep a DM main session
|
||||||
|
fresh), it should still provide inbound context so the session keeps its
|
||||||
|
explainer metadata. Extensions can do this by sending `ConversationLabel`,
|
||||||
|
`GroupSubject`, `GroupChannel`, `GroupSpace`, and `SenderName` in the inbound
|
||||||
|
context and calling `recordSessionMetaFromInbound` (or passing the same context
|
||||||
|
to `updateLastRoute`).
|
||||||
|
|||||||
@@ -552,6 +552,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|||||||
channel: "matrix",
|
channel: "matrix",
|
||||||
to: `room:${roomId}`,
|
to: `room:${roomId}`,
|
||||||
accountId: route.accountId,
|
accountId: route.accountId,
|
||||||
|
ctx: ctxPayload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,36 @@ describe("sessions", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updateLastRoute records origin + group metadata when ctx is provided", async () => {
|
||||||
|
const sessionKey = "agent:main:whatsapp:group:123@g.us";
|
||||||
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-"));
|
||||||
|
const storePath = path.join(dir, "sessions.json");
|
||||||
|
await fs.writeFile(storePath, "{}", "utf-8");
|
||||||
|
|
||||||
|
await updateLastRoute({
|
||||||
|
storePath,
|
||||||
|
sessionKey,
|
||||||
|
deliveryContext: {
|
||||||
|
channel: "whatsapp",
|
||||||
|
to: "123@g.us",
|
||||||
|
},
|
||||||
|
ctx: {
|
||||||
|
Provider: "whatsapp",
|
||||||
|
ChatType: "group",
|
||||||
|
GroupSubject: "Family",
|
||||||
|
From: "123@g.us",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = loadSessionStore(storePath);
|
||||||
|
expect(store[sessionKey]?.subject).toBe("Family");
|
||||||
|
expect(store[sessionKey]?.channel).toBe("whatsapp");
|
||||||
|
expect(store[sessionKey]?.groupId).toBe("123@g.us");
|
||||||
|
expect(store[sessionKey]?.origin?.label).toBe("Family id:123@g.us");
|
||||||
|
expect(store[sessionKey]?.origin?.provider).toBe("whatsapp");
|
||||||
|
expect(store[sessionKey]?.origin?.chatType).toBe("group");
|
||||||
|
});
|
||||||
|
|
||||||
it("updateSessionStore preserves concurrent additions", async () => {
|
it("updateSessionStore preserves concurrent additions", async () => {
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-"));
|
||||||
const storePath = path.join(dir, "sessions.json");
|
const storePath = path.join(dir, "sessions.json");
|
||||||
|
|||||||
@@ -368,8 +368,10 @@ export async function updateLastRoute(params: {
|
|||||||
to?: string;
|
to?: string;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
deliveryContext?: DeliveryContext;
|
deliveryContext?: DeliveryContext;
|
||||||
|
ctx?: MsgContext;
|
||||||
|
groupResolution?: import("./types.js").GroupKeyResolution | null;
|
||||||
}) {
|
}) {
|
||||||
const { storePath, sessionKey, channel, to, accountId } = params;
|
const { storePath, sessionKey, channel, to, accountId, ctx } = params;
|
||||||
return await withSessionStoreLock(storePath, async () => {
|
return await withSessionStoreLock(storePath, async () => {
|
||||||
const store = loadSessionStore(storePath);
|
const store = loadSessionStore(storePath);
|
||||||
const existing = store[sessionKey];
|
const existing = store[sessionKey];
|
||||||
@@ -389,13 +391,22 @@ export async function updateLastRoute(params: {
|
|||||||
accountId: merged?.accountId,
|
accountId: merged?.accountId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const next = mergeSessionEntry(existing, {
|
const metaPatch = ctx
|
||||||
|
? deriveSessionMetaPatch({
|
||||||
|
ctx,
|
||||||
|
sessionKey,
|
||||||
|
existing,
|
||||||
|
groupResolution: params.groupResolution,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
const basePatch: Partial<SessionEntry> = {
|
||||||
updatedAt: Math.max(existing?.updatedAt ?? 0, now),
|
updatedAt: Math.max(existing?.updatedAt ?? 0, now),
|
||||||
deliveryContext: normalized.deliveryContext,
|
deliveryContext: normalized.deliveryContext,
|
||||||
lastChannel: normalized.lastChannel,
|
lastChannel: normalized.lastChannel,
|
||||||
lastTo: normalized.lastTo,
|
lastTo: normalized.lastTo,
|
||||||
lastAccountId: normalized.lastAccountId,
|
lastAccountId: normalized.lastAccountId,
|
||||||
});
|
};
|
||||||
|
const next = mergeSessionEntry(existing, metaPatch ? { ...basePatch, ...metaPatch } : basePatch);
|
||||||
store[sessionKey] = next;
|
store[sessionKey] = next;
|
||||||
await saveSessionStoreUnlocked(storePath, store);
|
await saveSessionStoreUnlocked(storePath, store);
|
||||||
return next;
|
return next;
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
|||||||
to: `user:${author.id}`,
|
to: `user:${author.id}`,
|
||||||
accountId: route.accountId,
|
accountId: route.accountId,
|
||||||
},
|
},
|
||||||
|
ctx: ctxPayload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -475,6 +475,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
|||||||
to,
|
to,
|
||||||
accountId: route.accountId,
|
accountId: route.accountId,
|
||||||
},
|
},
|
||||||
|
ctx: ctxPayload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
|||||||
to: entry.senderRecipient,
|
to: entry.senderRecipient,
|
||||||
accountId: route.accountId,
|
accountId: route.accountId,
|
||||||
},
|
},
|
||||||
|
ctx: ctxPayload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
|||||||
to: `user:${message.user}`,
|
to: `user:${message.user}`,
|
||||||
accountId: route.accountId,
|
accountId: route.accountId,
|
||||||
},
|
},
|
||||||
|
ctx: prepared.ctxPayload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -537,6 +537,7 @@ export const buildTelegramMessageContext = async ({
|
|||||||
to: String(chatId),
|
to: String(chatId),
|
||||||
accountId: route.accountId,
|
accountId: route.accountId,
|
||||||
},
|
},
|
||||||
|
ctx: ctxPayload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { MsgContext } from "../../../auto-reply/templating.js";
|
||||||
import type { loadConfig } from "../../../config/config.js";
|
import type { loadConfig } from "../../../config/config.js";
|
||||||
import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js";
|
import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js";
|
||||||
import { formatError } from "../../session.js";
|
import { formatError } from "../../session.js";
|
||||||
@@ -20,6 +21,7 @@ export function updateLastRouteInBackground(params: {
|
|||||||
channel: "whatsapp";
|
channel: "whatsapp";
|
||||||
to: string;
|
to: string;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
|
ctx?: MsgContext;
|
||||||
warn: (obj: unknown, msg: string) => void;
|
warn: (obj: unknown, msg: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||||
@@ -33,6 +35,7 @@ export function updateLastRouteInBackground(params: {
|
|||||||
to: params.to,
|
to: params.to,
|
||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
},
|
},
|
||||||
|
ctx: params.ctx,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
params.warn(
|
params.warn(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { MsgContext } from "../../../auto-reply/templating.js";
|
||||||
import type { getReplyFromConfig } from "../../../auto-reply/reply.js";
|
import type { getReplyFromConfig } from "../../../auto-reply/reply.js";
|
||||||
import type { loadConfig } from "../../../config/config.js";
|
import type { loadConfig } from "../../../config/config.js";
|
||||||
import { logVerbose } from "../../../globals.js";
|
import { logVerbose } from "../../../globals.js";
|
||||||
@@ -94,6 +95,22 @@ export function createWebOnMessageHandler(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (msg.chatType === "group") {
|
if (msg.chatType === "group") {
|
||||||
|
const metaCtx = {
|
||||||
|
From: msg.from,
|
||||||
|
To: msg.to,
|
||||||
|
SessionKey: route.sessionKey,
|
||||||
|
AccountId: route.accountId,
|
||||||
|
ChatType: msg.chatType,
|
||||||
|
ConversationLabel: conversationId,
|
||||||
|
GroupSubject: msg.groupSubject,
|
||||||
|
SenderName: msg.senderName,
|
||||||
|
SenderId: msg.senderJid?.trim() || msg.senderE164,
|
||||||
|
SenderE164: msg.senderE164,
|
||||||
|
Provider: "whatsapp",
|
||||||
|
Surface: "whatsapp",
|
||||||
|
OriginatingChannel: "whatsapp",
|
||||||
|
OriginatingTo: conversationId,
|
||||||
|
} satisfies MsgContext;
|
||||||
updateLastRouteInBackground({
|
updateLastRouteInBackground({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
backgroundTasks: params.backgroundTasks,
|
backgroundTasks: params.backgroundTasks,
|
||||||
@@ -102,6 +119,7 @@ export function createWebOnMessageHandler(params: {
|
|||||||
channel: "whatsapp",
|
channel: "whatsapp",
|
||||||
to: conversationId,
|
to: conversationId,
|
||||||
accountId: route.accountId,
|
accountId: route.accountId,
|
||||||
|
ctx: metaCtx,
|
||||||
warn: params.replyLogger.warn.bind(params.replyLogger),
|
warn: params.replyLogger.warn.bind(params.replyLogger),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -206,26 +206,15 @@ export async function processMessage(params: {
|
|||||||
whatsappInboundLog.debug(`Inbound body: ${elide(combinedBody, 400)}`);
|
whatsappInboundLog.debug(`Inbound body: ${elide(combinedBody, 400)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.msg.chatType !== "group") {
|
const dmRouteTarget =
|
||||||
const to = (() => {
|
params.msg.chatType !== "group"
|
||||||
if (params.msg.senderE164) return normalizeE164(params.msg.senderE164);
|
? (() => {
|
||||||
// In direct chats, `msg.from` is already the canonical conversation id.
|
if (params.msg.senderE164) return normalizeE164(params.msg.senderE164);
|
||||||
if (params.msg.from.includes("@")) return jidToE164(params.msg.from);
|
// In direct chats, `msg.from` is already the canonical conversation id.
|
||||||
return normalizeE164(params.msg.from);
|
if (params.msg.from.includes("@")) return jidToE164(params.msg.from);
|
||||||
})();
|
return normalizeE164(params.msg.from);
|
||||||
if (to) {
|
})()
|
||||||
updateLastRouteInBackground({
|
: undefined;
|
||||||
cfg: params.cfg,
|
|
||||||
backgroundTasks: params.backgroundTasks,
|
|
||||||
storeAgentId: params.route.agentId,
|
|
||||||
sessionKey: params.route.mainSessionKey,
|
|
||||||
channel: "whatsapp",
|
|
||||||
to,
|
|
||||||
accountId: params.route.accountId,
|
|
||||||
warn: params.replyLogger.warn.bind(params.replyLogger),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const textLimit = params.maxMediaTextChunkLimit ?? resolveTextChunkLimit(params.cfg, "whatsapp");
|
const textLimit = params.maxMediaTextChunkLimit ?? resolveTextChunkLimit(params.cfg, "whatsapp");
|
||||||
let didLogHeartbeatStrip = false;
|
let didLogHeartbeatStrip = false;
|
||||||
@@ -285,6 +274,20 @@ export async function processMessage(params: {
|
|||||||
OriginatingTo: params.msg.from,
|
OriginatingTo: params.msg.from,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (dmRouteTarget) {
|
||||||
|
updateLastRouteInBackground({
|
||||||
|
cfg: params.cfg,
|
||||||
|
backgroundTasks: params.backgroundTasks,
|
||||||
|
storeAgentId: params.route.agentId,
|
||||||
|
sessionKey: params.route.mainSessionKey,
|
||||||
|
channel: "whatsapp",
|
||||||
|
to: dmRouteTarget,
|
||||||
|
accountId: params.route.accountId,
|
||||||
|
ctx: ctxPayload,
|
||||||
|
warn: params.replyLogger.warn.bind(params.replyLogger),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||||
agentId: params.route.agentId,
|
agentId: params.route.agentId,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user