From 1c43c0e440ce343eda2cf281f6cf8bc366de7c67 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 28 Jul 2026 17:00:19 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=8C=20fix:=20Detach=20Label=20Abort=20?= =?UTF-8?q?Listeners=20Even=20Without=20Claims?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A segment with labels enabled can end without a single claim (text-only, or handoff batches, which skip labels); the early return in settleActivityLabels skipped the detach added for HITL listener accumulation. The detach now runs on both paths. --- api/server/controllers/agents/client.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 1efa2f5c9b..2be9dc1b98 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -645,8 +645,19 @@ class AgentClient extends BaseClient { * timeout the label scope is closed and its abort controller fired, so a * straggler cannot mutate the saved response or emit into a dead job. */ async settleActivityLabels(timeoutMs = 3000) { + /** Detached even when nothing settled: the wiring attaches its abort + * listener at BUILD time, and a segment can end without a single claim + * (text-only, or handoff batches, which skip labels) — the early + * return below would otherwise leave that listener accumulating across + * HITL approval cycles on the shared job signal. Idempotent. */ + const detachScopeListeners = () => { + for (const scope of this.activityLabelScopes ?? []) { + scope.detach?.(); + } + }; const pending = this.pendingActivityLabelFills; if (!pending || pending.length === 0) { + detachScopeListeners(); return; } this.pendingActivityLabelFills = []; @@ -658,12 +669,7 @@ class AgentClient extends BaseClient { scope.abort.abort(); } }); - /** Settled either way — the abort listener has no remaining work, and - * leaving it attached across HITL approval cycles accumulates dead - * closures on the shared job signal. Idempotent on double settle. */ - for (const scope of this.activityLabelScopes ?? []) { - scope.detach?.(); - } + detachScopeListeners(); } /**