# Repowire Session Logs

## 2026-01-02: Major Implementation Session

### What is Repowire?

A **mesh network for Claude Code sessions** - enables multiple AI coding agents running in separate tmux windows to communicate with each other. Think: frontend Claude asks backend Claude about API schemas, infra Claude broadcasts deployment updates, etc.

### Architecture (3 layers)

1. **Local Mesh** - P2P via Unix sockets + tmux pane injection (same machine)
2. **Daemon** - Per-machine message routing
3. **Relay Server** - WebSocket server for cross-machine communication (optional)

### What We Built Today

#### 1. Simplified Setup (`repowire setup`)

Single command that:
- Installs Claude Code hooks (SessionStart, SessionEnd, Stop)
- Adds MCP server via `claude mcp add`

Supports `--dev` flag:
- Production: `uvx repowire mcp`
- Dev: `uv run --directory /path/to/project repowire mcp`

Hooks also use `uvx --from /path` for dev mode.

#### 2. Auto-Registration via Hooks

- **SessionStart hook** → auto-registers peer using tmux `session:window` and `cwd`
- **SessionEnd hook** → auto-unregisters peer
- **Stop hook** → captures Claude's response for `ask_peer` queries

No more manual `repowire peer register` needed!

#### 3. Session:Window Support

Fixed tmux targeting to support multiple Claude windows in one session:
- Old: `tmux_session: "backend"` (assumed separate sessions)
- New: `tmux_session: "0:clusterkit"` (session:window format)

#### 4. Peer Name Detection

MCP server now auto-detects "who am I" from tmux window name:
- `@repowire-code asks: ...` instead of generic `@repowire asks: ...`
- Allows responding peer to `notify_peer` back correctly

#### 5. Cloudflare Tunnel Deployment

Updated `deploy/relay.yaml`:
- Removed nginx ingress + cert-manager
- Added cloudflared sidecar container
- Simpler: no public IP needed, built-in TLS

Domain setup:
- `repowire.io` → GKE Gateway (34.149.49.202) with Google SSL cert
- `relay.repowire.io` → Cloudflare Tunnel (not yet deployed)

### Current State

**Working:**
- `repowire setup --dev` installs everything
- SessionStart hook auto-registers peers ✓
- Peers can see each other via `list_peers` ✓
- Query injection into target pane works ✓

**Not Yet Tested End-to-End:**
- Full `ask_peer` → response capture → return flow
- Stop hook response capture via Unix socket

### Key Files Changed

```
repowire/
├── cli.py                    # Added `setup` command, `hook` subcommands
├── mcp/server.py             # Auto-detect peer name, lazy manager init
├── session/manager.py        # Session:window parsing for tmux targets
├── hooks/
│   ├── installer.py          # Uses uvx commands, dev mode support
│   ├── session_handler.py    # SessionStart/End → register/unregister
│   └── stop_handler.py       # Response capture (unchanged)
deploy/
└── relay.yaml                # Cloudflare Tunnel sidecar
```

### Next Steps

1. **Test full ask_peer flow** - restart both sessions, try communication
2. **Verify Stop hook** - ensure response capture works via Unix socket
3. **Deploy relay** - create Cloudflare tunnel, push container, apply k8s manifests
4. **Multi-machine test** - test relay mode between different machines

### Commands Reference

```bash
# Setup (one-time)
repowire setup --dev

# Check peers
repowire peer list

# Manual operations (if needed)
repowire peer register <name> -t <session:window> -p <path>
repowire peer unregister <name>

# Test query (CLI)
repowire peer ask <peer> "question"

# Check config
repowire config show
```

### Architecture Decisions

- **uvx for hooks** - hooks run outside venv, uvx ensures deps available
- **tmux window names as peer names** - simple, human-readable
- **Lazy socket init** - MCP server starts socket on first tool call
- **Stop hook for responses** - captures Claude's last assistant message, not explicit reply

### Open Questions

- Should peers auto-unregister on SessionEnd? (currently yes)
- How to handle multiple panes in same window?
- Should we support non-tmux environments? (SSH, etc.)

---

## 2026-01-02 (continued): Daemon Architecture & Session ID Fix

### The Bug We Found

When testing `ask_peer`, the query was injected into clusterkit's pane and clusterkit responded - but the response never came back to the waiting caller. After deep investigation:

**Root cause #1: Socket collision**
- Each MCP server instance was creating its own Unix socket at `/tmp/repowire.sock`
- When second MCP server started, it **deleted** the first one's socket
- Stop hook responses went to wrong listener

**Root cause #2: Session ID mismatch**
- Daemon was *guessing* target peer's session ID by looking at most recently modified `.jsonl` file
- But Claude Code creates agent sessions (e.g., `agent-a38890d`) for tool calls
- Stop hook fires with the *actual* session ID, which didn't match our guess
- Pending file not found → response dropped

### The Fix: Daemon Architecture

Implemented central daemon model:

```
Claude A (MCP) ──┐
Claude B (MCP) ──┼──► Daemon (/tmp/repowire.sock)
Stop Hooks     ──┘         │
                           ├── Routes queries to target panes
                           ├── Tracks pending queries by correlation_id
                           └── Matches hook responses to waiting clients
```

**New files:**
- `repowire/daemon/server.py` - Central daemon that owns the socket
- `repowire/daemon/client.py` - Client for MCP servers to talk to daemon
- `repowire/daemon/__init__.py` - Module exports

**Key changes:**
- MCP server is now a thin client that delegates to daemon
- Only ONE socket exists, owned by daemon
- Daemon auto-starts when MCP tools are called

### Session ID as Primary Key

Changed peer registration to use `session_id` (from Claude Code) as the primary identifier instead of trying to guess it:

**Old model (broken):**
```yaml
peers:
  clusterkit:  # keyed by name
    tmux_session: 0:clusterkit
    path: /Users/...
```

**New model:**
```yaml
peers:
  abc123-session-id:  # keyed by session_id
    session_id: abc123-session-id
    name: clusterkit
    tmux_session: 0:clusterkit
    path: /Users/...
```

**Why this works:**
1. SessionStart hook receives `session_id` from Claude Code → registers peer with it
2. When sending query, daemon stores pending file as `{target_session_id}.json`
3. Stop hook receives same `session_id` → finds pending file → exact match!

### Files Changed

```
repowire/
├── config/models.py          # PeerConfig now has session_id, keyed by session_id
├── daemon/
│   ├── __init__.py           # NEW
│   ├── server.py             # NEW - central daemon
│   └── client.py             # NEW - client for MCP/CLI
├── mcp/server.py             # Now thin client to daemon
├── hooks/
│   ├── session_handler.py    # Registers with session_id
│   └── stop_handler.py       # Sends to daemon (unchanged logic)
└── cli.py                    # daemon start/stop/status commands
```

### New CLI Commands

```bash
repowire daemon start      # Start in background
repowire daemon start -f   # Start in foreground
repowire daemon stop       # Stop daemon
repowire daemon status     # Check if running
```

### Current State

**Done:**
- Daemon architecture implemented
- Session ID based registration
- Config model updated
- All code compiles

**Blocked on:**
- Need to delete old config (`rm ~/.repowire/config.yaml`)
- Need to start **new** Claude sessions (not `--continue`) so SessionStart hooks fire
- Then test full `ask_peer` flow

### To Test

1. `rm ~/.repowire/config.yaml`
2. `repowire daemon start`
3. Start new Claude session in clusterkit window
4. Start new Claude session in repowire window
5. Check `cat ~/.repowire/config.yaml` - should show session_id format
6. Test `ask_peer` from one to the other

---

## 2026-01-02 (continued): Peer Name & Stale Cleanup

### Fixes Applied

1. **Peer name now uses folder name** (`Path(cwd).name`) instead of tmux window name
   - More reliable - doesn't depend on timing
   - Works with worktrees (each has its own folder)
   - File: `repowire/hooks/session_handler.py`

2. **Tmux target lookup uses `-t $TMUX_PANE`**
   - Old: `tmux display-message -p "#{session_name}:#{window_name}"` (gets active window)
   - New: `tmux display-message -t $TMUX_PANE -p "#{session_name}:#{window_name}"` (gets correct window)
   - Fixes race condition when user switches windows before hook finishes

### Problem: Stale Peers Accumulating

- `SessionEnd` hook doesn't always fire (user kills terminal, crashes, etc.)
- Old peer entries stay in config indefinitely
- Causes duplicate/stale entries in `list_peers`

### Proposed Fix: Cleanup on Registration

When daemon receives a registration or SessionStart adds a peer:
1. Iterate all peers in config
2. For each peer with a `tmux_session`, check if that tmux session:window still exists
3. If not, remove the stale peer entry

### Implementation Pointers

- `daemon/server.py:316` - `_get_peers()` already checks status
- `daemon/server.py:342` - `_get_peer_status()` returns OFFLINE if tmux gone
- `config/models.py:88` - `remove_peer_by_session()` for cleanup

Could add cleanup in:
- `repowire/daemon/server.py` - in `_handle_message` for `register` type, or
- `repowire/hooks/session_handler.py` - in `main()` before `add_peer()`

---

## 2026-01-02 (continued): Relay Server Deployed! 🎉

### Relay Server Live

Successfully deployed relay server to GKE via Cloudflare Tunnel:

**URL:** `https://relay.repowire.io`
- Health check: `curl https://relay.repowire.io/health` → `{"status":"ok"}`
- DNS: 172.67.213.180, 104.21.93.182 (Cloudflare edge)

### Deployment Details

**Container:**
- Image: `us-docker.pkg.dev/baldmaninc/gcr.io/repowire-relay:latest`
- Platform: `linux/amd64` (important - Mac builds arm64 by default)
- Build: `docker buildx build --platform linux/amd64 -f deploy/Dockerfile --push`

**Cloudflare Tunnel:**
- Tunnel name: `repowire-relay`
- Tunnel ID: `0db2f9a7-fef5-4a1f-8149-ec6b0f538462`
- Auto-created CNAME: `relay.repowire.io` → tunnel

**Kubernetes:**
- Namespace: `repowire`
- Deployment: 2 replicas, each with relay + cloudflared sidecar
- Secret: `cloudflare-tunnel` (token from `cloudflared tunnel token repowire-relay`)

### Issues Encountered & Fixed

1. **`exec format error`** - Built ARM image on Mac, GKE needs AMD64
   - Fix: `docker buildx build --platform linux/amd64`

2. **Invalid tunnel token** - Secret had placeholder value
   - Fix: `cloudflared tunnel token repowire-relay` → update K8s secret

3. **README.md missing in Docker build** - hatchling requires it
   - Fix: Added `COPY README.md` to Dockerfile

### Collaboration with Clusterkit

This was done collaboratively between two Claude sessions:
- **repowire** (this session) - built container, updated manifests, deployed
- **clusterkit** - created Cloudflare tunnel, verified infra, created K8s secret

Used `ask_peer` to coordinate - first successful multi-agent deployment!

### Updated relay.yaml

```yaml
# Key changes:
- namespace: repowire (all resources)
- image: us-docker.pkg.dev/baldmaninc/gcr.io/repowire-relay:latest
- cloudflared sidecar with TUNNEL_TOKEN from secret
```

### Current State

**Working:**
- Local mesh: `ask_peer` between sessions ✓
- Relay server: deployed and healthy ✓
- DNS: propagated globally ✓

**Not yet tested:**
- Cross-machine communication via relay
- Relay authentication (API keys)

### Next Steps

1. **Test relay mode** - enable in config, test cross-machine
2. **API key generation** - `repowire auth create-key`
3. **Stale peer cleanup** - implement on registration
