-- ========================================== -- Chat Switchboard — 005 Channels & Conversations -- ========================================== -- Channels (all types, all columns), messages, participants, -- models, cursors, folders, user prefs, session participants. -- Consolidated v0.28.0: merges 005 + 016 (multiuser) + 017 (unread) -- + 021 (sessions) + 024 (workflow instance cols) + 026 (service type) -- + v0.28.0 (kb_auto_inject). -- -- Note: project_id/workspace_id FKs added by 010_projects.sql. -- Note: workflow_id FK added by 018_workflows.sql. -- ========================================== -- ========================================= -- FOLDERS -- ========================================= CREATE TABLE IF NOT EXISTS folders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, name VARCHAR(200) NOT NULL, parent_id UUID REFERENCES folders(id) ON DELETE CASCADE, sort_order INT DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(user_id, name, parent_id) ); CREATE INDEX IF NOT EXISTS idx_folders_user ON folders(user_id); CREATE INDEX IF NOT EXISTS idx_folders_parent ON folders(parent_id); DROP TRIGGER IF EXISTS folders_updated_at ON folders; CREATE TRIGGER folders_updated_at BEFORE UPDATE ON folders FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -- ========================================= -- CHANNELS -- ========================================= CREATE TABLE IF NOT EXISTS channels ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, title VARCHAR(500) NOT NULL, description TEXT, type VARCHAR(20) DEFAULT 'direct' CHECK (type IN ('direct', 'dm', 'group', 'channel', 'workflow', 'service')), model VARCHAR(100), system_prompt TEXT, provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL, is_archived BOOLEAN DEFAULT false, is_pinned BOOLEAN DEFAULT false, folder_id UUID REFERENCES folders(id) ON DELETE SET NULL, folder TEXT, -- legacy text folder (compat) team_id UUID REFERENCES teams(id) ON DELETE SET NULL, settings JSONB DEFAULT '{}'::jsonb, tags TEXT[], -- Multi-user (v0.23.1) ai_mode VARCHAR(20) NOT NULL DEFAULT 'auto' CHECK (ai_mode IN ('auto', 'mention_only', 'off')), topic TEXT, -- Anonymous sessions (v0.24.3) allow_anonymous BOOLEAN NOT NULL DEFAULT false, -- Workflow instance columns (v0.26.0) -- workflow_id FK added by 018_workflows.sql (deferred — table doesn't exist yet) workflow_id UUID, workflow_version INTEGER, current_stage INTEGER DEFAULT 0, stage_data JSONB DEFAULT '{}'::jsonb, workflow_status TEXT DEFAULT 'active' CHECK (workflow_status IN ('active', 'completed', 'stale', 'cancelled')), last_activity_at TIMESTAMPTZ DEFAULT NOW(), stage_entered_at TIMESTAMPTZ, -- KB auto-injection (v0.28.0) kb_auto_inject BOOLEAN NOT NULL DEFAULT false, -- Cross-domain FKs (added by 010_projects.sql) project_id UUID, workspace_id UUID, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_channels_user ON channels(user_id); CREATE INDEX IF NOT EXISTS idx_channels_updated ON channels(updated_at DESC); CREATE INDEX IF NOT EXISTS idx_channels_type ON channels(type); CREATE INDEX IF NOT EXISTS idx_channels_team ON channels(team_id) WHERE team_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_channels_tags ON channels USING GIN(tags); CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id) WHERE folder_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_channels_project ON channels(project_id) WHERE project_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_channels_workspace ON channels(workspace_id) WHERE workspace_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_channels_workflow ON channels(workflow_id) WHERE workflow_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_channels_workflow_status ON channels(workflow_status) WHERE workflow_status = 'active'; CREATE INDEX IF NOT EXISTS idx_channels_workflow_active ON channels(workflow_id, workflow_status) WHERE workflow_id IS NOT NULL AND workflow_status = 'active'; DROP TRIGGER IF EXISTS channels_updated_at ON channels; CREATE TRIGGER channels_updated_at BEFORE UPDATE ON channels FOR EACH ROW EXECUTE FUNCTION update_updated_at(); COMMENT ON COLUMN channels.type IS 'direct=single-user, dm=human-to-human, group=multi-participant, channel=named persistent, workflow=staged, service=autonomous task'; COMMENT ON COLUMN channels.ai_mode IS 'auto=respond always, mention_only=@mention required, off=AI disabled'; COMMENT ON COLUMN channels.topic IS 'Short channel description shown in header'; COMMENT ON COLUMN channels.allow_anonymous IS 'When true, unauthenticated visitors can join via session token'; COMMENT ON COLUMN channels.workflow_id IS 'FK to workflows — set when type=workflow (constraint added by 018)'; COMMENT ON COLUMN channels.workflow_version IS 'Pinned version number from workflow_versions'; COMMENT ON COLUMN channels.current_stage IS 'Ordinal of the active workflow stage'; COMMENT ON COLUMN channels.stage_data IS 'Accumulated form data collected across stages'; COMMENT ON COLUMN channels.workflow_status IS 'active/completed/stale/cancelled'; COMMENT ON COLUMN channels.last_activity_at IS 'Updated on each message — used by staleness sweep'; COMMENT ON COLUMN channels.stage_entered_at IS 'Timestamp when current workflow stage was entered — used for SLA computation'; COMMENT ON COLUMN channels.kb_auto_inject IS 'When true, top-K KB chunks auto-prepend to context'; -- ========================================= -- MESSAGES -- ========================================= CREATE TABLE IF NOT EXISTS messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, role VARCHAR(20) NOT NULL CHECK (role IN ('user', 'assistant', 'system', 'tool')), content TEXT NOT NULL, model VARCHAR(100), tokens_used INTEGER, tool_calls JSONB, metadata JSONB DEFAULT '{}'::jsonb, parent_id UUID REFERENCES messages(id) ON DELETE SET NULL, sibling_index INTEGER DEFAULT 0, participant_type VARCHAR(10) DEFAULT 'user', participant_id VARCHAR(255), provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel_id, created_at); CREATE INDEX IF NOT EXISTS idx_messages_parent ON messages(parent_id); CREATE INDEX IF NOT EXISTS idx_messages_alive ON messages(channel_id, created_at) WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS idx_messages_parent_alive ON messages(parent_id, sibling_index) WHERE deleted_at IS NULL; COMMENT ON COLUMN messages.parent_id IS 'Tree parent for conversation forking'; COMMENT ON COLUMN messages.sibling_index IS 'Position among siblings (0-indexed)'; -- ========================================= -- CHANNEL PARTICIPANTS & MODELS -- ========================================= CREATE TABLE IF NOT EXISTS channel_participants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, participant_type VARCHAR(10) NOT NULL DEFAULT 'user' CHECK (participant_type IN ('user', 'persona', 'session')), participant_id UUID NOT NULL, role VARCHAR(10) NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'member', 'observer')), display_name VARCHAR(200), avatar_url TEXT, joined_at TIMESTAMPTZ DEFAULT NOW(), last_read_at TIMESTAMPTZ DEFAULT NOW(), last_read_message_id UUID, -- v0.23.2: unread cursor UNIQUE(channel_id, participant_type, participant_id) ); CREATE INDEX IF NOT EXISTS idx_channel_participants_channel ON channel_participants(channel_id); CREATE INDEX IF NOT EXISTS idx_channel_participants_ref ON channel_participants(participant_type, participant_id); CREATE TABLE IF NOT EXISTS channel_models ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, model_id VARCHAR(255) NOT NULL, provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL, persona_id UUID REFERENCES personas(id) ON DELETE SET NULL, display_name VARCHAR(100), system_prompt TEXT, settings JSONB DEFAULT '{}'::jsonb, is_default BOOLEAN DEFAULT false, added_at TIMESTAMPTZ DEFAULT NOW() ); -- Raw model entries (no persona): unique per channel+model+provider CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_models_raw ON channel_models(channel_id, model_id, provider_config_id) WHERE persona_id IS NULL; -- Persona entries: one per persona per channel (same model allowed for different personas) CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_models_persona ON channel_models(channel_id, persona_id) WHERE persona_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_channel_models_channel ON channel_models(channel_id); -- ========================================= -- CHANNEL CURSORS (forking navigation) -- ========================================= CREATE TABLE IF NOT EXISTS channel_cursors ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, active_leaf_id UUID REFERENCES messages(id) ON DELETE SET NULL, updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(channel_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_channel_cursors_channel ON channel_cursors(channel_id); CREATE INDEX IF NOT EXISTS idx_channel_cursors_user ON channel_cursors(user_id); -- ========================================= -- USER MODEL SETTINGS -- ========================================= CREATE TABLE IF NOT EXISTS user_model_settings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, model_id TEXT NOT NULL, provider_config_id UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE, hidden BOOLEAN DEFAULT false, preferred_temperature FLOAT, preferred_max_tokens INT, sort_order INT DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(user_id, model_id, provider_config_id) ); CREATE INDEX IF NOT EXISTS idx_user_model_settings_user ON user_model_settings(user_id); DROP TRIGGER IF EXISTS user_model_settings_updated_at ON user_model_settings; CREATE TRIGGER user_model_settings_updated_at BEFORE UPDATE ON user_model_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -- ========================================= -- SESSION PARTICIPANTS (v0.24.3) -- ========================================= CREATE TABLE IF NOT EXISTS session_participants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_token TEXT NOT NULL UNIQUE, channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, display_name TEXT NOT NULL DEFAULT 'Visitor', fingerprint TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_session_participants_channel ON session_participants(channel_id); CREATE INDEX IF NOT EXISTS idx_session_participants_token ON session_participants(session_token); COMMENT ON TABLE session_participants IS 'Ephemeral identities for anonymous workflow channel visitors.';