diff --git a/apps/web/app/workflows/chat-post-finish-usage.test.ts b/apps/web/app/workflows/chat-post-finish-usage.test.ts index 66acd3be3..6f9e3bf23 100644 --- a/apps/web/app/workflows/chat-post-finish-usage.test.ts +++ b/apps/web/app/workflows/chat-post-finish-usage.test.ts @@ -28,6 +28,7 @@ function makeAssistantMessage( const spies = { recordUsage: mock(() => Promise.resolve()), + recordWorkflowRun: mock(() => Promise.resolve()), collectTaskToolUsageEvents: mock( (_message?: unknown) => [] as Array<{ @@ -64,6 +65,10 @@ mock.module("@/lib/db/usage", () => ({ recordUsage: spies.recordUsage, })); +mock.module("@/lib/db/workflow-runs", () => ({ + recordWorkflowRun: spies.recordWorkflowRun, +})); + mock.module("@open-harness/agent", () => ({ collectTaskToolUsageEvents: spies.collectTaskToolUsageEvents, sumLanguageModelUsage: spies.sumLanguageModelUsage, @@ -96,6 +101,96 @@ describe("recordWorkflowUsage", () => { }); }); + test("records workflow run timing when provided", async () => { + await recordWorkflowUsage( + "user-1", + "gpt-4", + undefined, + makeAssistantMessage(), + undefined, + { + workflowRunId: "wrun-1", + chatId: "chat-1", + sessionId: "session-1", + status: "completed", + startedAt: "2026-01-01T00:00:00.000Z", + finishedAt: "2026-01-01T00:00:05.000Z", + totalDurationMs: 5000, + stepTimings: [ + { + stepNumber: 1, + startedAt: "2026-01-01T00:00:00.000Z", + finishedAt: "2026-01-01T00:00:02.000Z", + durationMs: 2000, + finishReason: "tool-calls", + rawFinishReason: "provider_tool_use", + }, + { + stepNumber: 2, + startedAt: "2026-01-01T00:00:02.000Z", + finishedAt: "2026-01-01T00:00:05.000Z", + durationMs: 3000, + finishReason: "stop", + rawFinishReason: "provider_stop", + }, + ], + }, + ); + + expect(spies.recordWorkflowRun).toHaveBeenCalledTimes(1); + const calls = spies.recordWorkflowRun.mock.calls as unknown[][]; + expect(calls[0][0]).toMatchObject({ + id: "wrun-1", + chatId: "chat-1", + sessionId: "session-1", + userId: "user-1", + modelId: "gpt-4", + status: "completed", + totalDurationMs: 5000, + stepTimings: [ + expect.objectContaining({ stepNumber: 1, durationMs: 2000 }), + expect.objectContaining({ stepNumber: 2, durationMs: 3000 }), + ], + }); + }); + + test("continues recording usage when workflow run persistence fails", async () => { + spies.recordWorkflowRun.mockImplementationOnce(() => + Promise.reject(new Error("workflow runs table missing")), + ); + + const usage = makeUsage({ + inputTokens: 100, + outputTokens: 50, + totalTokens: 150, + }); + + await recordWorkflowUsage( + "user-1", + "gpt-4", + usage, + makeAssistantMessage(), + undefined, + { + workflowRunId: "wrun-1", + chatId: "chat-1", + sessionId: "session-1", + status: "completed", + startedAt: "2026-01-01T00:00:00.000Z", + finishedAt: "2026-01-01T00:00:05.000Z", + totalDurationMs: 5000, + stepTimings: [], + }, + ); + + expect(spies.recordWorkflowRun).toHaveBeenCalledTimes(1); + expect(spies.recordUsage).toHaveBeenCalledTimes(1); + expect((spies.recordUsage.mock.calls as unknown[][])[0][1]).toMatchObject({ + agentType: "main", + model: "gpt-4", + }); + }); + test("skips main recording when totalUsage is undefined", async () => { await recordWorkflowUsage( "user-1", diff --git a/apps/web/app/workflows/chat-post-finish.ts b/apps/web/app/workflows/chat-post-finish.ts index 212d935a1..03b3011a7 100644 --- a/apps/web/app/workflows/chat-post-finish.ts +++ b/apps/web/app/workflows/chat-post-finish.ts @@ -18,6 +18,11 @@ import { buildLifecycleActivityUpdate, } from "@/lib/sandbox/lifecycle"; import { dedupeMessageReasoning } from "@/lib/chat/dedupe-message-reasoning"; +import { + recordWorkflowRun, + type WorkflowRunStatus, + type WorkflowRunStepTiming, +} from "@/lib/db/workflow-runs"; import { recordUsage } from "@/lib/db/usage"; const cachedInputTokensFor = (usage: LanguageModelUsage) => @@ -229,6 +234,16 @@ export async function recordWorkflowUsage( totalUsage: LanguageModelUsage | undefined, responseMessage: WebAgentUIMessage, previousResponseMessage?: WebAgentUIMessage, + workflowRun?: { + workflowRunId: string; + chatId: string; + sessionId: string; + status: WorkflowRunStatus; + startedAt: string; + finishedAt: string; + totalDurationMs: number; + stepTimings: WorkflowRunStepTiming[]; + }, ): Promise { "use step"; @@ -236,6 +251,25 @@ export async function recordWorkflowUsage( const { collectTaskToolUsageEvents, sumLanguageModelUsage } = await import("@open-harness/agent"); + if (workflowRun) { + try { + await recordWorkflowRun({ + id: workflowRun.workflowRunId, + chatId: workflowRun.chatId, + sessionId: workflowRun.sessionId, + userId, + modelId, + status: workflowRun.status, + startedAt: workflowRun.startedAt, + finishedAt: workflowRun.finishedAt, + totalDurationMs: workflowRun.totalDurationMs, + stepTimings: workflowRun.stepTimings, + }); + } catch (error) { + console.error("[workflow] Failed to record workflow run:", error); + } + } + // Record main agent usage if (totalUsage) { await recordUsage(userId, { diff --git a/apps/web/app/workflows/chat.test.ts b/apps/web/app/workflows/chat.test.ts index bfe907fce..2c1441c99 100644 --- a/apps/web/app/workflows/chat.test.ts +++ b/apps/web/app/workflows/chat.test.ts @@ -308,6 +308,46 @@ describe("runAgentWorkflow", () => { expect(rwCalls[0][1]).toBe("gpt-4"); }); + test("marks workflow run as failed when maxSteps is exhausted", async () => { + agentFinishReason = "tool-calls"; + agentRawFinishReason = "provider_tool_use"; + + await runAgentWorkflow( + makeOptions({ + maxSteps: 2, + }), + ); + + const rwCalls = spies.recordWorkflowUsage.mock.calls as unknown[][]; + const workflowRun = rwCalls[0][5] as { + workflowRunId: string; + status: string; + totalDurationMs: number; + stepTimings: Array<{ + stepNumber: number; + durationMs: number; + finishReason?: string; + }>; + }; + + expect(workflowRun.workflowRunId).toBe("wrun_test-123"); + expect(workflowRun.status).toBe("failed"); + expect(workflowRun.totalDurationMs).toBeGreaterThanOrEqual(0); + expect(workflowRun.stepTimings).toHaveLength(2); + expect(workflowRun.stepTimings).toEqual([ + expect.objectContaining({ + stepNumber: 1, + durationMs: expect.any(Number), + finishReason: "tool-calls", + }), + expect.objectContaining({ + stepNumber: 2, + durationMs: expect.any(Number), + finishReason: "tool-calls", + }), + ]); + }); + test("logs full step diagnostics when the agent finishes with reason other", async () => { agentFinishReason = "other"; agentRawFinishReason = "provider_other"; diff --git a/apps/web/app/workflows/chat.ts b/apps/web/app/workflows/chat.ts index 4c10f5c7b..380be8e41 100644 --- a/apps/web/app/workflows/chat.ts +++ b/apps/web/app/workflows/chat.ts @@ -31,6 +31,10 @@ import { runAutoCreatePrStep, } from "./chat-post-finish"; import { dedupeMessageReasoning } from "@/lib/chat/dedupe-message-reasoning"; +import type { + WorkflowRunStatus, + WorkflowRunStepTiming, +} from "@/lib/db/workflow-runs"; type Options = { messages: WebAgentUIMessage[]; @@ -96,6 +100,34 @@ const generateId = async () => { return generateIdAi(); }; +function buildStepTiming( + stepNumber: number, + startedAt: Date, + finishedAt: Date, + finishReason?: string, + rawFinishReason?: string, +): WorkflowRunStepTiming { + return { + stepNumber, + startedAt: startedAt.toISOString(), + finishedAt: finishedAt.toISOString(), + durationMs: finishedAt.getTime() - startedAt.getTime(), + finishReason, + rawFinishReason, + }; +} + +function isStepTimingError( + error: unknown, +): error is Error & { stepTiming: WorkflowRunStepTiming } { + return ( + error instanceof Error && + "stepTiming" in error && + typeof error.stepTiming === "object" && + error.stepTiming !== null + ); +} + function isObjectRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } @@ -440,10 +472,17 @@ export async function runAgentWorkflow(options: Options) { await sendStart(writable, assistantId); + const runStartedAt = new Date(); + const previousResponseMessage = + latestMessage.role === "assistant" ? latestMessage : undefined; + const stepTimings: WorkflowRunStepTiming[] = []; let wasAborted = false; + let exhaustedMaxSteps = false; let totalUsage: LanguageModelUsage | undefined; let finalFinishReason: FinishReason | undefined; let streamClosed = false; + let workflowStatus: WorkflowRunStatus = "completed"; + let caughtError: unknown; const sandboxState = options.agentOptions.sandbox?.state; try { @@ -452,18 +491,29 @@ export async function runAgentWorkflow(options: Options) { options.maxSteps === undefined || step < options.maxSteps; step++ ) { - const result = await runAgentStep( - modelMessages, - originalMessagesForStep, - assistantId, - writable, - workflowRunId, - options.chatId, - options.sessionId, - options.modelId, - options.agentOptions, - ); + let result: Awaited>; + + try { + result = await runAgentStep( + modelMessages, + originalMessagesForStep, + assistantId, + writable, + workflowRunId, + options.chatId, + options.sessionId, + options.modelId, + options.agentOptions, + step + 1, + ); + } catch (error) { + if (isStepTimingError(error)) { + stepTimings.push(error.stepTiming); + } + throw error; + } + stepTimings.push(result.stepTiming); pendingAssistantResponse = result.responseMessage ?? pendingAssistantResponse; originalMessagesForStep = [pendingAssistantResponse]; @@ -477,12 +527,18 @@ export async function runAgentWorkflow(options: Options) { : result.stepUsage; } - if ( - result.finishReason !== "tool-calls" || - shouldPauseForToolInteraction( + const shouldContinue = + result.finishReason === "tool-calls" && + !shouldPauseForToolInteraction( result.responseMessage?.parts ?? pendingAssistantResponse.parts, - ) - ) { + ); + + if (!shouldContinue) { + break; + } + + if (options.maxSteps !== undefined && step + 1 >= options.maxSteps) { + exhaustedMaxSteps = true; break; } } @@ -505,14 +561,6 @@ export async function runAgentWorkflow(options: Options) { // lost if later post-finish work fails. await persistAssistantMessage(options.chatId, pendingAssistantResponse); - await recordWorkflowUsage( - options.userId, - options.modelId, - totalUsage, - pendingAssistantResponse, - latestMessage.role === "assistant" ? latestMessage : undefined, - ); - // Persist the sandbox state so lifecycle timers stay accurate. if (sandboxState) { await persistSandboxState(options.sessionId, sandboxState); @@ -657,16 +705,50 @@ export async function runAgentWorkflow(options: Options) { if (sandboxState) { await refreshDiffCache(options.sessionId, sandboxState); } + + workflowStatus = wasAborted + ? "aborted" + : exhaustedMaxSteps + ? "failed" + : "completed"; + } catch (error) { + workflowStatus = wasAborted ? "aborted" : "failed"; + caughtError = error; } finally { - // On unexpected errors, still clear the active stream and close - // so the chat is never permanently marked as streaming. - if (!streamClosed) { - await Promise.all([ - clearActiveStream(options.chatId, workflowRunId), - sendFinish(writable).then(() => closeStream(writable)), - ]); + try { + // On unexpected errors, still clear the active stream and close + // so the chat is never permanently marked as streaming. + if (!streamClosed) { + await Promise.all([ + clearActiveStream(options.chatId, workflowRunId), + sendFinish(writable).then(() => closeStream(writable)), + ]); + } + } finally { + const runFinishedAt = new Date(); + await recordWorkflowUsage( + options.userId, + options.modelId, + totalUsage, + pendingAssistantResponse, + previousResponseMessage, + { + workflowRunId, + chatId: options.chatId, + sessionId: options.sessionId, + status: workflowStatus, + startedAt: runStartedAt.toISOString(), + finishedAt: runFinishedAt.toISOString(), + totalDurationMs: runFinishedAt.getTime() - runStartedAt.getTime(), + stepTimings, + }, + ); } } + + if (caughtError) { + throw caughtError; + } } const runAgentStep = async ( @@ -679,9 +761,11 @@ const runAgentStep = async ( sessionId: string, selectedModelId: string, agentOptions: OpenHarnessAgentCallOptions, + stepNumber: number, ) => { "use step"; + const stepStartedAt = new Date(); const { webAgent } = await import("@/app/config"); const abortController = new AbortController(); @@ -830,6 +914,8 @@ const runAgentStep = async ( ); } + const stepFinishedAt = new Date(); + return { responseMessage, responseMessages: response.messages, @@ -837,8 +923,17 @@ const runAgentStep = async ( rawFinishReason, stepUsage, stepWasAborted: false, + stepTiming: buildStepTiming( + stepNumber, + stepStartedAt, + stepFinishedAt, + finishReason, + rawFinishReason, + ), }; } catch (error) { + const stepFinishedAt = new Date(); + if (isAbortError(error)) { const abortedFinishReason: FinishReason = "stop"; return { @@ -848,10 +943,27 @@ const runAgentStep = async ( rawFinishReason: undefined, stepUsage: undefined, stepWasAborted: true, + stepTiming: buildStepTiming( + stepNumber, + stepStartedAt, + stepFinishedAt, + abortedFinishReason, + ), }; } - throw error; + const errorWithStepTiming = + error instanceof Error ? error : new Error(String(error)); + Object.assign(errorWithStepTiming, { + stepTiming: buildStepTiming( + stepNumber, + stepStartedAt, + stepFinishedAt, + "error", + errorWithStepTiming.name, + ), + }); + throw errorWithStepTiming; } finally { stopMonitor.stop(); await stopMonitor.done; diff --git a/apps/web/lib/db/migrations/0028_optimal_human_torch.sql b/apps/web/lib/db/migrations/0028_optimal_human_torch.sql new file mode 100644 index 000000000..926f2eccb --- /dev/null +++ b/apps/web/lib/db/migrations/0028_optimal_human_torch.sql @@ -0,0 +1,34 @@ +CREATE TABLE "workflow_run_steps" ( + "id" text PRIMARY KEY NOT NULL, + "workflow_run_id" text NOT NULL, + "step_number" integer NOT NULL, + "started_at" timestamp NOT NULL, + "finished_at" timestamp NOT NULL, + "duration_ms" integer NOT NULL, + "finish_reason" text, + "raw_finish_reason" text, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "workflow_runs" ( + "id" text PRIMARY KEY NOT NULL, + "chat_id" text NOT NULL, + "session_id" text NOT NULL, + "user_id" text NOT NULL, + "model_id" text, + "status" text NOT NULL, + "started_at" timestamp NOT NULL, + "finished_at" timestamp NOT NULL, + "total_duration_ms" integer NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "workflow_run_steps" ADD CONSTRAINT "workflow_run_steps_workflow_run_id_workflow_runs_id_fk" FOREIGN KEY ("workflow_run_id") REFERENCES "public"."workflow_runs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workflow_runs" ADD CONSTRAINT "workflow_runs_chat_id_chats_id_fk" FOREIGN KEY ("chat_id") REFERENCES "public"."chats"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workflow_runs" ADD CONSTRAINT "workflow_runs_session_id_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workflow_runs" ADD CONSTRAINT "workflow_runs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "workflow_run_steps_run_id_idx" ON "workflow_run_steps" USING btree ("workflow_run_id");--> statement-breakpoint +CREATE UNIQUE INDEX "workflow_run_steps_run_step_idx" ON "workflow_run_steps" USING btree ("workflow_run_id","step_number");--> statement-breakpoint +CREATE INDEX "workflow_runs_chat_id_idx" ON "workflow_runs" USING btree ("chat_id");--> statement-breakpoint +CREATE INDEX "workflow_runs_session_id_idx" ON "workflow_runs" USING btree ("session_id");--> statement-breakpoint +CREATE INDEX "workflow_runs_user_id_idx" ON "workflow_runs" USING btree ("user_id"); \ No newline at end of file diff --git a/apps/web/lib/db/migrations/meta/0028_snapshot.json b/apps/web/lib/db/migrations/meta/0028_snapshot.json new file mode 100644 index 000000000..e671ac47a --- /dev/null +++ b/apps/web/lib/db/migrations/meta/0028_snapshot.json @@ -0,0 +1,1611 @@ +{ + "id": "bd4e8f58-7708-4bd8-91e6-99cc595639df", + "prevId": "c9258f1c-fc45-4ecc-8f1d-f0594aacdd8b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "external_user_id": { + "name": "external_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_user_id_provider_idx": { + "name": "accounts_user_id_provider_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_messages": { + "name": "chat_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parts": { + "name": "parts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_messages_chat_id_chats_id_fk": { + "name": "chat_messages_chat_id_chats_id_fk", + "tableFrom": "chat_messages", + "tableTo": "chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_reads": { + "name": "chat_reads", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_reads_chat_id_idx": { + "name": "chat_reads_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_reads_user_id_users_id_fk": { + "name": "chat_reads_user_id_users_id_fk", + "tableFrom": "chat_reads", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_reads_chat_id_chats_id_fk": { + "name": "chat_reads_chat_id_chats_id_fk", + "tableFrom": "chat_reads", + "tableTo": "chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_reads_user_id_chat_id_pk": { + "name": "chat_reads_user_id_chat_id_pk", + "columns": ["user_id", "chat_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chats": { + "name": "chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'anthropic/claude-haiku-4.5'" + }, + "active_stream_id": { + "name": "active_stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_assistant_message_at": { + "name": "last_assistant_message_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chats_session_id_idx": { + "name": "chats_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chats_session_id_sessions_id_fk": { + "name": "chats_session_id_sessions_id_fk", + "tableFrom": "chats", + "tableTo": "sessions", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_installations": { + "name": "github_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_selection": { + "name": "repository_selection", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_url": { + "name": "installation_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_installations_user_installation_idx": { + "name": "github_installations_user_installation_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_installations_user_account_idx": { + "name": "github_installations_user_account_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_login", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_installations_user_id_users_id_fk": { + "name": "github_installations_user_id_users_id_fk", + "tableFrom": "github_installations", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.linked_accounts": { + "name": "linked_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "linked_accounts_provider_external_workspace_idx": { + "name": "linked_accounts_provider_external_workspace_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "linked_accounts_user_id_users_id_fk": { + "name": "linked_accounts_user_id_users_id_fk", + "tableFrom": "linked_accounts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "clone_url": { + "name": "clone_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vercel_project_id": { + "name": "vercel_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vercel_project_name": { + "name": "vercel_project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vercel_team_id": { + "name": "vercel_team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vercel_team_slug": { + "name": "vercel_team_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_new_branch": { + "name": "is_new_branch", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_commit_push_override": { + "name": "auto_commit_push_override", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "auto_create_pr_override": { + "name": "auto_create_pr_override", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "global_skill_refs": { + "name": "global_skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sandbox_state": { + "name": "sandbox_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lifecycle_state": { + "name": "lifecycle_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle_version": { + "name": "lifecycle_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sandbox_expires_at": { + "name": "sandbox_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "hibernate_after": { + "name": "hibernate_after", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lifecycle_run_id": { + "name": "lifecycle_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle_error": { + "name": "lifecycle_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lines_added": { + "name": "lines_added", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "lines_removed": { + "name": "lines_removed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_status": { + "name": "pr_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "snapshot_url": { + "name": "snapshot_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "snapshot_created_at": { + "name": "snapshot_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "snapshot_size_bytes": { + "name": "snapshot_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cached_diff": { + "name": "cached_diff", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cached_diff_updated_at": { + "name": "cached_diff_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shares": { + "name": "shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "shares_chat_id_idx": { + "name": "shares_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shares_chat_id_chats_id_fk": { + "name": "shares_chat_id_chats_id_fk", + "tableFrom": "shares", + "tableTo": "chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_events": { + "name": "usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'web'" + }, + "agent_type": { + "name": "agent_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_call_count": { + "name": "tool_call_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "usage_events_user_id_users_id_fk": { + "name": "usage_events_user_id_users_id_fk", + "tableFrom": "usage_events", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_model_id": { + "name": "default_model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'anthropic/claude-haiku-4.5'" + }, + "default_subagent_model_id": { + "name": "default_subagent_model_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_sandbox_type": { + "name": "default_sandbox_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'vercel'" + }, + "default_diff_mode": { + "name": "default_diff_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'unified'" + }, + "auto_commit_push": { + "name": "auto_commit_push", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_create_pr": { + "name": "auto_create_pr", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "alerts_enabled": { + "name": "alerts_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "alert_sound_enabled": { + "name": "alert_sound_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "public_usage_enabled": { + "name": "public_usage_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "global_skill_refs": { + "name": "global_skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "model_variants": { + "name": "model_variants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "enabled_model_ids": { + "name": "enabled_model_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_unique": { + "name": "user_preferences_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "token_expires_at": { + "name": "token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_provider_external_id_idx": { + "name": "users_provider_external_id_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vercel_project_links": { + "name": "vercel_project_links", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_slug": { + "name": "team_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vercel_project_links_user_id_users_id_fk": { + "name": "vercel_project_links_user_id_users_id_fk", + "tableFrom": "vercel_project_links", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vercel_project_links_user_id_repo_owner_repo_name_pk": { + "name": "vercel_project_links_user_id_repo_owner_repo_name_pk", + "columns": ["user_id", "repo_owner", "repo_name"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run_steps": { + "name": "workflow_run_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "finish_reason": { + "name": "finish_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_finish_reason": { + "name": "raw_finish_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_run_steps_run_id_idx": { + "name": "workflow_run_steps_run_id_idx", + "columns": [ + { + "expression": "workflow_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_steps_run_step_idx": { + "name": "workflow_run_steps_run_step_idx", + "columns": [ + { + "expression": "workflow_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_run_steps_workflow_run_id_workflow_runs_id_fk": { + "name": "workflow_run_steps_workflow_run_id_workflow_runs_id_fk", + "tableFrom": "workflow_run_steps", + "tableTo": "workflow_runs", + "columnsFrom": ["workflow_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_runs": { + "name": "workflow_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_runs_chat_id_idx": { + "name": "workflow_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_runs_session_id_idx": { + "name": "workflow_runs_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_runs_user_id_idx": { + "name": "workflow_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_runs_chat_id_chats_id_fk": { + "name": "workflow_runs_chat_id_chats_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_runs_session_id_sessions_id_fk": { + "name": "workflow_runs_session_id_sessions_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "sessions", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_runs_user_id_users_id_fk": { + "name": "workflow_runs_user_id_users_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/web/lib/db/migrations/meta/_journal.json b/apps/web/lib/db/migrations/meta/_journal.json index 015ac55f5..4205f50fd 100644 --- a/apps/web/lib/db/migrations/meta/_journal.json +++ b/apps/web/lib/db/migrations/meta/_journal.json @@ -197,6 +197,13 @@ "when": 1775836447260, "tag": "0027_perfect_wallop", "breakpoints": true + }, + { + "idx": 28, + "version": "7", + "when": 1775924695719, + "tag": "0028_optimal_human_torch", + "breakpoints": true } ] } diff --git a/apps/web/lib/db/schema.ts b/apps/web/lib/db/schema.ts index d32e4d77a..a8fcea123 100644 --- a/apps/web/lib/db/schema.ts +++ b/apps/web/lib/db/schema.ts @@ -261,6 +261,59 @@ export const chatReads = pgTable( ], ); +export const workflowRuns = pgTable( + "workflow_runs", + { + id: text("id").primaryKey(), + chatId: text("chat_id") + .notNull() + .references(() => chats.id, { onDelete: "cascade" }), + sessionId: text("session_id") + .notNull() + .references(() => sessions.id, { onDelete: "cascade" }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + modelId: text("model_id"), + status: text("status", { + enum: ["completed", "aborted", "failed"], + }).notNull(), + startedAt: timestamp("started_at").notNull(), + finishedAt: timestamp("finished_at").notNull(), + totalDurationMs: integer("total_duration_ms").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + }, + (table) => [ + index("workflow_runs_chat_id_idx").on(table.chatId), + index("workflow_runs_session_id_idx").on(table.sessionId), + index("workflow_runs_user_id_idx").on(table.userId), + ], +); + +export const workflowRunSteps = pgTable( + "workflow_run_steps", + { + id: text("id").primaryKey(), + workflowRunId: text("workflow_run_id") + .notNull() + .references(() => workflowRuns.id, { onDelete: "cascade" }), + stepNumber: integer("step_number").notNull(), + startedAt: timestamp("started_at").notNull(), + finishedAt: timestamp("finished_at").notNull(), + durationMs: integer("duration_ms").notNull(), + finishReason: text("finish_reason"), + rawFinishReason: text("raw_finish_reason"), + createdAt: timestamp("created_at").defaultNow().notNull(), + }, + (table) => [ + index("workflow_run_steps_run_id_idx").on(table.workflowRunId), + uniqueIndex("workflow_run_steps_run_step_idx").on( + table.workflowRunId, + table.stepNumber, + ), + ], +); + export type Session = typeof sessions.$inferSelect; export type NewSession = typeof sessions.$inferInsert; export type VercelProjectLink = typeof vercelProjectLinks.$inferSelect; @@ -273,6 +326,10 @@ export type ChatMessage = typeof chatMessages.$inferSelect; export type NewChatMessage = typeof chatMessages.$inferInsert; export type ChatRead = typeof chatReads.$inferSelect; export type NewChatRead = typeof chatReads.$inferInsert; +export type WorkflowRun = typeof workflowRuns.$inferSelect; +export type NewWorkflowRun = typeof workflowRuns.$inferInsert; +export type WorkflowRunStep = typeof workflowRunSteps.$inferSelect; +export type NewWorkflowRunStep = typeof workflowRunSteps.$inferInsert; export type GitHubInstallation = typeof githubInstallations.$inferSelect; export type NewGitHubInstallation = typeof githubInstallations.$inferInsert; diff --git a/apps/web/lib/db/workflow-runs.ts b/apps/web/lib/db/workflow-runs.ts new file mode 100644 index 000000000..50af204f8 --- /dev/null +++ b/apps/web/lib/db/workflow-runs.ts @@ -0,0 +1,66 @@ +import { nanoid } from "nanoid"; +import { db } from "./client"; +import { workflowRuns, workflowRunSteps } from "./schema"; + +export type WorkflowRunStatus = "completed" | "aborted" | "failed"; + +export type WorkflowRunStepTiming = { + stepNumber: number; + startedAt: string; + finishedAt: string; + durationMs: number; + finishReason?: string; + rawFinishReason?: string; +}; + +export async function recordWorkflowRun(data: { + id: string; + chatId: string; + sessionId: string; + userId: string; + modelId?: string; + status: WorkflowRunStatus; + startedAt: string; + finishedAt: string; + totalDurationMs: number; + stepTimings: WorkflowRunStepTiming[]; +}) { + await db.transaction(async (tx) => { + await tx + .insert(workflowRuns) + .values({ + id: data.id, + chatId: data.chatId, + sessionId: data.sessionId, + userId: data.userId, + modelId: data.modelId ?? null, + status: data.status, + startedAt: new Date(data.startedAt), + finishedAt: new Date(data.finishedAt), + totalDurationMs: data.totalDurationMs, + }) + .onConflictDoNothing({ target: workflowRuns.id }); + + if (data.stepTimings.length === 0) { + return; + } + + await tx + .insert(workflowRunSteps) + .values( + data.stepTimings.map((stepTiming) => ({ + id: nanoid(), + workflowRunId: data.id, + stepNumber: stepTiming.stepNumber, + startedAt: new Date(stepTiming.startedAt), + finishedAt: new Date(stepTiming.finishedAt), + durationMs: stepTiming.durationMs, + finishReason: stepTiming.finishReason ?? null, + rawFinishReason: stepTiming.rawFinishReason ?? null, + })), + ) + .onConflictDoNothing({ + target: [workflowRunSteps.workflowRunId, workflowRunSteps.stepNumber], + }); + }); +} diff --git a/apps/web/public/.well-known/workflow/v1/manifest.json b/apps/web/public/.well-known/workflow/v1/manifest.json index 0c0715132..eea5512e3 100644 --- a/apps/web/public/.well-known/workflow/v1/manifest.json +++ b/apps/web/public/.well-known/workflow/v1/manifest.json @@ -6,17 +6,6 @@ "stepId": "step//workflow@4.2.0-beta.74//fetch" } }, - "node_modules/workflow/dist/internal/builtins.js": { - "__builtin_response_array_buffer": { - "stepId": "__builtin_response_array_buffer" - }, - "__builtin_response_json": { - "stepId": "__builtin_response_json" - }, - "__builtin_response_text": { - "stepId": "__builtin_response_text" - } - }, "app/workflows/sandbox-lifecycle.ts": { "clearLifecycleRunIdIfOwned": { "stepId": "step//./app/workflows/sandbox-lifecycle//clearLifecycleRunIdIfOwned" @@ -28,6 +17,17 @@ "stepId": "step//./app/workflows/sandbox-lifecycle//runLifecycleEvaluation" } }, + "node_modules/workflow/dist/internal/builtins.js": { + "__builtin_response_array_buffer": { + "stepId": "__builtin_response_array_buffer" + }, + "__builtin_response_json": { + "stepId": "__builtin_response_json" + }, + "__builtin_response_text": { + "stepId": "__builtin_response_text" + } + }, "app/workflows/chat-post-finish.ts": { "clearActiveStream": { "stepId": "step//./app/workflows/chat-post-finish//clearActiveStream" diff --git a/bun.lock b/bun.lock index a76e1c47f..ffa266bef 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "open-harness",