From 7f6422c8977ce782f15809fda26ae67a1d4c7aa9 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:20:39 -0600 Subject: [PATCH] Telegram: preserve topic IDs in restart notifications (#1807) Co-authored-by: hsrvc --- CHANGELOG.md | 1 + src/agents/tools/sessions-send-helpers.ts | 24 +++++++++++++++++++++-- src/gateway/server-restart-sentinel.ts | 16 ++++++++++----- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 480767383..dc46291fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Status: unreleased. - Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla. - Routing: precompile session key regexes. (#1697) Thanks @Ray0907. - TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein. +- Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc. ## 2026.1.24-3 diff --git a/src/agents/tools/sessions-send-helpers.ts b/src/agents/tools/sessions-send-helpers.ts index 5e758d426..c9940de0f 100644 --- a/src/agents/tools/sessions-send-helpers.ts +++ b/src/agents/tools/sessions-send-helpers.ts @@ -14,6 +14,7 @@ export type AnnounceTarget = { channel: string; to: string; accountId?: string; + threadId?: string; // Forum topic/thread ID }; export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget | null { @@ -22,7 +23,22 @@ export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget if (parts.length < 3) return null; const [channelRaw, kind, ...rest] = parts; if (kind !== "group" && kind !== "channel") return null; - const id = rest.join(":").trim(); + + // Extract topic/thread ID from rest (supports both :topic: and :thread:) + // Telegram uses :topic:, other platforms use :thread: + let threadId: string | undefined; + const restJoined = rest.join(":"); + const topicMatch = restJoined.match(/:topic:(\d+)$/); + const threadMatch = restJoined.match(/:thread:(\d+)$/); + const match = topicMatch || threadMatch; + + if (match) { + threadId = match[1]; // Keep as string to match AgentCommandOpts.threadId + } + + // Remove :topic:N or :thread:N suffix from ID for target + const id = match ? restJoined.replace(/:(topic|thread):\d+$/, "") : restJoined.trim(); + if (!id) return null; if (!channelRaw) return null; const normalizedChannel = normalizeAnyChannelId(channelRaw) ?? normalizeChatChannelId(channelRaw); @@ -37,7 +53,11 @@ export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget const normalized = normalizedChannel ? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(kindTarget) : undefined; - return { channel, to: normalized ?? kindTarget }; + return { + channel, + to: normalized ?? kindTarget, + threadId, + }; } export function buildAgentToAgentMessageContext(params: { diff --git a/src/gateway/server-restart-sentinel.ts b/src/gateway/server-restart-sentinel.ts index fa33b7c21..28719290e 100644 --- a/src/gateway/server-restart-sentinel.ts +++ b/src/gateway/server-restart-sentinel.ts @@ -28,11 +28,16 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) { return; } - const threadMarker = ":thread:"; - const threadIndex = sessionKey.lastIndexOf(threadMarker); - const baseSessionKey = threadIndex === -1 ? sessionKey : sessionKey.slice(0, threadIndex); + // Extract topic/thread ID from sessionKey (supports both :topic: and :thread:) + // Telegram uses :topic:, other platforms use :thread: + const topicIndex = sessionKey.lastIndexOf(":topic:"); + const threadIndex = sessionKey.lastIndexOf(":thread:"); + const markerIndex = Math.max(topicIndex, threadIndex); + const marker = topicIndex > threadIndex ? ":topic:" : ":thread:"; + + const baseSessionKey = markerIndex === -1 ? sessionKey : sessionKey.slice(0, markerIndex); const threadIdRaw = - threadIndex === -1 ? undefined : sessionKey.slice(threadIndex + threadMarker.length); + markerIndex === -1 ? undefined : sessionKey.slice(markerIndex + marker.length); const sessionThreadId = threadIdRaw?.trim() || undefined; const { cfg, entry } = loadSessionEntry(sessionKey); @@ -42,7 +47,7 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) { // Handles race condition where store wasn't flushed before restart const sentinelContext = payload.deliveryContext; let sessionDeliveryContext = deliveryContextFromSession(entry); - if (!sessionDeliveryContext && threadIndex !== -1 && baseSessionKey) { + if (!sessionDeliveryContext && markerIndex !== -1 && baseSessionKey) { const { entry: baseEntry } = loadSessionEntry(baseSessionKey); sessionDeliveryContext = deliveryContextFromSession(baseEntry); } @@ -74,6 +79,7 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) { const threadId = payload.threadId ?? + parsedTarget?.threadId ?? // From resolveAnnounceTargetFromKey (extracts :topic:N) sessionThreadId ?? (origin?.threadId != null ? String(origin.threadId) : undefined);