fix(telegram): cron and heartbeat messages land in wrong chat instead of target topic (#19367)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: bf02bbf9ce
Co-authored-by: Lukavyi <1013690+Lukavyi@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
Taras Lukavyi
2026-02-18 11:01:01 +01:00
committed by GitHub
parent 114736ed1a
commit d833dcd731
11 changed files with 441 additions and 15 deletions

View File

@@ -5,6 +5,7 @@ import type { OpenClawConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js";
import type { AgentDefaultsConfig } from "../../config/types.agent-defaults.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import { parseTelegramTarget } from "../../telegram/targets.js";
import { deliveryContextFromSession } from "../../utils/delivery-context.js";
import type {
DeliverableMessageChannel,
@@ -26,6 +27,7 @@ export type OutboundTarget = {
to?: string;
reason?: string;
accountId?: string;
threadId?: string | number;
lastChannel?: DeliverableMessageChannel;
lastAccountId?: string;
};
@@ -43,6 +45,8 @@ export type SessionDeliveryTarget = {
to?: string;
accountId?: string;
threadId?: string | number;
/** Whether threadId came from an explicit source (config/param/:topic: parsing) vs session history. */
threadIdExplicit?: boolean;
mode: ChannelOutboundTargetMode;
lastChannel?: DeliverableMessageChannel;
lastTo?: string;
@@ -75,20 +79,33 @@ export function resolveSessionDeliveryTarget(params: {
? requested
: undefined;
const explicitTo =
const rawExplicitTo =
typeof params.explicitTo === "string" && params.explicitTo.trim()
? params.explicitTo.trim()
: undefined;
const explicitThreadId =
params.explicitThreadId != null && params.explicitThreadId !== ""
? params.explicitThreadId
: undefined;
let channel = requestedChannel === "last" ? lastChannel : requestedChannel;
if (!channel && params.fallbackChannel && isDeliverableMessageChannel(params.fallbackChannel)) {
channel = params.fallbackChannel;
}
// Parse :topic:NNN from explicitTo (Telegram topic syntax).
// Only applies when we positively know the channel is Telegram.
// When channel is unknown, the downstream send path (resolveTelegramSession)
// handles :topic: parsing independently.
const isTelegramContext = channel === "telegram" || (!channel && lastChannel === "telegram");
let explicitTo = rawExplicitTo;
let parsedThreadId: number | undefined;
if (isTelegramContext && rawExplicitTo && rawExplicitTo.includes(":topic:")) {
const parsed = parseTelegramTarget(rawExplicitTo);
explicitTo = parsed.chatId;
parsedThreadId = parsed.messageThreadId;
}
const explicitThreadId =
params.explicitThreadId != null && params.explicitThreadId !== ""
? params.explicitThreadId
: parsedThreadId;
let to = explicitTo;
if (!to && lastTo) {
if (channel && channel === lastChannel) {
@@ -102,11 +119,13 @@ export function resolveSessionDeliveryTarget(params: {
const threadId = channel && channel === lastChannel ? lastThreadId : undefined;
const mode = params.mode ?? (explicitTo ? "explicit" : "implicit");
const resolvedThreadId = explicitThreadId ?? threadId;
return {
channel,
to,
accountId,
threadId: explicitThreadId ?? threadId,
threadId: resolvedThreadId,
threadIdExplicit: resolvedThreadId != null && explicitThreadId != null,
mode,
lastChannel,
lastTo,
@@ -281,6 +300,7 @@ export function resolveHeartbeatDeliveryTarget(params: {
to: resolved.to,
reason,
accountId: effectiveAccountId,
threadId: resolvedTarget.threadId,
lastChannel: resolvedTarget.lastChannel,
lastAccountId: resolvedTarget.lastAccountId,
};