From 30a6df4bfef2dc42e1712c5e6ebf02cb7da3bf56 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 17:59:54 +0000 Subject: [PATCH 1/7] Add v2 design docs for agent-relay improvements - DESIGN_V2.md: Comprehensive improvement plan covering storage, protocol simplification, reliability, and DX enhancements - TMUX_IMPROVEMENTS.md: Focused analysis of tmux implementation with specific takeaways for better activity tracking, reconnection, and session discovery --- docs/DESIGN_V2.md | 424 ++++++++++++++++++++++++++++++++++++++ docs/TMUX_IMPROVEMENTS.md | 411 ++++++++++++++++++++++++++++++++++++ 2 files changed, 835 insertions(+) create mode 100644 docs/DESIGN_V2.md create mode 100644 docs/TMUX_IMPROVEMENTS.md diff --git a/docs/DESIGN_V2.md b/docs/DESIGN_V2.md new file mode 100644 index 000000000..ae05ce636 --- /dev/null +++ b/docs/DESIGN_V2.md @@ -0,0 +1,424 @@ +# Agent-Relay v2 Design Document + +## Overview + +This document outlines improvements to agent-relay while preserving its core philosophy: **simple, transparent agent-to-agent communication via terminal output patterns**. + +The `@relay:` pattern is the killer feature. Agents communicate naturally by just printing text. No APIs, no SDKs, no special integrations. This must remain the foundation. + +--- + +## Current Pain Points + +### 1. Ephemeral Storage (`/tmp`) +- Data lives in `/tmp/agent-relay//` +- Cleared on reboot (macOS/Linux) +- Message history lost unexpectedly + +### 2. Dead Code +- ACK/NACK protocol defined but not implemented +- Session resume tokens always return `RESUME_TOO_OLD` +- PostgreSQL adapter throws "not implemented" + +### 3. Memory Leaks +- `sentMessageHashes` Set grows unbounded +- Long-running sessions will OOM + +### 4. Polling Overhead +- `capture-pane` every 200ms consumes CPU +- Latency up to 200ms for message detection + +### 5. Fragile Injection Timing +- 1.5s idle detection is a heuristic +- Race conditions if agent outputs during injection + +--- + +## Design Principles + +1. **Keep it simple** - Every feature must justify its complexity +2. **Terminal-native** - Users stay in tmux, not a browser +3. **Pattern-based** - `@relay:` is the API +4. **Zero config** - Works out of the box +5. **Debuggable** - Easy to understand what's happening + +--- + +## Proposed Changes + +### Phase 1: Foundation Fixes + +#### 1.1 Persistent Storage Location + +Move from `/tmp` to XDG-compliant location: + +``` +~/.local/share/agent-relay/ # XDG_DATA_HOME fallback +├── projects/ +│ └── / +│ ├── relay.sock # Unix socket +│ ├── messages.db # SQLite +│ └── agents.json # Connected agents +└── config.json # Global settings (optional) +``` + +**Migration path:** +- Check for existing `/tmp/agent-relay/` data on startup +- Offer one-time migration prompt +- Fall back to new location for fresh installs + +#### 1.2 Remove Dead Code + +Delete these unimplemented features: + +| Feature | Location | Action | +|---------|----------|--------| +| ACK handling | `connection.ts:114-116` | Remove | +| Resume tokens | `connection.ts:140-143` | Remove | +| PostgreSQL adapter | `storage/adapter.ts:152-162` | Remove | +| Topic subscriptions | `router.ts` | Keep but mark experimental | + +**Protocol simplification:** +```typescript +// Before: 10 message types +type MessageType = 'HELLO' | 'WELCOME' | 'SEND' | 'DELIVER' | 'ACK' | + 'PING' | 'PONG' | 'SUBSCRIBE' | 'UNSUBSCRIBE' | 'ERROR' | 'BYE'; + +// After: 6 message types +type MessageType = 'HELLO' | 'WELCOME' | 'SEND' | 'DELIVER' | + 'PING' | 'PONG' | 'ERROR'; +``` + +#### 1.3 Fix Memory Leak + +Replace unbounded Set with LRU cache: + +```typescript +// Before +private sentMessageHashes: Set = new Set(); + +// After +import { LRUCache } from 'lru-cache'; + +private sentMessageHashes = new LRUCache({ + max: 10000, // Max 10k unique messages tracked + ttl: 1000 * 60 * 60, // Expire after 1 hour +}); +``` + +#### 1.4 Simplify Binary Protocol + +Replace 4-byte length prefix with newline-delimited JSON: + +```typescript +// Before: Binary framing +[4-byte length][JSON payload] + +// After: NDJSON (newline-delimited JSON) +{"v":1,"type":"SEND","to":"Bob","payload":{"body":"Hello"}}\n +{"v":1,"type":"DELIVER","from":"Alice","payload":{"body":"Hello"}}\n +``` + +**Benefits:** +- Human-readable when debugging (`nc -U relay.sock`) +- Simpler parser (~20 lines vs ~50 lines) +- Standard format (NDJSON) + +**Trade-off:** Messages cannot contain literal newlines in body. Since we already sanitize newlines for injection (`replace(/[\r\n]+/g, ' ')`), this is acceptable. + +--- + +### Phase 2: Reliability Improvements + +#### 2.1 Improved Injection Strategy + +Replace time-based idle detection with input buffer detection: + +```typescript +// Current: Wait 1.5s after last output (fragile) +if (Date.now() - lastOutputTime > 1500) { + inject(); +} + +// Proposed: Check if input line is empty +async function isInputClear(): Promise { + // Capture current pane content + const { stdout } = await execAsync( + `tmux capture-pane -t ${session} -p -J` + ); + const lines = stdout.split('\n'); + const lastLine = lines[lines.length - 1] || ''; + + // Check if last line is just a prompt (no partial input) + return /^[>$%#➜]\s*$/.test(lastLine); +} +``` + +#### 2.2 Bracketed Paste Mode + +Use bracketed paste for safer injection: + +```typescript +// Wrap injection in bracketed paste markers +const PASTE_START = '\x1b[200~'; +const PASTE_END = '\x1b[201~'; + +async function injectSafe(text: string): Promise { + await sendKeysLiteral(PASTE_START + text + PASTE_END); + await sendKeys('Enter'); +} +``` + +**Benefits:** +- Prevents shell interpretation of special characters +- Atomic paste (no interleaving) +- Supported by most modern terminals/shells + +#### 2.3 Message Queue for Offline Agents + +Queue messages when target agent is disconnected: + +```typescript +interface QueuedMessage { + id: string; + from: string; + to: string; + payload: SendPayload; + queuedAt: number; + attempts: number; +} + +// In router.ts +if (!targetConnection || targetConnection.state !== 'ACTIVE') { + this.messageQueue.enqueue({ + id: envelope.id, + from: connection.agentName, + to: envelope.to, + payload: envelope.payload, + queuedAt: Date.now(), + attempts: 0, + }); + + // Notify sender + connection.send({ + type: 'QUEUED', + id: envelope.id, + reason: 'recipient_offline', + }); +} + +// On agent connect, flush queued messages +onAgentConnect(agentName: string) { + const queued = this.messageQueue.getForRecipient(agentName); + for (const msg of queued) { + this.deliverMessage(msg); + this.messageQueue.remove(msg.id); + } +} +``` + +--- + +### Phase 3: Developer Experience + +#### 3.1 Structured Logging + +Replace scattered `console.log` with leveled logging: + +```typescript +import { createLogger } from './logger.js'; + +const log = createLogger('daemon'); + +log.info('Agent registered', { name: 'Alice', cli: 'claude' }); +log.debug('Message routed', { from: 'Alice', to: 'Bob', id: '...' }); +log.error('Connection failed', { error: err.message }); +``` + +Output format (when `DEBUG=agent-relay`): +``` +[14:23:01.234] INFO daemon: Agent registered name=Alice cli=claude +[14:23:01.456] DEBUG router: Message routed from=Alice to=Bob id=abc123 +``` + +#### 3.2 Health Check Endpoint + +Add simple HTTP health check (optional, disabled by default): + +```typescript +// Enable with: agent-relay up --health-port 3889 +// Or: AGENT_RELAY_HEALTH_PORT=3889 + +GET http://localhost:3889/health +{ + "status": "ok", + "uptime": 3600, + "agents": ["Alice", "Bob"], + "messages": { + "sent": 42, + "delivered": 41, + "queued": 1 + } +} +``` + +#### 3.3 CLI Improvements + +```bash +# Current +agent-relay up +agent-relay -n Alice claude +agent-relay status +agent-relay read + +# Add +agent-relay agents # List connected agents +agent-relay send Alice "Hello" # Send from CLI (for testing) +agent-relay logs # Tail daemon logs +agent-relay logs Alice # Tail agent's relay activity +``` + +--- + +### Phase 4: Optional Enhancements + +#### 4.1 WebSocket Streaming (Optional) + +Replace polling with WebSocket-based output streaming: + +```typescript +// Instead of polling capture-pane, attach via PTY +import { spawn } from 'node-pty'; + +const pty = spawn('tmux', ['attach-session', '-t', session, '-r'], { + // Read-only attach +}); + +pty.onData((data) => { + // Real-time output, no polling + const { commands } = parser.parse(data); + for (const cmd of commands) { + sendRelayCommand(cmd); + } +}); +``` + +**Trade-offs:** +| Aspect | Polling | WebSocket/PTY | +|--------|---------|---------------| +| Latency | 0-200ms | ~1-10ms | +| CPU | Higher | Lower | +| Complexity | Simple | More complex | +| Dependencies | None | node-pty | + +**Recommendation:** Keep polling as default, offer streaming as `--experimental-streaming` flag. + +#### 4.2 Message Encryption (Optional) + +For sensitive inter-agent communication: + +```typescript +// Generate per-project key on first run +const projectKey = await generateKey(); +fs.writeFileSync(keyPath, projectKey, { mode: 0o600 }); + +// Encrypt message bodies +const encrypted = await encrypt(payload.body, projectKey); +``` + +**Scope:** Only encrypt message body, not metadata (to/from/timestamp). + +--- + +## Migration Plan + +### v1.x → v2.0 + +1. **Storage migration** + - Detect existing `/tmp/agent-relay/` data + - Copy to `~/.local/share/agent-relay/` + - Remove old location after successful migration + +2. **Protocol compatibility** + - v2 daemon accepts both binary and NDJSON + - v2 clients send NDJSON only + - Deprecation warning for binary clients + +3. **Breaking changes** + - Remove ACK/resume/PostgreSQL (unused) + - Change default storage location + +--- + +## File Structure (Post-Refactor) + +``` +src/ +├── cli/ +│ └── index.ts # CLI entry point +├── daemon/ +│ ├── server.ts # Main daemon +│ ├── connection.ts # Connection handling (simplified) +│ └── router.ts # Message routing + queue +├── wrapper/ +│ ├── tmux-wrapper.ts # Agent wrapper +│ ├── parser.ts # @relay: pattern parser +│ └── client.ts # Relay client +├── protocol/ +│ └── types.ts # Message types (reduced) +├── storage/ +│ └── sqlite-adapter.ts # SQLite only (removed abstraction) +└── utils/ + ├── logger.ts # Structured logging + ├── paths.ts # XDG-compliant paths + └── lru-cache.ts # For deduplication +``` + +--- + +## Success Metrics + +| Metric | Current | Target | +|--------|---------|--------| +| Lines of code | ~2500 | ~2000 | +| Message types | 10 | 6 | +| Dependencies | 12 | 10 | +| Memory (1hr session) | Unbounded | <50MB | +| Message detection latency | 0-200ms | 0-200ms (or <10ms with streaming) | +| Data persistence | Lost on reboot | Permanent | + +--- + +## Non-Goals + +- **Dashboard/Web UI**: Out of scope. Use `agent-relay logs` for visibility. +- **Multi-host support**: Single machine focus. Use SSH for remote. +- **Agent memory/RAG**: Separate concern. Agents manage their own context. +- **Authentication**: Unix socket permissions are sufficient for local use. + +--- + +## Timeline + +| Phase | Scope | Effort | +|-------|-------|--------| +| Phase 1 | Foundation fixes | 2-3 days | +| Phase 2 | Reliability | 2-3 days | +| Phase 3 | DX improvements | 1-2 days | +| Phase 4 | Optional enhancements | As needed | + +--- + +## Open Questions + +1. **NDJSON vs Binary**: Is the simplicity worth losing multi-line message support? + - Mitigation: Encode newlines as `\n` in JSON strings (already done) + +2. **Polling interval**: Should 200ms be configurable? + - Proposal: Add `--poll-interval` flag, default 200ms + +3. **Message TTL**: How long to queue messages for offline agents? + - Proposal: 24 hours default, configurable + +4. **Backward compatibility**: How long to support v1 binary protocol? + - Proposal: One minor version (v2.1 removes it) diff --git a/docs/TMUX_IMPROVEMENTS.md b/docs/TMUX_IMPROVEMENTS.md new file mode 100644 index 000000000..e93c342f9 --- /dev/null +++ b/docs/TMUX_IMPROVEMENTS.md @@ -0,0 +1,411 @@ +# tmux Implementation: Analysis & Improvements + +## Current Implementation Summary + +Our tmux wrapper uses an **attach-based polling architecture**: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ User Terminal │ +│ └─ tmux attach-session (stdio: 'inherit') │ +│ └─ User sees real tmux session │ +│ │ +│ Background (every 200ms) │ +│ └─ tmux capture-pane -p -J -S - │ +│ └─ Parse for @relay: patterns │ +│ └─ Send detected commands to daemon │ +│ │ +│ Message Injection │ +│ └─ Wait for 1.5s idle │ +│ └─ tmux send-keys (Escape, C-u, message, Enter) │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Alternative Approach: WebSocket + node-pty + +A different approach uses **real-time PTY streaming** instead of polling: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Browser/Client │ +│ └─ xterm.js terminal │ +│ └─ WebSocket connection │ +│ │ +│ Server │ +│ └─ node-pty spawns: tmux attach -t session │ +│ └─ pty.onData → ws.send (real-time streaming) │ +│ └─ ws.onMessage → pty.write (real-time input) │ +│ │ +│ No polling needed - events are instant │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Key Differences + +| Aspect | Our Approach (Polling) | Alternative (Streaming) | +|--------|------------------------|-------------------------| +| **Terminal location** | User's actual terminal | Browser (xterm.js) | +| **Data flow** | Periodic capture-pane | Real-time PTY events | +| **Latency** | 0-200ms | ~1-10ms | +| **CPU usage** | Constant (polling) | Event-driven (lower) | +| **Complexity** | Simple shell commands | node-pty + WebSocket | +| **Dependencies** | None (just tmux) | node-pty, ws, xterm.js | +| **User experience** | Native terminal feel | Browser-based | + +--- + +## What We Do Better + +### 1. Native Terminal Experience + +Users stay in their actual terminal. No browser, no xterm.js emulation quirks. + +```bash +# Our approach - user is IN the tmux +agent-relay -n Alice claude +# User types directly, sees real output + +# Alternative - user is in browser +# Terminal is rendered in xterm.js WebGL +# Subtle differences in keybindings, scrolling, copy/paste +``` + +**Keep this.** The native feel is valuable. + +### 2. Simpler Dependencies + +We only need tmux and Node.js. No native compilation (node-pty), no browser components. + +```json +// Our package.json - no native deps +{ + "dependencies": { + "commander": "^12.0.0", + "better-sqlite3": "^9.0.0" + // That's it for core functionality + } +} + +// Alternative needs +{ + "dependencies": { + "node-pty": "^1.0.0", // Native compilation required + "xterm": "^5.0.0", // Browser terminal + "xterm-addon-fit": "...", + "xterm-addon-webgl": "...", + "ws": "^8.0.0" + } +} +``` + +**Keep this.** Simpler install, fewer build issues. + +### 3. Pattern-Based Communication + +Agents just output `@relay:Name message`. No API calls, no special handling. + +``` +# Our approach - agent outputs text naturally +Claude: I'll ask Bob for help. +@relay:Bob Can you review the auth module? + +# Alternative - agent calls external script +Claude: I'll ask Bob for help. +!send-message Bob "Can you review the auth module?" +``` + +**Keep this.** It's our killer feature. + +--- + +## What We Can Improve + +### 1. Activity Tracking + +The alternative tracks session activity state (active/idle/disconnected) with timestamps: + +```typescript +// Their approach +const sessionActivity: Map = new Map(); + +// On any output +sessionActivity.set(sessionName, Date.now()); + +// Idle detection +const IDLE_THRESHOLD = 30_000; // 30 seconds +function getSessionStatus(name: string): 'active' | 'idle' | 'disconnected' { + const lastActivity = sessionActivity.get(name); + if (!lastActivity) return 'disconnected'; + return Date.now() - lastActivity > IDLE_THRESHOLD ? 'idle' : 'active'; +} +``` + +**Improvement:** Add activity tracking for better injection timing: + +```typescript +// In tmux-wrapper.ts +private lastActivityTime = Date.now(); +private activityState: 'active' | 'idle' = 'active'; + +private updateActivityState(): void { + const now = Date.now(); + const wasActive = this.activityState === 'active'; + + if (now - this.lastActivityTime > 30_000) { + this.activityState = 'idle'; + if (wasActive) { + this.logStderr('Session went idle'); + // Good time to check for messages + this.checkForInjectionOpportunity(); + } + } +} +``` + +### 2. Graceful Reconnection + +The alternative implements exponential backoff for WebSocket reconnection: + +```typescript +// Their approach +const RECONNECT_DELAYS = [100, 500, 1000, 2000, 5000]; +let reconnectAttempt = 0; + +function reconnect() { + if (reconnectAttempt >= RECONNECT_DELAYS.length) { + console.error('Max reconnection attempts reached'); + return; + } + + setTimeout(() => { + connect(); + reconnectAttempt++; + }, RECONNECT_DELAYS[reconnectAttempt]); +} +``` + +**Improvement:** Add to our RelayClient: + +```typescript +// In client.ts +private reconnectAttempts = 0; +private readonly MAX_RECONNECT_ATTEMPTS = 5; +private readonly RECONNECT_DELAYS = [100, 500, 1000, 2000, 5000]; + +private scheduleReconnect(): void { + if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) { + this.logStderr('Relay connection failed, operating offline'); + return; + } + + const delay = this.RECONNECT_DELAYS[this.reconnectAttempts]; + this.reconnectAttempts++; + + setTimeout(() => { + this.connect().catch(() => this.scheduleReconnect()); + }, delay); +} +``` + +### 3. Agent Registry Persistence + +The alternative stores agent metadata in a persistent registry: + +```typescript +// Their approach - ~/.aimaestro/agents/registry.json +{ + "agents": { + "agent-abc123": { + "id": "agent-abc123", + "name": "Alice", + "aliases": ["alice", "dev-alice"], + "workingDirectory": "/home/user/project", + "cli": "claude", + "createdAt": "2025-12-20T10:00:00Z", + "lastSeen": "2025-12-20T14:30:00Z" + } + } +} +``` + +**Improvement:** Add agent registry: + +```typescript +// New file: src/daemon/registry.ts +interface AgentRecord { + id: string; + name: string; + cli: string; + workingDirectory: string; + firstSeen: string; + lastSeen: string; + messagesSent: number; + messagesReceived: number; +} + +class AgentRegistry { + private registryPath: string; + private agents: Map = new Map(); + + constructor(dataDir: string) { + this.registryPath = path.join(dataDir, 'agents.json'); + this.load(); + } + + register(name: string, cli: string, cwd: string): AgentRecord { + const existing = this.agents.get(name); + if (existing) { + existing.lastSeen = new Date().toISOString(); + this.save(); + return existing; + } + + const record: AgentRecord = { + id: `agent-${randomId()}`, + name, + cli, + workingDirectory: cwd, + firstSeen: new Date().toISOString(), + lastSeen: new Date().toISOString(), + messagesSent: 0, + messagesReceived: 0, + }; + + this.agents.set(name, record); + this.save(); + return record; + } +} +``` + +### 4. Session Discovery + +The alternative auto-discovers tmux sessions: + +```typescript +// Their approach +async function discoverLocalSessions(): Promise { + const { stdout } = await execAsync('tmux list-sessions -F "#{session_name}"'); + const sessionNames = stdout.trim().split('\n').filter(Boolean); + + return Promise.all(sessionNames.map(async (name) => { + const { stdout: cwd } = await execAsync( + `tmux display-message -t ${name} -p '#{pane_current_path}'` + ); + return { name, workingDirectory: cwd.trim() }; + })); +} +``` + +**Improvement:** Add discovery for better `agent-relay status`: + +```typescript +// In cli/index.ts - enhance status command +async function discoverRelaySessions(): Promise { + try { + const { stdout } = await execAsync('tmux list-sessions -F "#{session_name}"'); + const sessions = stdout.trim().split('\n').filter(Boolean); + + // Filter to relay sessions only + return sessions + .filter(name => name.startsWith('relay-')) + .map(name => { + const match = name.match(/^relay-(.+)-\d+$/); + return match ? { sessionName: name, agentName: match[1] } : null; + }) + .filter(Boolean); + } catch { + return []; + } +} +``` + +### 5. Output Filtering + +The alternative filters noisy patterns from logs: + +```typescript +// Their approach - filter thinking indicators, escape sequences +const NOISE_PATTERNS = [ + /\[\d+\/\d+\]/, // [1/418] thinking steps + /\x1b\[[0-9;]*[mK]/, // ANSI escape sequences + /^Thinking\.{1,3}$/, // "Thinking..." lines +]; + +function filterNoise(output: string): string { + return output.split('\n') + .filter(line => !NOISE_PATTERNS.some(p => p.test(line))) + .join('\n'); +} +``` + +**Improvement:** Add optional output filtering for cleaner logs: + +```typescript +// In tmux-wrapper.ts +private filterForLogging(output: string): string { + if (!this.config.filterLogs) return output; + + return output + .split('\n') + .filter(line => { + // Skip thinking indicators + if (/^\[[\d/]+\]/.test(line)) return false; + // Skip empty ANSI-only lines + if (this.stripAnsi(line).trim() === '') return false; + return true; + }) + .join('\n'); +} +``` + +--- + +## Rejected Ideas + +### 1. Browser-Based Terminal + +Moving to xterm.js would lose the native terminal feel. Users expect to use their own terminal with their own keybindings, themes, and muscle memory. + +**Decision:** Keep native tmux attach. + +### 2. Full node-pty Integration + +Using node-pty for output streaming would add native dependencies and build complexity. The polling approach works well enough. + +**Decision:** Keep capture-pane polling. Consider optional streaming as future enhancement. + +### 3. Complex Agent Lifecycle + +The alternative supports agents without sessions, complex metadata, and persistent memory. This adds significant complexity. + +**Decision:** Keep it simple. Agent = wrapper process. When wrapper exits, agent is gone. + +--- + +## Implementation Priority + +| Improvement | Effort | Impact | Priority | +|-------------|--------|--------|----------| +| Activity tracking | Low | Medium | P1 | +| Reconnection backoff | Low | Medium | P1 | +| Session discovery | Low | Low | P2 | +| Agent registry | Medium | Medium | P2 | +| Output filtering | Low | Low | P3 | + +--- + +## Summary + +Our tmux implementation is **simpler and more native** than alternatives. The key improvements to adopt: + +1. **Activity state tracking** - Better injection timing +2. **Exponential backoff** - Graceful daemon reconnection +3. **Session discovery** - Better status output +4. **Agent registry** - Persistence across restarts + +These add minimal complexity while improving reliability. The core architecture (polling + pattern parsing + injection) remains unchanged. From cf7d2f20ed4a6e9ae69662fab7561f3df1e986c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 18:02:56 +0000 Subject: [PATCH 2/7] Add Phase 5: Multi-agent coordination for 5-10 agents New features for scaling beyond 2-3 agents: - Agent groups (@relay:@backend sends to all backend agents) - Terminal-based dashboard (agent-relay watch) - Coordination patterns (coordinator, pipeline, pub/sub) - Tmux layout helper for viewing multiple agents - Message priority levels (urgent !, normal, low ?) - Agent roles and permissions in teams.json --- docs/DESIGN_V2.md | 310 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 304 insertions(+), 6 deletions(-) diff --git a/docs/DESIGN_V2.md b/docs/DESIGN_V2.md index ae05ce636..b587a4d5a 100644 --- a/docs/DESIGN_V2.md +++ b/docs/DESIGN_V2.md @@ -380,18 +380,304 @@ src/ | Metric | Current | Target | |--------|---------|--------| -| Lines of code | ~2500 | ~2000 | -| Message types | 10 | 6 | -| Dependencies | 12 | 10 | -| Memory (1hr session) | Unbounded | <50MB | -| Message detection latency | 0-200ms | 0-200ms (or <10ms with streaming) | +| Lines of code | ~2500 | ~2800 (with TUI) | +| Message types | 10 | 8 (added GROUP, TOPIC) | +| Max agents | ~3 practical | 10+ comfortable | +| Dependencies | 12 | 14 (adds blessed for TUI) | +| Memory (1hr session) | Unbounded | <100MB (10 agents) | +| Message detection latency | 0-200ms | 0-200ms | | Data persistence | Lost on reboot | Permanent | +| Visibility | None | TUI dashboard | + +--- + +## Phase 5: Multi-Agent Coordination (5-10 Agents) + +Scaling from 2-3 agents to 5-10 requires better visibility, organization, and coordination patterns. + +### 5.1 Agent Groups + +Group agents for targeted messaging: + +```bash +# Define groups in teams.json +{ + "groups": { + "backend": ["ApiDev", "DbAdmin", "AuthService"], + "frontend": ["UiDev", "Stylist"], + "review": ["Reviewer", "QA"] + } +} + +# Send to group +@relay:@backend We need to refactor the user service +# → Message delivered to ApiDev, DbAdmin, AuthService + +# Broadcast to all +@relay:* Starting deployment in 5 minutes +``` + +**Implementation:** +```typescript +// In router.ts +route(from: Connection, envelope: Envelope) { + const to = envelope.to; + + if (to === '*') { + this.broadcast(from, envelope); + } else if (to.startsWith('@')) { + // Group message + const groupName = to.slice(1); + const members = this.groups.get(groupName) || []; + for (const member of members) { + if (member !== from.agentName) { + this.sendTo(member, envelope); + } + } + } else { + this.sendTo(to, envelope); + } +} +``` + +### 5.2 Terminal-Based Dashboard (TUI) + +A simple terminal UI for monitoring all agents without leaving the terminal: + +```bash +agent-relay watch +``` + +``` +┌─ Agent Relay ──────────────────────────────────────────────┐ +│ Agents (8 connected) │ +├─────────────────────────────────────────────────────────────┤ +│ ● Coordinator idle 2m msgs: 12↑ 8↓ │ +│ ● ApiDev active msgs: 5↑ 14↓ typing... │ +│ ● DbAdmin active msgs: 3↑ 6↓ │ +│ ● AuthService idle 45s msgs: 2↑ 4↓ │ +│ ● UiDev active msgs: 8↑ 10↓ typing... │ +│ ● Stylist idle 5m msgs: 1↑ 2↓ │ +│ ● Reviewer active msgs: 0↑ 15↓ │ +│ ○ QA offline queued: 3 │ +├─────────────────────────────────────────────────────────────┤ +│ Recent Messages │ +│ 14:23:01 ApiDev → DbAdmin: Can you check the user table? │ +│ 14:23:15 DbAdmin → ApiDev: Schema looks correct │ +│ 14:23:30 Coordinator → @backend: Stand up in 5 mins │ +│ 14:24:01 UiDev → Reviewer: PR ready for auth flow │ +├─────────────────────────────────────────────────────────────┤ +│ [a]ttach [s]end [g]roups [q]uit │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Features:** +- Real-time agent status (active/idle/offline) +- Message counts and queue depth +- Recent message feed +- Quick attach to any agent's tmux session +- Send messages from dashboard + +**Implementation:** Use `blessed` or `ink` for terminal UI: +```typescript +// src/cli/watch.ts +import blessed from 'blessed'; + +const screen = blessed.screen({ smartCSR: true }); +const agentList = blessed.list({ + parent: screen, + label: 'Agents', + // ... +}); + +// Subscribe to daemon events via WebSocket +const ws = new WebSocket(`ws+unix://${socketPath}`); +ws.on('message', (data) => { + const event = JSON.parse(data); + updateDisplay(event); +}); +``` + +### 5.3 Coordination Patterns + +#### Pattern 1: Coordinator Agent + +One agent orchestrates the others: + +``` +Coordinator + ├── @relay:ApiDev Implement /api/users endpoint + ├── @relay:DbAdmin Create users table + └── @relay:UiDev Build user profile page + +ApiDev → Coordinator: Done, endpoint at /api/users +DbAdmin → Coordinator: Table created with schema... +UiDev → Coordinator: Need API spec first + +Coordinator → UiDev: Here's the spec: GET /api/users... +``` + +#### Pattern 2: Pipeline + +Agents pass work sequentially: + +``` +Developer → Reviewer → QA → Deployer + +@relay:Reviewer PR #123 ready for review + ↓ +@relay:QA Review passed, ready for testing + ↓ +@relay:Deployer Tests passed, deploy when ready +``` + +#### Pattern 3: Pub/Sub Topics + +Agents subscribe to topics of interest: + +```bash +# Agent subscribes to topic +@relay:subscribe security-alerts + +# Any agent can publish +@relay:topic:security-alerts Found SQL injection in auth.ts + +# All subscribers receive the message +``` + +**Implementation:** +```typescript +// Subscribe syntax +@relay:+topic-name # Subscribe +@relay:-topic-name # Unsubscribe +@relay:#topic-name msg # Publish to topic + +// In parser.ts +const TOPIC_SUBSCRIBE = /^@relay:\+(\S+)$/; +const TOPIC_UNSUBSCRIBE = /^@relay:-(\S+)$/; +const TOPIC_PUBLISH = /^@relay:#(\S+)\s+(.+)$/; +``` + +### 5.4 Tmux Layout Helper + +Quickly set up multi-agent tmux layouts: + +```bash +# Create tiled layout with all agents +agent-relay layout tile + +# Create layout from teams.json +agent-relay layout teams + +# Custom layout +agent-relay layout grid 3x3 +``` + +**Generated tmux layout:** +``` +┌─────────────┬─────────────┬─────────────┐ +│ Coordinator │ ApiDev │ DbAdmin │ +├─────────────┼─────────────┼─────────────┤ +│ AuthService │ UiDev │ Stylist │ +├─────────────┼─────────────┼─────────────┤ +│ Reviewer │ QA │ (empty) │ +└─────────────┴─────────────┴─────────────┘ +``` + +**Implementation:** +```bash +#!/bin/bash +# agent-relay layout tile +AGENTS=$(agent-relay agents --json | jq -r '.[].name') +COUNT=$(echo "$AGENTS" | wc -l) + +tmux new-session -d -s relay-overview +for agent in $AGENTS; do + tmux split-window -t relay-overview + tmux send-keys -t relay-overview "tmux attach -t relay-$agent-*" Enter +done +tmux select-layout -t relay-overview tiled +tmux attach -t relay-overview +``` + +### 5.5 Agent Roles & Capabilities + +Define what each agent can do: + +```json +// teams.json +{ + "agents": { + "Coordinator": { + "role": "coordinator", + "canMessage": ["*"], + "canReceiveFrom": ["*"] + }, + "ApiDev": { + "role": "developer", + "groups": ["backend"], + "canMessage": ["Coordinator", "@backend", "Reviewer"], + "canReceiveFrom": ["Coordinator", "@backend"] + }, + "Reviewer": { + "role": "reviewer", + "canMessage": ["Coordinator", "QA"], + "canReceiveFrom": ["*"] + } + } +} +``` + +**Use cases:** +- Prevent junior agents from messaging senior ones directly +- Ensure QA only receives from Reviewer (enforced pipeline) +- Coordinator can message anyone + +### 5.6 Message Priority & Filtering + +With more agents, message prioritization becomes important: + +```bash +# Urgent message (interrupts immediately) +@relay:!ApiDev Production is down, check auth service + +# Normal message (waits for idle) +@relay:ApiDev When you have time, review this PR + +# Low priority (batched, delivered during quiet periods) +@relay:?ApiDev FYI: Updated the style guide +``` + +**Injection behavior:** +| Priority | Syntax | Behavior | +|----------|--------|----------| +| Urgent | `@relay:!Name` | Inject immediately, even if busy | +| Normal | `@relay:Name` | Wait for idle (current behavior) | +| Low | `@relay:?Name` | Batch and deliver during long idle | + +### 5.7 Status Broadcasts + +Agents automatically announce state changes: + +```typescript +// Automatic status messages +@relay:* STATUS: ApiDev is now idle +@relay:* STATUS: Reviewer completed task (closed PR #123) +@relay:* STATUS: QA disconnected + +// Agents can filter these +// In wrapper config: +{ + "hideStatusMessages": true, // Don't inject STATUS broadcasts + "showStatusInLogs": true // But log them for visibility +} +``` --- ## Non-Goals -- **Dashboard/Web UI**: Out of scope. Use `agent-relay logs` for visibility. +- **Browser Dashboard**: Out of scope. TUI (`agent-relay watch`) provides visibility. - **Multi-host support**: Single machine focus. Use SSH for remote. - **Agent memory/RAG**: Separate concern. Agents manage their own context. - **Authentication**: Unix socket permissions are sufficient for local use. @@ -406,6 +692,18 @@ src/ | Phase 2 | Reliability | 2-3 days | | Phase 3 | DX improvements | 1-2 days | | Phase 4 | Optional enhancements | As needed | +| Phase 5 | Multi-agent coordination | 3-5 days | + +### Phase 5 Breakdown + +| Feature | Effort | Priority | +|---------|--------|----------| +| Agent groups (`@relay:@groupname`) | 1 day | P1 | +| TUI dashboard (`agent-relay watch`) | 2 days | P1 | +| Tmux layout helper | 0.5 day | P2 | +| Message priority (`!`, `?`) | 0.5 day | P2 | +| Pub/sub topics | 1 day | P3 | +| Agent roles/permissions | 1 day | P3 | --- From f1249d7ccff710e66886ab4356f3338a58933241 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 19:11:24 +0000 Subject: [PATCH 3/7] Add technical tmux comparison and Gemini @ symbol analysis - Deep dive comparing our polling approach vs streaming alternative - Analyze robustness: injection, parsing, session management - Document Gemini @ symbol conflict with file references - Propose configurable prefix alternatives (>>, /relay, relay::) - Recommend auto-detection of best prefix per CLI type --- docs/TMUX_IMPROVEMENTS.md | 560 +++++++++++++++++++++++++++++++++++++- 1 file changed, 559 insertions(+), 1 deletion(-) diff --git a/docs/TMUX_IMPROVEMENTS.md b/docs/TMUX_IMPROVEMENTS.md index e93c342f9..352604ba3 100644 --- a/docs/TMUX_IMPROVEMENTS.md +++ b/docs/TMUX_IMPROVEMENTS.md @@ -1,4 +1,562 @@ -# tmux Implementation: Analysis & Improvements +# tmux Implementation: Technical Comparison & Analysis + +## Head-to-Head: Ours vs Alternative Approach + +### Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ OUR APPROACH (agent-relay) │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ User Terminal │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ spawn('tmux', ['attach-session', '-t'])│ ◄── stdio: 'inherit' │ +│ │ User sees REAL tmux session │ │ +│ └─────────────────────────────────────────┘ │ +│ │ +│ Background Process (same Node.js process) │ +│ │ │ +│ ├─► setInterval(200ms) │ +│ │ └─► execAsync('tmux capture-pane -p -J -S -') │ +│ │ └─► parser.parse(output) │ +│ │ └─► detect @relay: patterns │ +│ │ └─► send to daemon │ +│ │ │ +│ └─► onMessage from daemon │ +│ └─► wait for idle (1.5s no output) │ +│ └─► execAsync('tmux send-keys -l "msg"') │ +│ │ +│ Dependencies: NONE (just tmux + node child_process) │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ ALTERNATIVE APPROACH (streaming) │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ Browser (xterm.js) │ +│ │ │ +│ │ WebSocket │ +│ ▼ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ WebSocket Server (server.mjs) │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ node-pty.spawn('tmux', ['attach']) │ ◄── PTY pseudo-tty │ +│ │ │ │ │ +│ │ ├─► pty.onData(data) │ │ +│ │ │ └─► ws.send(data) │ Real-time to browser │ +│ │ │ │ │ +│ │ └─► ws.onMessage(input) │ │ +│ │ └─► pty.write(input) │ Real-time from user │ +│ └─────────────────────────────────────────┘ │ +│ │ +│ Messages: Separate system (filesystem + HTTP API) │ +│ └─► ~/.aimaestro/messages/inbox/{agent}/ │ +│ └─► Agent polls for new files or gets tmux notification │ +│ │ +│ Dependencies: node-pty (native), ws, xterm.js │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Technical Deep Dive + +### 1. Terminal I/O Method + +#### Ours: Polling with `capture-pane` + +```typescript +// Every 200ms, capture terminal buffer +private async pollForRelayCommands(): Promise { + const { stdout } = await execAsync( + `tmux capture-pane -t ${this.sessionName} -p -J -S - 2>/dev/null` + ); + // -p = print to stdout (not to buffer) + // -J = join wrapped lines + // -S - = start from beginning of scrollback + + const cleanContent = this.stripAnsi(stdout); + const { commands } = this.parser.parse(cleanContent); + // ... +} +``` + +#### Alternative: Streaming with node-pty + +```javascript +// Real-time event-driven +const pty = spawn('tmux', ['attach-session', '-t', session]); + +pty.onData((data) => { + // Fires immediately on ANY output + ws.send(data); // Forward to browser + filterAndLog(data); +}); +``` + +#### Comparison + +| Aspect | Polling (Ours) | Streaming (Theirs) | +|--------|----------------|-------------------| +| **Latency** | 0-200ms (poll interval) | <10ms (event-driven) | +| **CPU idle** | Constant ~1-2% (polling) | Near 0% (event-driven) | +| **CPU active** | Same | Same | +| **Missed output** | Possible if buffer wraps | Never (stream-based) | +| **Complexity** | ~20 lines | ~50 lines + native dep | +| **Build issues** | None | node-pty compilation | + +**Robustness verdict:** Streaming is technically more robust (no missed output), but polling is simpler and "good enough" for text-based agents that don't produce massive output bursts. + +--- + +### 2. User Terminal Experience + +#### Ours: Native tmux attach + +```typescript +// User's terminal is directly attached to tmux +this.attachProcess = spawn('tmux', ['attach-session', '-t', this.sessionName], { + stdio: 'inherit', // <-- KEY: User's stdin/stdout/stderr ARE the tmux session +}); +``` + +**What this means:** +- User's terminal emulator renders tmux directly +- All keybindings work natively (Ctrl+B, mouse, etc.) +- Scrollback, copy/paste work as expected +- No intermediate rendering layer + +#### Alternative: Browser-based xterm.js + +```javascript +// Terminal rendered in browser via WebGL +const term = new Terminal({ + rendererType: 'webgl', + convertEol: false, // PTY handles line endings +}); +term.loadAddon(new FitAddon()); +term.loadAddon(new WebglAddon()); +``` + +**What this means:** +- Terminal is *emulated* in browser +- Some keybindings may differ +- Scrollback limited by xterm.js buffer +- Copy/paste goes through browser + +#### Comparison + +| Aspect | Native tmux (Ours) | xterm.js (Theirs) | +|--------|-------------------|-------------------| +| **Keybindings** | 100% native | ~95% (some edge cases) | +| **Scrollback** | tmux buffer (configurable) | xterm.js buffer | +| **Performance** | Native | WebGL (good, but more overhead) | +| **Accessibility** | Terminal emulator's | Browser-based | +| **Remote access** | SSH | Browser (Tailscale) | + +**Robustness verdict:** Native is more robust for power users. Browser is more accessible for teams/remote. + +--- + +### 3. Message Injection + +#### Ours: Idle detection + send-keys + +```typescript +private async injectNextMessage(): Promise { + // Wait for output to settle + const timeSinceOutput = Date.now() - this.lastOutputTime; + if (timeSinceOutput < 1500) { // 1.5 seconds + setTimeout(() => this.checkForInjectionOpportunity(), 500); + return; + } + + // Clear any partial input + await this.sendKeys('Escape'); + await this.sleep(30); + await this.sendKeys('C-u'); // Clear line + await this.sleep(30); + + // Type the message + await this.sendKeysLiteral(message); + await this.sleep(50); + await this.sendKeys('Enter'); +} + +private async sendKeysLiteral(text: string): Promise { + const escaped = text + .replace(/[\r\n]+/g, ' ') + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\$/g, '\\$') + .replace(/`/g, '\\`') + .replace(/!/g, '\\!'); + await execAsync(`tmux send-keys -t ${this.sessionName} -l "${escaped}"`); +} +``` + +#### Alternative: Multiple injection methods + +```bash +# Method 1: display-message (non-intrusive popup) +tmux display-message -t $SESSION "Message from $FROM: $MSG" + +# Method 2: send-keys with echo (injects shell command) +tmux send-keys -t $SESSION "echo '📬 MESSAGE: $MSG'" Enter + +# Method 3: send-keys literal (injects text) +tmux send-keys -t $SESSION -l "Message: $MSG" +``` + +#### Comparison + +| Aspect | Our Approach | Their Approach | +|--------|--------------|----------------| +| **Idle detection** | Time-based (1.5s) | None (fire and forget) | +| **Input clearing** | Yes (Esc + Ctrl-U) | No | +| **Race conditions** | Reduced | Possible | +| **CLI-specific** | Yes (Gemini printf) | Partial | +| **Intrusive** | Yes (types into prompt) | display-message is not | + +**Robustness verdict:** Our approach is more robust because of idle detection and input clearing. Their `display-message` is less intrusive but also less reliable for LLM consumption. + +--- + +### 4. Message Detection/Parsing + +#### Ours: Pattern matching on terminal output + +```typescript +// Parser handles real-world terminal mess +const INLINE_RELAY = /^(?:\s*(?:[>$%#→➜›»●•◦‣⁃\-*⏺◆◇○□■]\s*)*)?@relay:(\S+)\s+(.+)$/; + +// Strip ANSI codes +const ANSI_PATTERN = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b\].*?(?:\x07|\x1b\\)|\r/g; + +// Handle continuation lines (TUI wrapping) +private joinContinuationLines(content: string): string { + // Claude Code and TUIs insert real newlines... +} + +// Track what we've already processed +private sentMessageHashes: Set = new Set(); +``` + +#### Alternative: No parsing needed + +```javascript +// Messages are sent via API/CLI, not terminal output +send-aimaestro-message.sh Bob "Subject" "Body" + +// Creates file in ~/.aimaestro/messages/inbox/Bob/ +// Agent's "subconscious" polls filesystem for new files +``` + +#### Comparison + +| Aspect | Pattern Parsing (Ours) | API-based (Theirs) | +|--------|------------------------|-------------------| +| **Agent effort** | Just output text | Call external script | +| **Natural** | Yes (`@relay:Bob hi`) | No (shell command) | +| **Reliable** | ~95% (edge cases) | 100% (structured) | +| **Multi-line** | Complex (continuation) | Easy (JSON body) | +| **ANSI codes** | Must strip | N/A | + +**Robustness verdict:** API-based is technically more robust (no parsing edge cases). But pattern-based is more natural for agents - they just "speak" instead of calling tools. + +--- + +### 5. Session Management + +#### Ours: One wrapper = one session + +```typescript +// Generate unique session name +this.sessionName = `relay-${config.name}-${process.pid}`; + +// Create session +execSync(`tmux new-session -d -s ${this.sessionName} -x ${cols} -y ${rows}`); + +// Set environment +execSync(`tmux setenv -t ${this.sessionName} AGENT_RELAY_NAME ${name}`); + +// When wrapper exits, session is killed +stop(): void { + execSync(`tmux kill-session -t ${this.sessionName} 2>/dev/null`); +} +``` + +#### Alternative: Discovery-based + +```typescript +// Discover existing sessions +async function discoverLocalSessions(): Promise { + const { stdout } = await execAsync('tmux list-sessions -F "#{session_name}"'); + return stdout.trim().split('\n').map(name => ({ + name, + // Fetch metadata + cwd: await execAsync(`tmux display-message -t ${name} -p '#{pane_current_path}'`) + })); +} + +// Sessions can exist without agents +// Agents can exist without sessions +// Linking is optional +``` + +#### Comparison + +| Aspect | Wrapper-owns-session (Ours) | Discovery-based (Theirs) | +|--------|----------------------------|-------------------------| +| **Lifecycle** | Coupled (wrapper=session) | Decoupled | +| **Pre-existing** | No | Yes | +| **Orphan sessions** | No (killed on exit) | Possible | +| **Flexibility** | Lower | Higher | + +**Robustness verdict:** Their approach is more flexible (can attach to existing sessions). Our approach is simpler and prevents orphan sessions. + +--- + +## Which is More Robust? + +### Our Strengths + +| Area | Why We're Stronger | +|------|-------------------| +| **Native experience** | Direct tmux attach, no emulation layer | +| **Simplicity** | No native dependencies, no WebSocket complexity | +| **Injection** | Idle detection prevents race conditions | +| **CLI support** | Special handling for Gemini, etc. | +| **Deduplication** | Won't send same message twice | + +### Their Strengths + +| Area | Why They're Stronger | +|------|---------------------| +| **Real-time** | Event-driven, no polling latency | +| **Visibility** | Browser dashboard shows all agents | +| **Message reliability** | Filesystem-based, never lost | +| **Remote access** | Browser-based, works via Tailscale | +| **Agent decoupling** | Agents exist independent of sessions | + +--- + +## Recommended Improvements for Robustness + +### 1. Add Activity State Tracking (from their approach) + +```typescript +// Track active/idle/disconnected state +private activityState: 'active' | 'idle' | 'disconnected' = 'active'; +private lastActivityTime = Date.now(); + +private updateActivityState(): void { + const elapsed = Date.now() - this.lastActivityTime; + + if (elapsed > 30_000) { + this.activityState = 'idle'; + // Idle is the BEST time to inject + this.flushMessageQueue(); + } else if (elapsed > 5_000) { + this.activityState = 'idle'; + } else { + this.activityState = 'active'; + } +} +``` + +### 2. Add Exponential Backoff for Reconnection + +```typescript +private readonly RECONNECT_DELAYS = [100, 500, 1000, 2000, 5000]; +private reconnectAttempt = 0; + +private reconnect(): void { + if (this.reconnectAttempt >= this.RECONNECT_DELAYS.length) { + this.logStderr('Max reconnection attempts, operating offline'); + return; + } + + const delay = this.RECONNECT_DELAYS[this.reconnectAttempt++]; + setTimeout(() => this.client.connect(), delay); +} +``` + +### 3. Consider Hybrid: Streaming + Pattern Parsing + +```typescript +// Best of both worlds (optional mode) +import { spawn } from 'node-pty'; + +// Read-only PTY attach for real-time output +const pty = spawn('tmux', ['attach-session', '-t', session, '-r']); + +pty.onData((data) => { + // Real-time pattern detection + const { commands } = this.parser.parse(data); + for (const cmd of commands) { + this.sendRelayCommand(cmd); + } +}); + +// Still use send-keys for injection (works on attached session) +``` + +### 4. Add Bracketed Paste for Safer Injection + +```typescript +const PASTE_START = '\x1b[200~'; +const PASTE_END = '\x1b[201~'; + +async function injectSafe(text: string): Promise { + // Bracketed paste prevents shell interpretation + await sendKeysLiteral(PASTE_START + text + PASTE_END); + await sendKeys('Enter'); +} +``` + +--- + +## Known Issue: `@` Symbol Conflicts with Gemini + +### The Problem + +Gemini CLI uses `@` for file references: +```bash +gemini> @src/main.ts # References a file +gemini> @relay:Bob Hi # Gemini might try to open file "relay:Bob"! +``` + +This could explain why Gemini agents have trouble sending relay messages - the CLI intercepts `@` before it reaches the terminal output. + +### Proposed Alternative Prefixes + +| Prefix | Example | Pros | Cons | +|--------|---------|------|------| +| `>>` | `>>Bob: Hello` | Simple, intuitive | Could conflict with shell redirect | +| `->` | `->Bob: Hello` | Clear direction | Might look like code | +| `#relay:` | `#relay:Bob Hello` | Hashtag is common | Could conflict with comments | +| `!relay:` | `!relay:Bob Hello` | Bang is distinct | Could trigger shell history | +| `/relay` | `/relay Bob Hello` | Slash command style | Familiar pattern | +| `[[relay]]` | `[[relay:Bob]] Hello` | Very distinct | Verbose | +| `@>` | `@>Bob: Hello` | Keeps @ but distinct | Still has @ | +| `relay::` | `relay::Bob Hello` | No special prefix char | Plain text | + +### Recommended: Configurable Prefix + +Support multiple prefixes with a default that works everywhere: + +```typescript +// In config +{ + "relayPrefix": "@relay:", // Default (works for Claude, Codex) + // Alternatives: + // "relayPrefix": ">>", // For Gemini + // "relayPrefix": "/relay", // Slash command style + // "relayPrefix": "relay::", // Plain text +} + +// In parser.ts +const prefix = config.relayPrefix || '@relay:'; +const pattern = new RegExp(`^(?:\\s*)?${escapeRegex(prefix)}(\\S+)\\s+(.+)$`); +``` + +### Testing Gemini with Alternative Prefix + +```bash +# Start with alternative prefix +agent-relay -n GeminiAgent --prefix=">>" gemini + +# Agent outputs: +>>Bob: Can you review this code? + +# Instead of: +@relay:Bob Can you review this code? +``` + +### Implementation: CLI Flag + +```typescript +// In cli/index.ts +.option('--prefix ', 'Relay pattern prefix (default: @relay:)') + +// In wrapper config +const wrapperConfig: TmuxWrapperConfig = { + name: options.name, + command: mainCommand, + args: commandArgs, + relayPrefix: options.prefix || '@relay:', + // ... +}; +``` + +### Parser Update + +```typescript +// In parser.ts +export class OutputParser { + private prefix: string; + private inlinePattern: RegExp; + + constructor(options: ParserOptions = {}) { + this.prefix = options.prefix || '@relay:'; + + // Build pattern dynamically + const escaped = this.escapeRegex(this.prefix); + this.inlinePattern = new RegExp( + `^(?:\\s*(?:[>$%#→➜›»●•◦‣⁃\\-*⏺◆◇○□■]\\s*)*)?${escaped}(\\S+)\\s+(.+)$` + ); + } + + private escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } +} +``` + +### Recommendation + +1. **Short term:** Add `--prefix` flag to test with Gemini +2. **Default for Gemini:** Auto-detect and use `>>` or `/relay` +3. **Long term:** Document which prefix works best per CLI + +```typescript +// Auto-detect best prefix for CLI type +function getDefaultPrefix(cliType: string): string { + switch (cliType) { + case 'gemini': + return '>>'; // Avoid @ conflict + case 'claude': + case 'codex': + default: + return '@relay:'; // Original, works fine + } +} +``` + +--- + +## Verdict: Overall Robustness + +| Category | Winner | Reason | +|----------|--------|--------| +| **Message detection** | Tie | Ours is natural, theirs is reliable | +| **Message delivery** | Ours | Idle detection prevents corruption | +| **Terminal fidelity** | Ours | Native > emulated | +| **Real-time** | Theirs | Streaming > polling | +| **Simplicity** | Ours | No native deps, no browser | +| **Visibility** | Theirs | Dashboard > logs | +| **Multi-agent** | Theirs | Built for teams | + +**Overall:** For 2-3 agents, **ours is more robust** (simpler, fewer failure modes). For 5-10 agents, **theirs scales better** (visibility, discovery). The recommended improvements above would close the gap. + +--- ## Current Implementation Summary From 19aa67cfb2c11aa76c113ea4a7147f984b46c860 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 19:18:42 +0000 Subject: [PATCH 4/7] Add scaling analysis: why theirs works for 10+ agents and how we match it Key insight: The problem isn't polling vs streaming - it's visibility. Their advantages at scale: - Browser dashboard = single pane of glass - Activity state tracking (active/idle/typing) - Persistent message inbox - Agent discovery Our solution (keep native terminal, add visibility): - TUI dashboard (agent-relay watch) with blessed - Daemon event stream for real-time updates - Wrapper reports activity state - CLI commands: agents, history, send - Architecture: daemon becomes event hub, not just router --- docs/DESIGN_V2.md | 357 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 357 insertions(+) diff --git a/docs/DESIGN_V2.md b/docs/DESIGN_V2.md index b587a4d5a..b63d515d5 100644 --- a/docs/DESIGN_V2.md +++ b/docs/DESIGN_V2.md @@ -675,6 +675,363 @@ Agents automatically announce state changes: --- +## Why They Scale Better (And How We Can Too) + +### The Scaling Problem + +With 2-3 agents, our current approach works well: +- Open 2-3 terminal tabs +- Switch between them manually +- Remember who's doing what + +With 5-10 agents, this breaks down: + +| Problem | Impact at 5-10 Agents | +|---------|----------------------| +| **No visibility** | Can't see what all agents are doing at once | +| **No status** | Don't know if agent is busy, idle, or stuck | +| **Lost context** | Forget which agent is working on what | +| **Message chaos** | Too many messages to track manually | +| **Terminal sprawl** | 10 tabs is unmanageable | + +### Why Their Approach Scales + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ THEIR ARCHITECTURE │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ BROWSER DASHBOARD │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ Agent 1 │ │ Agent 2 │ │ Agent 3 │ │ Agent 4 │ ... │ │ +│ │ │ ● active│ │ ○ idle │ │ ● active│ │ ✗ error │ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ +│ │ │ │ +│ │ [Live message feed] [Inbox: 3 unread] [Agent graph] │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ │ +│ Single pane of glass │ +│ │ +└─────────────────────────────────────────────────────────────────┘ + +Key insight: ONE place to see EVERYTHING +``` + +Their specific advantages at scale: + +| Feature | Why It Helps at Scale | +|---------|----------------------| +| **Dashboard** | See all 10 agents at once without switching | +| **Activity state** | Know instantly who's busy vs idle | +| **Message inbox** | Messages don't disappear into terminal history | +| **Agent discovery** | Auto-finds agents, no manual tracking | +| **Persistent storage** | Query historical messages anytime | + +### How We Keep Our Strengths AND Scale + +The goal: **Single pane of glass, but in the terminal** + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ OUR IMPROVED ARCHITECTURE │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ TUI DASHBOARD (agent-relay watch) │ │ +│ │ │ │ +│ │ Agents: Status: Messages: │ │ +│ │ ● Coordinator active 12↑ 8↓ │ │ +│ │ ● ApiDev typing... 5↑ 14↓ │ │ +│ │ ● DbAdmin idle 30s 3↑ 6↓ │ │ +│ │ ○ QA offline queued: 3 │ │ +│ │ │ │ +│ │ [Press 'a' to attach, 's' to send, 'q' to quit] │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ 'a' to attach │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ NATIVE TMUX SESSION │ │ +│ │ │ │ +│ │ claude> Working on the API endpoint... │ │ +│ │ @relay:DbAdmin Need the users table schema │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ Ctrl+B d to detach │ +│ ▼ │ +│ Back to TUI dashboard │ +│ │ +└─────────────────────────────────────────────────────────────────┘ + +Key insight: TUI for overview, native tmux for work +``` + +### Specific Scaling Improvements + +#### 1. Daemon Event Stream + +The daemon must broadcast events, not just route messages: + +```typescript +// NEW: Daemon broadcasts events to all listeners +interface DaemonEvent { + type: 'agent_connected' | 'agent_disconnected' | 'agent_active' | + 'agent_idle' | 'message_sent' | 'message_delivered' | 'message_queued'; + timestamp: number; + data: Record; +} + +// In daemon/server.ts +class Daemon { + private eventSubscribers: Set = new Set(); + + broadcast(event: DaemonEvent): void { + const envelope = { type: 'EVENT', event }; + for (const subscriber of this.eventSubscribers) { + subscriber.send(envelope); + } + } + + // Called when agent output detected + onAgentActivity(agentName: string): void { + this.broadcast({ + type: 'agent_active', + timestamp: Date.now(), + data: { agent: agentName } + }); + } +} +``` + +#### 2. Activity Reporting from Wrapper + +Wrappers must report activity state to daemon: + +```typescript +// In tmux-wrapper.ts +private reportActivity(): void { + const now = Date.now(); + const timeSinceOutput = now - this.lastOutputTime; + + let state: 'active' | 'idle' | 'typing'; + if (timeSinceOutput < 1000) { + state = 'active'; + } else if (this.detectTypingIndicator()) { + state = 'typing'; // Agent is thinking/working + } else if (timeSinceOutput < 30000) { + state = 'idle'; + } else { + state = 'idle'; + } + + // Only send if state changed + if (state !== this.lastReportedState) { + this.client.sendStatus(state); + this.lastReportedState = state; + } +} + +private detectTypingIndicator(): boolean { + // Claude Code shows "[1/418]" when thinking + // Detect this pattern in recent output + return /\[\d+\/\d+\]/.test(this.recentOutput); +} +``` + +#### 3. TUI Dashboard Implementation + +```typescript +// src/cli/watch.ts +import blessed from 'blessed'; + +export async function watchCommand(socketPath: string): Promise { + const screen = blessed.screen({ smartCSR: true }); + + // Agent list panel + const agentList = blessed.list({ + parent: screen, + label: ' Agents ', + top: 0, + left: 0, + width: '50%', + height: '60%', + border: { type: 'line' }, + style: { + selected: { bg: 'blue' } + }, + keys: true, + vi: true, + }); + + // Message feed panel + const messageFeed = blessed.log({ + parent: screen, + label: ' Messages ', + top: 0, + right: 0, + width: '50%', + height: '60%', + border: { type: 'line' }, + scrollable: true, + }); + + // Status bar + const statusBar = blessed.box({ + parent: screen, + bottom: 0, + height: 3, + content: ' [a]ttach [s]end [r]efresh [q]uit ', + }); + + // Connect to daemon event stream + const client = new RelayClient({ socketPath, subscribe: true }); + + client.onEvent = (event: DaemonEvent) => { + switch (event.type) { + case 'agent_connected': + updateAgentList(); + break; + case 'message_sent': + messageFeed.log(`${event.data.from} → ${event.data.to}: ${event.data.preview}`); + break; + // ... + } + screen.render(); + }; + + // Keyboard handlers + screen.key(['a'], () => attachToSelected()); + screen.key(['s'], () => showSendDialog()); + screen.key(['q'], () => process.exit(0)); + + screen.render(); +} + +function attachToSelected(): void { + const agent = getSelectedAgent(); + // Detach from blessed, attach to tmux + screen.destroy(); + execSync(`tmux attach-session -t relay-${agent}-*`, { stdio: 'inherit' }); + // When user detaches (Ctrl+B d), restart watch + watchCommand(socketPath); +} +``` + +#### 4. Message History Query + +```typescript +// src/cli/index.ts +program + .command('history') + .description('Show message history') + .option('-n ', 'Number of messages', '20') + .option('-f, --from ', 'Filter by sender') + .option('-t, --to ', 'Filter by recipient') + .option('--since