Environment
- OpenClaw version: 2026.5.28 (upgraded 2026-06-03T00:55:41Z;
journalctl --user -u openclaw-gateway: systemd[1053]: Started openclaw-gateway.service - OpenClaw Gateway (v2026.5.28))
- All cron jobs:
sessionTarget: isolated (58/58 verified via ~/.openclaw/cron/jobs.json raw on-disk count; the operator-facing oc cron list shows 46, the difference is likely disabled/inactive jobs filtered by the CLI view — the gateway runtime processes all 58)
cron.sessionRetention: not set → default 24h applies (resolveRetentionMs source, server-cron-CxzP3epc.js)
Bug description
cron.sessionRetention (default 24h) has no effect when all cron jobs use sessionTarget: isolated. The internal session reaper (sweepCronRunSessions) gates on isCronRunSessionKey(), which only matches session keys containing :run: — a key segment that isolated-target jobs never generate. All isolated cron sessions accumulate indefinitely regardless of the configured retention period.
Root cause (source-verified)
isCronRunSessionKey — ~/.npm-global/lib/node_modules/openclaw/dist/session-key-utils-CdfsDYvz.js:
function isCronRunSessionKey(sessionKey) {
const parsed = parseAgentSessionKey(sessionKey);
if (!parsed) return false;
return /^cron:[^:]+:run:[^:]+(?::|$)/.test(parsed.rest);
}
sweepCronRunSessions — ~/.npm-global/lib/node_modules/openclaw/dist/server-cron-CxzP3epc.js (§ session-reaper.ts):
for (const key of Object.keys(store)) {
if (!isCronRunSessionKey(key)) continue; // ← ALL isolated sessions rejected here
const entry = store[key];
if (!entry) continue;
if ((entry.updatedAt ?? 0) < cutoff) {
delete store[key];
pruned++;
}
}
Key shape written for isolated jobs — same file, resolveCronTaskChildSessionKey:
if (params.job.sessionTarget !== "isolated") return;
return resolveCronAgentSessionKey({
sessionKey: `cron:${params.job.id}`, // → produces "agent:main:cron:<uuid>"
agentId: params.job.agentId ?? params.state.deps.defaultAgentId ?? "main"
});
agent:main:cron:<uuid> has no :run: segment → isCronRunSessionKey() returns false → every isolated cron session skips past the gate and is never pruned regardless of updatedAt.
For comparison, main-target jobs generate agent:main:cron:<jobId>:run:<runId> (via resolveMainSessionCronRunSessionKey), which the regex matches. The retention system works for main-target deployments only.
The reaper only logs when it prunes: if (pruned > 0) params.log.info(...). Since zero sessions ever pass the gate, no log lines are written and the failure is silent.
Pre/post-5.28 cohort split
Upgrade epoch: 1780448141000 (2026-06-03T00:55:41Z). Source: ~/.openclaw/agents/main/sessions/sessions.json + journalctl banner.
| Cohort |
Count |
Oldest |
Newest |
Pre-upgrade (updatedAt < 1780448141000) |
43 |
2026-05-09T22:33:09Z |
2026-06-02T19:00:10Z |
Post-upgrade (updatedAt ≥ 1780448141000) |
4 |
2026-06-03T01:00:07Z |
2026-06-03T01:38:32Z |
| Post-upgrade AND older than 24h |
0 |
— |
— |
The 43 pre-upgrade sessions span all prior versions (5.4 → 5.22 → 5.28). No version introduced a key-shape change for isolated jobs — the :run: segment was never part of the isolated-target key shape on any version. The accumulation is architectural, not a regression in 5.28.
The 4 post-upgrade sessions are all under 1h old at time of investigation; none are yet beyond the 24h retention window. They cannot yet prove whether a fixed 5.28 reaper would prune them after 24h, but their field union (see below) is identical to the pre-upgrade corpus.
Cron session field union — provably missing terminal fields
jq query against ~/.openclaw/agents/main/sessions/sessions.json (all 47 cron sessions, verified 2026-06-03):
[to_entries[]
| select(.key | startswith("agent:main:cron:"))
| .value
| keys[]
] | unique | sort
Result union (20 fields):
["authProfileOverride", "authProfileOverrideCompactionCount", "authProfileOverrideSource",
"cacheRead", "cacheWrite", "contextTokens", "inputTokens", "label",
"lastInteractionAt", "model", "modelProvider", "outputTokens", "sessionFile",
"sessionId", "sessionStartedAt", "skillsSnapshot", "systemPromptReport",
"systemSent", "totalTokens", "totalTokensFresh", "updatedAt"]
Absent fields: status, completedAt, endedAt, state, exitCode, finishedAt.
No terminal/completion marker exists on any session across all versions. This rules out the hypothesis that the pruner keys on a completion field the runtime doesn't write — the reaper does not use completion fields; it uses session key shape. The absent fields are noted for completeness only.
openclaw sessions cleanup --dry-run — 0 cron prunes
This is the CLI cleanup tool (separate from the internal gateway reaper). Run 2026-06-03 during investigation. Summary header:
Session store: ~/.openclaw/agents/main/sessions/sessions.json
Maintenance mode: enforce
Entries: 316 -> 316 (remove 0)
Would prune missing transcripts: 0
Would retire stale direct DM sessions: 0
Would prune stale: 0
Would cap overflow: 0
Would prune unreferenced artifacts: 0
All 47 cron sessions, including the oldest (24d ago), show keep in the action table. Would prune stale: 0 confirmed. The CLI tool also treats all cron sessions as exempt from its stale-prune logic (separate implementation from the gateway reaper, same behavioral gap).
Secondary gap — orphaned sessions from deleted jobs
2 sessions (<job-uuid-A>, <job-uuid-B>, oldest 2026-05-09) have no corresponding entry in ~/.openclaw/cron/jobs-state.json. These jobs were deleted; their base sessions were never purged. No cleanup path exists for them without manual intervention.
Impact
- 47 base cron sessions on this host; oldest is 25 days old; growing with every cron tick.
- 2 sessions are orphaned from deleted jobs with no automated cleanup path.
cron.sessionRetention = false would disable retention outright; cron.sessionRetention = "1h" would still have no effect. The config is inert for all-isolated deployments regardless of the value set.
- Only mitigation today: manual
openclaw sessions cleanup --apply (which the dry-run shows would also not prune them under current logic — Would prune stale: 0).
Suggested fix
Option A — extend isCronRunSessionKey (or add a parallel isCronBaseSessionKey) to match agent:<agentId>:cron:<uuid> keys and make sweepCronRunSessions prune them when updatedAt < cutoff. Requires deciding whether a base session should be pruned while its job still exists and might run again (probably: only prune if the job was deleted, or only prune runs older than N retention windows with no recent activity).
Option B — have resolveCronTaskChildSessionKey write an ephemeral :run:<uuid> record per execution for isolated jobs (in addition to updating the base session), so the existing reaper can prune those run records. This matches the two-tier model described in the JSDoc comment.
Also: job deletion should purge the corresponding base session.
Filed by openclaw-issue-filer on behalf of operator on 2026-06-03T02:25:51Z
Environment
journalctl --user -u openclaw-gateway:systemd[1053]: Started openclaw-gateway.service - OpenClaw Gateway (v2026.5.28))sessionTarget: isolated(58/58 verified via~/.openclaw/cron/jobs.jsonraw on-disk count; the operator-facingoc cron listshows 46, the difference is likely disabled/inactive jobs filtered by the CLI view — the gateway runtime processes all 58)cron.sessionRetention: not set → default 24h applies (resolveRetentionMssource,server-cron-CxzP3epc.js)Bug description
cron.sessionRetention(default 24h) has no effect when all cron jobs usesessionTarget: isolated. The internal session reaper (sweepCronRunSessions) gates onisCronRunSessionKey(), which only matches session keys containing:run:— a key segment that isolated-target jobs never generate. All isolated cron sessions accumulate indefinitely regardless of the configured retention period.Root cause (source-verified)
isCronRunSessionKey—~/.npm-global/lib/node_modules/openclaw/dist/session-key-utils-CdfsDYvz.js:sweepCronRunSessions—~/.npm-global/lib/node_modules/openclaw/dist/server-cron-CxzP3epc.js(§session-reaper.ts):Key shape written for isolated jobs — same file,
resolveCronTaskChildSessionKey:agent:main:cron:<uuid>has no:run:segment →isCronRunSessionKey()returnsfalse→ every isolated cron session skips past the gate and is never pruned regardless ofupdatedAt.For comparison,
main-target jobs generateagent:main:cron:<jobId>:run:<runId>(viaresolveMainSessionCronRunSessionKey), which the regex matches. The retention system works formain-target deployments only.The reaper only logs when it prunes:
if (pruned > 0) params.log.info(...). Since zero sessions ever pass the gate, no log lines are written and the failure is silent.Pre/post-5.28 cohort split
Upgrade epoch:
1780448141000(2026-06-03T00:55:41Z). Source:~/.openclaw/agents/main/sessions/sessions.json+ journalctl banner.updatedAt < 1780448141000)updatedAt ≥ 1780448141000)The 43 pre-upgrade sessions span all prior versions (5.4 → 5.22 → 5.28). No version introduced a key-shape change for isolated jobs — the
:run:segment was never part of the isolated-target key shape on any version. The accumulation is architectural, not a regression in 5.28.The 4 post-upgrade sessions are all under 1h old at time of investigation; none are yet beyond the 24h retention window. They cannot yet prove whether a fixed 5.28 reaper would prune them after 24h, but their field union (see below) is identical to the pre-upgrade corpus.
Cron session field union — provably missing terminal fields
jq query against
~/.openclaw/agents/main/sessions/sessions.json(all 47 cron sessions, verified 2026-06-03):Result union (20 fields):
Absent fields:
status,completedAt,endedAt,state,exitCode,finishedAt.No terminal/completion marker exists on any session across all versions. This rules out the hypothesis that the pruner keys on a completion field the runtime doesn't write — the reaper does not use completion fields; it uses session key shape. The absent fields are noted for completeness only.
openclaw sessions cleanup --dry-run— 0 cron prunesThis is the CLI cleanup tool (separate from the internal gateway reaper). Run 2026-06-03 during investigation. Summary header:
All 47 cron sessions, including the oldest (24d ago), show
keepin the action table.Would prune stale: 0confirmed. The CLI tool also treats all cron sessions as exempt from its stale-prune logic (separate implementation from the gateway reaper, same behavioral gap).Secondary gap — orphaned sessions from deleted jobs
2 sessions (
<job-uuid-A>,<job-uuid-B>, oldest 2026-05-09) have no corresponding entry in~/.openclaw/cron/jobs-state.json. These jobs were deleted; their base sessions were never purged. No cleanup path exists for them without manual intervention.Impact
cron.sessionRetention = falsewould disable retention outright;cron.sessionRetention = "1h"would still have no effect. The config is inert for all-isolated deployments regardless of the value set.openclaw sessions cleanup --apply(which the dry-run shows would also not prune them under current logic —Would prune stale: 0).Suggested fix
Option A — extend
isCronRunSessionKey(or add a parallelisCronBaseSessionKey) to matchagent:<agentId>:cron:<uuid>keys and makesweepCronRunSessionsprune them whenupdatedAt < cutoff. Requires deciding whether a base session should be pruned while its job still exists and might run again (probably: only prune if the job was deleted, or only prune runs older than N retention windows with no recent activity).Option B — have
resolveCronTaskChildSessionKeywrite an ephemeral:run:<uuid>record per execution for isolated jobs (in addition to updating the base session), so the existing reaper can prune those run records. This matches the two-tier model described in the JSDoc comment.Also: job deletion should purge the corresponding base session.
Filed by openclaw-issue-filer on behalf of operator on 2026-06-03T02:25:51Z