Skip to content

fix(googlechat): preserve thread reply target through delivery#74235

Open
jai wants to merge 23 commits into
openclaw:mainfrom
jai:jaig-codex/fix-googlechat-thread-replies-upstream
Open

fix(googlechat): preserve thread reply target through delivery#74235
jai wants to merge 23 commits into
openclaw:mainfrom
jai:jaig-codex/fix-googlechat-thread-replies-upstream

Conversation

@jai

@jai jai commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Problem: Google Chat replies could leave the inbound Chat thread and appear as new top-level messages.
  • Why it matters: delayed or tool-driven assistant replies break group-thread UX when they detach from the conversation the user spoke into.
  • What changed: preserve message.thread.name as MessageThreadId, expose it to reply/message-tool threading, and normalize Google Chat outbound thread targets before they reach thread.name.
  • What did NOT change: no Google Chat per-thread OpenClaw session isolation; core reply plumbing remains channel-agnostic and Google Chat resource-name handling stays behind the Google Chat boundary.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Real behavior proof (required for external PRs)

  • Behavior or issue addressed: Google Chat assistant replies from existing Chat threads should remain in the inbound thread.
  • Real environment tested: Linux VPS-hosted OpenClaw Google Chat runtime with service-account auth and a real Google Chat space.
  • Exact steps or command run after this patch: Jai sent real Google Chat threaded messages and confirmed assistant replies stayed in-thread before this PR was marked ready.
  • Evidence after fix: screenshot evidence is attached below:
    • Before: Google Chat reply appeared outside the inbound thread.
    • After: Google Chat reply stayed inside the inbound thread.
    • Scope note: typing-placeholder editing is separate follow-up behavior; this PR fixes thread placement.
  • Observed result after fix: live Google Chat replies stayed in-thread; local seam tests also verified natural replies, message-tool sends, and current-reply behavior use the inbound Google Chat thread resource.
  • What was not tested: every Google Chat account/space policy combination; typing-placeholder edit/cleanup for message-tool sends; Google Chat per-thread OpenClaw session isolation, which is out of scope.
  • Before evidence: prior behavior re-rooted replies as new top-level Chat messages.

Root Cause (if applicable)

  • Root cause: Google Chat exposes different resource shapes for messages (spaces/.../messages/...) and threads (spaces/.../threads/...), and OpenClaw previously let message-shaped values flow into thread-routing seams.
  • Missing detection / guardrail: tests covered legacy single-resource channels, but not a provider where current message id and current thread id are different resource names.
  • Contributing context:
    • Inbound webhook context did not populate generic MessageThreadId from message.thread.name.
    • The message-tool path had no inbound-thread default when the agent omitted threadId.
    • [[reply_to_current]] resolves to the current message id; for Google Chat that value must be converted to the inbound thread before delivery.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • src/auto-reply/reply/reply-plumbing.test.ts
    • src/auto-reply/reply/agent-runner-payloads.test.ts
    • src/auto-reply/reply/agent-runner-utils.test.ts
    • src/auto-reply/reply/reply-delivery.test.ts
    • extensions/googlechat/src/monitor.webhook-routing.test.ts
    • extensions/googlechat/src/monitor.reply-delivery.test.ts
    • extensions/googlechat/src/channel.test.ts
    • extensions/googlechat/src/actions.test.ts
    • extensions/googlechat/src/targets.test.ts
  • Scenario the test should lock in: Google Chat has a current message resource and a distinct current thread resource; implicit replies and message-tool sends use the thread resource, while message-shaped reply ids are rewritten or rejected before reaching the Chat API.
  • Why this is the smallest reliable guardrail: it covers webhook ingress, core reply seams, message-tool routing, channel delivery, and the API boundary without requiring live Google Chat in CI.
  • Existing test that already covers this (if any): none.
  • If no new test is added, why not: N/A.

User-visible / Behavior Changes

Google Chat assistant replies from an existing Chat thread now stay in that thread instead of creating a new top-level message.

Diagram (if applicable)

Before:
Google Chat webhook -> currentMessageId=spaces/.../messages/...
                    -> delivery sends that as thread.name
                    -> Chat API rejects/falls back -> new top-level thread

After:
Google Chat webhook -> MessageThreadId=spaces/.../threads/...
                    -> reply/tool delivery resolves outbound thread
                    -> Chat API receives thread.name=spaces/.../threads/...

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: macOS local checkout; Linux VPS-hosted runtime for real Chat verification
  • Runtime/container: OpenClaw source checkout; local verification on exact rebased head a617644c81dd43bb7285d3001ca6d19750e62e68; live Google Chat verification on a Linux VPS-hosted runtime with the thread-placement patch applied
  • Model/provider: N/A
  • Integration/channel (if any): Google Chat service-account auth
  • Relevant config (redacted): Google Chat enabled with group mention policy; no secret/config changes required

Steps

  1. Send a threaded Google Chat message that mentions the bot.
  2. Let the assistant produce a normal final reply.
  3. Exercise message-tool sends without explicit threadId.
  4. Exercise [[reply_to_current]] behavior where the core seam supplies the current message id.

Expected

  • Replies and message-tool sends stay in the original Google Chat thread.
  • Explicit thread-shaped ids are preserved.
  • Message-shaped or malformed thread targets never reach thread.name as-is.

