feat(db): 0013 tasks tables and 0014 agent_run_event; tasks uses db migrations
tasks_run_meta/tasks_fact/tasks_op_ledger move into packages/db (if not exists, old tasks_migration tables dropped); packages/tasks/migrations and migrateTasksTables removed, tests migrate their schema with @umt/db. agent_run_event(run_id, seq, type, at, payload) with (run_id, seq) primary key; types regenerated, db-sync covers it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
db5352eecd
commit
da8e53bbca
13 files changed
+250
-36
No files matched your search
+11
-1
@@ -27,8 +27,10 @@ PostgreSQL 访问层:Kysely 客户端与连接池、表类型(`src/types.ts`)、
|
||||
| `0010_agent` | agent_run、artifact、decision、approval、automation_grant |
|
||||
| `0011_ops` | reconcile_run、server_mapping |
|
||||
| `0012_organization` | organization_record(change_seq 序列 + 触发器)、organization_file_result;回填 asset_operation.organization_record_id |
|
||||
| `0013_tasks_tables` | 并入 @umt/tasks 自有表:tasks_run_meta、tasks_fact、tasks_op_ledger(if not exists,可接管 W1 旧库) |
|
||||
| `0014_agent_run_event` | agent_run_event:Agent 运行事件,主键 (run_id, seq),删 run 级联 |
|
||||
|
||||
## 表清单(41 张)
|
||||
## 表清单(45 张)
|
||||
|
||||
### 底座 / 运维
|
||||
| 表 | 用途与关键不变量 |
|
||||
@@ -100,6 +102,14 @@ PostgreSQL 访问层:Kysely 客户端与连接池、表类型(`src/types.ts`)、
|
||||
| `decision` | 待处理中心条目;同一 `dedupe_key` 同时只有一个未关闭条目 |
|
||||
| `approval` | 单次审批,绑定 plan + payload_hash + expected_revision |
|
||||
| `automation_grant` | 持续授权(动作 + 范围 + 预算 + 策略版本),可撤销 |
|
||||
| `agent_run_event` | Agent 运行事件(9 种,见 contracts `AgentRunEventType`);**(run_id, seq) 主键**,先落库再推 SSE;payload = 事件主体去掉 type |
|
||||
|
||||
### 任务引擎(@umt/tasks 内部使用,不经 API 暴露)
|
||||
| 表 | 用途与关键不变量 |
|
||||
|---|---|
|
||||
| `tasks_run_meta` | 引擎 run 的业务侧元数据(traceId/优先级/取消标志) |
|
||||
| `tasks_fact` | signal 前先落库的事实,waitForFact 以它为准 |
|
||||
| `tasks_op_ledger` | 操作账本 fencing:attempt_token,旧 attempt 提交被拒 |
|
||||
|
||||
### 整理
|
||||
| 表 | 用途与关键不变量 |
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { dropTables, execDdl, oneOf } from "../src/migration-helpers.ts";
|
||||
|
||||
// @umt/tasks 的三张自有表并入统一迁移(W1 审查 S2;原 packages/tasks/migrations/0001_tasks_tables)。
|
||||
// 表结构与原迁移一致,tasks 包通过注入的 Kysely 访问:
|
||||
// - tasks_run_meta:引擎 run 的业务侧元数据(traceId/优先级/取消标志),取消标志是 abortSignal 的来源
|
||||
// - tasks_fact:signal 之前先落库的事实;waitForFact 以它为准,信号只用来加速唤醒
|
||||
// - tasks_op_ledger:操作账本(fencing),记录 attempt_token,旧 attempt 的提交会被拒绝
|
||||
// 用 if not exists:W1 期间用 migrateTasksTables 建过表的库可以直接接管;顺手删掉旧的独立迁移记录表。
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`
|
||||
create table if not exists tasks_run_meta (
|
||||
run_id text primary key,
|
||||
task_name text not null,
|
||||
task_version integer not null,
|
||||
idempotency_key text not null,
|
||||
trace_id text,
|
||||
priority integer not null default 0,
|
||||
cancel_requested_at timestamptz,
|
||||
cancel_reason text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index if not exists tasks_run_meta_task_idx on tasks_run_meta (task_name, created_at);
|
||||
|
||||
create table if not exists tasks_fact (
|
||||
id bigserial primary key,
|
||||
run_id text not null,
|
||||
name text not null,
|
||||
payload jsonb,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index if not exists tasks_fact_run_name_idx on tasks_fact (run_id, name, id);
|
||||
|
||||
create table if not exists tasks_op_ledger (
|
||||
op_key text primary key,
|
||||
run_id text,
|
||||
attempt_token uuid not null,
|
||||
attempts integer not null default 1,
|
||||
status text not null ${oneOf("status", ["started", "succeeded"])},
|
||||
result jsonb,
|
||||
last_error text,
|
||||
started_at timestamptz not null default now(),
|
||||
completed_at timestamptz
|
||||
);
|
||||
|
||||
do $$ begin
|
||||
execute format('drop table if exists %I.tasks_migration, %I.tasks_migration_lock', current_schema(), current_schema());
|
||||
end $$;
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(db, dropTables(["tasks_op_ledger", "tasks_fact", "tasks_run_meta"]));
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { dropTables, execDdl, oneOf } from "../src/migration-helpers.ts";
|
||||
|
||||
// Agent 运行事件持久化(W1 审查 B9;REQ §10.6):先落库再推 SSE,断线/跨进程按 (run_id, seq) 回放。
|
||||
// type 取值与 contracts AgentRunEventType 一致;payload = 事件主体去掉 type(contracts agentRunEventToRow)。
|
||||
|
||||
const TYPES = [
|
||||
"run_started",
|
||||
"text_delta",
|
||||
"thinking_delta",
|
||||
"tool_call_start",
|
||||
"tool_call_end",
|
||||
"tool_call_rejected",
|
||||
"compaction",
|
||||
"run_completed",
|
||||
"run_failed",
|
||||
];
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`
|
||||
create table agent_run_event (
|
||||
run_id uuid not null references agent_run (id) on delete cascade,
|
||||
seq bigint not null check (seq >= 0),
|
||||
type text not null ${oneOf("type", TYPES)},
|
||||
at timestamptz not null default now(),
|
||||
payload jsonb not null default '{}'::jsonb,
|
||||
constraint agent_run_event_pkey primary key (run_id, seq)
|
||||
);
|
||||
create index agent_run_event_at_idx on agent_run_event (at);
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(db, dropTables(["agent_run_event"]));
|
||||
}
|
||||
@@ -37,6 +37,16 @@ export type AgentRunStatus =
|
||||
| "failed"
|
||||
| "cancelled"
|
||||
| "budget_exceeded";
|
||||
export type AgentRunEventType =
|
||||
| "run_started"
|
||||
| "text_delta"
|
||||
| "thinking_delta"
|
||||
| "tool_call_start"
|
||||
| "tool_call_end"
|
||||
| "tool_call_rejected"
|
||||
| "compaction"
|
||||
| "run_completed"
|
||||
| "run_failed";
|
||||
export type ApprovalStatus = "pending" | "approved" | "rejected" | "revoked" | "expired" | "consumed";
|
||||
export type ArtifactKind = "evidence" | "plan" | "proposal" | "report" | "snapshot" | "log";
|
||||
export type AssetLocationLocationKind = "library" | "download_cache" | "hardlink" | "external";
|
||||
@@ -175,6 +185,7 @@ export type SubscriptionOrigin = "user" | "agent" | "policy" | "import";
|
||||
export type SubscriptionPriority = "immediate" | "tracking" | "new_work" | "upgrade";
|
||||
export type SubscriptionStatus = "active" | "paused" | "completed" | "cancelled";
|
||||
export type TaskStatus = "pending" | "running" | "waiting" | "succeeded" | "failed" | "cancelled" | "dead";
|
||||
export type TasksOpLedgerStatus = "started" | "succeeded";
|
||||
export type TitleAliasKind = "original" | "translated" | "short" | "site_title" | "romanized" | "other";
|
||||
export type TitleAliasSource = "tmdb" | "user" | "site" | "agent" | "nfo" | "import";
|
||||
|
||||
@@ -250,6 +261,14 @@ export interface AgentRunTable {
|
||||
updated_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface AgentRunEventTable {
|
||||
run_id: string;
|
||||
seq: number;
|
||||
type: AgentRunEventType;
|
||||
at: GeneratedTimestamptz;
|
||||
payload: Generated<Json>;
|
||||
}
|
||||
|
||||
export interface ApiKeyTable {
|
||||
id: Generated<string>;
|
||||
name: string;
|
||||
@@ -951,6 +970,38 @@ export interface TaskTable {
|
||||
updated_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface TasksFactTable {
|
||||
id: Generated<number>;
|
||||
run_id: string;
|
||||
name: string;
|
||||
payload: Json | null;
|
||||
created_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface TasksOpLedgerTable {
|
||||
op_key: string;
|
||||
run_id: string | null;
|
||||
attempt_token: string;
|
||||
attempts: Generated<number>;
|
||||
status: TasksOpLedgerStatus;
|
||||
result: Json | null;
|
||||
last_error: string | null;
|
||||
started_at: GeneratedTimestamptz;
|
||||
completed_at: Timestamptz | null;
|
||||
}
|
||||
|
||||
export interface TasksRunMetaTable {
|
||||
run_id: string;
|
||||
task_name: string;
|
||||
task_version: number;
|
||||
idempotency_key: string;
|
||||
trace_id: string | null;
|
||||
priority: Generated<number>;
|
||||
cancel_requested_at: Timestamptz | null;
|
||||
cancel_reason: string | null;
|
||||
created_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface TitleAliasTable {
|
||||
id: Generated<string>;
|
||||
media_entity_id: string;
|
||||
@@ -969,6 +1020,7 @@ export interface DB {
|
||||
acquisition_plan: AcquisitionPlanTable;
|
||||
acquisition_slot: AcquisitionSlotTable;
|
||||
agent_run: AgentRunTable;
|
||||
agent_run_event: AgentRunEventTable;
|
||||
api_key: ApiKeyTable;
|
||||
approval: ApprovalTable;
|
||||
artifact: ArtifactTable;
|
||||
@@ -1006,6 +1058,9 @@ export interface DB {
|
||||
source_release: SourceReleaseTable;
|
||||
subscription: SubscriptionTable;
|
||||
task: TaskTable;
|
||||
tasks_fact: TasksFactTable;
|
||||
tasks_op_ledger: TasksOpLedgerTable;
|
||||
tasks_run_meta: TasksRunMetaTable;
|
||||
title_alias: TitleAliasTable;
|
||||
}
|
||||
|
||||
@@ -1018,6 +1073,9 @@ export type AcquisitionSlotUpdate = Updateable<AcquisitionSlotTable>;
|
||||
export type AgentRunRow = Selectable<AgentRunTable>;
|
||||
export type NewAgentRun = Insertable<AgentRunTable>;
|
||||
export type AgentRunUpdate = Updateable<AgentRunTable>;
|
||||
export type AgentRunEventRow = Selectable<AgentRunEventTable>;
|
||||
export type NewAgentRunEvent = Insertable<AgentRunEventTable>;
|
||||
export type AgentRunEventUpdate = Updateable<AgentRunEventTable>;
|
||||
export type ApiKeyRow = Selectable<ApiKeyTable>;
|
||||
export type NewApiKey = Insertable<ApiKeyTable>;
|
||||
export type ApiKeyUpdate = Updateable<ApiKeyTable>;
|
||||
@@ -1129,6 +1187,15 @@ export type SubscriptionUpdate = Updateable<SubscriptionTable>;
|
||||
export type TaskRow = Selectable<TaskTable>;
|
||||
export type NewTask = Insertable<TaskTable>;
|
||||
export type TaskUpdate = Updateable<TaskTable>;
|
||||
export type TasksFactRow = Selectable<TasksFactTable>;
|
||||
export type NewTasksFact = Insertable<TasksFactTable>;
|
||||
export type TasksFactUpdate = Updateable<TasksFactTable>;
|
||||
export type TasksOpLedgerRow = Selectable<TasksOpLedgerTable>;
|
||||
export type NewTasksOpLedger = Insertable<TasksOpLedgerTable>;
|
||||
export type TasksOpLedgerUpdate = Updateable<TasksOpLedgerTable>;
|
||||
export type TasksRunMetaRow = Selectable<TasksRunMetaTable>;
|
||||
export type NewTasksRunMeta = Insertable<TasksRunMetaTable>;
|
||||
export type TasksRunMetaUpdate = Updateable<TasksRunMetaTable>;
|
||||
export type TitleAliasRow = Selectable<TitleAliasTable>;
|
||||
export type NewTitleAlias = Insertable<TitleAliasTable>;
|
||||
export type TitleAliasUpdate = Updateable<TitleAliasTable>;
|
||||
@@ -63,8 +63,24 @@ const EXPECTED_TABLES = [
|
||||
"server_mapping",
|
||||
"organization_record",
|
||||
"organization_file_result",
|
||||
// tasks 包自有表(0013)与 agent 事件(0014)
|
||||
"tasks_run_meta",
|
||||
"tasks_fact",
|
||||
"tasks_op_ledger",
|
||||
"agent_run_event",
|
||||
].sort();
|
||||
|
||||
/** 不走 entityTable() 的表:追加型/引擎自有表,没有 uuid id + created_at/updated_at 三件套 */
|
||||
const NON_ENTITY_TABLES = [
|
||||
"settings",
|
||||
"audit_event",
|
||||
"outbox_event",
|
||||
"tasks_run_meta",
|
||||
"tasks_fact",
|
||||
"tasks_op_ledger",
|
||||
"agent_run_event",
|
||||
];
|
||||
|
||||
describe.skipIf(skip)("schema migrations (real PG)", () => {
|
||||
let admin: Kysely<unknown>;
|
||||
let db: Kysely<DB>;
|
||||
@@ -108,7 +124,7 @@ describe.skipIf(skip)("schema migrations (real PG)", () => {
|
||||
select table_name, column_name, column_default from information_schema.columns
|
||||
where table_schema = current_schema() and column_name in ('id', 'created_at', 'updated_at')`.execute(db)
|
||||
).rows;
|
||||
for (const t of EXPECTED_TABLES.filter((x) => !["settings", "audit_event", "outbox_event"].includes(x))) {
|
||||
for (const t of EXPECTED_TABLES.filter((x) => !NON_ENTITY_TABLES.includes(x))) {
|
||||
const cols = rows.filter((r) => r.table_name === t);
|
||||
expect(cols.map((c) => c.column_name).sort(), t).toEqual(["created_at", "id", "updated_at"]);
|
||||
expect(cols.find((c) => c.column_name === "id")?.column_default, t).toBe("gen_random_uuid()");
|
||||
@@ -291,6 +307,23 @@ describe.skipIf(skip)("schema migrations (real PG)", () => {
|
||||
expect(hit.rows.map((r) => r.id)).toContain(ent.id);
|
||||
});
|
||||
|
||||
it("agent_run_event:(run_id, seq) 唯一,删 run 级联删事件", async () => {
|
||||
const run = await db
|
||||
.insertInto("agent_run")
|
||||
.values({ trigger: "user", goal: "g" })
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
const ev = { run_id: run.id, seq: 0, type: "run_started" as const, payload: JSON.stringify({ goal: "g" }) };
|
||||
await db.insertInto("agent_run_event").values(ev).execute();
|
||||
await expect(db.insertInto("agent_run_event").values(ev).execute()).rejects.toThrow(/agent_run_event_pkey/);
|
||||
await expect(
|
||||
sql`insert into agent_run_event (run_id, seq, type) values (${run.id}, 1, 'bogus')`.execute(db),
|
||||
).rejects.toThrow(/check constraint/);
|
||||
await db.deleteFrom("agent_run").where("id", "=", run.id).execute();
|
||||
const left = await db.selectFrom("agent_run_event").select("seq").where("run_id", "=", run.id).execute();
|
||||
expect(left).toEqual([]);
|
||||
});
|
||||
|
||||
it("迁移可完整 down 再 up(往返)", async () => {
|
||||
const migrator = createMigrator(db, { migrationTableSchema: schema });
|
||||
const down = await migrator.migrateTo(NO_MIGRATIONS);
|
||||
|
||||
Reference in new issue
Block a user