From 39af696552ab8d38197adb0a5f6bc5780cc2248f Mon Sep 17 00:00:00 2001 From: Keren Date: Tue, 10 Mar 2026 00:28:47 +0100 Subject: [PATCH] fix(exec): hash aggregated output instead of content text content.text can diverge from details.aggregated in two ways: - node-host path: content.text uses short-circuit OR (stdout || stderr || errorText), dropping stderr/error when stdout is non-empty - gateway path: content.text prepends warnings and uses "(no output)" fallback Use details.aggregated which always contains the full combined output, falling back to content text when aggregated is unavailable. Co-Authored-By: Claude Opus 4.6 --- src/agents/tool-loop-detection.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/agents/tool-loop-detection.ts b/src/agents/tool-loop-detection.ts index b436a5ed45e..e0d7a32eafe 100644 --- a/src/agents/tool-loop-detection.ts +++ b/src/agents/tool-loop-detection.ts @@ -230,8 +230,10 @@ function hashToolOutcome( // // For "running" results the content text itself embeds volatile metadata // (session id, pid) so we omit it and hash only the status + tail output. - // For "completed" results the content text mirrors `aggregated` which is - // stable, so we include it. + // For "completed" results we use details.aggregated rather than content text + // because content.text may drop stderr/error (node-host uses short-circuit + // OR) or prepend warnings (gateway path), while aggregated always contains + // the full combined stdout+stderr+error output. if (toolName === "exec") { if (details.status === "running") { return digestStable({ @@ -242,7 +244,7 @@ function hashToolOutcome( return digestStable({ status: details.status, exitCode: details.exitCode ?? null, - text, + aggregated: details.aggregated ?? text, }); }