Actual

  • Before this patch: replies could re-root as new top-level Chat messages.
  • After this patch: live verification and local seam coverage keep replies in the inbound thread.
  • Scope note: typing-placeholder editing is separate follow-up behavior and is not fixed by this PR.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Screenshot evidence attached below:

  • Before screenshot: Google Chat reply escaped the inbound thread and appeared outside the original conversation thread.
  • After screenshot: Google Chat reply stayed inside the inbound thread.
  • Scope note: the after screenshot may still show a stale typing placeholder when the message-tool path is used; typing-placeholder edit/cleanup is not part of this PR.

Verification commands run on exact rebased head a617644c81dd43bb7285d3001ca6d19750e62e68 after the May 26 rebase:

mise exec [email protected] -- node scripts/docs-list.js
# passed

mise exec [email protected] -- ./node_modules/.bin/oxfmt --check --threads=1 CHANGELOG.md extensions/googlechat/src/actions.test.ts extensions/googlechat/src/actions.ts extensions/googlechat/src/api.ts extensions/googlechat/src/channel.adapters.ts extensions/googlechat/src/channel.test.ts extensions/googlechat/src/google-auth.runtime.test.ts extensions/googlechat/src/google-auth.runtime.ts extensions/googlechat/src/monitor-reply-delivery.ts extensions/googlechat/src/monitor.reply-delivery.test.ts extensions/googlechat/src/monitor.ts extensions/googlechat/src/monitor.webhook-routing.test.ts extensions/googlechat/src/targets.test.ts extensions/googlechat/src/thread-resource.ts src/auto-reply/reply/agent-runner-execution.test.ts src/auto-reply/reply/agent-runner-execution.ts src/auto-reply/reply/agent-runner-payloads.test.ts src/auto-reply/reply/agent-runner-payloads.ts src/auto-reply/reply/agent-runner-utils.test.ts src/auto-reply/reply/agent-runner-utils.ts src/auto-reply/reply/agent-runner.ts src/auto-reply/reply/reply-delivery.test.ts src/auto-reply/reply/reply-delivery.ts src/auto-reply/reply/reply-payloads-base.ts src/auto-reply/reply/reply-plumbing.test.ts
# All matched files use the correct format.

mise exec [email protected] -- node scripts/test-projects.mjs extensions/googlechat/src/actions.test.ts extensions/googlechat/src/channel.test.ts extensions/googlechat/src/google-auth.runtime.test.ts extensions/googlechat/src/monitor.reply-delivery.test.ts extensions/googlechat/src/monitor.webhook-routing.test.ts extensions/googlechat/src/targets.test.ts src/auto-reply/reply/agent-runner-execution.test.ts src/auto-reply/reply/agent-runner-payloads.test.ts src/auto-reply/reply/agent-runner-utils.test.ts src/auto-reply/reply/reply-delivery.test.ts src/auto-reply/reply/reply-plumbing.test.ts
# 2 Vitest shards passed; 330 tests passed

mise exec [email protected] -- node scripts/changed-lanes.mjs --json
# lanes=core, coreTests, extensions, extensionTests, docs

Additional broad test signal from pre-ready validation on a prior PR head:

pnpm test src/auto-reply/reply extensions/googlechat extensions/telegram
# auto-reply: 113 files, 1508 tests passed
# telegram: 106 files, 1483 tests passed
# googlechat shard was rerun after fixture alignment: 15 files, 141 tests passed

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • Jai verified live Google Chat thread placement on a real Google Chat runtime and attached before/after screenshots below.
    • I verified the exact rebased code locally with the commands listed above.
  • Edge cases checked:
    • [[reply_to_current]] can still resolve to the core current message id; Google Chat rewrites message-shaped values at the channel boundary.
    • Explicit thread-shaped replyToId wins over the inbound thread.
    • Malformed thread values are rejected before reaching the Google Chat send API.
    • Message-tool sends without threadId use the inbound thread only when the current target matches and reply mode allows implicit threading.
  • What you did not verify:
    • Every Google Chat account/space policy combination.
    • Per-thread OpenClaw session isolation for Google Chat; this PR does not implement it.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: callers that accidentally pass a Google Chat message resource as thread now fail loudly instead of silently falling back.
    • Mitigation: channel delivery rewrites known message-resource reply targets to the inbound thread; the API guard reports invalid values before sending.
  • Risk: a future non-message, non-thread Google Chat resource shape could appear in replyToId.
    • Mitigation: only spaces/.../threads/... passes as thread.name; everything else is dropped or falls back to the inbound thread.

Screenshots (Bad, Good)

Bad (before patch)

openclaw-google-chat-bad

Good (after patch)

openclaw-google-chat-good

@openclaw-barnacle openclaw-barnacle Bot added channel: googlechat Channel integration: googlechat size: L labels Apr 29, 2026
@clawsweeper

clawsweeper Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: found issues before merge. Reviewed July 3, 2026, 4:10 PM ET / 20:10 UTC.

Summary
The branch preserves Google Chat thread resource names through inbound context, reply delivery, message-tool sends, outbound validation, and regression tests, while also touching Google Chat auth transport code and CHANGELOG.md.

