diff --git a/extensions/telegram/src/inbound-turn-delivery.test.ts b/extensions/telegram/src/inbound-turn-delivery.test.ts index b73a08afa17..6f27c8e7e88 100644 --- a/extensions/telegram/src/inbound-turn-delivery.test.ts +++ b/extensions/telegram/src/inbound-turn-delivery.test.ts @@ -46,6 +46,42 @@ describe("telegram inbound turn delivery", () => { end(); }); + it("matches provider-prefixed Telegram targets for delivery correlation", () => { + let count = 0; + const end = beginTelegramInboundTurnDeliveryCorrelation("sess:prefixed", { + outboundTo: "-100123", + markInboundTurnDelivered: () => { + count += 1; + }, + }); + + notifyTelegramInboundTurnOutboundSuccess({ + sessionKey: "sess:prefixed", + to: "telegram:-100123", + }); + + expect(count).toBe(1); + end(); + }); + + it("matches Telegram topic targets by conversation for delivery correlation", () => { + let count = 0; + const end = beginTelegramInboundTurnDeliveryCorrelation("sess:topic", { + outboundTo: "-100123", + markInboundTurnDelivered: () => { + count += 1; + }, + }); + + notifyTelegramInboundTurnOutboundSuccess({ + sessionKey: "sess:topic", + to: "telegram:-100123:topic:77", + }); + + expect(count).toBe(1); + end(); + }); + it("keeps user-request and room-event delivery correlations separate", () => { let userRequestCount = 0; let roomEventCount = 0; diff --git a/extensions/telegram/src/inbound-turn-delivery.ts b/extensions/telegram/src/inbound-turn-delivery.ts index 78cae4ba287..2ea45a0fd4b 100644 --- a/extensions/telegram/src/inbound-turn-delivery.ts +++ b/extensions/telegram/src/inbound-turn-delivery.ts @@ -9,6 +9,30 @@ type ActiveTurn = { const registry = new Map(); +function normalizeTelegramDeliveryTarget(value: string): string { + return value + .trim() + .toLowerCase() + .replace(/^(telegram|tg):/u, ""); +} + +function stripTelegramTopicTarget(value: string): string { + return value.replace(/:topic:\d+$/u, ""); +} + +function telegramDeliveryTargetsMatch(expected: string, actual: string): boolean { + const expectedTarget = normalizeTelegramDeliveryTarget(expected); + const actualTarget = normalizeTelegramDeliveryTarget(actual); + if (expectedTarget === actualTarget) { + return true; + } + const expectedBase = stripTelegramTopicTarget(expectedTarget); + const actualBase = stripTelegramTopicTarget(actualTarget); + return ( + expectedBase === actualBase && (expectedTarget === expectedBase || actualTarget === actualBase) + ); +} + export function resolveTelegramInboundTurnDeliveryCorrelationKey( sessionKey: string | undefined, inboundTurnKind?: TelegramInboundTurnDeliveryKind | string, @@ -52,7 +76,7 @@ export function notifyTelegramInboundTurnOutboundSuccess(params: { return; } const turn = registry.get(key); - if (!turn || turn.outboundTo !== params.to) { + if (!turn || !telegramDeliveryTargetsMatch(turn.outboundTo, params.to)) { return; } if (turn.outboundAccountId && params.accountId && params.accountId !== turn.outboundAccountId) { diff --git a/src/auto-reply/reply/dispatch-from-config.test.ts b/src/auto-reply/reply/dispatch-from-config.test.ts index 8661190f9eb..afe4ac04b61 100644 --- a/src/auto-reply/reply/dispatch-from-config.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.test.ts @@ -4785,6 +4785,43 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () => expect(dispatcher.sendToolResult).not.toHaveBeenCalled(); }); + it("suppresses marked runtime failure notices for room events", async () => { + setNoAbort(); + sessionStoreMocks.currentEntry = { + sessionId: "s1", + updatedAt: 0, + sendPolicy: "allow", + }; + const dispatcher = createDispatcher(); + const failureNotice = setReplyPayloadMetadata( + { text: "⚠️ You've reached your Codex subscription usage limit." }, + { deliverDespiteSourceReplySuppression: true }, + ); + const replyResolver = vi.fn(async () => failureNotice satisfies ReplyPayload); + const ctx = buildTestCtx({ + ChatType: "group", + InboundTurnKind: "room_event", + SessionKey: "test:session", + }); + + const result = await dispatchReplyFromConfig({ + ctx, + cfg: emptyConfig, + dispatcher, + replyResolver, + replyOptions: { + sourceReplyDeliveryMode: "message_tool_only", + }, + }); + + expect(replyResolver).toHaveBeenCalledTimes(1); + expect(result.queuedFinal).toBe(false); + expect(result.sourceReplyDeliveryMode).toBe("message_tool_only"); + expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); + expect(dispatcher.sendBlockReply).not.toHaveBeenCalled(); + expect(dispatcher.sendToolResult).not.toHaveBeenCalled(); + }); + it("mirrors internal source reply payloads into the active transcript", async () => { setNoAbort(); sessionStoreMocks.currentEntry = { diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index 217da1f2f59..095da53957b 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -1599,6 +1599,7 @@ export async function dispatchReplyFromConfig( let finalDeliveryFailed = false; const shouldDeliverDespiteSourceReplySuppression = (reply: ReplyPayload) => suppressAutomaticSourceDelivery && + ctx.InboundTurnKind !== "room_event" && !sendPolicyDenied && getReplyPayloadMetadata(reply)?.deliverDespiteSourceReplySuppression === true; for (const reply of replies) {