diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index ae4dcd728e..6cd3eb56bf 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -248,7 +248,10 @@ CREATE TYPE build_reason AS ENUM ( 'cli', 'ssh_connection', 'vscode_connection', - 'jetbrains_connection' + 'jetbrains_connection', + 'task_auto_pause', + 'task_manual_pause', + 'task_resume' ); CREATE TYPE connection_status AS ENUM ( @@ -1846,6 +1849,20 @@ CREATE TABLE tailnet_tunnels ( updated_at timestamp with time zone NOT NULL ); +CREATE TABLE task_snapshots ( + task_id uuid NOT NULL, + log_snapshot jsonb NOT NULL, + log_snapshot_created_at timestamp with time zone DEFAULT now() NOT NULL +); + +COMMENT ON TABLE task_snapshots IS 'Stores snapshots of task state when paused, currently limited to conversation history.'; + +COMMENT ON COLUMN task_snapshots.task_id IS 'The task this snapshot belongs to.'; + +COMMENT ON COLUMN task_snapshots.log_snapshot IS 'Task conversation history in JSON format, allowing users to view logs when the workspace is stopped.'; + +COMMENT ON COLUMN task_snapshots.log_snapshot_created_at IS 'When this log snapshot was captured.'; + CREATE TABLE task_workspace_apps ( task_id uuid NOT NULL, workspace_agent_id uuid, @@ -3157,6 +3174,9 @@ ALTER TABLE ONLY tailnet_peers ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_pkey PRIMARY KEY (coordinator_id, src_id, dst_id); +ALTER TABLE ONLY task_snapshots + ADD CONSTRAINT task_snapshots_pkey PRIMARY KEY (task_id); + ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_pkey PRIMARY KEY (task_id, workspace_build_number); @@ -3720,6 +3740,9 @@ ALTER TABLE ONLY tailnet_peers ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; +ALTER TABLE ONLY task_snapshots + ADD CONSTRAINT task_snapshots_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; + ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 0c295e4316..ac2c87fc95 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -46,6 +46,7 @@ const ( ForeignKeyTailnetClientsCoordinatorID ForeignKeyConstraint = "tailnet_clients_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_clients ADD CONSTRAINT tailnet_clients_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; ForeignKeyTailnetPeersCoordinatorID ForeignKeyConstraint = "tailnet_peers_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; ForeignKeyTailnetTunnelsCoordinatorID ForeignKeyConstraint = "tailnet_tunnels_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; + ForeignKeyTaskSnapshotsTaskID ForeignKeyConstraint = "task_snapshots_task_id_fkey" // ALTER TABLE ONLY task_snapshots ADD CONSTRAINT task_snapshots_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; ForeignKeyTaskWorkspaceAppsTaskID ForeignKeyConstraint = "task_workspace_apps_task_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; ForeignKeyTaskWorkspaceAppsWorkspaceAgentID ForeignKeyConstraint = "task_workspace_apps_workspace_agent_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; ForeignKeyTaskWorkspaceAppsWorkspaceAppID ForeignKeyConstraint = "task_workspace_apps_workspace_app_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_workspace_app_id_fkey FOREIGN KEY (workspace_app_id) REFERENCES workspace_apps(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000409_task_lifecycle.down.sql b/coderd/database/migrations/000409_task_lifecycle.down.sql new file mode 100644 index 0000000000..b90abe3785 --- /dev/null +++ b/coderd/database/migrations/000409_task_lifecycle.down.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS task_snapshots; + +-- Note: Cannot remove enum values in PostgreSQL. +-- The build_reason enum values (task_auto_pause, task_manual_pause, task_resume) +-- will remain but become unused. diff --git a/coderd/database/migrations/000409_task_lifecycle.up.sql b/coderd/database/migrations/000409_task_lifecycle.up.sql new file mode 100644 index 0000000000..c061db85df --- /dev/null +++ b/coderd/database/migrations/000409_task_lifecycle.up.sql @@ -0,0 +1,19 @@ +-- Create task_snapshots table for storing log snapshots when tasks are paused. +-- This table holds the conversation history from AgentAPI, allowing users to view +-- task logs even when the workspace is stopped. +CREATE TABLE task_snapshots ( + task_id UUID NOT NULL PRIMARY KEY REFERENCES tasks (id) ON DELETE CASCADE, + log_snapshot JSONB NOT NULL, + log_snapshot_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +COMMENT ON TABLE task_snapshots IS 'Stores snapshots of task state when paused, currently limited to conversation history.'; +COMMENT ON COLUMN task_snapshots.task_id IS 'The task this snapshot belongs to.'; +COMMENT ON COLUMN task_snapshots.log_snapshot IS 'Task conversation history in JSON format, allowing users to view logs when the workspace is stopped.'; +COMMENT ON COLUMN task_snapshots.log_snapshot_created_at IS 'When this log snapshot was captured.'; + +-- Add build reasons for task lifecycle events. +-- These distinguish task pause/resume operations from regular workspace lifecycle events. +ALTER TYPE build_reason ADD VALUE IF NOT EXISTS 'task_auto_pause'; +ALTER TYPE build_reason ADD VALUE IF NOT EXISTS 'task_manual_pause'; +ALTER TYPE build_reason ADD VALUE IF NOT EXISTS 'task_resume'; diff --git a/coderd/database/migrations/testdata/fixtures/000409_task_lifecycle.up.sql b/coderd/database/migrations/testdata/fixtures/000409_task_lifecycle.up.sql new file mode 100644 index 0000000000..41eb31cdb2 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000409_task_lifecycle.up.sql @@ -0,0 +1,16 @@ +INSERT INTO task_snapshots VALUES ( + 'f5a1c3e4-8b2d-4f6a-9d7e-2a8b5c9e1f3d', -- task_id (references existing task from 000366) + '{ + "format": "agentapi", + "data": { + "messages": [ + {"id": 0, "type": "output", "content": "Starting task execution...", "time": "2024-11-02T13:10:05Z"}, + {"id": 1, "type": "input", "content": "Create a React component for tasks", "time": "2024-11-02T13:10:06Z"}, + {"id": 2, "type": "output", "content": "Creating component structure...", "time": "2024-11-02T13:10:10Z"} + ], + "truncated": false, + "total_count": 3 + } + }'::JSONB, -- log_snapshot + '2024-11-02 13:15:00.000000+02' -- log_snapshot_at +) ON CONFLICT DO NOTHING; diff --git a/coderd/database/models.go b/coderd/database/models.go index 2f0a14b2e5..dc56bcebeb 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -936,6 +936,9 @@ const ( BuildReasonSshConnection BuildReason = "ssh_connection" BuildReasonVscodeConnection BuildReason = "vscode_connection" BuildReasonJetbrainsConnection BuildReason = "jetbrains_connection" + BuildReasonTaskAutoPause BuildReason = "task_auto_pause" + BuildReasonTaskManualPause BuildReason = "task_manual_pause" + BuildReasonTaskResume BuildReason = "task_resume" ) func (e *BuildReason) Scan(src interface{}) error { @@ -985,7 +988,10 @@ func (e BuildReason) Valid() bool { BuildReasonCli, BuildReasonSshConnection, BuildReasonVscodeConnection, - BuildReasonJetbrainsConnection: + BuildReasonJetbrainsConnection, + BuildReasonTaskAutoPause, + BuildReasonTaskManualPause, + BuildReasonTaskResume: return true } return false @@ -1004,6 +1010,9 @@ func AllBuildReasonValues() []BuildReason { BuildReasonSshConnection, BuildReasonVscodeConnection, BuildReasonJetbrainsConnection, + BuildReasonTaskAutoPause, + BuildReasonTaskManualPause, + BuildReasonTaskResume, } } @@ -4235,6 +4244,16 @@ type Task struct { OwnerAvatarUrl string `db:"owner_avatar_url" json:"owner_avatar_url"` } +// Stores snapshots of task state when paused, currently limited to conversation history. +type TaskSnapshot struct { + // The task this snapshot belongs to. + TaskID uuid.UUID `db:"task_id" json:"task_id"` + // Task conversation history in JSON format, allowing users to view logs when the workspace is stopped. + LogSnapshot json.RawMessage `db:"log_snapshot" json:"log_snapshot"` + // When this log snapshot was captured. + LogSnapshotCreatedAt time.Time `db:"log_snapshot_created_at" json:"log_snapshot_created_at"` +} + type TaskTable struct { ID uuid.UUID `db:"id" json:"id"` OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 94fe9cc239..dc2c3bd1dc 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -59,6 +59,7 @@ const ( UniqueTailnetCoordinatorsPkey UniqueConstraint = "tailnet_coordinators_pkey" // ALTER TABLE ONLY tailnet_coordinators ADD CONSTRAINT tailnet_coordinators_pkey PRIMARY KEY (id); UniqueTailnetPeersPkey UniqueConstraint = "tailnet_peers_pkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_pkey PRIMARY KEY (id, coordinator_id); UniqueTailnetTunnelsPkey UniqueConstraint = "tailnet_tunnels_pkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_pkey PRIMARY KEY (coordinator_id, src_id, dst_id); + UniqueTaskSnapshotsPkey UniqueConstraint = "task_snapshots_pkey" // ALTER TABLE ONLY task_snapshots ADD CONSTRAINT task_snapshots_pkey PRIMARY KEY (task_id); UniqueTaskWorkspaceAppsPkey UniqueConstraint = "task_workspace_apps_pkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_pkey PRIMARY KEY (task_id, workspace_build_number); UniqueTasksPkey UniqueConstraint = "tasks_pkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_pkey PRIMARY KEY (id); UniqueTelemetryItemsPkey UniqueConstraint = "telemetry_items_pkey" // ALTER TABLE ONLY telemetry_items ADD CONSTRAINT telemetry_items_pkey PRIMARY KEY (key);