600 lines
26 KiB
TypeScript
600 lines
26 KiB
TypeScript
import { sql } from "drizzle-orm";
|
|
import {
|
|
pgTable,
|
|
text,
|
|
timestamp,
|
|
integer,
|
|
boolean,
|
|
jsonb,
|
|
doublePrecision,
|
|
uniqueIndex,
|
|
index,
|
|
primaryKey,
|
|
} from "drizzle-orm/pg-core";
|
|
|
|
// Users are mirrored from Clerk on first sign-in.
|
|
// id = Clerk user id (e.g., "user_2abc..."), email is the canonical Clerk email.
|
|
export const users = pgTable(
|
|
"users",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
email: text("email").notNull(),
|
|
displayName: text("display_name"),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => ({
|
|
emailIdx: uniqueIndex("users_email_idx").on(t.email),
|
|
}),
|
|
);
|
|
|
|
// One per user. Tracks the user's unified agent's container stack + Git repo.
|
|
// Per changes.md §2A: per-user Gitea containers removed; central Gitea shared.
|
|
// Per changes.md §5: ONE actor per user manages the full orchestration layer.
|
|
export const userStacks = pgTable(
|
|
"user_stacks",
|
|
{
|
|
userId: text("user_id")
|
|
.primaryKey()
|
|
.references(() => users.id, { onDelete: "cascade" }),
|
|
status: text("status", {
|
|
enum: ["provisioning", "running", "stopped", "error"],
|
|
})
|
|
.notNull()
|
|
.default("provisioning"),
|
|
|
|
// Central Gitea (shared org-wide, changes.md §2A).
|
|
giteaRepoName: text("gitea_repo_name"),
|
|
giteaRepoOwner: text("gitea_repo_owner"),
|
|
|
|
// Per-user OpenCode container (from shared image, changes.md §3).
|
|
opencodeContainerId: text("opencode_container_id"),
|
|
opencodeContainerName: text("opencode_container_name"),
|
|
opencodeHost: text("opencode_host"),
|
|
opencodePort: integer("opencode_port"),
|
|
opencodePassword: text("opencode_password"),
|
|
|
|
workspacePath: text("workspace_path"),
|
|
|
|
// Version tracking for image rollouts (changes.md §9).
|
|
imageVersion: text("image_version"),
|
|
migrationVersion: text("migration_version"),
|
|
promptVersion: text("prompt_version"),
|
|
|
|
lastError: text("last_error"),
|
|
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => ({
|
|
statusIdx: index("user_stacks_status_idx").on(t.status),
|
|
}),
|
|
);
|
|
|
|
// Per changes.md §5: ONE unified actor per user (no separate grow/sub actors).
|
|
// The actor manages: infra state, git state, runtime comms, migrations, API orchestration.
|
|
export const actors = pgTable(
|
|
"actors",
|
|
{
|
|
actorId: text("actor_id").notNull(),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: "cascade" }),
|
|
kind: text("kind", { enum: ["user"] }).notNull().default("user"),
|
|
status: text("status", {
|
|
enum: ["idle", "running", "done", "error"],
|
|
})
|
|
.notNull()
|
|
.default("idle"),
|
|
lastActivityAt: timestamp("last_activity_at", { withTimezone: true }),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => ({
|
|
pk: primaryKey({ columns: [t.userId, t.actorId] }),
|
|
kindIdx: index("actors_user_kind_idx").on(t.userId, t.kind),
|
|
}),
|
|
);
|
|
|
|
// Per-user repo registry (in addition to the primary memory repo).
|
|
export const repos = pgTable(
|
|
"repos",
|
|
{
|
|
id: text("id").primaryKey(), // `${userId}:${name}`
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: "cascade" }),
|
|
name: text("name").notNull(),
|
|
role: text("role", { enum: ["memory", "project"] }).notNull(),
|
|
giteaOwner: text("gitea_owner").notNull(),
|
|
giteaName: text("gitea_name").notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => ({
|
|
userRoleIdx: index("repos_user_role_idx").on(t.userId, t.role),
|
|
}),
|
|
);
|
|
|
|
// OpenCode sessions opened by sub-agents.
|
|
export const opencodeSessions = pgTable(
|
|
"opencode_sessions",
|
|
{
|
|
id: text("id").primaryKey(), // OpenCode session id from POST /session
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: "cascade" }),
|
|
actorId: text("actor_id"),
|
|
title: text("title"),
|
|
parentId: text("parent_id"),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => ({
|
|
userIdx: index("opencode_sessions_user_idx").on(t.userId),
|
|
}),
|
|
);
|
|
|
|
// Audit/event log — small append-only stream used for debugging + the
|
|
// frontend's "task progress timeline" until we move it to Rivet streams only.
|
|
export const events = pgTable(
|
|
"events",
|
|
{
|
|
id: text("id")
|
|
.primaryKey()
|
|
.default(sql`gen_random_uuid()::text`),
|
|
userId: text("user_id").notNull(),
|
|
actorId: text("actor_id"),
|
|
type: text("type").notNull(),
|
|
payload: jsonb("payload").$type<Record<string, unknown>>(),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => ({
|
|
userIdx: index("events_user_idx").on(t.userId, t.createdAt),
|
|
}),
|
|
);
|
|
|
|
export type User = typeof users.$inferSelect;
|
|
export type NewUser = typeof users.$inferInsert;
|
|
export type UserStack = typeof userStacks.$inferSelect;
|
|
export type NewUserStack = typeof userStacks.$inferInsert;
|
|
export type ActorRow = typeof actors.$inferSelect;
|
|
export type RepoRow = typeof repos.$inferSelect;
|
|
|
|
export const missionRegistry = pgTable(
|
|
"mission_registry",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
version: text("version").notNull(),
|
|
title: text("title").notNull(),
|
|
shortTitle: text("short_title").notNull(),
|
|
actorType: text("actor_type"),
|
|
actorBacked: boolean("actor_backed").notNull().default(false),
|
|
skillPath: text("skill_path").notNull(),
|
|
displayOrder: integer("display_order").notNull(),
|
|
definition: jsonb("definition").$type<Record<string, unknown>>().notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
},
|
|
(t) => ({ displayIdx: index("mission_registry_display_idx").on(t.displayOrder) }),
|
|
);
|
|
|
|
export type MissionRegistryRow = typeof missionRegistry.$inferSelect;
|
|
|
|
export const workflowRuns = pgTable(
|
|
"workflow_runs",
|
|
{
|
|
id: text("id").primaryKey().default(sql`gen_random_uuid()::text`),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
workflowId: text("workflow_id").notNull(),
|
|
workflowVersion: text("workflow_version").notNull(),
|
|
status: text("status", { enum: ["draft", "running", "paused", "completed", "failed"] }).notNull().default("running"),
|
|
goal: text("goal"),
|
|
input: jsonb("input").$type<Record<string, unknown>>(),
|
|
currentStepId: text("current_step_id"),
|
|
progressPercent: integer("progress_percent").notNull().default(0),
|
|
qscoreBefore: jsonb("qscore_before").$type<Record<string, unknown>>(),
|
|
qscoreAfter: jsonb("qscore_after").$type<Record<string, unknown>>(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
},
|
|
(t) => ({ userIdx: index("workflow_runs_user_idx").on(t.userId, t.createdAt), workflowIdx: index("workflow_runs_workflow_idx").on(t.workflowId) }),
|
|
);
|
|
|
|
export const workflowRunModules = pgTable("workflow_run_modules", {
|
|
id: text("id").primaryKey().default(sql`gen_random_uuid()::text`),
|
|
runId: text("run_id").notNull().references(() => workflowRuns.id, { onDelete: "cascade" }),
|
|
moduleId: text("module_id").notNull(),
|
|
title: text("title").notNull(),
|
|
status: text("status").notNull().default("idle"),
|
|
service: text("service"),
|
|
idempotencyKey: text("idempotency_key"),
|
|
retryCount: integer("retry_count").notNull().default(0),
|
|
maxRetries: integer("max_retries").notNull().default(2),
|
|
outputSummary: text("output_summary"),
|
|
output: jsonb("output").$type<Record<string, unknown>>(),
|
|
error: text("error"),
|
|
startedAt: timestamp("started_at", { withTimezone: true }),
|
|
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
});
|
|
|
|
export const workflowArtifacts = pgTable("workflow_artifacts", {
|
|
id: text("id").primaryKey().default(sql`gen_random_uuid()::text`),
|
|
runId: text("run_id").notNull().references(() => workflowRuns.id, { onDelete: "cascade" }),
|
|
moduleId: text("module_id"),
|
|
type: text("type").notNull(),
|
|
title: text("title").notNull(),
|
|
repoPath: text("repo_path"),
|
|
publicUrl: text("public_url"),
|
|
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
});
|
|
|
|
export const workflowApprovals = pgTable("workflow_approvals", {
|
|
id: text("id").primaryKey().default(sql`gen_random_uuid()::text`),
|
|
runId: text("run_id").notNull().references(() => workflowRuns.id, { onDelete: "cascade" }),
|
|
approvalId: text("approval_id").notNull(),
|
|
status: text("status", { enum: ["pending", "approved", "rejected"] }).notNull().default("pending"),
|
|
payload: jsonb("payload").$type<Record<string, unknown>>(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
resolvedAt: timestamp("resolved_at", { withTimezone: true }),
|
|
});
|
|
|
|
export const qscoreSnapshots = pgTable("qscore_snapshots", {
|
|
id: text("id").primaryKey().default(sql`gen_random_uuid()::text`),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
runId: text("run_id").references(() => workflowRuns.id, { onDelete: "cascade" }),
|
|
snapshotType: text("snapshot_type", { enum: ["baseline", "module", "final"] }).notNull(),
|
|
score: integer("score"),
|
|
payload: jsonb("payload").$type<Record<string, unknown>>(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
});
|
|
|
|
export const workflowEvents = pgTable("workflow_events", {
|
|
id: text("id").primaryKey().default(sql`gen_random_uuid()::text`),
|
|
runId: text("run_id").notNull().references(() => workflowRuns.id, { onDelete: "cascade" }),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
type: text("type").notNull(),
|
|
payload: jsonb("payload").$type<Record<string, unknown>>(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
});
|
|
|
|
export const growConversations = pgTable(
|
|
"grow_conversations",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
title: text("title").notNull().default("Talk to Me"),
|
|
active: boolean("active").notNull().default(true),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
},
|
|
(t) => ({ userIdx: index("grow_conversations_user_idx").on(t.userId, t.updatedAt) }),
|
|
);
|
|
|
|
export const growConversationMessages = pgTable(
|
|
"grow_conversation_messages",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
conversationId: text("conversation_id").notNull().references(() => growConversations.id, { onDelete: "cascade" }),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
role: text("role", { enum: ["user", "assistant"] }).notNull(),
|
|
sender: text("sender").notNull(),
|
|
content: text("content").notNull(),
|
|
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
},
|
|
(t) => ({ conversationIdx: index("grow_conversation_messages_conversation_idx").on(t.conversationId, t.createdAt) }),
|
|
);
|
|
|
|
export const growActiveMissions = pgTable(
|
|
"grow_active_missions",
|
|
{
|
|
instanceId: text("instance_id").primaryKey(),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
missionId: text("mission_id").notNull(),
|
|
workflowId: text("workflow_id").notNull(),
|
|
actorType: text("actor_type"),
|
|
title: text("title").notNull(),
|
|
shortTitle: text("short_title").notNull(),
|
|
status: text("status").notNull(),
|
|
progressPercent: integer("progress_percent").notNull().default(0),
|
|
currentStageId: text("current_stage_id"),
|
|
goal: text("goal"),
|
|
mission: jsonb("mission").$type<Record<string, unknown>>().notNull(),
|
|
snapshot: jsonb("snapshot").$type<Record<string, unknown>>(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
},
|
|
(t) => ({ userIdx: index("grow_active_missions_user_idx").on(t.userId, t.updatedAt) }),
|
|
);
|
|
|
|
export type OpencodeSessionRow = typeof opencodeSessions.$inferSelect;
|
|
export type WorkflowRunRow = typeof workflowRuns.$inferSelect;
|
|
|
|
export const growEvents = pgTable(
|
|
"grow_events",
|
|
{
|
|
id: text("id").primaryKey().default(sql`gen_random_uuid()::text`),
|
|
userId: text("user_id").references(() => users.id, { onDelete: "cascade" }),
|
|
orgId: text("org_id"),
|
|
source: text("source").notNull(),
|
|
type: text("type").notNull(),
|
|
category: text("category", {
|
|
enum: ["mission", "service", "artifact", "usage", "qscore", "entitlement", "system"],
|
|
}).notNull().default("service"),
|
|
occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
|
|
receivedAt: timestamp("received_at", { withTimezone: true }).defaultNow().notNull(),
|
|
mission: jsonb("mission").$type<Record<string, unknown>>(),
|
|
subject: jsonb("subject").$type<Record<string, unknown>>(),
|
|
correlation: jsonb("correlation").$type<Record<string, unknown>>(),
|
|
payload: jsonb("payload").$type<Record<string, unknown>>().notNull().default(sql`'{}'::jsonb`),
|
|
raw: jsonb("raw").$type<Record<string, unknown>>(),
|
|
dedupeKey: text("dedupe_key"),
|
|
processingStatus: text("processing_status", {
|
|
enum: ["pending", "processing", "processed", "failed", "unresolved"],
|
|
}).notNull().default("pending"),
|
|
processingError: text("processing_error"),
|
|
processedAt: timestamp("processed_at", { withTimezone: true }),
|
|
},
|
|
(t) => ({
|
|
userIdx: index("grow_events_user_idx").on(t.userId, t.occurredAt),
|
|
statusIdx: index("grow_events_status_idx").on(t.processingStatus, t.receivedAt),
|
|
sourceIdx: index("grow_events_source_idx").on(t.source, t.type, t.occurredAt),
|
|
dedupeIdx: uniqueIndex("grow_events_dedupe_idx").on(t.dedupeKey),
|
|
}),
|
|
);
|
|
|
|
export const missionServiceSessions = pgTable(
|
|
"mission_service_sessions",
|
|
{
|
|
id: text("id").primaryKey().default(sql`gen_random_uuid()::text`),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
missionInstanceId: text("mission_instance_id").references(() => growActiveMissions.instanceId, { onDelete: "set null" }),
|
|
missionId: text("mission_id"),
|
|
stageId: text("stage_id"),
|
|
serviceId: text("service_id").notNull(),
|
|
externalId: text("external_id").notNull(),
|
|
status: text("status").notNull().default("active"),
|
|
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
|
|
lastEventId: text("last_event_id").references(() => growEvents.id, { onDelete: "set null" }),
|
|
lastCheckedAt: timestamp("last_checked_at", { withTimezone: true }),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
},
|
|
(t) => ({
|
|
userIdx: index("mission_service_sessions_user_idx").on(t.userId, t.updatedAt),
|
|
externalIdx: uniqueIndex("mission_service_sessions_external_idx").on(t.serviceId, t.externalId),
|
|
missionIdx: index("mission_service_sessions_mission_idx").on(t.missionInstanceId, t.stageId),
|
|
}),
|
|
);
|
|
|
|
export const missionArtifacts = pgTable(
|
|
"mission_artifacts",
|
|
{
|
|
id: text("id").primaryKey().default(sql`gen_random_uuid()::text`),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
missionInstanceId: text("mission_instance_id").references(() => growActiveMissions.instanceId, { onDelete: "cascade" }),
|
|
missionId: text("mission_id"),
|
|
stageId: text("stage_id"),
|
|
sourceEventId: text("source_event_id").references(() => growEvents.id, { onDelete: "set null" }),
|
|
serviceId: text("service_id"),
|
|
externalId: text("external_id"),
|
|
type: text("type").notNull(),
|
|
title: text("title").notNull(),
|
|
status: text("status").notNull().default("ready"),
|
|
summary: text("summary"),
|
|
contentMd: text("content_md"),
|
|
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
},
|
|
(t) => ({
|
|
userIdx: index("mission_artifacts_user_idx").on(t.userId, t.createdAt),
|
|
missionIdx: index("mission_artifacts_mission_idx").on(t.missionInstanceId, t.createdAt),
|
|
externalIdx: index("mission_artifacts_external_idx").on(t.serviceId, t.externalId),
|
|
}),
|
|
);
|
|
|
|
export const growQscoreSignals = pgTable(
|
|
"grow_qscore_signals",
|
|
{
|
|
id: text("id").primaryKey().default(sql`gen_random_uuid()::text`),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
sourceEventId: text("source_event_id").references(() => growEvents.id, { onDelete: "set null" }),
|
|
signalId: text("signal_id").notNull(),
|
|
score: doublePrecision("score").notNull(),
|
|
present: boolean("present").notNull().default(true),
|
|
source: text("source"),
|
|
raw: jsonb("raw").$type<Record<string, unknown>>(),
|
|
occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
},
|
|
(t) => ({
|
|
userIdx: index("grow_qscore_signals_user_idx").on(t.userId, t.occurredAt),
|
|
signalIdx: index("grow_qscore_signals_signal_idx").on(t.signalId, t.occurredAt),
|
|
}),
|
|
);
|
|
|
|
export const growQscoreLatest = pgTable(
|
|
"grow_qscore_latest",
|
|
{
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
signalId: text("signal_id").notNull(),
|
|
score: doublePrecision("score").notNull(),
|
|
present: boolean("present").notNull().default(true),
|
|
source: text("source"),
|
|
sourceEventId: text("source_event_id").references(() => growEvents.id, { onDelete: "set null" }),
|
|
raw: jsonb("raw").$type<Record<string, unknown>>(),
|
|
occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
},
|
|
(t) => ({
|
|
pk: primaryKey({ columns: [t.userId, t.signalId] }),
|
|
userIdx: index("grow_qscore_latest_user_idx").on(t.userId, t.updatedAt),
|
|
}),
|
|
);
|
|
|
|
export const growQscoreProjectionState = pgTable("grow_qscore_projection_state", {
|
|
userId: text("user_id").primaryKey().references(() => users.id, { onDelete: "cascade" }),
|
|
score: integer("score").notNull().default(0),
|
|
signalCount: integer("signal_count").notNull().default(0),
|
|
dimensions: jsonb("dimensions").$type<Record<string, unknown>>(),
|
|
summary: text("summary"),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
});
|
|
|
|
export const missionActions = pgTable(
|
|
"mission_actions",
|
|
{
|
|
id: text("id").primaryKey().default(sql`gen_random_uuid()::text`),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
missionInstanceId: text("mission_instance_id").notNull().references(() => growActiveMissions.instanceId, { onDelete: "cascade" }),
|
|
missionId: text("mission_id").notNull(),
|
|
stageId: text("stage_id"),
|
|
|
|
agentId: text("agent_id").notNull(),
|
|
agentName: text("agent_name").notNull(),
|
|
baseAgent: text("base_agent"),
|
|
serviceId: text("service_id"),
|
|
toolName: text("tool_name"),
|
|
|
|
mode: text("mode", { enum: ["autonomous", "approval_required", "user_input_required", "suggestion"] }).notNull(),
|
|
status: text("status", {
|
|
enum: ["queued", "running", "waiting_approval", "waiting_user_input", "done", "failed", "dismissed", "snoozed"],
|
|
}).notNull().default("queued"),
|
|
|
|
title: text("title").notNull(),
|
|
body: text("body").notNull(),
|
|
prompt: text("prompt"),
|
|
payload: jsonb("payload").$type<Record<string, unknown>>().notNull().default(sql`'{}'::jsonb`),
|
|
result: jsonb("result").$type<Record<string, unknown>>(),
|
|
error: text("error"),
|
|
|
|
sourceEventId: text("source_event_id").references(() => growEvents.id, { onDelete: "set null" }),
|
|
idempotencyKey: text("idempotency_key"),
|
|
priority: integer("priority").notNull().default(0),
|
|
urgency: text("urgency", { enum: ["now", "today", "soon", "calm"] }).notNull().default("calm"),
|
|
dueAt: timestamp("due_at", { withTimezone: true }),
|
|
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
resolvedAt: timestamp("resolved_at", { withTimezone: true }),
|
|
},
|
|
(t) => ({
|
|
missionIdx: index("mission_actions_mission_idx").on(t.userId, t.missionInstanceId, t.status, t.priority),
|
|
userIdx: index("mission_actions_user_idx").on(t.userId, t.status, t.updatedAt),
|
|
sourceIdx: index("mission_actions_source_idx").on(t.sourceEventId),
|
|
dueIdx: index("mission_actions_due_idx").on(t.dueAt),
|
|
idempotencyIdx: uniqueIndex("mission_actions_idempotency_idx").on(t.idempotencyKey),
|
|
}),
|
|
);
|
|
|
|
export const missionSuggestions = pgTable(
|
|
"mission_suggestions",
|
|
{
|
|
id: text("id").primaryKey().default(sql`gen_random_uuid()::text`),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
missionInstanceId: text("mission_instance_id").notNull().references(() => growActiveMissions.instanceId, { onDelete: "cascade" }),
|
|
missionId: text("mission_id").notNull(),
|
|
stageId: text("stage_id"),
|
|
role: text("role").notNull(),
|
|
type: text("type", { enum: ["action", "practice", "review", "artifact", "blocked", "insight"] }).notNull(),
|
|
title: text("title").notNull(),
|
|
body: text("body").notNull(),
|
|
reason: text("reason"),
|
|
priority: integer("priority").notNull().default(0),
|
|
urgency: text("urgency", { enum: ["now", "today", "soon", "calm"] }).notNull().default("calm"),
|
|
status: text("status", { enum: ["active", "done", "dismissed", "expired"] }).notNull().default("active"),
|
|
ctaLabel: text("cta_label").notNull(),
|
|
ctaHref: text("cta_href").notNull(),
|
|
sourceRefs: jsonb("source_refs").$type<Record<string, unknown>>().notNull().default(sql`'{}'::jsonb`),
|
|
generatedBy: text("generated_by", { enum: ["deterministic", "agent", "manual"] }).notNull().default("deterministic"),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
},
|
|
(t) => ({
|
|
missionIdx: index("mission_suggestions_mission_idx").on(t.userId, t.missionInstanceId, t.status, t.priority),
|
|
roleIdx: index("mission_suggestions_role_idx").on(t.missionInstanceId, t.role, t.status),
|
|
expiryIdx: index("mission_suggestions_expiry_idx").on(t.expiresAt),
|
|
}),
|
|
);
|
|
|
|
export const missionCoachRuns = pgTable(
|
|
"mission_coach_runs",
|
|
{
|
|
id: text("id").primaryKey().default(sql`gen_random_uuid()::text`),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
missionInstanceId: text("mission_instance_id").notNull().references(() => growActiveMissions.instanceId, { onDelete: "cascade" }),
|
|
missionId: text("mission_id").notNull(),
|
|
status: text("status", { enum: ["running", "completed", "failed"] }).notNull().default("running"),
|
|
windowStart: timestamp("window_start", { withTimezone: true }).notNull(),
|
|
windowEnd: timestamp("window_end", { withTimezone: true }).notNull(),
|
|
summary: text("summary"),
|
|
inputDigest: jsonb("input_digest").$type<Record<string, unknown>>().notNull().default(sql`'{}'::jsonb`),
|
|
output: jsonb("output").$type<Record<string, unknown>>().notNull().default(sql`'{}'::jsonb`),
|
|
model: text("model"),
|
|
promptVersion: text("prompt_version").notNull().default("mission-coach-v1"),
|
|
skillVersion: text("skill_version"),
|
|
error: text("error"),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
},
|
|
(t) => ({ missionIdx: index("mission_coach_runs_mission_idx").on(t.userId, t.missionInstanceId, t.createdAt) }),
|
|
);
|
|
|
|
export const growHomeNotifications = pgTable(
|
|
"grow_home_notifications",
|
|
{
|
|
id: text("id").primaryKey().default(sql`gen_random_uuid()::text`),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
moduleId: text("module_id", {
|
|
enum: ["suggestions", "missions", "social", "pathways", "productivity", "rewards"],
|
|
}).notNull(),
|
|
title: text("title").notNull(),
|
|
subtitle: text("subtitle").notNull(),
|
|
tag: text("tag").notNull(),
|
|
urgency: text("urgency", { enum: ["now", "today", "soon", "calm"] }).notNull().default("calm"),
|
|
href: text("href").notNull(),
|
|
source: text("source"),
|
|
sourceRef: jsonb("source_ref").$type<Record<string, unknown>>(),
|
|
priority: integer("priority").notNull().default(0),
|
|
generatedBy: text("generated_by", { enum: ["deterministic", "agent", "demo", "manual"] }).notNull().default("deterministic"),
|
|
reason: text("reason"),
|
|
status: text("status", { enum: ["active", "dismissed", "expired"] }).notNull().default("active"),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
},
|
|
(t) => ({
|
|
userIdx: index("grow_home_notifications_user_idx").on(t.userId, t.status, t.priority),
|
|
moduleIdx: index("grow_home_notifications_module_idx").on(t.userId, t.moduleId, t.status),
|
|
expiryIdx: index("grow_home_notifications_expiry_idx").on(t.expiresAt),
|
|
}),
|
|
);
|
|
|
|
export type GrowEventRow = typeof growEvents.$inferSelect;
|
|
export type NewGrowEvent = typeof growEvents.$inferInsert;
|
|
export type MissionActionRow = typeof missionActions.$inferSelect;
|
|
export type NewMissionAction = typeof missionActions.$inferInsert;
|
|
export type MissionSuggestionRow = typeof missionSuggestions.$inferSelect;
|
|
export type NewMissionSuggestion = typeof missionSuggestions.$inferInsert;
|
|
export type MissionCoachRunRow = typeof missionCoachRuns.$inferSelect;
|
|
export type GrowHomeNotificationRow = typeof growHomeNotifications.$inferSelect;
|
|
export type NewGrowHomeNotification = typeof growHomeNotifications.$inferInsert;
|