fix: thread accountId through subagent announce

Co-authored-by: Adam Holt <adam91holt@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-17 02:09:32 +00:00
parent 4ba6f6e8ee
commit d5332ae29a
9 changed files with 37 additions and 3 deletions

View File

@@ -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.

View File

@@ -108,6 +108,7 @@ export function createClawdbotTools(options?: {
createSessionsSpawnTool({
agentSessionKey: options?.agentSessionKey,
agentChannel: options?.agentChannel,
agentAccountId: options?.agentAccountId,
sandboxed: options?.sandboxed,
}),
createSessionStatusTool({

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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),

View File

@@ -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;

View File

@@ -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()),

View File

@@ -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,