fix(cron): treat zero sessionRetention as disabled instead of pruning all run sessions (#120213)

This commit is contained in:
zengLingbiao
2026-08-08 08:18:20 +08:00
committed by GitHub
parent 96a75be170
commit ae21eb7b93
9 changed files with 43 additions and 8 deletions
+1 -1
View File
@@ -799,7 +799,7 @@ Disable automations: `cron.enabled: false` or `OPENCLAW_SKIP_CRON=1`.
</Accordion>
<Accordion title="Maintenance">
`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.
</Accordion>
<Accordion title="Legacy store migration">
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.
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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`.
+1 -1
View File
@@ -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.
@@ -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:<jobId>` 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.
+1 -1
View File
@@ -100,7 +100,7 @@ export const AUTOMATION_FIELD_HELP: Record<string, string> = {
"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":
+1
View File
@@ -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;
+26
View File
@@ -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<string, SessionEntry> = {
"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";
+9 -1
View File
@@ -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;
}