Skip to content

fix(agents): bound abort-path session lock release; force-release on unsettled retained writes#90065

Open
matin wants to merge 1 commit into
openclaw:mainfrom
matin:issue-227-upstream-lock-release
Open

fix(agents): bound abort-path session lock release; force-release on unsettled retained writes#90065
matin wants to merge 1 commit into
openclaw:mainfrom
matin:issue-227-upstream-lock-release

Conversation

@matin

@matin matin commented Jun 3, 2026

Copy link
Copy Markdown

Problem

A turn timeout aborts the embedded run, but the abort-path session-lock release is unbounded. On a deployment running a release that already contains #87278 (release lock on timeout abort), every embedded-run timeout still turned into 5–30 minutes of total unresponsiveness: each subsequent turn failed instantly with SessionWriteLockTimeoutError ("Something went wrong while processing your request"), until the maxHoldMs watchdog finally reclaimed the session .jsonl lock. Starting a fresh session only helped until that session hit its own timeout.

Representative trace:

[agent/embedded] embedded run timeout: runId=… sessionId=… timeoutMs=120000
[agent/embedded] embedded abort settle timed out: runId=… timeoutMs=2000
[diagnostic] lane task error: lane=main durationMs=61545
  error="SessionWriteLockTimeoutError: session file locked (timeout 60000ms): pid=… .../sessions/…jsonl.lock"
Embedded agent failed before reply: session file locked (timeout 60000ms)

The trigger was a flaky provider endpoint that accepts the connection and then hangs the stream (streamGenerateContent that never settles even after runAbortController.abort()), but any provider hang reproduces it.

The gap #87278 left

releaseHeldLockForAbort delegates to the graceful fence release, whose waitForRetainedLockIdle is unbounded. A retained-lock transcript write pinned behind a hung provider stream keeps the retained-use count nonzero forever, so the abort release never reaches lock.release(). #87278 / #88623 / #89811 all release on aborts that settle; nothing covered an abort that a hung provider call never lets settle.

Fix

