mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-21 18:08:05 -06:00
fix(agents): stale sub-agent failure warning shown after successful spawn retry (#126218)
* fix(agents): clear sub-agent failure warning after successful spawn retry A failed sessions_spawn followed by a successful retry in the same run kept appending the durable "Sub-agent failed" warning to channel replies. The recovery seam (lastToolRecovery) already existed, but sessions_spawn has no stable-target arg key, so its recovery fingerprint fell back to display meta built from label/task/model. Retries adjust those args (drop a rejected cwd, reword the task), the fingerprints never matched, and recordSuccess could not clear the failure. Collapse the sessions_spawn recovery identity to tool level: any later successful spawn in the same run is recovery proof. A lone failed spawn still warns, and a failure after recovery still invalidates the receipt. Observed 2026-08-19 on team.openclaw.ai: Roboclaw's first nested spawn returned forbidden (visible-session cwd outside workspace), the retry without cwd succeeded, the reply said "Started Investigate…", and Discord still showed "⚠️ 🧑🔧 Sub-agent failed". * ci: register run-attempt-tools test in the attempt-light lane #126189 added extensions/codex/src/app-server/run-attempt-tools.test.ts without assigning it to a full-suite lane, so the Vitest ownership audit (test/vitest-projects-config.test.ts) fails on main and every PR head. Register it in the codex app-server attempt-light shard next to its run-attempt siblings.
This commit is contained in:
committed by
GitHub
parent
a4b265aa9b
commit
617bc7ebdd
@@ -50,6 +50,24 @@ describe("tool mutation helpers", () => {
|
||||
expect(readFingerprint).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps sessions_spawn recovery identity at tool level across adjusted retries", () => {
|
||||
// Spawn retries adjust args (drop a rejected cwd, reword the task); a
|
||||
// per-args identity would never let the successful retry clear the failure.
|
||||
const failed = buildToolMutationState(
|
||||
"sessions_spawn",
|
||||
{ task: "Investigate", label: "Investigate", cwd: "/outside" },
|
||||
"label Investigate, task Investigate",
|
||||
);
|
||||
const retried = buildToolMutationState(
|
||||
"sessions_spawn",
|
||||
{ task: "Investigate in repo scope" },
|
||||
"Investigate in repo scope",
|
||||
);
|
||||
expect(failed.mutatingAction).toBe(true);
|
||||
expect(failed.actionFingerprint).toBe("tool=sessions_spawn");
|
||||
expect(retried.actionFingerprint).toBe(failed.actionFingerprint);
|
||||
});
|
||||
|
||||
it("binds reordered exact arguments to one owner but separates changed facts and owners", () => {
|
||||
const ownerKey = '["memory-lancedb","memory_store"]';
|
||||
const metric = buildToolMutationState(
|
||||
|
||||
@@ -439,6 +439,13 @@ function buildToolActionFingerprint(
|
||||
return undefined;
|
||||
}
|
||||
const normalizedTool = normalizeLowercaseStringOrEmpty(toolName);
|
||||
// sessions_spawn has no stable target: retries adjust args (drop a rejected
|
||||
// cwd, reword the task), so arg/meta identity never matches and a recovered
|
||||
// failure keeps warning "Sub-agent failed". A later successful spawn in the
|
||||
// same run is the recovery proof; keep the identity at tool level.
|
||||
if (normalizedTool === "sessions_spawn") {
|
||||
return `tool=${normalizedTool}`;
|
||||
}
|
||||
const record = asRecord(args);
|
||||
const action = normalizeActionName(record?.action);
|
||||
const parts = [`tool=${normalizedTool}`];
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
resetAdjustedParamsByToolCallIdForTests,
|
||||
} from "./agent-tools.before-tool-call.state.js";
|
||||
import { buildPayloads } from "./embedded-agent-runner/run/payloads.test-helpers.js";
|
||||
import { inferToolMetaFromArgsCore } from "./tool-display.js";
|
||||
import { createToolTerminalObserver } from "./tool-terminal-outcome.js";
|
||||
|
||||
describe("tool terminal outcome observer", () => {
|
||||
@@ -167,6 +168,65 @@ describe("tool terminal outcome observer", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("clears a failed sessions_spawn once a retry with adjusted arguments succeeds", () => {
|
||||
const observe = createToolTerminalObserver("run-spawn-retry");
|
||||
const failedArgs = {
|
||||
task: "Investigate the flaky gateway test",
|
||||
label: "Investigate",
|
||||
cwd: "/outside/workspace",
|
||||
};
|
||||
// The retry the model actually issues: drops the rejected cwd and rewords the task.
|
||||
const retryArgs = { task: "Investigate the flaky gateway test in repo scope" };
|
||||
|
||||
observe({
|
||||
toolName: "sessions_spawn",
|
||||
arguments: failedArgs,
|
||||
meta: inferToolMetaFromArgsCore("sessions_spawn", failedArgs),
|
||||
outcome: "failure",
|
||||
failure: { error: "cwd is outside the workspace" },
|
||||
});
|
||||
const afterRetry = observe({
|
||||
toolName: "sessions_spawn",
|
||||
arguments: retryArgs,
|
||||
meta: inferToolMetaFromArgsCore("sessions_spawn", retryArgs),
|
||||
outcome: "success",
|
||||
});
|
||||
|
||||
expect(afterRetry.lastToolError).toBeUndefined();
|
||||
expect(afterRetry.lastToolRecovery).toEqual({ toolName: "sessions_spawn" });
|
||||
|
||||
const payloads = buildPayloads({
|
||||
assistantTexts: ["Started Investigate in a new session."],
|
||||
lastToolError: afterRetry.lastToolError,
|
||||
lastToolRecovery: afterRetry.lastToolRecovery,
|
||||
});
|
||||
expect(payloads.map((payload) => payload.text)).toEqual([
|
||||
"Started Investigate in a new session.",
|
||||
"✅ 🧑🔧 Sub-agent succeeded after retry.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the sessions_spawn failure warning when no later spawn succeeds", () => {
|
||||
const observe = createToolTerminalObserver("run-spawn-failed");
|
||||
const failedArgs = { task: "Investigate the flaky gateway test", label: "Investigate" };
|
||||
|
||||
const terminal = observe({
|
||||
toolName: "sessions_spawn",
|
||||
arguments: failedArgs,
|
||||
meta: inferToolMetaFromArgsCore("sessions_spawn", failedArgs),
|
||||
outcome: "failure",
|
||||
failure: { error: "cwd is outside the workspace" },
|
||||
});
|
||||
|
||||
const payloads = buildPayloads({
|
||||
assistantTexts: ["Started Investigate in a new session."],
|
||||
lastToolError: terminal.lastToolError,
|
||||
lastToolRecovery: terminal.lastToolRecovery,
|
||||
});
|
||||
expect(payloads.at(-1)?.isError).toBe(true);
|
||||
expect(payloads.at(-1)?.text).toContain("Sub-agent failed");
|
||||
});
|
||||
|
||||
it("preserves durable memory recall side-effect evidence", () => {
|
||||
const observe = createToolTerminalObserver("run-memory");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user