Changeset 0.6.0 (#36)

This commit is contained in:
2026-02-20 22:24:47 +00:00
parent 30d0c11219
commit 925b70f98c
34 changed files with 2575 additions and 637 deletions

View File

@@ -0,0 +1,114 @@
-- ==========================================
-- Migration 006: Unify Chats → Channels
-- ==========================================
-- "Everything is a channel." Merges the separate chats/channels
-- schema into a single unified channel model.
--
-- The old channels/channel_members/channel_messages tables (from 001)
-- were never populated — safe to drop and rebuild on top of chats.
-- ==========================================
-- ── 1. Drop unused legacy channel tables ────
-- These were placeholders from 001; no data, no handlers.
-- Order matters: drop dependents first.
-- Remove FK references in tool_usage_log before dropping
ALTER TABLE tool_usage_log DROP CONSTRAINT IF EXISTS tool_usage_log_channel_id_fkey;
ALTER TABLE tool_usage_log DROP COLUMN IF EXISTS channel_id;
DROP TABLE IF EXISTS channel_messages CASCADE;
DROP TABLE IF EXISTS channel_members CASCADE;
DROP TABLE IF EXISTS channels CASCADE;
-- Drop the old trigger (will re-create after rename)
DROP TRIGGER IF EXISTS channels_updated_at ON channels;
-- ── 2. Rename chats → channels ──────────────
ALTER TABLE chats RENAME TO channels;
ALTER TABLE chat_messages RENAME TO messages;
-- Rename columns to match new schema
ALTER TABLE messages RENAME COLUMN chat_id TO channel_id;
-- Rename indexes
ALTER INDEX IF EXISTS idx_chats_user RENAME TO idx_channels_user;
ALTER INDEX IF EXISTS idx_chats_updated RENAME TO idx_channels_updated;
ALTER INDEX IF EXISTS idx_chats_tags RENAME TO idx_channels_tags;
ALTER INDEX IF EXISTS idx_chat_messages_chat RENAME TO idx_messages_channel;
-- Rename constraints (PK and FK auto-renamed with table on some PG versions,
-- but let's be explicit for the FK)
-- Note: PG auto-renames PKs but not FKs or check constraints
-- Rename the updated_at trigger
DROP TRIGGER IF EXISTS chats_updated_at ON channels;
CREATE TRIGGER channels_updated_at BEFORE UPDATE ON channels
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- ── 3. Add channel type + description ───────
ALTER TABLE channels ADD COLUMN IF NOT EXISTS type VARCHAR(20) DEFAULT 'direct';
ALTER TABLE channels ADD COLUMN IF NOT EXISTS description TEXT;
COMMENT ON COLUMN channels.type IS 'direct=1:1 AI chat, group=multi-model, channel=named persistent';
-- Backfill: all existing rows are 1:1 AI chats
UPDATE channels SET type = 'direct' WHERE type IS NULL;
-- Index on type for filtered queries
CREATE INDEX IF NOT EXISTS idx_channels_type ON channels(type);
-- ── 4. Add message tree (parent_id) ─────────
ALTER TABLE messages ADD COLUMN IF NOT EXISTS parent_id UUID REFERENCES messages(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_messages_parent ON messages(parent_id);
-- Backfill linear parent chains on existing messages.
-- Each message's parent is the previous message in the same channel (by created_at).
WITH ordered AS (
SELECT id, channel_id, created_at,
LAG(id) OVER (PARTITION BY channel_id ORDER BY created_at) AS prev_id
FROM messages
)
UPDATE messages m
SET parent_id = o.prev_id
FROM ordered o
WHERE m.id = o.id AND o.prev_id IS NOT NULL AND m.parent_id IS NULL;
-- ── 5. Add participant columns on messages ──
-- Decouples messages from user-only: AI models are participants too.
ALTER TABLE messages ADD COLUMN IF NOT EXISTS participant_type VARCHAR(10) DEFAULT 'user';
ALTER TABLE messages ADD COLUMN IF NOT EXISTS participant_id VARCHAR(255);
COMMENT ON COLUMN messages.participant_type IS 'user or model';
COMMENT ON COLUMN messages.participant_id IS 'user UUID or model identifier string';
-- Backfill: user messages get the channel owner's user_id;
-- assistant messages get the model name as participant_id.
UPDATE messages m
SET participant_type = CASE WHEN m.role = 'assistant' THEN 'model' ELSE 'user' END,
participant_id = CASE
WHEN m.role = 'assistant' THEN COALESCE(m.model, 'unknown')
ELSE (SELECT c.user_id::text FROM channels c WHERE c.id = m.channel_id)
END
WHERE m.participant_id IS NULL;
-- ── 6. Update model_routing_log FK ──────────
-- The FK column was named chat_id — rename it
ALTER TABLE model_routing_log RENAME COLUMN chat_id TO channel_id;
ALTER INDEX IF EXISTS idx_routing_log_chat RENAME TO idx_routing_log_channel;
-- message_id FK still valid (messages table was renamed, FK follows)
-- ── 7. Update tool_usage_log ────────────────
-- We already dropped the old channel_id column above.
-- Re-add it pointing to the unified channels table.
-- Also rename the old chat_id column.
ALTER TABLE tool_usage_log RENAME COLUMN chat_id TO channel_id;
-- The FK auto-follows the table rename, but let's be safe:
-- (chat_id FK pointed to chats(id), which is now channels(id) — PG handles this)

View File

@@ -0,0 +1,56 @@
-- ==========================================
-- Migration 007: Channel Members & Models
-- ==========================================
-- Membership and model assignment tables for
-- multi-user and multi-model channels.
-- ==========================================
-- ── Channel Members ─────────────────────────
-- Who is in this channel (human participants).
CREATE TABLE IF NOT EXISTS channel_members (
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,
role VARCHAR(20) DEFAULT 'member', -- owner, admin, member
joined_at TIMESTAMPTZ DEFAULT NOW(),
last_read_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(channel_id, user_id)
);
CREATE INDEX idx_channel_members_channel ON channel_members(channel_id);
CREATE INDEX idx_channel_members_user ON channel_members(user_id);
-- Backfill: existing channels are 1:1, so the channel owner is the sole member.
INSERT INTO channel_members (channel_id, user_id, role)
SELECT id, user_id, 'owner'
FROM channels
WHERE user_id IS NOT NULL
ON CONFLICT (channel_id, user_id) DO NOTHING;
-- ── Channel Models ──────────────────────────
-- Which AI models are assigned to this channel.
-- For direct chats this is one model; for group/channel
-- there can be multiple with @mention routing.
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, -- e.g. 'claude-sonnet-4'
api_config_id UUID REFERENCES api_configs(id) ON DELETE SET NULL,
display_name VARCHAR(100), -- optional alias in this channel
system_prompt TEXT, -- per-model system prompt override
settings JSONB DEFAULT '{}'::jsonb, -- temperature, max_tokens overrides
is_default BOOLEAN DEFAULT false, -- auto-complete (no @mention needed)
added_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(channel_id, model_id)
);
CREATE INDEX idx_channel_models_channel ON channel_models(channel_id);
-- Backfill: existing channels have a model in the channels.model column.
INSERT INTO channel_models (channel_id, model_id, api_config_id, is_default)
SELECT id, model, api_config_id, true
FROM channels
WHERE model IS NOT NULL AND model != ''
ON CONFLICT (channel_id, model_id) DO NOTHING;

View File

@@ -0,0 +1,29 @@
-- ==========================================
-- Migration 008: Channel Cursors
-- ==========================================
-- Tracks each user's active branch position
-- per channel. Essential for conversation forking.
-- ==========================================
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 idx_channel_cursors_channel ON channel_cursors(channel_id);
CREATE INDEX idx_channel_cursors_user ON channel_cursors(user_id);
-- Backfill: set cursor to the last message in each channel for the owner.
INSERT INTO channel_cursors (channel_id, user_id, active_leaf_id)
SELECT c.id, c.user_id, (
SELECT m.id FROM messages m
WHERE m.channel_id = c.id
ORDER BY m.created_at DESC LIMIT 1
)
FROM channels c
WHERE c.user_id IS NOT NULL
ON CONFLICT (channel_id, user_id) DO NOTHING;

View File

@@ -0,0 +1,57 @@
-- ==========================================
-- Migration 009: Folders & Projects
-- ==========================================
-- Organizational structures for channels.
-- Folders are simple containers; projects are
-- tagged collections that can span folders.
-- ==========================================
-- ── 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 idx_folders_user ON folders(user_id);
CREATE INDEX idx_folders_parent ON folders(parent_id);
CREATE TRIGGER folders_updated_at BEFORE UPDATE ON folders
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- Add folder_id to channels (replaces the old text folder column)
ALTER TABLE channels ADD COLUMN IF NOT EXISTS folder_id UUID REFERENCES folders(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id);
-- ── Projects ────────────────────────────────
CREATE TABLE IF NOT EXISTS projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(200) NOT NULL,
description TEXT,
color VARCHAR(7), -- hex color for UI badge
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_projects_user ON projects(user_id);
CREATE TRIGGER projects_updated_at BEFORE UPDATE ON projects
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- Junction: channels can belong to multiple projects
CREATE TABLE IF NOT EXISTS project_channels (
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
added_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (project_id, channel_id)
);
CREATE INDEX idx_project_channels_channel ON project_channels(channel_id);

View File

@@ -0,0 +1,32 @@
-- ==========================================
-- Migration 010: Environment Banners
-- ==========================================
-- Environment banners for deployment context
-- (dev, staging, production, etc). Stored in
-- global_settings with a dedicated key.
-- ==========================================
-- Seed default banner config (disabled).
-- Schema: { enabled, text, position, bg, fg }
INSERT INTO global_settings (key, value) VALUES
('banner', '{
"enabled": false,
"text": "",
"position": "both",
"bg": "#007a33",
"fg": "#ffffff"
}'::jsonb)
ON CONFLICT (key) DO NOTHING;
-- Banner presets for quick selection in admin UI.
-- Generic environment labels — admins can set custom text.
INSERT INTO global_settings (key, value) VALUES
('banner_presets', '{
"development": { "text": "DEVELOPMENT", "bg": "#007a33", "fg": "#ffffff" },
"testing": { "text": "TESTING", "bg": "#502b85", "fg": "#ffffff" },
"staging": { "text": "STAGING", "bg": "#0033a0", "fg": "#ffffff" },
"production": { "text": "PRODUCTION", "bg": "#c8102e", "fg": "#ffffff" },
"training": { "text": "TRAINING", "bg": "#ff8c00", "fg": "#000000" },
"demo": { "text": "DEMO", "bg": "#fce83a", "fg": "#000000" }
}'::jsonb)
ON CONFLICT (key) DO NOTHING;

View File

@@ -0,0 +1,18 @@
-- ==========================================
-- Migration 011: Replace banner presets
-- ==========================================
-- Replaces legacy presets with
-- generic environment labels. Existing databases
-- that ran 010 have the old presets; this overwrites.
-- ==========================================
UPDATE global_settings
SET value = '{
"development": { "text": "DEVELOPMENT", "bg": "#007a33", "fg": "#ffffff" },
"testing": { "text": "TESTING", "bg": "#502b85", "fg": "#ffffff" },
"staging": { "text": "STAGING", "bg": "#0033a0", "fg": "#ffffff" },
"production": { "text": "PRODUCTION", "bg": "#c8102e", "fg": "#ffffff" },
"training": { "text": "TRAINING", "bg": "#ff8c00", "fg": "#000000" },
"demo": { "text": "DEMO", "bg": "#fce83a", "fg": "#000000" }
}'::jsonb
WHERE key = 'banner_presets';