Skip to content

feat(slack): channel identity, proactive messaging, and per-agent bot icon (#350 Phase 1 & 3, #292)#1414

Merged
vybe merged 2 commits into
devfrom
feature/350-slack-identity-proactive
Jul 2, 2026
Merged

feat(slack): channel identity, proactive messaging, and per-agent bot icon (#350 Phase 1 & 3, #292)#1414
vybe merged 2 commits into
devfrom
feature/350-slack-identity-proactive

Conversation

@dolho

@dolho dolho commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Advances epic #350 (Slack channel behavior, parallel to the shipped Telegram #349). Lands the two phases that reuse existing shared infra and need no OAuth re-consent. Observation mode (Phase 2) is split to sub-issue #1413 (needs new channels:history scopes → Slack app re-install, message-event subscription, a dual-track migration).

Phase 1 — user identity (AC: agent sees [Channel: #name] [From: Display Name (@username)])

  • New async ChannelAdapter.enrich_message hook (default no-op) — parse_message is sync, but Slack events carry only opaque IDs, so identity needs an async API call. The router calls it once after agent resolution.
  • SlackAdapter.enrich_message resolves sender display name (users.info) + channel name (conversations.info) into message.metadata; best-effort (degrades to bare IDs, never raises).
  • message_router._format_channel_identity prepends the identity block for enriched channel (non-DM) messages. Gated on channel_name presence — so DMs, un-enriched channels, and Telegram are unchanged, and I deliberately do not flip is_group (that would change the email-access-gate/group-bypass semantics — out of scope).
  • slack_service: get_user_info / get_channel_info.

Phase 3 — proactive channel messaging (AC: list_channel_groups + send_group_message for Slack, rate-limited)

Tests

tests/unit/test_350_slack_identity.py — 9 cases (identity-prefix builder incl. DM/empty cases, enrichment metadata population, DM-skips-channel, best-effort error swallow, no-token no-op). Updated the access-gate test double for the new async hook. Full run: 81 passed across the router + slack suites. MCP tsc clean for the changed files (only a pre-existing unrelated yaml dep error remains).

Scope notes

Related to #350

🤖 Generated with Claude Code


Also in this PR — #292 (Slack bot icon)

Bundled per request (same adapter, same Slack identity surface). The bot username was already correct (send_response passes username=agent_name since #63; chat:write.customize is in the requested scopes) — the gap was the icon: nothing populated agent_avatar_url, so icon_url was always None. message_router now attaches a best-effort public avatar URL ({public_chat_url}/api/agents/{name}/avatar, cache-busted by avatar_updated_at) which the Slack adapter forwards as icon_url. Channels, DMs, threads. Best-effort (None without a public base URL; 404s fall back to the default icon). 4 unit cases added.

Related to #292

dolho and others added 2 commits July 2, 2026 11:42
…Phase 1 & 3)

Slack channel behavior lacked two of the three things Telegram #349 already had.
This lands the two that reuse the shipped shared infra and need no OAuth
re-consent; observation mode (Phase 2) is split to sub-issue #1413.

Phase 1 — user identity:
- New async `ChannelAdapter.enrich_message` hook (default no-op); the router
  calls it after agent resolution. `SlackAdapter.enrich_message` resolves the
  sender display name (users.info) and channel name (conversations.info) into
  message metadata — best-effort, degrades to bare IDs.
- `message_router._format_channel_identity` prepends `[Channel: #name]\n
  [From: Display (@user)]` for enriched channel (non-DM) messages. Gated on
  `channel_name` presence, so DMs and un-enriched/Telegram paths are unchanged
  and `is_group` access semantics are untouched.
- slack_service: add get_user_info / get_channel_info.

Phase 3 — proactive channel messaging:
- Backend: GET /api/agents/{name}/slack/channels (list bound channels) and
  POST .../slack/channels/{channel_id}/messages (owner-gated, rate-limited
  10/hr/channel + 100/hr/agent via the shared limiter, optional thread_ts,
  posts via chat.postMessage with the agent identity).
- db.get_slack_channels_for_agent (cross-workspace); SlackChannelMessageRequest.
- MCP list_channel_groups / send_group_message now accept channel_type
  "telegram" | "slack" (+ Slack thread_ts); new TrinityClient methods.

Tests: tests/unit/test_350_slack_identity.py (identity prefix + enrichment,
9 cases). Updated the message-router access-gate double for the new async hook.

Related to #350

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Slack bot posted under the app's default name/icon regardless of which
agent replied. The username was already correct (`send_response` passes
`username=agent_name` since #63, and `chat:write.customize` is in the requested
scopes) — the missing piece was the icon: nothing ever populated
`agent_avatar_url`, so `icon_url` was always None.

`message_router` now attaches a best-effort public avatar URL
(`{public_chat_url}/api/agents/{name}/avatar`, cache-busted by
`avatar_updated_at`) to the reply metadata, which `SlackAdapter.send_response`
already forwards as `icon_url`. Applies to channels, DMs, and threads (the
shared reply path). Best-effort: None when no public base URL is configured;
the avatar endpoint 404s cleanly for avatar-less agents, so a private/
unreachable base just falls back to the channel default. Channel-agnostic —
non-Slack adapters ignore the key.

Tests: 4 cases for `_agent_avatar_url` (cache-bust, no-version, no-base,
error) in test_350_slack_identity.py.

Related to #292
Bundled with the #350 Slack identity/proactive work (same PR, same adapter).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dolho dolho changed the title feat(slack): channel user identity + proactive channel messaging (#350 Phase 1 & 3) feat(slack): channel identity, proactive messaging, and per-agent bot icon (#350 Phase 1 & 3, #292) Jul 2, 2026
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

@vybe vybe 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.

Validated via /validate-pr: #350 Phase 1&3 + #292 — Slack channel identity, proactive messaging, per-agent bot icon. All CI green (pytest×6, CodeQL, schema-parity, verify-non-root, prod-image-smoke); tests for slack identity + message-router access gate. Three surfaces in sync (router + adapters + MCP channels.ts). Note: #350 is an epic — this is a phase, so no closing keyword by design. Approving for merge to dev.

@vybe
vybe merged commit 14b3b79 into dev Jul 2, 2026
19 checks passed
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.

2 participants