Bug Description
When session.dmScope is set to per-channel-peer, every Telegram DM still calls updateLastRoute on agent:main:main (the webchat/dashboard session), overwriting its deliveryContext from webchat to telegram. This causes webchat responses to be routed to Telegram instead of back to the browser.
Steps to Reproduce
- Configure OpenClaw with Telegram + webchat enabled,
session.dmScope: "per-channel-peer"
- Send a message via the webchat/dashboard UI — creates
agent:main:main session with webchat delivery context
- Send a Telegram DM — the Telegram handler calls
recordInboundSession() with updateLastRoute targeting route.mainSessionKey (agent:main:main)
- The
agent:main:main session's deliveryContext is now { channel: "telegram", to: "<chatId>" }
- Next webchat response or heartbeat delivery reads the corrupted
deliveryContext and routes to Telegram
Expected Behavior
When dmScope is per-channel-peer, Telegram DMs already get their own session key (agent:main:telegram:direct:<chatId>). The updateLastRoute call should NOT overwrite agent:main:main's deliveryContext, since the DM conversation is isolated by design.
Root Cause (from source analysis)
In reply-DptDUVRg.js (~line 36731), recordInboundSession() is called with:
updateLastRoute: !isGroup ? {
sessionKey: route.mainSessionKey, // Always "agent:main:main"
channel: "telegram",
to: String(chatId),
...
} : void 0
route.mainSessionKey is computed in resolveAgentRoute() (~line 18346) and always resolves to agent:main:main regardless of dmScope.
The updateLastRoute function in sandbox-DiYTfhXZ.js (~line 2505) then unconditionally overwrites the deliveryContext on the target session.
Self-Reinforcing Corruption
Once corrupted, the issue is self-reinforcing:
- Heartbeat reads
deliveryContext from agent:main:main → delivers to Telegram
initSessionState() (~line 64596) falls back to baseEntry?.lastChannel when ctx.OriginatingChannel is unset (heartbeat/cron contexts), preserving the corruption
Suggested Fix
When dmScope is not "main" (i.e., DMs have their own sessions), skip the updateLastRoute write to mainSessionKey. Alternatively, store "last route" metadata separately from the webchat session entry.
Workaround
We worked around this by:
- Setting explicit
heartbeat.target: "telegram" + heartbeat.session: "heartbeat" to isolate heartbeat from the webchat session
- Running a monitoring script that detects and auto-fixes the corrupted
deliveryContext
Environment
- OpenClaw version: 2026.2.9 (33c75cb)
- Node.js: v22
- Channels: Telegram (polling) + webchat + Discord
- Session config:
dmScope: "per-channel-peer"
Bug Description
When
session.dmScopeis set toper-channel-peer, every Telegram DM still callsupdateLastRouteonagent:main:main(the webchat/dashboard session), overwriting itsdeliveryContextfrom webchat to telegram. This causes webchat responses to be routed to Telegram instead of back to the browser.Steps to Reproduce
session.dmScope: "per-channel-peer"agent:main:mainsession with webchat delivery contextrecordInboundSession()withupdateLastRoutetargetingroute.mainSessionKey(agent:main:main)agent:main:mainsession'sdeliveryContextis now{ channel: "telegram", to: "<chatId>" }deliveryContextand routes to TelegramExpected Behavior
When
dmScopeisper-channel-peer, Telegram DMs already get their own session key (agent:main:telegram:direct:<chatId>). TheupdateLastRoutecall should NOT overwriteagent:main:main'sdeliveryContext, since the DM conversation is isolated by design.Root Cause (from source analysis)
In
reply-DptDUVRg.js(~line 36731),recordInboundSession()is called with:route.mainSessionKeyis computed inresolveAgentRoute()(~line 18346) and always resolves toagent:main:mainregardless ofdmScope.The
updateLastRoutefunction insandbox-DiYTfhXZ.js(~line 2505) then unconditionally overwrites thedeliveryContexton the target session.Self-Reinforcing Corruption
Once corrupted, the issue is self-reinforcing:
deliveryContextfromagent:main:main→ delivers to TelegraminitSessionState()(~line 64596) falls back tobaseEntry?.lastChannelwhenctx.OriginatingChannelis unset (heartbeat/cron contexts), preserving the corruptionSuggested Fix
When
dmScopeis not"main"(i.e., DMs have their own sessions), skip theupdateLastRoutewrite tomainSessionKey. Alternatively, store "last route" metadata separately from the webchat session entry.Workaround
We worked around this by:
heartbeat.target: "telegram"+heartbeat.session: "heartbeat"to isolate heartbeat from the webchat sessiondeliveryContextEnvironment
dmScope: "per-channel-peer"