openclaw - ✅(Solved) Fix sessions.patch modelOverride lost due to stale-cache race in agent handler [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#52786Fetched 2026-04-08 01:19:27
View on GitHub
Comments
1
Participants
2
Timeline
2
Reactions
0
Timeline (top)
commented ×1cross-referenced ×1

Follow-up to #5369 (now closed/locked). The underlying root cause — a stale-cache race condition in the agent handler's session entry construction — appears to still be present on main.

The issue is intermittent (~3% reproduction rate) because it depends on timing between sessions.patch writing modelOverride to the store and the agent handler reading a cached (potentially stale) entry.

Root Cause

When the agent handler runs for a spawned sub-agent session:

  1. loadSessionEntry(key) returns a cached entry (may be stale — no modelOverride)
  2. nextEntry is built from that stale entry, including modelOverride: entry?.modelOverrideundefined
  3. updateSessionStore() acquires a lock and loads the store with skipCache: true (fresh from disk — has modelOverride from sessions.patch)
  4. But the mutator ignores the fresh store data and writes store[key] = nextEntry — overwriting the fresh modelOverride with the stale undefined

Relevant lines on current main in src/gateway/server-methods/agent.ts:

// Line 214: stale cached read
const { cfg, storePath, entry, canonicalKey } = loadSessionEntry(requestedSessionKey);

// Line 252: nextEntry built from stale entry
modelOverride: entry?.modelOverride,

// Lines 284-286: mutator ignores fresh store, writes stale nextEntry
await updateSessionStore(storePath, (store) => {
  store[canonicalSessionKey] = nextEntry;
});

Fix Action

Fix

PR #19328 moves session entry construction inside the updateSessionStore mutator callback, reading from freshEntry = store[primaryKey] (which was loaded with skipCache: true) instead of the stale entry. This guarantees modelOverride, providerOverride, and all other fields reflect the latest writes from sessions.patch.

PR fix notes

PR #19328: Fix: preserve modelOverride in agent handler (#5369)

Description (problem / solution / changelog)

Summary

Fixes the intermittent race condition where sub-agent model overrides were silently ignored (~3% failure rate).

  • When sessions_spawn sets a model override via sessions.patch, the agent handler could read stale cached data and overwrite the fresh modelOverride when writing back to the session store
  • The fix moves session entry construction inside the updateSessionStore mutator, ensuring it always uses fresh store data (loaded with skipCache: true)

Root Cause

The agent handler was:

  1. Reading session entry early via loadSessionEntry() (could return cached/stale data)
  2. Building nextEntry with modelOverride: entry?.modelOverride (potentially undefined)
  3. Calling updateSessionStore() which correctly re-reads with skipCache: true
  4. But the mutator ignored the fresh store data and just wrote store[key] = nextEntry

This meant freshly-written modelOverride values from sessions.patch could be overwritten.

Changes

  • src/gateway/server-methods/agent.ts: Build session entry inside updateSessionStore using fresh store data
  • src/gateway/server-methods/agent.test.ts: Add 4 comprehensive tests for the fix

Test plan

  • Existing tests updated and passing
  • New test: issue #5369: agent handler preserves fresh modelOverride from store
  • New test: issue #5369: request params override fresh store values for label/spawnedBy
  • New test: issue #5369: creates new entry when store has no existing entry
  • New test: issue #5369: preserves all important fields from fresh store
  • Full test suite passes (5,043 tests)

Fixes #5369

Supersedes #8398 (rebased onto latest main to resolve conflicts).

<!-- greptile_comment --> <h3>Greptile Summary</h3>

Fixes race condition where sub-agent modelOverride values set via sessions.patch could be silently overwritten. The agent handler previously built session entries using potentially stale cached data, then overwrote fresh store values. The fix moves session entry construction inside the updateSessionStore mutator, ensuring it always reads and uses fresh store data (loaded with skipCache: true).

  • Moved session entry construction from outside to inside updateSessionStore mutator (src/gateway/server-methods/agent.ts:391-443)
  • Uses freshEntry from store instead of stale entry for all fields that sessions.patch can modify
  • Adds proper sendPolicy denial handling inside the mutator with structured return value
  • Handles both storePath and no-storePath (fallback) cases correctly
  • Added 4 comprehensive regression tests specifically for issue #5369
  • Updated existing tests to handle new mutator return value structure
<h3>Confidence Score: 5/5</h3>
  • Safe to merge - well-tested fix for documented race condition
  • The fix correctly addresses the root cause by ensuring session entry construction uses fresh store data. The implementation is clean, properly handles edge cases (storePath vs no-storePath, sendPolicy denial), and includes comprehensive test coverage specifically targeting the race condition. The changes are localized to the affected code path with minimal risk of side effects.
  • No files require special attention

<sub>Last reviewed commit: ffe4cde</sub>

<!-- greptile_other_comments_section -->

<sub>(5/5) You can turn off certain types of comments like style here!</sub>

<!-- /greptile_comment -->

Changed files

  • src/gateway/server-methods/agent-stale-cache-race.test.ts (added, +273/-0)
  • src/gateway/server-methods/agent.test.ts (modified, +269/-25)
  • src/gateway/server-methods/agent.ts (modified, +118/-59)

Code Example

// Line 214: stale cached read
const { cfg, storePath, entry, canonicalKey } = loadSessionEntry(requestedSessionKey);

// Line 252: nextEntry built from stale entry
modelOverride: entry?.modelOverride,

// Lines 284-286: mutator ignores fresh store, writes stale nextEntry
await updateSessionStore(storePath, (store) => {
  store[canonicalSessionKey] = nextEntry;
});
RAW_BUFFERClick to expand / collapse

Summary

Follow-up to #5369 (now closed/locked). The underlying root cause — a stale-cache race condition in the agent handler's session entry construction — appears to still be present on main.

The issue is intermittent (~3% reproduction rate) because it depends on timing between sessions.patch writing modelOverride to the store and the agent handler reading a cached (potentially stale) entry.

Root cause

When the agent handler runs for a spawned sub-agent session:

  1. loadSessionEntry(key) returns a cached entry (may be stale — no modelOverride)
  2. nextEntry is built from that stale entry, including modelOverride: entry?.modelOverrideundefined
  3. updateSessionStore() acquires a lock and loads the store with skipCache: true (fresh from disk — has modelOverride from sessions.patch)
  4. But the mutator ignores the fresh store data and writes store[key] = nextEntry — overwriting the fresh modelOverride with the stale undefined

Relevant lines on current main in src/gateway/server-methods/agent.ts:

// Line 214: stale cached read
const { cfg, storePath, entry, canonicalKey } = loadSessionEntry(requestedSessionKey);

// Line 252: nextEntry built from stale entry
modelOverride: entry?.modelOverride,

// Lines 284-286: mutator ignores fresh store, writes stale nextEntry
await updateSessionStore(storePath, (store) => {
  store[canonicalSessionKey] = nextEntry;
});

Fix

PR #19328 moves session entry construction inside the updateSessionStore mutator callback, reading from freshEntry = store[primaryKey] (which was loaded with skipCache: true) instead of the stale entry. This guarantees modelOverride, providerOverride, and all other fields reflect the latest writes from sessions.patch.

Reproduction

A standalone test (agent-stale-cache-race.test.ts in PR #19328) reproduces both the vulnerable and fixed code patterns without requiring the full module chain. It confirms:

  • main's pattern drops modelOverride when the cache is stale
  • The fix pattern preserves modelOverride from the fresh store
  • When cache and store happen to agree, both patterns work identically (explains the intermittent nature)

Environment

Same as #5369 — affects any version where the agent handler builds nextEntry outside the updateSessionStore mutator.

extent analysis

Fix Plan

To resolve the stale-cache race condition, we need to move the session entry construction inside the updateSessionStore mutator callback. Here are the steps:

  • Update src/gateway/server-methods/agent.ts to load the fresh entry from the store within the updateSessionStore callback.
  • Use the fresh entry to build nextEntry, ensuring that modelOverride and other fields reflect the latest writes.

Example Code

await updateSessionStore(storePath, (store) => {
  const freshEntry = store[primaryKey];
  const nextEntry = {
    // ...
    modelOverride: freshEntry?.modelOverride,
    // ...
  };
  store[canonicalSessionKey] = nextEntry;
});

Replace the existing nextEntry construction with the above code, using the freshEntry loaded from the store with skipCache: true.

Verification

To verify the fix, run the standalone test agent-stale-cache-race.test.ts and confirm that:

  • The fixed code pattern preserves modelOverride from the fresh store.
  • The test reproduces the expected behavior for both the vulnerable and fixed code patterns.

Extra Tips

  • Ensure that the updateSessionStore callback is properly handling errors and edge cases.
  • Review the cache invalidation strategy to prevent similar issues in the future.
  • Consider adding additional logging or monitoring to detect and respond to cache-related issues.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING