mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 00:13:42 +00:00
fix(cron): treat transient tool error payloads as recoverable (openclaw#29527) thanks @Sid-Qin
Verified: - pnpm install --frozen-lockfile - pnpm check - pnpm test -- --run src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts Co-authored-by: Sid-Qin <201593046+Sid-Qin@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -197,6 +197,50 @@ describe("runCronIsolatedAgentTurn", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats transient error payloads as non-fatal when a later success payload exists", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
mockEmbeddedPayloads([
|
||||||
|
{
|
||||||
|
text: "⚠️ ✍️ Write: failed",
|
||||||
|
isError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Write completed successfully.",
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const { res } = await runCronTurn(home, {
|
||||||
|
jobPayload: DEFAULT_AGENT_TURN_PAYLOAD,
|
||||||
|
mockTexts: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe("ok");
|
||||||
|
expect(res.summary).toBe("Write completed successfully.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps error status when run-level error accompanies post-error text", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||||
|
payloads: [
|
||||||
|
{ text: "Model context overflow", isError: true },
|
||||||
|
{ text: "Partial assistant text before error" },
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
durationMs: 5,
|
||||||
|
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||||
|
error: { kind: "context_overflow", message: "exceeded context window" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { res } = await runCronTurn(home, {
|
||||||
|
jobPayload: DEFAULT_AGENT_TURN_PAYLOAD,
|
||||||
|
mockTexts: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe("error");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("passes resolved agentDir to runEmbeddedPiAgent", async () => {
|
it("passes resolved agentDir to runEmbeddedPiAgent", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const { res } = await runCronTurn(home, {
|
const { res } = await runCronTurn(home, {
|
||||||
|
|||||||
@@ -570,17 +570,29 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
Object.keys(deliveryPayload?.channelData ?? {}).length > 0;
|
Object.keys(deliveryPayload?.channelData ?? {}).length > 0;
|
||||||
const deliveryBestEffort = resolveCronDeliveryBestEffort(params.job);
|
const deliveryBestEffort = resolveCronDeliveryBestEffort(params.job);
|
||||||
const hasErrorPayload = payloads.some((payload) => payload?.isError === true);
|
const hasErrorPayload = payloads.some((payload) => payload?.isError === true);
|
||||||
|
const runLevelError = runResult.meta?.error;
|
||||||
|
const lastErrorPayloadIndex = payloads.findLastIndex((payload) => payload?.isError === true);
|
||||||
|
const hasSuccessfulPayloadAfterLastError =
|
||||||
|
!runLevelError &&
|
||||||
|
lastErrorPayloadIndex >= 0 &&
|
||||||
|
payloads
|
||||||
|
.slice(lastErrorPayloadIndex + 1)
|
||||||
|
.some((payload) => payload?.isError !== true && Boolean(payload?.text?.trim()));
|
||||||
|
// Tool wrappers can emit transient/false-positive error payloads before a valid final
|
||||||
|
// assistant payload. Only treat payload errors as recoverable when (a) the run itself
|
||||||
|
// did not report a model/context-level error and (b) a non-error payload follows.
|
||||||
|
const hasFatalErrorPayload = hasErrorPayload && !hasSuccessfulPayloadAfterLastError;
|
||||||
const lastErrorPayloadText = [...payloads]
|
const lastErrorPayloadText = [...payloads]
|
||||||
.toReversed()
|
.toReversed()
|
||||||
.find((payload) => payload?.isError === true && Boolean(payload?.text?.trim()))
|
.find((payload) => payload?.isError === true && Boolean(payload?.text?.trim()))
|
||||||
?.text?.trim();
|
?.text?.trim();
|
||||||
const embeddedRunError = hasErrorPayload
|
const embeddedRunError = hasFatalErrorPayload
|
||||||
? (lastErrorPayloadText ?? "cron isolated run returned an error payload")
|
? (lastErrorPayloadText ?? "cron isolated run returned an error payload")
|
||||||
: undefined;
|
: undefined;
|
||||||
const resolveRunOutcome = (params?: { delivered?: boolean; deliveryAttempted?: boolean }) =>
|
const resolveRunOutcome = (params?: { delivered?: boolean; deliveryAttempted?: boolean }) =>
|
||||||
withRunSession({
|
withRunSession({
|
||||||
status: hasErrorPayload ? "error" : "ok",
|
status: hasFatalErrorPayload ? "error" : "ok",
|
||||||
...(hasErrorPayload
|
...(hasFatalErrorPayload
|
||||||
? { error: embeddedRunError ?? "cron isolated run returned an error payload" }
|
? { error: embeddedRunError ?? "cron isolated run returned an error payload" }
|
||||||
: {}),
|
: {}),
|
||||||
summary,
|
summary,
|
||||||
@@ -637,7 +649,7 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
deliveryAttempted:
|
deliveryAttempted:
|
||||||
deliveryResult.result.deliveryAttempted ?? deliveryResult.deliveryAttempted,
|
deliveryResult.result.deliveryAttempted ?? deliveryResult.deliveryAttempted,
|
||||||
};
|
};
|
||||||
if (!hasErrorPayload || deliveryResult.result.status !== "ok") {
|
if (!hasFatalErrorPayload || deliveryResult.result.status !== "ok") {
|
||||||
return resultWithDeliveryMeta;
|
return resultWithDeliveryMeta;
|
||||||
}
|
}
|
||||||
return resolveRunOutcome({
|
return resolveRunOutcome({
|
||||||
|
|||||||
Reference in New Issue
Block a user