Skip to content

feat(channels): per-channel MentionRequiredInThread with mention-triggered backfill - #1798

Merged
Aaronontheweb merged 14 commits into
devfrom
feature/mention-required-in-thread-per-channel-impl
Aug 7, 2026
Merged

feat(channels): per-channel MentionRequiredInThread with mention-triggered backfill#1798
Aaronontheweb merged 14 commits into
devfrom
feature/mention-required-in-thread-per-channel-impl

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

This change makes MentionRequiredInThread a per-channel setting. It is a "tap"
on how a chat channel pushes messages to the LLM. When the tap is on for a
channel, un-mentioned thread messages stop auto-flowing to the bot after the
first mention. Each new mention re-runs the existing thread-history backfill, so
the bot catches up on everything since its last reply.

The feature changes only what makes the LLM respond. It adds no new security
mechanism. The existing backfill already applies live multi-party access rules.

Why

In a busy team thread, the bot replies to every message once it joins. An
operator wants the bot to stay quiet until someone mentions it, then read the
gap and respond. This is the direct-message "mention required" behavior applied
to non-DM channels, scoped per channel.

What changed

  • Per-channel storage. Each connector gains
    MentionRequiredInThreadByChannel (a Dictionary<string, bool>), which mirrors
    the existing ChannelAudiences per-channel map. An unset channel keeps today's
    behavior (off).
  • Routing gate. The conversation actor passes the per-channel value into
    RoutingPolicy.Evaluate. When the tap is on, an un-mentioned thread message
    routes to Ignore.
  • Mention-triggered backfill. On a tap-gated mention, the binding actor
    re-arms the existing one-shot hydration path
    (BuildInputWithDeferredHydrationAsync / ApplyDeferredHydrationAsync). No new
    backfill mechanism was added.
  • Add-time seed. The add-channel step writes the flag on at add for
    Team/Public channels and off for Personal/DM. The value comes from the audience
    the operator gives.
  • netclaw config. On the Channels & Permissions list, each channel row
    carries an arrow-free Require @mention: On/Off field that Space toggles and
    autosaves, next to the ←/→ audience cycler. A description line under the list
    follows the cursor and explains the selected channel's audience and mention
    state. The old per-channel detail leaf is removed: the list is now the single
    per-channel editor (see Config design: model channels as rich objects instead of parallel keyed collections #1801).
  • Connector bool deleted. The connector-wide bool from PR feat(channels): add MentionRequiredInThread — gate thread replies on @-mention #1783 is removed,
    not migrated. It was never released, so no config carries it.

Scope

Slack, Discord, and Mattermost binding actors and connector options.

In-flight re-arm guard

The re-arm uses a dedicated _turnInFlight flag as the "no in-flight turn"
signal. The prior cursor field is set only for inbounds with a parseable
ordering key, so a null-key turn could let a following mention re-hydrate
mid-turn (the concurrency the PR #733 invariant blocks). _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.

Testing

  • Netclaw.Actors.Tests: routing-policy, options-defaults, and
    Slack-thread-backfill integration tests, including a same-live-actor
    re-hydration case. All Channels-namespace tests pass (878).
  • Netclaw.Cli.Tests: per-channel config view-model and save tests pass (suite
    green, 1314). The mention round-trip proves load-surfacing on the channel row
    plus both autosave directions (off stores as absence, on stores true).
  • Native smoke tape config-mention-thread (list-based Space toggle, no leaf)
    with a paired assertion that Slack.MentionRequiredInThreadByChannel.C01
    persists true.
  • dotnet slopwatch analyze: 0 issues. Copyright headers present.

OpenSpec

Change per-channel-mention-required-in-thread is implemented and archived; the
delta specs are synced to openspec/specs/.

Related

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).
…ector 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).
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).
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.
…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.
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.
…pecs 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.
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.
Comment thread src/Netclaw.Cli/Tui/Config/ChannelsConfigPage.cs Fixed
Comment thread src/Netclaw.Cli/Tui/Config/ChannelsConfigViewModel.cs
…ns 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.
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).
@Aaronontheweb Aaronontheweb added the tui Terminal UI (Termina) issues label Aug 7, 2026

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

"AllowDirectMessages": { "type": "boolean" },
"MentionRequiredInDm": { "type": "boolean" },
"MentionRequiredInThread": { "type": "boolean" },
"MentionRequiredInThreadByChannel": { "type": "object", "additionalProperties": { "type": "boolean" } },

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagged this and I think we're opening a new design issue for it, but basically Channel should be a rich object and have all of these properties (audience, mention required, etc) - not a bunch of parallel arrays. This is a bit of config smell.

@Aaronontheweb
Aaronontheweb enabled auto-merge (squash) August 7, 2026 21:10
@Aaronontheweb
Aaronontheweb merged commit ab63515 into dev Aug 7, 2026
36 checks passed
@Aaronontheweb
Aaronontheweb deleted the feature/mention-required-in-thread-per-channel-impl branch August 7, 2026 21:39
@Aaronontheweb Aaronontheweb mentioned this pull request Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tui Terminal UI (Termina) issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant