Skip to content

[Bug]: Subagent announce delivers to wrong Telegram DM topic (stale threadId) #52217

@Lukavyi

Description

@Lukavyi

Bug Description

When a subagent is spawned from a Telegram DM topic conversation (using sessions_spawn), the completion announce is delivered to the main chat / wrong topic instead of the originating topic.

Steps to Reproduce

  1. Open a DM topic with the bot (e.g. topic ID 1045687)
  2. Trigger a sessions_spawn subagent from that topic
  3. Wait for subagent to complete
  4. The announce result appears in the main chat (General topic or a different topic), not in topic 1045687

Expected Behavior

The announce should be delivered to the same topic where the spawn was triggered, using the threadId captured in requesterOrigin at spawn time.

Actual Behavior

The announce is delivered to the main chat without message_thread_id, or to a stale topic from the session entry's lastThreadId.

Analysis

The requesterOrigin correctly captures threadId at spawn time in spawnSubagentDirect() (line ~333 in subagent-spawn.ts):

const requesterOrigin = normalizeDeliveryContext({
  channel: ctx.agentChannel,
  accountId: ctx.agentAccountId,
  to: ctx.agentTo,
  threadId: ctx.agentThreadId,
});

However, at announce time in runSubagentAnnounceFlow(), the main session entry (agent:main:main) may have a stale lastThreadId / deliveryContext.threadId from a different topic interaction that happened between spawn and completion.

When expectsCompletionMessage=true (the default), the flow goes through resolveSubagentCompletionOrigin() -> createBoundDeliveryRouter().resolveDestination(). For non-thread-bound spawns, this falls back correctly to requesterOrigin. But the final delivery path through sendSubagentAnnounceDirectly() may lose the threadId.

Additionally, there is a guard in resolveAnnounceOrigin() (added in commit 8178ea472d for Discord threading) that actively strips threadId from the fallback entry when the requester has to but no threadId:

const entryForMerge =
    normalizedRequester?.to &&
    normalizedRequester.threadId == null &&
    normalizedEntry?.threadId != null
      ? (() => {
          const { threadId: _ignore, ...rest } = normalizedEntry;
          return rest;
        })()
      : normalizedEntry;

Observability Gap

The gateway log only shows [telegram] sendMessage ok chat=63448508 message=XXXXX without the message_thread_id parameter, making it impossible to diagnose from logs alone whether the threadId was passed correctly.

Suggested Fix

  1. Logging: Include message_thread_id (if present) in the sendMessage ok log line
  2. Root cause: Ensure the threadId from requesterOrigin survives the full announce pipeline for Telegram DM topics (verify with a test case)

Environment

  • OpenClaw version: latest (March 2026)
  • Channel: Telegram (DM with bot topics enabled)
  • Session type: main session (agent:main:main) with DM topics

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions