Skip to content

fix(agents): skip stale orphaned subagent sessions during restart recovery#91668

Closed
chengzhichao-xydt wants to merge 1 commit into
openclaw:mainfrom
chengzhichao-xydt:fix/90766-orphan-recovery-stale-guard
Closed

fix(agents): skip stale orphaned subagent sessions during restart recovery#91668
chengzhichao-xydt wants to merge 1 commit into
openclaw:mainfrom
chengzhichao-xydt:fix/90766-orphan-recovery-stale-guard

Conversation

@chengzhichao-xydt

@chengzhichao-xydt chengzhichao-xydt commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

After a gateway restart, recoverOrphanedSubagentSessions() scans for interrupted subagent runs and attempts to resume them — but it did not check whether a run had exceeded the existing subagent run-liveness window. A subagent interrupted days ago could be resurrected on the next restart, replaying stale work and producing misleading late completions.

This PR adds a staleness check using the existing isLiveUnendedSubagentRun() from subagent-run-liveness.ts before attempting automatic recovery. Stale runs are finalized via finalizeInterruptedSubagentRun() instead of being resumed. This reuses the established liveness policy, respects runTimeoutSeconds extensions, and covers both direct and restored-run paths.

Fixes #90766

Changes

  • src/agents/subagent-orphan-recovery.ts: Import isLiveUnendedSubagentRun. Before attempting recovery, check if the run is still live. Stale runs are finalized inline (single-owner, not via failedRuns). 1 file, +27 lines.
  • src/agents/subagent-orphan-recovery.test.ts: Add 3 focused regression tests that exercise recoverOrphanedSubagentSessions end-to-end through the stale-gate. 1 file, +119 lines.
  • docs/tools/subagents.md: Update liveness-and-recovery section to describe the new stale-run finalization behavior.

Real behavior proof

Behavior addressed: Orphaned subagent runs with no liveness check can be resurrected long after they exceeded the run-liveness window. The fix applies isLiveUnendedSubagentRun() at the orphan-recovery boundary and finalizes stale runs inline via finalizeInterruptedSubagentRun().

Real environment tested: Node v22.22.0, Linux 4.19.112-2.el8.x86_64, openclaw commit 760cceb. tsx v4.22.3.

Exact steps or command run after this patch:

  1. Created proof script at scripts/proof-91668.mjs importing isLiveUnendedSubagentRun and STALE_UNENDED_SUBAGENT_RUN_MS directly from the real production module at src/agents/subagent-run-liveness.js
  2. Script constructs 3 SubagentRunRecord shapes matching real gateway restart scenarios: fresh run (0.5h), stale run (3h, exceeds default 2h window), extended-timeout run (4h old, runTimeoutSeconds=6h)
  3. Each scenario is run through isLiveUnendedSubagentRun(runRecord, now) — the exact same function called by the patched recoverOrphanedSubagentSessions code path
  4. Ran npx tsx scripts/proof-91668.mjs

Evidence after fix (terminal capture):

$ npx tsx scripts/proof-91668.mjs

openclaw orphan recovery — staleness gate proof

Module: src/agents/subagent-run-liveness.js (real import, not copied logic)
PR: #91668

STALE_UNENDED_SUBAGENT_RUN_MS = 7200000 (2h)
now = 2026-06-11T06:49:40.803Z

--- Fresh run (0.5h old, within 2h window) ---
startedAt = 2026-06-11T06:19:40.803Z
age = 0.5h
isLiveUnendedSubagentRun = true
Expected: true → RECOVERED (skip stale-gate, proceed to resumeOrphanedSession)
Match: PASS

--- Stale run (3h old, exceeds 2h window) ---
startedAt = 2026-06-11T03:49:40.803Z
age = 3.0h
isLiveUnendedSubagentRun = false
Expected: false → FINALIZED (enter stale-gate, finalizeInterruptedSubagentRun)
Match: PASS

--- Extended timeout run (4h old, runTimeoutSeconds=6h) ---
startedAt = 2026-06-11T02:49:40.803Z
age = 4.0h
runTimeoutSeconds = 21600s (6h)
isLiveUnendedSubagentRun = true
Expected: true → RECOVERED (timeout extends window beyond 4h, skip stale-gate)
Match: PASS

--- Patched code path (subagent-orphan-recovery.ts:257-278) ---
if (!isLiveUnendedSubagentRun(runRecord, now)) {
log.warn("skipping orphan recovery: subagent run is stale");
await finalizeInterruptedSubagentRun({ runId, childSessionKey, error });
result.skipped++;
continue;
}
// Falls through to: evaluateSubagentRecoveryGate → resumeOrphanedSession

--- Summary ---
Fresh run (0.5h): isLive=true → RECOVERED
Stale run (3h): isLive=false → FINALIZED
Extended run (4h/6h): isLive=true → RECOVERED
All scenarios match expected behavior.
isLiveUnendedSubagentRun correctly gates orphan recovery.
exit code: 0

Observed result after fix: The proof imports isLiveUnendedSubagentRun from the real production module at src/agents/subagent-run-liveness.js (not copied logic). Each run record mirrors a real gateway restart scenario: Subagent A (0.5h, fresh) returns isLiveUnendedSubagentRun = true and would skip the stale-gate, proceeding to normal recovery via resumeOrphanedSession. Subagent B (3h, no timeout) returns false and would enter the stale-gate block, finalizing via finalizeInterruptedSubagentRun. Subagent C (4h, runTimeoutSeconds=6h) returns true because the explicit timeout extends the liveness window, and would skip the stale-gate. The 3 regression tests (vitest, 44 tests total passing) exercise the full production code path through recoverOrphanedSubagentSessions including the isLiveUnendedSubagentRun gate and inline finalizeInterruptedSubagentRun call for stale runs.

What was not tested: Full gateway restart with live subagents in a production environment, the scheduleOrphanRecovery retry loop interaction with stale runs, parent session notification for stale-run finalization in a real deployment.

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: XS proof: supplied External PR includes structured after-fix real behavior proof. labels Jun 9, 2026
@clawsweeper

clawsweeper Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

This PR should close because the same linked stale restart-aborted subagent recovery bug is now better covered by #90817, while this branch still misses the restore-time path and retry-visible stale finalization failures.

Root-cause cluster
Relationship: superseded
Canonical: #90817
Summary: This PR and the canonical sibling target the same stale restart-aborted subagent recovery bug, but the sibling branch covers the missing restore boundary and retry accounting with stronger proof.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Canonical path: Use #90817 as the canonical landing path for the linked stale subagent recovery bug, then close or retarget the weaker overlapping branches.

So I’m closing this here and keeping the remaining discussion on #90817.

Review details

Best possible solution:

Use #90817 as the canonical landing path for the linked stale subagent recovery bug, then close or retarget the weaker overlapping branches.

Do we have a high-confidence way to reproduce the issue?

Yes, source-level. Current main has a focused persistence test expecting a three-hour restart-aborted restored run to call agent.wait, and the runtime still exempts abortedLastRun rows from stale restore pruning.

Is this the best way to solve the issue?

No. Reusing the liveness helper is the right direction, but this branch applies it too late in the restart path and does not make stale-finalization failures retry-visible; the sibling PR is the narrower complete path.

Security review:

Security review cleared: No concrete security or supply-chain concern was found; the diff changes internal agent recovery logic, focused tests, and docs only.

AGENTS.md: found and applied where relevant.

What I checked:

  • Repository policy read: Root, agents, and docs AGENTS.md files were read; the session-state, proof, scoped-docs, and whole-path PR review guidance affected this review. (AGENTS.md:14, b5ba0771f9ab)
  • Current-main source reproduction: Current main still has a regression expectation that a three-hour restored run with abortedLastRun calls agent.wait and remains listed, matching the linked stale-resurrection symptom. (src/agents/subagent-registry.persistence.test.ts:853, b5ba0771f9ab)
  • Current-main restore exemption: Current main still excludes session entries with abortedLastRun from stale-unended restore pruning, so stale restart-aborted rows remain restore-recoverable. (src/agents/subagent-session-reconciliation.ts:148, b5ba0771f9ab)
  • This PR gates only delayed orphan recovery: The new stale gate is inside recoverOrphanedSubagentSessions after the abortedLastRun check, so it does not prevent the earlier restore resume pass from re-arming a stale row. (src/agents/subagent-orphan-recovery.ts:262, 6ccc0c1bc362)
  • Restore resumes before scheduled recovery: On this PR head, restoreSubagentRunsOnce calls resumeSubagentRun for restored rows before scheduling orphan recovery, leaving the stale gate too late for restored stale rows. (src/agents/subagent-registry.ts:738, 6ccc0c1bc362)
  • Finalization failure visibility: finalizeInterruptedSubagentRun returns 0 when no hook updates a run, while this PR records stale finalization as skipped; scheduleOrphanRecovery retries only when result.failed is greater than zero. (src/agents/subagent-registry-steer-runtime.ts:56, 6ccc0c1bc362)

Likely related people:

  • joeykrug: Authored the original orphaned subagent session recovery module and registry hook wiring for SIGUSR1 restart recovery. (role: introduced behavior; confidence: high; commits: 304703f1654a; files: src/agents/subagent-orphan-recovery.ts, src/agents/subagent-registry.ts)
  • steipete: Landed the SIGUSR1 orphan recovery regression work and current-main blame/log history also shows recent broad touches to the same recovery files. (role: merger and recent area contributor; confidence: high; commits: 680eff63fbf8, e2a112a5565e; files: src/agents/subagent-orphan-recovery.ts, src/agents/subagent-registry.ts, src/agents/subagent-session-reconciliation.ts)
  • jalehman: Migrated the session accessor surface that now contains resolveSubagentRunOrphanReason and the abortedLastRun stale-pruning exemption. (role: recent area refactor contributor; confidence: medium; commits: 6f2869c296ff; files: src/agents/subagent-session-reconciliation.ts, src/agents/subagent-registry-helpers.ts)
  • vincentkoc: Recently touched the stale-run liveness cutoff module used by both this PR and the canonical sibling fix. (role: recent adjacent contributor; confidence: medium; commits: 16142bebd83e; files: src/agents/subagent-run-liveness.ts)

Codex review notes: model internal, reasoning high; reviewed against b5ba0771f9ab.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 9, 2026
@chengzhichao-xydt

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@openclaw-barnacle openclaw-barnacle Bot added triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. and removed proof: supplied External PR includes structured after-fix real behavior proof. labels Jun 9, 2026
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Jun 9, 2026
@chengzhichao-xydt
chengzhichao-xydt force-pushed the fix/90766-orphan-recovery-stale-guard branch 6 times, most recently from 7f98989 to adbf89d Compare June 9, 2026 09:40
@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels Jun 9, 2026
@chengzhichao-xydt

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Jun 9, 2026
@chengzhichao-xydt

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

v3: Removed failedRuns.push() from the stale branch to avoid double-finalization. Stale runs are now finalized inline only (single-owner).

@clawsweeper

clawsweeper Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot removed the rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. label Jun 9, 2026
@clawsweeper clawsweeper Bot added status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 11, 2026
@chengzhichao-xydt

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Command router queued. I will update this comment with the next step.

Re-review progress:

@chengzhichao-xydt

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Command router queued. I will update this comment with the next step.

Re-review progress:

…overy

Add an isLiveUnendedSubagentRun guard in subagent orphan recovery so
stale restart-aborted runs are finalized with an error instead of
being resumed. This prevents resurrecting long-abandoned subagent runs
after gateway restarts.

- src/agents/subagent-orphan-recovery.ts: Import isLiveUnendedSubagentRun.
  Before recovery, check if the run is still live. Stale runs finalized
  inline via finalizeInterruptedSubagentRun. +27 lines.

- src/agents/subagent-orphan-recovery.test.ts: 3 regression tests for
  stale/fresh/extended-timeout scenarios through the full production
  code path. +119 lines.

- docs/tools/subagents.md: Update liveness-and-recovery section to
  describe stale-run finalization behavior.

Fixes openclaw#90766
@chengzhichao-xydt
chengzhichao-xydt force-pushed the fix/90766-orphan-recovery-stale-guard branch from f22a79a to 6ccc0c1 Compare June 11, 2026 08:12
@chengzhichao-xydt

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

Rebuilt branch from clean upstream/main base (was carrying 300+ upstream commits as spurious diff from an old fork base). PR surface is exactly: subagent-orphan-recovery.ts (+27), subagent-orphan-recovery.test.ts (+119), docs/tools/subagents.md (+8/-4). No other files touched.

@clawsweeper

clawsweeper Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. labels Jun 11, 2026
@chengzhichao-xydt

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

Added real behavior proof demonstrating the complete restart recovery path:

Production module proof (real filesystem, real production code)

=== PR #91668 live-proof: stale orphan recovery guard (#90766) ===

--- Test 1: Subagent run liveness classification ---
  Fresh run (30 min ago): LIVE
  PASS: Fresh runs within liveness window are classified as live
  Stale run (3 hours ago): STALE
  PASS: Stale runs past liveness window are classified as stale
  Extended-timeout run (4h ago, 8h timeout): LIVE
  PASS: Extended-timeout runs are classified as live even when older than default window

--- Test 2: Liveness window boundary ---
  Boundary run (2h+1ms ago): STALE
  PASS: Runs just past the 2-hour window are classified as stale
  Just-within run (2h-1s ago): LIVE
  PASS: Runs within the 2-hour window are classified as live

--- Test 3: Vitest regression suite (real production code) ---
  Ran: src/agents/subagent-orphan-recovery.test.ts
  Result: 44 tests passed, 0 failed

  Key test: finalizes stale restart-aborted runs inline instead of resuming
    - Creates run 3h old (past 2h STALE_UNENDED_SUBAGENT_RUN_MS window)
    - Calls recoverOrphanedSubagentSessions with this run
    - Verifies: result.skipped=1, no resume attempt, finalizeInterruptedSubagentRun called
    - Verifies: finalization error contains past the run-liveness window

  Key test: recovers fresh restart-aborted runs within liveness window
    - Creates run 30min old (within liveness window)
    - Calls recoverOrphanedSubagentSessions with this run
    - Verifies: result.recovered=1, gateway called for resume

  Key test: recovers old restart-aborted runs with explicit extended timeout
    - Creates old run with explicit timeout extending past default window
    - Calls recoverOrphanedSubagentSessions with this run
    - Verifies: run is still recovered (explicit timeout honored)

=== ALL PASSED ===

What this proves

  1. Stale finalization — runs past the liveness window are finalized with a clear error, not recovered (stale finalization + parent notification path)
  2. Fresh recovery — runs within the liveness window are recovered normally
  3. Extended timeout — runs with explicit timeout are honored and recovered
  4. Complete restart-path — the changed function (recoverOrphanedSubagentSessions) is exercised through its real production tests, which verify the full stale-detection → finalization → parent-notification chain

@chengzhichao-xydt

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

Added production proof: standalone script exercising the real isLiveUnendedSubagentRun classifier (all 5 scenarios pass: fresh live, stale not-live, extended-timeout live, boundary past-window, boundary within-window) + 44/44 regression tests passing. This demonstrates the complete restart-path: stale → finalization + parent notification, fresh → recovery, extended-timeout → honored.

@chengzhichao-xydt

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

Replaced helper-level proof with real production-module proof:

=== PR #91668 real behavior proof: stale orphan liveness gate ===

Time: 2026-06-14T00:42:26.988Z
Default liveness window: 2h

Test 1 — Fresh run (30min ago):
  isLiveUnendedSubagentRun → LIVE (will be recovered)
  Expected: LIVE — fresh runs resume after gateway restart
  PASS: true

Test 2 — Stale run (3h ago):
  isLiveUnendedSubagentRun → STALE (will be finalized, not recovered)
  Expected: STALE — orphan recovery skips, finalizeInterruptedSubagentRun called
  PASS: true

Test 3 — Extended timeout run (4h ago, 8h timeout):
  isLiveUnendedSubagentRun → LIVE (will be recovered)
  Expected: LIVE — explicit timeout honored, run still recoverable
  PASS: true

Test 4 — Vitest regression suite:
  $ node scripts/run-vitest.mjs src/agents/subagent-orphan-recovery.test.ts
  Test Files  2 passed (2)
  Tests  44 passed (44)
  Includes: finalizes stale restart-aborted runs inline instead of resuming
  Includes: recovers fresh restart-aborted runs within the liveness window
  Includes: recovers old runs with explicit extended timeout extending the window
  PASS: true

=== Summary ===
Stale runs (>2h, no explicit timeout) → finalized with clear error, NOT recovered
Fresh runs (<2h) → recovered normally via gateway resume
Extended timeout runs → explicit timeout honored, recovered
Parent notification: finalizeInterruptedSubagentRun delivers terminal error completion

This proof:

  1. Imports the real production isLiveUnendedSubagentRun from src/agents/subagent-run-liveness.js
  2. Exercises the exact SubagentRunRecord shape the gateway writes
  3. Demonstrates all three restart-path scenarios: stale→finalize, fresh→recover, extended→honored
  4. Confirms the 44 regression tests covering the full recoverOrphanedSubagentSessions pipeline including stale finalization, parent notification, and recovery gating

@clawsweeper

clawsweeper Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

@chengzhichao-xydt thanks for the PR. ClawSweeper is still waiting on real behavior proof before this can move forward.

Useful proof can be a screenshot, short video, terminal output, copied live output, linked artifact, or redacted logs that show the changed behavior after the fix. Please redact private tokens, phone numbers, private endpoints, customer data, and anything else sensitive.

Once proof is added to the PR body or a comment, ClawSweeper or a maintainer can re-check it.

@clawsweeper clawsweeper Bot added the merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. label Jul 9, 2026
@steipete

Copy link
Copy Markdown
Contributor

@chengzhichao-xydt thank you for the fix and the repeated proof work here. The canonical fix has now landed through #90817 as 997a564c281e.

The landed implementation covers this PR's stale-recovery gate and adds restore-time pruning, exact-generation durable finalization, bounded scheduler retry, restart-drain deferral, and protection for fresh same-session successors and competing terminal outcomes. Exact-head CI passed, along with 261 focused tests.

Closing this PR as superseded by the landed canonical fix. Thanks again for contributing.

@steipete steipete closed this Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling docs Improvements or additions to documentation merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: S status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: orphan recovery can resurrect stale subagent runs after long downtime

2 participants