#16186: fix(slack): thread history on subsequent turns, inbound meta context, and heartbeat thread leaking
channel: slack
size: XS
Cluster:
Slack Thread Management Improvements
## 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:
```typescript
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.
Most Similar PRs
#19083: Slack: preserve per-thread context and consistent thread replies
by jkimbo · 2026-02-17
82.6%
#20389: fix(slack): inject thread history on first thread turn, not only on...
by lafawnduh1966 · 2026-02-18
81.7%
#10686: fix(slack): use thread-level sessions for channels to prevent conte...
by pablohrcarvalho · 2026-02-06
81.6%
#22485: fix(slack): use threadId from delivery context as threadTs fallback...
by dorukardahan · 2026-02-21
81.1%
#22433: Slack: fix thread context loss after session reset
by stgarrity · 2026-02-21
80.3%
#23320: fix(slack): respect replyToMode when incomingThreadTs is auto-created
by dorukardahan · 2026-02-22
79.4%
#22982: fix: prevent stale threadId from routing subagent announces to wron...
by unboxed-ai · 2026-02-21
78.7%
#15969: fix: per-thread session isolation for Slack DMs when replyToMode is...
by neeravmakwana · 2026-02-14
78.6%
#14720: fix(slack): pass threadId in plugin read action (#14706)
by lailoo · 2026-02-12
78.3%
#20406: fix(slack): respect replyToMode when computing statusThreadTs in DMs
by QuinnYates · 2026-02-18
78.0%