Skip to content

[Bug]: task maintenance can keep stale cli/cron tasks running forever when childSessionKey is blank or persistent #60299

Description

@dorukardahan

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:

  1. runtime="cron" with blank childSessionKey

    • early return true
    • stale cron task is always treated as backed
  2. 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
  3. 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:

  1. Create a TaskRecord with status="running"
  2. Set lastEventAt to older than the 5 minute reconcile grace
  3. Use one of these inputs:
    • runtime="cron", blank childSessionKey
    • runtime="cron", any non-empty childSessionKey
    • runtime="cli", childSessionKey pointing to a persistent channel session
  4. Call reconcileTaskRecordForOperatorInspection() or runTaskRegistryMaintenance()
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions