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. - Health: add per-agent session summaries and account-level health details, and allow selective probes. (#1047) — thanks @gumadeiras.
### Fixes ### 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. - 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: reset `compactionCount` on `/new` and `/reset`, and preserve `sessions.json` file mode (0600).
- Sessions: repair orphaned user turns before embedded prompts. - Sessions: repair orphaned user turns before embedded prompts.

View File

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

View File

@@ -306,6 +306,9 @@ async function sendAnnounce(item: AnnounceQueueItem) {
params: { params: {
sessionKey: item.sessionKey, sessionKey: item.sessionKey,
message: item.prompt, message: item.prompt,
channel: item.originatingChannel,
accountId: item.originatingAccountId,
to: item.originatingTo,
deliver: true, deliver: true,
idempotencyKey: crypto.randomUUID(), idempotencyKey: crypto.randomUUID(),
}, },
@@ -348,6 +351,7 @@ async function maybeQueueSubagentAnnounce(params: {
requesterSessionKey: string; requesterSessionKey: string;
triggerMessage: string; triggerMessage: string;
summaryLine?: string; summaryLine?: string;
requesterAccountId?: string;
}): Promise<"steered" | "queued" | "none"> { }): Promise<"steered" | "queued" | "none"> {
const { cfg, entry } = loadRequesterSessionEntry(params.requesterSessionKey); const { cfg, entry } = loadRequesterSessionEntry(params.requesterSessionKey);
const canonicalKey = resolveRequesterStoreKey(cfg, params.requesterSessionKey); const canonicalKey = resolveRequesterStoreKey(cfg, params.requesterSessionKey);
@@ -382,7 +386,7 @@ async function maybeQueueSubagentAnnounce(params: {
sessionKey: canonicalKey, sessionKey: canonicalKey,
originatingChannel: entry?.lastChannel, originatingChannel: entry?.lastChannel,
originatingTo: entry?.lastTo, originatingTo: entry?.lastTo,
originatingAccountId: entry?.lastAccountId, originatingAccountId: entry?.lastAccountId ?? params.requesterAccountId,
}, },
queueSettings, queueSettings,
); );
@@ -505,6 +509,7 @@ export async function runSubagentAnnounceFlow(params: {
childRunId: string; childRunId: string;
requesterSessionKey: string; requesterSessionKey: string;
requesterChannel?: string; requesterChannel?: string;
requesterAccountId?: string;
requesterDisplayKey: string; requesterDisplayKey: string;
task: string; task: string;
timeoutMs: number; timeoutMs: number;
@@ -600,6 +605,7 @@ export async function runSubagentAnnounceFlow(params: {
requesterSessionKey: params.requesterSessionKey, requesterSessionKey: params.requesterSessionKey,
triggerMessage, triggerMessage,
summaryLine: taskLabel, summaryLine: taskLabel,
requesterAccountId: params.requesterAccountId,
}); });
if (queued === "steered") { if (queued === "steered") {
didAnnounce = true; didAnnounce = true;
@@ -617,6 +623,8 @@ export async function runSubagentAnnounceFlow(params: {
sessionKey: params.requesterSessionKey, sessionKey: params.requesterSessionKey,
message: triggerMessage, message: triggerMessage,
deliver: true, deliver: true,
channel: params.requesterChannel,
accountId: params.requesterAccountId,
idempotencyKey: crypto.randomUUID(), idempotencyKey: crypto.randomUUID(),
}, },
expectFinal: true, expectFinal: true,

View File

@@ -13,6 +13,7 @@ export type SubagentRunRecord = {
childSessionKey: string; childSessionKey: string;
requesterSessionKey: string; requesterSessionKey: string;
requesterChannel?: string; requesterChannel?: string;
requesterAccountId?: string;
requesterDisplayKey: string; requesterDisplayKey: string;
task: string; task: string;
cleanup: "delete" | "keep"; cleanup: "delete" | "keep";
@@ -59,6 +60,7 @@ function resumeSubagentRun(runId: string) {
childRunId: entry.runId, childRunId: entry.runId,
requesterSessionKey: entry.requesterSessionKey, requesterSessionKey: entry.requesterSessionKey,
requesterChannel: entry.requesterChannel, requesterChannel: entry.requesterChannel,
requesterAccountId: entry.requesterAccountId,
requesterDisplayKey: entry.requesterDisplayKey, requesterDisplayKey: entry.requesterDisplayKey,
task: entry.task, task: entry.task,
timeoutMs: 30_000, timeoutMs: 30_000,
@@ -199,6 +201,7 @@ function ensureListener() {
childRunId: entry.runId, childRunId: entry.runId,
requesterSessionKey: entry.requesterSessionKey, requesterSessionKey: entry.requesterSessionKey,
requesterChannel: entry.requesterChannel, requesterChannel: entry.requesterChannel,
requesterAccountId: entry.requesterAccountId,
requesterDisplayKey: entry.requesterDisplayKey, requesterDisplayKey: entry.requesterDisplayKey,
task: entry.task, task: entry.task,
timeoutMs: 30_000, timeoutMs: 30_000,
@@ -248,6 +251,7 @@ export function registerSubagentRun(params: {
childSessionKey: string; childSessionKey: string;
requesterSessionKey: string; requesterSessionKey: string;
requesterChannel?: string; requesterChannel?: string;
requesterAccountId?: string;
requesterDisplayKey: string; requesterDisplayKey: string;
task: string; task: string;
cleanup: "delete" | "keep"; cleanup: "delete" | "keep";
@@ -264,6 +268,7 @@ export function registerSubagentRun(params: {
childSessionKey: params.childSessionKey, childSessionKey: params.childSessionKey,
requesterSessionKey: params.requesterSessionKey, requesterSessionKey: params.requesterSessionKey,
requesterChannel: params.requesterChannel, requesterChannel: params.requesterChannel,
requesterAccountId: params.requesterAccountId,
requesterDisplayKey: params.requesterDisplayKey, requesterDisplayKey: params.requesterDisplayKey,
task: params.task, task: params.task,
cleanup: params.cleanup, cleanup: params.cleanup,
@@ -318,6 +323,7 @@ async function waitForSubagentCompletion(runId: string, waitTimeoutMs: number) {
childRunId: entry.runId, childRunId: entry.runId,
requesterSessionKey: entry.requesterSessionKey, requesterSessionKey: entry.requesterSessionKey,
requesterChannel: entry.requesterChannel, requesterChannel: entry.requesterChannel,
requesterAccountId: entry.requesterAccountId,
requesterDisplayKey: entry.requesterDisplayKey, requesterDisplayKey: entry.requesterDisplayKey,
task: entry.task, task: entry.task,
timeoutMs: 30_000, timeoutMs: 30_000,

View File

@@ -48,6 +48,7 @@ function normalizeModelSelection(value: unknown): string | undefined {
export function createSessionsSpawnTool(opts?: { export function createSessionsSpawnTool(opts?: {
agentSessionKey?: string; agentSessionKey?: string;
agentChannel?: GatewayMessageChannel; agentChannel?: GatewayMessageChannel;
agentAccountId?: string;
sandboxed?: boolean; sandboxed?: boolean;
}): AnyAgentTool { }): AnyAgentTool {
return { return {
@@ -206,6 +207,7 @@ export function createSessionsSpawnTool(opts?: {
childSessionKey, childSessionKey,
requesterSessionKey: requesterInternalKey, requesterSessionKey: requesterInternalKey,
requesterChannel: opts?.agentChannel, requesterChannel: opts?.agentChannel,
requesterAccountId: opts?.agentAccountId,
requesterDisplayKey, requesterDisplayKey,
task, task,
cleanup, cleanup,

View File

@@ -48,13 +48,19 @@ export async function deliverAgentCommandResult(params: {
const targetMode: ChannelOutboundTargetMode = const targetMode: ChannelOutboundTargetMode =
opts.deliveryTargetMode ?? (opts.to ? "explicit" : "implicit"); 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 = const resolvedTarget =
deliver && isDeliveryChannelKnown && deliveryChannel deliver && isDeliveryChannelKnown && deliveryChannel
? resolveOutboundTarget({ ? resolveOutboundTarget({
channel: deliveryChannel, channel: deliveryChannel,
to: opts.to, to: opts.to,
cfg, cfg,
accountId: targetMode === "implicit" ? sessionEntry?.lastAccountId : undefined, accountId: resolvedAccountId,
mode: targetMode, mode: targetMode,
}) })
: null; : null;
@@ -112,6 +118,7 @@ export async function deliverAgentCommandResult(params: {
cfg, cfg,
channel: deliveryChannel, channel: deliveryChannel,
to: deliveryTarget, to: deliveryTarget,
accountId: resolvedAccountId,
payloads: deliveryPayloads, payloads: deliveryPayloads,
bestEffort: bestEffortDeliver, bestEffort: bestEffortDeliver,
onError: (err) => logDeliveryError(err), onError: (err) => logDeliveryError(err),

View File

@@ -23,6 +23,8 @@ export type AgentCommandOpts = {
/** Message channel context (webchat|voicewake|whatsapp|...). */ /** Message channel context (webchat|voicewake|whatsapp|...). */
messageChannel?: string; messageChannel?: string;
channel?: string; // delivery channel (whatsapp|telegram|...) channel?: string; // delivery channel (whatsapp|telegram|...)
/** Account ID for multi-account channel routing (e.g., WhatsApp account). */
accountId?: string;
deliveryTargetMode?: ChannelOutboundTargetMode; deliveryTargetMode?: ChannelOutboundTargetMode;
bestEffortDeliver?: boolean; bestEffortDeliver?: boolean;
abortSignal?: AbortSignal; abortSignal?: AbortSignal;

View File

@@ -52,6 +52,7 @@ export const AgentParamsSchema = Type.Object(
deliver: Type.Optional(Type.Boolean()), deliver: Type.Optional(Type.Boolean()),
attachments: Type.Optional(Type.Array(Type.Unknown())), attachments: Type.Optional(Type.Array(Type.Unknown())),
channel: Type.Optional(Type.String()), channel: Type.Optional(Type.String()),
accountId: Type.Optional(Type.String()),
timeout: Type.Optional(Type.Integer({ minimum: 0 })), timeout: Type.Optional(Type.Integer({ minimum: 0 })),
lane: Type.Optional(Type.String()), lane: Type.Optional(Type.String()),
extraSystemPrompt: Type.Optional(Type.String()), extraSystemPrompt: Type.Optional(Type.String()),

View File

@@ -60,6 +60,7 @@ export const agentHandlers: GatewayRequestHandlers = {
content?: unknown; content?: unknown;
}>; }>;
channel?: string; channel?: string;
accountId?: string;
lane?: string; lane?: string;
extraSystemPrompt?: string; extraSystemPrompt?: string;
idempotencyKey: string; idempotencyKey: string;
@@ -199,6 +200,10 @@ export const agentHandlers: GatewayRequestHandlers = {
const lastChannel = sessionEntry?.lastChannel; const lastChannel = sessionEntry?.lastChannel;
const lastTo = typeof sessionEntry?.lastTo === "string" ? sessionEntry.lastTo.trim() : ""; 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; const wantsDelivery = request.deliver === true;
@@ -235,7 +240,7 @@ export const agentHandlers: GatewayRequestHandlers = {
const fallback = resolveOutboundTarget({ const fallback = resolveOutboundTarget({
channel: resolvedChannel, channel: resolvedChannel,
cfg, cfg,
accountId: sessionEntry?.lastAccountId ?? undefined, accountId: resolvedAccountId,
mode: "implicit", mode: "implicit",
}); });
if (fallback.ok) { if (fallback.ok) {
@@ -269,6 +274,7 @@ export const agentHandlers: GatewayRequestHandlers = {
deliver, deliver,
deliveryTargetMode, deliveryTargetMode,
channel: resolvedChannel, channel: resolvedChannel,
accountId: resolvedAccountId,
timeout: request.timeout?.toString(), timeout: request.timeout?.toString(),
bestEffortDeliver, bestEffortDeliver,
messageChannel: resolvedChannel, messageChannel: resolvedChannel,