Skip to content

Live CLI session force-closed at run-end kills in-flight background subagents (process-teardown half of #99131; #99139 only spares the session binding) #103024

Description

@jwest75674

Upstream issue draft — live CLI session torn down at run-end kills in-flight background subagents

Repo: openclaw/openclaw
Companion to: PR #99139 (fix(claude-cli): preserve reused session on empty response) and issue #99131.
Status: draft for Josh to file.

Summary

Follow-up to #99131 (which we reported). #99139 fixes the session-binding half of #99131;
this issue is the process-teardown half that #99139 leaves open — the part that actually kills
in-flight background subagents.

When a claude-cli-backed turn ends, OpenClaw force-closes the reused "live session"
(closeClaudeLiveSessionForContextcloseLiveSession(session,"restart")
session.managedRun.cancel()), which cancels that CLI process and its whole tree. Any
background subagents (Task/Agent tool with run_in_background) that the ending turn spawned are
driven by that same live-session process, so they are stranded/killed the instant the turn ends —
they cannot outlive the turn that created them. Elapsed wall-clock is irrelevant: with the driver
gone, a "background" subagent makes zero further progress and is later reaped.

This is the process-layer half of the same family as #99131/#99139. #99139 correctly stops the
persisted session binding from being cleared on empty_response; it does not touch the process
teardown, so the subagent kill (and the self-reinforcing notification-preemption cascade) persists.

Repro

  1. In a session, spawn ≥1 background subagent (run_in_background: true).
  2. End the turn (normal reply, sessions_yield, or let a resumed turn return empty via a queued
    task-notification).
  3. Observe: the subagent processes stop making progress and are torn down; on the interactive path
    the gateway logs claude live session close: … reason=restart at every turn-end (even on
    successful turns), confirming cleanupCliLiveSessionOnRunEnd:true closes the live session
    unconditionally.

Root cause (from dist trace, 2026.6.11)

  • cli-runner's runCliAgentInternal: run-end cleanup calls closeClaudeLiveSessionForContext
    unconditionally when params.cleanupCliLiveSessionOnRunEnd === true, with no awareness of
    runError or of any still-running background children.
  • claude-live-session's session object has no in-flight/background-children counter — only a
    single currentTurn slot (scheduleIdleClose guards on !session.currentTurn, which never
    reflects a background child) and CLAUDE_LIVE_IDLE_TIMEOUT_MS = 600000 (hardcoded 10-min idle
    close, no env override).

Proposed fix (the real one)

Introduce a live-children refcount on the session (increment when a background subagent/child run
attaches to the live session, decrement on its completion) and make all three teardown paths
"drain before close":

  1. run-end cleanup (cleanupCliLiveSessionOnRunEnd) — skip/defer while liveChildren > 0;
  2. scheduleIdleClose — extend the guard from !session.currentTurn to
    !session.currentTurn && liveChildren === 0;
  3. closeLiveSession(reason) — for "idle"/"restart" (not "abort"/error-driven), no-op while
    liveChildren > 0 and re-arm a drain check.

Secondary: make CLAUDE_LIVE_IDLE_TIMEOUT_MS env-configurable.

Interim local mitigations we run (hand-patches, reapplied after each upgrade)

Note

shouldClearReusedCliSessionAfterError returns true for AbortError, so a genuine user
interrupt still clears the session binding (fresh context). If that's undesirable for
interactive sessions, consider sparing AbortError too, or making it configurable.

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