PR surface: Source +296, Tests +1374, Docs +1. Total +1671 across 27 files.

Reproducibility: yes. Source inspection shows current main and v2026.6.11 still pass Google Chat replyToId values directly into thread delivery, and the PR discussion includes live before/after screenshots plus a production confirmation of message-shaped resources escaping the inbound thread.

Review metrics: 2 noteworthy metrics.

  • Auth-provider surface: 1 runtime helper changed. The branch changes Google Chat auth transport dependency selection, which is outside the stated thread-delivery fix and affects upgrade behavior.
  • Release-owned changelog: 1 entry added. CHANGELOG.md is release-owned by repository policy, so this entry should not ride along in a normal runtime fix PR.

Root-cause cluster
Relationship: same_root_cause
Canonical: #89790
Summary: This PR and the open replacement PR share the Google Chat thread-routing root cause; the replacement is the current clean landing candidate, but it is not merged, so this source PR should stay open rather than close as superseded.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦐 gold shrimp
Proof: 🦞 diamond lobster ✨ media proof bonus
Patch quality: 🦐 gold shrimp
Result: needs maintainer review before merge.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

Proof path suggestion
A refreshed branch would benefit from exact-head visual proof that Google Chat replies remain in the inbound thread rather than posting top-level. Mantis is currently scoped to Telegram, Discord, and web UI chat proof, so it is not the right proof path for this surface.
Use maintainer screenshot/manual proof, browser or Playwright proof, Crabbox where appropriate, or normal local artifact proof instead.

Risk before merge

  • [P1] The branch is currently conflicting/dirty, so maintainers cannot inspect or merge the exact three-way result until it is refreshed.
  • [P1] The PR changes Google Chat auth transport selection inside a thread-delivery fix, overlapping fix(googlechat): fix googlechat grant_type #78554 and turning a message-routing merge into an auth-provider decision.
  • [P1] The stricter Google Chat thread-resource validation is likely the right boundary, but it can turn previously tolerated malformed or message-shaped thread inputs into local send errors for existing setups.
  • [P1] The direct CHANGELOG.md edit is release-owned churn and a likely conflict point for a normal runtime fix PR.

Maintainer options:

  1. Rebase and trim this source branch (recommended)
    Refresh this PR against current main and remove the auth transport plus CHANGELOG.md changes so the diff only owns Google Chat thread delivery and focused tests.
  2. Proceed with the clean replacement
    If maintainers accept or override proof for fix(googlechat): preserve thread targets through reply delivery #89790, use that clean branch as the canonical landing path while preserving credit from this source PR.
  3. Hold for the auth-provider decision
    Pause this branch until fix(googlechat): fix googlechat grant_type #78554 or an equivalent maintainer-approved auth repair resolves the Gaxios transport question separately.

Next step before merge

  • [P2] Maintainers need to choose the canonical Google Chat thread-routing branch and proof stance, then either refresh this source PR or proceed with the clean replacement.

Security
Cleared: No concrete security or supply-chain issue was found; the auth runtime change is a functional auth-provider merge risk, not a new dependency or secret-handling expansion.

Review findings

  • [P1] Remove the auth transport change from this thread fix — extensions/googlechat/src/google-auth.runtime.ts:526
  • [P2] Remove the release-owned changelog edit — CHANGELOG.md:4324
Review details

Best possible solution:

Land one narrow Google Chat thread-routing fix after maintainers choose this proof-backed source branch or the clean replacement PR; the landing branch should be current, thread-only, and free of auth-provider and CHANGELOG.md churn.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection shows current main and v2026.6.11 still pass Google Chat replyToId values directly into thread delivery, and the PR discussion includes live before/after screenshots plus a production confirmation of message-shaped resources escaping the inbound thread.

Is this the best way to solve the issue?

No, not as currently submitted. The thread-routing fix is plausible and well-proven, but the best merge shape is a rebased thread-only branch or the clean replacement PR after maintainers settle proof and canonical ownership.

Full review comments:

  • [P1] Remove the auth transport change from this thread fix — extensions/googlechat/src/google-auth.runtime.ts:526
    This PR is scoped to Google Chat thread delivery, but this hunk changes the Google auth runtime to prefer google-auth-library's bundled Gaxios over the direct gaxios import. That auth-provider repair is already tracked separately at fix(googlechat): fix googlechat grant_type #78554; keeping it here makes a message-routing merge also decide a conflicting auth dependency contract.
    Confidence: 0.88
  • [P2] Remove the release-owned changelog edit — CHANGELOG.md:4324
    CHANGELOG.md is release-owned in this repository; normal PRs should keep release-note context in the PR body or commit message. This direct entry adds avoidable conflict churn to a runtime fix and should be dropped before merge.
    Confidence: 0.9

Overall correctness: patch is incorrect
Overall confidence: 0.88

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 5361e5a0b455.

Label changes

Label justifications:

  • P1: The PR targets a confirmed Google Chat workflow regression where assistant replies can visibly land outside the intended thread.
  • merge-risk: 🚨 compatibility: The patch tightens accepted Google Chat thread resource shapes and can make malformed or message-shaped inputs fail instead of silently falling back.
  • merge-risk: 🚨 message-delivery: The touched monitor, delivery, adapter, and message-tool paths directly determine whether Google Chat replies land in the intended thread or top-level space.
  • merge-risk: 🚨 auth-provider: The branch changes Google Chat auth runtime Gaxios selection even though that auth repair is already tracked separately.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦞 diamond lobster and patch quality is 🦐 gold shrimp.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (screenshot): The PR body includes before/after Google Chat screenshots and live VPS runtime steps that directly show after-fix thread placement, though a rebase would need fresh exact-head confidence.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes before/after Google Chat screenshots and live VPS runtime steps that directly show after-fix thread placement, though a rebase would need fresh exact-head confidence.
  • proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. The PR body includes before/after Google Chat screenshots and live VPS runtime steps that directly show after-fix thread placement, though a rebase would need fresh exact-head confidence.
Evidence reviewed

PR surface:

Source +296, Tests +1374, Docs +1. Total +1671 across 27 files.

View PR surface stats
Area Files Added Removed Net
Source 13 333 37 +296
Tests 13 1408 34 +1374
Docs 1 1 0 +1
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 27 1742 71 +1671

What I checked:

  • Repository policy read: Root AGENTS.md was read and applied; channel delivery, auth/provider behavior, compatibility-sensitive fallback changes, and CHANGELOG.md are all review-sensitive for this PR. (AGENTS.md:1, 5361e5a0b455)
  • Scoped extension policy read: extensions/AGENTS.md was read and applied; Google Chat is a bundled plugin and the relevant changes stay inside the plugin boundary except for shared auto-reply plumbing. (extensions/AGENTS.md:1, 5361e5a0b455)
  • Current main still forwards raw thread values: sendGoogleChatMessage still writes any truthy thread value directly into body.thread.name and sets messageReplyOption, with no thread-resource validation. (extensions/googlechat/src/api.ts:162, 5361e5a0b455)
  • Current main delivery still uses replyToId as Google Chat thread: deliverGoogleChatReply still sends text and media with thread: payload.replyToId, so a message-shaped reply target can reach the Google Chat API thread field. (extensions/googlechat/src/monitor-reply-delivery.ts:75, 5361e5a0b455)
  • Current main message-tool path lacks the PR's inbound-thread default: The Google Chat message action handler currently uses only explicit threadId/replyTo values and does not default same-target tool sends to the inbound Google Chat thread resource. (extensions/googlechat/src/actions.ts:127, 5361e5a0b455)
  • Latest release is still affected: v2026.6.11 has the same payload.replyToId delivery and unvalidated sendGoogleChatMessage thread behavior, so the fix is not already shipped. (extensions/googlechat/src/monitor-reply-delivery.ts:75, e085fa1a3ffd)

Likely related people:

  • steipete: Recent Google Chat reply-delivery/API commits extracted the delivery module, preserved reply text after typing update failures, and landed earlier thread reply option support from a contributor PR. (role: recent area contributor and merger; confidence: high; commits: 4f91d81e1dbc, cdbed3c9b1c4, 1507a9701b83; files: extensions/googlechat/src/monitor-reply-delivery.ts, extensions/googlechat/src/api.ts, extensions/googlechat/src/monitor.ts)
  • jailbirt: Recent Google Chat work suppressed thread sends in DMs and changed group-only thread metadata behavior near the monitor and channel adapter paths implicated here. (role: recent adjacent contributor; confidence: high; commits: da822dd28bf8, 696c62400803; files: extensions/googlechat/src/monitor.ts, extensions/googlechat/src/channel.adapters.ts)
  • franco-viotti: Recent merged Google Chat work preserved thread targets for message-tool replies and returned sent thread metadata from the Chat API boundary. (role: recent adjacent contributor; confidence: high; commits: a71b121c69f0, 7305d9bacaec; files: extensions/googlechat/src/channel.adapters.ts, extensions/googlechat/src/api.ts, extensions/googlechat/src/channel.test.ts)
  • kevinslin: Recent Google Chat native approval work touched monitor, reply delivery, durable delivery, and API behavior around visible Google Chat message delivery. (role: recent feature contributor; confidence: medium; commits: d7759c6a35ac; files: extensions/googlechat/src/monitor.ts, extensions/googlechat/src/monitor-reply-delivery.ts, extensions/googlechat/src/api.ts)
  • novan: The earlier Google Chat messageReplyOption support was landed from novan's contributor PR and remains part of the thread-send behavior this PR builds on. (role: prior thread-reply groundwork contributor; confidence: medium; commits: cdbed3c9b1c4; files: extensions/googlechat/src/api.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@jai
jai marked this pull request as ready for review April 29, 2026 11:13
@greptile-apps

greptile-apps Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes three independent code paths that caused Google Chat assistant replies to escape the inbound thread and re-root as new top-level messages. It adds a MessageThreadId field to the inbound context, introduces a centralized isGoogleChatThreadResourceName validator that gates every path flowing into thread.name, and wires a channel-side delivery layer (resolveOutboundThread / resolveReplyTransport) that swaps message-resource replyToId values to the validated inbound thread before they reach the Chat API.

Only one minor finding:

  • The thread-resource regex /^spaces\\/[^/]+\\/threads\\/[^/]+$/ is defined both in api.ts (exported as isGoogleChatThreadResourceName) and independently in channel.adapters.ts (local GOOGLE_CHAT_THREAD_RESOURCE_RE). The PR description calls the api.ts export the "centralized validator," but channel.adapters.ts cannot import it due to the lazy-runtime boundary, so it carries a silent copy. If the pattern ever changes both copies must be updated in sync.

Confidence Score: 4/5

Safe to merge; all three bug-causing paths are closed by layered validation and the outbound thread resolver, with comprehensive unit and integration test coverage.

No P0 or P1 issues found. The only finding is a P2 style concern about the thread-resource regex being duplicated between api.ts and channel.adapters.ts. The fix is architecturally clean, well-tested (21 files, explicit seam tests for each bug scenario), and backward compatible.

No files require special attention beyond the regex duplication noted in channel.adapters.ts.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/googlechat/src/channel.adapters.ts
Line: 39-44

Comment:
**Duplicate thread resource regex**

`GOOGLE_CHAT_THREAD_RESOURCE_RE` here is the same pattern as the `isGoogleChatThreadResourceName` export in `api.ts`. The PR description calls `api.ts` the "centralized validator," but `channel.adapters.ts` cannot import it directly (lazy-runtime boundary) and so carries its own copy. This is a silent divergence risk: if the pattern ever needs to change (e.g. to account for a new resource name segment), the two copies must be updated in sync. Consider exposing the regex constant itself from a lightweight, import-safe utils module (not `api.ts`) so both callers can share it without pulling in network/auth dependencies.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(googlechat): harden thread reply bou..." | Re-trigger Greptile

Comment thread extensions/googlechat/src/channel.adapters.ts Outdated
@clawsweeper

clawsweeper Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge.

What this changes:

The PR carries Google Chat inbound thread resource names through reply delivery and message-tool paths, normalizes outbound thread targets at the Google Chat boundary, adds a shared thread-resource validator, and expands core plus Google Chat regression coverage.

Maintainer follow-up before merge:

This is an open implementation PR for a real current-main Google Chat threading gap, but it touches both shared reply plumbing and the Google Chat plugin boundary; the next action is maintainer review/CI validation rather than an automated replacement fix PR.

Review details

Best possible solution:

Review and land this PR, or request focused revisions, with the Google Chat resource-name logic kept inside the plugin boundary and regression coverage for final replies, block-streamed replies, message-tool sends, explicit reply tags, and malformed thread inputs. Related #39554 and #69422 should be reconciled after the fix lands.

Acceptance criteria:

  • pnpm test src/auto-reply/reply extensions/googlechat
  • pnpm exec oxfmt --check --threads=1 src/auto-reply/reply/ extensions/googlechat/src/
  • pnpm tsgo:core
  • pnpm tsgo:extensions
  • pnpm check:test-types

What I checked:

  • Current main does not preserve a generic inbound thread id for Google Chat: The webhook context sets MessageSid/MessageSidFull to the inbound message resource and ReplyToId/ReplyToIdFull to message.thread?.name, but there is no MessageThreadId field for the generic reply/tool context. (extensions/googlechat/src/monitor.ts:216, 8935dd154a95)
  • Current main delivers raw replyToId as Google Chat thread: deliverGoogleChatReply passes payload.replyToId directly as sendGoogleChatMessage({ thread }) for text and media sends, with no inbound-thread fallback or resource-name normalization. (extensions/googlechat/src/monitor-reply-delivery.ts:68, 8935dd154a95)
  • Current main does not validate thread.name before the API call: sendGoogleChatMessage writes any truthy thread value into body.thread.name and enables REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD; there is no isGoogleChatThreadResourceName helper on main. (extensions/googlechat/src/api.ts:153, 8935dd154a95)
  • Current message action has no inbound-thread default: googlechatMessageActions.handleAction does not accept toolContext and derives threadId only from explicit threadId or replyTo params, so the message tool cannot default to the inbound Google Chat thread. (extensions/googlechat/src/actions.ts:94, 8935dd154a95)
  • Current shared reply plumbing still uses currentMessageId as the implicit target: applyReplyThreading normalizes the implicit reply target from currentMessageId, and block reply delivery fills replyToId from currentMessageId, which is the message resource for Google Chat rather than the thread resource. (src/auto-reply/reply/reply-payloads-base.ts:93, 8935dd154a95)
  • Current tests do not cover the missing fallback behavior: Main has Google Chat tests for explicit thread-shaped replyToId and messageReplyOption, but no negative test for message-resource thread values and no inbound-thread fallback case matching this PR. (extensions/googlechat/src/targets.test.ts:220, 8935dd154a95)

