Summary
src/tasks/task-registry.maintenance.ts can leave some stale running task rows alive forever.
The problem is in shouldMarkLost().
It only marks a task as lost when !hasBackingSession(task) is true.
In current main and in OpenClaw 2026.4.2, hasBackingSession() returns true for some stale cli and cron tasks even when there is no real active child execution left.
Actual behavior
openclaw tasks audit can report stale_running, but tasks maintenance --apply still does not move these rows to lost.
The rows stay running forever and more stale rows can accumulate over time.
Expected behavior
If a task is still running, has no lifecycle progress for much longer than the reconcile grace, and no real child execution is alive anymore, maintenance should not keep it alive only because of a blank or persistent session key.
It should move that task to lost or another terminal state.
Root cause
Current code in src/tasks/task-registry.maintenance.ts:
function hasBackingSession(task: TaskRecord): boolean {
const childSessionKey = task.childSessionKey?.trim();
if (!childSessionKey) {
return true;
}
if (task.runtime === "acp") {
...
}
if (task.runtime === "subagent" || task.runtime === "cli") {
...
return Boolean(findSessionEntryByKey(store, childSessionKey));
}
return true;
}
This creates three bad paths:
-
runtime="cron" with blank childSessionKey
- early
return true
- stale cron task is always treated as backed
-
runtime="cron" with non-empty childSessionKey
- there is no cron-specific branch
- it falls through to the final
return true
- stale cron task is still always treated as backed
-
runtime="cli" with childSessionKey pointing to a persistent parent session
- the code only checks whether that session entry exists in the store
- for channel sessions like Slack or Telegram, that parent session can exist forever
- the task then never becomes
lost, even if the actual child work is gone
So the stale task check becomes:
- active task: yes
- grace expired: yes
- backing session missing: no
and the row stays running forever.
Live evidence
I hit this on my VPS with OpenClaw 2026.4.2 (d74a122).
Before cleanup, i had 6 stale running task rows in the task ledger:
- 4 x
exec-approval-followup:* with runtime="cli" and childSessionKey pattern agent:main:slack:channel:*
- 1 x
cron:: with runtime="cron" and blank childSessionKey
- 1 x
cron:: with runtime="cron" and childSessionKey pattern agent:main:slack:channel:*
Common properties:
ended_at = NULL
error = NULL
terminal_summary = NULL
last_event_at ~= started_at
- ages were many hours old
openclaw tasks audit reported stale_running
- new runs under the same owner still worked, so these rows did not block everything, but they kept accumulating
I also checked the shipped 2026.4.2 source and current origin/main. This hasBackingSession() logic is the same in both.
Why i think this is a real upstream bug
This is not just local config.
The false alive result comes from current task maintenance logic.
The task audit already knows these rows look stuck.
The problem is that maintenance uses session-key existence as a generic liveness signal, but that is not correct for these cron and cli cases.
Related issues
Repro notes
Small code-level repro:
- Create a
TaskRecord with status="running"
- Set
lastEventAt to older than the 5 minute reconcile grace
- Use one of these inputs:
runtime="cron", blank childSessionKey
runtime="cron", any non-empty childSessionKey
runtime="cli", childSessionKey pointing to a persistent channel session
- Call
reconcileTaskRecordForOperatorInspection() or runTaskRegistryMaintenance()
- The task does not become
lost
Proposed fix
I think the smallest correct fix starts in src/tasks/task-registry.maintenance.ts.
Direction:
- do not treat blank
childSessionKey as alive for runtimes that require a spawned child execution
- do not use a persistent parent session entry as the liveness signal for generic
cron and cli tasks
- use runtime-specific liveness checks where needed
- add regression tests in
src/tasks/task-registry.test.ts
Test cases that seem needed
runtime="cron" + blank childSessionKey becomes lost after grace
runtime="cron" + persistent parent session key becomes lost after grace
runtime="cli" + persistent channel session key does not stay alive forever just because the parent session exists
- existing healthy
subagent behavior does not regress
- existing ACP behavior is either preserved intentionally or handled by a separate explicit rule
Summary
src/tasks/task-registry.maintenance.tscan leave some stalerunningtask rows alive forever.The problem is in
shouldMarkLost().It only marks a task as
lostwhen!hasBackingSession(task)is true.In current
mainand inOpenClaw 2026.4.2,hasBackingSession()returnstruefor some stalecliandcrontasks even when there is no real active child execution left.Actual behavior
openclaw tasks auditcan reportstale_running, buttasks maintenance --applystill does not move these rows tolost.The rows stay
runningforever and more stale rows can accumulate over time.Expected behavior
If a task is still
running, has no lifecycle progress for much longer than the reconcile grace, and no real child execution is alive anymore, maintenance should not keep it alive only because of a blank or persistent session key.It should move that task to
lostor another terminal state.Root cause
Current code in
src/tasks/task-registry.maintenance.ts:This creates three bad paths:
runtime="cron"with blankchildSessionKeyreturn trueruntime="cron"with non-emptychildSessionKeyreturn trueruntime="cli"withchildSessionKeypointing to a persistent parent sessionlost, even if the actual child work is goneSo the stale task check becomes:
and the row stays
runningforever.Live evidence
I hit this on my VPS with
OpenClaw 2026.4.2 (d74a122).Before cleanup, i had 6 stale
runningtask rows in the task ledger:exec-approval-followup:*withruntime="cli"andchildSessionKeypatternagent:main:slack:channel:*cron::withruntime="cron"and blankchildSessionKeycron::withruntime="cron"andchildSessionKeypatternagent:main:slack:channel:*Common properties:
ended_at = NULLerror = NULLterminal_summary = NULLlast_event_at ~= started_atopenclaw tasks auditreportedstale_runningI also checked the shipped
2026.4.2source and currentorigin/main. ThishasBackingSession()logic is the same in both.Why i think this is a real upstream bug
This is not just local config.
The false alive result comes from current task maintenance logic.
The task audit already knows these rows look stuck.
The problem is that maintenance uses session-key existence as a generic liveness signal, but that is not correct for these
cronandclicases.Related issues
[Bug]: isolated cron agentTurn can get stuck/rerun stale running state and eventually time out on lightweight ClawHub JSON summarization #44541
runningrows[Bug]: ACP sessions created via sessions_spawn can enter stale running state with no close/kill mechanism #59561
tasks maintenance --applychecks backing session existence, not task ageLane queue has no task-level timeout — hung promises permanently block session lanes #48488
fix(exec): track background exec liveness with CLI tasks #59719
cronbehavior and the blank-key path still need explicit coverageRepro notes
Small code-level repro:
TaskRecordwithstatus="running"lastEventAtto older than the 5 minute reconcile graceruntime="cron", blankchildSessionKeyruntime="cron", any non-emptychildSessionKeyruntime="cli",childSessionKeypointing to a persistent channel sessionreconcileTaskRecordForOperatorInspection()orrunTaskRegistryMaintenance()lostProposed fix
I think the smallest correct fix starts in
src/tasks/task-registry.maintenance.ts.Direction:
childSessionKeyas alive for runtimes that require a spawned child executioncronandclitaskssrc/tasks/task-registry.test.tsTest cases that seem needed
runtime="cron"+ blankchildSessionKeybecomeslostafter graceruntime="cron"+ persistent parent session key becomeslostafter graceruntime="cli"+ persistent channel session key does not stay alive forever just because the parent session existssubagentbehavior does not regress