feat(slack): UI + API to change DM-default agent per workspace (#584) - #593
Merged
Conversation
Closes #584. Adds the missing setter for the Slack DM-default routing flag — until now it was auto-set on the first agent ever connected and unchangeable through any UI or API; the only workaround was direct DB edit. Multi-agent workspaces were stuck routing every DM to the first agent forever. Backend: - db/slack_channels.py — new `set_dm_default(team_id, agent_name)` uses single-tx clear-then-set inside one BEGIN/COMMIT to avoid any window with two defaults (schema has no exclusivity constraint). - routers/slack.py — `PUT /api/agents/{name}/slack/channel/dm-default`, owner-gated via `can_user_share_agent`, audit-logged via `AGENT_LIFECYCLE/slack_dm_default_changed` (details: team_id, workspace_name, previous, new_default). Idempotent: returns status=unchanged when caller is already the default. - routers/slack.py — DELETE /slack/channel now returns **409** if the target is the workspace's DM default and other agents are bound. When the target is the only agent, unbind is allowed (clean cascade). Promoting a sibling first is the only path past the guard — keeps the workspace from ending up with zero DM default. - routers/slack.py — GET response includes `workspace_agent_count` so the UI can decide whether to disable Unbind. Frontend: - SlackChannelPanel.vue — replaces the read-only `(DM default)` label with a "Make default" button (when not default) or a "✓ DM default" badge (when default). Both carry a hover tooltip explaining what DM default means. Unbind button is disabled with an explanatory tooltip when this agent is the DM default and the workspace has other bound agents. Tests (10 new, all pass): - test_slack_dm_default.py — set_dm_default contract: returns False when agent unbound, sets exclusively, idempotent, isolated per workspace, doesn't touch siblings. unbind_agent contract: pure delete (does NOT auto-promote), works on non-default + last-agent paths, unknown agent returns False. Live verification on dev: - DM routing flips immediately after `PUT /dm-default` (no restart, no cache — `get_slack_dm_default_agent()` queries DB on every call) - Channel @mention routing unaffected (channel binding wins over DM default) - 409 surfaces with explanatory message when blocked - audit_log row written with correct actor/details Docs: - feature-flows/slack-channel-routing.md — endpoint table, UI state description, and DM-default semantics updated - feature-flows.md index — Recent Updates row added - tests/registry.json — new entry Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vybe
approved these changes
Apr 30, 2026
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
Closes #584. Until now the Slack
is_dm_defaultflag was auto-set onthe first agent connected to a workspace and unchangeable through
any UI or API — the only workaround was direct SQLite edit. Multi-agent
workspaces were stuck routing every DM to the first agent forever.
This PR adds the missing setter and tightens unbind semantics so the
workspace can never end up with no DM default.
Changes
Backend
db/slack_channels.py— newset_dm_default(team_id, agent_name)uses single-tx clear-then-set inside oneBEGIN/COMMITto avoid any window with two defaults (schema has no exclusivity constraint).routers/slack.py—PUT /api/agents/{name}/slack/channel/dm-default: owner-gated viacan_user_share_agent, audit-logged viaAGENT_LIFECYCLE/slack_dm_default_changed(details:team_id,workspace_name,previous,new_default). Idempotent — returnsstatus=unchangedif caller is already default.routers/slack.py—DELETE /slack/channelnow returns 409 if the target is the workspace's DM default and other agents are bound. Promoting a sibling first is the only path past the guard. When the target is the only agent, unbind is allowed (clean cascade).routers/slack.py— GET response now includesworkspace_agent_countso the UI can decide whether to disable Unbind.Frontend
SlackChannelPanel.vue— replaces the read-only(DM default)label with a "Make default" button (when not default) or a "✓ DM default" badge (when default). Both carry a hover tooltip explaining DM-default semantics. Unbind button is disabled with an explanatory tooltip when this agent is the DM default and siblings remain.Tests — 10 unit tests, all pass:
set_dm_default: returns False when agent unbound, sets exclusively, idempotent, isolated per workspace, doesn't touch siblings (5 tests)unbind_agent: pure delete (does NOT auto-promote — router enforces guard), works on non-default + last-agent paths, unknown agent returns False (4 tests)Docs
feature-flows/slack-channel-routing.md— endpoint table, UI state, DM-default semanticsfeature-flows.mdindex — Recent Updates row addedtests/registry.json— entry for the new test fileLive verification on local dev
PUT /dm-default(no restart, no cache —get_slack_dm_default_agent()queries DB on every call)@mentionrouting unaffected (channel binding wins over DM default)audit_logrow written with correctactor_type=user,event_action=slack_dm_default_changed, full detailsSlackAdapter.get_agent_name()— flippingis_dm_defaultvia API changes which agent is returned for anis_dm=TrueNormalizedMessageTest plan
pytest tests/unit/test_slack_dm_default.py -v— 10/10 passpytest tests/unit/test_agent_shared_files_migration.py tests/unit/test_file_sharing_mixin.py tests/unit/test_file_download_no_session_gate.py -q— 39/39 pass/validate-pr 585before mergeOut of scope (deferred)
is_dm_defaultboolean schemaRelated
🤖 Generated with Claude Code