From 1113f17d4c75764f2ec86d7e8bcd599933562bac Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 23 Jan 2026 23:04:09 +0000 Subject: [PATCH] refactor: share reply prefix context --- .../matrix/src/matrix/monitor/handler.ts | 7 +- .../mattermost/src/mattermost/monitor.ts | 20 ++---- extensions/msteams/src/reply-dispatcher.ts | 64 +++++++++++-------- src/channels/reply-prefix.ts | 41 ++++++++++++ .../monitor/message-handler.process.ts | 27 ++------ src/imessage/monitor/monitor-provider.ts | 28 ++------ src/plugin-sdk/index.ts | 1 + src/signal/monitor/event-handler.ts | 26 ++------ src/slack/monitor/message-handler/dispatch.ts | 44 +++++++------ src/telegram/bot-message-dispatch.ts | 21 ++---- src/web/auto-reply/monitor/process-message.ts | 33 +++------- 11 files changed, 145 insertions(+), 167 deletions(-) create mode 100644 src/channels/reply-prefix.ts diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 878e3e47c..fdb3029b1 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -1,6 +1,7 @@ import type { LocationMessageEventContent, MatrixClient } from "matrix-bot-sdk"; import { + createReplyPrefixContext, createTypingCallbacks, formatAllowlistMatchMeta, type RuntimeEnv, @@ -553,6 +554,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam channel: "matrix", accountId: route.accountId, }); + const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId }); const typingCallbacks = createTypingCallbacks({ start: () => sendTypingMatrix(roomId, true, undefined, client), stop: () => sendTypingMatrix(roomId, false, undefined, client), @@ -565,8 +567,8 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam }); const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({ - responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId) - .responsePrefix, + responsePrefix: prefixContext.responsePrefix, + responsePrefixContextProvider: prefixContext.responsePrefixContextProvider, humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId), deliver: async (payload) => { await deliverMatrixReplies({ @@ -596,6 +598,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam replyOptions: { ...replyOptions, skillFilter: roomConfig?.skills, + onModelSelected: prefixContext.onModelSelected, }, }); markDispatchIdle(); diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index ba080294f..03a591924 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -7,6 +7,7 @@ import type { RuntimeEnv, } from "clawdbot/plugin-sdk"; import { + createReplyPrefixContext, createTypingCallbacks, buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, @@ -31,12 +32,9 @@ import { } from "./client.js"; import { createDedupeCache, - extractShortModelName, formatInboundFromLabel, rawDataToString, - resolveIdentityName, resolveThreadSessionKeys, - type ResponsePrefixContext, } from "./monitor-helpers.js"; import { sendMessageMattermost } from "./send.js"; @@ -710,9 +708,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} accountId: account.accountId, }); - let prefixContext: ResponsePrefixContext = { - identityName: resolveIdentityName(cfg, route.agentId), - }; + const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId }); const typingCallbacks = createTypingCallbacks({ start: () => sendTypingIndicator(channelId, threadRootId), @@ -722,9 +718,8 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} }); const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({ - responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId) - .responsePrefix, - responsePrefixContextProvider: () => prefixContext, + responsePrefix: prefixContext.responsePrefix, + responsePrefixContextProvider: prefixContext.responsePrefixContextProvider, humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId), deliver: async (payload: ReplyPayload) => { const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); @@ -766,12 +761,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} ...replyOptions, disableBlockStreaming: typeof account.blockStreaming === "boolean" ? !account.blockStreaming : undefined, - onModelSelected: (ctx) => { - prefixContext.provider = ctx.provider; - prefixContext.model = extractShortModelName(ctx.model); - prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; - prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; - }, + onModelSelected: prefixContext.onModelSelected, }, }); markDispatchIdle(); diff --git a/extensions/msteams/src/reply-dispatcher.ts b/extensions/msteams/src/reply-dispatcher.ts index 83adffb7b..31e334de2 100644 --- a/extensions/msteams/src/reply-dispatcher.ts +++ b/extensions/msteams/src/reply-dispatcher.ts @@ -1,4 +1,5 @@ import { + createReplyPrefixContext, createTypingCallbacks, resolveChannelMediaMaxBytes, type ClawdbotConfig, @@ -48,17 +49,20 @@ export function createMSTeamsReplyDispatcher(params: { // Typing indicator is best-effort. }, }); + const prefixContext = createReplyPrefixContext({ + cfg: params.cfg, + agentId: params.agentId, + }); - return core.channel.reply.createReplyDispatcherWithTyping({ - responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig( - params.cfg, - params.agentId, - ).responsePrefix, - humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId), - deliver: async (payload) => { - const tableMode = core.channel.text.resolveMarkdownTableMode({ - cfg: params.cfg, - channel: "msteams", + const { dispatcher, replyOptions, markDispatchIdle } = + core.channel.reply.createReplyDispatcherWithTyping({ + responsePrefix: prefixContext.responsePrefix, + responsePrefixContextProvider: prefixContext.responsePrefixContextProvider, + humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId), + deliver: async (payload) => { + const tableMode = core.channel.text.resolveMarkdownTableMode({ + cfg: params.cfg, + channel: "msteams", }); const messages = renderReplyPayloadsToMessages([payload], { textChunkLimit: params.textLimit, @@ -90,21 +94,27 @@ export function createMSTeamsReplyDispatcher(params: { mediaMaxBytes, }); if (ids.length > 0) params.onSentMessageIds?.(ids); - }, - onError: (err, info) => { - const errMsg = formatUnknownError(err); - const classification = classifyMSTeamsSendError(err); - const hint = formatMSTeamsSendErrorHint(classification); - params.runtime.error?.( - `msteams ${info.kind} reply failed: ${errMsg}${hint ? ` (${hint})` : ""}`, - ); - params.log.error("reply failed", { - kind: info.kind, - error: errMsg, - classification, - hint, - }); - }, - onReplyStart: typingCallbacks.onReplyStart, - }); + }, + onError: (err, info) => { + const errMsg = formatUnknownError(err); + const classification = classifyMSTeamsSendError(err); + const hint = formatMSTeamsSendErrorHint(classification); + params.runtime.error?.( + `msteams ${info.kind} reply failed: ${errMsg}${hint ? ` (${hint})` : ""}`, + ); + params.log.error("reply failed", { + kind: info.kind, + error: errMsg, + classification, + hint, + }); + }, + onReplyStart: typingCallbacks.onReplyStart, + }); + + return { + dispatcher, + replyOptions: { ...replyOptions, onModelSelected: prefixContext.onModelSelected }, + markDispatchIdle, + }; } diff --git a/src/channels/reply-prefix.ts b/src/channels/reply-prefix.ts new file mode 100644 index 000000000..4897426d0 --- /dev/null +++ b/src/channels/reply-prefix.ts @@ -0,0 +1,41 @@ +import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../agents/identity.js"; +import type { ClawdbotConfig } from "../config/config.js"; +import type { GetReplyOptions } from "../auto-reply/types.js"; +import { + extractShortModelName, + type ResponsePrefixContext, +} from "../auto-reply/reply/response-prefix-template.js"; + +type ModelSelectionContext = Parameters>[0]; + +export type ReplyPrefixContextBundle = { + prefixContext: ResponsePrefixContext; + responsePrefix?: string; + responsePrefixContextProvider: () => ResponsePrefixContext; + onModelSelected: (ctx: ModelSelectionContext) => void; +}; + +export function createReplyPrefixContext(params: { + cfg: ClawdbotConfig; + agentId: string; +}): ReplyPrefixContextBundle { + const { cfg, agentId } = params; + const prefixContext: ResponsePrefixContext = { + identityName: resolveIdentityName(cfg, agentId), + }; + + const onModelSelected = (ctx: ModelSelectionContext) => { + // Mutate the object directly instead of reassigning to ensure closures see updates. + prefixContext.provider = ctx.provider; + prefixContext.model = extractShortModelName(ctx.model); + prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; + prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; + }; + + return { + prefixContext, + responsePrefix: resolveEffectiveMessagesConfig(cfg, agentId).responsePrefix, + responsePrefixContextProvider: () => prefixContext, + onModelSelected, + }; +} diff --git a/src/discord/monitor/message-handler.process.ts b/src/discord/monitor/message-handler.process.ts index b94d860be..172f885ac 100644 --- a/src/discord/monitor/message-handler.process.ts +++ b/src/discord/monitor/message-handler.process.ts @@ -1,17 +1,9 @@ -import { - resolveAckReaction, - resolveEffectiveMessagesConfig, - resolveHumanDelayConfig, - resolveIdentityName, -} from "../../agents/identity.js"; -import { - extractShortModelName, - type ResponsePrefixContext, -} from "../../auto-reply/reply/response-prefix-template.js"; +import { resolveAckReaction, resolveHumanDelayConfig } from "../../agents/identity.js"; import { removeAckReactionAfterReply, shouldAckReaction as shouldAckReactionGate, } from "../../channels/ack-reactions.js"; +import { createReplyPrefixContext } from "../../channels/reply-prefix.js"; import { createTypingCallbacks } from "../../channels/typing.js"; import { formatInboundEnvelope, @@ -318,10 +310,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) ? deliverTarget.slice("channel:".length) : message.channelId; - // Create mutable context for response prefix template interpolation - let prefixContext: ResponsePrefixContext = { - identityName: resolveIdentityName(cfg, route.agentId), - }; + const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId }); const tableMode = resolveMarkdownTableMode({ cfg, channel: "discord", @@ -329,8 +318,8 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) }); const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({ - responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, - responsePrefixContextProvider: () => prefixContext, + responsePrefix: prefixContext.responsePrefix, + responsePrefixContextProvider: prefixContext.responsePrefixContextProvider, humanDelay: resolveHumanDelayConfig(cfg, route.agentId), deliver: async (payload: ReplyPayload) => { const replyToId = replyReference.use(); @@ -371,11 +360,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) ? !discordConfig.blockStreaming : undefined, onModelSelected: (ctx) => { - // Mutate the object directly instead of reassigning to ensure the closure sees updates - prefixContext.provider = ctx.provider; - prefixContext.model = extractShortModelName(ctx.model); - prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; - prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; + prefixContext.onModelSelected(ctx); }, }, }); diff --git a/src/imessage/monitor/monitor-provider.ts b/src/imessage/monitor/monitor-provider.ts index 576218416..8db3831ef 100644 --- a/src/imessage/monitor/monitor-provider.ts +++ b/src/imessage/monitor/monitor-provider.ts @@ -1,14 +1,6 @@ import fs from "node:fs/promises"; -import { - resolveEffectiveMessagesConfig, - resolveHumanDelayConfig, - resolveIdentityName, -} from "../../agents/identity.js"; -import { - extractShortModelName, - type ResponsePrefixContext, -} from "../../auto-reply/reply/response-prefix-template.js"; +import { resolveHumanDelayConfig } from "../../agents/identity.js"; import { resolveTextChunkLimit } from "../../auto-reply/chunk.js"; import { hasControlCommand } from "../../auto-reply/command-detection.js"; import { @@ -31,6 +23,7 @@ import { } from "../../auto-reply/reply/history.js"; import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js"; import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js"; +import { createReplyPrefixContext } from "../../channels/reply-prefix.js"; import { recordInboundSession } from "../../channels/session.js"; import { loadConfig } from "../../config/config.js"; import { @@ -531,14 +524,11 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P ); } - // Create mutable context for response prefix template interpolation - let prefixContext: ResponsePrefixContext = { - identityName: resolveIdentityName(cfg, route.agentId), - }; + const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId }); const dispatcher = createReplyDispatcher({ - responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, - responsePrefixContextProvider: () => prefixContext, + responsePrefix: prefixContext.responsePrefix, + responsePrefixContextProvider: prefixContext.responsePrefixContextProvider, humanDelay: resolveHumanDelayConfig(cfg, route.agentId), deliver: async (payload) => { await deliverReplies({ @@ -565,13 +555,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P typeof accountInfo.config.blockStreaming === "boolean" ? !accountInfo.config.blockStreaming : undefined, - onModelSelected: (ctx) => { - // Mutate the object directly instead of reassigning to ensure the closure sees updates - prefixContext.provider = ctx.provider; - prefixContext.model = extractShortModelName(ctx.model); - prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; - prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; - }, + onModelSelected: prefixContext.onModelSelected, }, }); if (!queuedFinal) { diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 7b2d2d43f..23f6582cb 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -130,6 +130,7 @@ export { shouldAckReactionForWhatsApp, } from "../channels/ack-reactions.js"; export { createTypingCallbacks } from "../channels/typing.js"; +export { createReplyPrefixContext } from "../channels/reply-prefix.js"; export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js"; export type { NormalizedLocation } from "../channels/location.js"; export { formatLocationText, toLocationContext } from "../channels/location.js"; diff --git a/src/signal/monitor/event-handler.ts b/src/signal/monitor/event-handler.ts index 802252487..944b66cd0 100644 --- a/src/signal/monitor/event-handler.ts +++ b/src/signal/monitor/event-handler.ts @@ -1,12 +1,4 @@ -import { - resolveEffectiveMessagesConfig, - resolveHumanDelayConfig, - resolveIdentityName, -} from "../../agents/identity.js"; -import { - extractShortModelName, - type ResponsePrefixContext, -} from "../../auto-reply/reply/response-prefix-template.js"; +import { resolveHumanDelayConfig } from "../../agents/identity.js"; import { hasControlCommand } from "../../auto-reply/command-detection.js"; import { formatInboundEnvelope, @@ -24,6 +16,7 @@ import { } from "../../auto-reply/reply/history.js"; import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js"; import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js"; +import { createReplyPrefixContext } from "../../channels/reply-prefix.js"; import { recordInboundSession } from "../../channels/session.js"; import { createTypingCallbacks } from "../../channels/typing.js"; import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js"; @@ -178,10 +171,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { logVerbose(`signal inbound: from=${ctxPayload.From} len=${body.length} preview="${preview}"`); } - // Create mutable context for response prefix template interpolation - let prefixContext: ResponsePrefixContext = { - identityName: resolveIdentityName(deps.cfg, route.agentId), - }; + const prefixContext = createReplyPrefixContext({ cfg: deps.cfg, agentId: route.agentId }); const typingCallbacks = createTypingCallbacks({ start: async () => { @@ -198,8 +188,8 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { }); const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({ - responsePrefix: resolveEffectiveMessagesConfig(deps.cfg, route.agentId).responsePrefix, - responsePrefixContextProvider: () => prefixContext, + responsePrefix: prefixContext.responsePrefix, + responsePrefixContextProvider: prefixContext.responsePrefixContextProvider, humanDelay: resolveHumanDelayConfig(deps.cfg, route.agentId), deliver: async (payload) => { await deps.deliverReplies({ @@ -228,11 +218,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { disableBlockStreaming: typeof deps.blockStreaming === "boolean" ? !deps.blockStreaming : undefined, onModelSelected: (ctx) => { - // Mutate the object directly instead of reassigning to ensure the closure sees updates - prefixContext.provider = ctx.provider; - prefixContext.model = extractShortModelName(ctx.model); - prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; - prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; + prefixContext.onModelSelected(ctx); }, }, }); diff --git a/src/slack/monitor/message-handler/dispatch.ts b/src/slack/monitor/message-handler/dispatch.ts index a846cd128..14f9cfb61 100644 --- a/src/slack/monitor/message-handler/dispatch.ts +++ b/src/slack/monitor/message-handler/dispatch.ts @@ -1,17 +1,11 @@ -import { - resolveEffectiveMessagesConfig, - resolveHumanDelayConfig, - resolveIdentityName, -} from "../../../agents/identity.js"; -import { - extractShortModelName, - type ResponsePrefixContext, -} from "../../../auto-reply/reply/response-prefix-template.js"; +import { resolveHumanDelayConfig } from "../../../agents/identity.js"; import { dispatchInboundMessage } from "../../../auto-reply/dispatch.js"; import { clearHistoryEntriesIfEnabled } from "../../../auto-reply/reply/history.js"; import { removeAckReactionAfterReply } from "../../../channels/ack-reactions.js"; +import { createReplyPrefixContext } from "../../../channels/reply-prefix.js"; import { createTypingCallbacks } from "../../../channels/typing.js"; import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js"; +import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js"; import { danger, logVerbose, shouldLogVerbose } from "../../../globals.js"; import { removeSlackReaction } from "../../actions.js"; import { resolveSlackThreadTargets } from "../../threading.js"; @@ -25,6 +19,23 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag const cfg = ctx.cfg; const runtime = ctx.runtime; + if (prepared.isDirectMessage) { + const sessionCfg = cfg.session; + const storePath = resolveStorePath(sessionCfg?.store, { + agentId: route.agentId, + }); + await updateLastRoute({ + storePath, + sessionKey: route.mainSessionKey, + deliveryContext: { + channel: "slack", + to: `user:${message.user}`, + accountId: route.accountId, + }, + ctx: prepared.ctxPayload, + }); + } + const { statusThreadTs } = resolveSlackThreadTargets({ message, replyToMode: ctx.replyToMode, @@ -69,14 +80,11 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag }, }); - // Create mutable context for response prefix template interpolation - let prefixContext: ResponsePrefixContext = { - identityName: resolveIdentityName(cfg, route.agentId), - }; + const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId }); const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({ - responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, - responsePrefixContextProvider: () => prefixContext, + responsePrefix: prefixContext.responsePrefix, + responsePrefixContextProvider: prefixContext.responsePrefixContextProvider, humanDelay: resolveHumanDelayConfig(cfg, route.agentId), deliver: async (payload) => { const replyThreadTs = replyPlan.nextThreadTs(); @@ -112,11 +120,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag ? !account.config.blockStreaming : undefined, onModelSelected: (ctx) => { - // Mutate the object directly instead of reassigning to ensure the closure sees updates - prefixContext.provider = ctx.provider; - prefixContext.model = extractShortModelName(ctx.model); - prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; - prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; + prefixContext.onModelSelected(ctx); }, }, }); diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index 784bcb10e..55d1e9f87 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -1,13 +1,9 @@ // @ts-nocheck -import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../agents/identity.js"; -import { - extractShortModelName, - type ResponsePrefixContext, -} from "../auto-reply/reply/response-prefix-template.js"; import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js"; import { clearHistoryEntriesIfEnabled } from "../auto-reply/reply/history.js"; import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js"; import { removeAckReactionAfterReply } from "../channels/ack-reactions.js"; +import { createReplyPrefixContext } from "../channels/reply-prefix.js"; import { createTypingCallbacks } from "../channels/typing.js"; import { danger, logVerbose } from "../globals.js"; import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; @@ -122,10 +118,7 @@ export const dispatchTelegramMessage = async ({ Boolean(draftStream) || (typeof telegramCfg.blockStreaming === "boolean" ? !telegramCfg.blockStreaming : undefined); - // Create mutable context for response prefix template interpolation - let prefixContext: ResponsePrefixContext = { - identityName: resolveIdentityName(cfg, route.agentId), - }; + const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId }); const tableMode = resolveMarkdownTableMode({ cfg, channel: "telegram", @@ -136,8 +129,8 @@ export const dispatchTelegramMessage = async ({ ctx: ctxPayload, cfg, dispatcherOptions: { - responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, - responsePrefixContextProvider: () => prefixContext, + responsePrefix: prefixContext.responsePrefix, + responsePrefixContextProvider: prefixContext.responsePrefixContextProvider, deliver: async (payload, info) => { if (info.kind === "final") { await flushDraft(); @@ -176,11 +169,7 @@ export const dispatchTelegramMessage = async ({ : undefined, disableBlockStreaming, onModelSelected: (ctx) => { - // Mutate the object directly instead of reassigning to ensure the closure sees updates - prefixContext.provider = ctx.provider; - prefixContext.model = extractShortModelName(ctx.model); - prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; - prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; + prefixContext.onModelSelected(ctx); }, }, }); diff --git a/src/web/auto-reply/monitor/process-message.ts b/src/web/auto-reply/monitor/process-message.ts index c1d280a65..57ad5448f 100644 --- a/src/web/auto-reply/monitor/process-message.ts +++ b/src/web/auto-reply/monitor/process-message.ts @@ -1,12 +1,4 @@ -import { - resolveEffectiveMessagesConfig, - resolveIdentityName, - resolveIdentityNamePrefix, -} from "../../../agents/identity.js"; -import { - extractShortModelName, - type ResponsePrefixContext, -} from "../../../auto-reply/reply/response-prefix-template.js"; +import { resolveIdentityNamePrefix } from "../../../agents/identity.js"; import { resolveTextChunkLimit } from "../../../auto-reply/chunk.js"; import { formatInboundEnvelope, @@ -22,6 +14,7 @@ import type { ReplyPayload } from "../../../auto-reply/types.js"; import { shouldComputeCommandAuthorized } from "../../../auto-reply/command-detection.js"; import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js"; import { toLocationContext } from "../../../channels/location.js"; +import { createReplyPrefixContext } from "../../../channels/reply-prefix.js"; import type { loadConfig } from "../../../config/config.js"; import { readSessionUpdatedAt, @@ -247,22 +240,20 @@ export async function processMessage(params: { ? await resolveWhatsAppCommandAuthorized({ cfg: params.cfg, msg: params.msg }) : undefined; const configuredResponsePrefix = params.cfg.messages?.responsePrefix; - const resolvedMessages = resolveEffectiveMessagesConfig(params.cfg, params.route.agentId); + const prefixContext = createReplyPrefixContext({ + cfg: params.cfg, + agentId: params.route.agentId, + }); const isSelfChat = params.msg.chatType !== "group" && Boolean(params.msg.selfE164) && normalizeE164(params.msg.from) === normalizeE164(params.msg.selfE164 ?? ""); const responsePrefix = - resolvedMessages.responsePrefix ?? + prefixContext.responsePrefix ?? (configuredResponsePrefix === undefined && isSelfChat ? (resolveIdentityNamePrefix(params.cfg, params.route.agentId) ?? "[clawdbot]") : undefined); - // Create mutable context for response prefix template interpolation - let prefixContext: ResponsePrefixContext = { - identityName: resolveIdentityName(params.cfg, params.route.agentId), - }; - const ctxPayload = finalizeInboundContext({ Body: combinedBody, RawBody: params.msg.body, @@ -334,7 +325,7 @@ export async function processMessage(params: { replyResolver: params.replyResolver, dispatcherOptions: { responsePrefix, - responsePrefixContextProvider: () => prefixContext, + responsePrefixContextProvider: prefixContext.responsePrefixContextProvider, onHeartbeatStrip: () => { if (!didLogHeartbeatStrip) { didLogHeartbeatStrip = true; @@ -393,13 +384,7 @@ export async function processMessage(params: { typeof params.cfg.channels?.whatsapp?.blockStreaming === "boolean" ? !params.cfg.channels.whatsapp.blockStreaming : undefined, - onModelSelected: (ctx) => { - // Mutate the object directly instead of reassigning to ensure the closure sees updates - prefixContext.provider = ctx.provider; - prefixContext.model = extractShortModelName(ctx.model); - prefixContext.modelFull = `${ctx.provider}/${ctx.model}`; - prefixContext.thinkingLevel = ctx.thinkLevel ?? "off"; - }, + onModelSelected: prefixContext.onModelSelected, }, });