Skip to content

cron.sessionRetention does not prune isolated-target cron sessions — reaper targets :run: key shape which isolated jobs never generate #89666

Description

@BryceMurray

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.jssession-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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions