# Message Hub V1 Design

> Every session restart loses wakeable status. The bash script dependency is fragile (PATH issues, subprocess overhead, no library reuse).

- Skill: `tools-only/message-hub-v1-design` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/message-hub-v1-design`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/message-hub-v1-design/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-29
- Page: https://skillmd.com/skills/tools-only/message-hub-v1-design

---

# Message Hub V1 Design

**Task**: VM-714 (expanded scope)
**Related**: VMD-102 (Wakeable Agents epic)
**Date**: 2026-02-16
**Status**: Implementation-ready

## Problem

Today, making an agent wakeable via VoiceMode Connect requires:

1. Agent creates a Claude Code team (TeamCreate)
2. Agent creates a symlink for a stable team name
3. Agent calls `register_wakeable` MCP tool with team name
4. Message delivery depends on `send-message` bash script being on PATH

Every session restart loses wakeable status. The bash script dependency is fragile (PATH issues, subprocess overhead, no library reuse).

## Design Decisions (from brainstorm session)

These were agreed in a ~45 minute voice session on 2026-02-16:

1. **Agent identity is VoiceMode-owned**, not tied to Claude Code teams. "cora" is a stable identity across sessions; the Claude team name is an ephemeral delivery detail.
2. **Wakeable is a live capability**, not a static attribute. VoiceMode announces it only after an agent registers.
3. **V1: local persistence only**. No server-side message storage. If agent is offline, voicemode.dev returns "agent offline".
4. **Python library replaces bash dependency**. `send-message` CLI becomes a thin wrapper. VoiceMode calls the library directly.
5. **Two layers**: stable identity (VoiceMode config) + ephemeral delivery target (per-session Claude team).

## Components

### 1. Configuration: `VOICEMODE_AGENT_NAME` env var

**File**: `voice_mode/config.py`

New config variables:

```python
# Agent identity for wakeable registration
AGENT_NAME = os.getenv("VOICEMODE_AGENT_NAME", "")
AGENT_PLATFORM = os.getenv("VOICEMODE_AGENT_PLATFORM", "claude-code")

# Auto-register as wakeable on startup (requires AGENT_NAME to be set)
AUTO_WAKEABLE = env_bool("VOICEMODE_AUTO_WAKEABLE", False)
```

Add to the default `voicemode.env` template:

```env
#############
# Agent Identity
#############

# Agent display name for VoiceMode Connect dashboard
# When set with AUTO_WAKEABLE, agent registers automatically on startup
# VOICEMODE_AGENT_NAME=Cora 7

# Agent platform identifier (default: claude-code)
# VOICEMODE_AGENT_PLATFORM=claude-code

# Auto-register as wakeable on MCP startup (true/false, default: false)
# Requires VOICEMODE_AGENT_NAME to be set
# VOICEMODE_AUTO_WAKEABLE=false
```

### 2. Python message delivery library

**New file**: `voice_mode/messaging.py`

Pure Python implementation of the `send-message` inbox injection protocol. No subprocess calls, no PATH dependency.

```python
"""Message delivery library for Claude Code agent inboxes.

Implements the same inbox file protocol as the send-message bash script,
but as a Python library callable from within VoiceMode directly.

Protocol: Write JSON messages to ~/.claude/teams/{team}/inboxes/{recipient}.json
Claude Code watches these files via FSEvents and wakes the agent automatically.
"""

def deliver_message(
    team_name: str,
    content: str,
    recipient: str = "team-lead",
    sender: str = "voicemode",
    summary: str | None = None,
) -> bool:
    """Deliver a message to a Claude Code agent's team inbox.

    Args:
        team_name: Team name (directory under ~/.claude/teams/)
        content: Message text
        recipient: Target agent name (default: team-lead)
        sender: Sender display name
        summary: Brief summary (default: first 50 chars of content)

    Returns:
        True if message was written successfully, False otherwise.
    """
```

Key implementation details:
- Reads existing inbox JSON, appends new message, writes atomically (write to temp, rename)
- Creates inbox directory if needed
- Follows same JSON schema as `send-message`: `{from, text, summary, timestamp, read}`
- Synchronous function (filesystem I/O only, no async needed)
- No external dependencies

### 3. Auto-register on startup

**File**: `voice_mode/connect_registry.py`

After the WebSocket connection is established and the `ready` message is sent, check if auto-wakeable is configured:

```python
# In _connection_loop(), after sending ready message:
if self._wakeable_team_name:
    # Re-registration (existing behavior)
    await self._send_wakeable_registration(ws)
elif AUTO_WAKEABLE and AGENT_NAME:
    # Auto-registration on first connect
    # Note: team_name comes from the agent at runtime, not config
    # Auto-wakeable only pre-configures the agent name
    logger.info(f"Auto-wakeable configured but no team registered yet "
                f"(agent: {AGENT_NAME})")
```

The flow is:
1. VoiceMode MCP starts, ConnectRegistry connects to voicemode.dev
2. VoiceMode announces TTS/STT capabilities (existing behavior)
3. Agent starts, creates team, calls `register_wakeable` with team name
4. VoiceMode announces wakeable capability with agent name

**Why not fully automatic?** The team name is ephemeral and created by the agent at runtime. VoiceMode can't know it ahead of time. What auto-wakeable does is:
- Pre-configure the agent name so `register_wakeable` doesn't need to provide it
- In future: if VoiceMode owns message storage (V2), it can auto-register without a team name at all

For V1, `register_wakeable` is still called by the agent, but it's simpler because the name defaults from config:

```python
@mcp.tool()
async def register_wakeable(
    team_name: str,
    agent_name: str = "",  # Empty = use VOICEMODE_AGENT_NAME config
    agent_platform: str = "",  # Empty = use VOICEMODE_AGENT_PLATFORM config
) -> str:
```

### 4. Wire `_handle_agent_message` to use library

**File**: `voice_mode/connect_registry.py`

Replace the subprocess call to `send-message` with a direct call to the messaging library:

```python
async def _handle_agent_message(self, text: str, sender: str):
    """Handle an incoming agent_message by delivering to team inbox."""
    from .messaging import deliver_message

    team_name = self._wakeable_team_name
    if not team_name:
        logger.warning("Received agent_message but not registered as wakeable")
        return

    if not text.strip():
        logger.warning("Received empty agent_message, ignoring")
        return

    success = await asyncio.to_thread(
        deliver_message,
        team_name=team_name,
        content=text,
        sender=sender,
    )

    if success:
        logger.info(f"Message delivered to team '{team_name}' from '{sender}'")
    else:
        logger.error(f"Failed to deliver message to team '{team_name}'")
```

This eliminates the `shutil.which("send-message")` dependency and the `subprocess.run` overhead.

## Files Changed

| File | Change |
|------|--------|
| `voice_mode/config.py` | Add `AGENT_NAME`, `AGENT_PLATFORM`, `AUTO_WAKEABLE` config vars |
| `voice_mode/messaging.py` | **New** - Python message delivery library |
| `voice_mode/connect_registry.py` | Wire `_handle_agent_message` to use `messaging.deliver_message` |
| `voice_mode/tools/connect.py` | Default `agent_name`/`agent_platform` from config |
| `tests/test_messaging.py` | **New** - Unit tests for messaging library |
| `tests/test_connect_registry.py` | Update tests for new message delivery path |

## Implementation Order

1. **`voice_mode/messaging.py`** + tests - standalone, no dependencies on other changes
2. **`voice_mode/config.py`** - add new config vars
3. **`voice_mode/connect_registry.py`** - replace subprocess with messaging library
4. **`voice_mode/tools/connect.py`** - default params from config

Steps 1-2 can be done in parallel. Steps 3-4 depend on both.

## Testing Strategy

- `test_messaging.py`: Test deliver_message with temp directories (create inbox, append to existing, handle missing team, atomic write)
- `test_connect_registry.py`: Test `_handle_agent_message` calls `deliver_message` instead of subprocess
- `test_config.py`: Verify new env vars are parsed correctly
- Manual: Set `VOICEMODE_AGENT_NAME=Test` in voicemode.env, start MCP, verify register_wakeable uses the name

## Out of Scope (V2+)

- Server-side message storage on voicemode.dev
- E2E encryption for stored messages
- VoiceMode-owned message directory (`~/.voicemode/messages/`)
- Pluggable delivery backends (WhatsApp, etc.)
- DNS-style agent addressing (`cora.mike.connect.voicemode.dev`)
- Message expiry / read receipts / QoS levels
- Auto-registration without agent needing to provide team name

