Summary
When a cron job with delivery.mode: "announce" fires while the main session is actively replying inside a Slack thread, the delivery inherits lastThreadId from the session and sends the cron output into the active thread instead of the top-level DM.
This causes unrelated content (research reports, reminders, proactive cron output) to appear inside whichever thread happens to be active at the moment — confusing and disruptive.
Root cause
resolveSessionDeliveryTarget() in targets.ts unconditionally inherits lastThreadId when the channel matches:
const threadId = channel && channel === lastChannel ? lastThreadId : void 0;
// ...
const resolvedThreadId = explicitThreadId ?? threadId;
The heartbeat/cron delivery path calls this via resolveHeartbeatDeliveryTarget() with mode: "heartbeat", but resolveSessionDeliveryTarget does not use this mode to suppress lastThreadId fallback.
Steps to reproduce
- Configure a cron job with
delivery.mode: "announce", delivery.channel: "slack", delivery.to: "user:<userId>"
- Start a conversation in a Slack thread (reply to any message)
- Wait for the cron job to fire while the thread conversation is active
- Observe: cron output appears inside the active thread instead of top-level DM
Expected behavior
Cron announce delivery should send to the top-level DM channel, ignoring any active lastThreadId from the session. Thread targeting should only happen when explicitly configured (e.g., the proposed C12345:thread:TS syntax from #19903).
Suggested fix
In resolveSessionDeliveryTarget(), skip lastThreadId fallback when mode === "heartbeat":
const threadId = (params.mode !== "heartbeat" && channel && channel === lastChannel)
? lastThreadId
: void 0;
This is a one-line change. The function already receives the mode parameter — it just needs to use it.
Environment
- OpenClaw version: 2026.2.23 (also present in 2026.2.22-2)
- Channel: Slack (Socket Mode)
- Cron job config:
sessionTarget: "isolated", delivery.mode: "announce", delivery.to: "user:<id>"
Related
AI-Assisted Disclosure
This issue was filed by an AI assistant (Claude, via OpenClaw). The root cause analysis was performed by reading the minified source in dist/reply-*.js and tracing the delivery flow through resolveHeartbeatDeliveryTarget → resolveSessionDeliveryTarget. The author (human operator) understands the issue and has experienced it repeatedly in production.
Summary
When a cron job with
delivery.mode: "announce"fires while the main session is actively replying inside a Slack thread, the delivery inheritslastThreadIdfrom the session and sends the cron output into the active thread instead of the top-level DM.This causes unrelated content (research reports, reminders, proactive cron output) to appear inside whichever thread happens to be active at the moment — confusing and disruptive.
Root cause
resolveSessionDeliveryTarget()intargets.tsunconditionally inheritslastThreadIdwhen the channel matches:The heartbeat/cron delivery path calls this via
resolveHeartbeatDeliveryTarget()withmode: "heartbeat", butresolveSessionDeliveryTargetdoes not use this mode to suppresslastThreadIdfallback.Steps to reproduce
delivery.mode: "announce",delivery.channel: "slack",delivery.to: "user:<userId>"Expected behavior
Cron announce delivery should send to the top-level DM channel, ignoring any active
lastThreadIdfrom the session. Thread targeting should only happen when explicitly configured (e.g., the proposedC12345:thread:TSsyntax from #19903).Suggested fix
In
resolveSessionDeliveryTarget(), skiplastThreadIdfallback whenmode === "heartbeat":This is a one-line change. The function already receives the
modeparameter — it just needs to use it.Environment
sessionTarget: "isolated",delivery.mode: "announce",delivery.to: "user:<id>"Related
lastThreadIdcaching issue on Telegram DMsAI-Assisted Disclosure
This issue was filed by an AI assistant (Claude, via OpenClaw). The root cause analysis was performed by reading the minified source in
dist/reply-*.jsand tracing the delivery flow throughresolveHeartbeatDeliveryTarget→resolveSessionDeliveryTarget. The author (human operator) understands the issue and has experienced it repeatedly in production.