Skip to content

fix(channels): decide what opens a turn, and make a DM one conversation - #6647

Draft
mmabrouk wants to merge 2 commits into
channels/fix-deploy-blockersfrom
channels/fix-conversation-semantics
Draft

fix(channels): decide what opens a turn, and make a DM one conversation#6647
mmabrouk wants to merge 2 commits into
channels/fix-deploy-blockersfrom
channels/fix-conversation-semantics

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Sep 8, 2026

Copy link
Copy Markdown
Member

Context

Stacked on #6646. Three defects from the August live QA share one seam: what a stored message does once it arrives. Findings F83, F94, F84, F88 in the ledger.

Changes

  • Only some messages open a turn. Before, every message in a channel ran a paid turn, the bot's own invitation included, because the trigger policy was computed and never read. Now the channel defaults admit a mention, a command, or a button, and the resolve step applies them at dispatch. Every message is still stored, since the fill reads it back as context. A turn opens for a DM, for a reply inside a thread the agent already holds, for an answer to a pending choice, for a mention, or for a command. Slack's system notices such as channel_join are dropped in the adapter.
  • A fresh thread sees its own history, not the whole channel. The forwardfill range is the thread's: everything in a DM, the same thread key elsewhere, the addressing event alone under message scope.
  • A DM is one conversation. It is keyed on the space, so the agent keeps its memory across messages, and replies go out top-level instead of threading under each message.

Decisions taken here

Two product calls, recorded in the decision record on the docs lane: the default trigger set above, and "a DM is one conversation, replies top-level". Both follow what a person expects from a Slack bot. The alternative for DMs, one thread per message, is what the branch did and is the behavior the QA called unusable.

Tests

  • New unit tests: the gate's five cases, the DM keying and locator, the thread-scoped fill, and the Slack subtype drop. Channels unit tier: 694 pass.
  • Integration tier against the stack's Postgres: 65 pass. The Agenta channel acceptance test passes live.

https://claude.ai/code/session_01HXt8WBUK9MrWKNTYbUKpaT

@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agenta-documentation Ready Ready Preview Sep 8, 2026 1:25pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

📝 Summary

Summary by CodeRabbit

  • New Features

    • Improved channel message routing using mentions, commands, replies, and direct messages.
    • Preserved thread-specific context when providing conversation history to the assistant.
    • Improved direct-message thread handling for more consistent conversation continuity.
    • Added support for Slack thread broadcasts and file-share messages as user messages.
  • Bug Fixes

    • System-generated Slack notices, such as join, topic, and pin notifications, are no longer treated as user messages.
    • Messages that do not address the assistant no longer unnecessarily start a response turn.

Walkthrough

Slack system notices are filtered before ingestion. Inbound events now carry addressing metadata. Channel routing applies trigger rules, private-conversation keys, fresh sessions, and thread-scoped forwardfill.

Changes

Channel routing

Layer / File(s) Summary
Slack event subtype filtering
api/oss/src/core/channels/adapters/slack/adapter.py, api/oss/tests/pytest/unit/channels/slack/test_slack_adapter.py
Slack accepts thread_broadcast and file_share as messages and ignores system-notice subtypes. Tests cover both paths.
Addressed state and trigger gating
api/oss/src/core/channels/dtos.py, api/oss/src/apis/fastapi/channels/ingress.py, api/oss/src/core/channels/service.py, api/oss/src/tasks/asyncio/channels/inbox.py, api/oss/tests/pytest/unit/channels/test_channels_service_routing.py
Ingress propagates addressed metadata. Dispatch passes capabilities to compose_input. Routing gates unaddressed messages using channel trigger policy.
Conversation resolution and forwardfill
api/oss/src/core/channels/service.py, api/oss/tests/pytest/unit/channels/test_channels_service_routing.py
Private conversations use space keys, new threads receive fresh sessions, replies use private-space locators, and forwardfill keeps events from the active thread or conversation scope.

Priority: ➖ Normal — Schedule the channel conversation-semantics change because it alters DM continuity, turn triggering, Slack message handling, and thread-scoped history across the channels surface.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟡 Moderate · up to cc30c

Slack /me messages will be silently ignored, and composition can fail when its connection has been removed. Resolve both paths before merge to avoid lost user input and failed channel processing.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 33 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: trigger decisions and DM conversation behavior.
Description check ✅ Passed The description directly explains the channel routing, trigger gating, Slack subtype filtering, DM continuity, thread-scoped history, and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch channels/fix-conversation-semantics

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mmabrouk

mmabrouk commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

Codex (astra, medium, simplify skill) reviewed this lane. Verdict: "request changes" on two points, both real, both fixed in the last commit:

  1. P1: !new in a DM did not reset. A new thread row reused the closed row's session id (the DM key is constant), and the fill had no offset, so the old history came back. A new thread row now gets a fresh session id, and a DM's fill keeps only what arrived since its current thread began.
  2. P2: the fill filter checked the DM rule before message scope, so message scope did not isolate a DM message. Scope is checked first now.

Also applied: the dispatcher passes the capabilities it already holds into compose_input; the reply locator drops the platform's thread-only fields generically instead of Slack's thread_ts by name; the redundant resolved_choice argument is gone; four tests added (two-message DM continuity, !new then a message, a mention inside a held thread, message-scope isolation in a DM). Channels unit tier: 698 pass.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

@mmabrouk I will review the latest changes on #6647, including the DM reset and message-scope cases.

⚠️ Action not completed

Head commit changed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Three defects from the live QA, one seam:

- Every message in a channel ran a paid turn, the bot's own invitation
  included (F83, F94). The trigger policy was computed and never read.
  The channel defaults now admit a mention, a command, or a button, and
  resolve() applies them at dispatch: every message is still stored (the
  fill reads it back as context), but a turn opens only for a DM, a
  reply inside a thread the agent already holds, an answer to a pending
  choice, a mention, or a command. The adapter's addressed verdict is
  carried into the stored event. Slack's system notices (channel_join
  and the like) are dropped in the adapter; thread_broadcast and
  file_share still carry a person's words and stay.
- A fresh thread inherited the whole channel's history, and a DM obeyed
  the oldest instruction in it (F84). The forwardfill range is now the
  thread's own: everything in a DM, the same thread key elsewhere, the
  addressing event alone under message scope.
- A DM was keyed per message, so the agent forgot each message as soon
  as it answered it, and every answer threaded under the message (F88).
  A DM is now one conversation keyed on the space, and its replies go
  out top-level.

Claude-Session: https://claude.ai/code/session_01HXt8WBUK9MrWKNTYbUKpaT
- A new thread row is a new conversation: it gets a fresh session id, so
  `!new` in a DM no longer resumes the closed row's session, and a DM's
  fill keeps only what arrived since its current thread began.
- Message scope wins over the DM rule in the fill filter: one message per
  turn there, whatever the space kind.
- The reply locator drops the platform's own thread-only fields instead
  of Slack's by name, so a bridge or the Agenta channel gets the same
  top-level DM reply.
- compose_input takes the capabilities the dispatcher already holds and
  reads the connection only when a caller has none.
- Tests: two-message DM continuity, `!new` then a message, a mention
  inside a held thread, and message-scope isolation in a DM.

Claude-Session: https://claude.ai/code/session_01HXt8WBUK9MrWKNTYbUKpaT
@mmabrouk

mmabrouk commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Advanced

Run ID: 4e510b23-87c9-4f35-b10d-e547df2ecc2a

📥 Commits

Reviewing files that changed from the base of the PR and between 8828bb0 and cc30cdd.