releaseHeldLockForAbort now runs a bounded variant:

  • Drain acquisition and the retained-idle wait are each raced against the abort settle bound (OPENCLAW_EMBEDDED_ABORT_SETTLE_TIMEOUT_MS, default 2s / 250ms under OPENCLAW_TEST_FAST), overridable per controller via abortReleaseTimeoutMs.
  • Retained writes that settle within the bound keep the existing graceful fence semantics — fence stays active, no behavior change (covered by the existing [Bug]: Session write lock leaked after embedded run timeout during subagent announce #86816 tests).
  • When the bound expires, the underlying file lock is force-released and the controller is poisoned via takeoverDetected, so the aborted run cannot perform torn writes after losing ownership: its later withSessionWriteLock throws EmbeddedAttemptSessionTakeoverError and cleanup degrades to the noop lock — the same posture as a real takeover.
  • If another release path owns the drain and is itself stuck, the abort path takes release ownership directly and hands the eventually-acquired drain straight back (no heldLockDraining leak; dispose() stays unblocked).

Relationship to #89673

Complementary, not overlapping. #89673 is the cross-process, contender-side maxHoldMs reclaim in session-write-lock.ts (let a contender reclaim a lock past the holder's own recorded maxHoldMs). This PR is the in-process abort path in attempt.session-lock.ts: it bounds the damage window at the source so the holder releases promptly on abort, rather than relying on a contender or the watchdog to reclaim minutes later. Both can land; together they close the live-but-stuck-holder hole from both sides.

Abort propagation (cancelling the provider fetch/stream itself) is the other complementary half; the signal plumbing already exists in fetch-guard.ts. This PR bounds the damage window regardless of provider behavior — the chokepoint fix.

Tests

Human-run on my own setup (OPENCLAW_TEST_FAST=1 pnpm vitest run src/agents/embedded-agent-runner/run/attempt.session-lock.test.ts):

  • force-releases the held lock when a retained write never settles — the repro: never-settling retained write, bounded abort release, lock released once, controller poisoned.
  • abort release stays graceful when retained writes settle within the bound — no poisoning on the happy path.
  • Full attempt.session-lock.test.ts suite: 90/90.
  • tsgo typecheck (node scripts/run-tsgo.mjs -p tsconfig.json --noEmit): clean.

AI-assisted (Claude Code). The tests above and the typecheck were run locally against my own OpenClaw instance.

🤖 Generated with Claude Code

…unsettled retained writes (#6)

A turn timeout aborts the run, but releaseHeldLockForAbort delegated to the
graceful fence release, whose waitForRetainedLockIdle is unbounded. A retained
transcript write pinned behind a hung provider stream (one that ignores abort,
e.g. a stalled streamGenerateContent) therefore held the session .jsonl lock
until the maxHoldMs watchdog (5-30 min), and every turn in between failed with
SessionWriteLockTimeoutError.

releaseHeldLockForAbort now races the graceful path against the abort settle
bound (OPENCLAW_EMBEDDED_ABORT_SETTLE_TIMEOUT_MS). When the bound expires the
underlying file lock is force-released and the controller poisoned via
takeoverDetected, so the aborted run cannot perform torn writes after losing
ownership. Retained writes that settle within the bound keep the existing
graceful fence semantics.

Prod incident: tulgey#225 (membrane 2026-06-01..03). Completes the lock-leak
family of openclaw#87278 / openclaw#88623 / openclaw#89811, which release on settled aborts but not on
aborts a hung provider call never lets settle.

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels Jun 3, 2026
@clawsweeper

clawsweeper Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 30, 2026, 9:34 AM ET / 13:34 UTC.

Summary
The branch adds a bounded abort-path session-lock release that force-releases on timeout and adds focused controller tests for never-settling and gracefully settling retained writes.

PR surface: Source +94, Tests +67. Total +161 across 2 files.

Reproducibility: yes. at source level: current main's abort release still waits on retained idle without a bound, and a retained withSessionWriteLock operation that never settles can keep retainedLockUseCount nonzero. I did not run a live gateway reproduction in this read-only review.

Review metrics: 1 noteworthy metric.

  • Abort release mode: 1 bounded force-release path added. This changes abort cleanup from graceful-only release to a timeout-triggered force release, so maintainer review should focus on session-state ownership interleavings.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🧂 unranked krab
Result: blocked until real behavior proof from a real setup is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Fix the drain-ownership branch and refresh the code onto current main's deferred-release controller.
  • [P1] Add redacted after-fix real or real-component proof for hung-provider/session-lock recovery within the bound.

Proof guidance:

  • [P1] Needs real behavior proof before merge: The PR body provides focused Vitest and typecheck output only, not redacted after-fix real or real-component proof for hung-provider/session-lock recovery; screenshots, terminal output, logs, recordings, or linked artifacts should redact private details. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] The PR head is dirty against current main and overlaps later merged session-lock lifecycle work, so the exact merge result needs refresh before review can pass.
  • [P1] The proposed force-release behavior is session-state sensitive because it releases the underlying file lock while retained writes may still be unsettled.
  • [P1] External contributor proof is currently only focused tests and typecheck output, not redacted real or real-component evidence for hung-provider recovery.

Maintainer options:

  1. Repair drain ownership on current main (recommended)
    Rebase or replace the branch so the abort-bound force-release path cannot leave heldLockDraining owned by a stalled release path, with a regression for that interleaving.
  2. Owner decision on force-release semantics
    Maintainers can explicitly accept the session-state policy of poisoning and force-releasing after the abort bound once the drain bug and proof gap are resolved.
  3. Pause behind canonical lock work
    If maintainers want one consolidated session-lock repair, pause this PR until the broader canonical lock and delivery work is split or landed.

Next step before merge

  • [P1] Human review is needed because the PR has a session-state correctness blocker, a dirty head against main, and contributor-gated real behavior proof.

Security
Cleared: No concrete security or supply-chain concern found; the diff is limited to internal session-lock runtime logic and colocated tests.

Review findings

  • [P1] Clear drain ownership before abort-bound takeover returns — src/agents/embedded-agent-runner/run/attempt.session-lock.ts:878-879
Review details

Best possible solution:

Rebuild the bounded abort-release fix on current main, preserve the deferred-release controller behavior, clear or transfer drain ownership before returning from force-release, and add real or real-component proof for the hung-provider case.

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

Yes, at source level: current main's abort release still waits on retained idle without a bound, and a retained withSessionWriteLock operation that never settles can keep retainedLockUseCount nonzero. I did not run a live gateway reproduction in this read-only review.

Is this the best way to solve the issue?

No, not as-is. Bounded abort release is the right owner layer for the reported failure, but the stuck-drain branch, dirty head, and missing real or real-component proof keep this from being the best mergeable solution.

Full review comments:

  • [P1] Clear drain ownership before abort-bound takeover returns — src/agents/embedded-agent-runner/run/attempt.session-lock.ts:878-879
    When beginHeldLockDrain() misses the abort bound because another release path already owns heldLockDraining, this branch force-releases heldLock but leaves heldLockDraining true until that original drain owner finishes. If the original owner is stuck waiting for the same retained write, later writes and cleanup still wait on waitForHeldLockDrain(), so the session can remain blocked even though the file lock was released. Clear or transfer drain ownership before returning and cover that interleaving.
    Confidence: 0.88

Overall correctness: patch is incorrect
Overall confidence: 0.86

AGENTS.md: found and applied where relevant.

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

Label changes

Label justifications:

  • P1: The PR targets stuck embedded-run session locks that can make later agent and channel turns fail until watchdog or manual recovery.
  • merge-risk: 🚨 session-state: The diff changes session write-lock ownership and takeover behavior while retained transcript writes may still be pending.
  • merge-risk: 🚨 availability: A stuck drain interleaving can keep cleanup or later writes waiting even after the underlying file lock is force-released.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR body provides focused Vitest and typecheck output only, not redacted after-fix real or real-component proof for hung-provider/session-lock recovery; screenshots, terminal output, logs, recordings, or linked artifacts should redact private details. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +94, Tests +67. Total +161 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 95 1 +94
Tests 1 67 0 +67
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 162 1 +161

What I checked:

Likely related people:

  • xialonglee: Authored the merged retained-lock deferred-release PR that now owns the nearest current-main code around active-scope abort cleanup. (role: recent adjacent fix author; confidence: high; commits: 0a042f68dfbe; files: src/agents/embedded-agent-runner/run/attempt.session-lock.ts, src/agents/embedded-agent-runner/run/attempt.session-lock.test.ts)
  • openperf: Authored the merged timeout-abort session-lock release PR that this PR explicitly builds on, and authored the adjacent open maxHoldMs reclaim PR. (role: prior timeout-abort fix author; confidence: high; commits: 65fb56513fb2, 25db525072d7; files: src/agents/embedded-agent-runner/run/attempt.ts, src/agents/embedded-agent-runner/run/attempt.session-lock.ts, src/agents/session-write-lock.ts)
  • steipete: Authored the merged manual-abort session-lock release path and recent adjacent session-write-lock stale/retry policy changes. (role: recent abort/session-lock contributor; confidence: high; commits: 95890fe15063; files: src/agents/embedded-agent-runner/run/attempt-abort.ts, src/agents/embedded-agent-runner/run/attempt.session-lock.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (1 earlier review cycle)
  • reviewed 2026-06-21T13:29:50.287Z sha 817dcd6 :: needs real behavior proof before merge. :: [P1] Clear drain ownership before abort-bound takeover returns

@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. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. labels Jun 4, 2026
@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added stale Marked as stale due to inactivity triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. and removed stale Marked as stale due to inactivity labels Jul 20, 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 merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P1 High-priority user-facing bug, regression, or broken workflow. 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. triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants