Skip to content

Commit c8a953a

Browse files
committed
fix: keep cron final output over tool warnings
1 parent ac69776 commit c8a953a

5 files changed

Lines changed: 83 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
1818

1919
- Mac app: keep local packaging signed with a stable app identity for permission testing and fix Control UI production builds under current Vite/Highlight.js exports.
2020
- macOS app: update the embedded Peekaboo bridge to 3.2.1 so OpenClaw-hosted UI automation works with current Peekaboo CLI capture flows.
21+
- Cron: deliver preferred final assistant output for successful scheduled runs when trailing plain tool warnings remain in diagnostics instead of marking the run failed.
2122
- fix(mattermost): fail closed on missing channel type [AI]. (#84091) Thanks @pgondhi987.
2223
- Recheck rebuilt system.run argv [AI]. (#84090) Thanks @pgondhi987.
2324
- CLI/cron: bound `openclaw cron show` job lookup pagination so non-advancing or unbounded `cron.list` responses fail instead of hanging the command. Fixes #83856. (#83989)

src/cron/isolated-agent.helpers.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,27 @@ describe("resolveCronPayloadOutcome", () => {
2828
expect(result.summary).toContain("Exec failed");
2929
});
3030

31+
it("lets preferred final assistant text recover a plain tool warning", () => {
32+
const result = resolveCronPayloadOutcome({
33+
payloads: [
34+
{
35+
text: "⚠️ 🛠️ jq -s '{total:length}' (agent) failed",
36+
isError: true,
37+
},
38+
],
39+
finalAssistantVisibleText: "**Clawsweeper 6h report**\nClosed: 34 total",
40+
preferFinalAssistantVisibleText: true,
41+
});
42+
43+
expect(result.hasFatalErrorPayload).toBe(false);
44+
expect(result.embeddedRunError).toBeUndefined();
45+
expect(result.summary).toBe("**Clawsweeper 6h report**\nClosed: 34 total");
46+
expect(result.outputText).toBe("**Clawsweeper 6h report**\nClosed: 34 total");
47+
expect(result.deliveryPayloads).toEqual([
48+
{ text: "**Clawsweeper 6h report**\nClosed: 34 total" },
49+
]);
50+
});
51+
3152
it("treats transient error payloads as non-fatal when a later success exists", () => {
3253
const result = resolveCronPayloadOutcome({
3354
payloads: [

src/cron/isolated-agent/helpers.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ function isCronMessagePresentationWarning(text: string | undefined): boolean {
202202
);
203203
}
204204

205+
function isCronToolWarning(text: string | undefined): boolean {
206+
return normalizeOptionalString(text)?.startsWith("⚠️ 🛠️ ") === true;
207+
}
208+
205209
function isNonTerminalToolErrorWarning(payload: object | undefined): boolean {
206210
return Boolean(payload && getReplyPayloadMetadata(payload)?.nonTerminalToolErrorWarning);
207211
}
@@ -236,6 +240,7 @@ export function resolveCronPayloadOutcome(params: {
236240
.toReversed()
237241
.find((payload) => payload?.isError === true && Boolean(payload?.text?.trim()))
238242
?.text?.trim();
243+
const errorPayloads = params.payloads.filter((payload) => payload?.isError === true);
239244
const normalizedFinalAssistantVisibleText = normalizeOptionalString(
240245
params.finalAssistantVisibleText,
241246
);
@@ -264,14 +269,23 @@ export function resolveCronPayloadOutcome(params: {
264269
lastErrorPayloadIndex >= 0 &&
265270
isCronMessagePresentationWarning(lastErrorPayloadText) &&
266271
(normalizedFinalAssistantVisibleText !== undefined || hasSuccessfulPayloadBeforeLastError);
272+
const hasStructuredDeliveryPayloads = selectedDeliveryPayloads.some((payload) =>
273+
payloadHasStructuredDeliveryContent(payload),
274+
);
275+
const hasRecoveredToolWarning =
276+
!params.runLevelError &&
277+
params.failureSignal?.fatalForCron !== true &&
278+
params.preferFinalAssistantVisibleText === true &&
279+
normalizedFinalAssistantVisibleText !== undefined &&
280+
!hasStructuredDeliveryPayloads &&
281+
errorPayloads.length > 0 &&
282+
errorPayloads.every((payload) => isCronToolWarning(payload?.text));
267283
const hasFatalStructuredErrorPayload =
268284
hasErrorPayload &&
269285
!hasSuccessfulPayloadAfterLastError &&
270286
!hasPendingPresentationWarning &&
271-
!hasNonTerminalToolErrorWarning;
272-
const hasStructuredDeliveryPayloads = selectedDeliveryPayloads.some((payload) =>
273-
payloadHasStructuredDeliveryContent(payload),
274-
);
287+
!hasNonTerminalToolErrorWarning &&
288+
!hasRecoveredToolWarning;
275289
// Keep structured/media announce payloads intact. Only collapse purely textual
276290
// cron announce output to the final assistant-visible answer.
277291
const shouldUseFinalAssistantVisibleText =

src/cron/run-diagnostics.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,45 @@ describe("cron run diagnostics", () => {
179179
expect(diagnostics?.summary).toBe("⚠️ Exec failed");
180180
});
181181

182+
it("downgrades recovered tool errors for successful runs", () => {
183+
const diagnostics = createCronRunDiagnosticsFromAgentResult(
184+
{
185+
payloads: [
186+
{
187+
toolName: "exec",
188+
text: "⚠️ 🛠️ jq -s '{total:length}' (agent) failed",
189+
isError: true,
190+
details: {
191+
status: "failed",
192+
exitCode: 1,
193+
aggregated: "jq syntax error",
194+
},
195+
},
196+
],
197+
},
198+
{ finalStatus: "ok", nowMs: () => 800 },
199+
);
200+
201+
expect(diagnostics?.entries).toEqual([
202+
{
203+
ts: 800,
204+
source: "exec",
205+
severity: "warn",
206+
message: "jq syntax error",
207+
toolName: "exec",
208+
exitCode: 1,
209+
},
210+
{
211+
ts: 800,
212+
source: "tool",
213+
severity: "warn",
214+
message: "⚠️ 🛠️ jq -s '{total:length}' (agent) failed",
215+
toolName: "exec",
216+
},
217+
]);
218+
expect(diagnostics?.summary).toBe("⚠️ 🛠️ jq -s '{total:length}' (agent) failed");
219+
});
220+
182221
it("captures silent failed exec details with a fallback message", () => {
183222
const diagnostics = createCronRunDiagnosticsFromAgentResult(
184223
{

src/cron/run-diagnostics.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ export function createCronRunDiagnosticsFromExecDetails(
224224
opts?: {
225225
nowMs?: () => number;
226226
toolName?: string;
227+
finalStatus?: "ok" | "error" | "skipped";
227228
},
228229
): CronRunDiagnostics | undefined {
229230
if (!isRecord(details)) {
@@ -248,7 +249,7 @@ export function createCronRunDiagnosticsFromExecDetails(
248249
{
249250
ts: opts?.nowMs?.() ?? Date.now(),
250251
source: "exec",
251-
severity: status === "failed" ? "error" : "warn",
252+
severity: opts?.finalStatus === "ok" ? "warn" : status === "failed" ? "error" : "warn",
252253
message,
253254
toolName: opts?.toolName,
254255
exitCode,
@@ -270,6 +271,7 @@ export function createCronRunDiagnosticsFromToolPayload(
270271
const detailsDiagnostics = createCronRunDiagnosticsFromExecDetails(payload.details, {
271272
nowMs: opts?.nowMs,
272273
toolName,
274+
finalStatus: opts?.finalStatus,
273275
});
274276
const isError = payload.isError === true;
275277
const text = typeof payload.text === "string" ? payload.text : undefined;
@@ -279,7 +281,7 @@ export function createCronRunDiagnosticsFromToolPayload(
279281
const textDiagnostics =
280282
isError && text
281283
? createCronRunDiagnosticsFromError("tool", text, {
282-
severity: isNonTerminalToolWarning ? "warn" : "error",
284+
severity: isNonTerminalToolWarning || opts?.finalStatus === "ok" ? "warn" : "error",
283285
nowMs: opts?.nowMs,
284286
toolName,
285287
})

0 commit comments

Comments
 (0)