Skip to content

docs: merge dual [Unreleased] sections in CHANGELOG#4240

Merged
houko merged 1 commit into
mainfrom
fix/3395-changelog-dual-unreleased
May 1, 2026
Merged

docs: merge dual [Unreleased] sections in CHANGELOG#4240
houko merged 1 commit into
mainfrom
fix/3395-changelog-dual-unreleased

Conversation

@houko

@houko houko commented May 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #3395.

CHANGELOG.md had two ## [Unreleased] blocks buried mid-history (between 2026.4.19/4.18 and 2026.4.13/4.11). Both the release.yml awk extractor and xtask/src/changelog.rs stop 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 for Unreleased.

Changes

Notes / Follow-ups

  • Did not add a parallel CI check. The pre-commit hook only runs for contributors who ran just setup; a 3-line workflow step (or a step in ci.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.
  • The stale [0.1.0]: https://... reference link at the bottom of CHANGELOG.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.md returns 1
  • All bullets from both original [Unreleased] blocks present in merged section
  • Pre-commit hook passes on the cleaned-up file
  • Reviewer manually confirms no entry was dropped

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
houko merged commit d1f1992 into main May 1, 2026
13 checks passed
@houko
houko deleted the fix/3395-changelog-dual-unreleased branch May 1, 2026 07:11
@github-actions github-actions Bot added area/docs Documentation and guides area/ci CI/CD and build tooling size/M 50-249 lines changed labels May 1, 2026
@houko houko mentioned this pull request May 1, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci CI/CD and build tooling area/docs Documentation and guides size/M 50-249 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CHANGELOG has two ## [Unreleased] sections — release tooling will silently drop one

1 participant