diff --git a/CHANGELOG.md b/CHANGELOG.md index 94e22223e..5fe720c79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai - Docs/Web search: remove outdated Brave free-tier wording and replace prescriptive AI ToS guidance with neutral compliance language in Brave setup docs. (#26860) Thanks @HenryLoenwind. - Tools/Diffs guidance loading: move diffs usage guidance from unconditional prompt-hook injection to the plugin companion skill path, reducing unrelated-turn prompt noise while keeping diffs tool behavior unchanged. (#32630) thanks @sircrumpet. - Agents/tool-result truncation: preserve important tail diagnostics by using head+tail truncation for oversized tool results while keeping configurable truncation options. (#20076) thanks @jlwestsr. +- Telegram/topic agent routing: support per-topic `agentId` overrides in forum groups and DM topics so topics can route to dedicated agents with isolated sessions. (#33647) Thanks @kesor. ### Fixes diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index c70c89ff2..9cbf7ac29 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -444,7 +444,8 @@ curl "https://api.telegram.org/bot/getUpdates" - message sends omit `message_thread_id` (Telegram rejects `sendMessage(...thread_id=1)`) - typing actions still include `message_thread_id` - Topic inheritance: topic entries inherit group settings unless overridden (`requireMention`, `allowFrom`, `skills`, `systemPrompt`, `enabled`, `groupPolicy`, `agentId`). + Topic inheritance: topic entries inherit group settings unless overridden (`requireMention`, `allowFrom`, `skills`, `systemPrompt`, `enabled`, `groupPolicy`). + `agentId` is topic-only and does not inherit from group defaults. **Per-topic agent routing**: Each topic can route to a different agent by setting `agentId` in the topic config. This gives each topic its own isolated workspace, memory, and session. Example: @@ -773,7 +774,7 @@ Primary reference: - `channels.telegram.groups..allowFrom`: per-group sender allowlist override. - `channels.telegram.groups..systemPrompt`: extra system prompt for the group. - `channels.telegram.groups..enabled`: disable the group when `false`. - - `channels.telegram.groups..topics..*`: per-topic overrides (same fields as group). + - `channels.telegram.groups..topics..*`: per-topic overrides (group fields + topic-only `agentId`). - `channels.telegram.groups..topics..agentId`: route this topic to a specific agent (overrides group-level and binding routing). - `channels.telegram.groups..topics..groupPolicy`: per-topic override for groupPolicy (`open | allowlist | disabled`). - `channels.telegram.groups..topics..requireMention`: per-topic mention gating override. diff --git a/src/telegram/bot-message-context.topic-agentid.test.ts b/src/telegram/bot-message-context.topic-agentid.test.ts index 4b983670d..b3b634b47 100644 --- a/src/telegram/bot-message-context.topic-agentid.test.ts +++ b/src/telegram/bot-message-context.topic-agentid.test.ts @@ -1,7 +1,30 @@ -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { loadConfig } from "../config/config.js"; import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; +const { defaultRouteConfig } = vi.hoisted(() => ({ + defaultRouteConfig: { + agents: { + list: [{ id: "main", default: true }, { id: "zu" }, { id: "q" }, { id: "support" }], + }, + channels: { telegram: {} }, + messages: { groupChat: { mentionPatterns: [] } }, + }, +})); + +vi.mock("../config/config.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadConfig: vi.fn(() => defaultRouteConfig), + }; +}); + describe("buildTelegramMessageContext per-topic agentId routing", () => { + beforeEach(() => { + vi.mocked(loadConfig).mockReturnValue(defaultRouteConfig as never); + }); + it("uses group-level agent when no topic agentId is set", async () => { const ctx = await buildTelegramMessageContextForTest({ message: { @@ -120,6 +143,41 @@ describe("buildTelegramMessageContext per-topic agentId routing", () => { expect(ctx?.ctxPayload?.SessionKey).toContain("agent:main:"); }); + it("falls back to default agent when topic agentId does not exist", async () => { + vi.mocked(loadConfig).mockReturnValue({ + agents: { + list: [{ id: "main", default: true }, { id: "zu" }], + }, + channels: { telegram: {} }, + messages: { groupChat: { mentionPatterns: [] } }, + } as never); + + const ctx = await buildTelegramMessageContextForTest({ + message: { + message_id: 1, + chat: { + id: -1001234567890, + type: "supergroup", + title: "Forum", + is_forum: true, + }, + date: 1700000000, + text: "@bot hello", + message_thread_id: 3, + from: { id: 42, first_name: "Alice" }, + }, + options: { forceWasMentioned: true }, + resolveGroupActivation: () => true, + resolveTelegramGroupConfig: () => ({ + groupConfig: { requireMention: false }, + topicConfig: { agentId: "ghost" }, + }), + }); + + expect(ctx).not.toBeNull(); + expect(ctx?.ctxPayload?.SessionKey).toContain("agent:main:"); + }); + it("routes DM topic to specific agent when agentId is set", async () => { const ctx = await buildTelegramMessageContextForTest({ message: {