Summary
A sessionTarget: "main" + payload.kind: "systemEvent" cron job that was bound to a Telegram forum topic session woke the correct topic heartbeat session, but its reminder reply was delivered to the user's Telegram DM instead of the originating topic.
This looks like a core delivery/heartbeat merge bug: the cron main-session path calls runHeartbeatOnce({ heartbeat: { target: "last" } }), but that shallow override still inherits the global agents.defaults.heartbeat.to value. The inherited explicit to then wins over the intended session-bound last route, causing a cross-context delivery leak.
Why this matters
This is a privacy / cross-context routing issue. A reminder created from a group/forum topic can leak into a DM if the global heartbeat config has a DM to configured.
Environment
- OpenClaw: v2026.4.26
- Channel: Telegram
- Scenario: Telegram forum supergroup topic -> user's Telegram DM
- Cron payload:
sessionTarget: "main", payload.kind: "systemEvent", wakeMode: "now"
Relevant config shape
Global heartbeat defaults include an explicit DM target:
{
"agents": {
"defaults": {
"heartbeat": {
"accountId": "default",
"every": "1h",
"isolatedSession": true,
"model": "openai-codex/gpt-5.5",
"session": "main",
"target": "none",
"to": "telegram:<user-dm-id>"
}
}
}
}
A one-shot cron reminder was created from a Telegram forum topic session:
{
"sessionTarget": "main",
"wakeMode": "now",
"payload": {
"kind": "systemEvent",
"text": "Reminder: ..."
},
"sessionKey": "agent:main:telegram:group:<group-id>:topic:<topic-id>"
}
Observed behavior
The cron fired and created/used the expected topic heartbeat session:
agent:main:telegram:group:<group-id>:topic:<topic-id>:heartbeat
However, the runtime context for that heartbeat turn showed the DM chat as the delivery context, and the final reminder reply was sent to the user's Telegram DM.
Evidence from local logs/session store (IDs redacted):
cron job created at 07:47:54
original topic session final messages delivered to the topic at 07:47:59 / 07:48:01
heartbeat session started at 08:00:08:
agent:main:telegram:group:<group-id>:topic:<topic-id>:heartbeat
runtime context chat_id for the heartbeat turn:
<user-dm-id>
Telegram sent map at 08:00:44:
<user-dm-id> -> message <dm-message-id>
The session store for the originating topic had the correct delivery context:
{
"chatType": "group",
"deliveryContext": {
"channel": "telegram",
"to": "telegram:<group-id>",
"accountId": "default",
"threadId": <topic-id>
},
"lastChannel": "telegram",
"lastTo": "telegram:<group-id>",
"lastThreadId": <topic-id>
}
Expected behavior
For a cron systemEvent bound to a session key like:
agent:main:telegram:group:<group-id>:topic:<topic-id>
the heartbeat wake should deliver any user-visible reminder reply back to that bound session route, including the forum topic thread id.
At minimum, if the cron path overrides heartbeat target to last, inherited global heartbeat.to should not override the session-bound route.
Actual behavior
The explicit global agents.defaults.heartbeat.to appears to be inherited during the cron wake. That explicit DM to overrides the intended target: "last" route, so the reminder reply is delivered to DM.
Likely root cause
In the main/systemEvent cron execution path, the runtime calls something equivalent to:
runHeartbeatOnce({
reason,
agentId,
sessionKey: targetMainSessionKey,
heartbeat: { target: "last" }
})
The server wrapper then merges this shallowly with the configured heartbeat defaults:
const heartbeatOverride = opts?.heartbeat
? { ...baseHeartbeat, ...opts.heartbeat }
: undefined
Because opts.heartbeat only sets target: "last", other default fields remain, including to: "telegram:<user-dm-id>".
Later heartbeat delivery resolution treats explicit heartbeat.to as an explicit destination, so it wins over the session's delivery context.
Suggested fix
When cron main/systemEvent forces heartbeat.target = "last", it should either:
- clear inherited explicit destination fields (
to, possibly channel/accountId if appropriate), or
- use a dedicated non-inheriting heartbeat override for cron/systemEvent wakes, or
- make
target: "last" semantically ignore inherited to unless to was explicitly supplied in the same override object.
The safest behavior is probably: session-bound systemEvent cron wakes should route to the bound session delivery context, not to global heartbeat.to.
Related issues
Summary
A
sessionTarget: "main"+payload.kind: "systemEvent"cron job that was bound to a Telegram forum topic session woke the correct topic heartbeat session, but its reminder reply was delivered to the user's Telegram DM instead of the originating topic.This looks like a core delivery/heartbeat merge bug: the cron main-session path calls
runHeartbeatOnce({ heartbeat: { target: "last" } }), but that shallow override still inherits the globalagents.defaults.heartbeat.tovalue. The inherited explicittothen wins over the intended session-boundlastroute, causing a cross-context delivery leak.Why this matters
This is a privacy / cross-context routing issue. A reminder created from a group/forum topic can leak into a DM if the global heartbeat config has a DM
toconfigured.Environment
sessionTarget: "main",payload.kind: "systemEvent",wakeMode: "now"Relevant config shape
Global heartbeat defaults include an explicit DM target:
{ "agents": { "defaults": { "heartbeat": { "accountId": "default", "every": "1h", "isolatedSession": true, "model": "openai-codex/gpt-5.5", "session": "main", "target": "none", "to": "telegram:<user-dm-id>" } } } }A one-shot cron reminder was created from a Telegram forum topic session:
{ "sessionTarget": "main", "wakeMode": "now", "payload": { "kind": "systemEvent", "text": "Reminder: ..." }, "sessionKey": "agent:main:telegram:group:<group-id>:topic:<topic-id>" }Observed behavior
The cron fired and created/used the expected topic heartbeat session:
However, the runtime context for that heartbeat turn showed the DM chat as the delivery context, and the final reminder reply was sent to the user's Telegram DM.
Evidence from local logs/session store (IDs redacted):
The session store for the originating topic had the correct delivery context:
{ "chatType": "group", "deliveryContext": { "channel": "telegram", "to": "telegram:<group-id>", "accountId": "default", "threadId": <topic-id> }, "lastChannel": "telegram", "lastTo": "telegram:<group-id>", "lastThreadId": <topic-id> }Expected behavior
For a cron
systemEventbound to a session key like:the heartbeat wake should deliver any user-visible reminder reply back to that bound session route, including the forum topic thread id.
At minimum, if the cron path overrides heartbeat target to
last, inherited globalheartbeat.toshould not override the session-bound route.Actual behavior
The explicit global
agents.defaults.heartbeat.toappears to be inherited during the cron wake. That explicit DMtooverrides the intendedtarget: "last"route, so the reminder reply is delivered to DM.Likely root cause
In the main/systemEvent cron execution path, the runtime calls something equivalent to:
The server wrapper then merges this shallowly with the configured heartbeat defaults:
Because
opts.heartbeatonly setstarget: "last", other default fields remain, includingto: "telegram:<user-dm-id>".Later heartbeat delivery resolution treats explicit
heartbeat.toas an explicit destination, so it wins over the session's delivery context.Suggested fix
When cron main/systemEvent forces
heartbeat.target = "last", it should either:to, possiblychannel/accountIdif appropriate), ortarget: "last"semantically ignore inheritedtounlesstowas explicitly supplied in the same override object.The safest behavior is probably: session-bound systemEvent cron wakes should route to the bound session delivery context, not to global heartbeat.to.
Related issues
@heartbeatsessionTarget: main+systemEventhandoff