diff --git a/CHANGELOG.md b/CHANGELOG.md index 64a07d9ef..527c637f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Health: add per-agent session summaries and account-level health details, and allow selective probes. (#1047) — thanks @gumadeiras. ### Fixes +- Sub-agents: route announce delivery through the correct channel account IDs. (#1061, #1058) — thanks @adam91holt. - Messages: `/stop` now hard-aborts queued followups and sub-agent runs; suppress zero-count stop notes. - Sessions: reset `compactionCount` on `/new` and `/reset`, and preserve `sessions.json` file mode (0600). - Sessions: repair orphaned user turns before embedded prompts. diff --git a/src/agents/clawdbot-tools.ts b/src/agents/clawdbot-tools.ts index e9efa14a4..b179795bc 100644 --- a/src/agents/clawdbot-tools.ts +++ b/src/agents/clawdbot-tools.ts @@ -108,6 +108,7 @@ export function createClawdbotTools(options?: { createSessionsSpawnTool({ agentSessionKey: options?.agentSessionKey, agentChannel: options?.agentChannel, + agentAccountId: options?.agentAccountId, sandboxed: options?.sandboxed, }), createSessionStatusTool({ diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index 6db380460..12fc5328d 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -306,6 +306,9 @@ async function sendAnnounce(item: AnnounceQueueItem) { params: { sessionKey: item.sessionKey, message: item.prompt, + channel: item.originatingChannel, + accountId: item.originatingAccountId, + to: item.originatingTo, deliver: true, idempotencyKey: crypto.randomUUID(), }, @@ -348,6 +351,7 @@ async function maybeQueueSubagentAnnounce(params: { requesterSessionKey: string; triggerMessage: string; summaryLine?: string; + requesterAccountId?: string; }): Promise<"steered" | "queued" | "none"> { const { cfg, entry } = loadRequesterSessionEntry(params.requesterSessionKey); const canonicalKey = resolveRequesterStoreKey(cfg, params.requesterSessionKey); @@ -382,7 +386,7 @@ async function maybeQueueSubagentAnnounce(params: { sessionKey: canonicalKey, originatingChannel: entry?.lastChannel, originatingTo: entry?.lastTo, - originatingAccountId: entry?.lastAccountId, + originatingAccountId: entry?.lastAccountId ?? params.requesterAccountId, }, queueSettings, ); @@ -505,6 +509,7 @@ export async function runSubagentAnnounceFlow(params: { childRunId: string; requesterSessionKey: string; requesterChannel?: string; + requesterAccountId?: string; requesterDisplayKey: string; task: string; timeoutMs: number; @@ -600,6 +605,7 @@ export async function runSubagentAnnounceFlow(params: { requesterSessionKey: params.requesterSessionKey, triggerMessage, summaryLine: taskLabel, + requesterAccountId: params.requesterAccountId, }); if (queued === "steered") { didAnnounce = true; @@ -617,6 +623,8 @@ export async function runSubagentAnnounceFlow(params: { sessionKey: params.requesterSessionKey, message: triggerMessage, deliver: true, + channel: params.requesterChannel, + accountId: params.requesterAccountId, idempotencyKey: crypto.randomUUID(), }, expectFinal: true, diff --git a/src/agents/subagent-registry.ts b/src/agents/subagent-registry.ts index 3dd821bef..6d36bdff7 100644 --- a/src/agents/subagent-registry.ts +++ b/src/agents/subagent-registry.ts @@ -13,6 +13,7 @@ export type SubagentRunRecord = { childSessionKey: string; requesterSessionKey: string; requesterChannel?: string; + requesterAccountId?: string; requesterDisplayKey: string; task: string; cleanup: "delete" | "keep"; @@ -59,6 +60,7 @@ function resumeSubagentRun(runId: string) { childRunId: entry.runId, requesterSessionKey: entry.requesterSessionKey, requesterChannel: entry.requesterChannel, + requesterAccountId: entry.requesterAccountId, requesterDisplayKey: entry.requesterDisplayKey, task: entry.task, timeoutMs: 30_000, @@ -199,6 +201,7 @@ function ensureListener() { childRunId: entry.runId, requesterSessionKey: entry.requesterSessionKey, requesterChannel: entry.requesterChannel, + requesterAccountId: entry.requesterAccountId, requesterDisplayKey: entry.requesterDisplayKey, task: entry.task, timeoutMs: 30_000, @@ -248,6 +251,7 @@ export function registerSubagentRun(params: { childSessionKey: string; requesterSessionKey: string; requesterChannel?: string; + requesterAccountId?: string; requesterDisplayKey: string; task: string; cleanup: "delete" | "keep"; @@ -264,6 +268,7 @@ export function registerSubagentRun(params: { childSessionKey: params.childSessionKey, requesterSessionKey: params.requesterSessionKey, requesterChannel: params.requesterChannel, + requesterAccountId: params.requesterAccountId, requesterDisplayKey: params.requesterDisplayKey, task: params.task, cleanup: params.cleanup, @@ -318,6 +323,7 @@ async function waitForSubagentCompletion(runId: string, waitTimeoutMs: number) { childRunId: entry.runId, requesterSessionKey: entry.requesterSessionKey, requesterChannel: entry.requesterChannel, + requesterAccountId: entry.requesterAccountId, requesterDisplayKey: entry.requesterDisplayKey, task: entry.task, timeoutMs: 30_000, diff --git a/src/agents/tools/sessions-spawn-tool.ts b/src/agents/tools/sessions-spawn-tool.ts index 6ae8b8dc3..3c17a8252 100644 --- a/src/agents/tools/sessions-spawn-tool.ts +++ b/src/agents/tools/sessions-spawn-tool.ts @@ -48,6 +48,7 @@ function normalizeModelSelection(value: unknown): string | undefined { export function createSessionsSpawnTool(opts?: { agentSessionKey?: string; agentChannel?: GatewayMessageChannel; + agentAccountId?: string; sandboxed?: boolean; }): AnyAgentTool { return { @@ -206,6 +207,7 @@ export function createSessionsSpawnTool(opts?: { childSessionKey, requesterSessionKey: requesterInternalKey, requesterChannel: opts?.agentChannel, + requesterAccountId: opts?.agentAccountId, requesterDisplayKey, task, cleanup, diff --git a/src/commands/agent/delivery.ts b/src/commands/agent/delivery.ts index 4c24a5b61..580e3222d 100644 --- a/src/commands/agent/delivery.ts +++ b/src/commands/agent/delivery.ts @@ -48,13 +48,19 @@ export async function deliverAgentCommandResult(params: { const targetMode: ChannelOutboundTargetMode = opts.deliveryTargetMode ?? (opts.to ? "explicit" : "implicit"); + const resolvedAccountId = + typeof opts.accountId === "string" && opts.accountId.trim() + ? opts.accountId.trim() + : targetMode === "implicit" + ? sessionEntry?.lastAccountId + : undefined; const resolvedTarget = deliver && isDeliveryChannelKnown && deliveryChannel ? resolveOutboundTarget({ channel: deliveryChannel, to: opts.to, cfg, - accountId: targetMode === "implicit" ? sessionEntry?.lastAccountId : undefined, + accountId: resolvedAccountId, mode: targetMode, }) : null; @@ -112,6 +118,7 @@ export async function deliverAgentCommandResult(params: { cfg, channel: deliveryChannel, to: deliveryTarget, + accountId: resolvedAccountId, payloads: deliveryPayloads, bestEffort: bestEffortDeliver, onError: (err) => logDeliveryError(err), diff --git a/src/commands/agent/types.ts b/src/commands/agent/types.ts index 3451f11a9..ff0637669 100644 --- a/src/commands/agent/types.ts +++ b/src/commands/agent/types.ts @@ -23,6 +23,8 @@ export type AgentCommandOpts = { /** Message channel context (webchat|voicewake|whatsapp|...). */ messageChannel?: string; channel?: string; // delivery channel (whatsapp|telegram|...) + /** Account ID for multi-account channel routing (e.g., WhatsApp account). */ + accountId?: string; deliveryTargetMode?: ChannelOutboundTargetMode; bestEffortDeliver?: boolean; abortSignal?: AbortSignal; diff --git a/src/gateway/protocol/schema/agent.ts b/src/gateway/protocol/schema/agent.ts index cf0b8be7f..0d594b3a9 100644 --- a/src/gateway/protocol/schema/agent.ts +++ b/src/gateway/protocol/schema/agent.ts @@ -52,6 +52,7 @@ export const AgentParamsSchema = Type.Object( deliver: Type.Optional(Type.Boolean()), attachments: Type.Optional(Type.Array(Type.Unknown())), channel: Type.Optional(Type.String()), + accountId: Type.Optional(Type.String()), timeout: Type.Optional(Type.Integer({ minimum: 0 })), lane: Type.Optional(Type.String()), extraSystemPrompt: Type.Optional(Type.String()), diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 3f15ad320..63452a584 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -60,6 +60,7 @@ export const agentHandlers: GatewayRequestHandlers = { content?: unknown; }>; channel?: string; + accountId?: string; lane?: string; extraSystemPrompt?: string; idempotencyKey: string; @@ -199,6 +200,10 @@ export const agentHandlers: GatewayRequestHandlers = { const lastChannel = sessionEntry?.lastChannel; const lastTo = typeof sessionEntry?.lastTo === "string" ? sessionEntry.lastTo.trim() : ""; + const resolvedAccountId = + typeof request.accountId === "string" && request.accountId.trim() + ? request.accountId.trim() + : sessionEntry?.lastAccountId; const wantsDelivery = request.deliver === true; @@ -235,7 +240,7 @@ export const agentHandlers: GatewayRequestHandlers = { const fallback = resolveOutboundTarget({ channel: resolvedChannel, cfg, - accountId: sessionEntry?.lastAccountId ?? undefined, + accountId: resolvedAccountId, mode: "implicit", }); if (fallback.ok) { @@ -269,6 +274,7 @@ export const agentHandlers: GatewayRequestHandlers = { deliver, deliveryTargetMode, channel: resolvedChannel, + accountId: resolvedAccountId, timeout: request.timeout?.toString(), bestEffortDeliver, messageChannel: resolvedChannel,