mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
cron: record lastErrorReason in job state (#14382)
Merged via squash.
Prepared head SHA: baa6b5d566
Co-authored-by: futuremind2026 <258860756+futuremind2026@users.noreply.github.com>
Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com>
Reviewed-by: @BunsDev
This commit is contained in:
@@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/exec approvals: reject `/approve` commands aimed at other bots, keep deterministic approval prompts visible when tool-result delivery fails, and stop resolved exact IDs from matching other pending approvals by prefix. (#37233) Thanks @huntharo.
|
||||
- Control UI/Sessions: restore single-column session table collapse on narrow viewport or container widths by moving the responsive table override next to the base grid rule and enabling inline-size container queries. (#12175) Thanks @benjipeng.
|
||||
- Telegram/final preview delivery: split active preview lifecycle from cleanup retention so missing archived preview edits avoid duplicate fallback sends without clearing the live preview or blocking later in-place finalization. (#41662) thanks @hougangdev.
|
||||
- Cron/state errors: record `lastErrorReason` in cron job state and keep the gateway schema aligned with the full failover-reason set, including regression coverage for protocol conformance. (#14382) thanks @futuremind2026.
|
||||
|
||||
## 2026.3.8
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MACOS_APP_SOURCES_DIR } from "../compat/legacy-names.js";
|
||||
import { CronDeliverySchema } from "../gateway/protocol/schema.js";
|
||||
import { CronDeliverySchema, CronJobStateSchema } from "../gateway/protocol/schema.js";
|
||||
|
||||
type SchemaLike = {
|
||||
anyOf?: Array<SchemaLike>;
|
||||
@@ -29,6 +29,16 @@ function extractDeliveryModes(schema: SchemaLike): string[] {
|
||||
return Array.from(new Set(unionModes));
|
||||
}
|
||||
|
||||
function extractConstUnionValues(schema: SchemaLike): string[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
(schema.anyOf ?? [])
|
||||
.map((entry) => entry?.const)
|
||||
.filter((value): value is string => typeof value === "string"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const UI_FILES = ["ui/src/ui/types.ts", "ui/src/ui/ui-types.ts", "ui/src/ui/views/cron.ts"];
|
||||
|
||||
const SWIFT_MODEL_CANDIDATES = [`${MACOS_APP_SOURCES_DIR}/CronModels.swift`];
|
||||
@@ -88,4 +98,19 @@ describe("cron protocol conformance", () => {
|
||||
expect(swift.includes("struct CronSchedulerStatus")).toBe(true);
|
||||
expect(swift.includes("let jobs:")).toBe(true);
|
||||
});
|
||||
|
||||
it("cron job state schema keeps the full failover reason set", () => {
|
||||
const properties = (CronJobStateSchema as SchemaLike).properties ?? {};
|
||||
const lastErrorReason = properties.lastErrorReason as SchemaLike | undefined;
|
||||
expect(lastErrorReason).toBeDefined();
|
||||
expect(extractConstUnionValues(lastErrorReason ?? {})).toEqual([
|
||||
"auth",
|
||||
"format",
|
||||
"rate_limit",
|
||||
"billing",
|
||||
"timeout",
|
||||
"model_not_found",
|
||||
"unknown",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveFailoverReasonFromError } from "../../agents/failover-error.js";
|
||||
import type { CronConfig, CronRetryOn } from "../../config/types.cron.js";
|
||||
import type { HeartbeatRunResult } from "../../infra/heartbeat-wake.js";
|
||||
import { DEFAULT_AGENT_ID } from "../../routing/session-key.js";
|
||||
@@ -322,6 +323,10 @@ export function applyJobResult(
|
||||
job.state.lastStatus = result.status;
|
||||
job.state.lastDurationMs = Math.max(0, result.endedAt - result.startedAt);
|
||||
job.state.lastError = result.error;
|
||||
job.state.lastErrorReason =
|
||||
result.status === "error" && typeof result.error === "string"
|
||||
? (resolveFailoverReasonFromError(result.error) ?? undefined)
|
||||
: undefined;
|
||||
job.state.lastDelivered = result.delivered;
|
||||
const deliveryStatus = resolveDeliveryStatus({ job, delivered: result.delivered });
|
||||
job.state.lastDeliveryStatus = deliveryStatus;
|
||||
@@ -670,7 +675,6 @@ export async function onTimer(state: CronServiceState) {
|
||||
if (completedResults.length > 0) {
|
||||
await locked(state, async () => {
|
||||
await ensureLoaded(state, { forceReload: true, skipRecompute: true });
|
||||
|
||||
for (const result of completedResults) {
|
||||
applyOutcomeToStoredJob(state, result);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FailoverReason } from "../agents/pi-embedded-helpers.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import type { CronJobBase } from "./types-shared.js";
|
||||
|
||||
@@ -105,7 +106,6 @@ type CronAgentTurnPayload = {
|
||||
type CronAgentTurnPayloadPatch = {
|
||||
kind: "agentTurn";
|
||||
} & Partial<CronAgentTurnPayloadFields>;
|
||||
|
||||
export type CronJobState = {
|
||||
nextRunAtMs?: number;
|
||||
runningAtMs?: number;
|
||||
@@ -115,6 +115,8 @@ export type CronJobState = {
|
||||
/** Back-compat alias for lastRunStatus. */
|
||||
lastStatus?: "ok" | "error" | "skipped";
|
||||
lastError?: string;
|
||||
/** Classified reason for the last error (when available). */
|
||||
lastErrorReason?: FailoverReason;
|
||||
lastDurationMs?: number;
|
||||
/** Number of consecutive execution errors (reset on success). Used for backoff. */
|
||||
consecutiveErrors?: number;
|
||||
|
||||
@@ -56,6 +56,15 @@ const CronDeliveryStatusSchema = Type.Union([
|
||||
Type.Literal("unknown"),
|
||||
Type.Literal("not-requested"),
|
||||
]);
|
||||
const CronFailoverReasonSchema = Type.Union([
|
||||
Type.Literal("auth"),
|
||||
Type.Literal("format"),
|
||||
Type.Literal("rate_limit"),
|
||||
Type.Literal("billing"),
|
||||
Type.Literal("timeout"),
|
||||
Type.Literal("model_not_found"),
|
||||
Type.Literal("unknown"),
|
||||
]);
|
||||
const CronCommonOptionalFields = {
|
||||
agentId: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
sessionKey: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
@@ -219,6 +228,7 @@ export const CronJobStateSchema = Type.Object(
|
||||
lastRunStatus: Type.Optional(CronRunStatusSchema),
|
||||
lastStatus: Type.Optional(CronRunStatusSchema),
|
||||
lastError: Type.Optional(Type.String()),
|
||||
lastErrorReason: Type.Optional(CronFailoverReasonSchema),
|
||||
lastDurationMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
consecutiveErrors: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastDelivered: Type.Optional(Type.Boolean()),
|
||||
|
||||
Reference in New Issue
Block a user