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 field — dist/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 thread — dist/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 only — dist/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
- Extend
OutboundReplyPayload with thread metadata (threadName, threadKey) and spaceId where relevant. Populate at inbound webhook ingress from the Google Chat event payload.
- Include these fields in
createBlockReplyPayloadKey / createBlockReplyContentKey so coalescing and dedup are thread-aware.
- Extend
replyToConflict to also trigger on thread mismatch, not only replyToId mismatch.
- 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).
- Plumb thread through
FollowupRun.originatingThreadId for Google Chat — update the docstring and add dispatcher logic to restore thread on queue drain.
- 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.
Summary
In the
googlechatoutbound 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 separategooglechataccounts in one deployment (same box, distinctaccounts.*entries).Symptoms
typingIndicator: "message"shows the typing inside the thread; final answer posts to the space 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.
OutboundReplyPayloadhas no thread field —dist/plugin-sdk/src/plugin-sdk/reply-payload.d.ts:5-10:Google Chat's REST API exposes
thread.name/threadKeyseparately from message-level reply. Without either,spaces.messages.createdefaults to a new top-level message in the space.2. Coalescer keys and conflict detection ignore thread —
dist/block-reply-pipeline-BG5Rf5FK.js:createBlockReplyPayloadKey:replyToConflict:A chunk arriving with
replyToId=undefinedis 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 only —
dist/plugin-sdk/src/auto-reply/reply/queue/types.d.ts:36-37: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).textChunkLimit(4000 chars default). First chunk in-thread, subsequent chunks top-level.Suggested fix
OutboundReplyPayloadwith thread metadata (threadName,threadKey) andspaceIdwhere relevant. Populate at inbound webhook ingress from the Google Chat event payload.createBlockReplyPayloadKey/createBlockReplyContentKeyso coalescing and dedup are thread-aware.replyToConflictto also trigger on thread mismatch, not onlyreplyToIdmismatch.googlechatadapter'ssendPayload, which should setthread.name/threadKeyon everyspaces.messages.createcall within the reply lifecycle (initial + chunk + append + edit + final flush).FollowupRun.originatingThreadIdfor Google Chat — update the docstring and add dispatcher logic to restore thread on queue drain.Workaround
Setting
blockStreaming: falseon affected accounts disables the coalescer and eliminates most cross-thread leakage — each reply goes as a single send carrying its ownreplyToId. 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
v2026.4.12googlechataccounts withaudienceType: "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.