* 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>
* fix(channels): restore transcript context after restart
Merge bounded active-branch session transcript turns at the shared prepared-turn seam so message channels retain assistant replies after restart or history eviction. Migrate Telegram's one-off merge while preserving exact projection and legacy dedupe behavior.\n\nCloses #112520. Slack case reported by Joe Tam (@joetam) in #102594.
* style(channels): avoid spread in transcript mapping
* refactor(telegram): drop obsolete transcript exports
* 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): ingress and preflight consumers ride ordered media facts
Media-facts program PR 2: channel context retains ordered MediaFact[]
through finalization; inbound gates, hook mapping, current-turn
normalization, and all five channel families' audio preflights consume
facts. Four review cycles hardened the canonical/legacy merge: canonical
facts are authoritative (legacy fills gaps only), one shared staging
predicate covers MediaStaged and MediaWorkspaceDir, kind inference runs
after fallback MIME fills, legacy counts use max array cardinality, and
alignment-only empty slots no longer count as media presence — all
pinned by a 54-cell canonical×legacy×type merge-matrix test.
* fix(media): hide internal fact helper
* fix(test): converge Slack harness state across module reloads
Five reaction tests in monitor.tool-result.test.ts failed whenever a sibling
file's vi.resetModules() ran earlier in the same non-isolated worker: the
cached globalThis __slackClient kept routing reactions to the OLD test-helpers
module's mocks while tests asserted on the recreated module's fresh mocks —
reactions 'never fired' (replies survived via freshly injected runtime).
slackTestState is now a globalThis-backed singleton so every module
incarnation and the cached client share one state object. Also: reaction
assertions wait on the mock (reactions apply via detached debounce/queue
work), and stale Bolt handler registrations clear per test/stop so
waitForSlackEvent cannot match a previous provider's handler. Proven with six
consecutive full extensions/slack runs (was ~50% failure).
* style(slack): bracket global test-state access for underscore lint
* 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.
* fix(slack): adopt durable ingress drain across Bolt and relay with logical twin guard
Slack events now enqueue their raw envelope durably before the transport ack
on all three modes: the Bolt receiver wrap covers Socket Mode and HTTP
(event_id = Events API event_id), and relay frames enqueue before the router
ack (keyed by logical team:channel:ts since router delivery-id redelivery
stability is undocumented). Dispatch, retry, dead-letter, and 30d/20k
tombstones run through the core drain; claimed relay events retry until the
relay source reattaches after a restart. The in-memory + persistent inbound
delivery caches and the app-mention race machinery are deleted.
One dedupe layer deliberately survives: Slack emits both a message and an
app_mention event (distinct event_ids) for a single mention post, so a new
claim-based dispatch guard keyed on logical (team, channel, ts) collapses the
twins — commit at turn adoption, release on gated/abandoned dispatch so the
surviving twin can re-run the same gate. 24h/20k retention mirrors the
retired persistent cache.
Slack monitor test state now scopes a fresh OPENCLAW_STATE_DIR per test;
persisted guard keys otherwise dedupe unrelated fixture messages that reuse
ts values. Five reaction tests in monitor.tool-result.test.ts are flaky on
clean origin/main under full-suite load (verified 2-of-3 baseline runs);
tracked separately.
Autoreview: secret scanner false-positives on token-shaped test fixtures
(three files); full manual review performed instead, which caught and fixed
the twin-guard regression. Part of the #109657 fleet adoption program.
* fix(slack): remove unused ingress exports
* chore(slack): prune max-lines baseline
* docs(plugin-sdk): refresh API baseline hash
* test(slack): type dispatch mock argument
* test(slack): use managed temp root
* refactor(sdk): collapse plan updates to typed steps
Per owner decision, remove the shipped steps: string[] SDK field and its unreleased planSteps replacement early. Collapse onPlanUpdate to one typed steps field. Retain wire-level string normalization for external Codex plugin version skew.
* chore(sdk): tighten rebased surface budget
* refactor(channels): shared supplemental sender gating, allowlist-match adoption, outbound mop-ups
* fix(plugin-sdk): skip-aware media sequence with text fallback for empty URLs
* chore(plugin-sdk): align surface budgets after rebase
* test(qqbot): type media sender mock calls
* fix(plugin-sdk): distinguish empty media sequences
* fix(plugin-sdk): track void media sends
* 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>
* test(slack): drop rotating loading-message fixture expectations
* refactor(slack): drive progress drafts through the shared compositor
Slack's status_final progress path now uses the shared channel compositor
(preamble headline, reasoning, tools, plan checklist, commentary
arbitration) instead of its hand-rolled gate/merge/render pipeline, and
native task cards consume compositor snapshots with stable row identity
(line ids first, fixed reasoning id, occurrence-suffixed content hashes).
Finals in progress mode post as fresh messages that follow the draft's
thread; the working draft then collapses into a compact activity receipt,
while failed or error finals leave the draft as the turn record. The
rotating native loading messages are gone in favor of the plain typing
status. Queued and assistant-boundary turns re-arm with clean delivery
state, and native completion status survives transient append failures.
* refactor(channels): generalize channel retry runner and retry-after parsing; drop dead configured-state probes
* fix(channels): align retry and metadata contracts
Extract shared LRU map helpers so resolveUserName touches recency on hit
and pruneMapToMaxSize caps the map at 2048 after each successful users.info write.
Rebased onto current main; Evidence scrubbed of token suffixes.
* feat(channels): render live plan checklists from typed update_plan snapshots
Codex, Copilot, CLI, and embedded update_plan events now carry typed
{step, status} snapshots end to end instead of flattened strings. Slack,
Discord, Telegram, Matrix, and MS Teams render a live checklist in
progress drafts; Slack native task cards show the real plan with
position-keyed rows and id reconciliation. The embedded update_plan tool
is default-on for every model (tools.experimental.planTool: false opts
out). The shipped onPlanUpdate steps: string[] SDK field stays populated
during a deprecation window alongside the canonical planSteps.
* fix(auto-reply): align plan bridge types with SDK callback contract
* fix(auto-reply): split agent event bridge module and fix plan type boundaries
* fix(channels): satisfy oxlint on plan checklist helpers
* fix(slack): mark connected status degraded without bot identity
When auth.test fails or botToken is a user token (user_id without bot_id),
Socket Mode can still come up. Keep connected=true for transport liveness
but publish healthState=degraded with lastError instead of advertising healthy.
Rebased onto current main; credentials revoked by operator.
* fix(slack): report degraded socket identity health
Co-authored-by: zw-xysk <zhao.wang1@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>