diff --git a/apps/backend/internal/analytics/repository/sqlite/repository_test.go b/apps/backend/internal/analytics/repository/sqlite/repository_test.go index bb1680c4da..b9afaa6b2f 100644 --- a/apps/backend/internal/analytics/repository/sqlite/repository_test.go +++ b/apps/backend/internal/analytics/repository/sqlite/repository_test.go @@ -53,9 +53,11 @@ func createTestDB(t *testing.T) *sqlx.DB { id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL DEFAULT '', board_id TEXT NOT NULL, + workflow_id TEXT NOT NULL DEFAULT '', workflow_step_id TEXT NOT NULL DEFAULT '', title TEXT NOT NULL, state TEXT DEFAULT 'TODO', + is_ephemeral INTEGER NOT NULL DEFAULT 0, archived_at TIMESTAMP, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL @@ -293,3 +295,261 @@ func TestGetGitStats_EmptyWorkspace(t *testing.T) { t.Errorf("expected 0 commits, got %d", stats.TotalCommits) } } + +func TestGetGlobalStats_ExcludesEphemeralTasks(t *testing.T) { + dbConn := createTestDB(t) + repo, err := NewWithDB(dbConn, dbConn) + if err != nil { + t.Fatalf("NewWithDB failed: %v", err) + } + + ctx := context.Background() + now := time.Now().UTC() + nowStr := now.Format(time.RFC3339) + + // Insert workspace + board + workflow step + execOrFatal(t, dbConn, `INSERT INTO workspaces (id, name, created_at, updated_at) VALUES ('ws-1', 'Test', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO boards (id, workspace_id, name, created_at, updated_at) VALUES ('board-1', 'ws-1', 'Board', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO workflow_steps (id, workflow_id, name, position, created_at, updated_at) VALUES ('step-todo', 'board-1', 'To Do', 0, ?, ?)`, nowStr, nowStr) + + // Insert a regular task + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, created_at, updated_at) VALUES ('task-regular', 'ws-1', 'board-1', 'step-todo', 'Regular', 0, ?, ?)`, nowStr, nowStr) + + // Insert an ephemeral (quick chat) task + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, created_at, updated_at) VALUES ('task-ephemeral', 'ws-1', 'board-1', '', 'Quick Chat', 1, ?, ?)`, nowStr, nowStr) + + // Add sessions for both tasks + execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, state, started_at, updated_at) VALUES ('sess-regular', 'task-regular', 'agent-1', 'COMPLETED', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, state, started_at, updated_at) VALUES ('sess-ephemeral', 'task-ephemeral', 'agent-1', 'COMPLETED', ?, ?)`, nowStr, nowStr) + + // Add turns for both sessions + execOrFatal(t, dbConn, `INSERT INTO task_session_turns (id, task_session_id, task_id, started_at, completed_at, created_at, updated_at) VALUES ('turn-regular', 'sess-regular', 'task-regular', ?, ?, ?, ?)`, nowStr, nowStr, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO task_session_turns (id, task_session_id, task_id, started_at, completed_at, created_at, updated_at) VALUES ('turn-ephemeral', 'sess-ephemeral', 'task-ephemeral', ?, ?, ?, ?)`, nowStr, nowStr, nowStr, nowStr) + + // Global stats should only count the regular task + stats, err := repo.GetGlobalStats(ctx, "ws-1", nil) + if err != nil { + t.Fatalf("GetGlobalStats failed: %v", err) + } + if stats.TotalTasks != 1 { + t.Errorf("expected 1 total task (ephemeral excluded), got %d", stats.TotalTasks) + } + if stats.TotalSessions != 1 { + t.Errorf("expected 1 total session (ephemeral excluded), got %d", stats.TotalSessions) + } + if stats.TotalTurns != 1 { + t.Errorf("expected 1 total turn (ephemeral excluded), got %d", stats.TotalTurns) + } +} + +func TestGetTaskStats_ExcludesEphemeralTasks(t *testing.T) { + dbConn := createTestDB(t) + repo, err := NewWithDB(dbConn, dbConn) + if err != nil { + t.Fatalf("NewWithDB failed: %v", err) + } + + ctx := context.Background() + now := time.Now().UTC() + nowStr := now.Format(time.RFC3339) + + execOrFatal(t, dbConn, `INSERT INTO workspaces (id, name, created_at, updated_at) VALUES ('ws-1', 'Test', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO boards (id, workspace_id, name, created_at, updated_at) VALUES ('board-1', 'ws-1', 'Board', ?, ?)`, nowStr, nowStr) + + // Insert regular and ephemeral tasks + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, created_at, updated_at) VALUES ('task-regular', 'ws-1', 'board-1', '', 'Regular', 0, ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, created_at, updated_at) VALUES ('task-ephemeral', 'ws-1', 'board-1', '', 'Quick Chat', 1, ?, ?)`, nowStr, nowStr) + + results, err := repo.GetTaskStats(ctx, "ws-1", nil) + if err != nil { + t.Fatalf("GetTaskStats failed: %v", err) + } + if len(results) != 1 { + t.Fatalf("expected 1 task stat (ephemeral excluded), got %d", len(results)) + } + if results[0].TaskID != "task-regular" { + t.Errorf("expected task-regular, got %s", results[0].TaskID) + } +} + +func TestGetDailyActivity_ExcludesEphemeralTasks(t *testing.T) { + dbConn := createTestDB(t) + repo, err := NewWithDB(dbConn, dbConn) + if err != nil { + t.Fatalf("NewWithDB failed: %v", err) + } + + ctx := context.Background() + now := time.Now().UTC() + nowStr := now.Format(time.RFC3339) + + execOrFatal(t, dbConn, `INSERT INTO workspaces (id, name, created_at, updated_at) VALUES ('ws-1', 'Test', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO boards (id, workspace_id, name, created_at, updated_at) VALUES ('board-1', 'ws-1', 'Board', ?, ?)`, nowStr, nowStr) + + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, created_at, updated_at) VALUES ('task-regular', 'ws-1', 'board-1', '', 'Regular', 0, ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, created_at, updated_at) VALUES ('task-ephemeral', 'ws-1', 'board-1', '', 'Quick Chat', 1, ?, ?)`, nowStr, nowStr) + + execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, state, started_at, updated_at) VALUES ('sess-regular', 'task-regular', 'agent-1', 'COMPLETED', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, state, started_at, updated_at) VALUES ('sess-ephemeral', 'task-ephemeral', 'agent-1', 'COMPLETED', ?, ?)`, nowStr, nowStr) + + execOrFatal(t, dbConn, `INSERT INTO task_session_turns (id, task_session_id, task_id, started_at, completed_at, created_at, updated_at) VALUES ('turn-regular', 'sess-regular', 'task-regular', ?, ?, ?, ?)`, nowStr, nowStr, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO task_session_turns (id, task_session_id, task_id, started_at, completed_at, created_at, updated_at) VALUES ('turn-ephemeral', 'sess-ephemeral', 'task-ephemeral', ?, ?, ?, ?)`, nowStr, nowStr, nowStr, nowStr) + + results, err := repo.GetDailyActivity(ctx, "ws-1", 7) + if err != nil { + t.Fatalf("GetDailyActivity failed: %v", err) + } + + totalTurns := 0 + for _, r := range results { + totalTurns += r.TurnCount + } + if totalTurns != 1 { + t.Errorf("expected 1 total turn (ephemeral excluded), got %d", totalTurns) + } +} + +func TestGetCompletedTaskActivity_ExcludesEphemeralTasks(t *testing.T) { + dbConn := createTestDB(t) + repo, err := NewWithDB(dbConn, dbConn) + if err != nil { + t.Fatalf("NewWithDB failed: %v", err) + } + + ctx := context.Background() + now := time.Now().UTC() + nowStr := now.Format(time.RFC3339) + + execOrFatal(t, dbConn, `INSERT INTO workspaces (id, name, created_at, updated_at) VALUES ('ws-1', 'Test', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO boards (id, workspace_id, name, created_at, updated_at) VALUES ('board-1', 'ws-1', 'Board', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO workflow_steps (id, workflow_id, name, position, created_at, updated_at) VALUES ('step-done', 'board-1', 'Done', 1, ?, ?)`, nowStr, nowStr) + + // Regular completed task + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, archived_at, created_at, updated_at) VALUES ('task-regular', 'ws-1', 'board-1', 'step-done', 'Regular', 0, ?, ?, ?)`, nowStr, nowStr, nowStr) + // Ephemeral completed task + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, archived_at, created_at, updated_at) VALUES ('task-ephemeral', 'ws-1', 'board-1', '', 'Quick Chat', 1, ?, ?, ?)`, nowStr, nowStr, nowStr) + + execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, state, started_at, completed_at, updated_at) VALUES ('sess-regular', 'task-regular', 'agent-1', 'COMPLETED', ?, ?, ?)`, nowStr, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, state, started_at, completed_at, updated_at) VALUES ('sess-ephemeral', 'task-ephemeral', 'agent-1', 'COMPLETED', ?, ?, ?)`, nowStr, nowStr, nowStr) + + results, err := repo.GetCompletedTaskActivity(ctx, "ws-1", 7) + if err != nil { + t.Fatalf("GetCompletedTaskActivity failed: %v", err) + } + + totalCompleted := 0 + for _, r := range results { + totalCompleted += r.CompletedTasks + } + if totalCompleted != 1 { + t.Errorf("expected 1 completed task (ephemeral excluded), got %d", totalCompleted) + } +} + +func TestGetAgentUsage_ExcludesEphemeralTasks(t *testing.T) { + dbConn := createTestDB(t) + repo, err := NewWithDB(dbConn, dbConn) + if err != nil { + t.Fatalf("NewWithDB failed: %v", err) + } + + ctx := context.Background() + now := time.Now().UTC() + nowStr := now.Format(time.RFC3339) + + execOrFatal(t, dbConn, `INSERT INTO workspaces (id, name, created_at, updated_at) VALUES ('ws-1', 'Test', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO boards (id, workspace_id, name, created_at, updated_at) VALUES ('board-1', 'ws-1', 'Board', ?, ?)`, nowStr, nowStr) + + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, created_at, updated_at) VALUES ('task-regular', 'ws-1', 'board-1', '', 'Regular', 0, ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, created_at, updated_at) VALUES ('task-ephemeral', 'ws-1', 'board-1', '', 'Quick Chat', 1, ?, ?)`, nowStr, nowStr) + + execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, agent_profile_snapshot, state, started_at, updated_at) VALUES ('sess-regular', 'task-regular', 'agent-1', '{"name":"Agent"}', 'COMPLETED', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, agent_profile_snapshot, state, started_at, updated_at) VALUES ('sess-ephemeral', 'task-ephemeral', 'agent-1', '{"name":"Agent"}', 'COMPLETED', ?, ?)`, nowStr, nowStr) + + results, err := repo.GetAgentUsage(ctx, "ws-1", 5, nil) + if err != nil { + t.Fatalf("GetAgentUsage failed: %v", err) + } + + totalSessions := 0 + for _, r := range results { + totalSessions += r.SessionCount + } + if totalSessions != 1 { + t.Errorf("expected 1 session (ephemeral excluded), got %d", totalSessions) + } +} + +func TestGetGitStats_ExcludesEphemeralTasks(t *testing.T) { + dbConn := createTestDB(t) + repo, err := NewWithDB(dbConn, dbConn) + if err != nil { + t.Fatalf("NewWithDB failed: %v", err) + } + + ctx := context.Background() + now := time.Now().UTC() + nowStr := now.Format(time.RFC3339) + + execOrFatal(t, dbConn, `INSERT INTO workspaces (id, name, created_at, updated_at) VALUES ('ws-1', 'Test', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO boards (id, workspace_id, name, created_at, updated_at) VALUES ('board-1', 'ws-1', 'Board', ?, ?)`, nowStr, nowStr) + + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, created_at, updated_at) VALUES ('task-regular', 'ws-1', 'board-1', '', 'Regular', 0, ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, created_at, updated_at) VALUES ('task-ephemeral', 'ws-1', 'board-1', '', 'Quick Chat', 1, ?, ?)`, nowStr, nowStr) + + execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, state, started_at, updated_at) VALUES ('sess-regular', 'task-regular', 'agent-1', 'COMPLETED', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, state, started_at, updated_at) VALUES ('sess-ephemeral', 'task-ephemeral', 'agent-1', 'COMPLETED', ?, ?)`, nowStr, nowStr) + + execOrFatal(t, dbConn, `INSERT INTO task_session_commits (id, session_id, commit_sha, committed_at, files_changed, insertions, deletions, created_at) VALUES ('c-regular', 'sess-regular', 'abc123', ?, 3, 10, 2, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO task_session_commits (id, session_id, commit_sha, committed_at, files_changed, insertions, deletions, created_at) VALUES ('c-ephemeral', 'sess-ephemeral', 'def456', ?, 5, 20, 8, ?)`, nowStr, nowStr) + + stats, err := repo.GetGitStats(ctx, "ws-1", nil) + if err != nil { + t.Fatalf("GetGitStats failed: %v", err) + } + if stats.TotalCommits != 1 { + t.Errorf("expected 1 commit (ephemeral excluded), got %d", stats.TotalCommits) + } + if stats.TotalFilesChanged != 3 { + t.Errorf("expected 3 files changed, got %d", stats.TotalFilesChanged) + } +} + +func TestGetRepositoryStats_ExcludesEphemeralTasks(t *testing.T) { + dbConn := createTestDB(t) + repo, err := NewWithDB(dbConn, dbConn) + if err != nil { + t.Fatalf("NewWithDB failed: %v", err) + } + + ctx := context.Background() + now := time.Now().UTC() + nowStr := now.Format(time.RFC3339) + + execOrFatal(t, dbConn, `INSERT INTO workspaces (id, name, created_at, updated_at) VALUES ('ws-1', 'Test', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO boards (id, workspace_id, name, created_at, updated_at) VALUES ('board-1', 'ws-1', 'Board', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO repositories (id, workspace_id, name, created_at, updated_at) VALUES ('repo-1', 'ws-1', 'my-repo', ?, ?)`, nowStr, nowStr) + + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, created_at, updated_at) VALUES ('task-regular', 'ws-1', 'board-1', '', 'Regular', 0, ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO tasks (id, workspace_id, board_id, workflow_step_id, title, is_ephemeral, created_at, updated_at) VALUES ('task-ephemeral', 'ws-1', 'board-1', '', 'Quick Chat', 1, ?, ?)`, nowStr, nowStr) + + execOrFatal(t, dbConn, `INSERT INTO task_repositories (id, task_id, repository_id, created_at, updated_at) VALUES ('tr-1', 'task-regular', 'repo-1', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO task_repositories (id, task_id, repository_id, created_at, updated_at) VALUES ('tr-2', 'task-ephemeral', 'repo-1', ?, ?)`, nowStr, nowStr) + + execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, repository_id, state, started_at, updated_at) VALUES ('sess-regular', 'task-regular', 'agent-1', 'repo-1', 'COMPLETED', ?, ?)`, nowStr, nowStr) + execOrFatal(t, dbConn, `INSERT INTO task_sessions (id, task_id, agent_profile_id, repository_id, state, started_at, updated_at) VALUES ('sess-ephemeral', 'task-ephemeral', 'agent-1', 'repo-1', 'COMPLETED', ?, ?)`, nowStr, nowStr) + + results, err := repo.GetRepositoryStats(ctx, "ws-1", nil) + if err != nil { + t.Fatalf("GetRepositoryStats failed: %v", err) + } + if len(results) != 1 { + t.Fatalf("expected 1 repo, got %d", len(results)) + } + if results[0].TotalTasks != 1 { + t.Errorf("expected 1 task (ephemeral excluded), got %d", results[0].TotalTasks) + } + if results[0].SessionCount != 1 { + t.Errorf("expected 1 session (ephemeral excluded), got %d", results[0].SessionCount) + } +} diff --git a/apps/backend/internal/analytics/repository/sqlite/stats.go b/apps/backend/internal/analytics/repository/sqlite/stats.go index 1006f388c2..79ba93a3f1 100644 --- a/apps/backend/internal/analytics/repository/sqlite/stats.go +++ b/apps/backend/internal/analytics/repository/sqlite/stats.go @@ -76,7 +76,7 @@ func (r *Repository) GetTaskStats(ctx context.Context, workspaceID string, start WHERE (? IS NULL OR s.started_at >= ?) GROUP BY s.task_id ) turn_stats ON turn_stats.task_id = t.id - WHERE t.workspace_id = ? AND (? IS NULL OR t.created_at >= ?) + WHERE t.workspace_id = ? AND t.is_ephemeral = 0 AND (? IS NULL OR t.created_at >= ?) ORDER BY t.updated_at DESC `, dur) @@ -133,37 +133,38 @@ func (r *Repository) GetGlobalStats(ctx context.Context, workspaceID string, sta query := fmt.Sprintf(` SELECT - (SELECT COUNT(*) FROM tasks WHERE workspace_id = ? AND (? IS NULL OR created_at >= ?)) as total_tasks, + (SELECT COUNT(*) FROM tasks WHERE workspace_id = ? AND is_ephemeral = 0 AND (? IS NULL OR created_at >= ?)) as total_tasks, (SELECT COUNT(*) FROM tasks t LEFT JOIN workflow_steps ws ON ws.id = t.workflow_step_id WHERE t.workspace_id = ? + AND t.is_ephemeral = 0 AND (t.archived_at IS NOT NULL OR ws.position = (SELECT MAX(ws2.position) FROM workflow_steps ws2 WHERE ws2.workflow_id = ws.workflow_id)) AND (? IS NULL OR t.created_at >= ?)) as completed_tasks, - (SELECT COUNT(*) FROM tasks WHERE workspace_id = ? AND state = 'IN_PROGRESS' AND archived_at IS NULL AND (? IS NULL OR created_at >= ?)) as in_progress_tasks, - (SELECT COUNT(*) FROM task_sessions s JOIN tasks t ON t.id = s.task_id WHERE t.workspace_id = ? AND (? IS NULL OR s.started_at >= ?)) as total_sessions, + (SELECT COUNT(*) FROM tasks WHERE workspace_id = ? AND is_ephemeral = 0 AND state = 'IN_PROGRESS' AND archived_at IS NULL AND (? IS NULL OR created_at >= ?)) as in_progress_tasks, + (SELECT COUNT(*) FROM task_sessions s JOIN tasks t ON t.id = s.task_id WHERE t.workspace_id = ? AND t.is_ephemeral = 0 AND (? IS NULL OR s.started_at >= ?)) as total_sessions, (SELECT COUNT(*) FROM task_session_turns turn JOIN task_sessions s ON s.id = turn.task_session_id JOIN tasks t ON t.id = s.task_id - WHERE t.workspace_id = ? AND (? IS NULL OR s.started_at >= ?)) as total_turns, + WHERE t.workspace_id = ? AND t.is_ephemeral = 0 AND (? IS NULL OR s.started_at >= ?)) as total_turns, (SELECT COUNT(*) FROM task_session_messages msg JOIN task_sessions s ON s.id = msg.task_session_id JOIN tasks t ON t.id = s.task_id - WHERE t.workspace_id = ? AND (? IS NULL OR s.started_at >= ?)) as total_messages, + WHERE t.workspace_id = ? AND t.is_ephemeral = 0 AND (? IS NULL OR s.started_at >= ?)) as total_messages, (SELECT COUNT(*) FROM task_session_messages msg JOIN task_sessions s ON s.id = msg.task_session_id JOIN tasks t ON t.id = s.task_id - WHERE t.workspace_id = ? AND msg.author_type = 'user' AND (? IS NULL OR s.started_at >= ?)) as total_user_messages, + WHERE t.workspace_id = ? AND t.is_ephemeral = 0 AND msg.author_type = 'user' AND (? IS NULL OR s.started_at >= ?)) as total_user_messages, (SELECT COUNT(*) FROM task_session_messages msg JOIN task_sessions s ON s.id = msg.task_session_id JOIN tasks t ON t.id = s.task_id - WHERE t.workspace_id = ? AND msg.type LIKE 'tool_%%' AND (? IS NULL OR s.started_at >= ?)) as total_tool_calls, + WHERE t.workspace_id = ? AND t.is_ephemeral = 0 AND msg.type LIKE 'tool_%%' AND (? IS NULL OR s.started_at >= ?)) as total_tool_calls, (SELECT COALESCE(SUM( CASE WHEN turn.completed_at IS NOT NULL THEN %s ELSE 0 END ), 0) FROM task_session_turns turn JOIN task_sessions s ON s.id = turn.task_session_id JOIN tasks t ON t.id = s.task_id - WHERE t.workspace_id = ? AND (? IS NULL OR s.started_at >= ?)) as total_duration_ms + WHERE t.workspace_id = ? AND t.is_ephemeral = 0 AND (? IS NULL OR s.started_at >= ?)) as total_duration_ms `, dur) var stats models.GlobalStats @@ -229,7 +230,7 @@ func (r *Repository) GetDailyActivity(ctx context.Context, workspaceID string, d JOIN tasks t ON t.id = s.task_id LEFT JOIN task_session_messages msg ON msg.task_session_id = s.id AND %s = %s - WHERE t.workspace_id = ? + WHERE t.workspace_id = ? AND t.is_ephemeral = 0 GROUP BY %s ) activity ON activity.activity_date = d.date ORDER BY d.date ASC @@ -277,7 +278,7 @@ func (r *Repository) GetCompletedTaskActivity(ctx context.Context, workspaceID s SELECT task_id, MAX(completed_at) as completed_at FROM task_sessions WHERE completed_at IS NOT NULL GROUP BY task_id ) ts ON ts.task_id = t.id - WHERE t.workspace_id = ? + WHERE t.workspace_id = ? AND t.is_ephemeral = 0 AND (t.archived_at IS NOT NULL OR ws.position = (SELECT MAX(ws2.position) FROM workflow_steps ws2 WHERE ws2.workflow_id = ws.workflow_id)) GROUP BY %s @@ -370,7 +371,7 @@ func buildRepositoryStatsQuery(drv string) string { FROM task_repositories tr JOIN tasks t ON t.id = tr.task_id LEFT JOIN workflow_steps ws ON ws.id = t.workflow_step_id - WHERE (? IS NULL OR t.created_at >= ?) + WHERE t.is_ephemeral = 0 AND (? IS NULL OR t.created_at >= ?) GROUP BY tr.repository_id ) task_stats ON task_stats.repository_id = r.id LEFT JOIN ( @@ -381,19 +382,21 @@ func buildRepositoryStatsQuery(drv string) string { COUNT(DISTINCT CASE WHEN msg.author_type = 'user' THEN msg.id END) as user_message_count, COUNT(DISTINCT CASE WHEN msg.type LIKE 'tool_%%' THEN msg.id END) as tool_call_count FROM task_repositories tr + JOIN tasks t ON t.id = tr.task_id JOIN task_sessions s ON s.task_id = tr.task_id LEFT JOIN task_session_turns turn ON turn.task_session_id = s.id LEFT JOIN task_session_messages msg ON msg.task_session_id = s.id - WHERE (? IS NULL OR s.started_at >= ?) + WHERE t.is_ephemeral = 0 AND (? IS NULL OR s.started_at >= ?) GROUP BY tr.repository_id ) session_stats ON session_stats.repository_id = r.id LEFT JOIN ( SELECT tr.repository_id, COALESCE(SUM(CASE WHEN turn.completed_at IS NOT NULL THEN %s ELSE 0 END), 0) as total_duration_ms FROM task_repositories tr + JOIN tasks t ON t.id = tr.task_id JOIN task_sessions s ON s.task_id = tr.task_id LEFT JOIN task_session_turns turn ON turn.task_session_id = s.id - WHERE (? IS NULL OR s.started_at >= ?) + WHERE t.is_ephemeral = 0 AND (? IS NULL OR s.started_at >= ?) GROUP BY tr.repository_id ) duration_stats ON duration_stats.repository_id = r.id LEFT JOIN ( @@ -404,7 +407,8 @@ func buildRepositoryStatsQuery(drv string) string { COALESCE(SUM(c.deletions), 0) as total_deletions FROM task_session_commits c JOIN task_sessions s ON s.id = c.session_id - WHERE s.repository_id != '' AND (? IS NULL OR c.committed_at >= ?) + JOIN tasks t ON t.id = s.task_id + WHERE t.is_ephemeral = 0 AND s.repository_id != '' AND (? IS NULL OR c.committed_at >= ?) GROUP BY s.repository_id ) git_stats ON git_stats.repository_id = r.id WHERE r.workspace_id = ? AND r.deleted_at IS NULL @@ -438,7 +442,7 @@ func (r *Repository) GetAgentUsage(ctx context.Context, workspaceID string, limi FROM task_sessions s JOIN tasks t ON t.id = s.task_id LEFT JOIN task_session_turns turn ON turn.task_session_id = s.id - WHERE t.workspace_id = ? AND s.agent_profile_id != '' AND (? IS NULL OR s.started_at >= ?) + WHERE t.workspace_id = ? AND t.is_ephemeral = 0 AND s.agent_profile_id != '' AND (? IS NULL OR s.started_at >= ?) GROUP BY s.agent_profile_id ORDER BY session_count DESC LIMIT ? @@ -484,7 +488,7 @@ func (r *Repository) GetGitStats(ctx context.Context, workspaceID string, start FROM task_session_commits c JOIN task_sessions s ON s.id = c.session_id JOIN tasks t ON t.id = s.task_id - WHERE t.workspace_id = ? AND (? IS NULL OR c.committed_at >= ?) + WHERE t.workspace_id = ? AND t.is_ephemeral = 0 AND (? IS NULL OR c.committed_at >= ?) ` var stats models.GitStats