fix(hooks): add isGroup and groupId to message:sent context

Adds group context fields to MessageSentHookContext so hooks can
correlate sent events with received events for the same conversation.

Previously, message:received included isGroup/groupId but message:sent
did not, forcing hooks to use mismatched identifiers (e.g. groupId vs
numeric chat ID) when tracking conversations.

Fields are derived from MsgContext in dispatch-from-config and threaded
through route-reply and deliver via the mirror parameter.

Addresses feedback from matskevich (production user, 550+ events)
reported on PR #6797.
This commit is contained in:
Eric Lytle
2026-03-02 12:02:48 +00:00
committed by Peter Steinberger
parent 7ad6a04058
commit b5102ba4f9
4 changed files with 29 additions and 0 deletions

View File

@@ -106,6 +106,9 @@ export async function dispatchReplyFromConfig(params: {
const sessionKey = ctx.SessionKey; const sessionKey = ctx.SessionKey;
const startTime = diagnosticsEnabled ? Date.now() : 0; const startTime = diagnosticsEnabled ? Date.now() : 0;
const canTrackSession = diagnosticsEnabled && Boolean(sessionKey); const canTrackSession = diagnosticsEnabled && Boolean(sessionKey);
const isGroup = Boolean(ctx.GroupSubject || ctx.GroupChannel);
const groupId =
ctx.From?.includes(":group:") || ctx.From?.includes(":channel:") ? ctx.From : undefined;
const recordProcessed = ( const recordProcessed = (
outcome: "completed" | "skipped" | "error", outcome: "completed" | "skipped" | "error",
@@ -291,6 +294,8 @@ export async function dispatchReplyFromConfig(params: {
cfg, cfg,
abortSignal, abortSignal,
mirror, mirror,
isGroup,
groupId,
}); });
if (!result.ok) { if (!result.ok) {
logVerbose(`dispatch-from-config: route-reply failed: ${result.error ?? "unknown error"}`); logVerbose(`dispatch-from-config: route-reply failed: ${result.error ?? "unknown error"}`);
@@ -316,6 +321,8 @@ export async function dispatchReplyFromConfig(params: {
accountId: ctx.AccountId, accountId: ctx.AccountId,
threadId: ctx.MessageThreadId, threadId: ctx.MessageThreadId,
cfg, cfg,
isGroup,
groupId,
}); });
queuedFinal = result.ok; queuedFinal = result.ok;
if (result.ok) { if (result.ok) {
@@ -499,6 +506,8 @@ export async function dispatchReplyFromConfig(params: {
accountId: ctx.AccountId, accountId: ctx.AccountId,
threadId: ctx.MessageThreadId, threadId: ctx.MessageThreadId,
cfg, cfg,
isGroup,
groupId,
}); });
if (!result.ok) { if (!result.ok) {
logVerbose( logVerbose(
@@ -549,6 +558,8 @@ export async function dispatchReplyFromConfig(params: {
accountId: ctx.AccountId, accountId: ctx.AccountId,
threadId: ctx.MessageThreadId, threadId: ctx.MessageThreadId,
cfg, cfg,
isGroup,
groupId,
}); });
queuedFinal = result.ok || queuedFinal; queuedFinal = result.ok || queuedFinal;
if (result.ok) { if (result.ok) {

View File

@@ -37,6 +37,10 @@ export type RouteReplyParams = {
abortSignal?: AbortSignal; abortSignal?: AbortSignal;
/** Mirror reply into session transcript (default: true when sessionKey is set). */ /** Mirror reply into session transcript (default: true when sessionKey is set). */
mirror?: boolean; mirror?: boolean;
/** Whether this message is being sent in a group/channel context */
isGroup?: boolean;
/** Group or channel identifier for correlation with received events */
groupId?: string;
}; };
export type RouteReplyResult = { export type RouteReplyResult = {
@@ -145,6 +149,8 @@ export async function routeReply(params: RouteReplyParams): Promise<RouteReplyRe
agentId: resolvedAgentId, agentId: resolvedAgentId,
text, text,
mediaUrls, mediaUrls,
...(params.isGroup != null ? { isGroup: params.isGroup } : {}),
...(params.groupId ? { groupId: params.groupId } : {}),
} }
: undefined, : undefined,
}); });

View File

@@ -85,6 +85,10 @@ export type MessageSentHookContext = {
conversationId?: string; conversationId?: string;
/** Message ID returned by the provider */ /** Message ID returned by the provider */
messageId?: string; messageId?: string;
/** Whether this message was sent in a group/channel context */
isGroup?: boolean;
/** Group or channel identifier, if applicable */
groupId?: string;
}; };
export type MessageSentHookEvent = InternalHookEvent & { export type MessageSentHookEvent = InternalHookEvent & {

View File

@@ -220,6 +220,10 @@ type DeliverOutboundPayloadsCoreParams = {
agentId?: string; agentId?: string;
text?: string; text?: string;
mediaUrls?: string[]; mediaUrls?: string[];
/** Whether this message is being sent in a group/channel context */
isGroup?: boolean;
/** Group or channel identifier for correlation with received events */
groupId?: string;
}; };
silent?: boolean; silent?: boolean;
}; };
@@ -478,6 +482,8 @@ async function deliverOutboundPayloadsCore(
}); });
const hookRunner = getGlobalHookRunner(); const hookRunner = getGlobalHookRunner();
const sessionKeyForInternalHooks = params.mirror?.sessionKey ?? params.session?.key; const sessionKeyForInternalHooks = params.mirror?.sessionKey ?? params.session?.key;
const mirrorIsGroup = params.mirror?.isGroup;
const mirrorGroupId = params.mirror?.groupId;
if ( if (
hookRunner?.hasHooks("message_sent") && hookRunner?.hasHooks("message_sent") &&
params.session?.agentId && params.session?.agentId &&
@@ -534,6 +540,8 @@ async function deliverOutboundPayloadsCore(
accountId: accountId ?? undefined, accountId: accountId ?? undefined,
conversationId: to, conversationId: to,
messageId: params.messageId, messageId: params.messageId,
...(mirrorIsGroup != null ? { isGroup: mirrorIsGroup } : {}),
...(mirrorGroupId ? { groupId: mirrorGroupId } : {}),
}), }),
).catch(() => {}); ).catch(() => {});
}; };