openclaw - 💡(How to fix) Fix Stale-entry writeback races for lastChannel/channel in agent handler session patch [1 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#70575Fetched 2026-04-24 05:56:12
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0

PR #19328 (fix for #5369) removed most stale-entry writebacks from the session-entry patch in src/gateway/server-methods/agent.ts. Two residual patterns in the same function still carry a stale cached value into the patch and can clobber a fresh concurrent write via the {...freshStore, ...patch} merge in updateSessionStore.

Root Cause

PR #19328 (fix for #5369) removed most stale-entry writebacks from the session-entry patch in src/gateway/server-methods/agent.ts. Two residual patterns in the same function still carry a stale cached value into the patch and can clobber a fresh concurrent write via the {...freshStore, ...patch} merge in updateSessionStore.

Fix Action

Fix / Workaround

PR #19328 (fix for #5369) removed most stale-entry writebacks from the session-entry patch in src/gateway/server-methods/agent.ts. Two residual patterns in the same function still carry a stale cached value into the patch and can clobber a fresh concurrent write via the {...freshStore, ...patch} merge in updateSessionStore.

Both patterns write entry?.X (stale cached read) into the patch. Failure mode is the same shape as #5369:

  1. loadSessionEntry returns cached entry (stale; missing the field)
  2. A concurrent write (e.g. sessions.patch) updates the field in the store
  3. The agent handler's updateSessionStore mutator merges {...freshStore, ...nextEntryPatch}
  4. nextEntryPatch.X = undefined (stale) clobbers the fresh store value

Code Example

lastChannel: effectiveDeliveryFields.lastChannel ?? entry?.lastChannel,
lastTo: effectiveDeliveryFields.lastTo ?? entry?.lastTo,
lastAccountId: effectiveDeliveryFields.lastAccountId ?? entry?.lastAccountId,
lastThreadId: effectiveDeliveryFields.lastThreadId ?? entry?.lastThreadId,
// ...
channel: entry?.channel ?? request.channel?.trim(),

---

const patch: Partial<SessionEntry> = {
  sessionId,
  updatedAt: now,
  deliveryContext: effectiveDeliveryFields.deliveryContext,
  label: labelValue,
  spawnedBy: spawnedByValue,
  groupId: resolvedGroupId ?? undefined,
  groupChannel: resolvedGroupChannel ?? undefined,
  space: resolvedGroupSpace ?? undefined,
};
if (effectiveDeliveryFields.lastChannel !== undefined) patch.lastChannel = effectiveDeliveryFields.lastChannel;
if (effectiveDeliveryFields.lastTo !== undefined) patch.lastTo = effectiveDeliveryFields.lastTo;
// ...etc
if (request.channel?.trim() && !entry?.channel) patch.channel = request.channel.trim();
RAW_BUFFERClick to expand / collapse

Context

PR #19328 (fix for #5369) removed most stale-entry writebacks from the session-entry patch in src/gateway/server-methods/agent.ts. Two residual patterns in the same function still carry a stale cached value into the patch and can clobber a fresh concurrent write via the {...freshStore, ...patch} merge in updateSessionStore.

Residual patterns

At roughly src/gateway/server-methods/agent.ts:675-681:

lastChannel: effectiveDeliveryFields.lastChannel ?? entry?.lastChannel,
lastTo: effectiveDeliveryFields.lastTo ?? entry?.lastTo,
lastAccountId: effectiveDeliveryFields.lastAccountId ?? entry?.lastAccountId,
lastThreadId: effectiveDeliveryFields.lastThreadId ?? entry?.lastThreadId,
// ...
channel: entry?.channel ?? request.channel?.trim(),

Both patterns write entry?.X (stale cached read) into the patch. Failure mode is the same shape as #5369:

  1. loadSessionEntry returns cached entry (stale; missing the field)
  2. A concurrent write (e.g. sessions.patch) updates the field in the store
  3. The agent handler's updateSessionStore mutator merges {...freshStore, ...nextEntryPatch}
  4. nextEntryPatch.X = undefined (stale) clobbers the fresh store value

Why they weren't fixed in #19328

Narrower blast radius than #5369:

  • lastChannel/lastTo/etc. only race when both effectiveDeliveryFields.X and entry?.X are undefined but the fresh store has a value — an unusual combination.
  • channel rarely changes mid-session, so stale cache typically matches fresh.

Neither was part of the reported user impact (sub-agent model override failures), so they were held out of the minimal fix to keep the PR focused and reviewable.

Proposed fix

Same pattern as #19328: drop the stale-cache writebacks from the patch and let mergeSessionEntry's {...existing, ...patch} spread preserve fresh store values. For the delivery fields, this means either conditional property inclusion (only set when effectiveDeliveryFields.X is defined) or moving the delivery merge into the updateSessionStore mutator so it reads fresh lastChannel as the fallback.

Rough sketch:

const patch: Partial<SessionEntry> = {
  sessionId,
  updatedAt: now,
  deliveryContext: effectiveDeliveryFields.deliveryContext,
  label: labelValue,
  spawnedBy: spawnedByValue,
  groupId: resolvedGroupId ?? undefined,
  groupChannel: resolvedGroupChannel ?? undefined,
  space: resolvedGroupSpace ?? undefined,
};
if (effectiveDeliveryFields.lastChannel !== undefined) patch.lastChannel = effectiveDeliveryFields.lastChannel;
if (effectiveDeliveryFields.lastTo !== undefined) patch.lastTo = effectiveDeliveryFields.lastTo;
// ...etc
if (request.channel?.trim() && !entry?.channel) patch.channel = request.channel.trim();

Regression test: extend the "stale cached entry / fresh store" pattern established by the #5369 broader test in agent.test.ts to cover these fields.

References

  • #5369 — the modelOverride race that prompted the investigation
  • #19328 — the minimal fix that established the "no stale writebacks in patch" pattern
  • agent.test.ts tests preserves fresh modelOverride when cached entry is stale (#5369) and preserves all fresh session fields when cached entry is stale (#5369 broader) — template for the regression tests here

extent analysis

TL;DR

Remove stale-cache writebacks from the session-entry patch in agent.ts to prevent clobbering fresh concurrent writes.

Guidance

  • Identify and update the residual patterns in agent.ts that carry stale cached values into the patch, specifically the lastChannel, lastTo, lastAccountId, lastThreadId, and channel fields.
  • Apply the proposed fix by conditionally including properties in the patch only when effectiveDeliveryFields.X is defined, or by moving the delivery merge into the updateSessionStore mutator.
  • Verify the fix by extending the "stale cached entry / fresh store" pattern established by the #5369 broader test in agent.test.ts to cover these fields.
  • Review the code changes to ensure that the mergeSessionEntry spread preserves fresh store values.

Example

const patch: Partial<SessionEntry> = {
  // ...
  if (effectiveDeliveryFields.lastChannel !== undefined) patch.lastChannel = effectiveDeliveryFields.lastChannel;
  if (effectiveDeliveryFields.lastTo !== undefined) patch.lastTo = effectiveDeliveryFields.lastTo;
  // ...
}

Notes

The proposed fix is based on the same pattern as #19328, which established the "no stale writebacks in patch" pattern. The residual patterns were not fixed in #19328 due to a narrower blast radius and rare occurrence of stale cache issues.

Recommendation

Apply the proposed workaround by removing stale-cache writebacks from the session-entry patch, as it is a targeted fix that addresses the specific issue and prevents clobbering fresh concurrent writes.

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

openclaw - 💡(How to fix) Fix Stale-entry writeback races for lastChannel/channel in agent handler session patch [1 participants]