Likely related people:

  • steipete: Recent GitHub path history repeatedly shows steipete maintaining Google Chat delivery/API files and shared reply threading; steipete also landed the earlier Google Chat thread reply option support from fix(googlechat): handle thread replies and Add-on event types #30965. (role: recent maintainer and merger; confidence: high; commits: 4f91d81e1dbc, 6ecc18463753, cdbed3c9b1c4; files: extensions/googlechat/src/monitor.ts, extensions/googlechat/src/monitor-reply-delivery.ts, extensions/googlechat/src/api.ts)
  • novan: The Google Chat API thread reply option support was landed from contributor PR fix(googlechat): handle thread replies and Add-on event types #30965 and credited to novan, which is directly adjacent to the thread.name behavior this PR hardens. (role: introduced related Google Chat thread support; confidence: medium; commits: cdbed3c9b1c4; files: extensions/googlechat/src/api.ts)
  • vincentkoc: Recent history on the touched boundary includes plugin SDK public reply payload routing and internalized Google Chat SDK imports, which are relevant to reviewing this PR's core/plugin boundary changes. (role: adjacent plugin SDK/reply boundary maintainer; confidence: medium; commits: ef1346e50339, 2b67a3f76e90, 74e7b8d47b18; files: src/auto-reply/reply/reply-delivery.ts, extensions/googlechat/src/api.ts, extensions/googlechat/src/monitor.ts)

Remaining risk / open question:

  • The PR changes shared reply plumbing as well as the Google Chat plugin boundary, so Slack/Mattermost/Telegram-style reply tags, replyToMode behavior, block streaming, and compaction notices still need maintainer review and CI proof.
  • The PR intentionally makes malformed Google Chat thread inputs fail locally before fetch; reviewers should confirm every internal caller now normalizes threadId/replyToId before reaching sendGoogleChatMessage.
  • This sweep was read-only, so I did not rerun the PR author's targeted test, typecheck, or live Google Chat verification commands.

Codex review notes: model gpt-5.5, reasoning high; reviewed against dc5a85d6064d.

@jai
jai force-pushed the jaig-codex/fix-googlechat-thread-replies-upstream branch from cde54a5 to b5c8b45 Compare April 30, 2026 00:13
@jai
jai force-pushed the jaig-codex/fix-googlechat-thread-replies-upstream branch 2 times, most recently from b64612c to 58bc421 Compare April 30, 2026 00:40
@jai

jai commented Apr 30, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the ClawSweeper P2 findings in b2e7abab42:

  • Restored shared implicit reply semantics: generic auto-reply now keeps implicitReplyToId on currentMessageId; Google Chat thread-resource substitution stays inside the Google Chat boundary.
  • Natural delivery no longer falls back to the inbound thread when replyToId is absent; it only preserves explicit thread resources or converts Google Chat message-resource replies to the validated inbound thread.
  • Routed delivery no longer promotes a bare threadId after reply-mode filtering removed replyToId.
  • Message-tool defaults now carry Google Chat replyToMode and current target in tool context, default to the inbound thread only for allowed same-target sends, and avoid fallback for replyToMode: "off" or different targets.

Local verification:

  • pnpm docs:list
  • pnpm exec oxfmt --check --threads=1 src/auto-reply/reply/agent-runner-utils.ts src/auto-reply/reply/agent-runner-utils.test.ts extensions/googlechat/src/thread-resource.ts extensions/googlechat/src/api.ts extensions/googlechat/src/monitor-reply-delivery.ts extensions/googlechat/src/monitor.reply-delivery.test.ts extensions/googlechat/src/channel.adapters.ts extensions/googlechat/src/channel.test.ts extensions/googlechat/src/actions.ts extensions/googlechat/src/actions.test.ts
  • pnpm test src/auto-reply/reply extensions/googlechat extensions/telegram
  • pnpm test src/infra/outbound/message-action-runner.threading.test.ts src/infra/outbound/message-action-runner.plugin-dispatch.test.ts
  • pnpm tsgo:core && pnpm tsgo:extensions && pnpm check:test-types
  • pnpm check:changed

GitHub now reports the PR as mergeable; CI is queued for the new head SHA b2e7abab42a609997a81961626aaa3e108d928db.

@jai

jai commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

We are investigating potential issues seen while testing this change in a live OpenClaw gateway runtime. Holding this PR as draft until we finish root-cause analysis and confirm whether the behavior is caused by this patch or by an environment/config interaction.

@jai

jai commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

Update from live/code investigation:

  • The draft/comment hold is still warranted.
  • Scout is now healthy on clean v2026.4.27 (cbc2ba0), and the live checkout has been put back on that tag after local testing.
  • The actual PR diff is narrow: Google Chat threading + auto-reply plumbing only. Targeted PR-head tests passed locally (6 files / 72 tests) and the PR checks shown by GitHub are passing/skipped, with no failing checks observed.
  • I do not currently see a direct gateway/RPC hang mechanism in the PR patch itself. The earlier unhealthy runtime was the local branch 5027abf, which is not the current PR head (b2e7abab) and includes a very broad post-release delta versus clean 4.27 (~1357 changed files, including gateway/cron/runtime changes). That branch is ahead/behind the PR remote substantially (6/506), so it should not be treated as equivalent to this PR.
  • Root-cause hypothesis has shifted: Scout’s hang was more likely caused by broad trunk/runtime changes present in the deployed local branch, or an interaction with those changes plus Scout config, than by the final PR patch alone.

Next safe validation I’d recommend before marking ready again: test the current PR head in an isolated/non-production gateway or a short-lived Scout clone, not the stale local hotpatch branch.

@jai

jai commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

Rechecked against latest origin/main (bfa48c4025) after the typing-indicator changes.

Conclusion: the PR still addresses a separate Google Chat thread-routing gap and is not made obsolete by the latest main typing fix.

