Summary
For sessionTarget: "isolated" cron jobs, a fresh per-run session row can inherit stale lifecycle metadata from older session state.
Observed stale fields include:
This produces run rows that look finalized before the new run has actually finished, or rows that end up with a mixed/partial state even though the underlying run really executed.
Why this looks like an OpenClaw bug
The actual isolated run can still work:
- a real
sessionId is created
- the transcript file exists
- usage / summary patches may be written later
But the run-scoped session-store row can start life with stale lifecycle fields or an incomplete lifecycle shape.
So the execution path appears to work, while the persistence/bookkeeping path is wrong.
Observed behavior
A new current run existed under the base cron session key:
- base key:
agent:main:cron:<job-id>
sessionId = 4b218b3d-...
status = running
But the matching per-run row for that same sessionId was already wrong:
- run key:
agent:main:cron:<job-id>:run:4b218b3d-...
sessionId = 4b218b3d-...
status = done
startedAt = <timestamp from previous run>
endedAt = <timestamp from previous run>
That means the new run row was born with stale lifecycle fields from the previous run.
I also observed earlier run rows that had:
- real transcript
- real token stats
- missing or inconsistent lifecycle fields
This suggests later merge/update paths can patch usage/summary data onto a bad initial row shape without reconstructing correct lifecycle metadata.
Expected behavior
For a fresh isolated cron run:
- the per-run
:run:<sessionId> row should start with fresh lifecycle state
- it should not inherit prior-run
status, startedAt, or endedAt
- the base cron key and run-scoped key should not share a mutable session-entry object that can carry stale run-scoped fields
Actual behavior
Fresh isolated cron run rows appear to be seeded from prior/base session entry state and can inherit stale lifecycle fields.
This causes symptoms like:
- run row marked
done while the current run is actually active
- mismatched
startedAt / endedAt
- partial rows with transcript + usage but missing correct lifecycle finalization
- confusing timeout / zombie / bookkeeping artifacts even when the run did real work
Suspected root cause
1) resolveCronSession() copies prior entry state into the new session entry
The isolated-session creation path appears to seed the new session entry via something equivalent to:
sessionEntry: {
...entry,
sessionId,
updatedAt,
systemSent,
...
}
Some routing fields are cleared for fresh sessions, but run-scoped lifecycle fields are not fully cleared.
2) createPersistCronSessionEntry() persists that session entry to both keys
The same session-entry object appears to be written to both:
- the base cron key
- the per-run
:run:<sessionId> key
So if stale lifecycle fields are already present on that object, the fresh run row inherits them immediately.
3) later updates merge onto the existing row instead of rebuilding lifecycle state
Later usage/summary updates can make the row look partly real, but they do not necessarily repair incorrect or missing lifecycle fields.
Related issues
This seems closely related to the broader isolated-cron state-inheritance family:
#66380 isolated cron run sessions inherit stale heartbeat cache fields
#62707 resolveCronSession leaks stale CLI session identity on forceNew
#11506 cron sessions inherit stale model from prior session-store entry
#70421 session status not synced with endedAt
#70347 cron timeout path does not finalize lifecycle cleanly
This report is specifically about stale inherited lifecycle fields on fresh isolated cron run rows.
Minimal fix direction
-
In the fresh isolated-run path, explicitly clear run-scoped lifecycle fields such as:
-
Avoid persisting the exact same mutable session-entry object to both:
-
Prefer treating per-run :run:<sessionId> entries as immutable run snapshots rather than mutable shared state.
Impact
This makes cron runs hard to trust operationally because a run may:
- really execute
- produce a transcript
- produce token stats
while still appearing stale, pre-finalized, or half-corrupted in session bookkeeping.
Summary
For
sessionTarget: "isolated"cron jobs, a fresh per-run session row can inherit stale lifecycle metadata from older session state.Observed stale fields include:
statusstartedAtendedAtThis produces run rows that look finalized before the new run has actually finished, or rows that end up with a mixed/partial state even though the underlying run really executed.
Why this looks like an OpenClaw bug
The actual isolated run can still work:
sessionIdis createdBut the run-scoped session-store row can start life with stale lifecycle fields or an incomplete lifecycle shape.
So the execution path appears to work, while the persistence/bookkeeping path is wrong.
Observed behavior
A new current run existed under the base cron session key:
agent:main:cron:<job-id>sessionId = 4b218b3d-...status = runningBut the matching per-run row for that same
sessionIdwas already wrong:agent:main:cron:<job-id>:run:4b218b3d-...sessionId = 4b218b3d-...status = donestartedAt = <timestamp from previous run>endedAt = <timestamp from previous run>That means the new run row was born with stale lifecycle fields from the previous run.
I also observed earlier run rows that had:
This suggests later merge/update paths can patch usage/summary data onto a bad initial row shape without reconstructing correct lifecycle metadata.
Expected behavior
For a fresh isolated cron run:
:run:<sessionId>row should start with fresh lifecycle statestatus,startedAt, orendedAtActual behavior
Fresh isolated cron run rows appear to be seeded from prior/base session entry state and can inherit stale lifecycle fields.
This causes symptoms like:
donewhile the current run is actually activestartedAt/endedAtSuspected root cause
1)
resolveCronSession()copies prior entry state into the new session entryThe isolated-session creation path appears to seed the new session entry via something equivalent to:
Some routing fields are cleared for fresh sessions, but run-scoped lifecycle fields are not fully cleared.
2)
createPersistCronSessionEntry()persists that session entry to both keysThe same session-entry object appears to be written to both:
:run:<sessionId>keySo if stale lifecycle fields are already present on that object, the fresh run row inherits them immediately.
3) later updates merge onto the existing row instead of rebuilding lifecycle state
Later usage/summary updates can make the row look partly real, but they do not necessarily repair incorrect or missing lifecycle fields.
Related issues
This seems closely related to the broader isolated-cron state-inheritance family:
#66380isolated cron run sessions inherit stale heartbeat cache fields#62707resolveCronSessionleaks stale CLI session identity on forceNew#11506cron sessions inherit stale model from prior session-store entry#70421session status not synced withendedAt#70347cron timeout path does not finalize lifecycle cleanlyThis report is specifically about stale inherited lifecycle fields on fresh isolated cron run rows.
Minimal fix direction
In the fresh isolated-run path, explicitly clear run-scoped lifecycle fields such as:
statusstartedAtendedAtAvoid persisting the exact same mutable session-entry object to both:
Prefer treating per-run
:run:<sessionId>entries as immutable run snapshots rather than mutable shared state.Impact
This makes cron runs hard to trust operationally because a run may:
while still appearing stale, pre-finalized, or half-corrupted in session bookkeeping.