CREATE TABLE IF NOT EXISTS projects ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, path TEXT NOT NULL UNIQUE, added_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(), last_session_id UUID ); CREATE TABLE IF NOT EXISTS sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, name TEXT NOT NULL, model TEXT NOT NULL, system_prompt TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(), updated_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp() ); CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id, updated_at DESC); CREATE TABLE IF NOT EXISTS messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, role TEXT NOT NULL, content TEXT NOT NULL DEFAULT '', tool_calls JSONB, tool_results JSONB, status TEXT NOT NULL DEFAULT 'complete', last_seq INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp() ); CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, created_at); ALTER TABLE messages ADD COLUMN IF NOT EXISTS tokens_used INTEGER; ALTER TABLE messages ADD COLUMN IF NOT EXISTS ctx_used INTEGER; ALTER TABLE messages ADD COLUMN IF NOT EXISTS ctx_max INTEGER; ALTER TABLE messages ADD COLUMN IF NOT EXISTS started_at TIMESTAMPTZ; ALTER TABLE messages ADD COLUMN IF NOT EXISTS finished_at TIMESTAMPTZ; ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(); CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value JSONB NOT NULL ); INSERT INTO settings (key, value) VALUES ('default_model', '"qwen3.6-35b-a3b-mxfp4"') ON CONFLICT (key) DO NOTHING; -- DEPRECATED: client-side pane state as of v1.2-batch4. Table retained per -- additive schema rule; no writes. Drop in a future destructive migration. CREATE TABLE IF NOT EXISTS session_panes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, position INTEGER NOT NULL, kind TEXT NOT NULL CHECK (kind IN ('chat', 'file_browser')), state JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(), UNIQUE (session_id, position) ); CREATE INDEX IF NOT EXISTS idx_session_panes_session ON session_panes (session_id); -- v1.4: backfill removed. Pane layout is client-side (localStorage) since v1.2-batch4. -- The CREATE TABLE above is retained for additive-schema discipline; drop is a -- future destructive migration. -- v1.2: sessions.status (open | archived) ALTER TABLE sessions ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'open'; -- v1.2: chats table CREATE TABLE IF NOT EXISTS chats ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, name TEXT, status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'archived')), created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(), updated_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp() ); CREATE INDEX IF NOT EXISTS idx_chats_session_status ON chats (session_id, status, updated_at DESC); -- v1.2: messages.chat_id + messages.kind ALTER TABLE messages ADD COLUMN IF NOT EXISTS chat_id UUID REFERENCES chats(id) ON DELETE CASCADE; ALTER TABLE messages ADD COLUMN IF NOT EXISTS kind TEXT NOT NULL DEFAULT 'message'; CREATE INDEX IF NOT EXISTS idx_messages_chat ON messages (chat_id, created_at); -- Backfill: one chat per existing session that has none yet INSERT INTO chats (session_id, name, status, created_at, updated_at) SELECT s.id, s.name, 'open', s.created_at, s.updated_at FROM sessions s WHERE NOT EXISTS ( SELECT 1 FROM chats c WHERE c.session_id = s.id ); -- Backfill: link orphaned messages to their session's first chat UPDATE messages SET chat_id = ( SELECT c.id FROM chats c WHERE c.session_id = messages.session_id ORDER BY c.created_at ASC LIMIT 1 ) WHERE chat_id IS NULL; -- Enforce NOT NULL on chat_id once all rows are backfilled DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'messages' AND column_name = 'chat_id' AND is_nullable = 'YES' ) AND NOT EXISTS ( SELECT 1 FROM messages WHERE chat_id IS NULL ) THEN ALTER TABLE messages ALTER COLUMN chat_id SET NOT NULL; END IF; END $$; -- v1.2.1: CHECK constraints for sessions.status and messages (role, status) -- KEEP IN SYNC: apps/server/src/types/api.ts (MESSAGE_ROLES, MESSAGE_STATUSES, SessionStatus) DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'sessions_status_chk') THEN ALTER TABLE sessions ADD CONSTRAINT sessions_status_chk CHECK (status IN ('open', 'archived')); END IF; IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'messages_role_chk') THEN ALTER TABLE messages ADD CONSTRAINT messages_role_chk CHECK (role IN ('user', 'assistant', 'system', 'tool')); END IF; IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'messages_status_chk') THEN ALTER TABLE messages ADD CONSTRAINT messages_status_chk CHECK (status IN ('streaming', 'complete', 'failed', 'cancelled')); END IF; END $$; -- v1.2-project-ux: projects.status + projects.gitea_remote -- KEEP IN SYNC: apps/server/src/types/api.ts PROJECT_STATUSES ALTER TABLE projects ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'open'; ALTER TABLE projects ADD COLUMN IF NOT EXISTS gitea_remote TEXT; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'projects_status_chk') THEN ALTER TABLE projects ADD CONSTRAINT projects_status_chk CHECK (status IN ('open', 'archived')); END IF; END $$; -- v1.3-tab-close-chat-archive: align chats.status vocabulary with projects ('archived' not 'closed') -- KEEP IN SYNC: apps/server/src/types/api.ts CHAT_STATUSES -- Order matters: (1) drop the OLD inline CHECK that only allowed ('open','closed'); -- (2) migrate existing rows; (3) add new named CHECK allowing ('open','archived'). ALTER TABLE chats DROP CONSTRAINT IF EXISTS chats_status_check; UPDATE chats SET status = 'archived' WHERE status = 'closed'; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chats_status_chk') THEN ALTER TABLE chats ADD CONSTRAINT chats_status_chk CHECK (status IN ('open', 'archived')); END IF; END $$;