Version: openclaw 2026.6.11 (npm latest)
Runtime: node v24.18.0, Linux 6.12.75 (arm64, Raspberry Pi)
Channel: whatsapp (plugin, ~/.openclaw/extensions/whatsapp)
Agent backend: claude-cli, activeSessions=1
Summary
If a second inbound message arrives on the same sessionKey while a reply run for
the first one is still active, the second message is dropped. It is never
answered, never queued, and never surfaced to the user — only an ERROR line in the
gateway log:
Failed handling inbound web message:
Error: reply session initialization conflicted for agent:main:whatsapp:direct:+41…
On WhatsApp the message is also never acked, so the provider re-delivers it on the
next reconnect with the same message_id. That produces a self-sustaining loop:
reconnect → redelivery → run → conflict → drop → no ack → reconnect. From the user's
side the channel looks dead (reactions still arrive, because the reaction path is
independent of the agent, but no text reply ever does).
Root cause
commitReplySessionInitialization() uses optimistic concurrency against
sessions.json. The revision token is the entire serialized session entry:
dist/session-accessor-*.js
function createReplySessionInitializationRevision(entry) {
return JSON.stringify(entry ?? null);
}
The snapshot is taken in initSessionStateAttempt() (dist/get-reply-*.js:4554) and
committed ~290 lines and several awaits later (:4844). While a run is active the
gateway keeps mutating that same entry — pendingFinalDelivery, token counters,
run-state, lastActivity. Any one of those writes changes the stringified entry and
therefore invalidates the snapshot, even though none of them are relevant to session
initialization.
The commit is retried exactly once, then it throws:
dist/get-reply-*.js:4875
if (!committed.ok) {
if (!staleSnapshotRetried) return await initSessionStateAttempt(params, true);
throw new Error(`reply session initialization conflicted for ${sessionKey}`);
}
The throw propagates to the inbound handler, which logs and discards the message.
There is no requeue and no user-visible error.
Reproduction
- Single agent, single channel,
claude-cli backend (activeSessions=1).
- Send a message that produces a long-running turn (> ~5 s).
- While it runs, send a second message to the same DM.
- Second message: reactions appear (
✅ 💭 ⏳ ⚠️), no reply. Log shows the conflict.
Observed here with two messages ~4 s apart (text, then an image).
Log evidence
08:52:23.878Z web-inbound body='wieder da'
08:52:40.334Z whatsapp/outbound Sent message 3EB0DD9B… (27ms) ← first: ok
08:52:44.914Z web-inbound body='<media:image>'
08:52:47.209Z diagnostic message dispatch completed: outcome=error
error="Error: reply session initialization conflicted
for agent:main:whatsapp:direct:+41…"
08:52:47.221Z whatsapp/inbound Failed handling inbound web message
08:53:15.595Z whatsapp/outbound Sent reaction "⚠️" ← only signal user gets
After the failure, lastInboundAt froze and messagesHandled stayed at 2 — the
inbound listener stopped accepting further messages until the gateway was restarted.
That secondary effect may deserve its own issue; the drop above is the trigger.
Impact
- Any user who sends a follow-up before the previous reply lands loses that message.
- Silent: no error to the user, no retry, no dead-letter.
- On WhatsApp it escalates into repeated redelivery of the same
message_id, which
looks to operators like a channel/model bug. It cost us roughly an hour of
misdiagnosis (suspected model pin, config hot-reload, pendingFinalDelivery).
Suggested fixes
- Narrow the revision token. Hash only the fields initialization actually depends
on (sessionId, sessionFile, agentId, reset/fork markers) instead of the whole
entry. Concurrent run-state writes then stop invalidating the snapshot. This is the
real fix.
- Bounded retry with backoff instead of a single retry. Optimistic concurrency
should loop, not surrender on the second try. (Applied locally as a stopgap: 8
attempts, 25 ms → 400 ms exponential backoff + jitter.)
- Never drop the message. If the commit ultimately fails, requeue the inbound or
surface a user-visible error. A dropped message with an unacked provider id is the
worst of both worlds.
Local workaround in use
Patched dist/get-reply-*.js to retry up to 8 times with exponential backoff. This
survives until the next npm update -g openclaw.
Version: openclaw
2026.6.11(npmlatest)Runtime: node v24.18.0, Linux 6.12.75 (arm64, Raspberry Pi)
Channel: whatsapp (plugin,
~/.openclaw/extensions/whatsapp)Agent backend:
claude-cli,activeSessions=1Summary
If a second inbound message arrives on the same
sessionKeywhile a reply run forthe first one is still active, the second message is dropped. It is never
answered, never queued, and never surfaced to the user — only an ERROR line in the
gateway log:
On WhatsApp the message is also never acked, so the provider re-delivers it on the
next reconnect with the same
message_id. That produces a self-sustaining loop:reconnect → redelivery → run → conflict → drop → no ack → reconnect. From the user's
side the channel looks dead (reactions still arrive, because the reaction path is
independent of the agent, but no text reply ever does).
Root cause
commitReplySessionInitialization()uses optimistic concurrency againstsessions.json. The revision token is the entire serialized session entry:dist/session-accessor-*.jsThe snapshot is taken in
initSessionStateAttempt()(dist/get-reply-*.js:4554) andcommitted ~290 lines and several
awaits later (:4844). While a run is active thegateway keeps mutating that same entry —
pendingFinalDelivery, token counters,run-state,
lastActivity. Any one of those writes changes the stringified entry andtherefore invalidates the snapshot, even though none of them are relevant to session
initialization.
The commit is retried exactly once, then it throws:
dist/get-reply-*.js:4875The throw propagates to the inbound handler, which logs and discards the message.
There is no requeue and no user-visible error.
Reproduction
claude-clibackend (activeSessions=1).✅ 💭 ⏳ ⚠️), no reply. Log shows the conflict.Observed here with two messages ~4 s apart (text, then an image).
Log evidence
After the failure,
lastInboundAtfroze andmessagesHandledstayed at2— theinbound listener stopped accepting further messages until the gateway was restarted.
That secondary effect may deserve its own issue; the drop above is the trigger.
Impact
message_id, whichlooks to operators like a channel/model bug. It cost us roughly an hour of
misdiagnosis (suspected model pin, config hot-reload,
pendingFinalDelivery).Suggested fixes
on (
sessionId,sessionFile,agentId, reset/fork markers) instead of the wholeentry. Concurrent run-state writes then stop invalidating the snapshot. This is the
real fix.
should loop, not surrender on the second try. (Applied locally as a stopgap: 8
attempts, 25 ms → 400 ms exponential backoff + jitter.)
surface a user-visible error. A dropped message with an unacked provider id is the
worst of both worlds.
Local workaround in use
Patched
dist/get-reply-*.jsto retry up to 8 times with exponential backoff. Thissurvives until the next
npm update -g openclaw.