From ae21eb7b93db9cba54fcd5eb2c2dbce5629b46b3 Mon Sep 17 00:00:00 2001 From: zengLingbiao Date: Sat, 8 Aug 2026 08:18:20 +0800 Subject: [PATCH] fix(cron): treat zero sessionRetention as disabled instead of pruning all run sessions (#120213) --- docs/automation/cron-jobs.md | 2 +- docs/cli/cron.md | 2 +- docs/gateway/configuration-reference.md | 4 +-- docs/gateway/configuration.md | 2 +- .../session-management-compaction.md | 2 +- src/config/schema.help.automation.ts | 2 +- src/config/types.cron.ts | 1 + src/cron/session-reaper.test.ts | 26 +++++++++++++++++++ src/cron/session-reaper.ts | 10 ++++++- 9 files changed, 43 insertions(+), 8 deletions(-) diff --git a/docs/automation/cron-jobs.md b/docs/automation/cron-jobs.md index 015bd2370590..49f4387c5e21 100644 --- a/docs/automation/cron-jobs.md +++ b/docs/automation/cron-jobs.md @@ -799,7 +799,7 @@ Disable automations: `cron.enabled: false` or `OPENCLAW_SKIP_CRON=1`. - `cron.sessionRetention` (default `24h`, `false` disables) prunes isolated run-session entries. Run history keeps the newest 2000 terminal rows per job; lost rows retain their 24-hour cleanup window. + `cron.sessionRetention` (default `24h`, `false` or `"0h"` disables) prunes isolated run-session entries. Run history keeps the newest 2000 terminal rows per job; lost rows retain their 24-hour cleanup window. On upgrade, run `openclaw doctor --fix` to import historical `~/.openclaw/cron/jobs.json`, `jobs-state.json`, `jobs-quarantine.json`, and `runs/*.jsonl` files into SQLite and archive the originals with a `.migrated` suffix. Malformed job rows remain recoverable in SQLite while valid jobs keep running. diff --git a/docs/cli/cron.md b/docs/cli/cron.md index 69c656c17f8e..7f447533d1d0 100644 --- a/docs/cli/cron.md +++ b/docs/cli/cron.md @@ -216,7 +216,7 @@ The scheduler does not classify final-output prose or approval-looking refusal p Retention behavior: -- `cron.sessionRetention` (default `24h`, or `false` to disable) prunes completed isolated run sessions. +- `cron.sessionRetention` (default `24h`, or `false` to disable; a zero duration such as `"0h"` also disables) prunes completed isolated run sessions. - Run history keeps the newest 2000 terminal rows per job. Lost rows retain the standard 24-hour lost-task cleanup window. ## Migrating older jobs diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index eddd312d8f28..6ae2aadad842 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -1470,14 +1470,14 @@ Current builds no longer include the TCP bridge. Nodes connect over the Gateway webhookSsrfPolicy: { allowedHostnames: ["127.0.0.1"], // optional exact exception for a trusted receiver }, - sessionRetention: "24h", // duration string or false + sessionRetention: "24h", // duration string ("0h" disables) or false }, } ``` - `enabled`: execute stored automation jobs (default: `true`). Set `false` to pause all automation execution without deleting jobs. - `triggers.enabled`: also run event-driven automation triggers (default: `false`). -- `sessionRetention`: how long to keep completed isolated automation run sessions before pruning SQLite session rows. Also controls cleanup of archived deleted automation transcripts. Default: `24h`; set `false` to disable. +- `sessionRetention`: how long to keep completed isolated automation run sessions before pruning SQLite session rows. Also controls cleanup of archived deleted automation transcripts. Default: `24h`; set `false` or a zero duration such as `"0h"` to disable (negative durations are invalid). - Run history automatically keeps the newest 2000 terminal rows per job. Lost rows retain their 24-hour cleanup window. - `webhookToken`: bearer token used for automation webhook POST delivery (`delivery.mode = "webhook"`), if omitted no auth header is sent. - `webhookSsrfPolicy`: shared outbound SSRF policy for primary, completion, failure-destination, and failure-alert webhooks. Private/internal targets are blocked when omitted. Prefer exact `allowedHostnames`; use `dangerouslyAllowPrivateNetwork: true` only for trusted private-network receivers. The narrow fake-IP proxy flags are `allowRfc2544BenchmarkRange` and `allowIpv6UniqueLocalRange`. diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index d594a712d7f2..495105186ae2 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -414,7 +414,7 @@ candidate contains a redacted secret placeholder such as `***` or `[redacted]`. } ``` - - `sessionRetention`: prune completed isolated run sessions from SQLite session rows (default `24h`; set `false` to disable). + - `sessionRetention`: prune completed isolated run sessions from SQLite session rows (default `24h`; set `false` or a zero duration such as `"0h"` to disable). - Run history automatically keeps the newest 2000 terminal rows per job; lost rows retain their 24-hour cleanup window. - See [Cron jobs](/automation/cron-jobs) for feature overview and CLI examples. diff --git a/docs/reference/session-management-compaction.md b/docs/reference/session-management-compaction.md index cca70b86e756..c41242244c81 100644 --- a/docs/reference/session-management-compaction.md +++ b/docs/reference/session-management-compaction.md @@ -108,7 +108,7 @@ artifacts before importing. Isolated cron runs create their own session entries/transcripts with dedicated retention: -- `cron.sessionRetention` (default `"24h"`) prunes old isolated cron run sessions from the store; `false` disables. +- `cron.sessionRetention` (default `"24h"`) prunes old isolated cron run sessions from the store; `false` or a zero duration such as `"0h"` disables. - Run history keeps the newest 2000 terminal rows per cron job. Lost rows retain their 24-hour cleanup window. When cron force-creates a new isolated run session, it sanitizes the previous `cron:` session entry before writing the new row: it carries safe preferences (thinking/fast/verbose/reasoning settings, labels, display name) and explicit user-selected model/auth overrides, but drops ambient conversation context (channel/group routing, send/queue policy, elevation, origin, ACP runtime binding) so a fresh isolated run cannot inherit stale delivery or runtime authority from an older run. diff --git a/src/config/schema.help.automation.ts b/src/config/schema.help.automation.ts index d678817b3ca6..8562afcd11fe 100644 --- a/src/config/schema.help.automation.ts +++ b/src/config/schema.help.automation.ts @@ -100,7 +100,7 @@ export const AUTOMATION_FIELD_HELP: Record = { "cron.webhookSsrfPolicy.allowIpv6UniqueLocalRange": "Allows automation webhooks to IPv6 Unique Local Addresses (fc00::/7). Use only with trusted fake-IP proxy environments.", "cron.sessionRetention": - "Controls how long completed automation run sessions are kept before pruning (`24h`, `7d`, `1h30m`, or `false` to disable pruning; default: `24h`). Use shorter retention to reduce storage growth on high-frequency schedules.", + "Controls how long completed automation run sessions are kept before pruning (`24h`, `7d`, `1h30m`, or `false` to disable pruning; a zero duration such as `0h` also disables; default: `24h`). Use shorter retention to reduce storage growth on high-frequency schedules.", transcripts: "Core transcript capture settings for meeting notes, recording-capable agent tools, and configured live meeting auto-start sources. Meeting plugins capture durable notes by default; set enabled to false to opt out globally.", "transcripts.enabled": diff --git a/src/config/types.cron.ts b/src/config/types.cron.ts index 7ec48b05f9dd..e7e2b208c713 100644 --- a/src/config/types.cron.ts +++ b/src/config/types.cron.ts @@ -32,6 +32,7 @@ export type CronConfig = { /** * How long to retain completed cron run sessions before automatic pruning. * Accepts a duration string (e.g. "24h", "7d", "1h30m") or `false` to disable pruning. + * A zero duration (e.g. "0h") also disables pruning; negative durations are invalid. * Default: "24h". */ sessionRetention?: string | false; diff --git a/src/cron/session-reaper.test.ts b/src/cron/session-reaper.test.ts index bbfd33cc02bf..0f1af6fbd5fb 100644 --- a/src/cron/session-reaper.test.ts +++ b/src/cron/session-reaper.test.ts @@ -527,6 +527,32 @@ describe("sweepCronRunSessions", () => { expect(result.pruned).toBe(0); }); + it.each([["0h"], ["0s"], ["0"]])( + "treats a zero retention (%s) as disabled instead of pruning everything", + async (sessionRetention) => { + const now = Date.now(); + const store: Record = { + "agent:main:cron:job1:run:run1": { + sessionId: "run1", + updatedAt: now - 100 * 3_600_000, + }, + }; + await seedSessionEntries(storePath, store); + + const result = await sweepCronRunSessions({ + cronConfig: { sessionRetention }, + sessionStorePath: storePath, + nowMs: now, + log, + force: true, + }); + + expect(result.swept).toBe(false); + expect(result.pruned).toBe(0); + expect(readSessionEntries(storePath)).toHaveProperty("agent:main:cron:job1:run:run1"); + }, + ); + it("sweeps immediately when disabled retention is enabled again", async () => { const now = Date.now(); const sessionKey = "agent:main:cron:job1:run:expired-run"; diff --git a/src/cron/session-reaper.ts b/src/cron/session-reaper.ts index 697b3bb69207..1062842db4fc 100644 --- a/src/cron/session-reaper.ts +++ b/src/cron/session-reaper.ts @@ -33,7 +33,15 @@ function resolveRetentionMs(cronConfig?: CronConfig): number | null { const raw = cronConfig?.sessionRetention; if (typeof raw === "string" && raw.trim()) { try { - return parseDurationMs(raw.trim(), { defaultUnit: "h" }); + const ms = parseDurationMs(raw.trim(), { defaultUnit: "h" }); + // A zero retention ("0h") is a disable signal, not "prune everything": + // cutoff would equal now and the next sweep would delete every cron run + // session. Negative durations never get here (the parser rejects them); + // the <= 0 check stays defensive. + if (ms <= 0) { + return null; + } + return ms; } catch { return DEFAULT_RETENTION_MS; }