Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .beads/issues.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
{"id":"agent-relay-317","title":"Make Threads sidebar collapsible","description":"Added localStorage persistence for threads collapsed state in Sidebar.tsx. The ThreadList component already had full collapse/expand UI with toggle button, chevron icon, and conditional rendering. Now the collapsed state persists across page reloads.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-31T21:00:57.490669+01:00","updated_at":"2025-12-31T21:03:20.51811+01:00","closed_at":"2025-12-31T21:01:02.392896+01:00"}
{"id":"agent-relay-318","title":"Add team assignment support to agent spawn command","description":"Currently the -\u003erelay:spawn command creates agents but doesn't support assigning them to a specific team. Add support for team assignment in the spawn syntax, e.g.: -\u003erelay:spawn AgentName claude --team=Dashboard 'task description'. This would allow better organization of spawned agents and automatic team membership.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-31T21:12:47.046439+01:00","updated_at":"2025-12-31T21:12:55.078684+01:00"}
{"id":"agent-relay-319","title":"Make notification filtering use dynamic current user instead of hardcoded 'Dashboard'","description":"The notification fix in useMessages.ts:124-125 hardcodes 'Dashboard' to filter out the current user's own messages. This is brittle - when other human users join the chat, they'll see notifications for their own messages. Should dynamically detect the current user's identity (from auth context, session, or websocket connection) and use that for the filter.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-31T21:18:19.280325+01:00","updated_at":"2025-12-31T21:18:19.280325+01:00"}
{"id":"agent-relay-320","title":"Agent Relay logo: mission-control neon mark","description":"Design a scalable Agent Relay logo with mission-control deep space look. Requirements: works on dark backgrounds, cyan #00d9ff glow variant; accents: orange #ff6b35, purple #a855f7, teal/green #00ffc8; explore network/relay nodes, signal waves, abstract AR monogram, orbital/satellite cues, or circuit patterns. Deliver SVG and PNGs (32,64,128,256,512) saved to src/dashboard/public/logos/.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T20:07:33.380814+01:00","updated_at":"2026-01-01T20:09:58.339797+01:00","closed_at":"2026-01-01T20:09:58.339797+01:00"}
{"id":"agent-relay-37i","title":"Message deduplication uses in-memory Set without limits","description":"In tmux-wrapper.ts:65, sentMessageHashes is a Set that grows unbounded. For long-running sessions, this could cause memory issues. Add: (1) Max size with LRU eviction, (2) Time-based expiration, (3) Bloom filter alternative for memory efficiency.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-20T00:18:47.229988+01:00","updated_at":"2025-12-20T00:18:47.229988+01:00"}
{"id":"agent-relay-3px","title":"Add playbook system for batch automation","description":"Implement playbook system (like Maestro's Auto Run) for batch-processing task lists through agents. Define workflows in YAML/markdown, execute automatically with context isolation. Enables reproducible multi-step automation.","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-23T17:04:54.464749+01:00","updated_at":"2025-12-23T17:04:54.464749+01:00"}
{"id":"agent-relay-3tx","title":"PR-9 Review: Document configurable timeouts","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-22T21:54:15.789418+01:00","updated_at":"2025-12-22T21:54:15.789418+01:00"}
Expand Down
2 changes: 2 additions & 0 deletions .claude/agents/lead.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ bd create --title="Add feature X" --type=feature --priority=P2
```

### 4. Delegate
* If the user mentions to create an agent they probably mean for you to spawn an agent using
the agent-relay api and not create a sub agent. If you are unsure then ask for clarification.
```
->relay:Implementer <<<
**TASK:** Add feature X
Expand Down
21 changes: 21 additions & 0 deletions docs/agent-relay-snippet.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,27 @@ Messages appear as:
Relay message from Alice [abc123]: Message content here
```

### Channel Routing (Important!)

Messages from #general (broadcast channel) include a `[#general]` indicator:
```
Relay message from Alice [abc123] [#general]: Hello everyone!
```

**When you see `[#general]`**: Reply to `*` (broadcast), NOT to the sender directly.

```
# Correct - responds to #general channel
->relay:* <<<
Response to the group message.>>>

# Wrong - sends as DM to sender instead of to the channel
->relay:Alice <<<
Response to the group message.>>>
```

This ensures your response appears in the same channel as the original message.

If truncated, read full message:
```bash
agent-relay read abc123
Expand Down
2 changes: 1 addition & 1 deletion prpm.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
},
{
"name": "agent-relay-snippet",
"version": "1.0.1",
"version": "1.0.2",
"description": "AGENTS.md / CLAUDE.md snippet for agents on how to use agent-relay",
"format": "generic",
"subtype": "snippet",
Expand Down
173 changes: 173 additions & 0 deletions public/logo-concepts.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agent Relay - Logo Concepts</title>
<style>
body {
background-color: #0a0a0f;
color: #ffffff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 40px;
display: flex;
flex-direction: column;
align-items: center;
}

h1 {
color: #00d9ff;
text-shadow: 0 0 10px rgba(0, 217, 255, 0.5);
margin-bottom: 10px;
font-weight: 300;
letter-spacing: 2px;
}

p.subtitle {
color: #889;
margin-bottom: 50px;
}

.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 40px;
width: 100%;
max-width: 1200px;
}

.card {
background: #0d0d14;
border: 1px solid #1a1a2e;
border-radius: 12px;
padding: 30px;
display: flex;
flex-direction: column;
align-items: center;
transition: transform 0.2s, box-shadow 0.2s;
}

.card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0, 217, 255, 0.1);
border-color: #00d9ff;
}

.card h2 {
font-size: 1.2rem;
color: #e0e0e0;
margin-top: 20px;
margin-bottom: 5px;
}

.card p {
color: #889;
font-size: 0.9rem;
text-align: center;
}

svg {
width: 120px;
height: 120px;
filter: drop-shadow(0 0 8px rgba(0, 217, 255, 0.2));
}

/* Color Palette Classes for Reference */
.c-cyan { color: #00d9ff; fill: #00d9ff; stroke: #00d9ff; }
.c-orange { color: #ff6b35; fill: #ff6b35; stroke: #ff6b35; }
.c-purple { color: #a855f7; fill: #a855f7; stroke: #a855f7; }
.c-teal { color: #00ffc8; fill: #00ffc8; stroke: #00ffc8; }

</style>
</head>
<body>

<h1>AGENT RELAY</h1>
<p class="subtitle">Mission Control // Identity Concepts</p>

<div class="grid">
<!-- Concept 1: Network Nodes -->
<div class="card">
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Connections -->
<line x1="50" y1="50" x2="20" y2="30" stroke="#00d9ff" stroke-width="2" opacity="0.5" />
<line x1="50" y1="50" x2="80" y2="30" stroke="#00d9ff" stroke-width="2" opacity="0.5" />
<line x1="50" y1="50" x2="20" y2="70" stroke="#00d9ff" stroke-width="2" opacity="0.5" />
<line x1="50" y1="50" x2="80" y2="70" stroke="#00d9ff" stroke-width="2" opacity="0.5" />

<!-- Satellite Nodes -->
<circle cx="20" cy="30" r="6" fill="#1a1a2e" stroke="#ff6b35" stroke-width="2" />
<circle cx="80" cy="30" r="6" fill="#1a1a2e" stroke="#a855f7" stroke-width="2" />
<circle cx="20" cy="70" r="6" fill="#1a1a2e" stroke="#00ffc8" stroke-width="2" />
<circle cx="80" cy="70" r="6" fill="#1a1a2e" stroke="#00d9ff" stroke-width="2" />

<!-- Central Node -->
<circle cx="50" cy="50" r="12" fill="#0a0a0f" stroke="#00d9ff" stroke-width="3" />
<circle cx="50" cy="50" r="4" fill="#00d9ff" />
</svg>
<h2>The Nexus</h2>
<p>Represents central coordination of distributed agents.</p>
</div>

<!-- Concept 2: Signal Waves -->
<div class="card">
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Waves -->
<path d="M50 80 Q 20 50 50 20" stroke="#00d9ff" stroke-width="3" stroke-linecap="round" />
<path d="M50 80 Q 80 50 50 20" stroke="#00d9ff" stroke-width="3" stroke-linecap="round" />

<path d="M50 70 Q 35 50 50 30" stroke="#a855f7" stroke-width="2" stroke-linecap="round" opacity="0.8"/>
<path d="M50 70 Q 65 50 50 30" stroke="#a855f7" stroke-width="2" stroke-linecap="round" opacity="0.8"/>

<path d="M50 60 Q 45 50 50 40" stroke="#ff6b35" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
<path d="M50 60 Q 55 50 50 40" stroke="#ff6b35" stroke-width="2" stroke-linecap="round" opacity="0.6"/>

<!-- Core -->
<circle cx="50" cy="50" r="3" fill="#ffffff" />
</svg>
<h2>Pulse Relay</h2>
<p>Visualizes transmission and constant communication.</p>
</div>

<!-- Concept 3: Abstract AR -->
<div class="card">
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- A shape -->
<path d="M30 80 L 50 20 L 70 80" stroke="#00d9ff" stroke-width="4" stroke-linejoin="round" />
<line x1="40" y1="50" x2="60" y2="50" stroke="#00d9ff" stroke-width="4" />

<!-- R overlay style -->
<path d="M50 20 L 50 80" stroke="#00ffc8" stroke-width="2" opacity="0.7" />
<path d="M50 20 C 80 20 80 50 50 50 L 80 80" stroke="#00ffc8" stroke-width="2" fill="none" opacity="0.7"/>
</svg>
<h2>Monogram AR</h2>
<p>Interwoven letters symbolising structure and flow.</p>
</div>

<!-- Concept 4: Orbital -->
<div class="card">
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Orbits -->
<ellipse cx="50" cy="50" rx="40" ry="15" stroke="#a855f7" stroke-width="2" transform="rotate(-30 50 50)" opacity="0.6" />
<ellipse cx="50" cy="50" rx="40" ry="15" stroke="#ff6b35" stroke-width="2" transform="rotate(30 50 50)" opacity="0.6" />

<!-- Planet -->
<circle cx="50" cy="50" r="15" fill="url(#grad1)" />
<defs>
<radialGradient id="grad1" cx="50%" cy="50%" r="50%" fx="30%" fy="30%">
<stop offset="0%" style="stop-color:#00d9ff;stop-opacity:1" />
<stop offset="100%" style="stop-color:#005577;stop-opacity:1" />
</radialGradient>
</defs>

<!-- Orbiting bit -->
<circle cx="85" cy="50" r="3" fill="#ffffff" transform="rotate(-30 50 50)"/>
</svg>
<h2>Mission Control</h2>
<p>Deep space aesthetic with orbital mechanics.</p>
</div>
</div>

</body>
</html>
34 changes: 34 additions & 0 deletions src/daemon/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,40 @@ describe('Router', () => {
expect(delivered2.from).toBe('agent1');
expect(delivered2.to).toBe('agent3');
});

it('should include originalTo=* in broadcast DELIVER envelopes for channel routing', () => {
const sender = new MockConnection('conn-1', 'agent1');
const recipient = new MockConnection('conn-2', 'agent2');

router.register(sender);
router.register(recipient);

// Broadcast message - originalTo should preserve '*' so agents can reply to channel
const envelope = createSendEnvelope('agent1', '*');
router.route(sender, envelope);

const delivered = recipient.sentEnvelopes[0] as DeliverEnvelope;
expect(delivered.from).toBe('agent1');
expect(delivered.to).toBe('agent2'); // Specific recipient
expect(delivered.delivery.originalTo).toBe('*'); // Original target was broadcast
});

it('should not include originalTo for direct messages', () => {
const sender = new MockConnection('conn-1', 'agent1');
const recipient = new MockConnection('conn-2', 'agent2');

router.register(sender);
router.register(recipient);

// Direct message - originalTo should be undefined (same as 'to')
const envelope = createSendEnvelope('agent1', 'agent2');
router.route(sender, envelope);

const delivered = recipient.sentEnvelopes[0] as DeliverEnvelope;
expect(delivered.from).toBe('agent1');
expect(delivered.to).toBe('agent2');
expect(delivered.delivery.originalTo).toBeUndefined(); // Not needed for direct messages
});
});

describe('Topic subscriptions', () => {
Expand Down
4 changes: 4 additions & 0 deletions src/daemon/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,9 @@ export class Router {
original: SendEnvelope,
target: RoutableConnection
): DeliverEnvelope {
// Preserve the original 'to' field for broadcasts so agents know to reply to '*'
const originalTo = original.to;

return {
v: PROTOCOL_VERSION,
type: 'DELIVER',
Expand All @@ -593,6 +596,7 @@ export class Router {
delivery: {
seq: target.getNextSeq(original.topic ?? 'default', from),
session_id: target.sessionId,
originalTo: originalTo !== to ? originalTo : undefined, // Only include if different
},
};
}
Expand Down
47 changes: 39 additions & 8 deletions src/dashboard-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@
await fs.promises.unlink(filePath);
evictedCount++;
}
} catch (err) {

Check warning on line 387 in src/dashboard-server/server.ts

View workflow job for this annotation

GitHub Actions / lint

'err' is defined but never used. Allowed unused caught errors must match /^_/u
// Ignore errors for individual files (may have been deleted)
}
}
Expand Down Expand Up @@ -688,15 +688,25 @@
}
}

// Include attachments in the message data field
const messageData = attachments && attachments.length > 0
? { attachments }
: undefined;
// Include attachments and channel context in the message data field
// For broadcasts (to='*'), include channel: 'general' so replies can be routed back
const isBroadcast = targets.length === 1 && targets[0] === '*';
const messageData: Record<string, unknown> = {};

if (attachments && attachments.length > 0) {
messageData.attachments = attachments;
}

if (isBroadcast) {
messageData.channel = 'general';
}

const hasMessageData = Object.keys(messageData).length > 0;

// Send to all targets (single agent, team members, or broadcast)
let allSent = true;
for (const target of targets) {
const sent = relayClient.sendMessage(target, message, 'message', messageData, thread);
const sent = relayClient.sendMessage(target, message, 'message', hasMessageData ? messageData : undefined, thread);
if (!sent) {
allSent = false;
console.error(`[dashboard] Failed to send message to ${target}`);
Expand Down Expand Up @@ -920,12 +930,26 @@
}
};

// Helper to check if an agent name is internal/system (should be hidden from UI)
// Convention: agent names starting with __ are internal (e.g., __spawner__, __DashboardBridge__)
const isInternalAgent = (name: string): boolean => {
return name.startsWith('__');
};

const mapStoredMessages = (rows: StoredMessage[]): Message[] => rows
// Filter out messages from/to internal system agents (e.g., __spawner__)
.filter((row) => !isInternalAgent(row.from) && !isInternalAgent(row.to))
.map((row) => {
// Extract attachments from the data field if present
// Extract attachments and channel from the data field if present
let attachments: Attachment[] | undefined;
if (row.data && typeof row.data === 'object' && 'attachments' in row.data) {
attachments = (row.data as { attachments: Attachment[] }).attachments;
let channel: string | undefined;
if (row.data && typeof row.data === 'object') {
if ('attachments' in row.data) {
attachments = (row.data as { attachments: Attachment[] }).attachments;
}
if ('channel' in row.data) {
channel = (row.data as { channel: string }).channel;
}
}

return {
Expand All @@ -939,6 +963,7 @@
replyCount: row.replyCount,
status: row.status,
attachments,
channel,
};
});

Expand Down Expand Up @@ -1986,6 +2011,9 @@

let messages = await storage.getMessages(query);

// Filter out messages from/to internal system agents (e.g., __spawner__)
messages = messages.filter(m => !isInternalAgent(m.from) && !isInternalAgent(m.to));

// Client-side search filter (basic substring match)
const searchTerm = req.query.search as string | undefined;
if (searchTerm && searchTerm.trim()) {
Expand Down Expand Up @@ -2041,6 +2069,9 @@
// Skip broadcasts for conversation pairing
if (msg.to === '*' || msg.is_broadcast) continue;

// Skip messages from/to internal system agents (e.g., __spawner__)
if (isInternalAgent(msg.from) || isInternalAgent(msg.to)) continue;

// Create normalized key (sorted participants)
const participants = [msg.from, msg.to].sort();
const key = participants.join(':');
Expand Down
Loading
Loading