From 8eea250168b608dc329f398daa60d787d11f65b1 Mon Sep 17 00:00:00 2001 From: duc15052006-dotcom Date: Sat, 19 Sep 2026 19:19:37 +0700 Subject: [PATCH 01/14] feat(workflows): add durable workflow state schema --- server/src/db/schema/coworker.ts | 97 ++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/server/src/db/schema/coworker.ts b/server/src/db/schema/coworker.ts index 97d5145b0..7924285e0 100644 --- a/server/src/db/schema/coworker.ts +++ b/server/src/db/schema/coworker.ts @@ -7,11 +7,13 @@ import { boolean, index, + integer, pgEnum, pgTable, primaryKey, text, timestamp, + uniqueIndex, } from "drizzle-orm/pg-core"; import { agents, users } from "./core"; @@ -169,3 +171,98 @@ export const routineRuns = pgTable( index("routine_runs_by_routine_idx").on(table.routineId, table.startedAt), ], ); + +export const workflowRunStatus = pgEnum("workflow_run_status", [ + "active", + "paused", + "succeeded", + "failed", + "cancelled", +]); + +export const workflowStepStatus = pgEnum("workflow_step_status", [ + "blocked", + "ready", + "running", + "waiting", + "succeeded", + "failed", + "cancelled", +]); + +/** + * A durable multi-step plan owned by one person and one Bot. + * + * The Bot id is part of the authority boundary, not only metadata: workflow-store reads and + * transitions filter by both owner and Bot so two coworkers belonging to the same person cannot + * mutate each other's pending work. + */ +export const workflowRuns = pgTable( + "workflow_runs", + { + id: text("id").primaryKey(), + ownerUserId: text("owner_user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + agentId: text("agent_id") + .notNull() + .references(() => agents.id, { onDelete: "cascade" }), + /** Channels soft-delete, so a broken destination must remain inspectable rather than cascade. */ + channelId: text("channel_id").notNull(), + title: text("title").notNull(), + status: workflowRunStatus("status").notNull().default("active"), + finishedAt: timestamp("finished_at", { withTimezone: true }), + createdAt: createdAt(), + updatedAt: updatedAt(), + }, + (table) => [ + index("workflow_runs_owner_status_idx").on( + table.ownerUserId, + table.status, + ), + index("workflow_runs_agent_status_idx").on(table.agentId, table.status), + ], +); + +/** + * One resumable unit inside a workflow. + * + * Dependencies are local step keys. The store only accepts references to earlier steps in the same + * create call, which makes the graph acyclic by construction and prevents cross-workflow references. + */ +export const workflowSteps = pgTable( + "workflow_steps", + { + id: text("id").primaryKey(), + workflowId: text("workflow_id") + .notNull() + .references(() => workflowRuns.id, { onDelete: "cascade" }), + key: text("key").notNull(), + position: integer("position").notNull(), + instruction: text("instruction").notNull(), + dependsOn: text("depends_on").array().notNull().default([]), + status: workflowStepStatus("status").notNull().default("blocked"), + attempts: integer("attempts").notNull().default(0), + /** Which external/model provider owns a pending wait, when there is one. Never a credential. */ + provider: text("provider"), + /** Exact durable wake target for a waiting step; execution wiring is a separate slice. */ + waitUntil: timestamp("wait_until", { withTimezone: true }), + failureReason: text("failure_reason"), + startedAt: timestamp("started_at", { withTimezone: true }), + finishedAt: timestamp("finished_at", { withTimezone: true }), + createdAt: createdAt(), + updatedAt: updatedAt(), + }, + (table) => [ + uniqueIndex("workflow_steps_workflow_key_idx").on( + table.workflowId, + table.key, + ), + index("workflow_steps_workflow_position_idx").on( + table.workflowId, + table.position, + ), + index("workflow_steps_status_wait_idx").on(table.status, table.waitUntil), + ], +); + From a9eac07910a1cf06d1027ce14b3130e3e3a2ac90 Mon Sep 17 00:00:00 2001 From: duc15052006-dotcom Date: Sat, 19 Sep 2026 19:20:55 +0700 Subject: [PATCH 02/14] feat(workflows): add durable workflow state store --- server/src/workflows/store.ts | 822 ++++++++++++++++++++++++++++++++++ 1 file changed, 822 insertions(+) create mode 100644 server/src/workflows/store.ts diff --git a/server/src/workflows/store.ts b/server/src/workflows/store.ts new file mode 100644 index 000000000..198a4d180 --- /dev/null +++ b/server/src/workflows/store.ts @@ -0,0 +1,822 @@ +import { + and, + asc, + eq, + inArray, + isNotNull, + isNull, + lte, + sql, +} from "drizzle-orm"; +import type { Database } from "../db/client"; +import { + channelAgents, + channelMemberships, + channels, + workflowRuns, + workflowSteps, +} from "../db/schema"; + +export const MAX_WORKFLOW_STEPS = 100; +export const MAX_WORKFLOW_TITLE_CODE_POINTS = 160; +export const MAX_WORKFLOW_INSTRUCTION_CODE_POINTS = 4000; +export const MAX_WORKFLOW_FAILURE_CODE_POINTS = 500; +export const MAX_WORKFLOW_PROVIDER_CODE_POINTS = 120; + +const ACTIVE_STEP_STATUSES = [ + "blocked", + "ready", + "running", + "waiting", +] as const; + +export type WorkflowStatus = + | "active" + | "paused" + | "succeeded" + | "failed" + | "cancelled"; + +export type WorkflowStepStatus = + | "blocked" + | "ready" + | "running" + | "waiting" + | "succeeded" + | "failed" + | "cancelled"; + +export type WorkflowIdentity = { + ownerUserId: string; + agentId: string; +}; + +export type WorkflowStepInput = { + key: string; + instruction: string; + dependsOn?: string[]; +}; + +export type WorkflowInput = WorkflowIdentity & { + channelId: string; + title: string; + steps: WorkflowStepInput[]; +}; + +export type WorkflowStep = { + id: string; + workflowId: string; + key: string; + position: number; + instruction: string; + dependsOn: string[]; + status: WorkflowStepStatus; + attempts: number; + provider: string | null; + waitUntil: Date | null; + failureReason: string | null; + startedAt: Date | null; + finishedAt: Date | null; + createdAt: Date; + updatedAt: Date; +}; + +export type WorkflowRun = { + id: string; + ownerUserId: string; + agentId: string; + channelId: string; + title: string; + status: WorkflowStatus; + finishedAt: Date | null; + createdAt: Date; + updatedAt: Date; +}; + +export type WorkflowPlan = WorkflowRun & { steps: WorkflowStep[] }; + +export type DueWorkflowWait = WorkflowIdentity & { + workflowId: string; + stepKey: string; + waitUntil: Date; +}; + +export class WorkflowNotFoundError extends Error { + constructor() { + super("That workflow does not exist."); + this.name = "WorkflowNotFoundError"; + } +} + +export class WorkflowRefusedError extends Error { + constructor(message: string) { + super(message); + this.name = "WorkflowRefusedError"; + } +} + +type Transaction = Parameters[0]>[0]; +type Handle = Database | Transaction; + +type WorkflowRunRow = typeof workflowRuns.$inferSelect; +type WorkflowStepRow = typeof workflowSteps.$inferSelect; + +function textWithin( + value: string, + label: string, + maxCodePoints: number, +): string { + const trimmed = value.trim(); + if (!trimmed) throw new WorkflowRefusedError(`${label} cannot be empty.`); + if (Array.from(trimmed).length > maxCodePoints) { + throw new WorkflowRefusedError( + `${label} can be at most ${maxCodePoints} characters.`, + ); + } + return trimmed; +} + +function stepKey(value: string): string { + const key = value.trim(); + if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(key)) { + throw new WorkflowRefusedError( + "A workflow step key must be 1-64 letters, numbers, dots, underscores or dashes.", + ); + } + return key; +} + +function toRun(row: WorkflowRunRow): WorkflowRun { + return { + id: row.id, + ownerUserId: row.ownerUserId, + agentId: row.agentId, + channelId: row.channelId, + title: row.title, + status: row.status, + finishedAt: row.finishedAt, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +function toStep(row: WorkflowStepRow): WorkflowStep { + return { + id: row.id, + workflowId: row.workflowId, + key: row.key, + position: row.position, + instruction: row.instruction, + dependsOn: row.dependsOn, + status: row.status, + attempts: row.attempts, + provider: row.provider, + waitUntil: row.waitUntil, + failureReason: row.failureReason, + startedAt: row.startedAt, + finishedAt: row.finishedAt, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +function terminal(status: WorkflowStatus): boolean { + return ["succeeded", "failed", "cancelled"].includes(status); +} + +export type WorkflowStore = { + create(input: WorkflowInput): Promise; + get(identity: WorkflowIdentity, id: string): Promise; + listFor(identity: WorkflowIdentity): Promise; + pause(identity: WorkflowIdentity, id: string): Promise; + resume(identity: WorkflowIdentity, id: string): Promise; + cancel(identity: WorkflowIdentity, id: string): Promise; + fail(identity: WorkflowIdentity, id: string): Promise; + startStep( + identity: WorkflowIdentity, + id: string, + key: string, + ): Promise; + waitStep( + identity: WorkflowIdentity, + id: string, + key: string, + input: { waitUntil: Date; provider?: string }, + ): Promise; + resumeWaitingStep( + identity: WorkflowIdentity, + id: string, + key: string, + expectedWaitUntil: Date, + ): Promise; + completeStep( + identity: WorkflowIdentity, + id: string, + key: string, + ): Promise; + failStep( + identity: WorkflowIdentity, + id: string, + key: string, + reason: string, + ): Promise; + retryStep( + identity: WorkflowIdentity, + id: string, + key: string, + ): Promise; + dueWaitingSteps(limit: number): Promise; +}; + +export function createWorkflowStore(database: Database): WorkflowStore { + async function loadOwned( + handle: Handle, + identity: WorkflowIdentity, + id: string, + ): Promise { + const [row] = await handle + .select() + .from(workflowRuns) + .where( + and( + eq(workflowRuns.id, id), + eq(workflowRuns.ownerUserId, identity.ownerUserId), + eq(workflowRuns.agentId, identity.agentId), + ), + ) + .limit(1); + if (!row) throw new WorkflowNotFoundError(); + return row; + } + + async function lockWorkflow( + transaction: Transaction, + id: string, + ): Promise { + await transaction.execute( + sql`select pg_advisory_xact_lock(hashtext(${`workflow-${id}`}))`, + ); + } + + async function channelAllowed(input: WorkflowInput): Promise { + const [row] = await database + .select({ id: channels.id }) + .from(channels) + .innerJoin( + channelMemberships, + and( + eq(channelMemberships.channelId, channels.id), + eq(channelMemberships.userId, input.ownerUserId), + ), + ) + .innerJoin( + channelAgents, + and( + eq(channelAgents.channelId, channels.id), + eq(channelAgents.agentId, input.agentId), + ), + ) + .where(and(eq(channels.id, input.channelId), isNull(channels.deletedAt))) + .limit(1); + if (!row) { + throw new WorkflowRefusedError( + "That channel is not shared by this person and this Bot.", + ); + } + } + + function preparedSteps(input: WorkflowStepInput[]) { + if (input.length === 0) { + throw new WorkflowRefusedError("A workflow needs at least one step."); + } + if (input.length > MAX_WORKFLOW_STEPS) { + throw new WorkflowRefusedError( + `A workflow can have at most ${MAX_WORKFLOW_STEPS} steps.`, + ); + } + + const seen = new Set(); + return input.map((raw, position) => { + const key = stepKey(raw.key); + if (seen.has(key)) { + throw new WorkflowRefusedError( + `Workflow step key "${key}" is duplicated.`, + ); + } + const dependencies = [...new Set(raw.dependsOn ?? [])].map(stepKey); + for (const dependency of dependencies) { + if (!seen.has(dependency)) { + throw new WorkflowRefusedError( + `Workflow step "${key}" depends on "${dependency}", which must be an earlier step in the same workflow.`, + ); + } + } + seen.add(key); + return { + key, + position, + instruction: textWithin( + raw.instruction, + "A workflow step instruction", + MAX_WORKFLOW_INSTRUCTION_CODE_POINTS, + ), + dependsOn: dependencies, + status: dependencies.length === 0 ? ("ready" as const) : ("blocked" as const), + }; + }); + } + + async function planFor( + identity: WorkflowIdentity, + id: string, + ): Promise { + const [run] = await database + .select() + .from(workflowRuns) + .where( + and( + eq(workflowRuns.id, id), + eq(workflowRuns.ownerUserId, identity.ownerUserId), + eq(workflowRuns.agentId, identity.agentId), + ), + ) + .limit(1); + if (!run) return null; + const steps = await database + .select() + .from(workflowSteps) + .where(eq(workflowSteps.workflowId, id)) + .orderBy(asc(workflowSteps.position), asc(workflowSteps.id)); + return { ...toRun(run), steps: steps.map(toStep) }; + } + + async function transitionRun( + identity: WorkflowIdentity, + id: string, + from: WorkflowStatus, + to: WorkflowStatus, + ): Promise { + await database.transaction(async (transaction) => { + await lockWorkflow(transaction, id); + await loadOwned(transaction, identity, id); + const changed = await transaction + .update(workflowRuns) + .set({ status: to, updatedAt: sql`now()` }) + .where( + and( + eq(workflowRuns.id, id), + eq(workflowRuns.ownerUserId, identity.ownerUserId), + eq(workflowRuns.agentId, identity.agentId), + eq(workflowRuns.status, from), + ), + ) + .returning({ id: workflowRuns.id }); + if (changed.length === 0) { + throw new WorkflowRefusedError( + `That workflow is not ${from}, so it cannot become ${to}.`, + ); + } + }); + return (await planFor(identity, id)) as WorkflowPlan; + } + + async function endRun( + identity: WorkflowIdentity, + id: string, + status: "failed" | "cancelled", + ): Promise { + await database.transaction(async (transaction) => { + await lockWorkflow(transaction, id); + const run = await loadOwned(transaction, identity, id); + if (terminal(run.status)) { + if (run.status === status) return; + throw new WorkflowRefusedError("That workflow has already finished."); + } + await transaction + .update(workflowRuns) + .set({ + status, + finishedAt: sql`now()`, + updatedAt: sql`now()`, + }) + .where(eq(workflowRuns.id, id)); + await transaction + .update(workflowSteps) + .set({ + status: "cancelled", + finishedAt: sql`now()`, + updatedAt: sql`now()`, + }) + .where( + and( + eq(workflowSteps.workflowId, id), + inArray(workflowSteps.status, ACTIVE_STEP_STATUSES), + ), + ); + }); + return (await planFor(identity, id)) as WorkflowPlan; + } + + return { + async create(input) { + const title = textWithin( + input.title, + "A workflow title", + MAX_WORKFLOW_TITLE_CODE_POINTS, + ); + const steps = preparedSteps(input.steps); + await channelAllowed(input); + + const id = `workflow_${crypto.randomUUID()}`; + await database.transaction(async (transaction) => { + await transaction.insert(workflowRuns).values({ + id, + ownerUserId: input.ownerUserId, + agentId: input.agentId, + channelId: input.channelId, + title, + }); + await transaction.insert(workflowSteps).values( + steps.map((step) => ({ + id: `workflow_step_${crypto.randomUUID()}`, + workflowId: id, + ...step, + })), + ); + }); + return (await planFor(input, id)) as WorkflowPlan; + }, + + get: planFor, + + async listFor(identity) { + const rows = await database + .select() + .from(workflowRuns) + .where( + and( + eq(workflowRuns.ownerUserId, identity.ownerUserId), + eq(workflowRuns.agentId, identity.agentId), + ), + ) + .orderBy(asc(workflowRuns.createdAt), asc(workflowRuns.id)); + return rows.map(toRun); + }, + + pause(identity, id) { + return transitionRun(identity, id, "active", "paused"); + }, + + resume(identity, id) { + return transitionRun(identity, id, "paused", "active"); + }, + + cancel(identity, id) { + return endRun(identity, id, "cancelled"); + }, + + fail(identity, id) { + return endRun(identity, id, "failed"); + }, + + async startStep(identity, id, key) { + return await database.transaction(async (transaction) => { + await lockWorkflow(transaction, id); + const run = await loadOwned(transaction, identity, id); + if (run.status !== "active") { + throw new WorkflowRefusedError( + "Only an active workflow can start a step.", + ); + } + const [row] = await transaction + .update(workflowSteps) + .set({ + status: "running", + attempts: sql`${workflowSteps.attempts} + 1`, + startedAt: sql`now()`, + finishedAt: null, + failureReason: null, + waitUntil: null, + updatedAt: sql`now()`, + }) + .where( + and( + eq(workflowSteps.workflowId, id), + eq(workflowSteps.key, stepKey(key)), + eq(workflowSteps.status, "ready"), + ), + ) + .returning(); + if (!row) { + throw new WorkflowRefusedError( + "That workflow step is not ready to start.", + ); + } + return toStep(row); + }); + }, + + async waitStep(identity, id, key, input) { + if ( + !(input.waitUntil instanceof Date) || + Number.isNaN(input.waitUntil.getTime()) || + input.waitUntil.getTime() <= Date.now() + ) { + throw new WorkflowRefusedError( + "A waiting workflow step needs a future wake time.", + ); + } + const provider = input.provider + ? textWithin( + input.provider, + "A workflow provider", + MAX_WORKFLOW_PROVIDER_CODE_POINTS, + ) + : null; + return await database.transaction(async (transaction) => { + await lockWorkflow(transaction, id); + const run = await loadOwned(transaction, identity, id); + if (run.status !== "active") { + throw new WorkflowRefusedError( + "Only an active workflow can enter a wait.", + ); + } + const [row] = await transaction + .update(workflowSteps) + .set({ + status: "waiting", + provider, + waitUntil: input.waitUntil, + updatedAt: sql`now()`, + }) + .where( + and( + eq(workflowSteps.workflowId, id), + eq(workflowSteps.key, stepKey(key)), + eq(workflowSteps.status, "running"), + ), + ) + .returning(); + if (!row) { + throw new WorkflowRefusedError( + "Only a running workflow step can enter a wait.", + ); + } + return toStep(row); + }); + }, + + async resumeWaitingStep(identity, id, key, expectedWaitUntil) { + return await database.transaction(async (transaction) => { + await lockWorkflow(transaction, id); + const run = await loadOwned(transaction, identity, id); + if (run.status !== "active") { + throw new WorkflowRefusedError( + "Only an active workflow can resume a waiting step.", + ); + } + const [row] = await transaction + .update(workflowSteps) + .set({ + status: "running", + waitUntil: null, + updatedAt: sql`now()`, + }) + .where( + and( + eq(workflowSteps.workflowId, id), + eq(workflowSteps.key, stepKey(key)), + eq(workflowSteps.status, "waiting"), + eq(workflowSteps.waitUntil, expectedWaitUntil), + lte(workflowSteps.waitUntil, sql`now()`), + ), + ) + .returning(); + if (!row) { + throw new WorkflowRefusedError( + "That wait is not due, or it was changed after this wake was scheduled.", + ); + } + return toStep(row); + }); + }, + + async completeStep(identity, id, key) { + await database.transaction(async (transaction) => { + await lockWorkflow(transaction, id); + const run = await loadOwned(transaction, identity, id); + if (terminal(run.status)) { + throw new WorkflowRefusedError("That workflow has already finished."); + } + + const [completed] = await transaction + .update(workflowSteps) + .set({ + status: "succeeded", + finishedAt: sql`now()`, + waitUntil: null, + failureReason: null, + updatedAt: sql`now()`, + }) + .where( + and( + eq(workflowSteps.workflowId, id), + eq(workflowSteps.key, stepKey(key)), + eq(workflowSteps.status, "running"), + ), + ) + .returning({ id: workflowSteps.id }); + if (!completed) { + throw new WorkflowRefusedError( + "Only a running workflow step can complete.", + ); + } + + const rows = await transaction + .select() + .from(workflowSteps) + .where(eq(workflowSteps.workflowId, id)) + .orderBy(asc(workflowSteps.position)); + const byKey = new Map(rows.map((row) => [row.key, row.status])); + + for (const step of rows) { + if (step.status !== "blocked") continue; + if ( + step.dependsOn.every( + (dependency) => byKey.get(dependency) === "succeeded", + ) + ) { + await transaction + .update(workflowSteps) + .set({ status: "ready", updatedAt: sql`now()` }) + .where( + and( + eq(workflowSteps.id, step.id), + eq(workflowSteps.status, "blocked"), + ), + ); + } + } + + const remaining = await transaction + .select({ id: workflowSteps.id }) + .from(workflowSteps) + .where( + and( + eq(workflowSteps.workflowId, id), + inArray(workflowSteps.status, [ + "blocked", + "ready", + "running", + "waiting", + "failed", + ]), + ), + ) + .limit(1); + if (remaining.length === 0) { + await transaction + .update(workflowRuns) + .set({ + status: "succeeded", + finishedAt: sql`now()`, + updatedAt: sql`now()`, + }) + .where(eq(workflowRuns.id, id)); + } + }); + return (await planFor(identity, id)) as WorkflowPlan; + }, + + async failStep(identity, id, key, reason) { + const failureReason = textWithin( + reason, + "A workflow failure reason", + MAX_WORKFLOW_FAILURE_CODE_POINTS, + ); + return await database.transaction(async (transaction) => { + await lockWorkflow(transaction, id); + const run = await loadOwned(transaction, identity, id); + if (terminal(run.status)) { + throw new WorkflowRefusedError("That workflow has already finished."); + } + const [row] = await transaction + .update(workflowSteps) + .set({ + status: "failed", + failureReason, + finishedAt: sql`now()`, + waitUntil: null, + updatedAt: sql`now()`, + }) + .where( + and( + eq(workflowSteps.workflowId, id), + eq(workflowSteps.key, stepKey(key)), + eq(workflowSteps.status, "running"), + ), + ) + .returning(); + if (!row) { + throw new WorkflowRefusedError( + "Only a running workflow step can fail.", + ); + } + return toStep(row); + }); + }, + + async retryStep(identity, id, key) { + return await database.transaction(async (transaction) => { + await lockWorkflow(transaction, id); + const run = await loadOwned(transaction, identity, id); + if (terminal(run.status)) { + throw new WorkflowRefusedError("That workflow has already finished."); + } + const wantedKey = stepKey(key); + const rows = await transaction + .select() + .from(workflowSteps) + .where(eq(workflowSteps.workflowId, id)); + const step = rows.find((candidate) => candidate.key === wantedKey); + if (!step || step.status !== "failed") { + throw new WorkflowRefusedError( + "Only a failed workflow step can be retried.", + ); + } + const byKey = new Map(rows.map((row) => [row.key, row.status])); + if ( + !step.dependsOn.every( + (dependency) => byKey.get(dependency) === "succeeded", + ) + ) { + throw new WorkflowRefusedError( + "That workflow step is still blocked by an unfinished dependency.", + ); + } + const [row] = await transaction + .update(workflowSteps) + .set({ + status: "ready", + failureReason: null, + finishedAt: null, + updatedAt: sql`now()`, + }) + .where( + and( + eq(workflowSteps.id, step.id), + eq(workflowSteps.status, "failed"), + ), + ) + .returning(); + if (!row) { + throw new WorkflowRefusedError( + "That workflow step changed before it could be retried.", + ); + } + return toStep(row); + }); + }, + + async dueWaitingSteps(limit) { + if (!Number.isInteger(limit) || limit <= 0 || limit > 500) { + throw new WorkflowRefusedError( + "A due-wait scan limit must be between 1 and 500.", + ); + } + const rows = await database + .select({ + ownerUserId: workflowRuns.ownerUserId, + agentId: workflowRuns.agentId, + workflowId: workflowRuns.id, + stepKey: workflowSteps.key, + waitUntil: workflowSteps.waitUntil, + }) + .from(workflowSteps) + .innerJoin(workflowRuns, eq(workflowRuns.id, workflowSteps.workflowId)) + .where( + and( + eq(workflowRuns.status, "active"), + eq(workflowSteps.status, "waiting"), + isNotNull(workflowSteps.waitUntil), + lte(workflowSteps.waitUntil, sql`now()`), + ), + ) + .orderBy(asc(workflowSteps.waitUntil), asc(workflowSteps.id)) + .limit(limit); + return rows.flatMap((row) => + row.waitUntil + ? [ + { + ownerUserId: row.ownerUserId, + agentId: row.agentId, + workflowId: row.workflowId, + stepKey: row.stepKey, + waitUntil: row.waitUntil, + }, + ] + : [], + ); + }, + }; +} From 6d079744686694ea7d93b19cb553320c61075b12 Mon Sep 17 00:00:00 2001 From: duc15052006-dotcom Date: Sat, 19 Sep 2026 19:21:13 +0700 Subject: [PATCH 03/14] feat(workflows): add durable workflow state migration --- server/drizzle/0043_workflow_state.sql | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 server/drizzle/0043_workflow_state.sql diff --git a/server/drizzle/0043_workflow_state.sql b/server/drizzle/0043_workflow_state.sql new file mode 100644 index 000000000..630a402e7 --- /dev/null +++ b/server/drizzle/0043_workflow_state.sql @@ -0,0 +1,40 @@ +CREATE TYPE "public"."workflow_run_status" AS ENUM('active', 'paused', 'succeeded', 'failed', 'cancelled');--> statement-breakpoint +CREATE TYPE "public"."workflow_step_status" AS ENUM('blocked', 'ready', 'running', 'waiting', 'succeeded', 'failed', 'cancelled');--> statement-breakpoint +CREATE TABLE "workflow_runs" ( + "id" text PRIMARY KEY NOT NULL, + "owner_user_id" text NOT NULL, + "agent_id" text NOT NULL, + "channel_id" text NOT NULL, + "title" text NOT NULL, + "status" "workflow_run_status" DEFAULT 'active' NOT NULL, + "finished_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "workflow_steps" ( + "id" text PRIMARY KEY NOT NULL, + "workflow_id" text NOT NULL, + "key" text NOT NULL, + "position" integer NOT NULL, + "instruction" text NOT NULL, + "depends_on" text[] DEFAULT '{}' NOT NULL, + "status" "workflow_step_status" DEFAULT 'blocked' NOT NULL, + "attempts" integer DEFAULT 0 NOT NULL, + "provider" text, + "wait_until" timestamp with time zone, + "failure_reason" text, + "started_at" timestamp with time zone, + "finished_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "workflow_runs" ADD CONSTRAINT "workflow_runs_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workflow_runs" ADD CONSTRAINT "workflow_runs_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workflow_steps" ADD CONSTRAINT "workflow_steps_workflow_id_workflow_runs_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow_runs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "workflow_runs_owner_status_idx" ON "workflow_runs" USING btree ("owner_user_id","status");--> statement-breakpoint +CREATE INDEX "workflow_runs_agent_status_idx" ON "workflow_runs" USING btree ("agent_id","status");--> statement-breakpoint +CREATE UNIQUE INDEX "workflow_steps_workflow_key_idx" ON "workflow_steps" USING btree ("workflow_id","key");--> statement-breakpoint +CREATE INDEX "workflow_steps_workflow_position_idx" ON "workflow_steps" USING btree ("workflow_id","position");--> statement-breakpoint +CREATE INDEX "workflow_steps_status_wait_idx" ON "workflow_steps" USING btree ("status","wait_until"); \ No newline at end of file From 0620b2164544244c07a60acea6c2abec2e496c1c Mon Sep 17 00:00:00 2001 From: duc15052006-dotcom Date: Sat, 19 Sep 2026 19:21:58 +0700 Subject: [PATCH 04/14] feat(workflows): snapshot durable workflow schema --- server/drizzle/meta/0043_snapshot.json | 3832 ++++++++++++++++++++++++ 1 file changed, 3832 insertions(+) create mode 100644 server/drizzle/meta/0043_snapshot.json diff --git a/server/drizzle/meta/0043_snapshot.json b/server/drizzle/meta/0043_snapshot.json new file mode 100644 index 000000000..9438bb82d --- /dev/null +++ b/server/drizzle/meta/0043_snapshot.json @@ -0,0 +1,3832 @@ +{ + "id": "4f6d9f1c-7e11-4f6a-b74c-9e3d5c2a8b10", + "prevId": "7af5e0ea-5f8e-4a72-9c47-21b2f0d42142", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "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": { + "accounts_provider_account_idx": { + "name": "accounts_provider_account_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.action_policy": { + "name": "action_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deny": { + "name": "deny", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "allow": { + "name": "allow", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_preferences": { + "name": "agent_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hidden_at": { + "name": "hidden_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_preferences_user_id_users_id_fk": { + "name": "agent_preferences_user_id_users_id_fk", + "tableFrom": "agent_preferences", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "agent_preferences_agent_id_agents_id_fk": { + "name": "agent_preferences_agent_id_agents_id_fk", + "tableFrom": "agent_preferences", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "agent_preferences_user_id_agent_id_pk": { + "name": "agent_preferences_user_id_agent_id_pk", + "columns": [ + "user_id", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_profiles": { + "name": "agent_profiles", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_description": { + "name": "role_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_seed": { + "name": "avatar_seed", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "agent_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "callback_token_hash": { + "name": "callback_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "callback_token_issued_at": { + "name": "callback_token_issued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "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": { + "agent_profiles_visibility_deleted_idx": { + "name": "agent_profiles_visibility_deleted_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "agent_profiles_agent_id_agents_id_fk": { + "name": "agent_profiles_agent_id_agents_id_fk", + "tableFrom": "agent_profiles", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "agent_profiles_owner_user_id_users_id_fk": { + "name": "agent_profiles_owner_user_id_users_id_fk", + "tableFrom": "agent_profiles", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "agent_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "configuration": { + "name": "configuration", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "package_id": { + "name": "package_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "override": { + "name": "override", + "type": "jsonb", + "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": {}, + "foreignKeys": { + "agents_package_id_deployment_packages_id_fk": { + "name": "agents_package_id_deployment_packages_id_fk", + "tableFrom": "agents", + "columnsFrom": [ + "package_id" + ], + "tableTo": "deployment_packages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.attachments": { + "name": "attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bytes": { + "name": "bytes", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attached_at": { + "name": "attached_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "upload_group": { + "name": "upload_group", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "attachments_channel_idx": { + "name": "attachments_channel_idx", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "attachments_uploaded_by_idx": { + "name": "attachments_uploaded_by_idx", + "columns": [ + { + "expression": "uploaded_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "attachments_upload_group_idx": { + "name": "attachments_upload_group_idx", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "upload_group", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"attachments\".\"attached_at\" is null", + "concurrently": false + }, + "attachments_staged_idx": { + "name": "attachments_staged_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"attachments\".\"attached_at\" is null", + "concurrently": false + } + }, + "foreignKeys": { + "attachments_channel_id_channels_id_fk": { + "name": "attachments_channel_id_channels_id_fk", + "tableFrom": "attachments", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "attachments_uploaded_by_users_id_fk": { + "name": "attachments_uploaded_by_users_id_fk", + "tableFrom": "attachments", + "columnsFrom": [ + "uploaded_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_events": { + "name": "audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initiator_kind": { + "name": "initiator_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'person'" + }, + "initiator_id": { + "name": "initiator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_events_created_at_idx": { + "name": "audit_events_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "audit_events_type_time_idx": { + "name": "audit_events_type_time_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "audit_events_actor_time_idx": { + "name": "audit_events_actor_time_idx", + "columns": [ + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "audit_events_target_time_idx": { + "name": "audit_events_target_time_idx", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "audit_events_initiator_time_idx": { + "name": "audit_events_initiator_time_idx", + "columns": [ + { + "expression": "initiator_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.channel_agents": { + "name": "channel_agents", + "schema": "", + "columns": { + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "channel_agents_channel_id_channels_id_fk": { + "name": "channel_agents_channel_id_channels_id_fk", + "tableFrom": "channel_agents", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "channel_agents_agent_id_agents_id_fk": { + "name": "channel_agents_agent_id_agents_id_fk", + "tableFrom": "channel_agents", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "channel_agents_channel_id_agent_id_pk": { + "name": "channel_agents_channel_id_agent_id_pk", + "columns": [ + "channel_id", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.channel_memberships": { + "name": "channel_memberships", + "schema": "", + "columns": { + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "channel_memberships_channel_id_channels_id_fk": { + "name": "channel_memberships_channel_id_channels_id_fk", + "tableFrom": "channel_memberships", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "channel_memberships_user_id_users_id_fk": { + "name": "channel_memberships_user_id_users_id_fk", + "tableFrom": "channel_memberships", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "channel_memberships_channel_id_user_id_pk": { + "name": "channel_memberships_channel_id_user_id_pk", + "columns": [ + "channel_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.channels": { + "name": "channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_prompts": { + "name": "suggested_prompts", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "allowed_groups": { + "name": "allowed_groups", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "package_id": { + "name": "package_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "override": { + "name": "override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_at": { + "name": "summary_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_message": { + "name": "last_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_message_agent_id": { + "name": "last_message_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "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": { + "channels_recent_activity_idx": { + "name": "channels_recent_activity_idx", + "columns": [ + { + "expression": "COALESCE(\"last_message_at\", \"created_at\") DESC", + "isExpression": true, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "channels_awaiting_summary_idx": { + "name": "channels_awaiting_summary_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"channels\".\"summary\" is null and \"channels\".\"deleted_at\" is null", + "concurrently": false + } + }, + "foreignKeys": { + "channels_package_id_deployment_packages_id_fk": { + "name": "channels_package_id_deployment_packages_id_fk", + "tableFrom": "channels", + "columnsFrom": [ + "package_id" + ], + "tableTo": "deployment_packages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "channels_last_message_agent_id_agents_id_fk": { + "name": "channels_last_message_agent_id_agents_id_fk", + "tableFrom": "channels", + "columnsFrom": [ + "last_message_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.component_exclusions": { + "name": "component_exclusions", + "schema": "", + "columns": { + "component_name": { + "name": "component_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "withheld_by": { + "name": "withheld_by", + "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": {}, + "foreignKeys": { + "component_exclusions_component_name_components_name_fk": { + "name": "component_exclusions_component_name_components_name_fk", + "tableFrom": "component_exclusions", + "columnsFrom": [ + "component_name" + ], + "tableTo": "components", + "columnsTo": [ + "name" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "component_exclusions_agent_id_agents_id_fk": { + "name": "component_exclusions_agent_id_agents_id_fk", + "tableFrom": "component_exclusions", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "component_exclusions_component_name_agent_id_pk": { + "name": "component_exclusions_component_name_agent_id_pk", + "columns": [ + "component_name", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.component_functions": { + "name": "component_functions", + "schema": "", + "columns": { + "component_name": { + "name": "component_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "function_name": { + "name": "function_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "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": {}, + "foreignKeys": { + "component_functions_component_name_components_name_fk": { + "name": "component_functions_component_name_components_name_fk", + "tableFrom": "component_functions", + "columnsFrom": [ + "component_name" + ], + "tableTo": "components", + "columnsTo": [ + "name" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "component_functions_component_name_function_name_pk": { + "name": "component_functions_component_name_function_name_pk", + "columns": [ + "component_name", + "function_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.components": { + "name": "components", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "draft_description": { + "name": "draft_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published_description": { + "name": "published_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.composio_connections": { + "name": "composio_connections", + "schema": "", + "columns": { + "toolkit": { + "name": "toolkit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "probe_action": { + "name": "probe_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "composio_connections_user_idx": { + "name": "composio_connections_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "composio_connections_toolkit_user_id_pk": { + "name": "composio_connections_toolkit_user_id_pk", + "columns": [ + "toolkit", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computer_page_frame": { + "name": "computer_page_frame", + "schema": "", + "columns": { + "computer_id": { + "name": "computer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frame": { + "name": "frame", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "captured_at": { + "name": "captured_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "computer_page_frame_captured_idx": { + "name": "computer_page_frame_captured_idx", + "columns": [ + { + "expression": "captured_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "computer_page_frame_computer_id_tool_call_id_pk": { + "name": "computer_page_frame_computer_id_tool_call_id_pk", + "columns": [ + "computer_id", + "tool_call_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computer_snapshot": { + "name": "computer_snapshot", + "schema": "", + "columns": { + "computer_id": { + "name": "computer_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "elements": { + "name": "elements", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "taken_at": { + "name": "taken_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "session": { + "name": "session", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kind": { + "name": "kind", + "type": "credential_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "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": { + "credentials_active_key_idx": { + "name": "credentials_active_key_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"credentials\".\"revoked_at\" IS NULL", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_packages": { + "name": "deployment_packages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "loaded_at": { + "name": "loaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deployment_packages_tenant_id_unique": { + "name": "deployment_packages_tenant_id_unique", + "columns": [ + "tenant_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.intelligence_channel_mappings": { + "name": "intelligence_channel_mappings", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "intelligence_channel_mappings_thread_idx": { + "name": "intelligence_channel_mappings_thread_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "intelligence_channel_mappings_user_id_users_id_fk": { + "name": "intelligence_channel_mappings_user_id_users_id_fk", + "tableFrom": "intelligence_channel_mappings", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "intelligence_channel_mappings_channel_id_channels_id_fk": { + "name": "intelligence_channel_mappings_channel_id_channels_id_fk", + "tableFrom": "intelligence_channel_mappings", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "intelligence_channel_mappings_user_id_channel_id_pk": { + "name": "intelligence_channel_mappings_user_id_channel_id_pk", + "columns": [ + "user_id", + "channel_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vendor": { + "name": "vendor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance": { + "name": "provenance", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'first-party'" + }, + "credential_id": { + "name": "credential_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "auth_scheme": { + "name": "auth_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tools_refreshed_at": { + "name": "tools_refreshed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "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": {}, + "foreignKeys": { + "mcp_servers_credential_id_credentials_id_fk": { + "name": "mcp_servers_credential_id_credentials_id_fk", + "tableFrom": "mcp_servers", + "columnsFrom": [ + "credential_id" + ], + "tableTo": "credentials", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_tools": { + "name": "mcp_tools", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "input_schema": { + "name": "input_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "effect": { + "name": "effect", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destructive": { + "name": "destructive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_tools_server_id_mcp_servers_id_fk": { + "name": "mcp_tools_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_tools", + "columnsFrom": [ + "server_id" + ], + "tableTo": "mcp_servers", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "mcp_tools_server_id_name_pk": { + "name": "mcp_tools_server_id_name_pk", + "columns": [ + "server_id", + "name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_user_credentials": { + "name": "mcp_user_credentials", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connected_at": { + "name": "connected_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": { + "mcp_user_credentials_user_idx": { + "name": "mcp_user_credentials_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "mcp_user_credentials_server_id_mcp_servers_id_fk": { + "name": "mcp_user_credentials_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_user_credentials", + "columnsFrom": [ + "server_id" + ], + "tableTo": "mcp_servers", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mcp_user_credentials_user_id_users_id_fk": { + "name": "mcp_user_credentials_user_id_users_id_fk", + "tableFrom": "mcp_user_credentials", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mcp_user_credentials_credential_id_credentials_id_fk": { + "name": "mcp_user_credentials_credential_id_credentials_id_fk", + "tableFrom": "mcp_user_credentials", + "columnsFrom": [ + "credential_id" + ], + "tableTo": "credentials", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": { + "mcp_user_credentials_server_id_user_id_pk": { + "name": "mcp_user_credentials_server_id_user_id_pk", + "columns": [ + "server_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_grants": { + "name": "plugin_grants", + "schema": "", + "columns": { + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "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": { + "plugin_grants_agent_idx": { + "name": "plugin_grants_agent_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plugin_grants_agent_id_agents_id_fk": { + "name": "plugin_grants_agent_id_agents_id_fk", + "tableFrom": "plugin_grants", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "plugin_grants_kind_ref_agent_id_pk": { + "name": "plugin_grants_kind_ref_agent_id_pk", + "columns": [ + "kind", + "ref", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.revoked_access": { + "name": "revoked_access", + "schema": "", + "columns": { + "email": { + "name": "email", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_runs": { + "name": "routine_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "routine_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "routine_runs_by_routine_idx": { + "name": "routine_runs_by_routine_idx", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "routine_runs_routine_id_routines_id_fk": { + "name": "routine_runs_routine_id_routines_id_fk", + "tableFrom": "routine_runs", + "columnsFrom": [ + "routine_id" + ], + "tableTo": "routines", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_sweeps": { + "name": "routine_sweeps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "swept_at": { + "name": "swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routines": { + "name": "routines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instruction": { + "name": "instruction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_kind": { + "name": "schedule_kind", + "type": "routine_schedule_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'recurring'" + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "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": { + "routines_due_idx": { + "name": "routines_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "routines_by_owner_idx": { + "name": "routines_by_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "routines_owner_user_id_users_id_fk": { + "name": "routines_owner_user_id_users_id_fk", + "tableFrom": "routines", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "routines_agent_id_agents_id_fk": { + "name": "routines_agent_id_agents_id_fk", + "tableFrom": "routines", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sandboxed_components": { + "name": "sandboxed_components", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "draft_description": { + "name": "draft_description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "draft_html": { + "name": "draft_html", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "draft_css": { + "name": "draft_css", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "draft_js_functions": { + "name": "draft_js_functions", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "draft_argument_schema": { + "name": "draft_argument_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "published_description": { + "name": "published_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_html": { + "name": "published_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_css": { + "name": "published_css", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_js_functions": { + "name": "published_js_functions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_argument_schema": { + "name": "published_argument_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sample_arguments": { + "name": "sample_arguments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "authored_by": { + "name": "authored_by", + "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": {}, + "foreignKeys": {}, + "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 + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "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": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "columns": [ + "token" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_tools": { + "name": "skill_tools", + "schema": "", + "columns": { + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "declared_by": { + "name": "declared_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_tools_ref_idx": { + "name": "skill_tools_ref_idx", + "columns": [ + { + "expression": "ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "skill_tools_skill_id_skills_id_fk": { + "name": "skill_tools_skill_id_skills_id_fk", + "tableFrom": "skill_tools", + "columnsFrom": [ + "skill_id" + ], + "tableTo": "skills", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "skill_tools_skill_id_ref_pk": { + "name": "skill_tools_skill_id_ref_pk", + "columns": [ + "skill_id", + "ref" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'yours'" + }, + "installed_by": { + "name": "installed_by", + "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": { + "skills_slug_key": { + "name": "skills_slug_key", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "skills_owner_idx": { + "name": "skills_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "skills_owner_user_id_users_id_fk": { + "name": "skills_owner_user_id_users_id_fk", + "tableFrom": "skills", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sso_providers_user_id_users_id_fk": { + "name": "sso_providers_user_id_users_id_fk", + "tableFrom": "sso_providers", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_providers_provider_id_unique": { + "name": "sso_providers_provider_id_unique", + "columns": [ + "provider_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_instructions": { + "name": "user_instructions", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "user_instructions_user_id_users_id_fk": { + "name": "user_instructions_user_id_users_id_fk", + "tableFrom": "user_instructions", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "user_roles_user_id_role_pk": { + "name": "user_roles_user_id_role_pk", + "columns": [ + "user_id", + "role" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "groups": { + "name": "groups", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "onboarding_step": { + "name": "onboarding_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "onboarding_completed_at": { + "name": "onboarding_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_signed_in_at": { + "name": "last_signed_in_at", + "type": "timestamp with time zone", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.work_items": { + "name": "work_items", + "schema": "", + "columns": { + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_at": { + "name": "run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "work_items_claimable_idx": { + "name": "work_items_claimable_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "work_items_kind_key_pk": { + "name": "work_items_kind_key_pk", + "columns": [ + "kind", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_runs": { + "name": "workflow_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "workflow_run_status", + "primaryKey": false, + "notNull": true, + "typeSchema": "public", + "default": "'active'" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "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": { + "workflow_runs_owner_status_idx": { + "name": "workflow_runs_owner_status_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "workflow_runs_agent_status_idx": { + "name": "workflow_runs_agent_status_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_runs_owner_user_id_users_id_fk": { + "name": "workflow_runs_owner_user_id_users_id_fk", + "tableFrom": "workflow_runs", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "workflow_runs_agent_id_agents_id_fk": { + "name": "workflow_runs_agent_id_agents_id_fk", + "tableFrom": "workflow_runs", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_steps": { + "name": "workflow_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "instruction": { + "name": "instruction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "workflow_step_status", + "primaryKey": false, + "notNull": true, + "typeSchema": "public", + "default": "'blocked'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wait_until": { + "name": "wait_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "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": { + "workflow_steps_workflow_key_idx": { + "name": "workflow_steps_workflow_key_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "workflow_steps_workflow_position_idx": { + "name": "workflow_steps_workflow_position_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "workflow_steps_status_wait_idx": { + "name": "workflow_steps_status_wait_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "wait_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_steps_workflow_id_workflow_runs_id_fk": { + "name": "workflow_steps_workflow_id_workflow_runs_id_fk", + "tableFrom": "workflow_steps", + "columnsFrom": [ + "workflow_id" + ], + "tableTo": "workflow_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.agent_type": { + "name": "agent_type", + "schema": "public", + "values": [ + "built_in", + "remote_ag_ui", + "remote_mastra" + ] + }, + "public.credential_kind": { + "name": "credential_kind", + "schema": "public", + "values": [ + "model", + "connector", + "agent", + "mcp", + "mcp_oauth_client", + "mcp_user_token" + ] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": [ + "admin", + "user" + ] + }, + "public.agent_visibility": { + "name": "agent_visibility", + "schema": "public", + "values": [ + "public", + "private" + ] + }, + "public.routine_run_status": { + "name": "routine_run_status", + "schema": "public", + "values": [ + "succeeded", + "failed", + "skipped" + ] + }, + "public.routine_schedule_kind": { + "name": "routine_schedule_kind", + "schema": "public", + "values": [ + "recurring", + "once" + ] + }, + "public.workflow_run_status": { + "name": "workflow_run_status", + "schema": "public", + "values": [ + "active", + "paused", + "succeeded", + "failed", + "cancelled" + ] + }, + "public.workflow_step_status": { + "name": "workflow_step_status", + "schema": "public", + "values": [ + "blocked", + "ready", + "running", + "waiting", + "succeeded", + "failed", + "cancelled" + ] + } + }, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} From bc178dcb6d71831a932787bf18fc2737ba27a064 Mon Sep 17 00:00:00 2001 From: duc15052006-dotcom Date: Sat, 19 Sep 2026 19:22:01 +0700 Subject: [PATCH 05/14] feat(workflows): journal durable workflow migration --- server/drizzle/meta/_journal.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/drizzle/meta/_journal.json b/server/drizzle/meta/_journal.json index 259f9e3e5..345800870 100644 --- a/server/drizzle/meta/_journal.json +++ b/server/drizzle/meta/_journal.json @@ -302,6 +302,13 @@ "when": 1789818500000, "tag": "0042_routine_one_shot_wake", "breakpoints": true + }, + { + "idx": 43, + "version": "7", + "when": 1789819800000, + "tag": "0043_workflow_state", + "breakpoints": true } ] } From ffde8d21611c741e2157147b5c0b198abbe61ec6 Mon Sep 17 00:00:00 2001 From: duc15052006-dotcom Date: Sat, 19 Sep 2026 19:22:53 +0700 Subject: [PATCH 06/14] test(workflows): cover durable state and recovery boundaries --- .../tests/workflow-store.integration.test.ts | 365 ++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100644 server/tests/workflow-store.integration.test.ts diff --git a/server/tests/workflow-store.integration.test.ts b/server/tests/workflow-store.integration.test.ts new file mode 100644 index 000000000..6e5f2a589 --- /dev/null +++ b/server/tests/workflow-store.integration.test.ts @@ -0,0 +1,365 @@ +import { afterAll, afterEach, describe, expect, test } from "bun:test"; +import { randomUUID } from "node:crypto"; +import { eq } from "drizzle-orm"; +import { createAgentProfileStore } from "../src/agents/profile-store"; +import type { AgentActor } from "../src/agents/profile-types"; +import { createChannelStore } from "../src/channels/routes"; +import { createThreadIdentity } from "../src/channels/thread-identity"; +import { createDatabase } from "../src/db/client"; +import { + agentProfiles, + agents, + channels, + intelligenceChannelMappings, + users, + workflowRuns, + workflowSteps, +} from "../src/db/schema"; +import { + createWorkflowStore, + WorkflowNotFoundError, + WorkflowRefusedError, +} from "../src/workflows/store"; +import { TEST_POOL, testDatabaseUrl } from "./support/database"; + +const database = createDatabase(testDatabaseUrl(), TEST_POOL); +const profileStore = createAgentProfileStore( + database, + new URL("https://managed.example.test/ag-ui"), +); +const channelStore = createChannelStore( + database, + profileStore, + createThreadIdentity("workflow-test-deployment"), +); +const store = createWorkflowStore(database); + +const prefix = `workflow-store-${randomUUID()}`; +const createdUserIds: string[] = []; +const createdAgentIds: string[] = []; +const createdChannelIds: string[] = []; + +afterEach(async () => { + for (const userId of createdUserIds) { + await database + .delete(workflowRuns) + .where(eq(workflowRuns.ownerUserId, userId)); + } + for (const channelId of createdChannelIds.splice(0)) { + await database + .delete(intelligenceChannelMappings) + .where(eq(intelligenceChannelMappings.channelId, channelId)); + await database.delete(channels).where(eq(channels.id, channelId)); + } + for (const agentId of createdAgentIds.splice(0)) { + await database + .delete(agentProfiles) + .where(eq(agentProfiles.agentId, agentId)); + await database.delete(agents).where(eq(agents.id, agentId)); + } + for (const userId of createdUserIds.splice(0)) { + await database.delete(users).where(eq(users.id, userId)); + } +}); + +afterAll(async () => { + await database.$client.close(); +}); + +async function createUser(): Promise { + const id = `${prefix}-user-${randomUUID()}`; + await database.insert(users).values({ + id, + email: `${id}@example.test`, + name: "Workflow Test User", + }); + createdUserIds.push(id); + return { id, role: "user" }; +} + +async function createAgent(owner: AgentActor, name = "Producer") { + const profile = await profileStore.create(owner, { + name, + title: "Content Producer", + roleDescription: "Runs a multi-step content workflow.", + visibility: "private", + }); + createdAgentIds.push(profile.id); + return profile.id; +} + +async function createChannel(owner: AgentActor, agentIds: string[]) { + const channel = await channelStore.create(owner, agentIds); + createdChannelIds.push(channel.id); + return channel; +} + +async function setUp() { + const owner = await createUser(); + const agentId = await createAgent(owner); + const channel = await createChannel(owner, [agentId]); + return { owner, agentId, channel }; +} + +function identity(owner: AgentActor, agentId: string) { + return { ownerUserId: owner.id, agentId }; +} + +function planInput( + owner: AgentActor, + agentId: string, + channelId: string, +) { + return { + ...identity(owner, agentId), + channelId, + title: "Produce a short product video", + steps: [ + { + key: "script", + instruction: "Write the approved scene script.", + }, + { + key: "render", + instruction: "Render the video from the approved script.", + dependsOn: ["script"], + }, + { + key: "review", + instruction: "Review the rendered video and collect the final output.", + dependsOn: ["render"], + }, + ], + }; +} + +describe("durable workflow creation", () => { + test("stores a bounded DAG and only roots begin ready", async () => { + const { owner, agentId, channel } = await setUp(); + const plan = await store.create( + planInput(owner, agentId, channel.id), + ); + + expect(plan.status).toBe("active"); + expect(plan.steps.map((step) => [step.key, step.status])).toEqual([ + ["script", "ready"], + ["render", "blocked"], + ["review", "blocked"], + ]); + expect(plan.steps[1]?.dependsOn).toEqual(["script"]); + }); + + test("refuses forward, missing and duplicate dependencies", async () => { + const { owner, agentId, channel } = await setUp(); + + await expect( + store.create({ + ...identity(owner, agentId), + channelId: channel.id, + title: "Bad graph", + steps: [ + { + key: "later", + instruction: "Wait for a future step.", + dependsOn: ["future"], + }, + { key: "future", instruction: "This arrives too late." }, + ], + }), + ).rejects.toBeInstanceOf(WorkflowRefusedError); + + await expect( + store.create({ + ...identity(owner, agentId), + channelId: channel.id, + title: "Duplicate", + steps: [ + { key: "same", instruction: "First." }, + { key: "same", instruction: "Second." }, + ], + }), + ).rejects.toBeInstanceOf(WorkflowRefusedError); + }); + + test("refuses a channel this owner and Bot do not share", async () => { + const owner = await createUser(); + const agentId = await createAgent(owner, "Producer"); + const otherAgent = await createAgent(owner, "Reviewer"); + const otherChannel = await createChannel(owner, [otherAgent]); + + await expect( + store.create( + planInput(owner, agentId, otherChannel.id), + ), + ).rejects.toBeInstanceOf(WorkflowRefusedError); + }); +}); + +describe("owner and Bot isolation", () => { + test("another person and another Bot cannot read or mutate the workflow", async () => { + const { owner, agentId, channel } = await setUp(); + const plan = await store.create( + planInput(owner, agentId, channel.id), + ); + const stranger = await createUser(); + const siblingAgent = await createAgent(owner, "Sibling"); + + expect( + await store.get(identity(stranger, agentId), plan.id), + ).toBeNull(); + expect( + await store.get(identity(owner, siblingAgent), plan.id), + ).toBeNull(); + + await expect( + store.pause(identity(stranger, agentId), plan.id), + ).rejects.toBeInstanceOf(WorkflowNotFoundError); + await expect( + store.startStep(identity(owner, siblingAgent), plan.id, "script"), + ).rejects.toBeInstanceOf(WorkflowNotFoundError); + }); +}); + +describe("dependency and lifecycle transitions", () => { + test("completion promotes dependencies and the last step completes the run", async () => { + const { owner, agentId, channel } = await setUp(); + const who = identity(owner, agentId); + const plan = await store.create( + planInput(owner, agentId, channel.id), + ); + + const first = await store.startStep(who, plan.id, "script"); + expect(first.status).toBe("running"); + expect(first.attempts).toBe(1); + + const afterScript = await store.completeStep(who, plan.id, "script"); + expect( + afterScript.steps.find((step) => step.key === "render")?.status, + ).toBe("ready"); + + await store.startStep(who, plan.id, "render"); + const afterRender = await store.completeStep(who, plan.id, "render"); + expect( + afterRender.steps.find((step) => step.key === "review")?.status, + ).toBe("ready"); + + await store.startStep(who, plan.id, "review"); + const done = await store.completeStep(who, plan.id, "review"); + expect(done.status).toBe("succeeded"); + expect(done.finishedAt).toBeInstanceOf(Date); + }); + + test("pause blocks new work, resume restores it, and cancel closes pending steps", async () => { + const { owner, agentId, channel } = await setUp(); + const who = identity(owner, agentId); + const plan = await store.create( + planInput(owner, agentId, channel.id), + ); + + expect((await store.pause(who, plan.id)).status).toBe("paused"); + await expect( + store.startStep(who, plan.id, "script"), + ).rejects.toBeInstanceOf(WorkflowRefusedError); + + expect((await store.resume(who, plan.id)).status).toBe("active"); + await store.startStep(who, plan.id, "script"); + + const cancelled = await store.cancel(who, plan.id); + expect(cancelled.status).toBe("cancelled"); + expect( + cancelled.steps.every((step) => step.status === "cancelled"), + ).toBe(true); + }); + + test("a failed step can be retried without losing its attempt history", async () => { + const { owner, agentId, channel } = await setUp(); + const who = identity(owner, agentId); + const plan = await store.create( + planInput(owner, agentId, channel.id), + ); + + await store.startStep(who, plan.id, "script"); + const failed = await store.failStep( + who, + plan.id, + "script", + "temporary provider outage", + ); + expect(failed.status).toBe("failed"); + expect(failed.attempts).toBe(1); + + const ready = await store.retryStep(who, plan.id, "script"); + expect(ready.status).toBe("ready"); + const second = await store.startStep(who, plan.id, "script"); + expect(second.attempts).toBe(2); + }); +}); + +describe("durable waits", () => { + test("a waiting step is discoverable when due and stale wakes cannot resume it", async () => { + const { owner, agentId, channel } = await setUp(); + const who = identity(owner, agentId); + const plan = await store.create( + planInput(owner, agentId, channel.id), + ); + await store.startStep(who, plan.id, "script"); + + const original = new Date(Date.now() + 60_000); + const waiting = await store.waitStep(who, plan.id, "script", { + waitUntil: original, + provider: "video-generator", + }); + expect(waiting.status).toBe("waiting"); + expect(waiting.provider).toBe("video-generator"); + + const dueAt = new Date(Date.now() - 60_000); + await database + .update(workflowSteps) + .set({ waitUntil: dueAt }) + .where(eq(workflowSteps.id, waiting.id)); + + const due = await store.dueWaitingSteps(20); + expect(due).toContainEqual({ + ownerUserId: owner.id, + agentId, + workflowId: plan.id, + stepKey: "script", + waitUntil: dueAt, + }); + + await expect( + store.resumeWaitingStep(who, plan.id, "script", original), + ).rejects.toBeInstanceOf(WorkflowRefusedError); + + const resumed = await store.resumeWaitingStep( + who, + plan.id, + "script", + dueAt, + ); + expect(resumed.status).toBe("running"); + expect(resumed.waitUntil).toBeNull(); + }); +}); + +describe("deleted Agents leave no stale workflow", () => { + test("the Agent foreign key cascades its pending workflow and steps", async () => { + const { owner, agentId, channel } = await setUp(); + const who = identity(owner, agentId); + const plan = await store.create( + planInput(owner, agentId, channel.id), + ); + + await database + .delete(agentProfiles) + .where(eq(agentProfiles.agentId, agentId)); + await database.delete(agents).where(eq(agents.id, agentId)); + + expect(await store.get(who, plan.id)).toBeNull(); + const rows = await database + .select({ id: workflowSteps.id }) + .from(workflowSteps) + .where(eq(workflowSteps.workflowId, plan.id)); + expect(rows).toEqual([]); + }); +}); From 6b1a9af17e2ffe1d74d20a1d35909e279cff86c8 Mon Sep 17 00:00:00 2001 From: duc15052006-dotcom Date: Sat, 19 Sep 2026 19:23:11 +0700 Subject: [PATCH 07/14] test(release): pin durable workflow state invariants --- scripts/release-preflight.ts | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/scripts/release-preflight.ts b/scripts/release-preflight.ts index 260497136..fe2e6f6c7 100644 --- a/scripts/release-preflight.ts +++ b/scripts/release-preflight.ts @@ -1472,6 +1472,50 @@ function checkDurableAgentWake(): void { } } +function checkDurableWorkflowState(): void { + const schema = read("server/src/db/schema/coworker.ts"); + const store = read("server/src/workflows/store.ts"); + const migration = read("server/drizzle/0043_workflow_state.sql"); + + for (const evidence of [ + 'workflowRunStatus = pgEnum("workflow_run_status"', + 'workflowStepStatus = pgEnum("workflow_step_status"', + 'export const workflowRuns = pgTable(', + 'export const workflowSteps = pgTable(', + 'dependsOn: text("depends_on").array().notNull().default([])', + ]) { + if (!schema.includes(evidence)) { + fail(`workflows: durable state schema is missing ${evidence}`); + } + } + + for (const evidence of [ + "eq(workflowRuns.ownerUserId, identity.ownerUserId)", + "eq(workflowRuns.agentId, identity.agentId)", + "pg_advisory_xact_lock", + "which must be an earlier step in the same workflow", + "eq(workflowSteps.waitUntil, expectedWaitUntil)", + "lte(workflowSteps.waitUntil, sql\`now()\`)", + "dueWaitingSteps(limit)", + ]) { + if (!store.includes(evidence)) { + fail(`workflows: durable recovery boundary is missing ${evidence}`); + } + } + + for (const evidence of [ + 'CREATE TYPE "public"."workflow_run_status"', + 'CREATE TYPE "public"."workflow_step_status"', + 'CREATE TABLE "workflow_runs"', + 'CREATE TABLE "workflow_steps"', + 'workflow_steps_workflow_key_idx', + ]) { + if (!migration.includes(evidence)) { + fail(`workflows: durable workflow migration is missing ${evidence}`); + } + } +} + function checkVersionSources(): void { const pkg = json("package.json"); const version = pkg.version; @@ -1503,6 +1547,7 @@ checkDesktopCredentialBoundary(); checkProviderConnectionTest(); checkFirstCoworkerHandoff(); checkDurableAgentWake(); +checkDurableWorkflowState(); checkVersionSources(); if (failures.length > 0) { From ef202c6658a84557f32a930a4b444cd2614f6962 Mon Sep 17 00:00:00 2001 From: duc15052006-dotcom Date: Sat, 19 Sep 2026 19:23:41 +0700 Subject: [PATCH 08/14] docs(workflows): document durable workflow state --- docs/workflows.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/workflows.md diff --git a/docs/workflows.md b/docs/workflows.md new file mode 100644 index 000000000..c9670c1d4 --- /dev/null +++ b/docs/workflows.md @@ -0,0 +1,80 @@ +# Durable workflows + +A workflow is a durable multi-step plan owned by one person and one Bot. It is the state layer used +for work that cannot safely live in one model turn: content pipelines, long-running generation, +review chains, or plans that sleep and resume later. + +This layer deliberately reuses the deployment's existing scheduler and `work_items` queue. It does +not create a second lease, timer, or coordinator. + +## State model + +A workflow run records its owner, Bot, destination channel, title and lifecycle: + +- `active` — new steps may start and due waits may resume; +- `paused` — state remains durable but no new step starts; +- `succeeded` — every step completed; +- `failed` — the workflow was explicitly abandoned as failed; +- `cancelled` — the owner/Bot cancelled the remaining work. + +Each step records a stable local key, position, instruction, dependency keys, status, attempt count, +optional provider, optional exact wait timestamp, failure reason and lifecycle timestamps. + +Dependencies may point only to earlier steps supplied in the same create operation. That rule makes +the graph acyclic by construction and prevents a step from naming another workflow's state. + +## Isolation + +Every person-facing transition is filtered by both `owner_user_id` and `agent_id`. Two Bots owned +by the same person therefore cannot read, pause, resume, cancel or mutate each other's workflow just +because they share an owner. + +The destination channel is accepted only when the owner is a member and the Bot is in that same +non-deleted channel. A missing, foreign or deleted channel is one refusal and does not reveal which +part of the check failed. + +Deleting an Agent cascades its workflow rows and steps, so a pending plan cannot survive as stale work +for an identity that no longer exists. + +## Dependencies and retries + +Root steps begin `ready`; dependent steps begin `blocked`. Starting a step is a compare-and-set from +`ready` to `running` and increments its durable attempt count. + +Completing a step promotes only blocked steps whose complete dependency set has succeeded. Workflow +transitions are serialized with a PostgreSQL transaction-scoped advisory lock, so two steps finishing +at the same time cannot leave a dependent step permanently blocked. + +A running step may fail without destroying the workflow. It becomes `failed`, keeps its attempt +history and failure reason, and can be moved back to `ready` only when all of its dependencies still +succeeded. The next start increments the same attempt counter. + +## Durable waits + +A running step can enter `waiting` with an exact future timestamp and an optional provider label. +The provider field is metadata only and must never contain a credential. + +Due-wait discovery uses PostgreSQL's clock. Resuming a wait compares the exact persisted timestamp as +well as the step status. A stale wake therefore cannot resume a step whose wait was cancelled or +rescheduled after that wake was created. + +The execution slice that turns due workflow waits into existing one-shot wake/queue items is separate +from this state foundation. Keeping the state transition separate from delivery lets the existing +`work_items` lease/idempotency mechanism remain the single durable execution queue. + +## Pause, resume and cancel + +Pause and resume change only the workflow gate; they do not erase steps, attempts, provider state or +wait timestamps. A paused workflow cannot start or resume work. + +Cancel marks the run terminal and cancels every still-blocked, ready, running or waiting step in one +serialized transaction. Completed or previously failed steps remain as evidence of what happened. + +## Security invariants + +Workflow state never stores API keys, browser passwords or host credentials. It does not widen Browser, +Files, Computer or plugin grants. When execution wiring calls a provider later, the Agent still acts +through its existing per-person/per-Bot permission and credential paths. + +The database state is a recovery ledger, not an authorization bypass: a persisted task is not evidence +that the action it describes is currently allowed. From 1dca4d0899a42d7a770b364a8fa85f301ec54c6a Mon Sep 17 00:00:00 2001 From: duc15052006-dotcom Date: Sat, 19 Sep 2026 19:24:45 +0700 Subject: [PATCH 09/14] style(workflows): format release preflight --- scripts/release-preflight.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/release-preflight.ts b/scripts/release-preflight.ts index fe2e6f6c7..48cbcf861 100644 --- a/scripts/release-preflight.ts +++ b/scripts/release-preflight.ts @@ -1480,8 +1480,8 @@ function checkDurableWorkflowState(): void { for (const evidence of [ 'workflowRunStatus = pgEnum("workflow_run_status"', 'workflowStepStatus = pgEnum("workflow_step_status"', - 'export const workflowRuns = pgTable(', - 'export const workflowSteps = pgTable(', + "export const workflowRuns = pgTable(", + "export const workflowSteps = pgTable(", 'dependsOn: text("depends_on").array().notNull().default([])', ]) { if (!schema.includes(evidence)) { @@ -1508,7 +1508,7 @@ function checkDurableWorkflowState(): void { 'CREATE TYPE "public"."workflow_step_status"', 'CREATE TABLE "workflow_runs"', 'CREATE TABLE "workflow_steps"', - 'workflow_steps_workflow_key_idx', + "workflow_steps_workflow_key_idx", ]) { if (!migration.includes(evidence)) { fail(`workflows: durable workflow migration is missing ${evidence}`); From bdb23052ffee12096a5127c4618242403b433660 Mon Sep 17 00:00:00 2001 From: duc15052006-dotcom Date: Sat, 19 Sep 2026 19:24:48 +0700 Subject: [PATCH 10/14] style(workflows): format workflow schema --- server/src/db/schema/coworker.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/server/src/db/schema/coworker.ts b/server/src/db/schema/coworker.ts index 7924285e0..f12dd1e7c 100644 --- a/server/src/db/schema/coworker.ts +++ b/server/src/db/schema/coworker.ts @@ -216,10 +216,7 @@ export const workflowRuns = pgTable( updatedAt: updatedAt(), }, (table) => [ - index("workflow_runs_owner_status_idx").on( - table.ownerUserId, - table.status, - ), + index("workflow_runs_owner_status_idx").on(table.ownerUserId, table.status), index("workflow_runs_agent_status_idx").on(table.agentId, table.status), ], ); @@ -265,4 +262,3 @@ export const workflowSteps = pgTable( index("workflow_steps_status_wait_idx").on(table.status, table.waitUntil), ], ); - From 2304ea4c8fe6e8c1ed34159cd8d10c7adc679e83 Mon Sep 17 00:00:00 2001 From: duc15052006-dotcom Date: Sat, 19 Sep 2026 19:24:51 +0700 Subject: [PATCH 11/14] style(workflows): format workflow store --- server/src/workflows/store.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/workflows/store.ts b/server/src/workflows/store.ts index 198a4d180..66cc3bcb0 100644 --- a/server/src/workflows/store.ts +++ b/server/src/workflows/store.ts @@ -321,7 +321,8 @@ export function createWorkflowStore(database: Database): WorkflowStore { MAX_WORKFLOW_INSTRUCTION_CODE_POINTS, ), dependsOn: dependencies, - status: dependencies.length === 0 ? ("ready" as const) : ("blocked" as const), + status: + dependencies.length === 0 ? ("ready" as const) : ("blocked" as const), }; }); } From f99335a52ef418082c80c5fb84b30c3dd4c3b57e Mon Sep 17 00:00:00 2001 From: duc15052006-dotcom Date: Sat, 19 Sep 2026 19:24:55 +0700 Subject: [PATCH 12/14] style(workflows): format integration coverage --- .../tests/workflow-store.integration.test.ts | 52 +++++-------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/server/tests/workflow-store.integration.test.ts b/server/tests/workflow-store.integration.test.ts index 6e5f2a589..d55a4ae3d 100644 --- a/server/tests/workflow-store.integration.test.ts +++ b/server/tests/workflow-store.integration.test.ts @@ -105,11 +105,7 @@ function identity(owner: AgentActor, agentId: string) { return { ownerUserId: owner.id, agentId }; } -function planInput( - owner: AgentActor, - agentId: string, - channelId: string, -) { +function planInput(owner: AgentActor, agentId: string, channelId: string) { return { ...identity(owner, agentId), channelId, @@ -136,9 +132,7 @@ function planInput( describe("durable workflow creation", () => { test("stores a bounded DAG and only roots begin ready", async () => { const { owner, agentId, channel } = await setUp(); - const plan = await store.create( - planInput(owner, agentId, channel.id), - ); + const plan = await store.create(planInput(owner, agentId, channel.id)); expect(plan.status).toBe("active"); expect(plan.steps.map((step) => [step.key, step.status])).toEqual([ @@ -188,9 +182,7 @@ describe("durable workflow creation", () => { const otherChannel = await createChannel(owner, [otherAgent]); await expect( - store.create( - planInput(owner, agentId, otherChannel.id), - ), + store.create(planInput(owner, agentId, otherChannel.id)), ).rejects.toBeInstanceOf(WorkflowRefusedError); }); }); @@ -198,18 +190,12 @@ describe("durable workflow creation", () => { describe("owner and Bot isolation", () => { test("another person and another Bot cannot read or mutate the workflow", async () => { const { owner, agentId, channel } = await setUp(); - const plan = await store.create( - planInput(owner, agentId, channel.id), - ); + const plan = await store.create(planInput(owner, agentId, channel.id)); const stranger = await createUser(); const siblingAgent = await createAgent(owner, "Sibling"); - expect( - await store.get(identity(stranger, agentId), plan.id), - ).toBeNull(); - expect( - await store.get(identity(owner, siblingAgent), plan.id), - ).toBeNull(); + expect(await store.get(identity(stranger, agentId), plan.id)).toBeNull(); + expect(await store.get(identity(owner, siblingAgent), plan.id)).toBeNull(); await expect( store.pause(identity(stranger, agentId), plan.id), @@ -224,9 +210,7 @@ describe("dependency and lifecycle transitions", () => { test("completion promotes dependencies and the last step completes the run", async () => { const { owner, agentId, channel } = await setUp(); const who = identity(owner, agentId); - const plan = await store.create( - planInput(owner, agentId, channel.id), - ); + const plan = await store.create(planInput(owner, agentId, channel.id)); const first = await store.startStep(who, plan.id, "script"); expect(first.status).toBe("running"); @@ -252,9 +236,7 @@ describe("dependency and lifecycle transitions", () => { test("pause blocks new work, resume restores it, and cancel closes pending steps", async () => { const { owner, agentId, channel } = await setUp(); const who = identity(owner, agentId); - const plan = await store.create( - planInput(owner, agentId, channel.id), - ); + const plan = await store.create(planInput(owner, agentId, channel.id)); expect((await store.pause(who, plan.id)).status).toBe("paused"); await expect( @@ -266,17 +248,15 @@ describe("dependency and lifecycle transitions", () => { const cancelled = await store.cancel(who, plan.id); expect(cancelled.status).toBe("cancelled"); - expect( - cancelled.steps.every((step) => step.status === "cancelled"), - ).toBe(true); + expect(cancelled.steps.every((step) => step.status === "cancelled")).toBe( + true, + ); }); test("a failed step can be retried without losing its attempt history", async () => { const { owner, agentId, channel } = await setUp(); const who = identity(owner, agentId); - const plan = await store.create( - planInput(owner, agentId, channel.id), - ); + const plan = await store.create(planInput(owner, agentId, channel.id)); await store.startStep(who, plan.id, "script"); const failed = await store.failStep( @@ -299,9 +279,7 @@ describe("durable waits", () => { test("a waiting step is discoverable when due and stale wakes cannot resume it", async () => { const { owner, agentId, channel } = await setUp(); const who = identity(owner, agentId); - const plan = await store.create( - planInput(owner, agentId, channel.id), - ); + const plan = await store.create(planInput(owner, agentId, channel.id)); await store.startStep(who, plan.id, "script"); const original = new Date(Date.now() + 60_000); @@ -346,9 +324,7 @@ describe("deleted Agents leave no stale workflow", () => { test("the Agent foreign key cascades its pending workflow and steps", async () => { const { owner, agentId, channel } = await setUp(); const who = identity(owner, agentId); - const plan = await store.create( - planInput(owner, agentId, channel.id), - ); + const plan = await store.create(planInput(owner, agentId, channel.id)); await database .delete(agentProfiles) From 53bb5b5537f9f4b5e0810f66f537aa9c382bf8c3 Mon Sep 17 00:00:00 2001 From: duc15052006-dotcom Date: Sat, 19 Sep 2026 19:26:55 +0700 Subject: [PATCH 13/14] style(workflows): remove useless preflight escapes --- scripts/release-preflight.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release-preflight.ts b/scripts/release-preflight.ts index 48cbcf861..9230a4ce2 100644 --- a/scripts/release-preflight.ts +++ b/scripts/release-preflight.ts @@ -1495,7 +1495,7 @@ function checkDurableWorkflowState(): void { "pg_advisory_xact_lock", "which must be an earlier step in the same workflow", "eq(workflowSteps.waitUntil, expectedWaitUntil)", - "lte(workflowSteps.waitUntil, sql\`now()\`)", + "lte(workflowSteps.waitUntil, sql`now()`)", "dueWaitingSteps(limit)", ]) { if (!store.includes(evidence)) { From 4f3ec8de25decf9f3aeccc0c2ec24942c1e4ffa8 Mon Sep 17 00:00:00 2001 From: duc15052006-dotcom Date: Sat, 19 Sep 2026 19:26:59 +0700 Subject: [PATCH 14/14] style(workflows): use optional chain in retry guard --- server/src/workflows/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/workflows/store.ts b/server/src/workflows/store.ts index 66cc3bcb0..49613cdce 100644 --- a/server/src/workflows/store.ts +++ b/server/src/workflows/store.ts @@ -740,7 +740,7 @@ export function createWorkflowStore(database: Database): WorkflowStore { .from(workflowSteps) .where(eq(workflowSteps.workflowId, id)); const step = rows.find((candidate) => candidate.key === wantedKey); - if (!step || step.status !== "failed") { + if (step?.status !== "failed") { throw new WorkflowRefusedError( "Only a failed workflow step can be retried.", );