mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:20:43 +00:00
fix(cron): ignore delivered presentation warnings
This commit is contained in:
@@ -89,6 +89,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Google Meet: preserve Gemini Live function names when replying to realtime tool calls so Google SDK validation accepts the `FunctionResponse` payload. Fixes #72425. (#72426) Thanks @BsnizND.
|
||||
- Matrix/E2EE: stabilize recovery and broken-device QA flows while avoiding Matrix device-cleanup sync races that could leave shutdown-time crypto work running. Thanks @gumadeiras.
|
||||
- Cron: apply `cron.maxConcurrentRuns` to a dedicated `cron-nested` isolated agent-turn lane as well as cron dispatch, so parallel cron jobs no longer serialize on inner LLM execution while non-cron nested flows keep their existing lane behavior. Fixes #72707. Thanks @kagura-agent.
|
||||
- Cron: report isolated runs as successful when fallback delivery already delivered the reply, and ignore no-delivery Message/Canvas presentation warnings without hiding real execution failures. Fixes #72732 and #50170; follow-up to #54188. Thanks @zNatix, @pixeldyn, and @ChickenEggRoll.
|
||||
- Cron: treat isolated run-level agent failures as job errors even when no reply payload is produced, synthesizing a safe error payload so model/provider failures increment error counters and trigger failure notifications instead of clearing as successful. Fixes #43604; carries forward #43631. Thanks @SPFAdvisors.
|
||||
- Cron: preserve exact `NO_REPLY` tool results from isolated jobs with empty final assistant turns as quiet successes instead of surfacing incomplete-turn errors. Fixes #68452; carries forward #68453. Thanks @anyech.
|
||||
- Cron: resolve failure alerts and failure-destination announcements against `session:<id>` targets before falling back to the creator session, so jobs created from group chats can notify the targeted direct session without cross-account routing errors. Refs #62777; carries forward #68535. Thanks @slideshow-dingo and @likewen-tech.
|
||||
|
||||
@@ -65,6 +65,56 @@ describe("resolveCronPayloadOutcome", () => {
|
||||
expect(result.summary).toBe("Write completed successfully.");
|
||||
});
|
||||
|
||||
it("treats trailing message delivery warnings as non-fatal when final assistant text exists", () => {
|
||||
const result = resolveCronPayloadOutcome({
|
||||
payloads: [{ text: "Draft output" }, { text: "⚠️ ✉️ Message failed", isError: true }],
|
||||
finalAssistantVisibleText: "Final cron report",
|
||||
preferFinalAssistantVisibleText: true,
|
||||
});
|
||||
|
||||
expect(result.hasFatalErrorPayload).toBe(false);
|
||||
expect(result.embeddedRunError).toBeUndefined();
|
||||
expect(result.summary).toBe("Final cron report");
|
||||
expect(result.outputText).toBe("Final cron report");
|
||||
expect(result.deliveryPayloads).toEqual([{ text: "Final cron report" }]);
|
||||
});
|
||||
|
||||
it("treats trailing canvas warnings as non-fatal when earlier assistant output exists", () => {
|
||||
const result = resolveCronPayloadOutcome({
|
||||
payloads: [{ text: "Saved report to disk." }, { text: "⚠️ 🖼️ Canvas failed", isError: true }],
|
||||
});
|
||||
|
||||
expect(result.hasFatalErrorPayload).toBe(false);
|
||||
expect(result.summary).toBe("Saved report to disk.");
|
||||
expect(result.outputText).toBe("Saved report to disk.");
|
||||
expect(result.deliveryPayloads).toEqual([{ text: "Saved report to disk." }]);
|
||||
});
|
||||
|
||||
it("keeps standalone presentation warnings fatal when there is no cron output", () => {
|
||||
const result = resolveCronPayloadOutcome({
|
||||
payloads: [{ text: "⚠️ ✉️ Message failed", isError: true }],
|
||||
});
|
||||
|
||||
expect(result.hasFatalErrorPayload).toBe(true);
|
||||
expect(result.embeddedRunError).toBe("⚠️ ✉️ Message failed");
|
||||
expect(result.deliveryPayloads).toEqual([{ text: "⚠️ ✉️ Message failed", isError: true }]);
|
||||
});
|
||||
|
||||
it("keeps real trailing errors fatal even when earlier assistant output exists", () => {
|
||||
const result = resolveCronPayloadOutcome({
|
||||
payloads: [{ text: "Partial result" }, { text: "model provider unreachable", isError: true }],
|
||||
finalAssistantVisibleText: "Partial result",
|
||||
preferFinalAssistantVisibleText: true,
|
||||
});
|
||||
|
||||
expect(result.hasFatalErrorPayload).toBe(true);
|
||||
expect(result.embeddedRunError).toBe("model provider unreachable");
|
||||
expect(result.outputText).toBe("model provider unreachable");
|
||||
expect(result.deliveryPayloads).toEqual([
|
||||
{ text: "model provider unreachable", isError: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps error payloads fatal when the run also reported a run-level error", () => {
|
||||
const result = resolveCronPayloadOutcome({
|
||||
payloads: [
|
||||
|
||||
@@ -241,6 +241,16 @@ export function resolveHeartbeatAckMaxChars(agentCfg?: { heartbeat?: { ackMaxCha
|
||||
return Math.max(0, raw);
|
||||
}
|
||||
|
||||
function isNonFatalCronPresentationWarning(text: string | undefined): boolean {
|
||||
const normalized = normalizeOptionalString(text)?.toLowerCase();
|
||||
return (
|
||||
normalized === "⚠️ ✉️ message failed" ||
|
||||
normalized?.startsWith("⚠️ ✉️ message failed:") === true ||
|
||||
normalized === "⚠️ 🖼️ canvas failed" ||
|
||||
normalized?.startsWith("⚠️ 🖼️ canvas failed:") === true
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveCronPayloadOutcome(params: {
|
||||
payloads: DeliveryPayload[];
|
||||
runLevelError?: unknown;
|
||||
@@ -259,16 +269,33 @@ export function resolveCronPayloadOutcome(params: {
|
||||
const lastErrorPayloadIndex = params.payloads.findLastIndex(
|
||||
(payload) => payload?.isError === true,
|
||||
);
|
||||
const lastErrorPayloadText = [...params.payloads]
|
||||
.toReversed()
|
||||
.find((payload) => payload?.isError === true && Boolean(payload?.text?.trim()))
|
||||
?.text?.trim();
|
||||
const normalizedFinalAssistantVisibleText = normalizeOptionalString(
|
||||
params.finalAssistantVisibleText,
|
||||
);
|
||||
const hasSuccessfulPayloadAfterLastError =
|
||||
!params.runLevelError &&
|
||||
lastErrorPayloadIndex >= 0 &&
|
||||
params.payloads
|
||||
.slice(lastErrorPayloadIndex + 1)
|
||||
.some((payload) => payload?.isError !== true && Boolean(payload?.text?.trim()));
|
||||
const hasFatalStructuredErrorPayload = hasErrorPayload && !hasSuccessfulPayloadAfterLastError;
|
||||
const normalizedFinalAssistantVisibleText = normalizeOptionalString(
|
||||
params.finalAssistantVisibleText,
|
||||
);
|
||||
const hasSuccessfulPayloadBeforeLastError =
|
||||
!params.runLevelError &&
|
||||
lastErrorPayloadIndex > 0 &&
|
||||
params.payloads
|
||||
.slice(0, lastErrorPayloadIndex)
|
||||
.some((payload) => payload?.isError !== true && Boolean(payload?.text?.trim()));
|
||||
const hasNonFatalTrailingPresentationWarning =
|
||||
lastErrorPayloadIndex >= 0 &&
|
||||
isNonFatalCronPresentationWarning(lastErrorPayloadText) &&
|
||||
(normalizedFinalAssistantVisibleText !== undefined || hasSuccessfulPayloadBeforeLastError);
|
||||
const hasFatalStructuredErrorPayload =
|
||||
hasErrorPayload &&
|
||||
!hasSuccessfulPayloadAfterLastError &&
|
||||
!hasNonFatalTrailingPresentationWarning;
|
||||
const hasStructuredDeliveryPayloads = selectedDeliveryPayloads.some((payload) =>
|
||||
payloadHasStructuredDeliveryContent(payload),
|
||||
);
|
||||
@@ -293,10 +320,6 @@ export function resolveCronPayloadOutcome(params: {
|
||||
: synthesizedText
|
||||
? [{ text: synthesizedText }]
|
||||
: [];
|
||||
const lastErrorPayloadText = [...params.payloads]
|
||||
.toReversed()
|
||||
.find((payload) => payload?.isError === true && Boolean(payload?.text?.trim()))
|
||||
?.text?.trim();
|
||||
const denialSignal = resolveCronDenialSignal([
|
||||
{ field: "summary", text: summary },
|
||||
{ field: "outputText", text: outputText },
|
||||
|
||||
Reference in New Issue
Block a user