Skip to content

fix(slack): thread history on subsequent turns, inbound meta context, and heartbeat thread leaking#16186

Closed
markshields-tl wants to merge 4 commits intoopenclaw:mainfrom
markshields-tl:fix/slack-thread-context-subsequent-turns
Closed

fix(slack): thread history on subsequent turns, inbound meta context, and heartbeat thread leaking#16186
markshields-tl wants to merge 4 commits intoopenclaw:mainfrom
markshields-tl:fix/slack-thread-context-subsequent-turns

Conversation

@markshields-tl
Copy link
Copy Markdown
Contributor

@markshields-tl markshields-tl commented Feb 14, 2026

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.ts line 508:

if (threadInitialHistoryLimit > 0 && !threadSessionPreviousTimestamp) {

The !threadSessionPreviousTimestamp condition 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: resolveHeartbeatDeliveryTarget in targets.ts passes through resolvedTarget.threadId unconditionally, which falls back to session lastThreadId.

Fix

Delta fetch for thread history

  • Remove the !threadSessionPreviousTimestamp gate — always fetch thread history when threadInitialHistoryLimit > 0
  • Delta fetch — pass threadSessionPreviousTimestamp as the oldest parameter to Slack's conversations.replies API, so only messages newer than the last seen timestamp are fetched
  • Add oldest param to resolveSlackThreadHistory to support the delta fetch

Expose thread context in inbound meta

  • thread_ts — Slack thread timestamp, present when message is a thread reply
  • message_ts — Slack message timestamp for the current message
  • channel_id — Raw Slack channel/conversation ID (e.g. D0AD6FBJB3R)
  • is_thread_reply flag in flags object
  • ProviderChannelId in template context, passed from message.channel
  • extractChannelId() helper that parses channel ID from GroupChannel, To, or raw value

Prevent heartbeat thread leaking

  • resolveHeartbeatDeliveryTarget now only uses threadId when explicitly configured (e.g. Telegram :topic: syntax), not inherited from session history
  • Proactive sends go top-level by default

Changes

  • src/slack/monitor/message-handler/prepare.ts — remove first-turn gate, add delta fetch with oldest, pass ProviderChannelId
  • src/slack/monitor/media.ts — add oldest param to resolveSlackThreadHistory
  • src/auto-reply/templating.ts — add ThreadTs, ProviderChannelId to MsgContext type
  • src/auto-reply/reply/inbound-meta.ts — add thread_ts, message_ts, channel_id, is_thread_reply to inbound meta JSON; add extractChannelId() helper
  • src/infra/outbound/targets.ts — filter threadId in heartbeat delivery to explicit-only
  • src/infra/heartbeat-runner.returns-default-unset.test.ts — regression test for heartbeat thread leaking
  • prepare.inbound-contract.test.ts — update test to verify delta fetch behavior

Testing

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.

@openclaw-barnacle openclaw-barnacle Bot added channel: slack Channel integration: slack size: XS labels Feb 14, 2026
@markshields-tl
Copy link
Copy Markdown
Contributor Author

Closing — upstream fixed this natively in 2026.2.13 via #14976 (auto-inject implicit reply threading for replyToMode). Config already has replyToMode: "all" + thread.historyScope: "thread" + thread.inheritParent: true. No PR needed. †

@markshields-tl
Copy link
Copy Markdown
Contributor Author

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 !threadSessionPreviousTimestamp (first turn), so subsequent thread replies arrive with zero context. Issues #12742/#12586 remain open. †

@markshields-tl markshields-tl force-pushed the fix/slack-thread-context-subsequent-turns branch from 7ecc649 to e7f0b2c Compare February 16, 2026 13:41
@markshields-tl markshields-tl changed the title fix(slack): fetch thread history on subsequent turns via delta fetch fix(slack): fetch thread history on subsequent turns + expose thread context in inbound meta Feb 16, 2026
@markshields-tl markshields-tl force-pushed the fix/slack-thread-context-subsequent-turns branch 4 times, most recently from 8514073 to 9126a17 Compare February 16, 2026 23:13
@steipete steipete closed this Feb 16, 2026
@steipete steipete reopened this Feb 17, 2026
@markshields-tl markshields-tl force-pushed the fix/slack-thread-context-subsequent-turns branch from 9126a17 to 509a74e Compare February 17, 2026 13:45
@markshields-tl
Copy link
Copy Markdown
Contributor Author

Hello @steipete ! Could we get this approved/merged soon?

@markshields-tl markshields-tl force-pushed the fix/slack-thread-context-subsequent-turns branch 3 times, most recently from 8e1b3ed to a0cc18f Compare February 17, 2026 17:02
@markshields-tl
Copy link
Copy Markdown
Contributor Author

Hello @steipete ! Could we get this approved/merged soon?

Ah, I did not follow https://github.com/openclaw/openclaw/blob/main/CONTRIBUTING.md; I apologize.

I've updated the PR based on above.

@markshields-tl markshields-tl force-pushed the fix/slack-thread-context-subsequent-turns branch 2 times, most recently from cda8812 to e3761cb Compare February 17, 2026 19:11
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: telegram Channel integration: telegram app: ios App: ios size: S and removed size: XS labels Feb 17, 2026
@openclaw-barnacle openclaw-barnacle Bot added size: XS and removed docs Improvements or additions to documentation channel: telegram Channel integration: telegram app: ios App: ios size: S labels Feb 17, 2026
@markshields-tl markshields-tl force-pushed the fix/slack-thread-context-subsequent-turns branch 2 times, most recently from 3e32e13 to 9f1df3c Compare February 18, 2026 15:15
@markshields-tl markshields-tl changed the title fix(slack): fetch thread history on subsequent turns + expose thread context in inbound meta fix(slack): thread history on subsequent turns, inbound meta context, and heartbeat thread leaking Feb 19, 2026
@markshields-tl markshields-tl force-pushed the fix/slack-thread-context-subsequent-turns branch 6 times, most recently from 5d7de37 to 6677bb7 Compare February 20, 2026 21:06
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.
@markshields-tl
Copy link
Copy Markdown
Contributor Author

Closing — upstream independently implemented these fixes in #23843, #23839, #23836, and #23804, all shipped in v2026.2.22 stable. Our branch is fully superseded. †

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: slack Channel integration: slack size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Slack thread sessions don't load prior thread messages (including thread parent)

2 participants