Skip to content

feat(daemon): Slack thread-to-agent bidirectional linking (FRI-47)#13

Merged
sethvoltz merged 6 commits intomainfrom
feature/thread-agent-linking
May 3, 2026
Merged

feat(daemon): Slack thread-to-agent bidirectional linking (FRI-47)#13
sethvoltz merged 6 commits intomainfrom
feature/thread-agent-linking

Conversation

@sethvoltz
Copy link
Copy Markdown
Owner

Summary

Implements bidirectional Slack thread ↔ agent linking (FRI-47). Once connected, a user can converse directly with a running Builder or Helper in a Slack thread without routing every message through the Orchestrator.

What changed

New files:

  • packages/shared/src/db/threads.ts — Drizzle query helpers for thread_connections
  • packages/shared/drizzle/0001_aromatic_shocker.sql — migration adding thread_connections table
  • services/friday/src/slack/thread-registry.ts — in-memory maps + SQLite persistence, idle timers, startup recovery
  • services/friday/src/slack/thread-tools.ts — orchestrator-only MCP server: thread_connect / thread_disconnect
  • services/friday/src/slack/thread-registry.test.ts — 13 registry tests
  • services/friday/src/slack/thread-tools.test.ts — 6 tool handler tests

Modified files:

  • packages/shared/src/db/schema.ts — adds threadConnections table definition
  • packages/shared/src/db/index.ts — exports threads query helpers
  • services/friday/src/agent/tools.ts — adds optional thread_ts param to slack_reply
  • services/friday/src/agent/lifecycle.ts — adds notifyThreadConnect / notifyThreadDisconnect
  • services/friday/src/agent/worker.ts — self-constructs createSlackTools(new WebClient(SLACK_BOT_TOKEN)) in allMcpServers (workers run in child_process.fork — live objects can't cross IPC)
  • services/friday/src/agent/prime.ts — thread connection docs in orchestrator, builder, helper prompts
  • services/friday/src/slack/events.ts — thread message routing: if thread_ts is connected, forward to agent via mail, skip orchestrator queue
  • services/friday/src/slack/helpers.ts — adds addReaction / removeReaction with permanent/transient error classification, 6 new tests
  • services/friday/src/index.ts — wires initThreadRegistry (with onIdleDisconnect callback) and friday-threads MCP server
  • docs/architecture.md — new modules, thread message flow path, thread_connections table, updated test coverage
  • docs/decisions.md — ADR-023: why thread_connections lives in existing friday.db

Message flow examples

Initial connect: Orchestrator calls thread_connect:link: added → thread post "Connected to builder-foo" → agent receives mail with channel_id + thread_ts

User message + agent reply: User types in thread → events.ts detects thread_ts → mailSend [thread] hello → agent wakes → calls slack_reply with thread_ts → reply lands in thread

Idle timeout (2h): Timer fires → thread post "Disconnected (idle timeout)." → :link: removed → agent notified

Manual disconnect: thread_disconnect("builder-foo") → thread post "Disconnected." → :link: removed → agent notified

Stolen connection: New thread_connect for same agent → old thread gets "Disconnected — agent connected to new thread: " → :link: removed from old anchor → new connection established

Architecture decisions

  • No new packages / db file. thread_connections is a Drizzle migration on the existing friday.dbagent_name PK + thread_ts UNIQUE enforce 0-or-1 at the DB level (see ADR-023 in decisions.md).
  • Workers self-construct Slack tools. worker.ts runs in child_process.fork() — live WebClient objects cannot be passed over IPC. Each worker creates new WebClient(process.env.SLACK_BOT_TOKEN).
  • Thread tools are orchestrator-only. friday-threads is injected only into the orchestrator's MCP server maps. Builders/helpers receive thread connections via mail and reply via slack_reply.

Test plan

  • pnpm test — 374 tests, 27 test files, all pass
  • pnpm --filter @friday/daemon exec tsc --noEmit — clean
  • pnpm --filter @friday/shared build — clean
  • thread-registry: connect/disconnect, 0-or-1 constraint, stolen-connection, touchActivity, idle timer, startup recovery
  • helpers: reaction success, permanent errors (message_not_found, already_reacted, no_reaction), transient errors (ratelimited, request_timeout)
  • thread-tools: happy paths for connect/disconnect, agent-not-found, thread-owned, stolen-connection

🤖 Generated with Claude Code

@sethvoltz sethvoltz force-pushed the feature/thread-agent-linking branch 2 times, most recently from 7a3b6d2 to d7ef5ea Compare May 3, 2026 05:37
sethvoltz and others added 6 commits May 2, 2026 22:54
Adds the persistence layer for bidirectional Slack thread-to-agent
linking. thread_connections uses agent_name as PK (one thread per
agent) and thread_ts as UNIQUE (one agent per thread), enforcing the
0-or-1 constraint at the DB level.

Also adds reaction helpers (addReaction/removeReaction) to slack/helpers,
thread_ts support to slack_reply, and thread connection documentation
to the orchestrator/builder/helper system prompts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
thread-registry.ts: in-memory maps + SQLite persistence via Drizzle
query helpers, idle timer management, connect/disconnect/touchActivity,
and startup recovery that prunes expired rows and restores live timers.

lifecycle.ts: notifyThreadConnect/notifyThreadDisconnect send mail to
the target agent with connection details and disconnect reason.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion

thread-tools.ts: orchestrator-only MCP server with thread_connect and
thread_disconnect tools that manage the registry, 🔗 reactions,
Slack posts, and agent mail notifications.

events.ts: detects thread_ts on incoming messages; if the thread is
connected, forwards to the agent via mail and returns early (skips the
orchestrator queue).

worker.ts: self-constructs createSlackTools(new WebClient(SLACK_BOT_TOKEN))
in allMcpServers so all builders/helpers have slack_reply with thread_ts
support without needing a live WebClient passed over IPC.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
index.ts: call initThreadRegistry after preflight with an onIdleDisconnect
callback that posts a Slack notice and removes the 🔗 reaction.
Injects friday-threads into the mail poller's orchestrator mcpServers.

events.ts: adds friday-threads to the orchestrator's mcpServers in
processQueue so thread_connect/thread_disconnect are available during
interactive Slack sessions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…read-tools

thread-registry.test.ts: covers connect/disconnect lifecycle, 0-or-1
constraint, stolen-connection path, touchActivity, idle timer firing,
and initThreadRegistry startup recovery (prune expired, restore live).

helpers.test.ts: adds addReaction/removeReaction tests covering success,
permanent errors (message_not_found, already_reacted, no_reaction), and
transient errors (ratelimited, request_timeout).

thread-tools.test.ts: covers happy paths for thread_connect and
thread_disconnect, plus agent-not-found, thread-owned, and
stolen-connection error cases.

Also fixes initThreadRegistry to clear in-memory maps before rebuilding
from DB — ensures a clean slate on restart and makes tests deterministic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
architecture.md:
- Add thread-registry.ts and thread-tools.ts to module table
- Add 'Thread Message Path' subsection to Message Flow section
- Add thread_connections to the Database Layer table
- Update testing coverage table with new test files

decisions.md:
- Add ADR-023: thread_connections added to existing friday.db via
  Drizzle migration rather than a separate file. Documents the why
  (shared WAL + Drizzle pattern, DB-level uniqueness constraints) and
  startup recovery behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sethvoltz sethvoltz force-pushed the feature/thread-agent-linking branch from d7ef5ea to c6d03eb Compare May 3, 2026 05:56
@sethvoltz sethvoltz merged commit 315a162 into main May 3, 2026
2 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.

1 participant