openclaw - ✅(Solved) Fix [Slack] Subagent results lose thread_ts in DM assistant threads, breaking conversation continuity [1 pull requests, 1 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#63585Fetched 2026-04-10 03:42:41
View on GitHub
Comments
1
Participants
1
Timeline
4
Reactions
1
Participants
Timeline (top)
closed ×1commented ×1cross-referenced ×1referenced ×1

When Slack's "Agents & Assistants" feature is enabled, bot DM messages arrive with thread_ts (assistant thread context). The first agent response correctly replies within the same thread. However, when a subagent completes and the main agent relays the result, the thread_ts is lost — causing the result to appear as a new top-level message or new assistant thread instead of a reply in the original conversation thread.

This breaks conversation continuity in orchestrator patterns where a main agent delegates to specialist sub-agents and reports results back to the user.

Root Cause

Three compounding issues:

Fix Action

Workaround

We implemented a globalThis cache that preserves the DM thread_ts across turns:

On inbound (provider): cache thread_ts for DM channels

if (incomingThreadTs && isSlackDirectMessageChannel(message.channel)) {
    globalThis.__openclaw_dm_thread_cache = globalThis.__openclaw_dm_thread_cache || {};
    globalThis.__openclaw_dm_thread_cache[message.channel] = incomingThreadTs;
}

On outbound (send): restore cached thread_ts when missing for DM channels

if (!params.threadTs && params.channelId?.startsWith("D") && globalThis.__openclaw_dm_thread_cache?.[params.channelId]) {
    params = { ...params, threadTs: globalThis.__openclaw_dm_thread_cache[params.channelId] };
}

Additionally, removeThreadFromDeliveryContext call was bypassed to preserve stored threadId in session context.

PR fix notes

PR #63592: fix(slack): preserve DM thread_ts across subagent result turns

Description (problem / solution / changelog)

Summary

Fixes #63585

When Slack's "Agents & Assistants" feature is enabled, DM messages arrive with thread_ts (assistant thread context). The first agent response correctly replies within the same thread. However, when a subagent completes and the main agent relays the result, the thread_ts is lost — causing the result to appear as a new top-level message instead of a reply in the original conversation thread.

Changes

extensions/slack/src/monitor/message-handler/dispatch.ts

  • Cache DM thread_ts on inbound: When a DM message arrives with thread_ts, store it in a global cache keyed by channel ID
  • Fallback in deliverNormally: When replyPlan.nextThreadTs() returns undefined for a DM channel, fall back to the cached thread_ts

extensions/slack/src/send.ts

  • Cache read/write in postSlackMessageBestEffort: Store threadTs when present for DM channels, restore from cache when missing

src/config/sessions/store.ts

  • Preserve session threadId: Stop calling removeThreadFromDeliveryContext which stripped the stored threadId when subagent results updated the session without an explicit thread value

Why a global cache?

Subagent result turns are processed as internal events without a Slack inbound message, so there is no message.thread_ts available. The replyPlan is freshly created with no incomingThreadTs, and the session's threadId was being stripped by removeThreadFromDeliveryContext. The global cache bridges this gap by persisting the DM thread context across turns within the same gateway process.

A more robust solution could persist this in the session metadata directly, but the global cache approach is minimal and non-breaking.

Test plan

  1. Enable "Agents & Assistants" in Slack App settings
  2. Send a DM requesting a task that involves subagent delegation
  3. Verify the delegation acknowledgment appears in the assistant thread
  4. Verify the subagent result also appears in the same assistant thread (not as a new message)
  5. Verify channel (non-DM) threading behavior is unchanged

Changed files

  • extensions/slack/src/monitor/message-handler/prepare.ts (modified, +8/-1)
  • extensions/slack/src/send.ts (modified, +10/-1)
  • src/config/sessions/store.ts (modified, +4/-4)

Code Example

// store.ts - session update logic
const merged = mergeDeliveryContext(
  mergedInput,
  explicitThreadValue == null
    ? removeThreadFromDeliveryContext(deliveryContextFromSession(existing))  // ← strips threadId
    : deliveryContextFromSession(existing)
);

---

if (incomingThreadTs && isSlackDirectMessageChannel(message.channel)) {
    globalThis.__openclaw_dm_thread_cache = globalThis.__openclaw_dm_thread_cache || {};
    globalThis.__openclaw_dm_thread_cache[message.channel] = incomingThreadTs;
}

---

if (!params.threadTs && params.channelId?.startsWith("D") && globalThis.__openclaw_dm_thread_cache?.[params.channelId]) {
    params = { ...params, threadTs: globalThis.__openclaw_dm_thread_cache[params.channelId] };
}
RAW_BUFFERClick to expand / collapse

Summary

When Slack's "Agents & Assistants" feature is enabled, bot DM messages arrive with thread_ts (assistant thread context). The first agent response correctly replies within the same thread. However, when a subagent completes and the main agent relays the result, the thread_ts is lost — causing the result to appear as a new top-level message or new assistant thread instead of a reply in the original conversation thread.

This breaks conversation continuity in orchestrator patterns where a main agent delegates to specialist sub-agents and reports results back to the user.

Root Cause Analysis

Three compounding issues:

1. replyToMode: "first" consumes thread_ts on first reply

createReplyReferencePlanner in src/auto-reply/reply/reply-reference.ts marks hasReplied=true after the delegation acknowledgment. Subsequent responses (subagent results) receive undefined from nextThreadTs() because isSingleUseReplyToMode("first") returns true and hasReplied is already set.

Even with replyToMode: "all", the subagent result is processed as a new turn with a fresh replyPlan that has no incomingThreadTs.

2. removeThreadFromDeliveryContext strips stored threadId

In the session store (src/sessions/store.ts), when a subagent result updates the session without an explicit threadId in the delivery context, removeThreadFromDeliveryContext() deletes the previously stored thread context:

// store.ts - session update logic
const merged = mergeDeliveryContext(
  mergedInput,
  explicitThreadValue == null
    ? removeThreadFromDeliveryContext(deliveryContextFromSession(existing))  // ← strips threadId
    : deliveryContextFromSession(existing)
);

3. Subagent result turns lack original thread_ts

When a subagent completes, the gateway processes the result as a new agent turn. The new createSlackReplyDeliveryPlan receives no incomingThreadTs (since it's an internal event, not a Slack inbound message), so nextThreadTs() returns undefined.

Workaround

We implemented a globalThis cache that preserves the DM thread_ts across turns:

On inbound (provider): cache thread_ts for DM channels

if (incomingThreadTs && isSlackDirectMessageChannel(message.channel)) {
    globalThis.__openclaw_dm_thread_cache = globalThis.__openclaw_dm_thread_cache || {};
    globalThis.__openclaw_dm_thread_cache[message.channel] = incomingThreadTs;
}

On outbound (send): restore cached thread_ts when missing for DM channels

if (!params.threadTs && params.channelId?.startsWith("D") && globalThis.__openclaw_dm_thread_cache?.[params.channelId]) {
    params = { ...params, threadTs: globalThis.__openclaw_dm_thread_cache[params.channelId] };
}

Additionally, removeThreadFromDeliveryContext call was bypassed to preserve stored threadId in session context.

Proposed Proper Fix

  1. Persist thread_ts in session metadata so subagent result turns can access the originating thread context
  2. Pass originating thread_ts through the subagent spawn/result pipeline so the relay response inherits the parent turn's thread context
  3. Remove or condition removeThreadFromDeliveryContext to not strip threadId when the session already has a valid thread context from a previous turn

Reproduction

  1. Enable "Agents & Assistants" in Slack App settings
  2. Configure an orchestrator agent (main) with sub-agents (e.g., specialist workers)
  3. Send a DM to the bot requesting a task that requires subagent delegation (e.g., "Ask Lee to summarize recent bugs")
  4. Expected: Main agent's delegation acknowledgment AND final result report both appear in the same assistant thread
  5. Actual: Delegation acknowledgment appears in the thread, but the subagent result appears as a new top-level message

Environment

  • OpenClaw: v2026.4.8 (also affects v2026.4.5, v2026.4.7)
  • Platform: macOS (local gateway mode)
  • Slack: Socket Mode with Agents & Assistants enabled
  • Agent pattern: Multi-agent orchestration with sessions_spawn

extent analysis

TL;DR

To fix the issue of lost thread_ts when a subagent completes and the main agent relays the result, persist the thread_ts in session metadata and pass it through the subagent spawn/result pipeline.

Guidance

  • Identify and modify the createReplyReferencePlanner function in src/auto-reply/reply/reply-reference.ts to handle replyToMode: "all" correctly and preserve the thread_ts for subsequent responses.
  • Update the removeThreadFromDeliveryContext function in src/sessions/store.ts to conditionally remove the stored threadId only when necessary, ensuring that the thread context is preserved for subagent results.
  • Implement a mechanism to pass the originating thread_ts through the subagent spawn/result pipeline, allowing the relay response to inherit the parent turn's thread context.

Example

// Example of persisting thread_ts in session metadata
const sessionMetadata = {
  threadTs: incomingThreadTs,
  // Other metadata...
};

// Example of passing originating thread_ts through subagent spawn/result pipeline
const subagentResult = {
  threadTs: sessionMetadata.threadTs,
  // Other result data...
};

Notes

The provided workaround using a globalThis cache may have limitations and potential issues, such as cache management and thread safety. A proper fix should focus on persisting the thread_ts in session metadata and passing it through the subagent pipeline.

Recommendation

Apply the proposed proper fix by persisting thread_ts in session metadata and passing it through the subagent spawn/result pipeline. This approach ensures that the thread context is preserved and allows for a more robust and reliable solution.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING