Grand Diomande Research · Full HTML Reader

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

Embodied Trajectory Systems architecture technical paper candidate score 62 .md

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 modified

Properties: 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.92

Properties: 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 14m

Properties: severity (info/warning/critical), auto-resolve when condition clears, escalation after 12h.

---

3. Data Model (Supabase)

3.1 Tables

sql
-- 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 threads

3.3 RLS Policies

sql
-- 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)

CategoryThread TypesSourceDiscord Equivalent
`agent`conversationHuman-initiated#bridge, #quick
`comp-core`conversationProject-scoped#cc-runtime, #cc-semantic, etc.
`pulse-control`pulsePulse sessions#pulse-feed, #pulse-control
`heartbeat`feedheartbeat_pulse#heartbeat
`service-health`feed, alertinfra_watchdog#service-health
`node-health`feed, alertmac_heartbeat(new)
`morning-brief`feedmorning_brief#morning-brief
`memory-log`feedmemory_summarizer#memory-log
`chronicles`feeddream/creative_chronicle#chronicles
`blooms`feedgarden_tender#blooms
`weekly-review`feedweekly_capacity_report#weekly-review
`dispatch`dispatchMAC task system#task-dispatch
`serenity`feedSS pipeline flows#ss-content-signals
`research`conversationOpen-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]│
└─────────────────────────────────────────────────┘
TabFeatureDescription
ThreadsThreadListFeatureAll threads, filtered by category, search
QuadQuadViewFeature4 concurrent thread views (inspired by terminal quad)
FeedsFeedAggregatorFeatureUnified timeline of all feed/alert threads
FleetFleetOverviewFeature + ServiceHealthFeatureAgent fleet, service health, node status
SettingsSettingsFeatureGateway 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:

python
# 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:

swift
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_typeRenderingExample
`text`Plain textUser messages
`markdown`Markdown with syntax highlightingAgent responses
`code`Syntax-highlighted code blockCode generation
`embed`Rich embed card (Discord-style)Feed posts
`image`Async image with previewVisual queries
`voice_note`Audio waveform playerVoice input
`system`Muted system messageThread lifecycle events

---

8. Notification Routing

Thread TypePush NotificationBadgeSound
Conversation (active)Yes — agent responsesYesDefault
FeedNo (passive)Counter onlyNone
PulseOnly on COMPLETE/BLOCKEDYesChime
DispatchOn complete/failedYesDefault
Alert (critical)Yes — immediateYesAlarm
Alert (warning)Only if unresolved >1hYesNone

---

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