#Slack Subagents reporting to main and thread

1 messages · Page 1 of 1 (latest)

rugged topaz
#

Sometime this past week, the slack subagents started in a thread report their results to both the main channel and the thread. This results in duplicate posts, one in main slack channel and one in the thread, and a lot of noise in main channel. I've asked two different instances, and 4x models to try and solve it but we haven't made any progress. Any ideas?

twilit sirenBOT
gusty kelpBOT
#

I’m pretty sure this is a real regression, not your Slack setup.

If you’re on 2026.4.9, the best next step is to update to 2026.4.10+. I checked the recent fixes, and v2026.4.10 contains four changes that match your symptom almost exactly:

  • 110782a26a8b17685bdd777ebd12fe2ae955c0c5 - preserve thread routing in delivery context for Slack
  • 47c0a5135a0f37a07953128a1b69ac55d0044c7d - dedupe delivered subagent completion announces
  • 03f2951e63c444da80f049477b975314583c6dfe - preserve announce threadId on fallback
  • ac13b09b742d604c055221d65403fcfafabbeecc - keep subagent announces in the original thread

Also, the Slack sender path does not appear to use reply_broadcast, so this looks much more like duplicate sends / lost thread_ts than a Slack “broadcast reply to channel” config problem.

I’d do this first:

openclaw --version
openclaw update
rugged topaz
#

Summary: Sub-agent completion announcements post to the main Slack channel as top-level messages even when the originating conversation is in a thread. The thread_ts is lost somewhere in the announce delivery pipeline, resulting in duplicate messages — one correctly in-thread (from the agent's manual message tool relay) and one leaked to the main channel (from the announce delivery).

Environment:
• OpenClaw 2026.4.10 (44e5b62)
• Linux 6.17.0-1010-aws (x64)
• Slack Socket Mode
• Model: anthropic/claude-opus-4-6

Config (relevant):

replyToMode: "all"
replyToModeByChatType: { channel: "all", group: "all" }
streaming: { mode: "partial", nativeTransport: true }
groupPolicy: "allowlist"
channels: { "C0AJFUC6Z0R": { enabled: true } }
subagents: { maxConcurrent: 8, model: "anthropic/claude-sonnet-4-6" }
Repro:

  1. Reply in a Slack thread mentioning the bot
  2. From the thread session, spawn a sub-agent via sessions_spawn
  3. When completion arrives, use message tool with threadId to post result in-thread, then respond NO_REPLY
  4. Expected: One message in thread
  5. Actual: Two messages — one in thread (correct), one in main channel (leaked, no thread_ts)
#

Evidence:

Leaked main channel message (no thread_ts):

{ "ts": "1775936636.290499", "text": "âś… Round 3 complete..." }
Session store shows correct lastThreadId for both thread session AND sub-agent sessions:

Thread session: lastThreadId: 1775934442.601329 âś…
Sub-agent: lastThreadId: 1775934442.601329 âś…
Tests:
• Round 1 — default config → leaked
• Round 2 — careful NO_REPLY → leaked
• Round 3 — streaming.mode: "off" → leaked (NOT a streaming issue)

Source code trace (threadId propagation — all correct):

  1. Spawn: requesterOrigin.threadId from ctx.agentThreadId âś…
  2. Announce registry: resolveAnnounceOrigin() merges lastThreadId âś…
  3. Announce delivery: passes threadId to resolveExternalBestEffortDeliveryTarget() âś…
  4. Agent handler: explicitThreadId from request.threadId → resolveAgentDeliveryPlan() ✅
  5. Delivery plan: resolvedThreadId from baseDelivery.threadId âś…
  6. Reply transport: Slack resolveReplyTransport maps threadId → replyToId (thread_ts) ✅
  7. Outbound delivery: replyToId passed to deliverOutboundPayloads âś…
  8. Slack send: thread_ts: params.threadTs in chat.postMessage âś…

Despite every layer looking correct, the Slack API receives messages without thread_ts.

#
  1. NO_REPLY doesn't suppress announce delivery: The agent responds NO_REPLY but the announce's deliver: true agent call still delivers. Suppression may not apply to announce-mode dispatchReplyFromConfig runs, or delivery may fire before the model produces NO_REPLY.
  2. Delivery target resolution drops threadId: resolveChatSendOriginatingRoute (server.impl:10294) has multiple fallback paths. For announce-sourced agent calls, the explicitOrigin path may not be active, causing fallback that drops threadId under certain conditions (e.g. isFromWebchatClient gate at line 10333).
  3. Duplicate delivery path: Two separate delivery mechanisms firing — deliverAgentCommandResult (correct threadId) AND a separate dispatch path (without it).

Related commits in 2026.4.10 (supposed to fix this):
• 110782a — preserve thread routing in delivery context for Slack
• 47c0a51 — dedupe delivered subagent completion announces
• 03f2951 — preserve announce threadId on fallback
• ac13b09 — keep subagent announces in the original thread

Confirmed non-causes: Not reply_broadcast (no broadcast flag in sender path), not streaming, not session store (correct lastThreadId), not replyToMode.

gusty kelpBOT
#

Yeah, with that repro on 2026.4.10 (44e5b62), I think this is still a real bug.

A few reasons:

  • The subagents docs explicitly say that if the sub-agent’s latest assistant text is exact NO_REPLY, the announce output should be suppressed: https://docs.openclaw.ai/tools/subagents
  • Slack send path does not look like it’s using reply_broadcast, so this probably is not Slack “broadcast reply to channel” behavior
  • That points back to the announce delivery path losing threadId or bypassing the NO_REPLY suppression somewhere in the follow-up agent delivery

So I’d narrow the likely bad path to:

  • src/agents/subagent-announce.ts
  • src/agents/subagent-announce-delivery.ts
  • src/gateway/server-methods/agent.ts

My guess is the bug is specifically in the case where the announce-triggered turn does:

  1. message.send(... threadId=...)
#
  1. final assistant reply = NO_REPLY

and OpenClaw still does an external top-level deliver on that same announce turn.

Two useful next steps:

  1. Quick diagnostic: temporarily set Slack streaming to off and retry once

    channels: {
      slack: {
        streaming: { mode: "off", nativeTransport: false }
      }
    }
    

    If the main-channel duplicate still appears, that rules out preview/streaming leakage and confirms it’s a real second delivery.

  2. File a GitHub issue with exactly the repro you pasted here, because this is now specific enough to be actionable:

#

https://github.com/openclaw/openclaw/issues

Issue title I’d use:

Slack subagent completion announce leaks top-level channel message when thread reply uses message tool + NO_REPLY

If you want, I can turn your repro into a tight ready-to-paste GitHub issue body.

#

Nice, this actually narrows it down a lot.

If your trace is right, then I’d stop blaming threadId propagation itself.

The key clue is this:

  • session store has the right lastThreadId
  • announce routing resolves the right threadId
  • reply transport maps it correctly
  • but Slack still gets a postMessage without thread_ts

That usually means the leaked post is not the same payload you traced. It’s probably a second outbound payload being generated later, with replyToId/threadId unset.

So my updated guess is:

  • one payload = the correct threaded relay
  • a second payload = the leaked top-level channel post
#

The places I’d focus on now are:

  • src/auto-reply/reply/reply-delivery.ts
  • src/auto-reply/reply/block-reply-pipeline.ts
  • src/infra/outbound/deliver.ts

Why those:

  • deliverOutboundPayloads() fans out multiple normalized payloads
  • block/final payload handling can treat same content with different replyToId as separate sends
  • your symptom, “same result once threaded, once unthreaded”, matches that shape almost perfectly

So I think the bug report should be framed as:

Slack subagent announce is producing two outbound payloads, and one of them loses threading before the final send, even though the main announce route still carries the correct threadId.

At this point I don’t think there’s a clean config workaround. I’d file it here with the evidence you already have:
https://github.com/openclaw/openclaw/issues

#

If you want, I can turn everything you’ve gathered into a tight ready-to-paste GitHub issue body.

#

Yep, this is a solid hypothesis set.

My ranking would be:

  1. #3 duplicate delivery path , most likely
  2. #1 NO_REPLY not suppressing announce delivery , very plausible
  3. #2 threadId dropped in route fallback , possible, but weaker

Why:

  • Your own trace already shows the “normal” threaded path carrying threadId correctly all the way down.
  • That makes a pure “threadId got lost in transit” bug less convincing.
  • The symptom, one threaded send and one top-level send, fits two outbound payloads / two delivery paths almost perfectly.

I’d frame it like this:

On announce-sourced agent runs, OpenClaw appears to emit a correct threaded delivery and a second unthreaded user delivery. NO_REPLY is not suppressing that second path.

#

One extra detail that supports this: in dispatch-from-config.ts, the route-reply side uses:

  • ctx.MessageThreadId
  • or thread parsed from the session key

not the normalized session-store route directly. So if the leaked post comes from a different dispatch path, #2 can still be involved, but probably as a side effect of #3, not the root bug.

If someone debugs this, the best checkpoints are:

  • dispatchReplyFromConfig()
  • dispatcher.sendFinalReply(...)
  • dispatcher.sendToolResult(...)
  • routeReplyToOriginating(...)
  • deliverOutboundPayloads(...)
  • sendSlackOutboundMessage(...)

Specifically, log for the announce turn:

#
  • payload text
  • replyToId
  • threadId
  • ctx.MessageThreadId
  • ctx.ExplicitDeliverRoute
  • whether it came from sendFinalReply vs tool/media/block reply vs route-reply

So yeah, I think you’ve basically narrowed it to “announce turn is double-delivering, and one branch is unthreaded”.

If you want, I can turn your findings into a polished GitHub issue body with the hypothesis ranking and repro.

rugged topaz
gusty kelpBOT
#

Related, yes. Same bug, probably not exactly.

What #64454 describes is the older 2026.4.9 shape:

  • subagent completion loses thread routing
  • result lands in the main Slack channel
  • no proper Slack subagent thread-target override

Your current 2026.4.10 repro looks like a follow-on / residual version of that problem:

  • one path is now correctly threaded
  • but a second path still leaks a top-level channel post
  • so it looks more like duplicate delivery after the original routing fix, not just “Slack never had thread routing”

So I’d describe it as:

#64454 is the same bug family, but your case looks like the post-fix leftover/regression edge.

#

In other words:

  • 64454 = “completion goes to main channel instead of thread”
  • your case = “threaded delivery works, but an extra unthreaded delivery still escapes”

I’d definitely link #64454 in any new issue and say “possibly related / not fully fixed by the 2026.4.10 thread-routing changes.”

If you want, I can draft the exact “related but not identical” GitHub issue text.

rugged topaz
#

yes please.

gusty kelpBOT
#

Yep, here’s a tight issue draft you can paste into GitHub:

Title

[Bug]: Slack subagent completion still leaks a top-level channel post after 2026.4.10 thread-routing fixes

Body

### Bug type

Behavior bug (incorrect output/state without crash)

### Summary

Slack subagent completions spawned from a thread still produce a duplicate top-level channel post on `2026.4.10`, even though thread routing appears to be preserved correctly in the main delivery path.
#

This looks related to #64454, but not identical.

- `#64454` described the older `2026.4.9` behavior where the completion simply landed in the main channel instead of the thread.
- On `2026.4.10`, I now get:
  - one correct in-thread message
  - one leaked top-level channel message

So this appears to be a remaining edge/regression after the 2026.4.10 fixes.

### OpenClaw version

`2026.4.10 (44e5b62)`

### Operating system
#

Linux 6.17.0-1010-aws (x64)

### Channel / provider

Slack Socket Mode

### Model

Router: `anthropic/claude-opus-4-6`  
Subagents: `anthropic/claude-sonnet-4-6`

Model choice does not appear relevant. This looks like delivery/routing logic.

### Relevant config
#

```json5
{
  channels: {
    slack: {
      replyToMode: "all",
      replyToModeByChatType: { channel: "all", group: "all" },
      streaming: { mode: "partial", nativeTransport: true },
      groupPolicy: "allowlist",
      channels: { "C0AJFUC6Z0R": { enabled: true } },
    },
  },
  subagents: {
    maxConcurrent: 8,
    model: "anthropic/claude-sonnet-4-6",
  },
#

}


I also tested with:

```json5
streaming: { mode: "off", nativeTransport: false }

and the leak still occurs, so this is not a streaming-only issue.

Steps to reproduce

  1. Reply inside an existing Slack thread mentioning the bot
  2. From that thread session, spawn a subagent via sessions_spawn
  3. When the completion arrives, the responding agent uses the message tool with the correct threadId to post the result in-thread, then returns exact NO_REPLY
#
4. Wait for completion delivery

### Expected behavior

Exactly one message in the originating Slack thread.

### Actual behavior

Two messages are produced:

- one correct in-thread message
- one leaked top-level channel message with no `thread_ts`

Example leaked message:

```json
#

{ "ts": "1775936636.290499", "text": "âś… Round 3 complete..." }


### Evidence

Session store has the correct `lastThreadId` for both the thread session and the subagent session:

- thread session: `1775934442.601329`
- subagent session: `1775934442.601329`

### What I verified

I traced thread propagation and it appears correct through the primary path:

1. spawn captures `requesterOrigin.threadId`
2. announce registry merges `lastThreadId`
#
3. announce delivery passes `threadId` into `resolveExternalBestEffortDeliveryTarget()`
4. agent handler receives explicit `request.threadId`
5. delivery plan resolves `threadId`
6. Slack reply transport maps thread id to `replyToId`
7. outbound delivery passes `replyToId`
8. Slack sender supports `thread_ts` on `chat.postMessage`

Despite that, Slack still receives a top-level post without `thread_ts`.

### Confirmed non-causes

- not `reply_broadcast`
- not Slack streaming
- not `replyToMode`
- not session-store thread state
#
- not a missing `lastThreadId`

### Suspected cause

This now looks less like pure thread-id loss and more like a **duplicate delivery path**:

- one payload is correctly threaded
- another payload is emitted separately and sent without `replyToId/threadId`

Two likely areas:

1. `NO_REPLY` is not fully suppressing announce-sourced `deliver: true` reply dispatch
2. announce-sourced runs are emitting both:
   - the intended threaded delivery
   - a second unthreaded final/user delivery
#

### Likely relevant files

- `src/agents/subagent-announce.ts`
- `src/agents/subagent-announce-delivery.ts`
- `src/gateway/server-methods/agent.ts`
- `src/auto-reply/reply/dispatch-from-config.ts`
- `src/auto-reply/reply/reply-delivery.ts`
- `src/infra/outbound/deliver.ts`
- `extensions/slack/src/outbound-adapter.ts`
- `extensions/slack/src/send.ts`

### Related issue

- #64454
#

This feels like the same bug family, but not the same exact failure mode:
- `#64454`: completion routed only to main channel
- this issue: threaded delivery now works, but an extra top-level post still leaks

If you file it, I’d link #64454 right near the top. Glad you chased this down, the repro is actually pretty damn solid.

rugged topaz