-- Migration 010: Sessions table with bulletproof auto-incrementing -- Purpose: Store session metadata with unique session numbers per project -- Dependencies: 001_base_schema.sql (projects table, pgvector extension) -- Sessions table: Core session metadata with temporal tracking CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, -- Format: "session_{timestamp}_{uuid}" project TEXT REFERENCES projects(key) ON DELETE SET NULL, session_number INTEGER, -- Auto-increment per project (e.g., "Session 439") -- Temporal tracking started_at TIMESTAMP WITH TIME ZONE NOT NULL, ended_at TIMESTAMP WITH TIME ZONE, duration_minutes INTEGER GENERATED ALWAYS AS (EXTRACT(EPOCH FROM (ended_at - started_at)) / 60) STORED, -- Context working_directory TEXT, git_branch TEXT, initial_prompt TEXT, -- First user message summary TEXT, -- Auto-generated summary -- Semantic search embedding vector(1024), -- Embedding of summary for similarity search -- Metrics message_count INTEGER DEFAULT 0, token_count INTEGER DEFAULT 0, tools_used TEXT[], -- Array of tool names used -- Status status TEXT DEFAULT 'active' CHECK (status IN ('active', 'completed', 'interrupted')), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Indexes for efficient querying CREATE INDEX idx_sessions_project ON sessions(project); CREATE INDEX idx_sessions_started ON sessions(started_at DESC); CREATE INDEX idx_sessions_status ON sessions(status); CREATE INDEX idx_sessions_number ON sessions(project, session_number DESC); -- HNSW index for semantic similarity search (requires pgvector) CREATE INDEX idx_sessions_embedding ON sessions USING hnsw (embedding vector_cosine_ops); -- Unique session number per project (partial index - only when project is set) CREATE UNIQUE INDEX idx_sessions_project_number ON sessions(project, session_number) WHERE project IS NOT NULL; -- Session number sequences per project (bulletproof autoincrement) CREATE TABLE IF NOT EXISTS session_sequences ( project TEXT PRIMARY KEY REFERENCES projects(key) ON DELETE CASCADE, next_number INTEGER DEFAULT 1, last_updated TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Function for atomic session number generation (prevents race conditions) CREATE OR REPLACE FUNCTION get_next_session_number(p_project TEXT) RETURNS INTEGER AS $$ DECLARE v_number INTEGER; BEGIN -- Insert project if doesn't exist INSERT INTO projects (key, name) VALUES (p_project, p_project) ON CONFLICT (key) DO NOTHING; -- Insert sequence if doesn't exist INSERT INTO session_sequences (project, next_number) VALUES (p_project, 1) ON CONFLICT (project) DO NOTHING; -- Atomically increment and return (no race conditions possible) UPDATE session_sequences SET next_number = next_number + 1, last_updated = NOW() WHERE project = p_project RETURNING next_number - 1 INTO v_number; RETURN v_number; END; $$ LANGUAGE plpgsql; -- Trigger to auto-assign session numbers on insert CREATE OR REPLACE FUNCTION assign_session_number() RETURNS TRIGGER AS $$ BEGIN IF NEW.project IS NOT NULL AND NEW.session_number IS NULL THEN NEW.session_number := get_next_session_number(NEW.project); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_assign_session_number BEFORE INSERT ON sessions FOR EACH ROW EXECUTE FUNCTION assign_session_number(); -- Session-Task relationship enhancement -- Add foreign key constraint to existing task_activity table ALTER TABLE task_activity DROP CONSTRAINT IF EXISTS fk_task_activity_session; -- Drop NOT NULL constraint to allow NULL session_ids (for orphaned references) ALTER TABLE task_activity ALTER COLUMN session_id DROP NOT NULL; -- First, set NULL for any session_ids that don't exist (orphaned references) -- This cleans up existing data before adding the constraint UPDATE task_activity SET session_id = NULL WHERE session_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sessions WHERE id = task_activity.session_id); ALTER TABLE task_activity ADD CONSTRAINT fk_task_activity_session FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE SET NULL; -- Helper view: Session tasks with activity summary CREATE OR REPLACE VIEW session_tasks AS SELECT DISTINCT ta.session_id, ta.task_id, t.project, t.title, t.status, MIN(ta.created_at) as first_touched, MAX(ta.created_at) as last_touched, COUNT(*) as activity_count FROM task_activity ta JOIN tasks t ON ta.task_id = t.id GROUP BY ta.session_id, ta.task_id, t.project, t.title, t.status; -- Session-Commit linking table CREATE TABLE IF NOT EXISTS session_commits ( id SERIAL PRIMARY KEY, session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, commit_sha TEXT NOT NULL, repo TEXT NOT NULL, commit_message TEXT, committed_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(session_id, commit_sha) ); CREATE INDEX idx_session_commits_session ON session_commits(session_id); CREATE INDEX idx_session_commits_sha ON session_commits(commit_sha); -- Helper view: Link sessions to versions through commits CREATE OR REPLACE VIEW session_versions AS SELECT DISTINCT sc.session_id, tc.task_id, t.version_id, v.version, v.status as version_status FROM session_commits sc JOIN task_commits tc ON sc.commit_sha = tc.commit_sha JOIN tasks t ON tc.task_id = t.id LEFT JOIN versions v ON t.version_id = v.id WHERE v.id IS NOT NULL; -- Comments for documentation COMMENT ON TABLE sessions IS 'Session metadata with unique session numbers per project'; COMMENT ON TABLE session_sequences IS 'Atomic counters for session numbers per project'; COMMENT ON TABLE session_commits IS 'Links sessions to git commits'; COMMENT ON COLUMN sessions.session_number IS 'Auto-incrementing number per project (1, 2, 3, ...)'; COMMENT ON COLUMN sessions.embedding IS 'Vector embedding of session summary for semantic search'; COMMENT ON FUNCTION get_next_session_number IS 'Atomic function to get next session number for a project';