diff --git a/packages/db/src/migrations/0375_short_mercury.sql b/packages/db/src/migrations/0375_short_mercury.sql new file mode 100644 index 00000000000..eac51f532dc --- /dev/null +++ b/packages/db/src/migrations/0375_short_mercury.sql @@ -0,0 +1,2 @@ +DROP INDEX "execution_workspace_restore_points_live_checkout_uniq";--> statement-breakpoint +CREATE UNIQUE INDEX "execution_workspace_restore_points_live_status_uniq" ON "execution_workspace_restore_points" USING btree ("execution_workspace_id","generation","checkout_name") WHERE "execution_workspace_restore_points"."status" in ('pending', 'active', 'expiring'); \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0370_snapshot.json b/packages/db/src/migrations/meta/0375_snapshot.json similarity index 99% rename from packages/db/src/migrations/meta/0370_snapshot.json rename to packages/db/src/migrations/meta/0375_snapshot.json index d6870986af0..7bd1861bd2c 100644 --- a/packages/db/src/migrations/meta/0370_snapshot.json +++ b/packages/db/src/migrations/meta/0375_snapshot.json @@ -1,6 +1,6 @@ { - "id": "a7c9681c-fb33-47c3-8139-3d88d7bbfe19", - "prevId": "9fa514b5-5cd1-4350-9b77-f60924655298", + "id": "3e17203b-ec95-41e9-8363-685982450199", + "prevId": "945c31ed-f39f-4272-a91d-85de925f5361", "version": "7", "dialect": "postgresql", "tables": { @@ -8252,6 +8252,24 @@ "name": "case_handoffs", "schema": "", "columns": { + "workflow_transition": { + "name": "workflow_transition", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_prompt": { + "name": "workflow_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_workflow_document_revision_id": { + "name": "base_workflow_document_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "id": { "name": "id", "type": "uuid", @@ -9067,6 +9085,12 @@ "primaryKey": false, "notNull": false }, + "workflow_id": { + "name": "workflow_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "handoff_version": { "name": "handoff_version", "type": "integer", @@ -9410,6 +9434,19 @@ "onDelete": "restrict", "onUpdate": "no action" }, + "cases_workflow_id_workflows_id_fk": { + "name": "cases_workflow_id_workflows_id_fk", + "tableFrom": "cases", + "tableTo": "workflows", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, "cases_parent_case_id_cases_id_fk": { "name": "cases_parent_case_id_cases_id_fk", "tableFrom": "cases", @@ -28436,6 +28473,250 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.execution_workspace_restore_points": { + "name": "execution_workspace_restore_points", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checkout_name": { + "name": "checkout_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tree_closed_at": { + "name": "tree_closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delete_after": { + "name": "delete_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_generation": { + "name": "review_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspace_restore_points_object_key_uniq": { + "name": "execution_workspace_restore_points_object_key_uniq", + "columns": [ + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspace_restore_points_live_status_uniq": { + "name": "execution_workspace_restore_points_live_status_uniq", + "columns": [ + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "checkout_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"execution_workspace_restore_points\".\"status\" in ('pending', 'active', 'expiring')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspace_restore_points_status_delete_after_idx": { + "name": "execution_workspace_restore_points_status_delete_after_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delete_after", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspace_restore_points_company_idx": { + "name": "execution_workspace_restore_points_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspace_restore_points_source_issue_idx": { + "name": "execution_workspace_restore_points_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspace_restore_points_active_source_idx": { + "name": "execution_workspace_restore_points_active_source_idx", + "columns": [ + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_workspace_restore_points\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.execution_workspace_runtime_leases": { "name": "execution_workspace_runtime_leases", "schema": "", @@ -38590,6 +38871,18 @@ "primaryKey": false, "notNull": false }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dismissed_by_user_id": { + "name": "dismissed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, "created_at": { "name": "created_at", "type": "timestamp with time zone", @@ -59279,6 +59572,113 @@ }, "isRLSEnabled": false }, + "public.workflows": { + "name": "workflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflows_company_idx": { + "name": "workflows_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflows_company_id_companies_id_fk": { + "name": "workflows_company_id_companies_id_fk", + "tableFrom": "workflows", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflows_created_by_agent_id_agents_id_fk": { + "name": "workflows_created_by_agent_id_agents_id_fk", + "tableFrom": "workflows", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.workspace_operations": { "name": "workspace_operations", "schema": "", diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 2e91dea8c03..801be2f3d07 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -2612,6 +2612,13 @@ "when": 1792713110842, "tag": "0374_rare_wolfpack", "breakpoints": true + }, + { + "idx": 375, + "version": "7", + "when": 1792775097887, + "tag": "0375_short_mercury", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/execution_workspace_restore_points.ts b/packages/db/src/schema/execution_workspace_restore_points.ts index afb5e0aeea4..fd1f6dc8195 100644 --- a/packages/db/src/schema/execution_workspace_restore_points.ts +++ b/packages/db/src/schema/execution_workspace_restore_points.ts @@ -35,9 +35,9 @@ export const executionWorkspaceRestorePoints = pgTable( }, (table) => ({ objectKeyUniqueIdx: uniqueIndex("execution_workspace_restore_points_object_key_uniq").on(table.objectKey), - liveGenerationUniqueIdx: uniqueIndex("execution_workspace_restore_points_live_checkout_uniq") + liveGenerationUniqueIdx: uniqueIndex("execution_workspace_restore_points_live_status_uniq") .on(table.executionWorkspaceId, table.generation, table.checkoutName) - .where(sql`${table.status} <> 'deleted'`), + .where(sql`${table.status} in ('pending', 'active', 'expiring')`), statusDeleteAfterIdx: index("execution_workspace_restore_points_status_delete_after_idx").on(table.status, table.deleteAfter), companyIdx: index("execution_workspace_restore_points_company_idx").on(table.companyId), sourceIssueIdx: index("execution_workspace_restore_points_source_issue_idx").on(table.companyId, table.sourceIssueId), diff --git a/server/src/__tests__/git-workspace-expiration.test.ts b/server/src/__tests__/git-workspace-expiration.test.ts index 4a5b6a7d307..8e9db5c0b8c 100644 --- a/server/src/__tests__/git-workspace-expiration.test.ts +++ b/server/src/__tests__/git-workspace-expiration.test.ts @@ -389,6 +389,24 @@ describeEmbeddedPostgres("reconstructable Git expiration", () => { vi.unstubAllEnvs(); }); + it("expires despite a stale pending restore point left by an earlier attempt", async () => { + vi.stubEnv("PAPERCLIP_SECRETS_MASTER_KEY", "31".repeat(32)); + const f = await nestedFixture(false); + await unpushedNestedWork(f.nested.cwd); + const generation = Number((await f.row()).metadata?.lifecycleGeneration ?? 0) + 1; + const base = { companyId: f.companyId, executionWorkspaceId: f.workspaceId, sourceIssueId: f.issueId, generation, + sha256: "0".repeat(64), byteSize: 1 }; + const [stale] = await db.insert(executionWorkspaceRestorePoints).values([ + { ...base, checkoutName: path.basename(f.nested.cwd), objectKey: `stale/${randomUUID()}.pcrp`, status: "pending" }, + { ...base, checkoutName: "", objectKey: `stale/${randomUUID()}.pcrp`, status: "superseded" }, + ]).returning(); + expect(await f.expire({ expireBase: false, restorePoint: restorePoint() })).toMatchObject({ expired: true, restorePoint: true, skipReason: null }); + const rows = await ledger(f.workspaceId); + expect(rows.find(row => row.id === stale!.id)).toMatchObject({ status: "superseded" }); + expect(rows.filter(row => row.status === "active").map(row => row.checkoutName)).toEqual([path.basename(f.nested.cwd)]); + vi.unstubAllEnvs(); + }); + it("keeps a workspace whose nested checkout has unpushed work on another branch", async () => { vi.stubEnv("PAPERCLIP_SECRETS_MASTER_KEY", "31".repeat(32)); const f = await nestedFixture(false); diff --git a/server/src/services/git-restore-point-storage.ts b/server/src/services/git-restore-point-storage.ts index 1b7fb4bc9b9..32fb3dc15df 100644 --- a/server/src/services/git-restore-point-storage.ts +++ b/server/src/services/git-restore-point-storage.ts @@ -101,6 +101,11 @@ export async function storeGitRestorePoint(input: { const objectKey = restorePointObjectKey(input.workspace.companyId, input.workspace.id, input.generation, id, input.checkout); const [ledger] = await input.db.transaction(async (tx) => { await notifyGitRestorePointWork(tx, `workspace:${input.workspace.id}`); + await tx.update(executionWorkspaceRestorePoints).set({ status: "superseded", updatedAt: new Date() }) + .where(and(eq(executionWorkspaceRestorePoints.executionWorkspaceId, input.workspace.id), + eq(executionWorkspaceRestorePoints.generation, input.generation), + eq(executionWorkspaceRestorePoints.checkoutName, input.checkout ?? ""), + eq(executionWorkspaceRestorePoints.status, "pending"))); return tx.insert(executionWorkspaceRestorePoints).values({ companyId: input.workspace.companyId, executionWorkspaceId: input.workspace.id,