Server Architecture Integration — Aura
Aura currently has a flat thread model: `HubThread` objects live in `hub_threads`, categorized by `ThreadCategory` and `ThreadType`, with no parent container. The Discord ecosystem, however, operates on three distinct architectural patterns — each representing an evolution in how the Clawdbot gateway handles task dispatch and message delivery. This document describes those three patterns in abstract form and specifies how they integrate into Aura as a **secluded feature** that does not interfere with the existing t
Full Public Reader
Server Architecture Integration — Aura
Context
Aura currently has a flat thread model: `HubThread` objects live in `hub_threads`, categorized by `ThreadCategory` and `ThreadType`, with no parent container. The Discord ecosystem, however, operates on three distinct architectural patterns — each representing an evolution in how the Clawdbot gateway handles task dispatch and message delivery. This document describes those three patterns in abstract form and specifies how they integrate into Aura as a secluded feature that does not interfere with the existing thread hierarchy.
---
The Three Server Architectures
Architecture 1: Command Dispatch (Koji Pattern)
Abstract model: A stateless request-response loop. The client sends a named command with arguments. The server executes a direct database query, formats the result, and returns it immediately. No task queue, no progress streaming, no persistent connection.
Execution flow:
Client sends command → Server parses command name + args
→ Server executes database query (direct, synchronous)
→ Server formats result as text
→ Server returns result to client
→ Transaction complete. No state retained.Characteristics:
- Commands are a fixed set: each maps to exactly one query/action
- No model routing — the server IS the executor
- No progress updates — result arrives in one shot
- Stateless — each command is independent, no session continuity
- Response format: plain text, chunked if exceeding size limits
Data requirements:
- Command registry (name → handler function)
- Direct database access (reads from domain-specific tables)
- No task table, no thread table, no message history
When to use: Domain-specific dashboards where you need instant answers from structured data. Sales pipeline queries, inventory lookups, metric summaries.
---
Architecture 2: Flat Task Pipeline (Buf Barista Pattern)
Abstract model: A persistent connection receives messages, classifies them, creates tasks in a queue, dispatches to mesh executors, and streams progress back into the originating channel as flat sequential messages.
Execution flow:
Client sends message → Server classifies task type and model affinity
→ Server creates task record in queue (status: pending)
→ Mesh executor claims task (status: running)
→ Server polls task record every 2 seconds
→ On partial_output change → post progress message to client
→ On status=complete → post final result to client
→ On status=failed → post error to clientCharacteristics:
- Intelligent model routing: content analysis determines Claude vs Gemini vs Codex
- Platform affinity: Swift/Xcode tasks route only to macOS executors
- Progress streaming: up to 15 incremental updates per task
- Team decomposition: single request splits into parallel subtasks, results aggregated
- Rate limit awareness: automatic fallback when primary model is throttled
- Channel-to-project mapping: message origin determines working directory
- All messages (progress, results) appear sequentially in the same channel — no spatial isolation
Data requirements:
- Task queue table (`mac_tasks`): content, project, status, model_preference, claimed_by, partial_output, result
- Channel configuration: maps channels to project paths and model preferences
- Mesh device registry: tracks online status, available models, rate limits
- Message history in the channel (flat, chronological)
When to use: Active development workflows where you want to watch work happen in real time. Quick iterations, debugging sessions, single-operator workflows.
---
Architecture 3: Threaded Task Isolation (Cloud Hub Pattern)
Abstract model: Same task pipeline as Architecture 2, but each task spawns a dedicated conversational thread. All progress and results are posted into that thread, not the parent channel. Threads have a managed lifecycle with automatic archival.
Execution flow:
Client sends message → Server classifies and creates task (same as Arch 2)
→ Server spawns a dedicated thread for this task
→ Thread record created in database (status: active)
→ Mesh executor claims task
→ Server polls and posts progress INTO the thread
→ On completion → post color-coded result embed in thread
→ Thread enters decay lifecycle:
ACTIVE (receiving messages)
→ 2 hours idle → IDLE (warning posted)
→ 1 hour idle → DECAYING
→ next cycle → ARCHIVED (summary posted, thread closed)
→ On server restart → recover orphaned tasks, re-deliver missed resultsCharacteristics:
- Everything from Architecture 2, plus:
- Spatial isolation: each task has its own conversation space
- Rich formatting: color-coded embeds (green=success, red=failure, blue=decomposed, yellow=synthesizing)
- Lifecycle management: decay loop runs every 60 seconds, auto-archives stale threads
- Crash recovery: on startup, scans for completed tasks with undelivered results
- Thread tracking table: records thread state, message count, last activity
- Parent channel stays clean — only the initial "Task queued" message appears there
Data requirements:
- Everything from Architecture 2, plus:
- Thread tracking table (`discord_threads`): thread_id, parent_channel, task_id, status, message_count, last_activity, archived_at
- Decay configuration: active→idle timeout, idle→archive timeout, check interval
- Embed schema: title, description, color, fields[], footer, timestamp
When to use: Multi-operator environments, high-volume task dispatch, infrastructure operations where you need to track many concurrent tasks without channel noise.
---
Integration into Aura
Design Principle: Secluded Section
These three architectures live in a dedicated feature area that does NOT touch the existing thread hierarchy. The current `ThreadListFeature` → `ThreadDetailFeature` → `AgentChatFeature` chain remains untouched. The server architecture feature is a peer, not a parent.
Where It Lives in the Tab Hierarchy
Existing tabs (unchanged):
Threads | Quad | Feeds | Dispatch | Fleet | Panes | Content | Timeline | Settings
New addition:
Threads tab gets a segmented control at the top:
[Conversations] [Servers]
"Conversations" = current thread list (default, unchanged)
"Servers" = new server architecture sectionThis keeps the feature discoverable without adding a new tab or disrupting the thread list.
Navigation Hierarchy
Threads Tab
├─ Segment: Conversations (existing ThreadListFeature — no changes)
│ ├─ Thread A (conversation) → AgentChatView
│ ├─ Thread B (feed) → ThreadDetailView
│ └─ Thread C (dispatch) → ThreadDetailView
│
└─ Segment: Servers (new ServerListFeature)
├─ Server: Koji Pipeline (Architecture 1)
│ ├─ #leads → CommandChannelView
│ ├─ #accounts → CommandChannelView
│ └─ #sweeps → CommandChannelView
│
├─ Server: Buf Barista (Architecture 2)
│ ├─ #bridge → FlatTaskChannelView
│ ├─ #cc-semantic → FlatTaskChannelView
│ ├─ #workshop → FlatTaskChannelView
│ └─ #research → FlatTaskChannelView
│
└─ Server: Cloud Hub (Architecture 3)
├─ #dispatch → ThreadedTaskChannelView
│ ├─ Task Thread: "a1b2 — Build REST API" → TaskThreadView
│ ├─ Task Thread: "c3d4 — Fix auth bug" → TaskThreadView
│ └─ Task Thread: "e5f6 — Refactor DB" [archived]
├─ #pulse-control → ThreadedTaskChannelView
└─ #infrastructure → ThreadedTaskChannelViewData Models
HubServer
struct HubServer: Codable, Identifiable, Equatable, Sendable {
let id: UUID
let name: String // "koji-pipeline", "buf-barista", "cloud-hub"
var displayName: String // "Koji Pipeline", "Buf Barista", "Cloud Hub"
var description: String? // "Sales pipeline command center"
var icon: String // SF Symbol name
var architectureType: ServerArchitecture // .commandDispatch, .flatTaskPipeline, .threadedTaskIsolation
var isDefault: Bool // Pre-created servers are default
var channelCount: Int
var metadata: [String: String]?
let createdAt: Date
var updatedAt: Date
}
enum ServerArchitecture: String, Codable, Sendable {
case commandDispatch // Architecture 1 (Koji)
case flatTaskPipeline // Architecture 2 (Buf Barista)
case threadedTaskIsolation // Architecture 3 (Cloud Hub)
}HubChannel
struct HubChannel: Codable, Identifiable, Equatable, Sendable {
let id: UUID
let serverId: UUID // FK to HubServer
let name: String // "leads", "bridge", "dispatch"
var displayName: String // "#leads", "#bridge", "#dispatch"
var description: String?
var categoryGroup: String? // "sales", "comp-core", "ops" — visual grouping
var projectPath: String? // Working directory for task execution
var modelPreference: ModelPreference // .claude, .gemini, .any, .codex
var isReadOnly: Bool // Feed channels
var messageCount: Int
var lastMessageAt: Date?
var metadata: [String: String]?
let createdAt: Date
}
enum ModelPreference: String, Codable, Sendable {
case claude, gemini, codex, any
}ServerMessage
Reuse the existing `HubMessage` model. The `threadId` field links to a `HubChannel.id` (channels are conceptually threads in Supabase). No new message model needed.
However, for Architecture 3's task threads, we need:
TaskThread
struct TaskThread: Codable, Identifiable, Equatable, Sendable {
let id: UUID
let channelId: UUID // Parent channel
let taskId: UUID? // Linked mac_task
let teamId: UUID? // Linked team task
var name: String // "a1b2c3d4 — Build REST API..."
var status: TaskThreadStatus
var messageCount: Int
var lastActivityAt: Date?
var archivedAt: Date?
var metadata: [String: String]?
let createdAt: Date
}
enum TaskThreadStatus: String, Codable, Sendable {
case active, idle, decaying, archived
}Supabase Tables
-- New tables (do not modify existing hub_threads / hub_messages)
CREATE TABLE hub_servers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
description TEXT,
icon TEXT NOT NULL DEFAULT 'server.rack',
architecture_type TEXT NOT NULL CHECK (architecture_type IN ('command_dispatch', 'flat_task_pipeline', 'threaded_task_isolation')),
is_default BOOLEAN DEFAULT false,
channel_count INTEGER DEFAULT 0,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE hub_channels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
server_id UUID NOT NULL REFERENCES hub_servers(id) ON DELETE CASCADE,
name TEXT NOT NULL,
display_name TEXT NOT NULL,
description TEXT,
category_group TEXT,
project_path TEXT,
model_preference TEXT DEFAULT 'any',
is_read_only BOOLEAN DEFAULT false,
message_count INTEGER DEFAULT 0,
last_message_at TIMESTAMPTZ,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (server_id, name)
);
CREATE TABLE hub_channel_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID NOT NULL REFERENCES hub_channels(id) ON DELETE CASCADE,
task_thread_id UUID REFERENCES hub_task_threads(id), -- NULL for flat messages
sender_type TEXT NOT NULL,
sender_id TEXT,
sender_label TEXT,
content TEXT NOT NULL,
content_type TEXT DEFAULT 'text',
embed_data JSONB,
reply_to UUID,
is_progress BOOLEAN DEFAULT false, -- Progress update vs final message
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE hub_task_threads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID NOT NULL REFERENCES hub_channels(id) ON DELETE CASCADE,
task_id UUID REFERENCES mac_tasks(id),
team_id UUID,
name TEXT NOT NULL,
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'idle', 'decaying', 'archived')),
message_count INTEGER DEFAULT 0,
last_activity_at TIMESTAMPTZ DEFAULT now(),
archived_at TIMESTAMPTZ,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Indexes
CREATE INDEX idx_hub_channels_server ON hub_channels(server_id);
CREATE INDEX idx_hub_channel_messages_channel ON hub_channel_messages(channel_id);
CREATE INDEX idx_hub_channel_messages_thread ON hub_channel_messages(task_thread_id);
CREATE INDEX idx_hub_task_threads_channel ON hub_task_threads(channel_id);
CREATE INDEX idx_hub_task_threads_status ON hub_task_threads(status);
-- RLS (basic — lock to authenticated users)
ALTER TABLE hub_servers ENABLE ROW LEVEL SECURITY;
ALTER TABLE hub_channels ENABLE ROW LEVEL SECURITY;
ALTER TABLE hub_channel_messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE hub_task_threads ENABLE ROW LEVEL SECURITY;
CREATE POLICY "authenticated_read_servers" ON hub_servers FOR SELECT TO authenticated USING (true);
CREATE POLICY "authenticated_read_channels" ON hub_channels FOR SELECT TO authenticated USING (true);
CREATE POLICY "authenticated_read_messages" ON hub_channel_messages FOR SELECT TO authenticated USING (true);
CREATE POLICY "authenticated_insert_messages" ON hub_channel_messages FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "authenticated_read_task_threads" ON hub_task_threads FOR SELECT TO authenticated USING (true);TCA Feature Reducers
ServerListFeature (top-level for the "Servers" segment)
@Reducer
struct ServerListFeature {
@ObservableState
struct State: Equatable, Sendable {
var servers: [HubServer] = []
var isLoading = false
@Presents var selectedServer: ServerDetailFeature.State?
}
enum Action: Sendable {
case onAppear
case serversLoaded([HubServer])
case selectServer(HubServer)
case serverDetail(PresentationAction<ServerDetailFeature.Action>)
}
}ServerDetailFeature (channel list for a server)
@Reducer
struct ServerDetailFeature {
@ObservableState
struct State: Equatable, Sendable {
var server: HubServer
var channels: [HubChannel] = []
var groupedChannels: [String: [HubChannel]] {
Dictionary(grouping: channels, by: { $0.categoryGroup ?? "General" })
}
var isLoading = false
@Presents var selectedChannel: ChannelFeature.State?
}
enum Action: Sendable {
case onAppear
case channelsLoaded([HubChannel])
case selectChannel(HubChannel)
case channel(PresentationAction<ChannelFeature.Action>)
}
}ChannelFeature (behavior depends on server architecture)
This is the key reducer — it adapts its behavior based on the parent server's `architectureType`:
@Reducer
struct ChannelFeature {
@ObservableState
struct State: Equatable, Sendable {
var channel: HubChannel
var architectureType: ServerArchitecture // Inherited from parent server
var messages: [HubChannelMessage] = []
var taskThreads: [TaskThread] = [] // Only populated for .threadedTaskIsolation
var composeText = ""
var isLoading = false
var isExecuting = false // Command in progress (Arch 1)
var isPolling = false // Task polling active (Arch 2 & 3)
@Presents var selectedTaskThread: TaskThreadFeature.State?
}
enum Action: Sendable {
// Common
case onAppear
case messagesLoaded([HubChannelMessage])
case compose(String)
case send
// Architecture 1: Command Dispatch
case commandResult(String)
case commandError(String)
// Architecture 2: Flat Task Pipeline
case taskCreated(MACTask)
case progressUpdate(String)
case taskCompleted(MACTask)
case taskFailed(MACTask, String)
// Architecture 3: Threaded Task Isolation
case taskThreadsLoaded([TaskThread])
case taskThreadCreated(TaskThread)
case selectTaskThread(TaskThread)
case taskThread(PresentationAction<TaskThreadFeature.Action>)
case threadDecayTick // 60-second lifecycle check
// Realtime
case newMessageReceived(HubChannelMessage)
}
}Reducer behavior per architecture:
case .send:
let text = state.composeText
state.composeText = ""
switch state.architectureType {
case .commandDispatch:
// Architecture 1: Parse command, execute directly, return result
state.isExecuting = true
return .run { send in
let result = try await serverClient.executeCommand(channel.id, text)
await send(.commandResult(result))
}
case .flatTaskPipeline:
// Architecture 2: Create task, start polling, stream progress as flat messages
return .run { send in
let message = HubChannelMessage(content: text, senderType: .human, ...)
try await serverClient.postChannelMessage(channel.id, message)
let task = try await infraClient.createMACTask(text, channel.projectPath, ...)
await send(.taskCreated(task))
// Start polling loop
for await update in serverClient.pollTaskProgress(task.id) {
await send(.progressUpdate(update))
}
}
case .threadedTaskIsolation:
// Architecture 3: Create task, spawn thread, stream progress into thread
return .run { send in
let message = HubChannelMessage(content: text, senderType: .human, ...)
try await serverClient.postChannelMessage(channel.id, message)
let task = try await infraClient.createMACTask(text, channel.projectPath, ...)
let thread = try await serverClient.createTaskThread(channel.id, task)
await send(.taskThreadCreated(thread))
// Polling happens inside TaskThreadFeature
}
}TaskThreadFeature (Architecture 3 only)
@Reducer
struct TaskThreadFeature {
@ObservableState
struct State: Equatable, Sendable {
var taskThread: TaskThread
var messages: [HubChannelMessage] = []
var task: MACTask?
var isPolling = false
}
enum Action: Sendable {
case onAppear
case messagesLoaded([HubChannelMessage])
case startPolling
case progressReceived(String)
case taskCompleted(MACTask)
case taskFailed(MACTask, String)
case newMessageReceived(HubChannelMessage)
}
}ServerClient (New Dependency)
struct ServerClient: Sendable {
// Server & channel CRUD
var fetchServers: @Sendable () async throws -> [HubServer]
var fetchChannels: @Sendable (_ serverId: UUID) async throws -> [HubChannel]
var fetchChannelMessages: @Sendable (_ channelId: UUID, _ limit: Int, _ before: Date?) async throws -> [HubChannelMessage]
// Architecture 1: Command dispatch
var executeCommand: @Sendable (_ channelId: UUID, _ command: String) async throws -> String
// Architecture 2 & 3: Task pipeline
var postChannelMessage: @Sendable (_ channelId: UUID, _ message: HubChannelMessage) async throws -> HubChannelMessage
var pollTaskProgress: @Sendable (_ taskId: UUID) -> AsyncStream<String>
// Architecture 3: Thread management
var fetchTaskThreads: @Sendable (_ channelId: UUID) async throws -> [TaskThread]
var createTaskThread: @Sendable (_ channelId: UUID, _ task: MACTask) async throws -> TaskThread
var updateThreadStatus: @Sendable (_ threadId: UUID, _ status: TaskThreadStatus) async throws -> Void
var fetchThreadMessages: @Sendable (_ threadId: UUID, _ limit: Int) async throws -> [HubChannelMessage]
// Realtime
var subscribeToChannelMessages: @Sendable (_ channelId: UUID) -> AsyncStream<HubChannelMessage>
var subscribeToTaskThreadUpdates: @Sendable (_ channelId: UUID) -> AsyncStream<TaskThread>
}Views
ServerListView
┌──────────────────────────────────┐
│ Servers │
├──────────────────────────────────┤
│ ┌────┐ │
│ │ 🔧 │ Koji Pipeline │
│ │ │ Command dispatch │
│ │ │ 3 channels │
│ └────┘ │
│ ┌────┐ │
│ │ ☕ │ Buf Barista │
│ │ │ Flat task pipeline │
│ │ │ 15 channels │
│ └────┘ │
│ ┌────┐ │
│ │ ☁️ │ Cloud Hub │
│ │ │ Threaded task isolation │
│ │ │ 8 channels │
│ └────┘ │
└──────────────────────────────────┘ServerDetailView (channel list with category groups)
┌──────────────────────────────────┐
│ ← Buf Barista │
│ Flat task pipeline │
├──────────────────────────────────┤
│ HUB │
│ # bridge │
│ # quick │
│ │
│ COMP-CORE │
│ # cc-runtime │
│ # cc-semantic │
│ # cc-retrieval │
│ # cc-motion │
│ # cc-agents │
│ # cc-media │
│ │
│ CREATIVE │
│ # workshop │
│ # research │
│ # garden │
└──────────────────────────────────┘CommandChannelView (Architecture 1)
┌──────────────────────────────────┐
│ ← # leads │
│ Koji Pipeline · Command Dispatch│
├──────────────────────────────────┤
│ │
│ > !stats │
│ ┌─────────────────────────────┐ │
│ │ Pipeline: 23 active leads │ │
│ │ Hot: 8 | Warm: 12 | Cold: 3│ │
│ │ Revenue MTD: $4,200 │ │
│ └─────────────────────────────┘ │
│ │
│ > !hot brooklyn │
│ ┌─────────────────────────────┐ │
│ │ 1. Bakeri (★4.8) - sample │ │
│ │ 2. Laurel (★4.6) - outreach │ │
│ │ 3. ACRE (★4.5) - follow_up │ │
│ └─────────────────────────────┘ │
│ │
├──────────────────────────────────┤
│ !_________________________ ⏎ │
└──────────────────────────────────┘Note the `!` prefix in the compose field — visual hint that this is command mode.
FlatTaskChannelView (Architecture 2)
┌──────────────────────────────────┐
│ ← # cc-semantic │
│ Buf Barista · Flat Pipeline │
├──────────────────────────────────┤
│ │
│ Mohamed │
│ Fix the graph kernel cache │
│ invalidation bug │
│ │
│ ⏳ Task a1b2 queued │
│ Model: claude | Project: │
│ cc-graph-kernel │
│ │
│ 📋 a1b2: Analyzing cache.rs... │
│ 📋 a1b2: Found stale TTL logic │
│ 📋 a1b2: Patching invalidation │
│ │
│ ✅ a1b2 complete [claude/mac1] │
│ Fixed cache invalidation by │
│ switching from fixed TTL to │
│ LRU eviction with 512 entry... │
│ │
├──────────────────────────────────┤
│ ___________________________ ⏎ │
└──────────────────────────────────┘Sequential messages in the channel. Progress updates inline. No threading.
ThreadedTaskChannelView (Architecture 3)
┌──────────────────────────────────┐
│ ← # dispatch │
│ Cloud Hub · Threaded Isolation │
├──────────────────────────────────┤
│ │
│ Active Threads │
│ ┌─────────────────────────────┐ │
│ │ 🟢 a1b2 — Build REST API │ │
│ │ 3 messages · 2m ago │ │
│ ├─────────────────────────────┤ │
│ │ 🟢 c3d4 — Fix auth bug │ │
│ │ 7 messages · 5m ago │ │
│ ├─────────────────────────────┤ │
│ │ 🟡 e5f6 — Refactor DB │ │
│ │ idle · 2h ago │ │
│ └─────────────────────────────┘ │
│ │
│ Archived │
│ ┌─────────────────────────────┐ │
│ │ ⚫ g7h8 — Update configs │ │
│ │ archived · yesterday │ │
│ └─────────────────────────────┘ │
│ │
├──────────────────────────────────┤
│ ___________________________ ⏎ │
│ New message creates a thread │
└──────────────────────────────────┘Tapping a thread opens TaskThreadView with the isolated conversation.
TaskThreadView (Architecture 3 — inside a thread)
┌──────────────────────────────────┐
│ ← a1b2 — Build REST API 🟢 │
│ dispatch · cloud-hub │
├──────────────────────────────────┤
│ │
│ Mohamed │
│ Build a REST API for the user │
│ profile service │
│ │
│ ┌─────────────────────────────┐ │
│ │ 🔵 Task Queued │ │
│ │ Model: claude │ │
│ │ Device: — (pending) │ │
│ └─────────────────────────────┘ │
│ │
│ 📋 Creating route handlers... │
│ 📋 Adding validation middleware│
│ │
│ ┌─────────────────────────────┐ │
│ │ 🟢 Task Complete │ │
│ │ Model: claude-sonnet-4-6 │ │
│ │ Device: mac1 │ │
│ │ Duration: 42s │ │
│ └─────────────────────────────┘ │
│ │
│ Created 4 files: │
│ - src/routes/profile.ts │
│ - src/middleware/validate.ts │
│ - src/models/user.ts │
│ - tests/profile.test.ts │
│ │
├──────────────────────────────────┤
│ ___________________________ ⏎ │
│ Reply continues in this thread │
└──────────────────────────────────┘---
Integration Points with Existing Features
What Does NOT Change
| Component | Status |
|---|---|
| `ThreadListFeature` | Untouched — conversations, feeds, alerts continue as-is |
| `ThreadDetailFeature` | Untouched — thread detail view unchanged |
| `AgentChatFeature` | Untouched — conversation streaming unchanged |
| `HubThread` model | Untouched — no new fields |
| `HubMessage` model | Untouched — no new fields |
| `hub_threads` table | Untouched — no schema changes |
| `hub_messages` table | Untouched — no schema changes |
| `DispatchFeature` | Untouched — task creation via Dispatch tab is separate |
| `FeedAggregatorFeature` | Untouched — feed aggregation is separate |
What Connects
1. Task creation (Arch 2 & 3) reuses `infraClient.createMACTask()` — same task queue, same mesh executors
2. Model routing heuristic can be shared from `GatewayClient`'s `ModelHeuristic` logic
3. Realtime subscriptions use the same Supabase Realtime infrastructure (new channels for `hub_channel_messages` and `hub_task_threads`)
4. Embeds reuse the existing `EmbedData` struct from `HubModels.swift`
Where to Wire In
AppReducer.swift — add `serverList` to State and scope it:
// In AppReducer.State:
var serverList = ServerListFeature.State()
// In AppReducer body:
Scope(state: \.serverList, action: \.serverList) {
ServerListFeature()
}ThreadListView.swift — add segmented control at the top:
// At top of ThreadListView body:
Picker("View", selection: $viewMode) {
Text("Conversations").tag(ViewMode.conversations)
Text("Servers").tag(ViewMode.servers)
}
.pickerStyle(.segmented)
// Below picker:
switch viewMode {
case .conversations:
// existing thread list
case .servers:
ServerListView(store: store.scope(state: \.serverList, action: \.serverList))
}Alternatively, if scoping through the parent is preferred, pass the server store from AppView into the threads tab.
---
Seed Data
On first launch (or via migration), create three default servers:
[
{
"name": "koji-pipeline",
"display_name": "Koji Pipeline",
"description": "Sales pipeline command center. Direct query access to leads, accounts, prospects, and market sweeps.",
"icon": "chart.bar.xaxis",
"architecture_type": "command_dispatch",
"is_default": true,
"channels": [
{ "name": "leads", "display_name": "#leads", "category_group": "Sales", "description": "Lead queries and enrichment status" },
{ "name": "accounts", "display_name": "#accounts", "category_group": "Sales", "description": "Active account lookups" },
{ "name": "sweeps", "display_name": "#sweeps", "category_group": "Ops", "description": "Market sweep commands and status" },
{ "name": "digest", "display_name": "#digest", "category_group": "Ops", "description": "Daily pipeline summaries" }
]
},
{
"name": "buf-barista",
"display_name": "Buf Barista",
"description": "Development task pipeline with real-time progress streaming. Tasks execute on mesh devices and report back inline.",
"icon": "cup.and.saucer.fill",
"architecture_type": "flat_task_pipeline",
"is_default": true,
"channels": [
{ "name": "bridge", "display_name": "#bridge", "category_group": "Hub", "project_path": "Desktop/homelab", "model_preference": "any" },
{ "name": "quick", "display_name": "#quick", "category_group": "Hub", "project_path": "Desktop/homelab", "model_preference": "any" },
{ "name": "cc-runtime", "display_name": "#cc-runtime", "category_group": "Comp-Core", "project_path": "Desktop/Comp-Core/core/runtime", "model_preference": "claude" },
{ "name": "cc-semantic", "display_name": "#cc-semantic", "category_group": "Comp-Core", "project_path": "Desktop/Comp-Core/core/semantic", "model_preference": "claude" },
{ "name": "cc-retrieval", "display_name": "#cc-retrieval", "category_group": "Comp-Core", "project_path": "Desktop/Comp-Core/core/retrieval", "model_preference": "claude" },
{ "name": "cc-motion", "display_name": "#cc-motion", "category_group": "Comp-Core", "project_path": "Desktop/Comp-Core/core/motion", "model_preference": "claude" },
{ "name": "cc-agents", "display_name": "#cc-agents", "category_group": "Comp-Core", "project_path": "Desktop/Comp-Core/core/agents", "model_preference": "claude" },
{ "name": "cc-media", "display_name": "#cc-media", "category_group": "Comp-Core", "project_path": "Desktop/Comp-Core/core/audio-media", "model_preference": "claude" },
{ "name": "workshop", "display_name": "#workshop", "category_group": "Creative", "project_path": "Desktop/homelab", "model_preference": "any" },
{ "name": "research", "display_name": "#research", "category_group": "Creative", "project_path": "Desktop/homelab", "model_preference": "any" },
{ "name": "garden", "display_name": "#garden", "category_group": "Creative", "project_path": "Desktop/homelab", "model_preference": "any" }
]
},
{
"name": "cloud-hub",
"display_name": "Cloud Hub",
"description": "Infrastructure operations with threaded task isolation. Each task gets its own conversation thread with lifecycle management.",
"icon": "cloud.fill",
"architecture_type": "threaded_task_isolation",
"is_default": true,
"channels": [
{ "name": "dispatch", "display_name": "#dispatch", "category_group": "Ops", "project_path": "Desktop/homelab", "model_preference": "claude" },
{ "name": "pulse-control", "display_name": "#pulse-control", "category_group": "Ops", "project_path": "Desktop/homelab", "model_preference": "claude" },
{ "name": "infrastructure", "display_name": "#infrastructure", "category_group": "Ops", "project_path": "Desktop/homelab", "model_preference": "claude" },
{ "name": "morning-brief", "display_name": "#morning-brief", "category_group": "Feeds", "is_read_only": true },
{ "name": "heartbeat", "display_name": "#heartbeat", "category_group": "Feeds", "is_read_only": true },
{ "name": "memory-log", "display_name": "#memory-log", "category_group": "Feeds", "is_read_only": true }
]
}
]---
Thread Decay Implementation (Architecture 3)
The decay loop runs as a background task in `ChannelFeature` when `architectureType == .threadedTaskIsolation`:
// In ChannelFeature reducer, on .onAppear for Architecture 3:
case .onAppear where state.architectureType == .threadedTaskIsolation:
return .merge(
loadChannelMessages(),
loadTaskThreads(),
startDecayLoop() // Fires .threadDecayTick every 60 seconds
)
case .threadDecayTick:
let now = Date()
for thread in state.taskThreads where thread.status != .archived {
let idle = now.timeIntervalSince(thread.lastActivityAt ?? thread.createdAt)
switch thread.status {
case .active where idle > 7200: // 2 hours
// Transition to .idle, post warning message
case .idle where idle > 3600: // 1 more hour
// Transition to .archived, post summary
default:
break
}
}---
Command Registry (Architecture 1)
For the Koji command dispatch pattern, commands are registered as a fixed map:
struct CommandRegistry {
static let commands: [String: CommandDefinition] = [
"stats": CommandDefinition(name: "stats", description: "Pipeline summary", usage: "!stats"),
"hot": CommandDefinition(name: "hot", description: "Hot-tier prospects", usage: "!hot [market]"),
"warm": CommandDefinition(name: "warm", description: "Warm-tier prospects", usage: "!warm [market]"),
"sweep": CommandDefinition(name: "sweep", description: "Launch market sweep", usage: "!sweep <city>"),
"sweep-status": CommandDefinition(name: "sweep-status", description: "Active sweep status", usage: "!sweep-status"),
"lead": CommandDefinition(name: "lead", description: "Prospect detail", usage: "!lead <id>"),
"export": CommandDefinition(name: "export", description: "CSV export", usage: "!export [market]"),
"send-samples": CommandDefinition(name: "send-samples", description: "Top prospects for sampling", usage: "!send-samples <n>"),
"followup": CommandDefinition(name: "followup", description: "Trigger follow-ups", usage: "!followup"),
"digest": CommandDefinition(name: "digest", description: "Daily summary", usage: "!digest"),
]
}The `serverClient.executeCommand()` implementation routes through the Koji Supabase Edge Function (existing `koji-discord-bot`), adapting HTTP calls for the mobile client.
---
Files to Create
| File | Purpose |
|---|---|
| `Models/ServerModels.swift` | `HubServer`, `HubChannel`, `TaskThread`, `ServerArchitecture`, `ModelPreference`, `TaskThreadStatus`, `CommandDefinition` |
| `Services/ServerClient.swift` | `ServerClient` dependency with Supabase live implementation |
| `Features/Servers/ServerListFeature.swift` | TCA reducer for server list |
| `Features/Servers/ServerListView.swift` | Server list UI |
| `Features/Servers/ServerDetailFeature.swift` | TCA reducer for channel list within a server |
| `Features/Servers/ServerDetailView.swift` | Channel list UI with category groups |
| `Features/Servers/Channels/ChannelFeature.swift` | Polymorphic TCA reducer (adapts to architecture type) |
| `Features/Servers/Channels/CommandChannelView.swift` | Architecture 1 UI |
| `Features/Servers/Channels/FlatTaskChannelView.swift` | Architecture 2 UI |
| `Features/Servers/Channels/ThreadedTaskChannelView.swift` | Architecture 3 UI |
| `Features/Servers/Channels/TaskThreadFeature.swift` | TCA reducer for individual task threads |
| `Features/Servers/Channels/TaskThreadView.swift` | Task thread conversation UI |
Files to Modify
| File | Change |
|---|---|
| `App/AppReducer.swift` | Add `serverList` state, scope reducer |
| `Views/MainTabView.swift` or `Features/ThreadList/ThreadListView.swift` | Add segmented control for Conversations/Servers |
| `project.yml` | Add new source files to target |
---
Verification
1. Schema: Run the SQL migration. Verify tables created with `\dt hub_*` in psql.
2. Seed data: Insert three default servers. Verify with `SELECT * FROM hub_servers`.
3. Server list: Launch app → Threads tab → tap "Servers" segment → see three servers.
4. Architecture 1: Tap Koji Pipeline → #leads → type `!stats` → verify instant result.
5. Architecture 2: Tap Buf Barista → #cc-semantic → type a task → verify task queued in `mac_tasks`, progress streams as flat messages.
6. Architecture 3: Tap Cloud Hub → #dispatch → type a task → verify thread created in `hub_task_threads`, progress streams into thread, thread shows in channel's thread list.
7. Decay: Wait 2+ hours (or mock time) → verify thread transitions to idle/archived.
8. Isolation: Verify no changes to existing Conversations segment — threads, messages, agent chat all work as before.
Promotion Decision
Promote into a technical note or architecture paper with implementation anchors.
Source Anchor
OpenClawHub/SERVER-ARCHITECTURE-DESIGN.md
Detected Structure
Method · Evaluation · References · Code Anchors · Architecture