Skip to content

Google Chat outbound: thread/reply routing metadata leaks across streamed block replies #69422

Description

@rubensandrade-sketch

Summary

In the googlechat outbound channel, chunks of a single reply can land outside the thread of the inbound message (or split between thread and top-level of the space). The pipeline preserves the chat destination but not the thread target across chunks.

Affects: v2026.4.12 (likely earlier). Reproducible in DMs and spaces with threaded replies. Observed in production across three separate googlechat accounts in one deployment (same box, distinct accounts.* entries).

Symptoms

  • Case A: typingIndicator: "message" shows the typing inside the thread; final answer posts to the space top-level.
  • Case B: first chunk lands in the thread; continuation/append chunks land on the space top-level.
  • Queued follow-ups originating from a thread drain back to top-level.

Root cause (evidence)

Thread is not a first-class field of the outbound payload or the coalescer's identity. It is never carried forward between chunks of the same reply.

1. OutboundReplyPayload has no thread fielddist/plugin-sdk/src/plugin-sdk/reply-payload.d.ts:5-10:

export type OutboundReplyPayload = {
    text?: string;
    mediaUrls?: string[];
    mediaUrl?: string;
    replyToId?: string;   // no threadKey / thread.name
};

Google Chat's REST API exposes thread.name / threadKey separately from message-level reply. Without either, spaces.messages.create defaults to a new top-level message in the space.

2. Coalescer keys and conflict detection ignore threaddist/block-reply-pipeline-BG5Rf5FK.js:

  • L136-143 createBlockReplyPayloadKey:
    return JSON.stringify({ text, mediaList, replyToId: payload.replyToId ?? null });
  • L79 replyToConflict:
    const replyToConflict = Boolean(
      bufferText && payload.replyToId &&
      (!bufferReplyToId || bufferReplyToId !== payload.replyToId)
    );

A chunk arriving with replyToId=undefined is silently merged into a buffer whose intended thread may differ — on flush, the resulting send has no thread hint and defaults to top-level.

3. Follow-up routing type enumerates Telegram/Matrix onlydist/plugin-sdk/src/auto-reply/reply/queue/types.d.ts:36-37:

/** Thread id for reply routing (Telegram topic id or Matrix thread event id). */
originatingThreadId?: string | number;

Google Chat thread is not plumbed through FollowupRun, so queued/drained replies from a thread cannot be restored to their source thread.

Reproduction

Config: typingIndicator: "message", blockStreaming: true (default).

  • Case A: send a message inside a thread in a space where the bot is mentioned. Observe typing indicator in-thread; final answer posts top-level.
  • Case B: send a message long enough to cross textChunkLimit (4000 chars default). First chunk in-thread, subsequent chunks top-level.

Suggested fix

  1. Extend OutboundReplyPayload with thread metadata (threadName, threadKey) and spaceId where relevant. Populate at inbound webhook ingress from the Google Chat event payload.
  2. Include these fields in createBlockReplyPayloadKey / createBlockReplyContentKey so coalescing and dedup are thread-aware.
  3. Extend replyToConflict to also trigger on thread mismatch, not only replyToId mismatch.
  4. Pass thread through to the googlechat adapter's sendPayload, which should set thread.name / threadKey on every spaces.messages.create call within the reply lifecycle (initial + chunk + append + edit + final flush).
  5. Plumb thread through FollowupRun.originatingThreadId for Google Chat — update the docstring and add dispatcher logic to restore thread on queue drain.
  6. Hard stop: if a reply started in-thread and thread info is missing on a later chunk / edit / append / final flush, abort with an explicit log rather than silently sending top-level.

Workaround

Setting blockStreaming: false on affected accounts disables the coalescer and eliminates most cross-thread leakage — each reply goes as a single send carrying its own replyToId. Final-chunk continuation leakage can still occur via edit/append flows, but the symptom frequency drops dramatically. Trade-off: UX loses the streaming-typing experience.

Environment

  • openclaw v2026.4.12
  • Node 22 on Ubuntu 25.10
  • 3 googlechat accounts with audienceType: "app-url", service-account auth, typingIndicator: "message"

Credit

Original technical report authored by Igor Almeida (internal ops, downstream deployment). This issue re-frames that report against the current bundle source with specific file/line references for maintainer review.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority user-facing bug, regression, or broken workflow.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.staleMarked as stale due to inactivity

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions