diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ce4fa2698e..485b5f08e53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Heartbeat: allow explicit accountId routing for multi-account channels. (#8702) Thanks @lsh411. +- Routing: refresh bindings per message by loading config at route resolution so binding changes apply without restart. (#11372) Thanks @juanpablodlc. - TUI/Gateway: handle non-streaming finals, refresh history for non-local chat runs, and avoid event gap warnings for targeted tool streams. (#8432) Thanks @gumadeiras. - Shell completion: auto-detect and migrate slow dynamic patterns to cached files for faster terminal startup; add completion health checks to doctor/update/onboard. - Telegram: honor session model overrides in inline model selection. (#8193) Thanks @gildo. diff --git a/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts b/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts index a514bf3909d..369ae80b390 100644 --- a/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts +++ b/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts @@ -10,6 +10,7 @@ const updateLastRouteMock = vi.fn(); const dispatchMock = vi.fn(); const readAllowFromStoreMock = vi.fn(); const upsertPairingRequestMock = vi.fn(); +const loadConfigMock = vi.fn(); vi.mock("./send.js", () => ({ sendMessageDiscord: (...args: unknown[]) => sendMock(...args), @@ -30,6 +31,13 @@ vi.mock("../pairing/pairing-store.js", () => ({ readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args), upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args), })); +vi.mock("../config/config.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadConfig: (...args: unknown[]) => loadConfigMock(...args), + }; +}); vi.mock("../config/sessions.js", async (importOriginal) => { const actual = await importOriginal(); return { @@ -50,6 +58,7 @@ beforeEach(() => { }); readAllowFromStoreMock.mockReset().mockResolvedValue([]); upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true }); + loadConfigMock.mockReset().mockReturnValue({}); __resetDiscordChannelInfoCacheForTest(); }); @@ -685,6 +694,7 @@ describe("discord tool result dispatch", () => { }, bindings: [{ agentId: "support", match: { channel: "discord", guildId: "g1" } }], } as ReturnType; + loadConfigMock.mockReturnValue(cfg); const handler = createDiscordMessageHandler({ cfg, diff --git a/src/discord/monitor/message-handler.preflight.ts b/src/discord/monitor/message-handler.preflight.ts index 9b8648c7f1f..38126a050ec 100644 --- a/src/discord/monitor/message-handler.preflight.ts +++ b/src/discord/monitor/message-handler.preflight.ts @@ -17,6 +17,7 @@ import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js"; import { resolveControlCommandGate } from "../../channels/command-gating.js"; import { logInboundDrop } from "../../channels/logging.js"; import { resolveMentionGatingWithBypass } from "../../channels/mention-gating.js"; +import { loadConfig } from "../../config/config.js"; import { logVerbose, shouldLogVerbose } from "../../globals.js"; import { recordChannelActivity } from "../../infra/channel-activity.js"; import { enqueueSystemEvent } from "../../infra/system-events.js"; @@ -218,8 +219,9 @@ export async function preflightDiscordMessage( earlyThreadParentType = parentInfo.type; } + // Fresh config for bindings lookup; other routing inputs are payload-derived. const route = resolveAgentRoute({ - cfg: params.cfg, + cfg: loadConfig(), channel: "discord", accountId: params.accountId, guildId: params.data.guild_id ?? undefined, diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 328f6c4b4e9..dfca10a74d1 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -26,6 +26,7 @@ import { logInboundDrop } from "../channels/logging.js"; import { resolveMentionGatingWithBypass } from "../channels/mention-gating.js"; import { recordInboundSession } from "../channels/session.js"; import { formatCliCommand } from "../cli/command-format.js"; +import { loadConfig } from "../config/config.js"; import { readSessionUpdatedAt, resolveStorePath } from "../config/sessions.js"; import { logVerbose, shouldLogVerbose } from "../globals.js"; import { recordChannelActivity } from "../infra/channel-activity.js"; @@ -163,8 +164,9 @@ export const buildTelegramMessageContext = async ({ const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId); const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId); const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId }); + // Fresh config for bindings lookup; other routing inputs are payload-derived. const route = resolveAgentRoute({ - cfg, + cfg: loadConfig(), channel: "telegram", accountId: account.accountId, peer: { diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index ef03ad343c7..61e2038b6ce 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -449,8 +449,9 @@ export function createTelegramBot(opts: TelegramBotOptions) { : undefined; const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId); const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId }); + // Fresh config for bindings lookup; other routing inputs are payload-derived. const route = resolveAgentRoute({ - cfg, + cfg: loadConfig(), channel: "telegram", accountId: account.accountId, peer: { kind: isGroup ? "group" : "direct", id: peerId }, diff --git a/src/web/auto-reply/monitor/on-message.ts b/src/web/auto-reply/monitor/on-message.ts index 28ded02876e..d9232dcd808 100644 --- a/src/web/auto-reply/monitor/on-message.ts +++ b/src/web/auto-reply/monitor/on-message.ts @@ -1,10 +1,10 @@ import type { getReplyFromConfig } from "../../../auto-reply/reply.js"; import type { MsgContext } from "../../../auto-reply/templating.js"; -import type { loadConfig } from "../../../config/config.js"; import type { MentionConfig } from "../mentions.js"; import type { WebInboundMsg } from "../types.js"; import type { EchoTracker } from "./echo.js"; import type { GroupHistoryEntry } from "./group-gating.js"; +import { loadConfig } from "../../../config/config.js"; import { logVerbose } from "../../../globals.js"; import { resolveAgentRoute } from "../../../routing/resolve-route.js"; import { buildGroupHistoryKey } from "../../../routing/session-key.js"; @@ -63,8 +63,9 @@ export function createWebOnMessageHandler(params: { return async (msg: WebInboundMsg) => { const conversationId = msg.conversationId ?? msg.from; const peerId = resolvePeerId(msg); + // Fresh config for bindings lookup; other routing inputs are payload-derived. const route = resolveAgentRoute({ - cfg: params.cfg, + cfg: loadConfig(), channel: "whatsapp", accountId: msg.accountId, peer: {