mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
improve(cron): show consecutive failure count and last error in cron CLI output (#99602)
* improve(cron): show consecutive failure count and last error in cron list/show * improve(cron): show consecutive failure count and last error in cron CLI output * fix(clownfish): address review for live-pr-inventory-20260706T074223-002 (1) --------- Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
parent
1b44efec8c
commit
310c2f58b9
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- **Android PTT microphone ownership:** pause realtime relay capture while push-to-talk owns the microphone, then resume only for the same completed capture. (#99986) Thanks @NianJiuZst.
|
||||
- **Bundled channel loading:** resolve source-only bundled channel entries from their current registry root and canonicalize active-release aliases before enforcing module boundaries, avoiding false setup-entry warnings in mixed source/dist deployments without admitting paths outside the active package boundary.
|
||||
- **Cron CLI diagnostics:** show repeated failure counts and the last error in human `cron list`/`cron show` output while keeping JSON status fields undecorated. (#99602) Thanks @masatohoshino.
|
||||
- **Small-context compaction:** cap the effective reserve against the known model context window so small local models do not enter compaction from the first token. (#100621) Thanks @vincentkoc.
|
||||
- **Plugin install diagnostics:** suppress the misleading hook-pack fallback after plugin install failures only when the hook manifest is absent, while preserving actionable malformed hook-pack errors. (#100554) Thanks @vincentkoc.
|
||||
- **Config validation diagnostics:** emit each unchanged sanitized validation-warning payload once per config path, reset deduplication after a clean validation, and preserve the warning fingerprint across transient invalid reads and failed refreshes. (#100569, #25574) Thanks @vincentkoc.
|
||||
|
||||
+1
-1
@@ -302,7 +302,7 @@ openclaw cron runs --id <job-id> --run-id <run-id>
|
||||
|
||||
`openclaw cron get <job-id>` returns the stored job JSON directly. Use `cron show <job-id>` when you want the human-readable view with delivery-route preview.
|
||||
|
||||
`cron list --json` and `cron show <job-id> --json` include a top-level `status` field on each job, computed from `enabled`, `state.runningAtMs`, and `state.lastRunStatus`. Values: `disabled`, `running`, `ok`, `error`, `skipped`, or `idle`. This mirrors the human-readable status column so external tooling can read job state without re-deriving it.
|
||||
`cron list --json` and `cron show <job-id> --json` include a top-level `status` field on each job, computed from `enabled`, `state.runningAtMs`, and `state.lastRunStatus`. Values: `disabled`, `running`, `ok`, `error`, `skipped`, or `idle`. JSON status stays canonical and undecorated so external tooling can read job state without re-deriving it; human output may decorate repeated `error` statuses with a failure count.
|
||||
|
||||
`cron runs` entries include delivery diagnostics with the intended cron target, the resolved target, message-tool sends, fallback use, and delivered state.
|
||||
|
||||
|
||||
@@ -174,6 +174,79 @@ describe("printCronList", () => {
|
||||
expectLogsToInclude(show.logs, "schedule: on-exit pnpm build @ /repo");
|
||||
});
|
||||
|
||||
it("shows the consecutive failure count for chronically failing jobs", () => {
|
||||
const failing = createBaseJob({
|
||||
id: "failing-job",
|
||||
name: "Failing",
|
||||
state: { lastRunStatus: "error", consecutiveErrors: 12, lastError: "boom" },
|
||||
});
|
||||
const singleFailure = createBaseJob({
|
||||
id: "single-failure-job",
|
||||
name: "Failed Once",
|
||||
state: { lastRunStatus: "error", consecutiveErrors: 1, lastError: "boom" },
|
||||
});
|
||||
|
||||
const { logs, runtime } = createRuntimeLogCapture();
|
||||
printCronList([failing, singleFailure], runtime);
|
||||
|
||||
expectLogsToInclude(logs, "error (12x)");
|
||||
// A single failure keeps the bare status token; the count only marks repeats.
|
||||
const singleLine = logs.find((line) => line.includes("single-failure-job")) ?? "";
|
||||
expect(singleLine).toContain("error");
|
||||
expect(singleLine).not.toContain("(1x)");
|
||||
});
|
||||
|
||||
it("caps the failure count so the status column never overflows", () => {
|
||||
const { logs, runtime } = createRuntimeLogCapture();
|
||||
printCronList(
|
||||
[
|
||||
createBaseJob({
|
||||
id: "minute-cron-job",
|
||||
state: { lastRunStatus: "error", consecutiveErrors: 1440, lastError: "boom" },
|
||||
}),
|
||||
],
|
||||
runtime,
|
||||
);
|
||||
expectLogsToInclude(logs, "error (99+x)");
|
||||
expect(logs.join("\n")).not.toContain("1440");
|
||||
});
|
||||
|
||||
it("keeps the --json status field free of the failure-count decoration", () => {
|
||||
const job = createBaseJob({
|
||||
id: "json-job",
|
||||
state: { lastRunStatus: "error", consecutiveErrors: 12, lastError: "boom" },
|
||||
});
|
||||
const enriched = enrichCronJsonWithStatus({
|
||||
jobs: [job],
|
||||
}) as { jobs: Array<{ status?: string }> };
|
||||
expect(enriched.jobs[0]?.status).toBe("error");
|
||||
expect(enrichCronJsonWithStatus(job)).toMatchObject({
|
||||
status: "error",
|
||||
state: { consecutiveErrors: 12, lastError: "boom" },
|
||||
});
|
||||
});
|
||||
|
||||
it("shows last error and failure count in cron show output", () => {
|
||||
const failing = createRuntimeLogCapture();
|
||||
printCronShow(
|
||||
createBaseJob({
|
||||
id: "show-failing-job",
|
||||
state: { lastRunStatus: "error", consecutiveErrors: 3, lastError: "provider exploded" },
|
||||
}),
|
||||
failing.runtime,
|
||||
);
|
||||
expectLogsToInclude(failing.logs, "status: error (3x)");
|
||||
expectLogsToInclude(failing.logs, "last error: provider exploded");
|
||||
|
||||
const healthy = createRuntimeLogCapture();
|
||||
printCronShow(
|
||||
createBaseJob({ id: "healthy-job", state: { lastRunStatus: "ok" } }),
|
||||
healthy.runtime,
|
||||
);
|
||||
expectLogsToInclude(healthy.logs, "status: ok");
|
||||
expectLogsToInclude(healthy.logs, "last error: -");
|
||||
});
|
||||
|
||||
it("shows dash for unset agentId instead of default", () => {
|
||||
const { logs, runtime } = createRuntimeLogCapture();
|
||||
const job = createBaseJob({
|
||||
|
||||
@@ -165,6 +165,24 @@ function computeStatus(job: CronJob): string {
|
||||
return state.lastRunStatus ?? state.lastStatus ?? "idle";
|
||||
}
|
||||
|
||||
// Human-facing decoration only: enrichCronJsonWithStatus() emits computeStatus()
|
||||
// verbatim as the --json `status` field, so the failure count must stay out of it.
|
||||
// consecutiveErrors resets to 0 on the next successful run, so the count is live.
|
||||
function decorateStatusWithFailures(status: string, consecutiveErrors: number | undefined): string {
|
||||
const failures = consecutiveErrors ?? 0;
|
||||
if (status !== "error" || failures <= 1) {
|
||||
return status;
|
||||
}
|
||||
// Capped so the Status column never overflows (a minute cron failing for a day
|
||||
// reaches 4 digits); past 99 the exact figure adds nothing over "chronic".
|
||||
return failures > 99 ? `${status} (99+x)` : `${status} (${failures}x)`;
|
||||
}
|
||||
|
||||
function formatCronStatusForDisplay(job: CronJob): string {
|
||||
const state = job.state ?? {};
|
||||
return decorateStatusWithFailures(computeStatus(job), state.consecutiveErrors);
|
||||
}
|
||||
|
||||
export function handleCronCliError(err: unknown) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
@@ -318,7 +336,7 @@ const CRON_NAME_PAD = 24;
|
||||
const CRON_SCHEDULE_PAD = 32;
|
||||
const CRON_NEXT_PAD = 10;
|
||||
const CRON_LAST_PAD = 10;
|
||||
const CRON_STATUS_PAD = 9;
|
||||
const CRON_STATUS_PAD = 12;
|
||||
const CRON_TARGET_PAD = 9;
|
||||
const CRON_DELIVERY_PAD = 64;
|
||||
const CRON_AGENT_PAD = 10;
|
||||
@@ -473,7 +491,7 @@ export function printCronList(
|
||||
);
|
||||
const lastLabel = pad(formatRelative(state.lastRunAtMs, now), CRON_LAST_PAD);
|
||||
const statusRaw = computeStatus(job);
|
||||
const statusLabel = pad(statusRaw, CRON_STATUS_PAD);
|
||||
const statusLabel = pad(formatCronStatusForDisplay(job), CRON_STATUS_PAD);
|
||||
const targetLabel = pad(job.sessionTarget ?? "-", CRON_TARGET_PAD);
|
||||
const deliveryPreview = opts?.deliveryPreviews?.get(job.id);
|
||||
const deliveryText = deliveryPreview
|
||||
@@ -560,7 +578,9 @@ export function printCronShow(
|
||||
runtime.log(`delivery: ${preview.label} (${preview.detail})`);
|
||||
runtime.log(`next: ${formatRelative(job.state.nextRunAtMs, Date.now())}`);
|
||||
runtime.log(`last: ${formatRelative(job.state.lastRunAtMs, Date.now())}`);
|
||||
runtime.log(`status: ${computeStatus(job)}`);
|
||||
runtime.log(`status: ${formatCronStatusForDisplay(job)}`);
|
||||
// lastError is the run/schedule failure message; the diagnostic line below is
|
||||
// the run-diagnostics summary and can be empty when only lastError is set.
|
||||
runtime.log(`last error: ${job.state.lastError ?? "-"}`);
|
||||
runtime.log(`last delivery: ${job.state.lastDeliveryStatus ?? "-"}`);
|
||||
runtime.log(`last delivery error: ${job.state.lastDeliveryError ?? "-"}`);
|
||||
|
||||
Reference in New Issue
Block a user