* feat(slack): unify the native progress turn into one streamed message
Native progress mode now streams the whole turn into a single Slack message:
narration flows as markdown_text chunks interleaved with plan/task chunks,
task rows carry file-path details and +N/-N output, the terminal task links
the session via url_source, and the final answer lands through stopStream in
the same message. Media/oversized/error finals keep their normal-delivery
fallbacks.
Deletes the finished-card receipt collapse (the '\u{1F6E0} N tool calls · ⏱ Ns' edit)
outright: the card now stays as its finished self.
Live-verified on a real workspace: exactly one bot message per turn.
* fix(slack): serialize native stream updates and keep append-only rendered text monotonic
Overlapping progress updates (compositor render, narration payload, final)
computed their narration delta before awaiting the network and committed
state only afterwards, so concurrent updates re-appended identical
narration into the streamed message (each status line landed 3x live).
A single ordering chain now makes each update's compute -> append -> commit
atomic.
applyAppendOnlyStreamUpdate also replaced the accumulated rendered text
with the incoming cumulative partial once an appended chunk had diverged
rendered from source, dropping content the sink already displayed; rendered
now only ever extends.
* fix(slack): stop refreshing thread status once a turn has visible output
Slack clears the assistant thread status as soon as the app puts anything
in the thread, and renders its own rotating agent-working row ("Generating
response...", "Finding answers...") for every status write after that -- it
ignores the app-supplied string. The typing keepalive re-set the status
every 3s for up to 60s, so each turn painted a duplicate status row under
the streamed card or progress message.
The status write is now gated on the turn having visible output, which the
dispatcher already tracks (delivered reply, committed preview, or posted
draft message). The first status still fires before any output, so slow
turns keep their indicator, and the typing reaction is tracked separately
so a suppressed status write still cleans up its reaction.
* fix(slack): let the plan card own the status line instead of echoing it
The status headline and plan explanation fed both the streamed narration
markdown and the plan card title, so every headline rendered twice: once as
static text and once in the card that keeps updating it in place. Narration
now carries only authored commentary and reasoning, and a preamble payload
whose text the card title already shows is not streamed again.
* feat(slack): make the native agent card the default progress surface
Slack's native plan/task card was opt-in behind streaming.progress
.nativeTaskCards while the Block Kit session card shipped as the default.
The native surface is the better product on every axis we can measure --
one streamed message instead of three artifacts, live task rows with file
paths and diff counts, and Slack's own agent chrome -- so it becomes the
default and an explicit false selects the Block Kit card instead.
The session link is now emitted only when it can actually work: the
operator set gateway.publicOrigin and left the Control UI enabled.
Installations with no externally reachable Gateway get no link rather than
a dead one.
The progress card still only appears for turns that do real work; the
existing compositor start gate keeps plain question-and-answer turns
card-free.
* fix(slack): finish the final inside a buffered native stream
A short narration leaves the SDK session un-flushed, so `delivered` stays
false until `stop` makes its first network call. Requiring delivery before
finishing in-stream sent the final through normal delivery and then
finalized the stream anyway, producing exactly the second message this path
exists to prevent. Stop-time rejection already falls back via
SlackStreamNotDeliveredError, so a live session is enough.
Addresses the ClawSweeper P1/P2 finding on this PR.
* refactor(slack): collapse duplicate streaming surfaces and drop dead code
Cleanup pass over the progress/streaming neighborhood, all verified unused
by exhaustive reference search:
- Deleted buildSlackProgressStreamStartChunks/UpdateChunks: byte-identical
pass-throughs to the same builder, plus the render-module branch that
chose between them. One exported builder now.
- Collapsed slackStreaming.draftMode, a lossless restatement of the mode it
was derived from, and its outbound mapper; nine comparisons now read the
mode directly. Inbound legacy parsing stays for doctor migration.
- Dropped stopSlackStream's text parameter, the draft stream's stop() member
and onMessageSent hook, a redundant nativeStreaming argument, four dead
members on the progress runtime, and two single-expression wrappers.
- Deduped the native card title, which was computed twice per render.
Production LOC for the whole PR drops from +216 to +114.
* chore(config): regenerate bundled channel metadata for the Slack card default
The generated metadata still carried the old opt-in help text and
default-false description for streaming.progress.nativeTaskCards, so
config UI and diagnostics would publish stale guidance.
* fix(slack): un-export the now-internal legacy draft-mode type
Collapsing draftMode removed the type's only external consumer, so knip
flagged it as an unused export. Doctor migration still parses these legacy
values inbound, so the type stays module-local.
* feat(slack): render live session cards as the default Slack progress mode
Slack streaming.mode default flips partial->progress.
Progress mode renders one live Block Kit session card with a status header, narration, plan, activity, diff stat, and elapsed time; it is edited in place and finalized to success or error with an Open in OpenClaw button when gateway.publicOrigin is set.
Final assistant text always delivers separately.
The shared progress compositor gains a success-only additive per-turn diffStat mirroring the task ledger fold.
resolveGatewayPublicOrigin is exported through the plugin SDK.
The diffs viewer URL falls back to publicOrigin.
The old rich/text progress render fork is deleted.
Native task cards remain unchanged and opt-in.
* chore(config): regenerate bundled channel config metadata
* refactor(slack): keep session card state type internal
* refactor(slack): split session-card and diff-stat owners under lint ceilings
* refactor(channels): reuse diff-stat type from its owner module
Import ChannelProgressDraftDiffStat from progress-draft-diffstat instead of
redeclaring it in the compositor, resolving the all-exports deadcode scan.
* chore(plugin-sdk): regenerate api baselines for channel barrels
Baselines drifted after the rebase reconciled them against main; regenerate to
match the branch's actual channel-message/channel-outbound surface.
* fix(slack): drop a session card that cannot terminalize after final delivery
If the final reply is delivered but the terminal card edit fails, the caller
now clears the stale card instead of leaving it stuck in its Working state
(mirrors the pre-card preview cleanup). Adds a transport-failure regression and
corrects three tests that asserted the prior ignore-the-result behavior.
Documents resolveGatewayPublicOrigin as a dependency-light runtime helper on the
config-contracts SDK subpath, which previously described a type-only surface.
Addresses ClawSweeper P2 (unfinalized card) and P1 (runtime SDK contract).
* fix(slack): suppress default tool messages under the default progress card
resolveChannelStreamingSuppressDefaultToolProgressMessages re-derived the stream
mode from config with an "off" default, unlike its sibling resolvers which take
a caller-resolved mode override. After this branch made progress the Slack
default, a default-config channel turn saw mode "off" and left a stray
"Using tool: X" plain message posting alongside the session card. Thread the
caller-resolved mode through (compositor passes params.mode; Slack dispatch passes
slackStreaming.mode), matching resolveChannelStreamingPreviewToolProgress.
Retarget the progress-session-card delivery-trace golden at an EMPTY Slack config
so it proves the real default path; the regenerated golden is byte-identical,
confirming defaults now yield the clean card sequence (one card post, separate
final text, one terminal update with the Open in OpenClaw button, no stray tool
message). Switch the dispatch delivery-mode mock to the real resolver so the card
tests exercise the true channel default (automatic), not a hand-rolled one.
* chore(plugin-sdk): regenerate api baselines for the streaming mode param
resolveChannelStreamingSuppressDefaultToolProgressMessages gained an optional
mode override; the changed signature reflows the surface hash of every barrel
that re-exports it, so regenerate the affected baselines.
* chore(config): regenerate config baselines
Keep raw commands, paths, and provider errors out of ordinary chat while preserving explicit raw diagnostics and structured admin history.
Default command progress is status-only; `/verbose full` and `commandText: "raw"` retain diagnostic detail.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* docs(slack): drop retired socketMode transport tuning
channels.slack.socketMode is rejected by Slack config validation, pinned by
the 'rejects retired Socket Mode ping/pong transport tuning' schema test, and
the client pong timeout is now a hardcoded 15s constant. The page still told
operators to configure it.
* fix(slack): migrate retired socketMode with doctor instead of hand-editing
The retired object fails schema validation at root and account scope, so the
docs previously left manual removal as the only recovery. Slack already owns
legacyConfigRules and normalizeCompatibilityConfig, so the strip belongs there.
* test(slack): narrow the doctor contract config record for check-test-types
normalizeCompatibilityConfig returns a possibly-absent channel entry, so the
socketMode assertions need expectDefined before indexing.
* docs(slack): describe socketMode retirement by observed behavior
openclaw config validate reports a config carrying the retired object as valid,
so the earlier 'validation rejects it / fails to load' wording was wrong. The
observable facts are that it is not read and that doctor --fix removes it.
* revert(slack): drop the duplicate socketMode doctor migration
Core already strips channels.slack.socketMode at root and account scope in
legacy-config-migrations.runtime.retired.ts via visitChannelEntries, so the
plugin-side rule was duplicate policy. Keeps this PR to the docs correction.
* docs(slack): state what doctor actually cleans up for socketMode
Doctor flags retired layout knobs with a general notice, and --fix removes the
three named fields and drops the object only once empty, so an unknown key
inside it survives.
* refactor(slack): retire inline interactive directives
Release note: Slack configs using channels.slack.capabilities.interactiveReplies, including per-account settings, are removed by openclaw doctor --fix. Slack-only [[slack_buttons:...]] and [[slack_select:...]] markup is no longer parsed; emit typed presentation buttons and selects instead.
* docs: refresh Slack docs map
* 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>
* feat(slack): add ignoreOtherMentions channel config
Mirrors the existing Discord `ignoreOtherMentions` option for Slack
channels. When set on a channel entry, drop channel/group/MPIM messages
that mention another user or subteam but not this bot — the inverse of
`requireMention`. Useful in busy channels where the bot would otherwise
reply to side conversations.
Implementation lives in `prepareSlackMessage` right after the
`allowBots: "mentions"` bot-drop gate and before the `shouldRequireMention`
non-mention drop, so it integrates with the new `messageIngress` ingress
pipeline. The gate is conditional on `canDetectMention` (botUserId
resolvable via `auth.test` or explicit mention regexes configured) to
avoid false drops when we have no reliable way to tell bot vs non-bot
mentions apart.
Slack implicit mentions (thread participation) are intentionally NOT
respected here — they fire for every message in a bot-participated
thread, so honoring them would defeat the feature in any active thread.
The gate matches on `wasMentioned` (explicit) and `hasAnyMention`, with
the existing `shouldBypassMention` override (e.g. authorized commands)
also respected.
Includes config types, zod schema, channel-config resolution,
prepare-message implementation, full test coverage in
`prepare.test.ts`, regenerated channel + docs baselines, and Slack
docs entry.
* refactor(slack): harden other-mention filtering
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>