Let an engaged Slack channel keep listening, and read the thread it joined - #1168
Merged
Conversation
…oined
A bot that must be @-mentioned for every message cannot hold a conversation:
the person asking has to keep re-addressing a participant that is visibly
already in the room. And a bot mentioned halfway down a thread could not
answer "what do you think?", because all it ever received was those five
words.
Two per-channel options, both bounded:
[channels.my-bot]
follow_up = "thread" # off | thread | channel
thread_context = 50 # earlier thread messages read on joining; 0 = none
follow_up decides where an untagged message still counts as ours. "Already
engaged" is derived, not stored: a channel is engaged in a conversation
exactly when it already routes a session for it. There is no second
participation record to drift from the routing table or need expiry. Each
thread stays its own session in every mode, so unrelated topics never share
context.
thread_context reads a thread's earlier messages when the bot is first pulled
into one, and only then — afterwards the session has been present for
everything said in the thread.
Both need Slack app changes the operator makes: the message.channels event
subscription, and the channels:history scope. Without them Slack sends no
untagged messages and refuses the read, so a channel behaves exactly as it
does today. A refused history read costs context, never the answer.
Reading a thread widens who can put text in front of a session that holds
tools: before this only the person addressing the bot supplied input, now
everyone in the thread does. History is fenced in a block marked as material
to read rather than instructions to follow, kept to the one thread, and
disabled with thread_context = 0. The fence is a mitigation, not a guarantee,
and it is documented as such.
Also keys request dedup on the message rather than the delivery. Slack fires
both app_mention and message.channels for a message that mentions the bot,
under different event ids, so the old event-keyed dedup would have let one
message start two turns as soon as the second subscription was enabled.
Fixes a flaky test introduced with the progress affordance: its stub served
one request per connection without Connection: close, so reqwest pooled the
socket and the next call raced the server's close.
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.
Two per-channel options, both bounded:
Answering without being mentioned
A bot you must
@-mention for every message can't hold a conversation — you keep re-addressing a participant that's visibly already in the room. DMs have always worked untagged;follow_upextends that to channels.thread(default)channeloff"Already engaged" is derived, not stored. A channel is engaged in a conversation exactly when it already routes a session for it. No second participation record that could drift from the routing table, survive a restart wrongly, or need expiry. Each thread stays its own session in every mode, so unrelated topics never share context.
Reading the thread it joined
A bot mentioned halfway down a thread couldn't answer "what do you think?" — all it ever received was those five words.
thread_contextreads the thread's earlier messages when the bot is first pulled in, and only then; afterwards the session has been present for everything said.What you have to change in Slack
Neither works until the app grants it, and both fail closed:
follow_upmessage.channelsevent subscription (+message.groupsfor private)offthread_contextchannels:historyscope (+groups:history)So merging this changes nothing about your current bot until you reinstall the app with those.
Trust boundary — worth reading before enabling
thread_contextThis widens who can put text in front of a session that holds tools. Before, only the person addressing the bot supplied input; now everyone in the thread does.
Mitigations: history is fenced in a block explicitly marked as material to read rather than instructions to follow, it's confined to the one thread, and
thread_context = 0disables it. The fence is a mitigation, not a guarantee — that's stated in the docs too. If a channel's participants aren't people you'd let instruct the agent directly, leave it at0.A bug this would have introduced
Slack fires both
app_mentionandmessage.channelsfor a message that mentions the bot, under different event ids (docs). The existing dedup keyed on the event id, so the moment you enabled the second subscription, one message would have started two turns. Dedup is now keyed on the message (channel+ts), which also still absorbs Slack's own retries.Also fixes a flaky test I introduced in #1166
Its stub server served one request per connection without
Connection: close, so reqwest pooled the socket and the next call raced the server's close. The older tests in that file get this right; mine didn't. It failed ~1 run in 4 under load; 4 clean full-suite runs since.Tests
7 new, covering: untagged channel messages parse as candidates rather than deliveries; the follow-up decision table; one message having one identity across both subscriptions; history fencing (including that an injection-looking line stays inside the fence and the triggering message isn't duplicated); empty-thread handling; engagement lookups being per-channel; and TOML parsing incl. defaults for definitions written before these options existed.
cargo test --workspace: 51 test binaries, 0 failures.Note on flakes:
service_supervisorport-binding tests flake under parallel load on this machine — I confirmed they flake on cleanmainat a similar rate (1 in 4), so they're pre-existing and unrelated.Not covered by tests: the
conversations.repliescall itself is unit-tested against its parsed shape, not a live workspace.Binary
Touches
crates/daemon→ the relevant binary isconstruct.New spec
0179-an-engaged-channel-keeps-listening.md;docs/services.mdgains both options.