Commit 100b23
2026-03-20 05:02:42 Claude (MCP): [mcp] docs: add Directed Message Routing feature spec| /dev/null .. Minsky/Directed_Message_Routing.md | |
| @@ 0,0 1,109 @@ | |
| + | --- |
| + | category: feature |
| + | tags: [agents, irc, supervisor] |
| + | last_updated: 2026-03-20 |
| + | confidence: high |
| + | --- |
| + | |
| + | # Directed Message Routing |
| + | |
| + | Event-driven delivery of IRC messages to specific agents, replacing poll-only communication. |
| + | |
| + | ## Problem |
| + | |
| + | Agents only see IRC messages when the supervisor's 30-second heartbeat prompts them to `read_messages`. If the PM or EM posts a message mentioning an agent by nick, it sits unread until the next poll. This creates up to 30 seconds of latency on directed communication and makes the system feel unresponsive. |
| + | |
| + | The `IrcWatcher` already has an event-driven IRC connection that fires instantly on `PRIVMSG`. It currently only handles `TASK:` prefixes. It should also route messages that mention a specific agent. |
| + | |
| + | ## Design |
| + | |
| + | Extend `IrcWatcher` to detect nick mentions and notify the supervisor, which immediately sends the message content to the targeted agent. |
| + | |
| + | ### Detection |
| + | |
| + | A message is "directed" if it contains `@AgentName` where `AgentName` is a currently active agent nick. The watcher already has access to a callback — add a second callback for directed messages alongside the existing `on_task`. |
| + | |
| + | ```python |
| + | class IrcWatcher: |
| + | def __init__( |
| + | self, |
| + | server: str, |
| + | port: int, |
| + | nick: str, |
| + | channels: list[str], |
| + | on_task: Callable[[str, str, str], None], |
| + | on_mention: Callable[[str, str, str, str], None], |
| + | # on_mention(channel, sender, mentioned_nick, full_message) |
| + | ) -> None: |
| + | ``` |
| + | |
| + | ### Routing |
| + | |
| + | In `_handle_message`, after the existing `TASK:` check: |
| + | |
| + | 1. Scan the message for `@{nick}` where nick is in the set of active agent names |
| + | 2. If found, call `on_mention(channel, sender_nick, mentioned_nick, message)` |
| + | 3. A message can match both `TASK:` and `@mention` — that's fine, they serve different purposes |
| + | |
| + | The supervisor registers `on_mention` and calls `agent.send()` with the message content, bypassing the poll loop: |
| + | |
| + | ```python |
| + | def _on_mention(self, channel: str, sender: str, target_nick: str, message: str) -> None: |
| + | agent = self.agents.get(target_nick) |
| + | if agent: |
| + | asyncio.get_running_loop().create_task( |
| + | agent.session.send( |
| + | f"[IRC {channel}] <{sender}> {message}\n\n" |
| + | "Respond in the appropriate channel." |
| + | ) |
| + | ) |
| + | ``` |
| + | |
| + | ### Active nick registry |
| + | |
| + | The `IrcWatcher` needs to know which nicks are active to avoid matching mentions of non-agent names. The supervisor already tracks this in `self.agents` (keyed by name). Add a method to update the watcher's nick set: |
| + | |
| + | ```python |
| + | # On IrcWatcher |
| + | def update_active_nicks(self, nicks: set[str]) -> None: |
| + | self._active_nicks = nicks |
| + | |
| + | # Supervisor calls this after spawn/kill/shift-change |
| + | self._watcher.update_active_nicks(set(self.agents.keys())) |
| + | ``` |
| + | |
| + | ### Deduplication |
| + | |
| + | Without guard, an agent could receive the same message twice: once via directed routing, once via the next heartbeat's `read_messages`. Two options: |
| + | |
| + | 1. **Track delivered message timestamps per agent.** The supervisor records the timestamp of the last directed delivery. The heartbeat prompt changes to "Check your IRC channels for messages since {timestamp}." |
| + | 2. **Accept duplicates.** Agents are LLMs — seeing a message twice is redundant but not harmful. The simpler option for MVP. |
| + | |
| + | **Recommendation:** Accept duplicates for MVP. Add dedup later if it becomes noisy. |
| + | |
| + | ## Channels to watch |
| + | |
| + | The watcher currently joins only `#standup-{slug}`. For mention routing to work across channels, it should also join `#project-{slug}` and any `#work-{task_id}` channels that get created. The supervisor should call `watcher.join_channel()` when creating work channels for new tasks. |
| + | |
| + | ## Scope |
| + | |
| + | ### In scope |
| + | - Extend `IrcWatcher._handle_message` with nick-mention detection |
| + | - Add `on_mention` callback to `IrcWatcher.__init__` |
| + | - Add `update_active_nicks()` method |
| + | - Wire `Supervisor._on_mention` to call `agent.send()` |
| + | - Update active nicks on spawn/kill/shift-change |
| + | - Join `#project-{slug}` in addition to `#standup-{slug}` on startup |
| + | - Tests for mention detection regex |
| + | |
| + | ### Out of scope |
| + | - DM (private message) routing — agents only operate in channels for now |
| + | - Message deduplication — accept duplicates for MVP |
| + | - Priority/interruption — directed messages queue behind any in-progress `send()` |
| + | - Rate limiting — a burst of mentions could queue many `send()` calls |
| + | |
| + | ## Files to change |
| + | |
| + | - `supervisor/src/minsky_supervisor/irc_watcher.py` — add mention detection, `on_mention` callback, `update_active_nicks()` |
| + | - `supervisor/src/minsky_supervisor/supervisor.py` — wire `_on_mention`, call `update_active_nicks()` after agent lifecycle changes, join `#project-{slug}` |
| + | - `supervisor/tests/test_irc_watcher.py` — add mention detection tests |
