diff --git a/AGENTS.md b/AGENTS.md index 1fc3089268a5..9c61385e99ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -146,6 +146,7 @@ Skills own workflows; root owns hard policy and routing. - No `@ts-nocheck`. Lint suppressions only intentional + explained. - External boundaries: prefer `zod` or existing schema helpers. - Runtime branching: discriminated unions/closed codes over freeform strings. Avoid semantic sentinels (`?? 0`, empty object/string). +- Cross-function state: when valid combos matter, return a closed mode/result shape. Avoid parallel nullable fields or derived booleans that callers must keep in sync; make impossible states unrepresentable. - Formatter-friendly shape: when oxfmt explodes an expression vertically, extract named booleans, payloads, or small helpers. Do not change width or use format-ignore for local compactness. - Calls should be boring: complex decisions happen above; call args/object fields are names, literals, or simple property reads. - Prefer early returns over nested condition pyramids. Split code into gather -> normalize -> decide -> act. diff --git a/extensions/telegram/src/bot-message-context.acp-bindings.test.ts b/extensions/telegram/src/bot-message-context.acp-bindings.test.ts index 10c28a34bc92..8243239f3fbf 100644 --- a/extensions/telegram/src/bot-message-context.acp-bindings.test.ts +++ b/extensions/telegram/src/bot-message-context.acp-bindings.test.ts @@ -63,70 +63,73 @@ function createConfiguredTelegramBinding() { function createConfiguredTelegramRoute() { const configuredBinding = createConfiguredTelegramBinding(); return { - configuredBinding: { - conversation: { - channel: "telegram", - accountId: "work", - conversationId: "-1001234567890:topic:42", - parentConversationId: "-1001234567890", - }, - compiledBinding: { - channel: "telegram", - accountPattern: "work", - binding: { - type: "acp", - agentId: "codex", - match: { - channel: "telegram", - accountId: "work", - peer: { - kind: "group", - id: "-1001234567890:topic:42", - }, - }, - }, - bindingConversationId: "-1001234567890:topic:42", - target: { + bindingMode: { + kind: "configured", + binding: { + conversation: { + channel: "telegram", + accountId: "work", conversationId: "-1001234567890:topic:42", parentConversationId: "-1001234567890", }, - agentId: "codex", - provider: { - compileConfiguredBinding: () => ({ - conversationId: "-1001234567890:topic:42", - parentConversationId: "-1001234567890", - }), - matchInboundConversation: () => ({ - conversationId: "-1001234567890:topic:42", - parentConversationId: "-1001234567890", - }), - }, - targetFactory: { - driverId: "acp", - materialize: () => ({ - record: configuredBinding.record, - statefulTarget: { - kind: "stateful", - driverId: "acp", - sessionKey: configuredBinding.record.targetSessionKey, - agentId: configuredBinding.spec.agentId, + compiledBinding: { + channel: "telegram", + accountPattern: "work", + binding: { + type: "acp", + agentId: "codex", + match: { + channel: "telegram", + accountId: "work", + peer: { + kind: "group", + id: "-1001234567890:topic:42", + }, }, - }), + }, + bindingConversationId: "-1001234567890:topic:42", + target: { + conversationId: "-1001234567890:topic:42", + parentConversationId: "-1001234567890", + }, + agentId: "codex", + provider: { + compileConfiguredBinding: () => ({ + conversationId: "-1001234567890:topic:42", + parentConversationId: "-1001234567890", + }), + matchInboundConversation: () => ({ + conversationId: "-1001234567890:topic:42", + parentConversationId: "-1001234567890", + }), + }, + targetFactory: { + driverId: "acp", + materialize: () => ({ + record: configuredBinding.record, + statefulTarget: { + kind: "stateful", + driverId: "acp", + sessionKey: configuredBinding.record.targetSessionKey, + agentId: configuredBinding.spec.agentId, + }, + }), + }, + }, + match: { + conversationId: "-1001234567890:topic:42", + parentConversationId: "-1001234567890", + }, + record: configuredBinding.record, + statefulTarget: { + kind: "stateful", + driverId: "acp", + sessionKey: configuredBinding.record.targetSessionKey, + agentId: configuredBinding.spec.agentId, }, }, - match: { - conversationId: "-1001234567890:topic:42", - parentConversationId: "-1001234567890", - }, - record: configuredBinding.record, - statefulTarget: { - kind: "stateful", - driverId: "acp", - sessionKey: configuredBinding.record.targetSessionKey, - agentId: configuredBinding.spec.agentId, - }, + sessionKey: configuredBinding.record.targetSessionKey, }, - configuredBindingSessionKey: configuredBinding.record.targetSessionKey, route: { agentId: "codex", accountId: "work", diff --git a/extensions/telegram/src/bot-message-context.thread-binding.test.ts b/extensions/telegram/src/bot-message-context.thread-binding.test.ts index 86cbafb2992b..4dd1c97a9a6f 100644 --- a/extensions/telegram/src/bot-message-context.thread-binding.test.ts +++ b/extensions/telegram/src/bot-message-context.thread-binding.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { telegramRouteTestSessionRuntime } from "./bot-message-context.route-test-support.js"; import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; +import type { TelegramConversationBindingMode } from "./conversation-route.js"; const recordInboundSessionMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); const resolveTelegramConversationRouteMock = vi.hoisted(() => vi.fn()); @@ -32,12 +33,13 @@ function createBoundRoute(params: { accountId: string; sessionKey: string; agentId: string; - pluginOwnedRuntimeBinding?: boolean; + bindingMode?: TelegramConversationBindingMode; }) { return { - configuredBinding: null, - configuredBindingSessionKey: "", - pluginOwnedRuntimeBinding: params.pluginOwnedRuntimeBinding ?? false, + bindingMode: params.bindingMode ?? { + kind: "runtime-bound", + sessionKey: params.sessionKey, + }, route: { accountId: params.accountId, agentId: params.agentId, @@ -112,7 +114,7 @@ describe("buildTelegramMessageContext thread binding override", () => { accountId: "default", sessionKey: "plugin-binding:openclaw-codex-app-server:session-1", agentId: "main", - pluginOwnedRuntimeBinding: true, + bindingMode: { kind: "plugin-owned-runtime" }, }), ); diff --git a/extensions/telegram/src/bot-message-context.ts b/extensions/telegram/src/bot-message-context.ts index ccab8e410f35..9d5a81cd3ac6 100644 --- a/extensions/telegram/src/bot-message-context.ts +++ b/extensions/telegram/src/bot-message-context.ts @@ -242,17 +242,16 @@ export const buildTelegramMessageContext = async ({ const freshCfg = loadFreshConfig?.() ?? (runtime?.getRuntimeConfig ?? (await loadTelegramMessageContextRuntime()).getRuntimeConfig)(); - let { route, configuredBinding, configuredBindingSessionKey, pluginOwnedRuntimeBinding } = - resolveTelegramConversationRoute({ - cfg: freshCfg, - accountId: account.accountId, - chatId, - isGroup, - resolvedThreadId, - replyThreadId, - senderId, - topicAgentId: topicConfig?.agentId, - }); + let { route, bindingMode } = resolveTelegramConversationRoute({ + cfg: freshCfg, + accountId: account.accountId, + chatId, + isGroup, + resolvedThreadId, + replyThreadId, + senderId, + topicAgentId: topicConfig?.agentId, + }); const requiresExplicitAccountBinding = ( candidate: ReturnType["route"], ): boolean => @@ -372,7 +371,7 @@ export const buildTelegramMessageContext = async ({ } let initialTypingCueSent = false; const ensureConfiguredBindingReady = async (): Promise => { - if (!configuredBinding) { + if (bindingMode.kind !== "configured") { return true; } const ensureConfiguredBindingRouteReady = @@ -380,22 +379,22 @@ export const buildTelegramMessageContext = async ({ (await loadTelegramMessageContextRuntime()).ensureConfiguredBindingRouteReady; const ensured = await ensureConfiguredBindingRouteReady({ cfg: freshCfg, - bindingResolution: configuredBinding, + bindingResolution: bindingMode.binding, }); if (ensured.ok) { logVerbose( - `telegram: using configured ACP binding for ${configuredBinding.record.conversation.conversationId} -> ${configuredBindingSessionKey}`, + `telegram: using configured ACP binding for ${bindingMode.binding.record.conversation.conversationId} -> ${bindingMode.sessionKey}`, ); return true; } logVerbose( - `telegram: configured ACP binding unavailable for ${configuredBinding.record.conversation.conversationId}: ${ensured.error}`, + `telegram: configured ACP binding unavailable for ${bindingMode.binding.record.conversation.conversationId}: ${ensured.error}`, ); logInboundDrop({ log: logVerbose, channel: "telegram", reason: "configured ACP binding unavailable", - target: configuredBinding.record.conversation.conversationId, + target: bindingMode.binding.record.conversation.conversationId, }); return false; }; @@ -432,7 +431,7 @@ export const buildTelegramMessageContext = async ({ }); const baseRequireMention = resolveGroupRequireMention(chatId); const requireMention = - isGroup && pluginOwnedRuntimeBinding + isGroup && bindingMode.kind === "plugin-owned-runtime" ? false : firstDefined( topicConfig?.requireMention, diff --git a/extensions/telegram/src/bot-native-commands.ts b/extensions/telegram/src/bot-native-commands.ts index e01b2d2aae91..25bf6533f522 100644 --- a/extensions/telegram/src/bot-native-commands.ts +++ b/extensions/telegram/src/bot-native-commands.ts @@ -886,7 +886,7 @@ export const registerTelegramNativeCommands = ({ isForum, messageThreadId: resolvedThreadId ?? messageThreadId, }); - let { route, configuredBinding } = resolveTelegramConversationRoute({ + let { route, bindingMode } = resolveTelegramConversationRoute({ cfg: runtimeCfg, accountId, chatId, @@ -897,14 +897,14 @@ export const registerTelegramNativeCommands = ({ topicAgentId, }); const nativeCommandRuntime = await loadTelegramNativeCommandRuntime(); - if (configuredBinding) { + if (bindingMode.kind === "configured") { const ensured = await nativeCommandRuntime.ensureConfiguredBindingRouteReady({ cfg: runtimeCfg, - bindingResolution: configuredBinding, + bindingResolution: bindingMode.binding, }); if (!ensured.ok) { logVerbose( - `telegram native command: configured ACP binding unavailable for topic ${configuredBinding.record.conversation.conversationId}: ${ensured.error}`, + `telegram native command: configured ACP binding unavailable for topic ${bindingMode.binding.record.conversation.conversationId}: ${ensured.error}`, ); await withTelegramApiErrorLogging({ operation: "sendMessage", diff --git a/extensions/telegram/src/conversation-route.base-session-key.test.ts b/extensions/telegram/src/conversation-route.base-session-key.test.ts index 95f623a77df3..37bc4b4e779e 100644 --- a/extensions/telegram/src/conversation-route.base-session-key.test.ts +++ b/extensions/telegram/src/conversation-route.base-session-key.test.ts @@ -156,12 +156,10 @@ describe("resolveTelegramConversationBaseSessionKey", () => { }); expect(touch).not.toHaveBeenCalled(); - expect(result.configuredBinding).toBeNull(); - expect(result.configuredBindingSessionKey).toBe(""); + expect(result.bindingMode).toEqual({ kind: "none" }); expect(result.route.agentId).toBe("main"); expect(result.route.sessionKey).toBe("agent:main:main"); expect(result.route.matchedBy).toBe("default"); - expect(result.pluginOwnedRuntimeBinding).toBe(false); }); it("detects plugin-owned runtime bindings without replacing the route", () => { @@ -205,11 +203,9 @@ describe("resolveTelegramConversationBaseSessionKey", () => { }); expect(touch).toHaveBeenCalledWith("binding-plugin-owned", undefined); - expect(result.configuredBinding).toBeNull(); - expect(result.configuredBindingSessionKey).toBe(""); + expect(result.bindingMode).toEqual({ kind: "plugin-owned-runtime" }); expect(result.route.agentId).toBe("main"); expect(result.route.sessionKey).toBe("agent:main:telegram:group:-1001234567890:topic:11"); expect(result.route.matchedBy).toBe("default"); - expect(result.pluginOwnedRuntimeBinding).toBe(true); }); }); diff --git a/extensions/telegram/src/conversation-route.ts b/extensions/telegram/src/conversation-route.ts index 742bebb54c09..d190c0b6593f 100644 --- a/extensions/telegram/src/conversation-route.ts +++ b/extensions/telegram/src/conversation-route.ts @@ -20,6 +20,27 @@ import { resolveTelegramDirectPeerId, } from "./bot/helpers.js"; +type TelegramResolvedRoute = ReturnType; +type ConfiguredTelegramBinding = NonNullable; + +export type TelegramConversationBindingMode = + | { kind: "none" } + | { + kind: "configured"; + binding: ConfiguredTelegramBinding; + sessionKey: string; + } + | { + kind: "runtime-bound"; + sessionKey: string; + } + | { kind: "plugin-owned-runtime" }; + +export type TelegramConversationRouteResult = { + route: TelegramResolvedRoute; + bindingMode: TelegramConversationBindingMode; +}; + export function resolveTelegramConversationRoute(params: { cfg: OpenClawConfig; accountId: string; @@ -29,12 +50,7 @@ export function resolveTelegramConversationRoute(params: { replyThreadId?: number; senderId?: string | number | null; topicAgentId?: string | null; -}): { - route: ReturnType; - configuredBinding: ConfiguredBindingRouteResult["bindingResolution"]; - configuredBindingSessionKey: string; - pluginOwnedRuntimeBinding: boolean; -} { +}): TelegramConversationRouteResult { const peerId = params.isGroup ? buildTelegramGroupPeerId(params.chatId, params.resolvedThreadId) : resolveTelegramDirectPeerId({ @@ -102,9 +118,14 @@ export function resolveTelegramConversationRoute(params: { parentConversationId: params.isGroup ? String(params.chatId) : undefined, }, }); - let configuredBinding = configuredRoute.bindingResolution; - let configuredBindingSessionKey = configuredRoute.boundSessionKey ?? ""; route = configuredRoute.route; + let bindingMode: TelegramConversationBindingMode = configuredRoute.bindingResolution + ? { + kind: "configured", + binding: configuredRoute.bindingResolution, + sessionKey: configuredRoute.boundSessionKey ?? route.sessionKey, + } + : { kind: "none" }; const runtimeBindingConversationId = params.replyThreadId != null @@ -119,12 +140,10 @@ export function resolveTelegramConversationRoute(params: { }, }); route = runtimeRoute.route; - const pluginOwnedRuntimeBinding = Boolean( - runtimeRoute.bindingRecord && !runtimeRoute.boundSessionKey, - ); if (runtimeRoute.bindingRecord) { - configuredBinding = null; - configuredBindingSessionKey = ""; + bindingMode = runtimeRoute.boundSessionKey + ? { kind: "runtime-bound", sessionKey: runtimeRoute.boundSessionKey } + : { kind: "plugin-owned-runtime" }; logVerbose( runtimeRoute.boundSessionKey ? `telegram: routed via bound conversation ${runtimeBindingConversationId} -> ${runtimeRoute.boundSessionKey}` @@ -134,9 +153,7 @@ export function resolveTelegramConversationRoute(params: { return { route, - configuredBinding, - configuredBindingSessionKey, - pluginOwnedRuntimeBinding, + bindingMode, }; }