Read-side only — write paths land with claim_task / complete_task in the next epic. Keyset pagination on started_at, three procedures (listRecent, listForTask, summary), and a /settings/runs view that mirrors the audit page's visual language. Co-authored-by: Cursor <cursoragent@cursor.com>
81 lines
2.9 KiB
TypeScript
81 lines
2.9 KiB
TypeScript
import {
|
|
pgTable,
|
|
uuid,
|
|
varchar,
|
|
text,
|
|
integer,
|
|
jsonb,
|
|
timestamp,
|
|
index,
|
|
} from "drizzle-orm/pg-core";
|
|
|
|
import { workspaces } from "./workspaces";
|
|
import { users } from "./users";
|
|
import { markdownBacklogItems } from "./markdown_backlog";
|
|
|
|
/**
|
|
* One row per agent session against a backlog item. The MCP tools
|
|
* (`claim_task`, `complete_task`, …) will be the write path in the next
|
|
* epic; this table is read-side + storage only at this stage. Rows can
|
|
* be inserted manually for development.
|
|
*
|
|
* Conventions captured here so future maintainers don't drift:
|
|
*
|
|
* - `outcome` is constrained at the application layer to one of
|
|
* {succeeded, failed, cancelled, stalled}. The column is nullable
|
|
* because in-flight runs (no `finished_at`) carry NULL outcomes —
|
|
* surfaced in the UI as the synthetic "open" bucket.
|
|
*
|
|
* - `actorUserId` is null for anonymous / dev sessions where no user
|
|
* is attached. The settings UI shows "—" for null actors.
|
|
*
|
|
* - Token totals follow the Symphony model: a single absolute
|
|
* `tokens_total` recorded at close-out, not incremental deltas.
|
|
* `tokens_input` / `tokens_output` are optional breakdowns.
|
|
*
|
|
* - `metadata` is JSONB but should stay small (roughly < 1 KB).
|
|
* Don't dump full transcripts here — that's what an external
|
|
* object store is for.
|
|
*
|
|
* - Indexes are tuned for the two read paths the UI exercises:
|
|
* `(workspace_id, started_at DESC)` for the recent-runs settings
|
|
* view and `(backlog_item_id, started_at DESC)` for per-task
|
|
* history on the task detail panel (deferred to a follow-up task
|
|
* to avoid stepping on the parent's concurrent work).
|
|
*/
|
|
export const agentRuns = pgTable(
|
|
"agent_runs",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
workspaceId: uuid("workspace_id")
|
|
.notNull()
|
|
.references(() => workspaces.id, { onDelete: "cascade" }),
|
|
backlogItemId: uuid("backlog_item_id")
|
|
.notNull()
|
|
.references(() => markdownBacklogItems.id, { onDelete: "cascade" }),
|
|
actorUserId: uuid("actor_user_id").references(() => users.id, {
|
|
onDelete: "set null",
|
|
}),
|
|
startedAt: timestamp("started_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
finishedAt: timestamp("finished_at", { withTimezone: true }),
|
|
outcome: varchar("outcome", { length: 20 }),
|
|
error: text("error"),
|
|
tokensInput: integer("tokens_input"),
|
|
tokensOutput: integer("tokens_output"),
|
|
tokensTotal: integer("tokens_total"),
|
|
notes: text("notes"),
|
|
metadata: jsonb("metadata").$type<Record<string, unknown> | null>(),
|
|
},
|
|
(table) => ({
|
|
workspaceStartedIdx: index("agent_runs_workspace_id_started_at_idx").on(
|
|
table.workspaceId,
|
|
table.startedAt.desc(),
|
|
),
|
|
backlogStartedIdx: index("agent_runs_backlog_item_id_started_at_idx").on(
|
|
table.backlogItemId,
|
|
table.startedAt.desc(),
|
|
),
|
|
}),
|
|
);
|