HUB-2: Threaded Messaging Architecture
Replace Discord's channel model with a threaded architecture tailored to OpenClaw: - **Threaded, not channel-based** — every conversation is a thread with a parent category - **Quad-inspired layout** — 4 concurrent contexts visible (like the terminal quad) - **Feed integration** — 33 Prefect flows post directly to threads (no Discord webhooks) - **Agent-native** — threads can be owned by agents, not just humans - **Voice-first** — every thread supports voice input/output - **Offline-capable** — SwiftData persistenc
Full Public Reader
HUB-2: Threaded Messaging Architecture
Date: 2026-03-01
Status: Design Complete — Ready for HUB-3 Implementation
---
1. Design Goals
Replace Discord's channel model with a threaded architecture tailored to OpenClaw:
- Threaded, not channel-based — every conversation is a thread with a parent category
- Quad-inspired layout — 4 concurrent contexts visible (like the terminal quad)
- Feed integration — 33 Prefect flows post directly to threads (no Discord webhooks)
- Agent-native — threads can be owned by agents, not just humans
- Voice-first — every thread supports voice input/output
- Offline-capable — SwiftData persistence with Supabase Realtime sync
---
2. Thread Types
### 2.1 Conversation Threads (interactive)
Human ↔ Agent bidirectional conversations. Created when user starts a chat or dispatches a task.
Thread: "Deploy RAG++ update"
├── [human] Deploy the latest RAG++ to cloud-vm
├── [agent:claude] Building container...
├── [agent:claude] Deployed successfully to :8000
├── [system] Service health check: ✅ passing
└── [human] 👍Properties: owner (human), agent (assigned), model (claude/gemini/ollama), session_key, gateway connection, conversation history (10-turn), context hints from Kimi.
### 2.2 Feed Threads (read-only, system-generated)
Automated posts from Prefect flows. Replace Discord webhook channels.
Thread: "#heartbeat — 2026-03-01"
├── [flow:heartbeat_pulse] 5/8 services healthy
├── [flow:heartbeat_pulse] 6/8 services healthy
├── [flow:infra_watchdog] ⚠️ Graph Kernel timeout (2 consecutive)
└── [flow:heartbeat_pulse] 8/8 services healthy ✅Properties: flow_name, auto-created daily (or per-run), read-only for humans, supports reactions/bookmarks.
### 2.3 Pulse Threads (session-tracking)
Track Pulse autonomous development sessions with iteration-by-iteration progress.
Thread: "Pulse: Fix auth token refresh"
├── [pulse] Session started (max 10 iterations)
├── [pulse:iter-1] Read auth code, identified stale token path
├── [pulse:iter-2] Implemented token refresh logic
├── [pulse:iter-3] Tests passing, committing
└── [pulse] ✅ COMPLETE — 3 iterations, 4 files modifiedProperties: pulse_session_id, iteration count, signal (CONTINUE/COMPLETE/BLOCKED), files modified, commit hashes.
### 2.4 Dispatch Threads (task lifecycle)
MAC task dispatch with full lifecycle tracking.
Thread: "Task: Build iOS widget"
├── [dispatch] Task created → assigned to mac1-claude
├── [agent:claude] Starting task...
├── [agent:claude] Widget implemented, building...
├── [system] Build succeeded (exit code 0)
└── [dispatch] ✅ Task completed — quality score 0.92Properties: mac_task_id, device, agent, status (pending→running→complete/failed), quality score, reputation impact.
### 2.5 Alert Threads (service health)
Auto-created when infra_watchdog or mac_heartbeat detects issues.
Thread: "🔴 Alert: Mac4 unreachable"
├── [watchdog] Mac4 unreachable for 2 hours
├── [watchdog] Auto-restart attempted: Graph Kernel
├── [watchdog] Mac4 back online
└── [system] Alert resolved — downtime: 2h 14mProperties: severity (info/warning/critical), auto-resolve when condition clears, escalation after 12h.
---
3. Data Model (Supabase)
3.1 Tables
-- Core threading
CREATE TABLE hub_threads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type TEXT NOT NULL CHECK (type IN ('conversation', 'feed', 'pulse', 'dispatch', 'alert')),
category TEXT NOT NULL, -- 'agent', 'heartbeat', 'pulse-control', etc.
title TEXT NOT NULL,
subtitle TEXT, -- e.g., flow name, agent name
created_by UUID REFERENCES auth.users(id),
agent_id TEXT, -- assigned agent (for conversation/dispatch)
model TEXT, -- claude/gemini/ollama
session_key TEXT, -- gateway session key
pulse_session_id TEXT, -- for pulse threads
mac_task_id UUID, -- for dispatch threads
severity TEXT, -- for alert threads
is_resolved BOOLEAN DEFAULT false,
is_pinned BOOLEAN DEFAULT false,
is_muted BOOLEAN DEFAULT false,
last_message_at TIMESTAMPTZ,
message_count INTEGER DEFAULT 0,
unread_count INTEGER DEFAULT 0, -- per-user (computed client-side)
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE hub_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
thread_id UUID NOT NULL REFERENCES hub_threads(id) ON DELETE CASCADE,
sender_type TEXT NOT NULL CHECK (sender_type IN ('human', 'agent', 'system', 'flow', 'pulse', 'dispatch')),
sender_id TEXT, -- user ID, agent name, flow name
sender_label TEXT, -- display name
content TEXT NOT NULL,
content_type TEXT DEFAULT 'text' CHECK (content_type IN ('text', 'markdown', 'embed', 'code', 'image', 'voice_note', 'system')),
embed_data JSONB, -- for rich embeds (like Discord webhook payloads)
reply_to UUID REFERENCES hub_messages(id),
iteration_number INTEGER, -- for pulse messages
is_streaming BOOLEAN DEFAULT false,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE hub_reactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id UUID NOT NULL REFERENCES hub_messages(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id),
emoji TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(message_id, user_id, emoji)
);
CREATE TABLE hub_thread_reads (
thread_id UUID NOT NULL REFERENCES hub_threads(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id),
last_read_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (thread_id, user_id)
);
CREATE TABLE hub_bookmarks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id),
thread_id UUID REFERENCES hub_threads(id),
message_id UUID REFERENCES hub_messages(id),
note TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Indexes
CREATE INDEX idx_hub_messages_thread ON hub_messages(thread_id, created_at);
CREATE INDEX idx_hub_threads_category ON hub_threads(category, last_message_at DESC);
CREATE INDEX idx_hub_threads_type ON hub_threads(type, last_message_at DESC);
CREATE INDEX idx_hub_messages_sender ON hub_messages(sender_type, sender_id);3.2 Supabase Realtime Subscriptions
Channel: hub_messages — INSERT events → live message delivery
Channel: hub_threads — UPDATE events → unread count, last_message_at
Filter: thread_id = {current_thread} — only subscribe to visible threads3.3 RLS Policies
-- All authenticated users can read all threads and messages
-- (OpenClaw Hub is single-user, but RLS keeps data organized)
ALTER TABLE hub_threads ENABLE ROW LEVEL SECURITY;
ALTER TABLE hub_messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "read_all_threads" ON hub_threads FOR SELECT TO authenticated USING (true);
CREATE POLICY "insert_threads" ON hub_threads FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "update_own_threads" ON hub_threads FOR UPDATE TO authenticated USING (true);
CREATE POLICY "read_all_messages" ON hub_messages FOR SELECT TO authenticated USING (true);
CREATE POLICY "insert_messages" ON hub_messages FOR INSERT TO authenticated WITH CHECK (true);---
4. Thread Categories (replacing Discord channels)
Active Categories (mapped from Discord)
| Category | Thread Types | Source | Discord Equivalent |
|---|---|---|---|
| `agent` | conversation | Human-initiated | #bridge, #quick |
| `comp-core` | conversation | Project-scoped | #cc-runtime, #cc-semantic, etc. |
| `pulse-control` | pulse | Pulse sessions | #pulse-feed, #pulse-control |
| `heartbeat` | feed | heartbeat_pulse | #heartbeat |
| `service-health` | feed, alert | infra_watchdog | #service-health |
| `node-health` | feed, alert | mac_heartbeat | (new) |
| `morning-brief` | feed | morning_brief | #morning-brief |
| `memory-log` | feed | memory_summarizer | #memory-log |
| `chronicles` | feed | dream/creative_chronicle | #chronicles |
| `blooms` | feed | garden_tender | #blooms |
| `weekly-review` | feed | weekly_capacity_report | #weekly-review |
| `dispatch` | dispatch | MAC task system | #task-dispatch |
| `serenity` | feed | SS pipeline flows | #ss-content-signals |
| `research` | conversation | Open-ended | #research, #workshop |
---
5. UI Architecture (TCA)
5.1 Reducer Tree
AppReducer
├── AuthFeature
├── ThreadListFeature ← NEW: replaces ChannelView
│ ├── State: threads, filter, selectedThread
│ ├── Action: loadThreads, selectThread, createThread, muteThread
│ └── Dependency: HubClient (Supabase)
├── ThreadDetailFeature ← NEW: message view + compose
│ ├── State: messages, isStreaming, compose
│ ├── Action: sendMessage, loadMore, react, bookmark
│ └── Dependencies: HubClient, ClawdbotBridge, GatewayWebSocketClient
├── QuadViewFeature ← NEW: 4 concurrent threads
│ ├── State: quadrants[4], layout, focusedQuadrant
│ ├── Action: assignThread, swapQuadrant, toggleFocus
│ └── Child: ThreadDetailFeature × 4
├── FeedAggregatorFeature ← NEW: unified feed timeline
│ ├── State: feeds, filters, timeline
│ ├── Action: loadFeeds, filterByCategory, markRead
│ └── Dependency: HubClient
├── FleetOverviewFeature (existing from ACC)
├── PulseChainFeature (existing from ACC)
├── ServiceHealthFeature ← NEW: from infra_watchdog data
├── SettingsFeature (existing from ACC)
└── SearchEverythingFeature (existing from ACC, updated for threads)5.2 Tab Structure
┌─────────────────────────────────────────────────┐
│ [Threads] [Quad] [Feeds] [Fleet] [Settings]│
└─────────────────────────────────────────────────┘| Tab | Feature | Description |
|---|---|---|
| Threads | ThreadListFeature | All threads, filtered by category, search |
| Quad | QuadViewFeature | 4 concurrent thread views (inspired by terminal quad) |
| Feeds | FeedAggregatorFeature | Unified timeline of all feed/alert threads |
| Fleet | FleetOverviewFeature + ServiceHealthFeature | Agent fleet, service health, node status |
| Settings | SettingsFeature | Gateway config, notifications, voice, appearance |
5.3 Quad View Layout
┌───────────────────┬───────────────────┐
│ Thread A │ Thread B │
│ (conversation) │ (heartbeat feed) │
│ │ │
├───────────────────┼───────────────────┤
│ Thread C │ Thread D │
│ (pulse session) │ (dispatch) │
│ │ │
└───────────────────┴───────────────────┘- Tap a quadrant to focus (expand to full width)
- Double-tap to go full-screen on that thread
- Swipe to replace a quadrant with a different thread
- Long-press for thread actions (mute, pin, bookmark)
---
6. Feed Integration (replacing Discord webhooks)
6.1 Dual-Post Adapter
During migration, flows post to BOTH Discord and Supabase:
# webhook_poster.py — add Supabase posting
async def post_to_hub(thread_category: str, content: str, embed_data: dict = None):
"""Post to Supabase hub_messages (new path)."""
supabase = get_supabase_client()
# Find or create today's feed thread
thread = await find_or_create_feed_thread(supabase, thread_category)
# Insert message
await supabase.table('hub_messages').insert({
'thread_id': thread['id'],
'sender_type': 'flow',
'sender_id': f'flow:{thread_category}',
'sender_label': thread_category.replace('-', ' ').title(),
'content': content,
'content_type': 'embed' if embed_data else 'markdown',
'embed_data': embed_data,
}).execute()
async def post_to_webhook(channel: str, embeds: list, ...):
"""Existing Discord webhook posting."""
# ... existing code ...
# Also post to hub (dual-post)
for embed in embeds:
await post_to_hub(channel, format_embed_as_markdown(embed), embed)6.2 Embed Rendering
Discord embeds → SwiftUI views:
struct EmbedView: View {
let embed: EmbedData // title, description, color, fields, footer, timestamp
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Color bar on left edge (like Discord)
HStack(spacing: 0) {
Rectangle()
.fill(Color(hex: embed.color))
.frame(width: 3)
VStack(alignment: .leading, spacing: 6) {
if let title = embed.title {
Text(title).font(.subheadline).bold()
}
if let desc = embed.description {
Text(desc).font(.caption).foregroundStyle(.secondary)
}
// Fields grid
LazyVGrid(columns: embed.fields.map { _ in GridItem() }) {
ForEach(embed.fields) { field in
VStack(alignment: .leading) {
Text(field.name).font(.caption2).foregroundStyle(.tertiary)
Text(field.value).font(.caption).bold()
}
}
}
// Footer
if let footer = embed.footer {
Text(footer).font(.caption2).foregroundStyle(.tertiary)
}
}
.padding(10)
}
.background(.ultraThinMaterial)
.cornerRadius(8)
}
}
}---
7. Message Types & Rendering
| content_type | Rendering | Example |
|---|---|---|
| `text` | Plain text | User messages |
| `markdown` | Markdown with syntax highlighting | Agent responses |
| `code` | Syntax-highlighted code block | Code generation |
| `embed` | Rich embed card (Discord-style) | Feed posts |
| `image` | Async image with preview | Visual queries |
| `voice_note` | Audio waveform player | Voice input |
| `system` | Muted system message | Thread lifecycle events |
---
8. Notification Routing
| Thread Type | Push Notification | Badge | Sound |
|---|---|---|---|
| Conversation (active) | Yes — agent responses | Yes | Default |
| Feed | No (passive) | Counter only | None |
| Pulse | Only on COMPLETE/BLOCKED | Yes | Chime |
| Dispatch | On complete/failed | Yes | Default |
| Alert (critical) | Yes — immediate | Yes | Alarm |
| Alert (warning) | Only if unresolved >1h | Yes | None |
---
9. Migration Plan
### Phase A: Dual-Post (HUB-4)
- Add `post_to_hub()` to webhook_poster.py
- All 33 flows post to both Discord and Supabase
- iOS app shows Supabase feed threads
- Discord continues working unchanged
### Phase B: Agent Wiring (HUB-5)
- Conversation threads connect to Clawdbot gateway
- Dispatch threads create MAC tasks
- Pulse threads track sessions
### Phase C: Feature Parity
- All feed categories populated
- Quad view working
- Search across all threads
- Voice input/output
### Phase D: Discord Sunset
- Disable Discord webhook posting
- Keep Discord for external community only
- All operational comms through Hub
---
10. Key Technical Decisions
1. Supabase Realtime over custom WS: Eliminates building a custom pub/sub. Supabase Realtime is battle-tested and already in the stack.
2. Daily feed threads: Each feed category creates one thread per day (e.g., "#heartbeat — 2026-03-01"). This prevents thread explosion while keeping history browsable.
3. Quad view over tab switching: The terminal quad pattern is proven for concurrent context management. iOS 18's large screens (16 Pro Max) support 4-up layout.
4. TCA for state management: Consistent with ACC architecture decision. Composable reducers make the quad view natural (4 instances of ThreadDetailFeature).
5. SwiftData for offline: Messages cached locally via SwiftData. Sync on reconnect via Supabase Realtime replay.
6. Embed rendering, not embed generation: The Hub renders existing Discord embed payloads as SwiftUI views. No need to change embed format — just add a parallel Supabase posting path.
Promotion Decision
Promote into a technical note or architecture paper with implementation anchors.
Source Anchor
AgentCommandCenter/HUB-THREAD-ARCHITECTURE.md
Detected Structure
Method · Evaluation · References · Code Anchors · Architecture