From 43c8cde3de2c4ec2e4c7312cee8a8722954e9fbc Mon Sep 17 00:00:00 2001 From: hope Date: Wed, 4 Mar 2026 19:35:04 +0800 Subject: [PATCH 1/5] fix: deliver inter-session replies to bound channel (Telegram) Fixes #34308 When a session has a deliveryContext bound to an external channel (e.g., Telegram), replies to inter-session messages (via sessions_send) were not being delivered to the bound channel - they were only stored in session history. This fix adds logic to resolveLastChannelRaw and resolveLastToRaw: - When incoming channel is INTERNAL but session has a persisted external channel (deliveryContext), use the persisted channel for reply delivery. --- src/auto-reply/reply/session-delivery.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/auto-reply/reply/session-delivery.ts b/src/auto-reply/reply/session-delivery.ts index 9a112a6829e..e69c3c7267f 100644 --- a/src/auto-reply/reply/session-delivery.ts +++ b/src/auto-reply/reply/session-delivery.ts @@ -117,6 +117,14 @@ export function resolveLastChannelRaw(params: { resolved = sessionKeyChannelHint; } } + // Fix #34308: When channel is INTERNAL but session has a persisted external channel + // (deliveryContext), use the persisted channel for reply delivery. + if ( + originatingChannel === INTERNAL_MESSAGE_CHANNEL && + isExternalRoutingChannel(persistedChannel) + ) { + resolved = persistedChannel; + } return resolved; } @@ -151,6 +159,15 @@ export function resolveLastToRaw(params: { } } + // Fix #34308: When channel is INTERNAL but session has a persisted external channel + // (deliveryContext), use the persisted to address for reply delivery. + if ( + originatingChannel === INTERNAL_MESSAGE_CHANNEL && + isExternalRoutingChannel(persistedChannel) + ) { + return params.persistedLastTo; + } + return params.originatingToRaw || params.toRaw || params.persistedLastTo; } From 48f0d08be65c246cdb0e66a13f83eee7c461120e Mon Sep 17 00:00:00 2001 From: hope Date: Wed, 4 Mar 2026 17:20:05 +0800 Subject: [PATCH 2/5] fix(web): add fallback for clipboard API in insecure contexts (#34092) - Clipboard API requires secure context (HTTPS) to work - Windows HTTP environments fail silently - Add textarea element fallback for non-secure contexts --- ui/src/ui/chat/copy-as-markdown.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/ui/src/ui/chat/copy-as-markdown.ts b/ui/src/ui/chat/copy-as-markdown.ts index 12aeb6999e6..d402bdeccb7 100644 --- a/ui/src/ui/chat/copy-as-markdown.ts +++ b/ui/src/ui/chat/copy-as-markdown.ts @@ -17,11 +17,28 @@ async function copyTextToClipboard(text: string): Promise { return false; } + // Try Clipboard API first (works in secure contexts/HTTPS) try { await navigator.clipboard.writeText(text); return true; } catch { - return false; + // Fallback for non-secure contexts (HTTP on Windows/localhost) + try { + // Use textarea element fallback for insecure contexts + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + textarea.style.left = "-9999px"; + textarea.style.top = "-9999px"; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + const success = document.execCommand("copy"); + document.body.removeChild(textarea); + return success; + } catch { + return false; + } } } From 51f31021b985cccc1bd8ff125b7ff220f0b96991 Mon Sep 17 00:00:00 2001 From: hope Date: Wed, 4 Mar 2026 18:04:13 +0800 Subject: [PATCH 3/5] fix(web): use try/finally to guarantee textarea cleanup This addresses Greptile feedback about DOM leak when execCommand throws. The textarea element is now always removed in the finally block. Ref: #34092 --- ui/src/ui/chat/copy-as-markdown.ts | 37 +++++++++++++++++------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/ui/src/ui/chat/copy-as-markdown.ts b/ui/src/ui/chat/copy-as-markdown.ts index d402bdeccb7..4065f5afda9 100644 --- a/ui/src/ui/chat/copy-as-markdown.ts +++ b/ui/src/ui/chat/copy-as-markdown.ts @@ -23,22 +23,27 @@ async function copyTextToClipboard(text: string): Promise { return true; } catch { // Fallback for non-secure contexts (HTTP on Windows/localhost) - try { - // Use textarea element fallback for insecure contexts - const textarea = document.createElement("textarea"); - textarea.value = text; - textarea.style.position = "fixed"; - textarea.style.left = "-9999px"; - textarea.style.top = "-9999px"; - document.body.appendChild(textarea); - textarea.focus(); - textarea.select(); - const success = document.execCommand("copy"); - document.body.removeChild(textarea); - return success; - } catch { - return false; - } + return copyViaExecCommand(text); + } +} + +function copyViaExecCommand(text: string): boolean { + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + textarea.style.left = "-9999px"; + textarea.style.top = "-9999px"; + document.body.appendChild(textarea); + + try { + textarea.focus(); + textarea.select(); + return document.execCommand("copy"); + } catch { + return false; + } finally { + // Always clean up the textarea element to prevent DOM leak + document.body.removeChild(textarea); } } From 17243b9cb73c94796bf7b1e5c3ee03cd3a03acc6 Mon Sep 17 00:00:00 2001 From: hope Date: Thu, 12 Mar 2026 19:17:19 +0800 Subject: [PATCH 4/5] fix: address Greptile review - remove redundant block and add persistedLastTo guard - Remove redundant if block in resolveLastChannelRaw (lines 113-120) The existing block at lines 106-112 already handles this case - Add params.persistedLastTo guard in resolveLastToRaw (lines 153-160) Prevents premature return of undefined when persistedLastTo is falsy This avoids silently dropping message delivery Ref: #43703 (PR review from Greptile) --- src/auto-reply/reply/session-delivery.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/auto-reply/reply/session-delivery.ts b/src/auto-reply/reply/session-delivery.ts index e69c3c7267f..08da775f603 100644 --- a/src/auto-reply/reply/session-delivery.ts +++ b/src/auto-reply/reply/session-delivery.ts @@ -117,14 +117,6 @@ export function resolveLastChannelRaw(params: { resolved = sessionKeyChannelHint; } } - // Fix #34308: When channel is INTERNAL but session has a persisted external channel - // (deliveryContext), use the persisted channel for reply delivery. - if ( - originatingChannel === INTERNAL_MESSAGE_CHANNEL && - isExternalRoutingChannel(persistedChannel) - ) { - resolved = persistedChannel; - } return resolved; } @@ -160,10 +152,11 @@ export function resolveLastToRaw(params: { } // Fix #34308: When channel is INTERNAL but session has a persisted external channel - // (deliveryContext), use the persisted to address for reply delivery. + // (deliveryContext) AND a persisted lastTo address, use the persisted to address for reply delivery. if ( originatingChannel === INTERNAL_MESSAGE_CHANNEL && - isExternalRoutingChannel(persistedChannel) + isExternalRoutingChannel(persistedChannel) && + params.persistedLastTo ) { return params.persistedLastTo; } From 9da7988efc07814d1a9798e661ca4641225e5d40 Mon Sep 17 00:00:00 2001 From: hope Date: Fri, 13 Mar 2026 09:55:14 +0800 Subject: [PATCH 5/5] test: cover bound channel delivery and persistedLastTo fallback Add regression tests for inter-session reply delivery: - Test delivery to bound external channel when persistedLastTo is set - Test fallback to originatingToRaw when persistedLastTo is undefined - Regression test: ensure messages are not silently dropped Fixes #34308 Ref: #43703 (Greptile review feedback) --- src/auto-reply/reply/session-delivery.test.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/auto-reply/reply/session-delivery.test.ts b/src/auto-reply/reply/session-delivery.test.ts index bd5f0d7c27b..526a8b161be 100644 --- a/src/auto-reply/reply/session-delivery.test.ts +++ b/src/auto-reply/reply/session-delivery.test.ts @@ -1,6 +1,63 @@ import { describe, expect, it } from "vitest"; import { resolveLastChannelRaw, resolveLastToRaw } from "./session-delivery.js"; +describe("session delivery bound channel routing", () => { + it("delivers to bound external channel when persistedLastTo is set", () => { + // Session has deliveryContext bound to Telegram channel + const sessionKey = "agent:main:telegram:default:channel:C123456"; + + expect( + resolveLastChannelRaw({ + originatingChannelRaw: "INTERNAL_MESSAGE_CHANNEL", + persistedLastChannel: "telegram", + sessionKey, + }), + ).toBe("telegram"); + + expect( + resolveLastToRaw({ + originatingChannelRaw: "INTERNAL_MESSAGE_CHANNEL", + originatingToRaw: "session:inter-session-123", + persistedLastChannel: "telegram", + persistedLastTo: "C123456", + sessionKey, + }), + ).toBe("C123456"); + }); + + it("falls back to originatingToRaw when persistedLastTo is not set", () => { + // Session bound to external channel but lastTo not yet persisted + const sessionKey = "agent:main:telegram:default:channel:C123456"; + + expect( + resolveLastToRaw({ + originatingChannelRaw: "INTERNAL_MESSAGE_CHANNEL", + originatingToRaw: "session:inter-session-123", + persistedLastChannel: "telegram", + persistedLastTo: undefined, + sessionKey, + }), + ).toBe("session:inter-session-123"); + }); + + it("does not drop delivery when persistedLastTo is falsy", () => { + // Regression test: ensure we don't silently drop messages + const sessionKey = "agent:main:telegram:default:channel:C123456"; + + const result = resolveLastToRaw({ + originatingChannelRaw: "INTERNAL_MESSAGE_CHANNEL", + originatingToRaw: "session:inter-session-123", + persistedLastChannel: "telegram", + persistedLastTo: undefined, + sessionKey, + }); + + // Should NOT return undefined (which would drop the message) + expect(result).not.toBeUndefined(); + expect(result).toBe("session:inter-session-123"); + }); +}); + describe("session delivery direct-session routing overrides", () => { it.each([ "agent:main:direct:user-1",