From 99a98270774c4eb8ca26968dacbb4672ee7556cd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 12 Aug 2026 13:32:15 -0700 Subject: [PATCH] fix(cron): retain deleted job history by agent (#122791) --- src/cron/task-run-history.ts | 10 ++++------ src/gateway/server-methods/cron.ts | 2 +- src/gateway/server.cron.test.ts | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/cron/task-run-history.ts b/src/cron/task-run-history.ts index 6bc0476bebff..2d2b805d437c 100644 --- a/src/cron/task-run-history.ts +++ b/src/cron/task-run-history.ts @@ -4,6 +4,7 @@ import { normalizeOptionalString, } from "@openclaw/normalization-core/string-coerce"; import { uniqueValues } from "@openclaw/normalization-core/string-normalization"; +import { normalizeAgentId } from "../routing/session-key.js"; import { listTaskRegistryRecordsByRuntimeSourceIdFromSqlite } from "../tasks/task-registry.store.sqlite.js"; import type { TaskRecord } from "../tasks/task-registry.types.js"; import type { CronRunLogEntry } from "./run-log-types.js"; @@ -23,8 +24,7 @@ type ReadCronTaskRunHistoryPageOptions = { limit?: number; offset?: number; jobId?: string; - /** Narrows the page to these job ids (caller-scope filtering). */ - jobIds?: readonly string[]; + agentId?: string; runId?: string; status?: CronRunHistoryStatusFilter; statuses?: CronRunStatus[]; @@ -127,7 +127,7 @@ export function readCronTaskRunHistoryPage( const statuses = normalizeStatuses(options); const deliveryStatuses = normalizeDeliveryStatuses(options); const runId = normalizeOptionalString(options.runId); - const jobIds = options.jobIds ? new Set(options.jobIds) : undefined; + const agentId = options.agentId ? normalizeAgentId(options.agentId) : undefined; const query = normalizeLowercaseStringOrEmpty(options.query); const sortDir: CronRunHistorySortDir = options.sortDir === "asc" ? "asc" : "desc"; const rows = listTaskRegistryRecordsByRuntimeSourceIdFromSqlite({ @@ -135,12 +135,10 @@ export function readCronTaskRunHistoryPage( sourceId: jobId, }) .filter((task) => cronTaskRecordStoreKey(task) === options.storeKey) + .filter((task) => !agentId || task.agentId === agentId) .map((task) => ({ task, entry: cronTaskRecordToRunLogEntry(task) })) .filter((row): row is { task: TaskRecord; entry: CronRunLogEntry } => row.entry !== null) .filter(({ entry }) => { - if (jobIds && !jobIds.has(entry.jobId)) { - return false; - } if (runId && entry.runId !== runId) { return false; } diff --git a/src/gateway/server-methods/cron.ts b/src/gateway/server-methods/cron.ts index b8f9d5305c68..461e0a17534b 100644 --- a/src/gateway/server-methods/cron.ts +++ b/src/gateway/server-methods/cron.ts @@ -1192,7 +1192,7 @@ export const cronHandlers: GatewayRequestHandlers = { const page = readCronTaskRunHistoryPage({ storeKey: cronStoreKey(context.cronStorePath), ...cronRunLogPageFilters(p), - ...(p.agentId ? { jobIds: jobs.map((job) => job.id) } : {}), + agentId: p.agentId, jobNameById, }); respond(true, page, undefined); diff --git a/src/gateway/server.cron.test.ts b/src/gateway/server.cron.test.ts index 5a867e18efdf..da6fe2bdc244 100644 --- a/src/gateway/server.cron.test.ts +++ b/src/gateway/server.cron.test.ts @@ -11,6 +11,7 @@ import { resetConfigRuntimeState } from "../config/config.js"; import { loadCronStore, saveCronStore } from "../cron/store.js"; import type { GuardedFetchOptions } from "../infra/net/fetch-guard.js"; import { peekSystemEvents } from "../infra/system-events.js"; +import { listTaskRegistryRecordsByRuntimeSourceIdFromSqlite } from "../tasks/task-registry.store.sqlite.js"; import { getGatewayProcessInstanceId } from "./process-instance.js"; import type { GatewayCronState } from "./server-cron.js"; import { @@ -1470,6 +1471,23 @@ describe("gateway server cron", () => { expect.objectContaining({ jobId: writerJobId }), ); + const removeWriter = await directCronReq(cronState, "cron.remove", { id: writerJobId }); + expect(removeWriter.ok).toBe(true); + expect( + listTaskRegistryRecordsByRuntimeSourceIdFromSqlite({ + runtime: "cron", + sourceId: writerJobId, + }), + ).toEqual([expect.objectContaining({ agentId: "writer" })]); + const retainedWriterRuns = await directCronReq(cronState, "cron.runs", { + scope: "all", + agentId: "writer", + }); + expect(retainedWriterRuns.payload).toMatchObject({ + entries: [expect.objectContaining({ jobId: writerJobId })], + total: 1, + }); + const statusRes = await directCronReq(cronState, "cron.status", {}); expect(statusRes.ok).toBe(true); const statusPayload = statusRes.payload as