fix(slack): thread history on subsequent turns, inbound meta context, and heartbeat thread leaking#16186
Conversation
|
Closing — upstream fixed this natively in 2026.2.13 via #14976 (auto-inject implicit reply threading for |
|
Reopening — the 2026.2.13 fix (#14976) addresses outbound auto-reply threading, not the inbound thread context bug. This PR fixes the core issue: thread history is only fetched on |
bfc1ccb to
f92900f
Compare
7ecc649 to
e7f0b2c
Compare
8514073 to
9126a17
Compare
9126a17 to
509a74e
Compare
|
Hello @steipete ! Could we get this approved/merged soon? |
8e1b3ed to
a0cc18f
Compare
Ah, I did not follow https://github.com/openclaw/openclaw/blob/main/CONTRIBUTING.md; I apologize. I've updated the PR based on above. |
cda8812 to
e3761cb
Compare
3e32e13 to
9f1df3c
Compare
5d7de37 to
6677bb7
Compare
Previously, thread history was only fetched on the first message of a thread session. Subsequent replies received no thread context because the `!threadSessionPreviousTimestamp` guard skipped the fetch entirely. Now thread history is always fetched when `threadInitialHistoryLimit > 0`, but uses the session's last-seen timestamp as the `oldest` parameter to Slack's conversations.replies API, ensuring only new (delta) messages are fetched on subsequent turns. Also adds `ThreadTs` to the inbound context so agents always know when they're in a thread. Fixes #12742, #12586
… context - Add thread_ts and message_ts to inbound meta JSON so agents always know thread context - Add channel_id extraction (from ProviderChannelId, GroupChannel, or To field parsing) - Add is_thread_reply flag to inbound meta flags - Pass ProviderChannelId from Slack message.channel into template context This enables agents to call slack_thread_context without hardcoding channel IDs.
Heartbeat/proactive messages were threading into whatever conversation was last active in the session. For example, a hygiene scanner result would land in an unrelated ACC-16 thread because that was the session's lastThreadId. The fix: resolveHeartbeatDeliveryTarget now only uses threadId when it was explicitly configured (e.g. Telegram :topic: syntax), not inherited from session history. Proactive sends go top-level by default. Adds regression test verifying session lastThreadId does not leak into heartbeat delivery targets.
6677bb7 to
1f14941
Compare
Why This Matters
When OpenClaw replies in Slack, threaded conversations keep parallel work streams organized — you can have five things going at once without messages bleeding together. But today, the agent loses thread context after the first message, so it can't follow the conversation it's in. Additionally, proactive heartbeat messages incorrectly thread into whatever conversation was last active. This PR fixes both gaps.
Problem (Technical)
1. Missing thread history on subsequent turns
When a user sends a 2nd+ message in a Slack thread, the agent receives no thread history context. Only the first message in a thread session fetches history.
Root Cause:
prepare.tsline 508:The
!threadSessionPreviousTimestampcondition means history is only fetched on the first turn.2. Heartbeat messages threading into unrelated conversations
Proactive heartbeat/cron messages inherit the session's
lastThreadId, causing them to reply in whatever thread was last active instead of going top-level.Root Cause:
resolveHeartbeatDeliveryTargetintargets.tspasses throughresolvedTarget.threadIdunconditionally, which falls back to sessionlastThreadId.Fix
Delta fetch for thread history
!threadSessionPreviousTimestampgate — always fetch thread history whenthreadInitialHistoryLimit > 0threadSessionPreviousTimestampas theoldestparameter to Slack'sconversations.repliesAPI, so only messages newer than the last seen timestamp are fetchedoldestparam toresolveSlackThreadHistoryto support the delta fetchExpose thread context in inbound meta
thread_ts— Slack thread timestamp, present when message is a thread replymessage_ts— Slack message timestamp for the current messagechannel_id— Raw Slack channel/conversation ID (e.g.D0AD6FBJB3R)is_thread_replyflag inflagsobjectProviderChannelIdin template context, passed frommessage.channelextractChannelId()helper that parses channel ID fromGroupChannel,To, or raw valuePrevent heartbeat thread leaking
resolveHeartbeatDeliveryTargetnow only usesthreadIdwhen explicitly configured (e.g. Telegram:topic:syntax), not inherited from session historyChanges
src/slack/monitor/message-handler/prepare.ts— remove first-turn gate, add delta fetch witholdest, passProviderChannelIdsrc/slack/monitor/media.ts— addoldestparam toresolveSlackThreadHistorysrc/auto-reply/templating.ts— addThreadTs,ProviderChannelIdtoMsgContexttypesrc/auto-reply/reply/inbound-meta.ts— addthread_ts,message_ts,channel_id,is_thread_replyto inbound meta JSON; addextractChannelId()helpersrc/infra/outbound/targets.ts— filterthreadIdin heartbeat delivery to explicit-onlysrc/infra/heartbeat-runner.returns-default-unset.test.ts— regression test for heartbeat thread leakingprepare.inbound-contract.test.ts— update test to verify delta fetch behaviorTesting
All existing tests pass. New regression test added for heartbeat thread leaking. Validated against live Slack threads.
Fixes #12742, #12586
🤖 AI-Assisted Contribution
Built by Claude (via OpenClaw agent). Fully tested against live Slack threads — validated that delta fetch resolves the core problem of missing thread context on subsequent turns. The author understands the changes and the codebase context that motivated them.