What latest main fixed:

  • 45b8645079 keeps typing indicator startup off the reply critical path.
  • 4f91d81e1d preserves reply text when Google Chat typing placeholder update/cleanup fails.

What latest main still does not fix:

  • Google Chat webhook context still does not populate MessageThreadId; it only carries ReplyToId / ReplyToIdFull from message.thread?.name.
  • deliverGoogleChatReply still passes payload.replyToId directly into sendGoogleChatMessage({ thread }).
  • sendGoogleChatMessage on main still accepts any truthy thread as body.thread.name with no thread-resource validation.
  • Google Chat message actions still derive thread target only from explicit threadId / replyTo, not the inbound thread context.

Additional fix pushed in 7b253154a7:

  • Preserves explicit valid thread resources for outbound media sends, matching the text path.
  • Adds a regression test for sendMedia({ threadId: "spaces/.../threads/..." }).

Local verification after the media-path fix:

  • pnpm test extensions/googlechat/src/channel.test.ts
  • pnpm test src/auto-reply/reply extensions/googlechat extensions/telegram
  • pnpm exec oxfmt --check --threads=1 src/auto-reply/reply/ extensions/googlechat/src/ extensions/telegram/src/
  • pnpm tsgo:core && pnpm tsgo:extensions
  • pnpm check:test-types
  • pnpm check:changed

Merge simulation against latest origin/main found no conflict markers. The PR remains draft because the prior live-runtime hold/comment is still a process decision, not because latest main makes the thread fix redundant.

@jai
jai force-pushed the jaig-codex/fix-googlechat-thread-replies-upstream branch 2 times, most recently from db72afb to 04dac3d Compare May 4, 2026 20:26
@jai

jai commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

Rebased and revalidated current head 04dac3da41 against latest origin/main (ef0dbcf49d).

What changed since the prior head:

  • Rebased onto latest upstream main, including the latest Google Chat auth transport changes.
  • Preserved the required changelog entry and the Google Chat webhook-routing test updates for latest-main's background webhook processor.

Local verification:

  • pnpm docs:list
  • pnpm test src/auto-reply/reply extensions/googlechat extensions/telegram
  • pnpm test src/infra/outbound/message-action-runner.threading.test.ts src/infra/outbound/message-action-runner.plugin-dispatch.test.ts
  • pnpm exec oxfmt --check --threads=1 src/auto-reply/reply/ extensions/googlechat/src/ extensions/telegram/src/
  • pnpm tsgo:core && pnpm tsgo:extensions && pnpm check:test-types
  • pnpm check:changed

Deploy/live status:

  • Deployed exact head 04dac3da41 to a Linux VPS-hosted Google Chat runtime.
  • Gateway is active on that head, runs with Node 22, /health is live, and config validate passes.
  • I am leaving the PR draft for now: exact-head live Google Chat reply placement is blocked because the runtime's Google Chat probe currently fails with unsupported_grant_type: Invalid grant_type: before an outbound reply can be sent.

CI for the pushed head is pending as of this update. The only inline review thread I see remains resolved/outdated.

@jai
jai force-pushed the jaig-codex/fix-googlechat-thread-replies-upstream branch from 04dac3d to 770bcc8 Compare May 4, 2026 20:43
@jai

jai commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

Rebased and revalidated pushed head 770bcc86bb against upstream main at 021373a454.

Local verification on this head:

  • pnpm docs:list
  • pnpm test src/auto-reply/reply extensions/googlechat extensions/telegram
  • pnpm test src/infra/outbound/message-action-runner.threading.test.ts src/infra/outbound/message-action-runner.plugin-dispatch.test.ts
  • pnpm exec oxfmt --check --threads=1 src/auto-reply/reply/ extensions/googlechat/src/ extensions/telegram/src/
  • pnpm tsgo:core && pnpm tsgo:extensions && pnpm check:test-types
  • pnpm check:changed

Deploy/live status:

  • Deployed exact head 770bcc86bb to a Linux VPS-hosted Google Chat runtime.
  • Gateway is active on that head, runs with Node 22, /health is live, and config validate passes.
  • Google Chat is configured/running, but exact-head live reply placement remains blocked because the channel probe currently fails with unsupported_grant_type: Invalid grant_type: before an outbound reply can be sent.

I am keeping the PR draft. Note: upstream main advanced again after this validation finished, so I am not marking this ready or claiming current merge-readiness.

@jai
jai force-pushed the jaig-codex/fix-googlechat-thread-replies-upstream branch from 770bcc8 to 2530254 Compare May 4, 2026 20:53
@jai

jai commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

Rebased and pushed head 25302549b5, which contains current upstream main at be8b4dc845 as observed before push.

What changed since the prior pushed head:

  • Resolved the CHANGELOG.md conflict by keeping both the upstream diagnostics entry and this PR's Google Chat fix entry.
  • Rebased over the latest agents/plugin fixes from upstream main.

Verification:

  • Full regression surface was run on the immediately preceding rebased head: pnpm test src/auto-reply/reply extensions/googlechat extensions/telegram.
  • Exact-head checks after the final rebase: pnpm docs:list, pnpm test extensions/googlechat, pnpm test src/infra/outbound/message-action-runner.threading.test.ts src/infra/outbound/message-action-runner.plugin-dispatch.test.ts, and pnpm check:changed.

