resolveChannelStreamingPreviewToolProgress guessed a mode when streaming.mode
was unset, so it could never be right for every channel: guessing "partial"
dropped Discord and Telegram's explicit progress.toolProgress opt-out, and my
earlier switch to "progress" broke it the other way for Slack, Mattermost, and
Teams, whose real default is partial. With both keys configured and disagreeing,
the wrong one won.
The helper has no channel identity, so it stops guessing and takes the mode the
caller already resolved. All nine call sites pass theirs. Omitting it keeps the
previous configured-mode reading, so untouched callers behave exactly as before.
Reported by ClawSweeper on #116143.
Preserve the authoritative inbound group origin for private Mattermost channels without changing public channels, direct messages, thread keys, or the native channel delivery target. Prove both Bot API channel types over real HTTP, gateway discovery, and SQLite-backed outbound persistence.
Credit the original group-origin observation in #95669.
Co-authored-by: hansraj <hansraj136@gmail.com>
* fix(mattermost): record pending history for non-allowlisted group senders
When `groupPolicy` is `"allowlist"`, messages from senders not in
`groupAllowFrom` were silently dropped before reaching
`recordPendingHistoryEntryIfEnabled()`. This meant the bot had no
context of what non-allowlisted users said, breaking use cases like
conversation summarization.
This change introduces a `senderBlockedByGroupAllowlist` flag so that
non-allowlisted messages continue through the history-recording code
path before returning without generating a response.
Changes:
1. Add `senderBlockedByGroupAllowlist` flag before access check
2. `GROUP_POLICY_NOT_ALLOWLISTED` sets flag instead of returning
3. Skip `commandGate` check for blocked senders
4. Call `recordPendingHistory()` then return before response processing
Fixesopenclaw/openclaw#57607
* test(mattermost): verify denied history over real transport
Co-authored-by: Jason Wang <jasonwang@barkingdog.ai>
---------
Co-authored-by: Jason Wang <jasonwang@barkingdog.ai>
* fix(mattermost): key private channels as group on outbound routing
A Mattermost private channel (server type `P`) is authoritatively chat_type
`group`, but it is addressed as the delivery target `channel:<id>` — the same
prefix as a public channel. Inbound classified it correctly as `group`, while
outbound/session reconstruction re-derived `channel` from the target string, so
one conversation was keyed under two session namespaces
(`...:mattermost:group:<id>:thread` inbound vs a phantom `...:channel:<id>:thread`
on delivery). Threaded/scheduled deliveries bound to one then failed to match the
other (fail-closed delivery, or a conversation split across two session keys).
The Mattermost outbound path could not represent `group` at all:
resolveMattermostOutboundSessionRoute only produced direct/channel, and
resolveMattermostOpaqueTarget only classified user/channel.
- session-route: key a conversation as `group` from an authoritative signal — the
resolved target kind, an explicit `group:` prefix, or the inbound
currentSessionKey peer kind — so outbound shares the inbound `group:<id>`
namespace instead of forking `channel:<id>`.
- target-resolution: classify a bare channel id by its real channel type
(P/G -> group, O -> channel), cached per id.
The wire target stays `channel:<id>` (Mattermost posts to the channel id either
way; parseMattermostTarget only accepts channel:/user:) — the group distinction
lives in the session key. Adds unit coverage for both paths.
Resolves#95646.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cron): route bound-session cron deliveries under the bound namespace
An isolated cron run executes under an ephemeral agentSessionKey that does not
carry the source conversation's namespace. resolveDirectCronDeliverySessionKey
resolved the outbound delivery route from that isolated key, so for a job bound
to a real conversation thread (e.g. the gitlab-pipeline-watch recheck bound to
agent:...:mattermost:group:<id>🧵<root>) the lossy channel:<id> target was
re-derived as `channel`, forking a phantom channel:<id> session and splitting the
private-channel thread across two namespaces (#95646).
Prefer the job's bound conversation identity as the currentSessionKey used to
resolve the route (new selectCronRouteCurrentSessionKey helper), so the existing
currentSessionKey-based namespace resolution keeps group:<id>. No channel-type
cache is introduced — which is what made the cache-based attempts brittle on cold
restart (a sibling PR documented exactly that failure mode). Falls back to the
isolated key for unbound jobs and cron-namespace bindings. Adds unit coverage.
Refs #95646.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mattermost): key public channels as channel in directory listing
listMattermostDirectoryGroups labeled every joined channel — public `O`
and private `P` — as kind `group`. A name-resolved public channel could
then be keyed as `mattermost:group:<id>` on outbound routing, forking a
phantom group session and splitting the transcript from the inbound
`channel:<id>` one. Derive the kind from the authoritative Mattermost
channel type (`O` -> channel, `P`/`G` -> group) and add a regression
test. This closes the public-channel regression path flagged in review
for #95646 while keeping private channels keyed as `group`.
* fix(mattermost): harden private channel routing
* test: expose cron route selection through production module
* fix. scope cron session reuse to Mattermost delivery
* fix(cron): validate bound delivery peer and channel authority
* fix(cron): capture validated delivery destination peer
---------
Co-authored-by: leon <leon@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Track full dispatch completion separately for error handling and shutdown drain while allowing same-session follow-ups to steer active runs. Fixes#113180.
Co-authored-by: Taksh <takshkothari09@gmail.com>
* build(deps): remove npm shrinkwrap; mirror pnpm lock into transient package locks
npm 12 removed shrinkwrap (command + tarball/root loading). Delete all 82
committed npm-shrinkwrap.json files and stop publishing lockfiles; keep
pnpm-lock.yaml as the single reviewed dependency boundary. The generator
becomes scripts/generate-npm-package-lock.mjs and feeds plugin bundling via
a transient package-lock.json + npm ci (works on npm 11 and 12). Tarball
validation treats the published 2026.7.2 beta train as a shrinkwrap
transition; self-update npm detection now uses install topology instead of
the shipped shrinkwrap.
* fix(deps): repair lint, deadcode, and test-type lanes for the npm 12 migration
- sort integrity comparisons with an explicit comparator (oxlint)
- keep resolveBunGlobalNodeModules module-local (knip unused-export gate)
- model npm pack --json as npm<=11 array / npm 12 name-keyed object
- default calver destructuring in the tarball test fixture
* test: consolidate OpenClaw test state fixtures
* test(plugin-sdk): expose isolated test state
Promote the isolated OpenClaw test-state lifecycle through a narrow published Plugin SDK subpath so extension tests no longer import private core helpers. This intentional SDK surface addition is maintainer-approved.
* test: use SDK test-state seam in extensions
Route bundled extension suites through the focused repo-local Plugin SDK test-state entrypoint and remove the Codex projector harness exports made stale by fixture consolidation. Keep the seam out of production builds and published package artifacts while auditing its real consumers in the full-tree deadcode scan.
* test(plugins): map test-state in package boundaries
* refactor(channels): remove flat streaming compat
maintainer-approved early removal of v2026.7.2-gated compat
* refactor(channels): remove group intro hint adapter
maintainer-approved early removal of v2026.7.2-gated compat
Plugin SDK surface baseline update is maintainer-approved for this intentional removal.
* feat(sdk): always persist media facts and ship facts-first replacements for legacy Media* surfaces
PR 1 of the media legacy retirement program (audit-frozen, 4 PRs).
- Every media-bearing user turn now persists normalized __openclaw.media
facts unconditionally while continuing to emit the legacy top-level
Media* projection byte-identically (dual-write bridge; the conditional
shouldPersistStructuredMediaEntries gate now always includes media).
- New replacement APIs, shipped before any removal: typed hook media
facts (media[], originalMedia[], mediaStagingPending) on message
events; {{AttachmentPath}}/{{AttachmentUrl}}/{{AttachmentContentType}}/
{{AttachmentDir}}/{{AttachmentIndex}} template variables; focused
openclaw/plugin-sdk/media-local-roots subpath split out of the
deprecated agent-media-payload facade.
- Every legacy surface carries @deprecated naming its replacement, under
one named compatibility record media-legacy-projection with the
operator-approved removeAfter 2026-10-01 (two release trains; deletion
additionally gates on a clean published-plugin artifact sweep).
- Generic transcript append invariant documented; SDK migration, hooks,
and configuration docs updated to the facts-first path.
Writer golden matrix proves legacy bytes and model prompt bytes are
unchanged while nested facts become unconditional. 2,189 broad media
tests green; SDK api-baseline regenerated on fresh-env Testbox.
* feat(sdk): register media-local-roots subpath exports and deprecation metadata
Completes PR 1: package export map for openclaw/plugin-sdk/media-local-roots
plus the deprecated-subpath inventory and doc metadata entries for the
media-legacy-projection record.
* chore(sdk): track media-local-roots entrypoint and deprecated-export budgets
* fix(sdk): keep deprecated MSTeams buildMediaPayload re-export through the compat window
Deleting shipped runtime-api re-exports belongs to retirement PR 4 after
the media-legacy-projection window; PR 1 only deprecates. Also formats
the migration-guide schedule table.
* docs: regenerate docs map for media migration additions
* feat(channels): add channel-owned setup contracts
* test(channels): align legacy setup fixtures
* chore(channels): regenerate config and SDK baselines after rebase
* fix(update): run fresh doctor after current-process core changes
* fix(channels): align add pre-scan with execution precedence
* style(cli): format channels-cli test additions
* fix(channels): restore option-before-positional channel resolution via metadata arity scan
* fix(channels): keep help flags out of metadata arity escalation
* test(update): mock fresh post-update doctor in current-process suites
* style: format review fixes and correct entrypoint mock type
* fix(channels): register only modern contract options for dual-publishing plugins
* test(update): align downgrade suites with fresh-doctor child invocation
* docs(channels): record empty-contract and input-forwarding invariants
* fix(line): keep the shipped --token switch as a channel access token alias
* fix(signal): stop treating exact cross-family loopback endpoints as bind-aligned
* chore(config): regenerate docs config baselines after second rebase
* style: format rebased channels add tests
* fix(channels): enforce field-key and flag-name agreement in setup contracts
* fix(signal): detect container endpoints for bare --http-url setup
* fix(signal): ignore unconfigured accounts in transport collision checks
* fix(channels): validate negated setup flags in contract and normalizer
* fix(signal): preserve existing transport kind when setup detection is unreachable
* style(signal): use direct boolean check in collision guard
* style(signal): type test config literals
* docs(update): record two-read design of fresh-doctor validation gate
* fix(channels): satisfy post-rebase architecture gates
* docs: refresh channel setup map
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor(channels)!: shrink ChannelSetupInput to a generic envelope with a deprecated compatibility tier
* fix(channels): keep ChannelSetupInput structurally assignable without an index signature
* docs: regenerate docs map
* refactor(media): consolidate parallel media-kind unions onto canonical MediaKind
One canonical MediaKind union (media-core constants) replaces ~40
duplicate/parallel kind declarations across core and channel plugins;
channel-specific narrower contracts derive via Extract/Exclude. Also
fixes a review-caught fallback bug where a stored "unknown" reply-chain
kind preempted MIME inference and relabeled images as documents.
* refactor(ui): derive attachment kinds from MediaKind
* fix(telegram): drop type-dead unknown guard in reply-context kind fallback
* style(telegram): format media kind fallback
* feat(channels): batch 1 producers drop media placeholder bodies
Media-placeholder program batch 1: Google Chat, Zalo, LINE, and
Mattermost stop minting <media:kind> placeholder bodies. Media-only
messages carry an empty caption plus one structured fact per native
attachment (type-only when a download fails or is rejected, so payload
positions and kind signals stay aligned). The shared
formatMediaPlaceholderText SDK formatter renders text-only carriers
(Mattermost pending-room lines) from structured facts; per-channel
placeholder builders and the expected-count side channel are deleted.
* fix(mattermost): satisfy type, deadcode, and SDK manifest gates
* improve(gateway): restart only the changed account on channel config reload
When a config change is scoped entirely to one channel account
(channels.{kind}.accounts.{accountId}[.*]), restart just that account
instead of the whole channel. Wholesale channel restarts disconnect every
account on the channel; on gateways running many accounts, each
account-scoped config write (adding an account, changing one account's
settings) briefly dropped every other account's connection.
The reload plan gains a restartChannelAccounts bucket populated only when
all of a channel's changed paths are account-scoped; any channel-global
path falls back to the existing wholesale restart, and a channel scheduled
for wholesale restart drops its per-account entries so each (channel,
account) pair restarts at most once. The executor runs per-account
restarts through the existing stopChannel/startChannel accountId parameter.
* fix(gateway): preserve account reload admission
* fix(gateway): scope surgical reloads to isolated plugins
* test(gateway): avoid preactivating reload snapshot
* docs(channels): define account reload isolation contract
* fix(gateway): preflight scoped account reloads
* fix(gateway): re-drain live scoped reload targets
* docs: refresh generated docs map
* fix(mattermost): align scoped reload with durable ingress
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(mattermost): adopt durable ingress drain at the websocket chokepoint
Posted events processed detached from the websocket receive with only a
5-minute in-memory guard; a crash lost the post and reconnect never replays
it. Raw posted envelopes now journal durably (event_id = post.id per the
upstream Post model, lane per channel_id, one row per post) before handler
scheduling; dispatch runs through the core drain with deferred claims through
debounce, merged-flush fan-out adoption, gated-turn settlement, and 30d/20k
tombstones covering the old 5min/2k guard, which is deleted after its parity
test. post_edited stays excluded and cannot be swallowed by posted
tombstones. Cold-gap limitation stated: Mattermost cannot replay posts missed
while disconnected.
Autoreview blocked by codex sandbox network in the build stage; full manual
review performed (updated one websocket test asserting the pre-adoption
parsed-post contract to the raw-envelope contract). Part of #109657 wave 2.
* style(mattermost): keep ingress monitor type internal
* fix(mattermost): retry then loudly escalate a failed durable append
Landing autoreview caught a real loss path: a durable enqueue failure at the
websocket chokepoint was logged and swallowed — the raw envelope discarded,
the connection kept running against a broken store, and reconnect never
replays, so a transient SQLite failure silently lost the post. The append now
retries with short backoff for transient blips; a persistent failure
propagates and the websocket terminates loudly so the outage is
operator-visible instead of silently dropping every subsequent post.
Regression test covers both the absorbed-transient and escalation paths.
* style(mattermost): format rebased ingress handler
* docs(mattermost): document bounded auth-failure retries under deferred claims
* fix(mattermost): guard the drain pump against stop racing the async prune
stop() disposing before the startup pump finished pruning let the pump
lazily create a fresh undisposed drain and dispatch after shutdown. The pump
now re-checks running after the prune, and stop() disposes again after
awaiting the pump so a drain created mid-race is torn down. Regression test
blocks the prune across stop and asserts no dispatch.
* fix(mattermost): serialize durable admissions to preserve lane order
Concurrent websocket callbacks let a post in append-retry backoff be
overtaken by its successor, inverting same-channel arrival order in the
queue. Admissions now chain (order over latency, mirroring the iMessage
admission tail); regression proves a retried post still lands ahead of a
concurrently received one.
* test(mattermost): assert lane order via dispatch sequence
* fix(mattermost): stop() awaits in-flight admissions before disposal
* fix(mattermost): satisfy ingress lint checks
* fix(mattermost): honor envelope-level channel ids in the durable inspector
Posts can carry their channel id on the post, the event data, or the
broadcast envelope — the monitor dispatch honors all three, but the ingress
inspector and claim-side validator required the nested field, rejecting valid
posts as permanent and (via the storage-failure escalation) tearing down the
socket for a failure that never happened, losing posts reconnect cannot
replay. Both sites accept the three shapes; regression proves an
envelope-level post dispatches.
* Harden Mattermost API paths
* fix(mattermost): guard direct media file paths
* refactor(mattermost): streamline API path validation
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>