diff --git a/CHANGELOG.md b/CHANGELOG.md index c05e582f0e0..cc51b08e184 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Telegram: inherit the process DNS result order for Bot API transport and downgrade recovered sticky IPv4 fallback promotions to debug logs, while keeping pinned-IP escalation warnings visible. Fixes #75904. Thanks @highfly-hi and @neeravmakwana. - Subagents: honor `sessions_spawn` with `expectsCompletionMessage: false` by skipping parent completion handoff delivery while still running child cleanup. Fixes #75848. Thanks @alfredjbclaw. - Gateway/logging: keep deferred channel startup logs on the subsystem logger, so Slack, Discord, Telegram, and voice-call startup messages keep timestamped prefixes. Thanks @vincentkoc. - ACP/Discord: suppress completion announce delivery for inline thread-bound ACP session runs, so Discord thread-bound ACP replies are not delivered twice. Fixes #60780. Thanks @solavrc. diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index da7043fc761..599aa25ec80 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -878,7 +878,7 @@ channels: proxy: socks5://:@proxy-host:1080 ``` - - Node 22+ defaults to `autoSelectFamily=true` (except WSL2) and `dnsResultOrder=ipv4first`. + - Node 22+ defaults to `autoSelectFamily=true` (except WSL2). Telegram DNS result order honors `OPENCLAW_TELEGRAM_DNS_RESULT_ORDER`, then `channels.telegram.network.dnsResultOrder`, then the process default such as `NODE_OPTIONS=--dns-result-order=ipv4first`; if none applies, Node 22+ falls back to `ipv4first`. - If your host is WSL2 or explicitly works better with IPv4-only behavior, force family selection: ```yaml diff --git a/extensions/telegram/src/fetch.test.ts b/extensions/telegram/src/fetch.test.ts index e4ac543e8a7..90c73dc9576 100644 --- a/extensions/telegram/src/fetch.test.ts +++ b/extensions/telegram/src/fetch.test.ts @@ -2,9 +2,11 @@ import { resolveFetch } from "openclaw/plugin-sdk/fetch-runtime"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const setDefaultResultOrder = vi.hoisted(() => vi.fn()); +const getDefaultResultOrder = vi.hoisted(() => vi.fn(() => "ipv4first")); const setDefaultAutoSelectFamily = vi.hoisted(() => vi.fn()); const loggerInfo = vi.hoisted(() => vi.fn()); const loggerDebug = vi.hoisted(() => vi.fn()); +const loggerWarn = vi.hoisted(() => vi.fn()); const undiciFetch = vi.hoisted(() => vi.fn()); const setGlobalDispatcher = vi.hoisted(() => vi.fn()); @@ -46,6 +48,7 @@ vi.mock("node:dns", async () => { const actual = await vi.importActual("node:dns"); return { ...actual, + getDefaultResultOrder, setDefaultResultOrder, }; }); @@ -70,12 +73,12 @@ vi.mock("openclaw/plugin-sdk/runtime-env", () => ({ createSubsystemLogger: () => ({ info: loggerInfo, debug: loggerDebug, - warn: vi.fn(), + warn: loggerWarn, error: vi.fn(), child: () => ({ info: loggerInfo, debug: loggerDebug, - warn: vi.fn(), + warn: loggerWarn, error: vi.fn(), }), }), @@ -129,6 +132,9 @@ beforeEach(() => { } loggerInfo.mockReset(); loggerDebug.mockReset(); + loggerWarn.mockReset(); + getDefaultResultOrder.mockReset(); + getDefaultResultOrder.mockReturnValue("ipv4first"); }); afterEach(() => { @@ -368,9 +374,9 @@ describe("resolveTelegramFetch", () => { resolveTelegramFetchOrThrow(); expect(loggerInfo).not.toHaveBeenCalledWith("autoSelectFamily=true (default-node22)"); - expect(loggerInfo).not.toHaveBeenCalledWith("dnsResultOrder=ipv4first (default-node22)"); + expect(loggerInfo).not.toHaveBeenCalledWith("dnsResultOrder=ipv4first (process-default)"); expect(loggerDebug).toHaveBeenCalledWith("autoSelectFamily=true (default-node22)"); - expect(loggerDebug).toHaveBeenCalledWith("dnsResultOrder=ipv4first (default-node22)"); + expect(loggerDebug).toHaveBeenCalledWith("dnsResultOrder=ipv4first (process-default)"); }); it("uses EnvHttpProxyAgent dispatcher when proxy env is configured", async () => { @@ -813,6 +819,12 @@ describe("resolveTelegramFetch", () => { autoSelectFamily: false, }), ); + expect(loggerDebug).toHaveBeenCalledWith( + expect.stringContaining("fetch fallback: enabling sticky IPv4-only dispatcher"), + ); + expect(loggerWarn).not.toHaveBeenCalledWith( + expect.stringContaining("fetch fallback: enabling sticky IPv4-only dispatcher"), + ); }); it("escalates from IPv4 fallback to pinned Telegram IP and keeps it sticky", async () => { @@ -841,6 +853,9 @@ describe("resolveTelegramFetch", () => { expect(secondDispatcher).not.toBe(thirdDispatcher); expect(thirdDispatcher).toBe(fourthDispatcher); expectPinnedFallbackIpDispatcher(3); + expect(loggerWarn).toHaveBeenCalledWith( + expect.stringContaining("fetch fallback: DNS-resolved IP unreachable"), + ); }); it("keeps the armed fallback sticky when all attempts fail", async () => { diff --git a/extensions/telegram/src/fetch.ts b/extensions/telegram/src/fetch.ts index 0fdb95f1dc9..23dc8af19bb 100644 --- a/extensions/telegram/src/fetch.ts +++ b/extensions/telegram/src/fetch.ts @@ -75,6 +75,7 @@ type TelegramDispatcherAttempt = { type TelegramTransportAttempt = { createDispatcher: () => TelegramDispatcher; exportAttempt: TelegramDispatcherAttempt; + logLevel?: "debug" | "warn"; logMessage?: string; }; @@ -518,6 +519,7 @@ function createTelegramTransportAttempts(params: { return ipv4Dispatcher; }, exportAttempt: { dispatcherPolicy: fallbackPolicy }, + logLevel: "debug", logMessage: "fetch fallback: enabling sticky IPv4-only dispatcher", }); @@ -542,6 +544,7 @@ function createTelegramTransportAttempts(params: { return fallbackIpDispatcher; }, exportAttempt: { dispatcherPolicy: fallbackIpPolicy }, + logLevel: "warn", logMessage: "fetch fallback: DNS-resolved IP unreachable; trying alternative Telegram API IP", }); @@ -643,7 +646,12 @@ export function resolveTelegramTransport( const nextAttempt = transportAttempts[nextIndex]; if (nextAttempt.logMessage) { const reasonText = reason ? `, reason=${reason}` : ""; - log.warn(`${nextAttempt.logMessage} (codes=${formatErrorCodes(err)}${reasonText})`); + const logLine = `${nextAttempt.logMessage} (codes=${formatErrorCodes(err)}${reasonText})`; + if (nextAttempt.logLevel === "debug") { + log.debug(logLine); + } else { + log.warn(logLine); + } } stickyAttemptIndex = nextIndex; return true; diff --git a/extensions/telegram/src/network-config.test.ts b/extensions/telegram/src/network-config.test.ts index c0a9b978d48..6c037af8216 100644 --- a/extensions/telegram/src/network-config.test.ts +++ b/extensions/telegram/src/network-config.test.ts @@ -202,31 +202,53 @@ describe("resolveTelegramDnsResultOrderDecision", () => { name: "ignores invalid env and config values before applying Node 22 default", env: { OPENCLAW_TELEGRAM_DNS_RESULT_ORDER: "bogus" }, network: { dnsResultOrder: "invalid" } as unknown as TelegramNetworkConfig, + defaultResultOrder: "ipv6first", nodeMajor: 22, expected: { value: "ipv4first", source: "default-node22" }, }, + { + name: "inherits process default when env and config are unset", + defaultResultOrder: "ipv4first", + nodeMajor: 20, + expected: { value: "ipv4first", source: "process-default" }, + }, + { + name: "prefers config over process default", + network: { dnsResultOrder: "verbatim" }, + defaultResultOrder: "ipv4first", + nodeMajor: 20, + expected: { value: "verbatim", source: "config" }, + }, ] satisfies Array<{ name: string; env?: NodeJS.ProcessEnv; network?: TelegramNetworkConfig; + defaultResultOrder?: string | null; nodeMajor: number; expected: ReturnType; - }>)("$name", ({ env, network, nodeMajor, expected }) => { + }>)("$name", ({ env, network, defaultResultOrder, nodeMajor, expected }) => { const decision = resolveTelegramDnsResultOrderDecision({ env, network, + defaultResultOrder, nodeMajor, }); expect(decision).toEqual(expected); }); it("defaults to ipv4first on Node 22", () => { - const decision = resolveTelegramDnsResultOrderDecision({ nodeMajor: 22 }); + const decision = resolveTelegramDnsResultOrderDecision({ + defaultResultOrder: null, + nodeMajor: 22, + }); expect(decision).toEqual({ value: "ipv4first", source: "default-node22" }); }); it("returns null when no dns decision applies", () => { - const decision = resolveTelegramDnsResultOrderDecision({ nodeMajor: 20 }); + const decision = resolveTelegramDnsResultOrderDecision({ + defaultResultOrder: null, + nodeMajor: 20, + }); expect(decision).toEqual({ value: null }); }); }); diff --git a/extensions/telegram/src/network-config.ts b/extensions/telegram/src/network-config.ts index 0ea6790b2ab..d849fd29499 100644 --- a/extensions/telegram/src/network-config.ts +++ b/extensions/telegram/src/network-config.ts @@ -1,3 +1,4 @@ +import * as dns from "node:dns"; import process from "node:process"; import type { TelegramNetworkConfig } from "openclaw/plugin-sdk/config-types"; import { isTruthyEnvValue, isWSL2Sync } from "openclaw/plugin-sdk/runtime-env"; @@ -66,12 +67,14 @@ export function resolveTelegramAutoSelectFamilyDecision(params?: { * Priority: * 1. Environment variable OPENCLAW_TELEGRAM_DNS_RESULT_ORDER * 2. Config: channels.telegram.network.dnsResultOrder - * 3. Default: "ipv4first" on Node 22+ (to work around common IPv6 issues) + * 3. Process default: dns.getDefaultResultOrder() + * 4. Default: "ipv4first" on Node 22+ (to work around common IPv6 issues) */ export function resolveTelegramDnsResultOrderDecision(params?: { network?: TelegramNetworkConfig; env?: NodeJS.ProcessEnv; nodeMajor?: number; + defaultResultOrder?: string | null; }): TelegramDnsResultOrderDecision { const env = params?.env ?? process.env; const nodeMajor = @@ -93,6 +96,15 @@ export function resolveTelegramDnsResultOrderDecision(params?: { return { value: configValue, source: "config" }; } + const processDefaultValue = normalizeOptionalLowercaseString( + params && "defaultResultOrder" in params + ? params.defaultResultOrder + : dns.getDefaultResultOrder?.(), + ); + if (processDefaultValue === "ipv4first" || processDefaultValue === "verbatim") { + return { value: processDefaultValue, source: "process-default" }; + } + // Default to ipv4first on Node 22+ to avoid IPv6 issues if (Number.isFinite(nodeMajor) && nodeMajor >= 22) { return { value: "ipv4first", source: "default-node22" };