-- name: InsertTask :one INSERT INTO tasks (id, organization_id, owner_id, name, display_name, workspace_id, template_version_id, template_parameters, prompt, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *; -- name: UpdateTaskWorkspaceID :one UPDATE tasks SET workspace_id = $2 FROM workspaces w JOIN template_versions tv ON tv.template_id = w.template_id WHERE tasks.id = $1 AND tasks.workspace_id IS NULL AND w.id = $2 AND tv.id = tasks.template_version_id RETURNING tasks.*; -- name: UpsertTaskWorkspaceApp :one INSERT INTO task_workspace_apps (task_id, workspace_build_number, workspace_agent_id, workspace_app_id) VALUES ($1, $2, $3, $4) ON CONFLICT (task_id, workspace_build_number) DO UPDATE SET workspace_agent_id = EXCLUDED.workspace_agent_id, workspace_app_id = EXCLUDED.workspace_app_id RETURNING *; -- name: GetTaskByID :one SELECT * FROM tasks_with_status WHERE id = @id::uuid; -- name: GetTaskByWorkspaceID :one SELECT * FROM tasks_with_status WHERE workspace_id = @workspace_id::uuid; -- name: GetTaskByOwnerIDAndName :one SELECT * FROM tasks_with_status WHERE owner_id = @owner_id::uuid AND deleted_at IS NULL AND LOWER(name) = LOWER(@name::text); -- name: ListTasks :many SELECT * FROM tasks_with_status tws WHERE tws.deleted_at IS NULL AND CASE WHEN @owner_id::UUID != '00000000-0000-0000-0000-000000000000' THEN tws.owner_id = @owner_id::UUID ELSE TRUE END AND CASE WHEN @organization_id::UUID != '00000000-0000-0000-0000-000000000000' THEN tws.organization_id = @organization_id::UUID ELSE TRUE END AND CASE WHEN @status::text != '' THEN tws.status = @status::task_status ELSE TRUE END ORDER BY tws.created_at DESC; -- name: DeleteTask :one WITH deleted_task AS ( UPDATE tasks SET deleted_at = @deleted_at::timestamptz WHERE id = @id::uuid AND deleted_at IS NULL RETURNING id ), deleted_snapshot AS ( DELETE FROM task_snapshots WHERE task_id = @id::uuid ) SELECT id FROM deleted_task; -- name: UpdateTaskPrompt :one UPDATE tasks SET prompt = @prompt::text WHERE id = @id::uuid AND deleted_at IS NULL RETURNING *; -- name: UpsertTaskSnapshot :exec INSERT INTO task_snapshots (task_id, log_snapshot, log_snapshot_created_at) VALUES ($1, $2, $3) ON CONFLICT (task_id) DO UPDATE SET log_snapshot = EXCLUDED.log_snapshot, log_snapshot_created_at = EXCLUDED.log_snapshot_created_at; -- name: GetTaskSnapshot :one SELECT * FROM task_snapshots WHERE task_id = $1; -- name: GetTelemetryTaskEvents :many -- Returns all data needed to build task lifecycle events for telemetry -- in a single round-trip. For each task whose workspace is in the -- given set, fetches: -- - the latest workspace app binding (task_workspace_apps) -- - the most recent stop and start builds (workspace_builds) -- - the last "working" app status (workspace_app_statuses) -- - the first app status after resume, for active workspaces -- -- Assumptions: -- - 1:1 relationship between tasks and workspaces. All builds on the -- workspace are considered task-related. -- - Idle duration approximation: If the agent reports "working", does -- work, then reports "done", we miss that working time. -- - lws and active_dur join across all historical app IDs for the task, -- because each resume cycle provisions a new app ID. This ensures -- pre-pause statuses contribute to idle duration and active duration. WITH task_app_ids AS ( SELECT task_id, workspace_app_id FROM task_workspace_apps ), task_status_timeline AS ( -- All app statuses across every historical app for each task, -- plus synthetic "boundary" rows at each stop/start build transition. -- This allows us to correctly take gaps due to pause/resume into account. SELECT tai.task_id, was.created_at, was.state::text AS state FROM workspace_app_statuses was JOIN task_app_ids tai ON tai.workspace_app_id = was.app_id UNION ALL SELECT t.id AS task_id, wb.created_at, '_boundary' AS state FROM tasks t JOIN workspace_builds wb ON wb.workspace_id = t.workspace_id WHERE t.deleted_at IS NULL AND t.workspace_id IS NOT NULL AND wb.build_number > 1 ), task_event_data AS ( SELECT t.id AS task_id, t.workspace_id, twa.workspace_app_id, -- Latest stop build. stop_build.created_at AS stop_build_created_at, stop_build.reason AS stop_build_reason, -- Latest start build (task_resume only). start_build.created_at AS start_build_created_at, start_build.reason AS start_build_reason, start_build.build_number AS start_build_number, -- Last "working" app status (for idle duration). lws.created_at AS last_working_status_at, -- First app status after resume (for resume-to-status duration). -- Only populated for workspaces in an active phase (started more -- recently than stopped). fsar.created_at AS first_status_after_resume_at, -- Cumulative time spent in "working" state. active_dur.total_working_ms AS active_duration_ms FROM tasks t LEFT JOIN LATERAL ( SELECT task_app.workspace_app_id FROM task_workspace_apps task_app WHERE task_app.task_id = t.id ORDER BY task_app.workspace_build_number DESC LIMIT 1 ) twa ON TRUE LEFT JOIN LATERAL ( SELECT wb.created_at, wb.reason, wb.build_number FROM workspace_builds wb WHERE wb.workspace_id = t.workspace_id AND wb.transition = 'stop' ORDER BY wb.build_number DESC LIMIT 1 ) stop_build ON TRUE LEFT JOIN LATERAL ( SELECT wb.created_at, wb.reason, wb.build_number FROM workspace_builds wb WHERE wb.workspace_id = t.workspace_id AND wb.transition = 'start' ORDER BY wb.build_number DESC LIMIT 1 ) start_build ON TRUE LEFT JOIN LATERAL ( SELECT tst.created_at FROM task_status_timeline tst WHERE tst.task_id = t.id AND tst.state = 'working' -- Only consider status before the latest pause so that -- post-resume statuses don't mask pre-pause idle time. AND (stop_build.created_at IS NULL OR tst.created_at <= stop_build.created_at) ORDER BY tst.created_at DESC LIMIT 1 ) lws ON TRUE LEFT JOIN LATERAL ( SELECT was.created_at FROM workspace_app_statuses was WHERE was.app_id = twa.workspace_app_id AND was.created_at > start_build.created_at ORDER BY was.created_at ASC LIMIT 1 ) fsar ON twa.workspace_app_id IS NOT NULL AND start_build.created_at IS NOT NULL AND (stop_build.created_at IS NULL OR start_build.created_at > stop_build.created_at) -- Active duration: cumulative time spent in "working" state across all -- historical app IDs for this task. Uses LEAD() to convert point-in-time -- statuses into intervals, then sums intervals where state='working'. For -- the last status, falls back to stop_build time (if paused) or @now (if -- still running). LEFT JOIN LATERAL ( SELECT COALESCE( SUM(EXTRACT(EPOCH FROM (interval_end - interval_start)) * 1000)::bigint, 0 )::bigint AS total_working_ms FROM ( SELECT tst.created_at AS interval_start, COALESCE( LEAD(tst.created_at) OVER (ORDER BY tst.created_at ASC, CASE WHEN tst.state = '_boundary' THEN 1 ELSE 0 END ASC), CASE WHEN stop_build.created_at IS NOT NULL AND (start_build.created_at IS NULL OR stop_build.created_at > start_build.created_at) THEN stop_build.created_at ELSE @now::timestamptz END ) AS interval_end, tst.state FROM task_status_timeline tst WHERE tst.task_id = t.id ) intervals WHERE intervals.state = 'working' ) active_dur ON TRUE WHERE t.deleted_at IS NULL AND t.workspace_id IS NOT NULL AND EXISTS ( SELECT 1 FROM workspace_builds wb WHERE wb.workspace_id = t.workspace_id AND wb.created_at > @created_after ) ) SELECT * FROM task_event_data ORDER BY task_id;