← Back to PRs

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

by markshields-tl open 2026-02-14 12:37 View on GitHub →
channel: slack size: XS
## 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