Skip to content

Design: run lifecycle reliability — bounded liveness, abort⇒release, crash resume, completion drive #101863

Description

@obviyus

Summary

Agent runs today can hang without bound, die without a user-visible outcome, and strand their session's queued messages until a gateway restart. These are not independent bugs: ~160 tracker reports cluster into a small set of structural gaps in how we own a run's lifecycle. This issue proposes the long-term design — four invariants and four implementation tracks — that retires the class instead of patching instances.

Problem to solve

Users report two experiences, both costing us real deployments to competing frameworks:

  1. "Long multi-step tasks hang partway and never complete; I have to handhold."
  2. "I come back to a session and the agent died on a turn; everything I sent since is silently queued."

Cross-referencing the open tracker with a source audit, these decompose into three root causes:

Root cause 1 — stall detection is heuristic and self-defeating, and detection ≠ recovery.

Root cause 2 — zero durability: a process death loses the in-flight turn and every queued followup, with no resume.

  • The unhandled-rejection policy exits the process for any unclassified rejection (src/infra/unhandled-rejections.ts, final branch → exitWithTerminalRestoreprocess.exit(1)), and the turn pipeline contains many fire-and-forget promises. One stray rejection anywhere kills the gateway.
  • The followup queue is a purely in-memory global map (src/auto-reply/reply/queue/state.ts, FOLLOWUP_QUEUES); active-run/abort state is in-memory; nothing persists "turn in progress". After restart, busy flags can even survive while dispatch never resumes ([Bug]: Session stuck in permanent busy state after mid-turn gateway restart — no auto-recovery, only /new or /reset clears it #92519: 301 assembles, 0 dispatches over 12h).

Root cause 3 — no completion drive: an early model stop is accepted as final, and some failures are silent.

Proposed solution

Four invariants, then four tracks that implement them. The invariants are the contract; every track is judged against them.

I1 — Bounded liveness. Every run reaches a terminal state in bounded time, where "time" is measured as gap since last real forward-progress event, never raw wall clock and never synthetic heartbeats.
I2 — Terminal ⇒ released + reported. Reaching a terminal state atomically releases the session lane/locks and emits exactly one user-visible outcome (or an explicitly-silent one for room_event/silent modes). No path may terminate a run without doing both.
I3 — Restart-safe. The followup queue and a minimal turn-in-progress marker are persisted; boot reconciles interrupted runs into a resumable state instead of stranding them.
I4 — Early stop ≠ done. A model stopping prematurely triggers bounded continuation, not silent acceptance.

Track A — single run-lifecycle owner (abort ⇒ release)

Track B — evidence-based liveness (RunActivity)

Track C — durability (crash resume)

  • Move the followup queue and a minimal turn_in_progress marker into the shared state DB (state/openclaw.sqlite) per the repo storage mandate — the in-memory global map is existing migration debt.
  • Boot reconcile: runs marked running at startup transition to interrupted (Track A ⇒ lane released, outcome recorded), then the session receives one synthesized continuation turn ("previous turn was interrupted; do not re-execute completed tool calls"), bounded by a restart-loop guard (e.g. 3 resume-boots/60s → suspend and notify).
  • Rescope the unhandled-rejection policy: rejections attributable to an owning run fail that run terminally (Track A ⇒ release + notify) instead of process.exit(1); process exit remains only for truly unattributable/fatal states. The current classify-or-die policy makes every unannotated fire-and-forget promise a gateway-wide kill switch.
  • Retires: [Bug]: Session stuck in permanent busy state after mid-turn gateway restart — no auto-recovery, only /new or /reset clears it #92519, [Bug] Restart-sentinel continuation can get stuck after transient Telegram sendMessage failure #76087, the "came back and it was dead" class, and the queued-message loss on every crash/restart.

Track D — completion drive

  • Never-silent terminals: every terminal outcome produces a user-visible message unless the turn is explicitly silent (room_event/silent modes). Audit the suppressing branches in followup-runner.ts (fallback exhaustion, abort routes).
  • Compaction-safe tasks: mid-turn compaction re-injects the active plan/remaining work and resumes the interrupted turn instead of abandoning it (Auto-compaction during ongoing turn silently abandons task execution without resumption #59618); orphaned tool_use blocks are closed with synthetic error results so the session stays resumable (bug: compaction preserves orphaned tool_use blocks after request timeout, permanently breaking session #93321).
  • Bounded continuation nudges: when the model replies with a pure intent acknowledgement ("I'll do X") without tool calls mid-task, inject a bounded continue nudge (cap ~2/turn). Deliberately small and capped — this is a re-prompt, not an autonomy loop.
  • Poison-item cap: per-item failure counter in the followup queue; after N consecutive failures drop the item with a user-visible notice instead of rescheduling forever.

Sequencing

A → B → C → D. A alone (abort ⇒ release + staleness takeover) removes the biggest amplifier and is small. B removes both false positives and false negatives at the root and deletes three ad-hoc mechanisms. C is the largest but rides existing SQLite/doctor machinery. D is independent polish that closes the loop on "works through to completion". Each track is separately PR-able and separately testable against the invariants.

Alternatives considered

Impact

  • Directly addresses the two most-reported reliability complaints (hung multi-step tasks; dead sessions with silently-queued messages) — the top reason cited by users actively migrating to competing frameworks.
  • Consolidates ~40+ open issues across six clusters into four owned tracks; each track lists the issues it retires so they can be closed with proof as tracks land.
  • Deletes code and concepts on net: three liveness mechanisms → one, scattered lane releases → one transition, in-memory queue → the mandated SQLite store.

Evidence/examples

Cluster → canonical open issues (non-exhaustive; full sweep covered ~160):

Code evidence (verified on current main):

  • src/infra/unhandled-rejections.ts — final unclassified branch exits the process.
  • src/agents/embedded-agent-runner/run/llm-idle-timeout.tsrunTimeoutMs >= MAX_TIMER_TIMEOUT_MS returns 0 (idle watchdog disabled when budget is "unlimited").
  • src/auto-reply/reply/reply-run-registry.ts — takeover restricted to terminal leftovers; no staleness reclaim of a hung "running" op.
  • src/auto-reply/reply/queue/state.tsFOLLOWUP_QUEUES in-memory global map; no persistence.
  • src/utils/queue-helpers.ts + src/auto-reply/reply/queue/drain.ts — item removed only after successful run; no per-item failure cap.

Additional information

Prior art: competing agent frameworks (e.g. Hermes) implement exactly this shape — event-stamped activity heartbeats with per-reasoning-model floors, staged user warning before interrupt, SQLite task claims with lease TTL + dead-worker reclaim, resume_pending sessions that receive a synthesized continuation turn on boot, and bounded anti-premature-stop nudges. Their users cite "never drops a task" as the primary switching reason; this design closes that gap with mechanisms consistent with our architecture rules (SQLite-first state, one canonical path, doctor-owned migration).

Metadata

Metadata

Assignees

Labels

P0Emergency: data loss, security bypass, crash loop, or unusable core runtime.agentsAgent runtime and toolingclawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape 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.enhancementNew feature or requestgatewayGateway runtimeimpact:message-lossChannel message delivery can be lost, duplicated, or misrouted.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.impact:ux-release-blockerA non-technical user is blocked without terminal, logs, config, or support.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.maintainerMaintainer-authored PRmaturity:stableIssue affects a taxonomy feature currently scored M4/M5.

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions