From 5a5c969ee54f83364a34db89c1e00d0d40422a7b Mon Sep 17 00:00:00 2001 From: 1alyx Date: Fri, 30 Jan 2026 00:34:17 -0500 Subject: [PATCH] fix: prevent sub-agent announces from bypassing queued user messages - Fix 2: Check followup queue depth before direct announce delivery - Fix 3: Drain followup queue in runReplyAgent finally block (prevents stuck messages on error paths) Co-authored-by: Zach Canepa --- src/agents/subagent-announce.ts | 34 +++++++++++++++++++++++++++- src/auto-reply/reply/agent-runner.ts | 11 ++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index 444726efc..3a0426097 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -9,7 +9,7 @@ import { resolveStorePath, } from "../config/sessions.js"; import { normalizeMainKey } from "../routing/session-key.js"; -import { resolveQueueSettings } from "../auto-reply/reply/queue.js"; +import { resolveQueueSettings, getFollowupQueueDepth } from "../auto-reply/reply/queue.js"; import { callGateway } from "../gateway/call.js"; import { defaultRuntime } from "../runtime.js"; import { @@ -413,6 +413,38 @@ export async function runSubagentAnnounceFlow(params: { return true; } + // Fix 2: If followup queue has pending user messages, enqueue the announce + // instead of sending directly. This prevents the announce from jumping + // ahead of queued user messages in the per-session lane. + { + const cfg = loadConfig(); + const canonicalKey = resolveRequesterStoreKey(cfg, params.requesterSessionKey); + const followupDepth = getFollowupQueueDepth(canonicalKey); + if (followupDepth > 0) { + const { entry } = loadRequesterSessionEntry(params.requesterSessionKey); + const queueSettings = resolveQueueSettings({ + cfg, + channel: entry?.channel ?? entry?.lastChannel, + sessionEntry: entry, + }); + const origin = resolveAnnounceOrigin(entry, requesterOrigin); + enqueueAnnounce({ + key: canonicalKey, + item: { + prompt: triggerMessage, + summaryLine: taskLabel, + enqueuedAt: Date.now(), + sessionKey: canonicalKey, + origin, + }, + settings: queueSettings, + send: sendAnnounce, + }); + didAnnounce = true; + return true; + } + } + // Send to main agent - it will respond in its own voice let directOrigin = requesterOrigin; if (!directOrigin) { diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index 227e6f17e..f03fd4027 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -34,7 +34,12 @@ import { appendUsageLine, formatResponseUsageLine } from "./agent-runner-utils.j import { createAudioAsVoiceBuffer, createBlockReplyPipeline } from "./block-reply-pipeline.js"; import { resolveBlockStreamingCoalescing } from "./block-streaming.js"; import { createFollowupRunner } from "./followup-runner.js"; -import { enqueueFollowupRun, type FollowupRun, type QueueSettings } from "./queue.js"; +import { + enqueueFollowupRun, + scheduleFollowupDrain, + type FollowupRun, + type QueueSettings, +} from "./queue.js"; import { createReplyToModeFilterForChannel, resolveReplyToMode } from "./reply-threading.js"; import { persistSessionUsageUpdate } from "./session-usage.js"; import { incrementCompactionCount } from "./session-updates.js"; @@ -510,5 +515,9 @@ export async function runReplyAgent(params: { } finally { blockReplyPipeline?.stop(); typing.markRunComplete(); + // Fix 3: Ensure queued followup messages are drained even on error paths. + // Without this, messages enqueued in FOLLOWUP_QUEUES can get stuck if an + // exception is thrown before any finalizeWithFollowup() call is reached. + scheduleFollowupDrain(queueKey, runFollowupTurn); } }