Skip to content

Discord second message fails: false stale-snapshot from skillsSnapshot hydration mismatch #96698

Description

@wiidz

Summary

On Discord (and likely any channel using reply session initialization), the first inbound message succeeds but the second message always fails with:

reply session initialization conflicted for agent:main:discord:channel:<channelId>

Root cause is a false positive stale-snapshot in optimistic concurrency: createReplySessionInitializationRevision() serializes the full session entry via JSON.stringify, but snapshot load and commit compare different shapes of skillsSnapshot.

Environment

  • OpenClaw 2026.6.10 (commit a96418c)
  • Channel: Discord
  • Single agent (main), minimal config
  • Repro: send two messages in the same Discord channel; first OK, second fails

Root cause

loadReplySessionInitializationSnapshot() loads the store with default hydration, which persists skillsSnapshot as { skills: [...], promptRef: "..." }.

Inside commitReplySessionInitialization(), the writer's in-memory store is loaded with hydrated skill prompt blobs, so the same field becomes { skills: [...], prompt: "<full text>" }.

Revision comparison then fails even when updatedAt / lastInteractionAt are identical. Diagnostic logs showed:

  • fieldDiff.changedKeys: ['skillsSnapshot']
  • firstDiffIndex around 1036
  • JSON diff at that index: "skills" (expected) vs "prompt" (actual)

After the first agent run writes a full skillsSnapshot, the second inbound turn's init snapshot no longer matches the commit-time revision built from the hydrated writer cache → retry once → hard conflict.

Suggested fix

Use the same persisted shape for revision on both paths:

  1. loadReplySessionInitializationSnapshot: loadSessionStore(..., { hydrateSkillPromptRefs: false })
  2. commitReplySessionInitialization: when building the revision for comparison, re-read from disk with hydrateSkillPromptRefs: false instead of using the writer cache entry (which may be hydrated).

Alternative: change revision to exclude or normalize skillsSnapshot.prompt vs promptRef.

Secondary issue (optional hardening)

In src/channels/session.ts, recordInboundSession used void metaTask before updateLastRoute, allowing concurrent writes to the same session store. Changing to await metaTask avoids an additional race (not the primary cause of the every-second-message failure).

Verified locally

With the two changes above, consecutive Discord messages succeed (no stale-snapshot, model fetch 200 on second turn).

Metadata

Metadata

Assignees

Labels

P1High-priority user-facing bug, regression, or broken workflow.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.no-staleExclude from stale automation

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions