diff --git a/src/auto-reply/envelope.ts b/src/auto-reply/envelope.ts index 628e13e54..0130ed59d 100644 --- a/src/auto-reply/envelope.ts +++ b/src/auto-reply/envelope.ts @@ -34,3 +34,17 @@ export function formatAgentEnvelope(params: AgentEnvelopeParams): string { const header = `[${parts.join(" ")}]`; return `${header} ${params.body}`; } + +export function formatThreadStarterEnvelope(params: { + provider: string; + author?: string; + timestamp?: number | Date; + body: string; +}): string { + return formatAgentEnvelope({ + provider: params.provider, + from: params.author, + timestamp: params.timestamp, + body: params.body, + }); +} diff --git a/src/discord/monitor.ts b/src/discord/monitor.ts index 01ac99d50..11482199d 100644 --- a/src/discord/monitor.ts +++ b/src/discord/monitor.ts @@ -27,7 +27,10 @@ import { listNativeCommandSpecs, shouldHandleTextCommands, } from "../auto-reply/commands-registry.js"; -import { formatAgentEnvelope } from "../auto-reply/envelope.js"; +import { + formatAgentEnvelope, + formatThreadStarterEnvelope, +} from "../auto-reply/envelope.js"; import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js"; import { buildMentionRegexes, @@ -55,6 +58,7 @@ import { buildAgentSessionKey, resolveAgentRoute, } from "../routing/resolve-route.js"; +import { resolveThreadSessionKeys } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; import { loadWebMedia } from "../web/media.js"; import { fetchDiscordApplicationId } from "./probe.js"; @@ -863,9 +867,9 @@ export function createDiscordMessageHandler(params: { parentId: threadParentId, }); if (starter?.text) { - const starterEnvelope = formatAgentEnvelope({ + const starterEnvelope = formatThreadStarterEnvelope({ provider: "Discord", - from: starter.author, + author: starter.author, timestamp: starter.timestamp, body: starter.text, }); @@ -885,13 +889,19 @@ export function createDiscordMessageHandler(params: { } const mediaPayload = buildDiscordMediaPayload(mediaList); const discordTo = `channel:${message.channelId}`; + const threadKeys = resolveThreadSessionKeys({ + baseSessionKey, + threadId: threadChannel ? message.channelId : undefined, + parentSessionKey, + useSuffix: false, + }); const ctxPayload = { Body: combinedBody, From: isDirectMessage ? `discord:${author.id}` : `group:${message.channelId}`, To: discordTo, - SessionKey: baseSessionKey, + SessionKey: threadKeys.sessionKey, AccountId: route.accountId, ChatType: isDirectMessage ? "direct" : "group", SenderName: @@ -909,7 +919,7 @@ export function createDiscordMessageHandler(params: { Surface: "discord" as const, WasMentioned: wasMentioned, MessageSid: message.id, - ParentSessionKey: parentSessionKey, + ParentSessionKey: threadKeys.parentSessionKey, ThreadStarterBody: threadStarterBody, ThreadLabel: threadLabel, Timestamp: resolveTimestampMs(message.timestamp), diff --git a/src/routing/session-key.ts b/src/routing/session-key.ts index 52563936f..f0efb1004 100644 --- a/src/routing/session-key.ts +++ b/src/routing/session-key.ts @@ -89,3 +89,20 @@ export function buildAgentPeerSessionKey(params: { const peerId = (params.peerId ?? "").trim() || "unknown"; return `agent:${normalizeAgentId(params.agentId)}:${provider}:${peerKind}:${peerId}`; } + +export function resolveThreadSessionKeys(params: { + baseSessionKey: string; + threadId?: string | null; + parentSessionKey?: string; + useSuffix?: boolean; +}): { sessionKey: string; parentSessionKey?: string } { + const threadId = (params.threadId ?? "").trim(); + if (!threadId) { + return { sessionKey: params.baseSessionKey, parentSessionKey: undefined }; + } + const useSuffix = params.useSuffix ?? true; + const sessionKey = useSuffix + ? `${params.baseSessionKey}:thread:${threadId}` + : params.baseSessionKey; + return { sessionKey, parentSessionKey: params.parentSessionKey }; +} diff --git a/src/slack/monitor.ts b/src/slack/monitor.ts index 3af63cff7..d5ede54d1 100644 --- a/src/slack/monitor.ts +++ b/src/slack/monitor.ts @@ -14,7 +14,10 @@ import { listNativeCommandSpecs, shouldHandleTextCommands, } from "../auto-reply/commands-registry.js"; -import { formatAgentEnvelope } from "../auto-reply/envelope.js"; +import { + formatAgentEnvelope, + formatThreadStarterEnvelope, +} from "../auto-reply/envelope.js"; import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js"; import { buildMentionRegexes, @@ -34,6 +37,7 @@ import { resolveStorePath, updateLastRoute, } from "../config/sessions.js"; +import { resolveThreadSessionKeys } from "../routing/session-key.js"; import { danger, logVerbose, shouldLogVerbose } from "../globals.js"; import { enqueueSystemEvent } from "../infra/system-events.js"; import { getChildLogger } from "../logging.js"; @@ -930,12 +934,12 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { const isThreadReply = hasThreadTs && (threadTs !== message.ts || Boolean(message.parent_user_id)); - const threadSessionKey = - isThreadReply && threadTs - ? `${baseSessionKey}:thread:${threadTs}` - : undefined; - const parentSessionKey = isThreadReply ? baseSessionKey : undefined; - const sessionKey = threadSessionKey ?? baseSessionKey; + const threadKeys = resolveThreadSessionKeys({ + baseSessionKey, + threadId: isThreadReply ? threadTs : undefined, + parentSessionKey: isThreadReply ? baseSessionKey : undefined, + }); + const sessionKey = threadKeys.sessionKey; enqueueSystemEvent(`${inboundLabel}: ${preview}`, { sessionKey, contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}`, @@ -978,9 +982,9 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { : null; const starterName = starterUser?.name ?? starter.userId ?? "Unknown"; const starterWithId = `${starter.text}\n[slack message id: ${starter.ts ?? threadTs} channel: ${message.channel}]`; - threadStarterBody = formatAgentEnvelope({ + threadStarterBody = formatThreadStarterEnvelope({ provider: "Slack", - from: starterName, + author: starterName, timestamp: starter.ts ? Math.round(Number(starter.ts) * 1000) : undefined, @@ -1007,7 +1011,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { Surface: "slack" as const, MessageSid: message.ts, ReplyToId: message.thread_ts ?? message.ts, - ParentSessionKey: parentSessionKey, + ParentSessionKey: threadKeys.parentSessionKey, ThreadStarterBody: threadStarterBody, ThreadLabel: threadLabel, Timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,