Deploy/live status:

  • Deployed exact head 25302549b5 to a Linux VPS-hosted Google Chat runtime.
  • Gateway is active on that head, runs with Node 22, /health is live, and config validate passes.
  • Google Chat is configured/running, but exact-head live reply placement remains blocked because the channel probe currently fails with unsupported_grant_type: Invalid grant_type: before an outbound reply can be sent.

Keeping the PR draft; not marking ready while live reply placement remains unverified.

@jai
jai force-pushed the jaig-codex/fix-googlechat-thread-replies-upstream branch from 2530254 to 2c5e3d7 Compare May 6, 2026 09:54
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 6, 2026
@clawsweeper

clawsweeper Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

✨ Hatched: 🥚 common Brave Lint Imp

Hatch command

Comment @clawsweeper hatch when this PR is hatchable.

Hatchability rules:

  • Merged PRs are hatchable.
  • Open PRs are hatchable when they are status: 👀 ready for maintainer look, status: 🚀 automerge armed, or labeled clawsweeper:automerge.
  • Closed unmerged PRs are hatchable only when one of those hatchable labels is still present in the durable record.

Rarity: 🥚 common.
Trait: sleeps inside passing CI.
Image traits: location status garden; accessory release bell; palette charcoal, cyan, and signal green; mood sleepy but ready; pose standing beside its cracked shell; shell glossy opal shell; lighting soft underwater shimmer; background tiny shells and proof notes.
Share on X: post this hatch
Copy: My PR egg hatched a 🥚 common Brave Lint Imp in ClawSweeper.

What is this egg doing here?
  • Eggs appear after the PR passes real-behavior proof. It is here for vibes, not verdicts: it does not change labels, ratings, merge decisions, or automation.
  • The shell reacts to review momentum: open follow-up work warms it up, re-review makes it wobble, and a clean final review lets it hatch.
  • Hatchability usually comes from sufficient real-behavior proof, no blocking P0/P1/P2 findings, no security attention needed, and clean correctness. A merged PR is already final, so merge makes the egg hatchable independently.
  • The hatch is seeded from this repository and PR number, so the same PR keeps the same creature; the reviewed head SHA can only change safe visual details.
  • Rarity is just collectible sparkle: 🥚 common, 🌱 uncommon, 💎 rare, ✨ glimmer, and 🌈 legendary.

@merit-blake

Copy link
Copy Markdown

Confirming this from a production deployment — we hit exactly this on OpenClaw 2026.4.12 with Google Chat (service-account auth). Any agent that makes a tool call mid-reply inside a Chat thread posts its first message in-thread, then drops the post-tool continuation as a new top-level message off the space root.

Traced in our installed bundle to the same place this PR addresses: the continuation's reply target is built from the inbound message resource (spaces/X/messages/…) rather than the thread resource (spaces/X/threads/…), then passed as thread: on send — Google Chat silently rejects the message-resource-as-thread and posts at top level. Both identifiers are present on the inbound event; the message resource is the one plumbed forward.

For anyone blocked before this lands: channels.googlechat.replyToMode: off makes replies consistently top-level instead of intermittently leaking, which is at least predictable. Two things that did not help, in case it saves someone time: blockStreaming: false (it's not a chunking artifact — the malformed thread value is sent even as a single message), and typingIndicator: reaction (requires user OAuth, so it silently falls back to message mode under service-account auth).

Would be great to see this merged — happy to test a build if useful.

jai and others added 23 commits June 19, 2026 12:31
- Extract resolveAutoThreadingTargets so the implicit-reply rule has a single, unit-tested home shared by runReplyAgent and runAgentTurnWithFallback.
- Cover the missing-thread fallback in createBlockReplyDeliveryHandler so non-Google-Chat channels keep threading on currentMessageId.
- Add a DM-shaped Google Chat webhook test asserting MessageThreadId stays unset when the inbound message has no thread.
- Expand sendGoogleChatMessage thread validation with negative cases for malformed resource names.
When the agent uses the message tool to reply mid-turn without an explicit
threadId/replyTo, the send rooted a new top-level thread. The googlechat
threading adapter now exposes the inbound thread resource as
currentThreadTs so the shared message tool's outbound action can default
to it; the action handler reads toolContext and uses the inbound thread
when threadId/replyTo are omitted.
…er-utils

Pre-existing test mock for ./agent-runner-utils.js missed the
resolveAutoThreadingTargets export added when the auto-threading helper
was hoisted into shared utils, so 51 agent-runner-execution test cases
failed at module-load. Add the mock entry so the changed gate stays
green.
… delivery

When core reply plumbing surfaces a Google Chat message resource
(spaces/X/messages/Y) as the implicit reply target, sendGoogleChatMessage
rejects it as a thread name and the reply re-roots as a new top-level
thread. Resolve the outbound thread at the channel boundary in
deliverGoogleChatReply: keep an explicit thread-shaped replyToId,
otherwise fall back to the inbound thread captured from message.thread.
Core reply plumbing remains channel-agnostic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: googlechat Channel integration: googlechat extensions: google extensions: minimax merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P1 High-priority user-facing bug, regression, or broken workflow. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. size: XL status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants