diff --git a/src/agents/subagent-announce-delivery.test.ts b/src/agents/subagent-announce-delivery.test.ts index 5ea0537336b..8002893be52 100644 --- a/src/agents/subagent-announce-delivery.test.ts +++ b/src/agents/subagent-announce-delivery.test.ts @@ -395,6 +395,27 @@ async function deliverSlackChannelAnnouncement(params: { } describe("resolveAnnounceOrigin threaded route targets", () => { + it("does not inherit a target or thread from another account on the same channel", () => { + expect( + resolveAnnounceOrigin( + { + lastChannel: "telegram", + lastTo: "peer-b", + lastAccountId: "bot-b", + lastThreadId: 99, + }, + { + channel: "telegram", + accountId: "bot-a", + }, + ), + ).toEqual({ + channel: "telegram", + to: undefined, + accountId: "bot-a", + }); + }); + it("preserves stored thread ids when requester origin omits one for the same chat", () => { expect( resolveAnnounceOrigin( diff --git a/src/agents/tools/media-generate-background-shared.ts b/src/agents/tools/media-generate-background-shared.ts index 1c32aaf7e51..eabe1797cba 100644 --- a/src/agents/tools/media-generate-background-shared.ts +++ b/src/agents/tools/media-generate-background-shared.ts @@ -19,12 +19,7 @@ import { resolveRequiredCompletionDeliveryFailureTerminalResult, type RequiredCompletionTerminalResult, } from "../../tasks/task-completion-contract.js"; -import { - deliveryContextFromSession, - normalizeDeliveryContext, - type DeliveryContext, -} from "../../utils/delivery-context.js"; -import type { DeliveryContextSessionSource } from "../../utils/delivery-context.types.js"; +import { normalizeDeliveryContext, type DeliveryContext } from "../../utils/delivery-context.js"; import { INTERNAL_MESSAGE_CHANNEL, isDeliverableMessageChannel, @@ -65,21 +60,6 @@ export type MediaGenerateBackgroundScheduler = (work: () => Promise) => vo /** Optional callback invoked when async media generation starts. */ export type MediaGenerateAsyncStartCallback = (message: string) => Promise | void; -function resolvePinnedMediaRequesterOrigin(params: { - requesterOrigin?: DeliveryContext; - sessionEntry?: DeliveryContextSessionSource; -}): DeliveryContext | undefined { - const requesterOrigin = normalizeDeliveryContext(params.requesterOrigin); - const sessionOrigin = deliveryContextFromSession(params.sessionEntry); - const accountsConflict = - requesterOrigin?.accountId && - sessionOrigin?.accountId && - requesterOrigin.accountId !== sessionOrigin.accountId; - return accountsConflict - ? requesterOrigin - : resolveAnnounceOrigin(params.sessionEntry, requesterOrigin); -} - /** Returns whether a media generation request should detach for a session. */ export function shouldDetachMediaGenerationTask(sessionKey: string | undefined): boolean { const normalizedSessionKey = sessionKey?.trim(); @@ -168,10 +148,10 @@ function createMediaGenerationTaskRun(params: { try { // Pin the complete requester route when detached work starts. Completion-time // session state can move to another peer while generation is still running. - const requesterOrigin = resolvePinnedMediaRequesterOrigin({ - requesterOrigin: params.requesterOrigin, - sessionEntry: loadRequesterSessionEntry(sessionKey).entry, - }); + const requesterOrigin = resolveAnnounceOrigin( + loadRequesterSessionEntry(sessionKey).entry, + params.requesterOrigin, + ); const task = createRunningTaskRun({ runtime: "cli", taskKind: params.taskKind, diff --git a/src/utils/delivery-context.shared.ts b/src/utils/delivery-context.shared.ts index a7dcce48f4a..7bdc528c1e4 100644 --- a/src/utils/delivery-context.shared.ts +++ b/src/utils/delivery-context.shared.ts @@ -229,7 +229,7 @@ export function deliveryContextFromSession( return normalizeSessionDeliveryFields(source).deliveryContext; } -/** Merges delivery contexts without mixing target/account/thread fields across channels. */ +/** Merges delivery contexts without mixing target/account/thread fields across route owners. */ export function mergeDeliveryContext( primary?: DeliveryContext, fallback?: DeliveryContext, @@ -243,17 +243,22 @@ export function mergeDeliveryContext( normalizedPrimary?.channel && normalizedFallback?.channel && normalizedPrimary.channel !== normalizedFallback.channel; + const accountsConflict = + normalizedPrimary?.accountId && + normalizedFallback?.accountId && + normalizedPrimary.accountId !== normalizedFallback.accountId; + const routesConflict = channelsConflict || accountsConflict; return normalizeDeliveryContext({ - channel: normalizedPrimary?.channel ?? normalizedFallback?.channel, - // Keep route fields paired to their channel; avoid crossing fields between - // unrelated channels during session context merges. - to: channelsConflict - ? normalizedPrimary?.to - : (normalizedPrimary?.to ?? normalizedFallback?.to), - accountId: channelsConflict + channel: accountsConflict + ? normalizedPrimary?.channel + : (normalizedPrimary?.channel ?? normalizedFallback?.channel), + // Keep route fields paired to their channel account; crossing either owner + // can address one account's target through another account's credentials. + to: routesConflict ? normalizedPrimary?.to : (normalizedPrimary?.to ?? normalizedFallback?.to), + accountId: routesConflict ? normalizedPrimary?.accountId : (normalizedPrimary?.accountId ?? normalizedFallback?.accountId), - threadId: channelsConflict + threadId: routesConflict ? normalizedPrimary?.threadId : (normalizedPrimary?.threadId ?? normalizedFallback?.threadId), }); diff --git a/src/utils/delivery-context.test.ts b/src/utils/delivery-context.test.ts index bae7a0576e8..7707589f67d 100644 --- a/src/utils/delivery-context.test.ts +++ b/src/utils/delivery-context.test.ts @@ -53,6 +53,31 @@ describe("delivery context helpers", () => { }); }); + it("does not inherit route fields from a different account on the same channel", () => { + const merged = mergeDeliveryContext( + { channel: "telegram", accountId: "bot-a" }, + { channel: "telegram", to: "123", accountId: "bot-b", threadId: "99" }, + ); + + expect(merged).toEqual({ + channel: "telegram", + to: undefined, + accountId: "bot-a", + }); + expect(merged?.threadId).toBeUndefined(); + + expect( + mergeDeliveryContext( + { accountId: "bot-a" }, + { channel: "telegram", to: "123", accountId: "bot-b", threadId: "99" }, + ), + ).toEqual({ + channel: undefined, + to: undefined, + accountId: "bot-a", + }); + }); + it("uses fallback route fields when fallback has no channel", () => { const merged = mergeDeliveryContext( { channel: "demo-channel" },