📒 Files selected for processing (7)
  • api/oss/src/apis/fastapi/channels/ingress.py
  • api/oss/src/core/channels/adapters/slack/adapter.py
  • api/oss/src/core/channels/dtos.py
  • api/oss/src/core/channels/service.py
  • api/oss/src/tasks/asyncio/channels/inbox.py
  • api/oss/tests/pytest/unit/channels/slack/test_slack_adapter.py
  • api/oss/tests/pytest/unit/channels/test_channels_service_routing.py

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.


# Message subtypes that are still a person's message: a reply also sent to the
# channel, and a file shared with a comment. Every other subtype is a notice.
_MESSAGE_SUBTYPES = {"thread_broadcast", "file_share"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🌐 Web query:

Does Slack's current Events API define the me_messagesubtype as a user-authored/memessage event withchannel, user, text, and ts fields?

💡 Result:

Yes, Slack's Events API defines the me_message subtype for messages sent using the /me command, and it includes the channel, user, text, and ts fields [1][2]. When a user performs an action via the /me command, the Events API delivers a message event with the subtype field set to me_message [1][3]. The event object typically includes: - type: Always "message" [1][4]. - subtype: Always "me_message" [1][4]. - channel: The ID of the channel where the message was sent [1][4]. - user: The ID of the user who sent the message [1][4]. - text: The content of the message [1][4]. - ts: The timestamp of the message [1][4]. These fields are standard components of the me_message event structure as documented in Slack's developer resources [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- adapter.py relevant sections ---'
sed -n '1,90p;240,295p' api/oss/src/core/channels/adapters/slack/adapter.py
printf '%s\n' '--- references to subtype handling ---'
rg -n -C 3 '_MESSAGE_SUBTYPES|subtype|me_message' api/oss/src/core/channels/adapters/slack

Repository: Agenta-AI/agenta

Length of output: 9253


🤖 get_repo_knowledge executed:

get_repo_knowledge Agenta-AI/agenta /tmp/coderabbit-repo-knowledge/agenta-ai-agenta-4b53879a/conventions /tmp/coderabbit-repo-knowledge/agenta-ai-agenta-4b53879a/learnings

Length of output: 43911


Allow Slack me_message events.

When Slack emits a /me message, it uses subtype me_message with the fields consumed by this parser. The subtype filter returns None for it before storage or routing. Add me_message to _MESSAGE_SUBTYPES.

Comment on lines +1529 to +1536
if capabilities is None:
connection = await self.channels_dao.fetch_connection(
project_id=project_id,
connection_id=resolution.space.connection_id,
)
capabilities = await self.fetch_capabilities(
channel=connection.channel, connection=connection
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the service before inspecting callers and DAO contracts.
ast-grep outline api/oss/src/core/channels/service.py --items all --match 'compose_input|fetch_connection'

# Find every composition call and identify callers that use the fallback.
rg -n -C 5 --glob '*.py' '\bcompose_input\s*\(' api/oss

# Find the DAO interface and implementation declarations for fetch_connection.
fd -t f -e py . api/oss | xargs rg -n -C 4 'async def fetch_connection\s*\('

Repository: Agenta-AI/agenta

Length of output: 14140


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- service fetch_connection and compose_input ---'
sed -n '340,410p;1490,1555p' api/oss/src/core/channels/service.py

printf '%s\n' '--- DAO contract and implementation ---'
sed -n '45,75p' api/oss/src/core/channels/interfaces.py
sed -n '108,145p' api/oss/src/dbs/postgres/channels/dao.py

printf '%s\n' '--- relevant compose_input callers ---'
sed -n '330,360p' api/oss/src/tasks/asyncio/channels/inbox.py
sed -n '155,178p' api/oss/tests/pytest/integration/channels/test_channels_service_routing_dao.py

Repository: Agenta-AI/agenta

Length of output: 9130


Guard the optional connection before reading channel.

channels_dao.fetch_connection returns None when the connection is absent. The fallback then reads connection.channel, which raises AttributeError. Use self.fetch_connection(...) for the hydrated result, check for None, and apply the path’s domain error before calling fetch_capabilities.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant