* fix(imessage): harden remote Mac transport
Route SSH-backed iMessage actions through JSON-RPC, preserve remote database paths, and stage outbound files on the Messages Mac with bounded cleanup. Keep local action semantics intact while failing closed on ambiguous wrappers and surfacing the remaining imsg v0.13.4 limits.
* fix(imessage): remove test-only exports
* docs(config-channels): replace legacy allow:true with enabled:true for Discord, Slack, and Google Chat config examples
* docs(config-channels): repair channel configuration examples
Make the complete Discord, Slack, and Google Chat setup examples valid and keep Slack channel routing on stable IDs.
Co-authored-by: 赵旺0668001248 <zhao.wang1@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(slack): add user-identity config surface (session token/cookie)
Introduce an account-level identity discriminator ("bot" default | "user") for
the Slack channel, plus sessionToken/sessionCookie secret fields for the
browser-session (user) path. Schema enforces structural coupling only
(identity="user" rejects appToken/signingSecret/relay/non-default mode);
credential presence is deferred to runtime like bot/app tokens. Account entries
inherit the top-level identity. Adds env fallback (SLACK_SESSION_TOKEN/COOKIE,
default account only), secret-contract registration, provenance *Source fields,
and UI hints. Config surface only; resolver/transport are later phases.
* refactor(slack): identity-aware operation-token resolver + dedup
Route Slack read/write token selection through a single identity-aware
resolveSlackOperationToken: identity="user" selects the browser-session token
for both operations (companion cookie attached later at client construction),
identity="bot" keeps the exact userToken/botToken + userTokenReadOnly behavior.
Delete the duplicated token-selection gate in action-runtime and route direct
sends/DM opens through the shared resolver with identity-neutral error wording.
Behavior-neutral for bot identity; net non-test LOC -2.
* refactor(slack): retarget user identity to xoxp + companion app
Replace the abandoned browser-session (xoxc/xoxd) approach for identity="user"
with the official path: authenticate as the human via the existing userToken
(xoxp) while a companion app (appToken/socket or signingSecret/http) carries
events. Remove sessionToken/sessionCookie config, env vars, secret-contract
entries, resolver fields, and their tests. Invert the schema coupling so
identity="user" permits the companion-app transport credentials, and drop the
now-vestigial user-identity structural guard entirely (identity is a permissive
discriminator; credential presence is a runtime concern). Resolver returns the
user token for user-identity reads and writes. Behavior-neutral for bot
identity.
* feat(slack): user-identity provider transport (xoxp + companion app)
Wire the identity="user" event transport. The companion Slack app runs Socket
Mode (appToken) or HTTP (signingSecret) while Bolt authenticates as the human
via the user token (no bot token). Startup auth.test runs on the user token and
its user_id becomes the self/mention target and self-send dedupe id (Bolt's
lazy per-event authorize populates context.botUserId=user_id, so the existing
self-event middleware drops the human's own messages). Identity-aware token
requirements and diagnostics; the bot-token identity warning is suppressed for
user identity. Behavior-neutral for identity="bot".
* feat(slack): user-identity setup, doctor, and docs
Add the setup wizard, doctor/status, and documentation for identity="user".
The wizard collects a user token plus the companion-app transport credential
(app token for socket, signing secret for http) and only persists identity when
"user" so bot setups keep byte-identical generated config; doctor validates the
user token via auth.test and warns on a missing transport credential; account
inspection reports user-token status. Message actions are gated on the active
identity's credential so user accounts expose actions. Docs add the exact
companion-app recipe (user scopes, on-behalf-of-user message events, socket/http
examples, the DM/group-DM-only-via-user-scope note, and self-send dedupe).
Behavior-neutral for identity="bot".
* test(slack): isolate user-identity provider tests to fix parallel flake
The new user-identity provider tests shared the globalThis-stored Slack test
client/handlers across files under parallel isolate:false, so their reaction
closures could reference a prior file's state and intermittently fail sibling
monitor tests (monitor.tool-result reactions). Track started monitors and
abort/await them all in afterEach (even on failed assertions), reset the shared
auth.test/mock state, and dispose the global Slack test runtime per file via a
new disposeSlackTestRuntime helper. Full Slack suite green across repeated
parallel runs.
* fix(slack): regenerate channel config metadata after rebase
* fix(slack): resolve setup consistent-return and doctor dead-export lint
* style: format install script test
* docs(slack): regenerate docs map for user-identity section
* test(slack): use renamed createSlackBoltApp token param in interop test
The rebase folded main's new wrapReceiver test into the renamed
createSlackBoltApp signature (botToken -> token); update the call site.
* feat(mattermost): add thread.requireExplicitMention to opt out of thread auto-follow
Mattermost treats any reply in a thread the bot has participated in as an
implicit mention, so requireMention only gates the first message and the bot
then answers follow-ups addressed to other people for the participation TTL.
Slack exposes channels.slack.thread.requireExplicitMention for exactly this;
Mattermost had no equivalent and its strict schema rejected the key.
Add channels.mattermost.thread.requireExplicitMention (channel + per-account),
mirroring Slack. When set, thread participation no longer counts as a mention.
Default (unset/false) keeps today's auto-follow behavior unchanged.
Related: #108269
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(channels): add implicit mention policy foundation
* feat(channels): unify implicit mention policy
Co-authored-by: leon <dodoma0919@gmail.com>
* refactor(channels): keep implicit policy inside evaluator
* fix(channels): use exported implicit mention type
* chore(channels): satisfy extension lint
* fix(config): break implicit mention type cycle
* fix(plugin-sdk): account for implicit mention config export
* refactor(config): isolate implicit mention schema
* chore(plugin-sdk): align implicit mention surface budget
* fix(config): remove unused schema re-export
---------
Co-authored-by: leon <dodoma0919@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Phase 3 of #107237. Removes config.apply/config.patch/restart from the
regular-agent gateway tool (mirroring Phase 2's update.run removal); only
config.get and config.schema.lookup reads remain. Persistent config changes
and restarts now go exclusively through the human-approved openclaw
delegation path — closing the last unmediated agent config-write surface.
gateway stays owner-only/control-plane gated (config reads expose secrets and
host topology). Legacy setups can re-enable writes via the existing per-agent
tool allowlist; no new config key. Net -2271 LOC.
Refs #107237
* refactor(config): retire flat streaming keys from the last six channel schemas
signal, irc, nextcloud-talk, whatsapp, googlechat, and mattermost now accept
only the nested streaming.{chunkMode,block.enabled,block.coalesce} shape
(mattermost also drops scalar/boolean streaming); flat spellings migrate via
each channel's defineChannelAliasMigration doctor contract with root seeding
for their wholesale-replace account merges.
* feat(channels): warn once per key when the deprecated flat streaming fallback is used
Bundled schemas now reject the flat delivery keys, so the streaming.ts
fallback only serves external SDK plugin configs; emit a once-per-process
per-key subsystem warning and pin the removal plan to the next release train.
* chore(config): regenerate bundled channel config metadata for nested-only streaming
* docs: describe nested-only channel streaming config and the SDK flat-key deprecation window
* fix(whatsapp): seed migrated named-account streaming from the accounts.default layer
WhatsApp resolution layers accounts.default shared config between root and
named accounts, so doctor-materialized account streaming objects now inherit
default-account settings over root ones; mattermost schema test moves to the
nested-only shape with explicit rejection coverage; scope flat-key deprecation
notes to the pending Matrix/Feishu migrations.
* fix(whatsapp): resolve the default account case-insensitively when seeding migrated streaming
* chore(plugin-sdk): repin API baseline for nested-only channel streaming types
In messages.visibleReplies "message_tool" sessions, a successful agent turn that produced a substantive private final without calling message(action=send) previously left the user with silence and only an operator log. The gateway now enqueues one protected front-of-queue retry prompting the model to deliver the reply, and falls back to a sanitized visible diagnostic when the retry cannot be enqueued or also strands. Queue overflow protection is unified with the in-flight-aware drop policy (skip in-flight or protected items, reject when nothing is droppable), rejected overflow no longer refreshes the drain debounce, heartbeat turns are excluded from recovery, and recovery retries no longer share the client turn's queued-turn lifecycle.
Fixes#85714
Thanks to Eva (@100yenadmin) for the contribution.
* fix(telegram): resolve local Bot API container file paths against trustedLocalFileRoots
When the self-hosted telegram-bot-api server runs with --local inside a
container, getFile returns absolute file_path values rooted at the
container data dir (/var/lib/telegram-bot-api/...). The host process
mounts that volume at a different path, so the absolute path never
matches a trustedLocalFileRoots entry and inbound media fails to
materialize (messages reach the agent as bare <media:document>
placeholders without bytes).
Map container-absolute paths back to relative candidates and resolve
them under each trusted root, trying both with and without the
per-token directory segment the local server uses. Relative file_path
values are now also resolved against trusted roots before falling back
to the HTTP file endpoint, which keeps large-file (>20MB) local-mode
downloads working.
Path traversal protections: candidates are normalized, NUL bytes and
dot-segments are rejected, and reads stay behind the trusted-roots
file-access sandbox.
AI-assisted (Claude Code), validated with targeted vitest run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Dizesales <269209351+Dizesales@users.noreply.github.com>
* fix(telegram): map local Bot API container media paths
Co-authored-by: Lucas Magalhaes <ellucasrj@gmail.com>
* chore(config): refresh channel metadata
---------
Co-authored-by: Lucas Magalhaes <ellucasrj@gmail.com>
Co-authored-by: Dizesales <269209351+Dizesales@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The config example in docs/gateway/config-channels.md incorrectly
listed streaming default as "off". The actual runtime default is
"partial", as confirmed by the Telegram plugin's default resolver.
docs/channels/telegram.md already correctly documents the default
as "partial".
Co-authored-by: Claude <noreply@anthropic.com>
Add optional directUserId field to ChannelModelOverrideParams so the
shared channels.modelByChannel resolver can match DM-specific config
entries. Callers pass sessionEntry.origin?.nativeDirectUserId.
Closes#53638
Co-authored-by: Thomas Zhengtao <thomas.zhengtao@gmail.com>
* 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>
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:
- Adds a symptom-keyed troubleshooting block to `docs/gateway/config-channels.md` for group/channel @mentions that log `queuedFinal=false, replies=0` and explains the `visibleReplies` remedies.
- Reproducibility: yes. for the docs gap and source behavior: current main lacks the exact symptom-keyed troubleshooting entry, and the resolver/tests show when message-tool mode suppresses automatic final delivery.
Automerge notes:
- PR branch already contained follow-up commit before automerge: docs(gateway): make group reply fix restart conditional
- PR branch already contained follow-up commit before automerge: docs(gateway): qualify direct-chat reply default in troubleshooting
- PR branch already contained follow-up commit before automerge: docs(gateway): align group reply troubleshooting with current automat…
- PR branch already contained follow-up commit before automerge: docs(gateway): scope group reply suppression cause to group config
Validation:
- ClawSweeper review passed for head e60ae89b20.
- Required merge gates passed before the squash merge.
Prepared head SHA: e60ae89b20
Review: https://github.com/openclaw/openclaw/pull/77052#issuecomment-4367898048
Co-authored-by: yetval <yetvald@gmail.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>