Restore readable standard Telegram text delivery by default after Bot API 10.1 rich messages rendered as unsupported in current clients. Keep native rich tables and structured messages available through the account-level richMessages opt-in, with account-aware capability advertising and documented structural limits.
Fixes#93263.
* fix(telegram): control group history context
* fix(telegram): keep history mode type local
* fix(telegram): respect history mode during forum recovery
* fix(feishu): re-resolve route when dynamic agent binding already exists in runtime config
When dynamicAgentCreation is enabled and a binding was previously written
to the config file (e.g. from a prior message), the in-memory cfg may be
stale and not contain the binding. Previously, maybeCreateDynamicAgent
returned { created: false, updatedCfg: cfg } with the stale cfg, and
bot.ts only re-resolved the route when created === true. This caused
subsequent messages to still route to agent:main.
Fix: check runtime.config.current() for the binding when it is missing
from the in-memory cfg. When found, return the runtime's current config
so the caller can re-resolve the route with up-to-date bindings.
Fixes#42837
* fix(feishu): serialize dynamic agent config updates
* fix(feishu): route with refreshed runtime config
* fix(feishu): use current dynamic-agent policy
* fix(feishu): reauthorize refreshed dynamic routes
* fix(feishu): authorize dynamic agent mutations
* fix(feishu): complete account-scoped dynamic routing
* fix(feishu): revalidate current direct routes
* fix(feishu): isolate named-account dynamic agents
* fix(feishu): bound named dynamic agent ids
* docs(feishu): explain legacy dynamic agent cap
* test(feishu): fix dynamic routing check types
---------
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
Allow an explicit canonical ClickClack enable/setup selection to record ClickClack in a nonempty plugin allowlist, while preserving unrelated allowlist rejection, denylist authority, and global plugin disablement.
Validated at source head 24af9d8e75 with focused regressions, built-CLI disposable-config E2E, security checks, and autoreview. Merged under owner authorization despite the two documented untouched-main agent-core baseline failures.
* feat(imessage): always-on inbound recovery, deprecate catchup
Replaces the opt-in catchup subsystem with always-on inbound replay
protection that brings iMessage in line with the other channels, and
fixes#89237 (stale backlog dispatched as fresh after bridge recovery).
- New inbound-dedupe.ts: persistent claimable GUID dedupe (claim/commit/
release) plus a stale-backlog age fence that suppresses live rows whose
send date is materially older than arrival (logged, never silent).
- monitor-provider: claim at ingestion, carry the exact claimed key on the
debouncer entry, commit on successful flush / release on dispatch failure
(per-unit so a coalesced bucket cannot strand a sibling claim). Keeps the
local startup since_rowid watermark so startup-window rows are not skipped.
- Deprecate catchup: delete catchup.ts + catchup-bridge.ts, remove the
channels.imessage.catchup schema, cursor migration, and config-guard nag.
Back-compat: strip the retired key before validation; new imessage doctor
contract reports + removes it on doctor --fix.
- Docs updated for the new recovery model.
Net -947 prod LOC.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(imessage): recover downtime messages via since_rowid replay
Builds downtime recovery on the new inbound dedupe instead of restoring the
old catchup subsystem. On startup the monitor passes the last dispatched rowid
(a persisted per-account cursor) to imsg watch.subscribe as since_rowid, so imsg
replays the messages that landed while the gateway was down, then tails live.
The GUID dedupe drops anything already handled, so no cursor/retry bookkeeping
is needed.
- recovery-cursor.ts: minimal persisted per-account lastDispatchedRowid.
- monitor-provider: since_rowid = cursor (capped to the most recent
IMESSAGE_RECOVERY_MAX_ROWS); split the age fence on the startup rowid boundary
so replayed rows (<= boundary) use the wider recovery window and live rows
(> boundary) keep the tight #89237 fence; advance the cursor on commit.
- Local only: remote SSH cliPath cannot read chat.db, so it tails from the
current rowid (suppress-and-move-on) as before.
Restores missed-message recovery that the catchup removal dropped, with no
config and a fraction of the old LOC.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(imessage): make recovery cursor advance failure- and suppression-safe
Addresses two cursor-state regressions in the downtime-recovery path:
- Failed replay rows could be skipped forever: a released (failed) row keeps
its dedupe claim for retry, but a later successful row in the same flush
advanced the cursor past it, so the next startup's since_rowid skipped it.
Hold a per-session floor at the lowest released rowid and never advance the
cursor past it.
- Suppressed live backlog could be re-delivered after a restart: a live row
suppressed under the tight live fence was not recorded, so after a restart it
fell under the wider recovery window (its rowid now below the new boundary)
and was delivered. Commit its dedupe key on suppression so the recovery
replay treats it as already handled.
Both caught by Codex autoreview. Adds regression tests for the floor and the
suppression record.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(imessage): bound the GUID-less replay key length
Hash the composite fallback key's variable parts (conversation, sender,
created_at, text) so the key is length-bounded regardless of message text.
The persistent dedupe store already hashes keys internally, so this was not a
live overflow, but the bounded key removes the dependency on that and keeps the
fallback fail-open. Flagged by autoreview.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(imessage): recover downtime messages on remote cliPath setups too
The since_rowid replay runs over the imsg RPC client, so driving it from the
persisted recovery cursor (not the local chat.db boundary) makes downtime
recovery work for remote SSH cliPath gateways — the topology the old RPC-based
catchup served and that the rowid-boundary-only version regressed. Local setups
keep the wider, capped recovery window via the chat.db boundary; remote uses the
live age-fence window. Flagged by autoreview.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(imessage): seed recovery cursor from retired catchup cursor on upgrade
A one-time, self-cleaning migration: when the recovery cursor is empty on the
first startup after upgrade, seed it from the retired imessage.catchup-cursors
lastSeenRowid and consume the legacy entry. Without this a user who had catchup
enabled would not replay messages missed across the upgrade restart. Flagged by
autoreview.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(imessage): preserve catchup recovery on upgrade
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Gate iMessage same-sender DM split-send coalescing on imsg's structural
`balloon_bundle_id` URL-balloon marker (openclaw/imsg#137) instead of timing/
text-shape inference, with a session capability latch and a back-compat path:
- URL-balloon marker present -> merge (precise split-send).
- Build known to emit balloon metadata (session latch) -> keep non-marker
buckets separate (the precision win).
- Build that never emits balloon metadata (older imsg) -> preserve the legacy
unconditional merge, so split-send users do not regress to two turns.
Never merges more than shipped main already did. Verified live end-to-end: the
patched gateway, watching a real chat.db via an imsg #137 build, merged a real
iPhone-sent `Dump <url>` split-send into one turn. Client-side removal once imsg
coalesces upstream is tracked in #91243 (openclaw/imsg#141).
Closes#90795
* fix(qqbot): migrate group tool policy config
* test: stabilize changed check lanes
* style: format changed main files
* test: align CI matrix expectations
## Summary
- Adds native Google Chat approval cards for exec and plugin approval requests that originate from Google Chat spaces or threads.
- Uses opaque server-side action tokens for Google Chat `cardsV2` button callbacks and updates delivered approval messages after resolution or expiry.
- Preserves the shipped Google Chat typing-message default while keeping approval cards on the channel-local native path.
- Suppresses duplicate manual `/approve ...` follow-up delivery inside `extensions/googlechat/` when the native card path owns the approval prompt.
- Documents Google Chat native approval behavior and the `typingIndicator: "message"` default.
## Linked context
Which issue does this close?
Closes #
Which issues, PRs, or discussions are related?
Related Spec 24.8: Google Chat native approval cards.
Was this requested by a maintainer or owner?
Requested by maintainer in the Codex task thread.
## Real behavior proof (required for external PRs)
- Behavior addressed: Google Chat exec and plugin approvals render as native cards and resolve through Google Chat button clicks. The latest change verifies an exec approval card is not accompanied by a duplicate manual `/approve` instruction bubble.
- Real environment tested: OpenClaw dev profile with a real Google Chat DM to the OpenClaw app, local gateway behind a temporary Cloudflare quick tunnel, and Arc/Computer Use against the signed-in Google Chat session.
- Exact steps or command run after this patch: Rebuilt the gateway runtime, started the dev-profile gateway with the Google Chat webhook routed through the tunnel, sent a fresh exec request from Google Chat, verified only the native approval card appeared, clicked `Allow Once` in Google Chat, and checked the command output reply plus marker file.
- Evidence after fix (screenshot, recording, terminal capture, console output, redacted runtime log, linked artifact, or copied live output): Latest proof used nonce `GCHAT_NODOUBLE_LIVE_20260604070730`, approval id `949bc08c-9e57-47c0-b045-137603782292`, and proof directory `.mem/main/proofs/demo-89502-dev-gchat-exec-approval-no-double-send-channel-race/`. `raw/google-chat-gchat-nodouble-request-card-only-clean.png` shows the fresh user message followed by a single native `Exec Approval Required` card with `Allow Once`/`Deny` and no manual `/approve` follow-up bubble. `raw/google-chat-gchat-nodouble-resolved-clean.png` shows the card edited to `Exec Approval: Allowed once` and the final successful command reply. `raw/gchat-nodouble-live-filtered-log.txt` contains `googlechat approval resolved id=949bc08c-9e57-47c0-b045-137603782292 decision=allow-once`. `raw/marker-file-check.txt` records `/tmp/openclaw-gchat-no-double-GCHAT_NODOUBLE_LIVE_20260604070730` as created.
- Observed result after fix: The approval prompt posted as a native Google Chat card only. No duplicate manual approval-instruction bubble was sent. Clicking `Allow Once` resolved the approval through the gateway and OpenClaw replied with the successful exec output in the same Google Chat DM.
- What was not tested: A persistent production Google Chat app URL; live proof used a temporary Cloudflare tunnel for the local dev callback.
- Proof limitations or environment constraints: Video was not captured for the final resumed manual UI run; still screenshots, gateway/proxy logs, a marker-file artifact, and Showboat verification were captured.
- Before evidence (optional but encouraged): Before the final channel-local suppression path, Google Chat could show both the native approval card and a separate manual `/approve` instruction bubble.
## Tests and validation
Which commands did you run?
- `node scripts/build-all.mjs gatewayWatch`
- `node scripts/run-vitest.mjs extensions/googlechat/src/monitor-webhook.test.ts extensions/googlechat/src/monitor.test.ts extensions/googlechat/src/monitor.reply-delivery.test.ts extensions/googlechat/src/monitor-durable.test.ts extensions/googlechat/src/approval-card-actions.test.ts extensions/googlechat/src/approval-handler.runtime.test.ts extensions/googlechat/src/approval-native.test.ts extensions/googlechat/src/approval-card-click.test.ts extensions/googlechat/src/channel-config.test.ts extensions/googlechat/src/targets.test.ts`
- `git diff --check`
- `pnpm docs:list`
- `uvx showboat --workdir .mem/main/proofs/demo-89502-dev-gchat-exec-approval-no-double-send-channel-race verify .mem/main/proofs/demo-89502-dev-gchat-exec-approval-no-double-send-channel-race/raw/showboat-summary.md`
- Live dev-profile Google Chat proof described above.
What regression coverage was added or updated?
- Added Google Chat native approval capability, runtime delivery, card token, and card-click resolver tests.
- Added in-flight native card send suppression coverage so manual follow-up text is suppressed while native card delivery is pending.
- Added cleanup coverage so manual follow-ups are restored if native card send fails.
- Updated webhook ACK coverage for card-click events and default typing-indicator behavior coverage.
What failed before this fix, if known?
Google Chat could deliver the native approval card and still allow a model/message-tool manual `/approve` follow-up to appear as a second visible bubble.
If no test was added, why not?
Tests were added for the changed runtime and webhook behavior.
## Risk checklist
Did user-visible behavior change? (`Yes/No`)
Yes.
Did config, environment, or migration behavior change? (`Yes/No`)
No migration. The shipped Google Chat `typingIndicator: "message"` default is preserved.
Did security, auth, secrets, network, or tool execution behavior change? (`Yes/No`)
Yes.
What is the highest-risk area?
Approval authorization and callback token handling for native Google Chat card actions.
How is that risk mitigated?
Callbacks carry opaque action tokens only, token bindings check account, space, message, expiry, allowed decision, and in-flight state, and actor authorization reuses the existing Google Chat approver allowlist adapter based on stable `users/<id>` principals.
## Current review state
What is the next action?
Merge after current-head CI for `5923f2af46`.
What is still waiting on author, maintainer, CI, or external proof?
Current-head CI is green for `5923f2af46`; live dev-profile proof is complete.
Which bot or reviewer comments were addressed?
Addressed duplicate approval delivery by keeping the final suppression path inside `extensions/googlechat/`, preserving default typing-message behavior, and proving the current Google Chat surface sends only the native approval card.
* refactor: move imessage monitor state to sqlite
* test: use OpenClaw temp root in iMessage state helper
* test: avoid pending promise lint in chat tests
* test: harden gateway ci flakes
* test: align session list merge expectation
Remove Telegram runtime JSON sidecar read/write fallback for the prompt-context message cache. Keep legacy sidecar parsing for doctor import into SQLite plugin state and update docs/tests to match.
Deliver plugin-owned bound-thread replies even when the source room is configured for `message_tool` visible replies. Normal agent final text still stays private unless the agent calls `message(action=send)`.
Document the distinction in the group/channel docs and root routing policy, and keep ambient room-event plus unauthorized text-slash suppression covered by regression tests.
Fixes#87721.
## Summary
- Document scoped configured mention-pattern policy on the Groups page, including allow/deny mode semantics, supported conversation IDs, account-level precedence, and native-mention behavior.
- Add config UI help for `mentionPatterns.mode`, `allowIn`, and `denyIn` on Discord, Matrix, Slack, Telegram, and WhatsApp.
- Regenerate channel config/docs/plugin SDK metadata baselines for the new hint copy.
Refs #70864.
## Verification
- git diff --check
- pnpm format:docs:check
- pnpm docs:check-mdx
- pnpm docs:check-links
- pnpm config:channels:check
- pnpm config:docs:check
- pnpm plugin-sdk:api:check
- node scripts/run-vitest.mjs src/config/schema.hints.test.ts
- .agents/skills/autoreview/scripts/autoreview --mode local
## Real behavior proof
Behavior addressed: Documentation and config UI metadata for scoped configured mention-pattern policy.
Real environment tested: Local OpenClaw checkout on macOS.
Exact steps or command run after this patch: The verification commands listed above.
Evidence after fix: Docs formatting, MDX, link audit, generated config/channel/API baselines, and config hint tests passed; autoreview reported no accepted/actionable findings.
Observed result after fix: The Groups page now explains how to scope `messages.groupChat.mentionPatterns` with `channels.<channel>.mentionPatterns`, and config metadata exposes field help for the supported channels.
What was not tested: Live Discord, Matrix, Slack, Telegram, or WhatsApp inbound messages; this PR is documentation/config metadata only and follows the already-landed runtime behavior from #70864.
Provider-scoped configured regex mention patterns for Discord, Matrix, Slack, Telegram, and WhatsApp.
Native platform mentions keep their existing behavior, and unsupported channels do not opt into the new regex policy path. The new policy supports per-channel allow/deny routing through mentionPatterns.mode with allowIn and denyIn so group auto-reply regexes can be limited without broad global blast radius.
Refs #70864.
Supersedes #87200.
Thanks @patrick-slimelab.