Skip to content

fix: prevent orphaned session locks after cron job completion#31867

Closed
YUJIE2002 wants to merge 4 commits into
openclaw:mainfrom
YUJIE2002:fix/session-lock-race-condition
Closed

fix: prevent orphaned session locks after cron job completion#31867
YUJIE2002 wants to merge 4 commits into
openclaw:mainfrom
YUJIE2002:fix/session-lock-race-condition

Conversation

@YUJIE2002

Copy link
Copy Markdown
Contributor

Problem

Session lock files are not being released after cron job completion, causing complete system paralysis. Lock is never released even when isError=false.

Root Cause

  • Lock release logic not guaranteed on all code paths
  • No protection against stale locks blocking the system
  • Missing error handling around lock.release()

Fix

  1. session-write-lock.ts: Add LOCK_ACQUISITION_REJECT_THRESHOLD_MS (5min) to reject locks held too long
  2. store.ts: Add comprehensive logging for lock acquire/release, improve error handling
  3. attempt.ts & compact.ts: Wrap lock.release() in try-catch with error logging

Testing

  • Build passes
  • session-write-lock tests pass (14 tests)

Closes #31749

…annel

When sessions_send dispatches a message, it uses channel='webchat'
(INTERNAL_MESSAGE_CHANNEL) as the transport. The agent method handler
in server-methods/agent.ts would fall back to this transport channel
when building the session entry patch (entry?.channel ?? request.channel),
overwriting the session's real channel (e.g. 'telegram') to 'webchat'.

This broke subsequent message delivery — the Telegram session would
stop receiving messages because the routing layer saw it as a 'webchat'
session.

Fix: When inputProvenance.kind is 'inter_session', do not use the
request's channel as a fallback for the session channel field, and do
not propagate it as the turn's source channel for delivery routing.
The internal transport channel is now treated as opaque — it never
leaks into the session's identity or delivery metadata.

Closes openclaw#31671
When entry.channel is absent during an inter-session send, the previous
code wrote channel: undefined into the patch. mergeSessionEntry uses
object spread ({...existing, ...patch}), so an explicit undefined value
clobbers a concurrently written real channel. Fix by conditionally
spreading the channel field — omit it entirely when there is nothing
to preserve.

Addresses review feedback from Codex (P1).
- Add LOCK_ACQUISITION_REJECT_THRESHOLD_MS to reject locks held >5min
- Add comprehensive logging for lock acquire/release in store.ts
- Add try-catch around lock.release() in attempt.ts and compact.ts
- Fixes race condition where locks weren't released after cron runs
@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime agents Agent runtime and tooling size: S labels Mar 2, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 01170c29dd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/session-write-lock.ts Outdated
Comment on lines +488 to +492
if (inspected.ageMs !== null && inspected.ageMs > LOCK_ACQUISITION_REJECT_THRESHOLD_MS) {
const owner = typeof payload?.pid === "number" ? `pid=${payload.pid}` : "unknown";
throw new Error(
`session file locked by ${owner} for ${Math.round(inspected.ageMs / 1000)}s (rejecting acquisition, threshold: ${LOCK_ACQUISITION_REJECT_THRESHOLD_MS}ms): ${lockPath}`,
{ cause: err },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reclaim stale lock files before aborting acquisition

This guard throws immediately when a contended lock is older than 5 minutes, but it executes before the existing stale-lock recovery path (shouldReclaimContendedLockFile) can run. If a crashed process leaves behind an old lock file (the exact scenario this code is trying to recover from), acquisition now always fails fast and the stale lock is never removed, which can block subsequent session writes until someone manually cleans up the lock.

Useful? React with 👍 / 👎.

@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label Mar 2, 2026
@greptile-apps

greptile-apps Bot commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR bundles three fixes: session lock management improvements (main focus), and channel corruption prevention for inter-session messages. The lock changes add error handling around lock.release() calls with try-catch blocks and comprehensive logging, which is good. However, there's a critical logic error in session-write-lock.ts where the new 5-minute age threshold check blocks reclamation of orphaned locks with dead PIDs, potentially causing permanent deadlocks. The channel handling fixes in agent.ts correctly prevent sessions_send from corrupting target session channels by preserving existing channels and omitting undefined values from patches.

Confidence Score: 1/5

  • This PR contains a critical logic bug that could cause system-wide deadlocks
  • The 5-minute age threshold in session-write-lock.ts prevents orphaned locks (with dead PIDs) from being reclaimed, creating permanent session blocks - the exact issue the PR aims to fix. The try-catch improvements are good, but the core lock acquisition logic needs correction before merge.
  • Critical issue in src/agents/session-write-lock.ts lines 487-494 must be resolved

Last reviewed commit: 69cff09

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread src/agents/session-write-lock.ts Outdated
Comment on lines +487 to +494
// Check if lock is too old to even attempt acquisition - prevent deadlock
if (inspected.ageMs !== null && inspected.ageMs > LOCK_ACQUISITION_REJECT_THRESHOLD_MS) {
const owner = typeof payload?.pid === "number" ? `pid=${payload.pid}` : "unknown";
throw new Error(
`session file locked by ${owner} for ${Math.round(inspected.ageMs / 1000)}s (rejecting acquisition, threshold: ${LOCK_ACQUISITION_REJECT_THRESHOLD_MS}ms): ${lockPath}`,
{ cause: err },
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reject threshold blocks reclamation of orphaned locks with dead PIDs

The age check happens before shouldReclaimContendedLockFile, preventing locks with dead PIDs from being reclaimed. If a process crashes leaving a 6-minute-old lock, this throws an error instead of cleaning it up via the existing reclaim logic (line 496-498), permanently blocking the session.

Fix: only reject if inspected.pidAlive === true, or move this check after the reclaim logic

Suggested change
// Check if lock is too old to even attempt acquisition - prevent deadlock
if (inspected.ageMs !== null && inspected.ageMs > LOCK_ACQUISITION_REJECT_THRESHOLD_MS) {
const owner = typeof payload?.pid === "number" ? `pid=${payload.pid}` : "unknown";
throw new Error(
`session file locked by ${owner} for ${Math.round(inspected.ageMs / 1000)}s (rejecting acquisition, threshold: ${LOCK_ACQUISITION_REJECT_THRESHOLD_MS}ms): ${lockPath}`,
{ cause: err },
);
}
// Check if lock is too old to even attempt acquisition - prevent deadlock
if (inspected.ageMs !== null && inspected.ageMs > LOCK_ACQUISITION_REJECT_THRESHOLD_MS && inspected.pidAlive) {
const owner = typeof payload?.pid === "number" ? `pid=${payload.pid}` : "unknown";
throw new Error(
`session file locked by ${owner} for ${Math.round(inspected.ageMs / 1000)}s (rejecting acquisition, threshold: ${LOCK_ACQUISITION_REJECT_THRESHOLD_MS}ms): ${lockPath}`,
{ cause: err },
);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/session-write-lock.ts
Line: 487-494

Comment:
Reject threshold blocks reclamation of orphaned locks with dead PIDs

The age check happens before `shouldReclaimContendedLockFile`, preventing locks with dead PIDs from being reclaimed. If a process crashes leaving a 6-minute-old lock, this throws an error instead of cleaning it up via the existing reclaim logic (line 496-498), permanently blocking the session.

Fix: only reject if `inspected.pidAlive === true`, or move this check after the reclaim logic

```suggestion
      // Check if lock is too old to even attempt acquisition - prevent deadlock
      if (inspected.ageMs !== null && inspected.ageMs > LOCK_ACQUISITION_REJECT_THRESHOLD_MS && inspected.pidAlive) {
        const owner = typeof payload?.pid === "number" ? `pid=${payload.pid}` : "unknown";
        throw new Error(
          `session file locked by ${owner} for ${Math.round(inspected.ageMs / 1000)}s (rejecting acquisition, threshold: ${LOCK_ACQUISITION_REJECT_THRESHOLD_MS}ms): ${lockPath}`,
          { cause: err },
        );
      }
```

How can I resolve this? If you propose a fix, please make it concise.

- hooks.md: update bootstrap file list to include all 5 files (AGENTS, SOUL, TOOLS, IDENTITY, USER)
- session-write-lock.ts: only reject acquisition if lock >5min AND PID is alive
  - If PID is dead (orphaned lock), allow reclamation instead of permanent deadlock
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 gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 Session lock files not released after cron job completion (race condition)

1 participant