From 6a27db0cd7f59d80fc5cfa8a76b9c65d8cdbce22 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 17 Mar 2026 03:56:17 +0000 Subject: [PATCH] refactor(outbound): share thread id normalization --- extensions/discord/src/channel.ts | 15 +-------------- extensions/slack/src/channel.ts | 15 +-------------- extensions/telegram/src/channel.ts | 15 +-------------- src/infra/outbound/outbound-session.ts | 17 ++--------------- src/infra/outbound/thread-id.test.ts | 20 ++++++++++++++++++++ src/infra/outbound/thread-id.ts | 13 +++++++++++++ 6 files changed, 38 insertions(+), 57 deletions(-) create mode 100644 src/infra/outbound/thread-id.test.ts create mode 100644 src/infra/outbound/thread-id.ts diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 68e12e1e78b..b598f004cf7 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -26,6 +26,7 @@ import { type OpenClawConfig, } from "openclaw/plugin-sdk/discord"; import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js"; +import { normalizeOutboundThreadId } from "../../../src/infra/outbound/thread-id.js"; import { normalizeMessageChannel } from "../../../src/utils/message-channel.js"; import { listDiscordAccountIds, @@ -196,20 +197,6 @@ function parseDiscordExplicitTarget(raw: string) { } } -function normalizeOutboundThreadId(value?: string | number | null): string | undefined { - if (value == null) { - return undefined; - } - if (typeof value === "number") { - if (!Number.isFinite(value)) { - return undefined; - } - return String(Math.trunc(value)); - } - const trimmed = value.trim(); - return trimmed ? trimmed : undefined; -} - function buildDiscordBaseSessionKey(params: { cfg: OpenClawConfig; agentId: string; diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index e1c515576d9..74b283884a7 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -25,6 +25,7 @@ import { type OpenClawConfig, } from "openclaw/plugin-sdk/slack"; import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js"; +import { normalizeOutboundThreadId } from "../../../src/infra/outbound/thread-id.js"; import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js"; import { listEnabledSlackAccounts, @@ -136,20 +137,6 @@ function parseSlackExplicitTarget(raw: string) { }; } -function normalizeOutboundThreadId(value?: string | number | null): string | undefined { - if (value == null) { - return undefined; - } - if (typeof value === "number") { - if (!Number.isFinite(value)) { - return undefined; - } - return String(Math.trunc(value)); - } - const trimmed = value.trim(); - return trimmed ? trimmed : undefined; -} - function buildSlackBaseSessionKey(params: { cfg: OpenClawConfig; agentId: string; diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 45cd93cd9e5..797b60c85d8 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -31,6 +31,7 @@ import { type OutboundSendDeps, resolveOutboundSendDep, } from "../../../src/infra/outbound/send-deps.js"; +import { normalizeOutboundThreadId } from "../../../src/infra/outbound/thread-id.js"; import { normalizeMessageChannel } from "../../../src/utils/message-channel.js"; import { inspectTelegramAccount } from "./account-inspect.js"; import { @@ -185,20 +186,6 @@ function parseTelegramExplicitTarget(raw: string) { }; } -function normalizeOutboundThreadId(value?: string | number | null): string | undefined { - if (value == null) { - return undefined; - } - if (typeof value === "number") { - if (!Number.isFinite(value)) { - return undefined; - } - return String(Math.trunc(value)); - } - const trimmed = value.trim(); - return trimmed ? trimmed : undefined; -} - function buildTelegramBaseSessionKey(params: { cfg: OpenClawConfig; agentId: string; diff --git a/src/infra/outbound/outbound-session.ts b/src/infra/outbound/outbound-session.ts index c8da99c5f66..a65e2da313e 100644 --- a/src/infra/outbound/outbound-session.ts +++ b/src/infra/outbound/outbound-session.ts @@ -8,6 +8,7 @@ import { buildAgentSessionKey, type RoutePeer } from "../../routing/resolve-rout import { resolveThreadSessionKeys } from "../../routing/session-key.js"; import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../whatsapp/normalize.js"; import type { ResolvedMessagingTarget } from "./target-resolver.js"; +import { normalizeOutboundThreadId } from "./thread-id.js"; export type OutboundSessionRoute = { sessionKey: string; @@ -30,20 +31,6 @@ export type ResolveOutboundSessionRouteParams = { threadId?: string | number | null; }; -function normalizeThreadId(value?: string | number | null): string | undefined { - if (value == null) { - return undefined; - } - if (typeof value === "number") { - if (!Number.isFinite(value)) { - return undefined; - } - return String(Math.trunc(value)); - } - const trimmed = value.trim(); - return trimmed ? trimmed : undefined; -} - function stripProviderPrefix(raw: string, channel: string): string { const trimmed = raw.trim(); const lower = trimmed.toLowerCase(); @@ -240,7 +227,7 @@ function resolveMattermostSession( channel: "mattermost", peer: { kind: isUser ? "direct" : "channel", id: rawId }, }); - const threadId = normalizeThreadId(params.replyToId ?? params.threadId); + const threadId = normalizeOutboundThreadId(params.replyToId ?? params.threadId); const threadKeys = resolveThreadSessionKeys({ baseSessionKey, threadId, diff --git a/src/infra/outbound/thread-id.test.ts b/src/infra/outbound/thread-id.test.ts new file mode 100644 index 00000000000..a872c0d78d7 --- /dev/null +++ b/src/infra/outbound/thread-id.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { normalizeOutboundThreadId } from "./thread-id.js"; + +describe("normalizeOutboundThreadId", () => { + it("returns undefined for missing values", () => { + expect(normalizeOutboundThreadId()).toBeUndefined(); + expect(normalizeOutboundThreadId(null)).toBeUndefined(); + expect(normalizeOutboundThreadId(" ")).toBeUndefined(); + }); + + it("normalizes numbers and trims strings", () => { + expect(normalizeOutboundThreadId(123.9)).toBe("123"); + expect(normalizeOutboundThreadId(" 456 ")).toBe("456"); + }); + + it("drops non-finite numeric values", () => { + expect(normalizeOutboundThreadId(Number.NaN)).toBeUndefined(); + expect(normalizeOutboundThreadId(Number.POSITIVE_INFINITY)).toBeUndefined(); + }); +}); diff --git a/src/infra/outbound/thread-id.ts b/src/infra/outbound/thread-id.ts new file mode 100644 index 00000000000..287ce99d34a --- /dev/null +++ b/src/infra/outbound/thread-id.ts @@ -0,0 +1,13 @@ +export function normalizeOutboundThreadId(value?: string | number | null): string | undefined { + if (value == null) { + return undefined; + } + if (typeof value === "number") { + if (!Number.isFinite(value)) { + return undefined; + } + return String(Math.trunc(value)); + } + const trimmed = value.trim(); + return trimmed ? trimmed : undefined; +}