feat(channels): add MentionRequiredInThread — gate thread replies on @-mention - #1783
Merged
Aaronontheweb merged 4 commits intoAug 7, 2026
Merged
Conversation
… @-mention Adds a per-connector MentionRequiredInThread option (default false) to Slack, Discord, and Mattermost channel options. When enabled, the routing policies require a bot mention for thread replies even when the thread already has an active session, and the daemon-restart rehydration path is gated the same way. Drops get a distinct ThreadMentionRequired ignore reason / telemetry label so the behavior is observable in logs and counters. Closes netclaw-dev#1782
Aaronontheweb
enabled auto-merge (squash)
August 7, 2026 00:53
Aaronontheweb
approved these changes
Aug 7, 2026
Aaronontheweb
left a comment
Collaborator
There was a problem hiding this comment.
LGTM - have some more engineering work I'm going to layer on top of this for the next release, but this is a bug UX/DX improvement for multi-speaker rooms
This was referenced Aug 7, 2026
Aaronontheweb
added a commit
that referenced
this pull request
Aug 7, 2026
…gered backfill (#1798) * docs(openspec): add per-channel MentionRequiredInThread change plan Adds the OpenSpec change per-channel-mention-required-in-thread with proposal, design, six spec deltas, and tasks. Planning only; no runtime code. The change makes MentionRequiredInThread a per-channel value, reuses the existing thread-history backfill on a mention, seeds the value at channel-add time, and deletes the connector-wide bool from #1783 (never deployed, so no migration). * feat(channels): make MentionRequiredInThread per-channel; delete connector bool Replaces the connector-wide MentionRequiredInThread bool (never deployed) with a per-channel MentionRequiredInThreadByChannel map on the Slack, Discord, and Mattermost channel options, plus a MentionRequiredInThreadFor(channelId) resolver that defaults to false for unmapped channels. The three conversation actors resolve the per-channel value and pass the resolved bool into the unchanged, pure routing policies. Updates the config schema (adds the per-channel map, removes the connector bool) and adds per-channel binding/resolution tests. Implements OpenSpec tasks group 2 (config storage) and group 3 (routing gate). * chore(openspec): mark tasks groups 1-3 done (config storage + routing gate) * feat(channels): re-hydrate the thread gap on a tap-gated mention When MentionRequiredInThread is on for a channel, the conversation actor forwards only mentions to the thread binding actor, so each inbound is a deliberate re-entry. The binding actor now re-arms the existing deferred thread-history hydration on such an inbound, guarded on no in-flight turn (cursor not lagging) to preserve the PR #733 no-duplicate invariant, so the mention catches up on the gap the tap held. Applies to the Slack, Discord, and Mattermost binding actors, reusing the existing fetch / gap-computation / prompt-injection-gate / merge path (no new fetch path, no new watermark). Adds a Slack integration test proving a second mention on the same live actor re-fetches (fetchCount 1 -> 2 with no restart); Discord and Mattermost use the identical re-arm. Implements OpenSpec tasks group 4 (backfill re-trigger). * chore(openspec): mark tasks group 4 (backfill re-trigger) done * docs(channels): document per-channel MentionRequiredInThreadByChannel Updates the config reference (docs/spec/configuration.md), the three channel integration pages, slack-acl-policy.md, and the adding-a-channel runbook to describe the per-channel MentionRequiredInThreadByChannel map and the removal of the connector-wide bool. The netclaw-operations skill covers operational tasks (scheduling, doctor, approvals, MCP), not channel config, so no skill change is warranted. Implements OpenSpec tasks group 6. * feat(cli): add per-channel MentionRequiredInThread toggle to netclaw config Adds a per-channel MentionRequiredInThread control to the netclaw config Channels editor, mirroring the ChannelAudiences per-channel machinery for a bool map (MentionRequiredInThreadByChannel): - The EditAudience leaf shows 'Mention required in thread: On/Off' and toggles it with Space; Enter persists it alongside the audience. - The add-channel step seeds the rule from the assigned audience (Team/Public -> on; Personal/DM -> off). - Name->id canonicalization remaps the mention map like audiences, so a rule set on a channel name is not silently dropped when the name resolves to its id. - Adds a config round-trip test and a native smoke tape (config-mention-thread) registered in the light suite. Implements OpenSpec tasks group 5. * test(smoke): add config-mention-thread post-tape assertion The config-mention-thread tape is a config-writing tape, so it requires a paired semantic assertion. Adds tests/smoke/assertions/config-mention-thread.sh, which verifies Slack.MentionRequiredInThreadByChannel.C01 = true after the leaf toggle + apply. Marks OpenSpec tasks groups 5-6 and quality gates 7.1-7.4 done. * chore(openspec): sync per-channel MentionRequiredInThread deltas to specs and archive openspec archive synced the six delta specs into openspec/specs/ (7 added requirements, 3 modified across netclaw-input-adapters, thread-history-backfill, the three socket specs, and channel-audience-tui) and moved the completed change to openspec/changes/archive/2026-08-07-per-channel-mention-required-in-thread. Closes out OpenSpec task group 7 (quality gates + close-out): full solution build clean, routing/backfill/TUI tests green, slopwatch 0 issues, copyright headers verified, and the full native smoke light suite (23 tapes + 9 scenarios) green including the new config-mention-thread tape. * fix(channels): harden mention re-arm guard against null-key turns Replace the _pendingCursor* null check with a dedicated _turnInFlight flag in the Slack, Discord, and Mattermost binding actors. The cursor is set only for inbounds with a parseable ordering key, so a null-key in-flight turn left the cursor unset and let a following mention re-arm hydration mid-turn -- the concurrency the PR #733 invariant prevents. _turnInFlight is set on every enqueue (live inbound and hydration backfill), independent of the ordering key, and cleared at TurnCompleted and on pipeline reinit so an abandoned turn cannot leave it stuck. Document two accepted trade-offs of the reuse-based backfill in the re-arm comment: one thread-history fetch per mention on an active thread, and a mention in the in-flight window adopts chatter on the next idle mention. * feat(cli): fold the channel detail leaf into the Channels & Permissions list Remove the per-channel EditAudience leaf. The list edited a channel's audience inline already, so the leaf only re-presented one field; its sole unique job was the mention toggle. Make the list the single per-channel editor. On the Channels & Permissions list a real channel row now shows the audience cycler plus an arrow-free 'Require @mention: On/Off' field. Space toggles the rule on the focused row and autosaves, like the left/right audience toggle. A description line under the list follows the cursor and explains the selected row's audience and mention state. A DM row shows audience only; the rule does not apply to one-to-one messages. Delete BuildEditAudience, HandleEditAudienceKey, OpenSelectedChannelAudience, the leaf-only view-model state, and the EditAudience screen. Enter on the list now only activates the Add/Done rows. Rewrite the config-mention-thread smoke tape to toggle on the list row instead of the leaf; the paired assertion is unchanged (C01 persists true). Update the view-model tests to the list flow. * fix(cli): restore [Enter] Done in the Channels & Permissions footer The list footer legend lost its [Enter] binding when the mention toggle was added, but Enter still activates the Add/Done rows. Restore [Enter] Done so the footer and the secondary hint line agree. Drop the [a] accelerator from the footer to keep it within terminal width; the hint line still documents it. Addresses a code-review finding. The paired finding (raw channel ID no longer shown after removing the detail leaf) is intentionally not fixed: the list deliberately hides raw IDs for resolved channels (see the Channels_ChannelPermissions_RendersResolvedDiscordLabelWithoutRawId test).
Merged
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
Implements #1782: adds a
MentionRequiredInThreadoption to the Slack, Discord, and Mattermost connectors, mirroring the existingMentionRequiredInDm.Today, once a thread has an active session the bot responds to everything in that thread without an @-mention (the active-session bypass). Operators who want strictly mention-gated interaction in busy channels have no opt-out. This PR adds that opt-out.
Behavior
With
MentionRequiredInThread: true:ThreadMentionRequiredignore reason (distinct telemetry labelrouting_policy_ignore:ThreadMentionRequired, observable in logs/counters).Default is
false— existing behavior is completely unchanged.Changes
*ChannelOptions.cs(Slack/Discord/Mattermost): newMentionRequiredInThreadproperty (defaultfalse)*RoutingPolicy.cs(all three): newmentionRequiredInThreadparameter, gating on both the live-thread and rehydration branches; newThreadMentionRequiredignore reason + telemetry label*ConversationActor.cs(all three): pass the option throughnetclaw-config.v1.schema.json: new property in all three channel sectionsRoutingPolicyContractTests(newExistingThread_HonorsMentionRequiredInThreadandThreadReplyRehydration_HonorsMentionRequiredInThreadtheories run against all three fixtures), options-default assertions, and all existing call sites updateddocs/spec/configuration.md, per-channel integration docs,slack-acl-policy.md, and theadding-a-channel.mdrunbook templateVerification
dotnet build Netclaw.slnx— cleandotnet test Netclaw.slnx— all green except 5 pre-existingNetclaw.Cli.Testsdoctor-check failures (config-file-missing tests; they fail identically on the base commit in this environment and are unrelated)