fix(agents): preserve distinct empty exec failures

This commit is contained in:
Peter Steinberger
2026-04-27 12:40:39 +01:00
parent 4f50921e0f
commit 9b2f10dcf8
2 changed files with 39 additions and 1 deletions

View File

@@ -560,6 +560,36 @@ describe("tool-loop-detection", () => {
}
});
it("keeps changing empty-output exec failures below the global no-progress breaker", () => {
const state = createState();
const params = { command: "openclaw flaky-helper" };
for (let index = 0; index < GLOBAL_CIRCUIT_BREAKER_THRESHOLD; index += 1) {
recordSuccessfulCall(
state,
"exec",
params,
{
content: [{ type: "text", text: `Runtime failed before spawn: attempt ${index}` }],
details: {
status: "failed",
exitCode: null,
durationMs: 100 + index,
aggregated: "",
},
},
index,
);
}
const loopResult = detectToolCallLoop(state, "exec", params, enabledLoopDetectionConfig);
expect(loopResult.stuck).toBe(true);
if (loopResult.stuck) {
expect(loopResult.level).toBe("warning");
expect(loopResult.detector).toBe("generic_repeat");
}
});
it("does not block repeated unknown-tool failures before the unknown-tool threshold", () => {
const state = createState();
const toolName = "exec";

View File

@@ -206,6 +206,14 @@ function stringField(value: unknown): string | null {
return typeof value === "string" ? value : null;
}
function nonEmptyStringField(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
function hashExecToolOutcome(details: Record<string, unknown>, text: string): string | undefined {
const status = stringField(details.status);
if (!status) {
@@ -224,7 +232,7 @@ function hashExecToolOutcome(details: Record<string, unknown>, text: string): st
status,
exitCode: typeof details.exitCode === "number" ? details.exitCode : null,
timedOut: details.timedOut === true,
output: stringField(details.aggregated) ?? text,
output: nonEmptyStringField(details.aggregated) ?? text,
});
}