docs: merge dual [Unreleased] sections in CHANGELOG#4240
Merged
Conversation
CHANGELOG.md had two `## [Unreleased]` blocks buried mid-history (between 2026.4.19/4.18 and 2026.4.13/4.11). The release.yml awk extractor and xtask/changelog.rs both stop at the first match, silently dropping every entry in the second block. Fold both into a single canonical `## [Unreleased]` at the top of the version list, ordered Added / Changed / Security. No verbatim duplicates existed between the two blocks; every bullet is preserved. Add a one-line guard to scripts/hooks/pre-commit that rejects commits when CHANGELOG.md contains more than one `## [Unreleased]` heading, so the regression is caught locally before push.
houko
added a commit
that referenced
this pull request
May 20, 2026
Two CHANGELOG fixes: 1. Add the missing `(@houko)` attribution to the Slack and Discord RBAC-regression bullets under [Unreleased] — the only thing failing PR 5302's "CHANGELOG Attribution" / "CHANGELOG Attribution (full [Unreleased])" CI checks. 2. Remove a stale, misplaced second `## [Unreleased]` block (wedged between the 2026.5.2 and 2026.4.28 dated releases, an orphan left by the #4240 dual-section merge) and the pre-CalVer 0.x version history (`[0.7.0]` … `[0.1.0]`). The duplicate [Unreleased] was tripping the pre-commit duplicate-section guard; the 0.x block is long-dead release history still recoverable from git. All 2026.x CalVer history is preserved.
houko
added a commit
that referenced
this pull request
May 20, 2026
) * feat(channels)!: migrate slack from in-process adapter to sidecar Replace the 1890-line in-process librefang-channels::slack adapter (Socket Mode WS + Web API) with an out-of-process Python sidecar at sdk/python/librefang/sidecar/adapters/slack.py (stdlib-only). Behaviour parity: - POST /api/auth.test startup probe to discover bot_user_id - POST /api/apps.connections.open to mint Socket Mode WSS URL - Envelope-id ACK loop for events_api / interactive - message + app_mention event handling with message_changed subtype extraction; all-other-subtype skip - Self-skip on bot_id presence OR user == bot_user_id - allowed_channels filter with DM exemption (channels starting with D) - Slash-command routing on /cmd args -> Command (text otherwise) - thread_ts captured as thread_id - DM detection via channel-id prefix; is_group = !channel.startswith("D") - block_actions interactive payload -> ButtonCallback content with action_id / trigger_id / block_action metadata - chat.postMessage send with optional thread_ts + unfurl_links + Block Kit blocks; 3000-char chunking matches SLACK_MSG_LIMIT - eyes reaction on receive flipped to white_check_mark on send-complete (opt-out via SLACK_REACTIONS=false) - force_flat_replies knob posts replies as top-level channel messages instead of threads - sender_user_id metadata key matches Rust SENDER_USER_ID_KEY parity - account_id injection for multi-bot routing One improvement over the Rust adapter: pending-reaction map is now bounded at MAX_PENDING_REACTIONS=2000 entries with oldest-eviction. The Rust adapter used an unbounded RwLock<HashMap> so a flood of inbound messages followed by a hang in the agent loop would grow the map without bound — a small but real memory-leak surface. Two regressions to call out (both match the telegram-#5241 and discord-#5299 precedents; flagged so operators aren't surprised): - **Live Slack workspace-role RBAC is gone.** ChannelRoleQuery is a Rust trait the sidecar process cannot implement, so role_query is None for Slack post-migration; the kernel's resolve_role_for_sender falls through to the default-deny Viewer branch and [channel_role_mapping.slack] is no longer consulted. Workaround: enumerate authorised operators under [users] with channel_bindings = { slack = ["<slack_user_id>"] } and an explicit role. The parse_users_info precedence parser is preserved in the sidecar so a future sidecar-protocol query/response pair can re-use it. - **Per-channel proxy override (#4795) is not wired through.** The sidecar honours HTTP_PROXY / HTTPS_PROXY / ALL_PROXY via stdlib but has no SLACK_PROXY_URL env var yet. Filed as a follow-up. Cascade removal: - src/slack.rs deleted; lib.rs mod decl, channels-allowlist.txt entry, cargo features in librefang-channels + librefang-api (incl. membership in all-channels / all-channels-no-email / core-channels / mini) removed - SlackConfig struct + Default impl + canonical deny_unknown_fields rustdoc anchor moved to WhatsAppConfig; ChannelsConfig.slack field + default removed; validation hook removed - channel_bridge.rs: import, spawn block, find_channel_info! arm, check_channel! arm, default-empty test assertion removed - routes/channels.rs: ChannelMeta entry + 5 match arms removed; live-test slack branch (POST to slack.com/api/chat.postMessage) retired with a comment explaining why; SIDECAR_CATALOG slack entry added; instance_helper_tests reanchored on matrix; test_channel tests reanchored on matrix - routes/config.rs ch!(slack) call removed; is_writable_config_path test fixtures switched to matrix - kernel channel_sender for_each_channel_field! macro entry + EXPECTED name-list entry removed - CLI: librefang setup slack wizard arm + channel list row removed; TUI ChannelDef removed - channels_routes_test: SlackConfig fixtures retargeted to MatrixConfig (bot_token_env -> access_token_env), test names / display_name assertions updated, "other rows must not flip" check uses mattermost instead - routes/skills.rs tests: channels.slack -> channels.matrix bulk rename across all TOML write/read tests; ad-hoc local Slack struct renamed to Matrix - types/config/mod.rs: test_slack_config_defaults + test_slack_config_unfurl_links_deserialization tests removed (covered by slack.rs unit tests which were also deleted); validate_missing_env_vars retargeted to WhatsAppConfig; test_one_or_many_single_toml_table + test_one_or_many_array_of_tables retargeted to MatrixConfig; well_formed_repeated_channel_table_still_parses_5130 drops the slack fixture (slack was the last working anchor — now only whatsapp + mattermost remain as deny_unknown_fields drift sentinels) - openclaw migrator: slack channel input now produces a SkippedItem with a sidecar-redirect reason rather than writing [channels.slack] (mirrors telegram + discord); test_policy_migration retargeted at mattermost (slack and discord both sidecar-skipped); test_full_migration legacy YAML fixture now includes mattermost.yaml so the "report.imported.iter().any(Channel)" assertion still has a witness; test_json5_channel_extraction adds mattermost and asserts all three of telegram/discord/slack are sidecar-skipped - openfang test fixtures reanchored on [channels.whatsapp] - librefang.toml.example + cli init_default_config.toml: [channels.slack] block replaced with commented [[sidecar_channels]] template - docs (en + zh): configuration/page.mdx, configuration/channels/page.mdx, integrations/channels/page.mdx, integrations/channels/core/page.mdx — [channels.slack] blocks replaced with [[sidecar_channels]] templates - ChannelType::Slack enum variant kept (still used by the bridge / router; same pattern as ChannelType::Telegram / ChannelType::Discord) New env-var knobs (read by the sidecar from [sidecar_channels.env]): SLACK_ALLOWED_CHANNELS, SLACK_UNFURL_LINKS (tri-state), SLACK_FORCE_FLAT_REPLIES, SLACK_REACTIONS, SLACK_ACCOUNT_ID. SLACK_APP_TOKEN and SLACK_BOT_TOKEN go to ~/.librefang/secrets.env. Operator action required: an existing [channels.slack] block is no longer recognised — re-declare as a [[sidecar_channels]] running python3 -m librefang.sidecar.adapters.slack. Verification: - cargo check --workspace --lib: clean - cargo check --workspace --features librefang-api/all-channels: clean - cargo clippy --workspace --all-targets --features librefang-api/all-channels -- -D warnings: zero warnings - cargo test -p librefang-channels --features all-channels --lib: 766/766 - cargo test -p librefang-types --lib: 838/838 - cargo test -p librefang-kernel --lib: 1085/1085 - cargo test -p librefang-migrate --lib: 51/51 - cargo test -p librefang-api --lib --features all-channels: 692/692 - cargo test -p librefang-api --test channels_routes_test --features all-channels: 38/38 - cargo test -p librefang-api --test config_routes_integration --features all-channels: 31/31 - cd sdk/python && pytest: 488/488 (72 new for slack) * docs(changelog): add missing attribution and prune stale history Two CHANGELOG fixes: 1. Add the missing `(@houko)` attribution to the Slack and Discord RBAC-regression bullets under [Unreleased] — the only thing failing PR 5302's "CHANGELOG Attribution" / "CHANGELOG Attribution (full [Unreleased])" CI checks. 2. Remove a stale, misplaced second `## [Unreleased]` block (wedged between the 2026.5.2 and 2026.4.28 dated releases, an orphan left by the #4240 dual-section merge) and the pre-CalVer 0.x version history (`[0.7.0]` … `[0.1.0]`). The duplicate [Unreleased] was tripping the pre-commit duplicate-section guard; the 0.x block is long-dead release history still recoverable from git. All 2026.x CalVer history is preserved. * fix(kernel): resolve channel owner for sidecar channels in channel_send mirror `resolve_channel_owner` only scanned the in-process `cfg.channels` (via `for_each_channel_field!`), so once a channel migrated to a sidecar (`[[sidecar_channels]]`) it returned None and the agent's outbound `channel_send` was no longer mirrored back into the channel-owning agent's session (#4824). This silently affected every migrated sidecar channel — telegram (#5241), discord (#5299), nextcloud, rocketchat, reddit, bluesky, mastodon, and now slack — not just the one this PR migrates. Consult `cfg.sidecar_channels[*].default_agent` too (the same field that seeds inbound routing via `AgentRouter.channel_defaults`), keyed by `channel_type` falling back to `name`, so the mirror resolves an owner uniformly across in-process and sidecar channels. First matching entry with a non-empty default_agent wins (deterministic Vec order). Surfaced by automated review on this PR. Extracted the matching into a pure `sidecar_default_agent` helper with unit coverage (`sidecar_default_agent_matches_by_channel_type_then_name`, `sidecar_default_agent_skips_entries_without_agent_and_is_first_match`). * fix(slack): thread top-level replies and finalize 👀 on the right message `parse_slack_event` set `thread_id = thread_ts` only, so a top-level message carried `thread_id = None`. Two consequences: 1. The bot's reply posted at the channel root instead of threading under the triggering message. `force_flat_replies` exists to opt *out* of threading, so threaded-by-default is the intended behaviour (and what the Rust adapter did). 2. `on_send` finalized the `👀` → `✅` reaction using the posting `thread_ts` (forced to None under SLACK_FORCE_FLAT_REPLIES, and None anyway for top-level), so it fell back to "first pending reaction in the channel" and flipped the wrong message under concurrency, leaving the real request stuck on `👀`. `thread_id` now falls back to the message's own `ts` (mirroring rocketchat / nextcloud's `thread_id = parent or own_id`), and `on_send` finalizes against the inbound thread id rather than the force-flattened posting `thread_ts`. The eyes is tracked by the message's own ts, so the top-level case (the dominant @-mention pattern) now finalizes exactly. In-thread replies stay best-effort — the `Send` protocol carries no inbound `message_id` to key on. Surfaced by automated review on PR #5302. Tests: test_parse_event_top_level_thread_id_falls_back_to_ts, test_on_send_force_flat_finalizes_correct_message. Full sdk/python suite: 548 passed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #3395.
CHANGELOG.mdhad two## [Unreleased]blocks buried mid-history (between2026.4.19/4.18and2026.4.13/4.11). Both therelease.ymlawk extractor andxtask/src/changelog.rsstop at the first## [they find after the version heading, so the second block was silently dropped from every release note and from any tooling that searches forUnreleased.Changes
CHANGELOG.md: Folded both[Unreleased]blocks into a single canonical## [Unreleased]at the top of the version list (above2026.4.28), ordered Added / Changed / Security. No verbatim duplicates existed between the two blocks; every bullet is preserved (feat(runtime): add config-driven session_mode for agent triggers #2341/session_mode, Windows automatically initializes after installation #2766/api_listen, ci(mobile): iOS + Android release jobs and PR build smoke #3970/iOS 16, channel HMAC, fix(channels): mandatory webhook HMAC verification + SSRF guard #3942/SSRF, fix(api): derive require_auth_for_reads from api_key when unset #2448/require_auth_for_reads).scripts/hooks/pre-commit: Added a one-line guard — whenCHANGELOG.mdis staged, fail the commit ifgrep -c '^## \[Unreleased\]' CHANGELOG.mdis greater than 1. Fits naturally into the existing pre-commit hook (already enabled viajust setup/core.hooksPath = scripts/hooks).Notes / Follow-ups
just setup; a 3-line workflow step (or a step inci.yml's docs lane) would catch it for everyone else. Left out per the issue's "one-line guard that fits naturally" guidance — adding a new workflow step is non-trivial scaffolding here.[0.1.0]: https://...reference link at the bottom ofCHANGELOG.md(also flagged by the issue) is left untouched; the issue's explicit Fix scope only covers the[Unreleased]duplication.Test plan
grep -c '^## \[Unreleased\]' CHANGELOG.mdreturns1[Unreleased]blocks present in merged section