👷 build: add agent task system database schema (#13280)

* 🗃️ chore: add agent task system database schema

Add 6 new tables for the Agent Task System:
- tasks: core task with tree structure, heartbeat, scheduling
- task_dependencies: inter-task dependency graph (blocks/relates)
- task_documents: MVP workspace document pinning
- task_topics: topic tracking with handoff (jsonb) and review results
- task_comments: user/agent comments with author tracking (text id: cmt_)
- briefs: unresolved notification system (text id: brf_)

All sub-tables include userId FK for row-level user isolation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🗃️ chore: add self-referential FK on tasks.parentTaskId (ON DELETE SET NULL)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: use foreignKey() for self-referential parentTaskId to avoid TS circular inference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🗃️ chore: add FK on task_topics.topic_id → topics.id (ON DELETE SET NULL)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: resolve pre-existing TS type-check errors

- Fix i18next defaultValue type (string | null → string)
- Fix i18next options type mismatches
- Fix fieldTags.webhook possibly undefined

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🗃️ chore: add FK on tasks.currentTopicId → topics.id (ON DELETE SET NULL)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🗃️ chore: add FK constraints for assignee, author, topic, and parent fields

- tasks.assigneeUserId → users.id (ON DELETE SET NULL)
- tasks.assigneeAgentId → agents.id (ON DELETE SET NULL)
- tasks.parentTaskId → tasks.id (ON DELETE SET NULL)
- tasks.currentTopicId → topics.id (ON DELETE SET NULL)
- task_comments.authorUserId → users.id (ON DELETE SET NULL)
- task_comments.authorAgentId → agents.id (ON DELETE SET NULL)
- task_topics.topicId → topics.id (ON DELETE SET NULL)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🗃️ chore: change task_topics.topicId FK to ON DELETE CASCADE

Topic deleted → task_topic mapping row removed (not just nulled).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: use inline .references() for currentTopicId FK

No circular inference issue — only parentTaskId (self-ref) needs foreignKey().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🗃️ chore: add FK on task_comments.briefId and topicId (ON DELETE SET NULL)

- task_comments.briefId → briefs.id (SET NULL)
- task_comments.topicId → topics.id (SET NULL)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: merge briefs table into task.ts to fix circular dependency

brief.ts imported task.ts (briefs.taskId FK) and task.ts imported
brief.ts (taskComments.briefId FK), causing circular dependency error.
Merged briefs into task.ts since briefs are part of the task system.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🗃️ chore: add FK on tasks.createdByAgentId → agents.id (ON DELETE SET NULL)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arvin Xu
2026-03-26 13:56:01 +08:00
committed by GitHub
parent b53abaa3b2
commit b005a9c73b
11 changed files with 14983 additions and 4 deletions

View File

@@ -1340,6 +1340,166 @@ table sessions {
}
}
table briefs {
id text [pk, not null]
user_id text [not null]
task_id text
cron_job_id text
topic_id text
agent_id text
type text [not null]
priority text [default: 'info']
title text [not null]
summary text [not null]
artifacts jsonb
actions jsonb
resolved_action text
resolved_comment text
read_at "timestamp with time zone"
resolved_at "timestamp with time zone"
created_at "timestamp with time zone" [not null, default: `now()`]
indexes {
user_id [name: 'briefs_user_id_idx']
task_id [name: 'briefs_task_id_idx']
cron_job_id [name: 'briefs_cron_job_id_idx']
agent_id [name: 'briefs_agent_id_idx']
type [name: 'briefs_type_idx']
priority [name: 'briefs_priority_idx']
(user_id, resolved_at) [name: 'briefs_unresolved_idx']
}
}
table task_comments {
id text [pk, not null]
task_id text [not null]
user_id text [not null]
author_user_id text
author_agent_id text
content text [not null]
editor_data jsonb
brief_id text
topic_id text
accessed_at "timestamp with time zone" [not null, default: `now()`]
created_at "timestamp with time zone" [not null, default: `now()`]
updated_at "timestamp with time zone" [not null, default: `now()`]
indexes {
task_id [name: 'task_comments_task_id_idx']
user_id [name: 'task_comments_user_id_idx']
author_user_id [name: 'task_comments_author_user_id_idx']
author_agent_id [name: 'task_comments_agent_id_idx']
brief_id [name: 'task_comments_brief_id_idx']
topic_id [name: 'task_comments_topic_id_idx']
}
}
table task_dependencies {
id uuid [pk, not null, default: `gen_random_uuid()`]
task_id text [not null]
depends_on_id text [not null]
user_id text [not null]
type text [not null, default: 'blocks']
condition jsonb
created_at "timestamp with time zone" [not null, default: `now()`]
indexes {
(task_id, depends_on_id) [name: 'task_deps_unique_idx', unique]
task_id [name: 'task_deps_task_id_idx']
depends_on_id [name: 'task_deps_depends_on_id_idx']
user_id [name: 'task_deps_user_id_idx']
}
}
table task_documents {
id uuid [pk, not null, default: `gen_random_uuid()`]
task_id text [not null]
document_id text [not null]
user_id text [not null]
pinned_by text [not null, default: 'agent']
created_at "timestamp with time zone" [not null, default: `now()`]
indexes {
(task_id, document_id) [name: 'task_docs_unique_idx', unique]
task_id [name: 'task_docs_task_id_idx']
document_id [name: 'task_docs_document_id_idx']
user_id [name: 'task_docs_user_id_idx']
}
}
table task_topics {
id uuid [pk, not null, default: `gen_random_uuid()`]
task_id text [not null]
topic_id text
user_id text [not null]
seq integer [not null]
operation_id text
status text [not null, default: 'running']
handoff jsonb
review_passed integer
review_score integer
review_scores jsonb
review_iteration integer
reviewed_at "timestamp with time zone"
accessed_at "timestamp with time zone" [not null, default: `now()`]
created_at "timestamp with time zone" [not null, default: `now()`]
updated_at "timestamp with time zone" [not null, default: `now()`]
indexes {
(task_id, topic_id) [name: 'task_topics_unique_idx', unique]
task_id [name: 'task_topics_task_id_idx']
topic_id [name: 'task_topics_topic_id_idx']
user_id [name: 'task_topics_user_id_idx']
(task_id, status) [name: 'task_topics_status_idx']
}
}
table tasks {
id text [pk, not null]
identifier text [not null]
seq integer [not null]
created_by_user_id text [not null]
created_by_agent_id text
assignee_user_id text
assignee_agent_id text
parent_task_id text
name text
description varchar(255)
instruction text [not null]
status text [not null, default: 'backlog']
priority integer [default: 0]
sort_order integer [default: 0]
heartbeat_interval integer [default: 300]
heartbeat_timeout integer
last_heartbeat_at "timestamp with time zone"
schedule_pattern text
schedule_timezone text [default: 'UTC']
total_topics integer [default: 0]
max_topics integer
current_topic_id text
context jsonb [default: `{}`]
config jsonb [default: `{}`]
error text
started_at "timestamp with time zone"
completed_at "timestamp with time zone"
accessed_at "timestamp with time zone" [not null, default: `now()`]
created_at "timestamp with time zone" [not null, default: `now()`]
updated_at "timestamp with time zone" [not null, default: `now()`]
indexes {
(identifier, created_by_user_id) [name: 'tasks_identifier_idx', unique]
created_by_user_id [name: 'tasks_created_by_user_id_idx']
created_by_agent_id [name: 'tasks_created_by_agent_id_idx']
assignee_user_id [name: 'tasks_assignee_user_id_idx']
assignee_agent_id [name: 'tasks_assignee_agent_id_idx']
parent_task_id [name: 'tasks_parent_task_id_idx']
status [name: 'tasks_status_idx']
priority [name: 'tasks_priority_idx']
(status, last_heartbeat_at) [name: 'tasks_heartbeat_idx']
}
}
table threads {
id text [pk, not null]
title text

View File

@@ -0,0 +1,189 @@
CREATE TABLE IF NOT EXISTS "briefs" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"task_id" text,
"cron_job_id" text,
"topic_id" text,
"agent_id" text,
"type" text NOT NULL,
"priority" text DEFAULT 'info',
"title" text NOT NULL,
"summary" text NOT NULL,
"artifacts" jsonb,
"actions" jsonb,
"resolved_action" text,
"resolved_comment" text,
"read_at" timestamp with time zone,
"resolved_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "task_comments" (
"id" text PRIMARY KEY NOT NULL,
"task_id" text NOT NULL,
"user_id" text NOT NULL,
"author_user_id" text,
"author_agent_id" text,
"content" text NOT NULL,
"editor_data" jsonb,
"brief_id" text,
"topic_id" text,
"accessed_at" timestamp with time zone DEFAULT now() NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "task_dependencies" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"task_id" text NOT NULL,
"depends_on_id" text NOT NULL,
"user_id" text NOT NULL,
"type" text DEFAULT 'blocks' NOT NULL,
"condition" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "task_documents" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"task_id" text NOT NULL,
"document_id" text NOT NULL,
"user_id" text NOT NULL,
"pinned_by" text DEFAULT 'agent' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "task_topics" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"task_id" text NOT NULL,
"topic_id" text,
"user_id" text NOT NULL,
"seq" integer NOT NULL,
"operation_id" text,
"status" text DEFAULT 'running' NOT NULL,
"handoff" jsonb,
"review_passed" integer,
"review_score" integer,
"review_scores" jsonb,
"review_iteration" integer,
"reviewed_at" timestamp with time zone,
"accessed_at" timestamp with time zone DEFAULT now() NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "tasks" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"seq" integer NOT NULL,
"created_by_user_id" text NOT NULL,
"created_by_agent_id" text,
"assignee_user_id" text,
"assignee_agent_id" text,
"parent_task_id" text,
"name" text,
"description" varchar(255),
"instruction" text NOT NULL,
"status" text DEFAULT 'backlog' NOT NULL,
"priority" integer DEFAULT 0,
"sort_order" integer DEFAULT 0,
"heartbeat_interval" integer DEFAULT 300,
"heartbeat_timeout" integer,
"last_heartbeat_at" timestamp with time zone,
"schedule_pattern" text,
"schedule_timezone" text DEFAULT 'UTC',
"total_topics" integer DEFAULT 0,
"max_topics" integer,
"current_topic_id" text,
"context" jsonb DEFAULT '{}'::jsonb,
"config" jsonb DEFAULT '{}'::jsonb,
"error" text,
"started_at" timestamp with time zone,
"completed_at" timestamp with time zone,
"accessed_at" timestamp with time zone DEFAULT now() NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "briefs" DROP CONSTRAINT IF EXISTS "briefs_user_id_users_id_fk";--> statement-breakpoint
ALTER TABLE "briefs" ADD CONSTRAINT "briefs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "briefs" DROP CONSTRAINT IF EXISTS "briefs_task_id_tasks_id_fk";--> statement-breakpoint
ALTER TABLE "briefs" ADD CONSTRAINT "briefs_task_id_tasks_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."tasks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "briefs" DROP CONSTRAINT IF EXISTS "briefs_cron_job_id_agent_cron_jobs_id_fk";--> statement-breakpoint
ALTER TABLE "briefs" ADD CONSTRAINT "briefs_cron_job_id_agent_cron_jobs_id_fk" FOREIGN KEY ("cron_job_id") REFERENCES "public"."agent_cron_jobs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_comments" DROP CONSTRAINT IF EXISTS "task_comments_task_id_tasks_id_fk";--> statement-breakpoint
ALTER TABLE "task_comments" ADD CONSTRAINT "task_comments_task_id_tasks_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."tasks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_comments" DROP CONSTRAINT IF EXISTS "task_comments_user_id_users_id_fk";--> statement-breakpoint
ALTER TABLE "task_comments" ADD CONSTRAINT "task_comments_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_comments" DROP CONSTRAINT IF EXISTS "task_comments_author_user_id_users_id_fk";--> statement-breakpoint
ALTER TABLE "task_comments" ADD CONSTRAINT "task_comments_author_user_id_users_id_fk" FOREIGN KEY ("author_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_comments" DROP CONSTRAINT IF EXISTS "task_comments_author_agent_id_agents_id_fk";--> statement-breakpoint
ALTER TABLE "task_comments" ADD CONSTRAINT "task_comments_author_agent_id_agents_id_fk" FOREIGN KEY ("author_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_comments" DROP CONSTRAINT IF EXISTS "task_comments_brief_id_briefs_id_fk";--> statement-breakpoint
ALTER TABLE "task_comments" ADD CONSTRAINT "task_comments_brief_id_briefs_id_fk" FOREIGN KEY ("brief_id") REFERENCES "public"."briefs"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_comments" DROP CONSTRAINT IF EXISTS "task_comments_topic_id_topics_id_fk";--> statement-breakpoint
ALTER TABLE "task_comments" ADD CONSTRAINT "task_comments_topic_id_topics_id_fk" FOREIGN KEY ("topic_id") REFERENCES "public"."topics"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_dependencies" DROP CONSTRAINT IF EXISTS "task_dependencies_task_id_tasks_id_fk";--> statement-breakpoint
ALTER TABLE "task_dependencies" ADD CONSTRAINT "task_dependencies_task_id_tasks_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."tasks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_dependencies" DROP CONSTRAINT IF EXISTS "task_dependencies_depends_on_id_tasks_id_fk";--> statement-breakpoint
ALTER TABLE "task_dependencies" ADD CONSTRAINT "task_dependencies_depends_on_id_tasks_id_fk" FOREIGN KEY ("depends_on_id") REFERENCES "public"."tasks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_dependencies" DROP CONSTRAINT IF EXISTS "task_dependencies_user_id_users_id_fk";--> statement-breakpoint
ALTER TABLE "task_dependencies" ADD CONSTRAINT "task_dependencies_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_documents" DROP CONSTRAINT IF EXISTS "task_documents_task_id_tasks_id_fk";--> statement-breakpoint
ALTER TABLE "task_documents" ADD CONSTRAINT "task_documents_task_id_tasks_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."tasks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_documents" DROP CONSTRAINT IF EXISTS "task_documents_document_id_documents_id_fk";--> statement-breakpoint
ALTER TABLE "task_documents" ADD CONSTRAINT "task_documents_document_id_documents_id_fk" FOREIGN KEY ("document_id") REFERENCES "public"."documents"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_documents" DROP CONSTRAINT IF EXISTS "task_documents_user_id_users_id_fk";--> statement-breakpoint
ALTER TABLE "task_documents" ADD CONSTRAINT "task_documents_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_topics" DROP CONSTRAINT IF EXISTS "task_topics_task_id_tasks_id_fk";--> statement-breakpoint
ALTER TABLE "task_topics" ADD CONSTRAINT "task_topics_task_id_tasks_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."tasks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_topics" DROP CONSTRAINT IF EXISTS "task_topics_topic_id_topics_id_fk";--> statement-breakpoint
ALTER TABLE "task_topics" ADD CONSTRAINT "task_topics_topic_id_topics_id_fk" FOREIGN KEY ("topic_id") REFERENCES "public"."topics"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_topics" DROP CONSTRAINT IF EXISTS "task_topics_user_id_users_id_fk";--> statement-breakpoint
ALTER TABLE "task_topics" ADD CONSTRAINT "task_topics_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "tasks" DROP CONSTRAINT IF EXISTS "tasks_created_by_user_id_users_id_fk";--> statement-breakpoint
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_created_by_user_id_users_id_fk" FOREIGN KEY ("created_by_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "tasks" DROP CONSTRAINT IF EXISTS "tasks_created_by_agent_id_agents_id_fk";--> statement-breakpoint
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_created_by_agent_id_agents_id_fk" FOREIGN KEY ("created_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "tasks" DROP CONSTRAINT IF EXISTS "tasks_assignee_user_id_users_id_fk";--> statement-breakpoint
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_assignee_user_id_users_id_fk" FOREIGN KEY ("assignee_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "tasks" DROP CONSTRAINT IF EXISTS "tasks_assignee_agent_id_agents_id_fk";--> statement-breakpoint
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_assignee_agent_id_agents_id_fk" FOREIGN KEY ("assignee_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "tasks" DROP CONSTRAINT IF EXISTS "tasks_current_topic_id_topics_id_fk";--> statement-breakpoint
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_current_topic_id_topics_id_fk" FOREIGN KEY ("current_topic_id") REFERENCES "public"."topics"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "tasks" DROP CONSTRAINT IF EXISTS "tasks_parent_task_id_tasks_id_fk";--> statement-breakpoint
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_parent_task_id_tasks_id_fk" FOREIGN KEY ("parent_task_id") REFERENCES "public"."tasks"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "briefs_user_id_idx" ON "briefs" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "briefs_task_id_idx" ON "briefs" USING btree ("task_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "briefs_cron_job_id_idx" ON "briefs" USING btree ("cron_job_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "briefs_agent_id_idx" ON "briefs" USING btree ("agent_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "briefs_type_idx" ON "briefs" USING btree ("type");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "briefs_priority_idx" ON "briefs" USING btree ("priority");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "briefs_unresolved_idx" ON "briefs" USING btree ("user_id","resolved_at");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_comments_task_id_idx" ON "task_comments" USING btree ("task_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_comments_user_id_idx" ON "task_comments" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_comments_author_user_id_idx" ON "task_comments" USING btree ("author_user_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_comments_agent_id_idx" ON "task_comments" USING btree ("author_agent_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_comments_brief_id_idx" ON "task_comments" USING btree ("brief_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_comments_topic_id_idx" ON "task_comments" USING btree ("topic_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "task_deps_unique_idx" ON "task_dependencies" USING btree ("task_id","depends_on_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_deps_task_id_idx" ON "task_dependencies" USING btree ("task_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_deps_depends_on_id_idx" ON "task_dependencies" USING btree ("depends_on_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_deps_user_id_idx" ON "task_dependencies" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "task_docs_unique_idx" ON "task_documents" USING btree ("task_id","document_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_docs_task_id_idx" ON "task_documents" USING btree ("task_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_docs_document_id_idx" ON "task_documents" USING btree ("document_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_docs_user_id_idx" ON "task_documents" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "task_topics_unique_idx" ON "task_topics" USING btree ("task_id","topic_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_topics_task_id_idx" ON "task_topics" USING btree ("task_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_topics_topic_id_idx" ON "task_topics" USING btree ("topic_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_topics_user_id_idx" ON "task_topics" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "task_topics_status_idx" ON "task_topics" USING btree ("task_id","status");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "tasks_identifier_idx" ON "tasks" USING btree ("identifier","created_by_user_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "tasks_created_by_user_id_idx" ON "tasks" USING btree ("created_by_user_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "tasks_created_by_agent_id_idx" ON "tasks" USING btree ("created_by_agent_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "tasks_assignee_user_id_idx" ON "tasks" USING btree ("assignee_user_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "tasks_assignee_agent_id_idx" ON "tasks" USING btree ("assignee_agent_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "tasks_parent_task_id_idx" ON "tasks" USING btree ("parent_task_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "tasks_status_idx" ON "tasks" USING btree ("status");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "tasks_priority_idx" ON "tasks" USING btree ("priority");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "tasks_heartbeat_idx" ON "tasks" USING btree ("status","last_heartbeat_at");

File diff suppressed because it is too large Load Diff

View File

@@ -665,6 +665,13 @@
"when": 1773764776073,
"tag": "0094_agent_bot_providers_add_settings",
"breakpoints": true
},
{
"idx": 95,
"version": "7",
"when": 1774502940061,
"tag": "0095_add_agent_task_system",
"breakpoints": true
}
],
"version": "6"

View File

@@ -19,6 +19,7 @@ export * from './ragEvals';
export * from './rbac';
export * from './relations';
export * from './session';
export * from './task';
export * from './topic';
export * from './user';
export * from './userMemories';

View File

@@ -0,0 +1,309 @@
import {
foreignKey,
index,
integer,
jsonb,
pgTable,
text,
uniqueIndex,
uuid,
} from 'drizzle-orm/pg-core';
import { idGenerator } from '../utils/idGenerator';
import { createdAt, timestamps, timestamptz, varchar255 } from './_helpers';
import { agents } from './agent';
import { agentCronJobs } from './agentCronJob';
import { documents } from './file';
import { topics } from './topic';
import { users } from './user';
// ── Tasks ────────────────────────────────────────────────
export const tasks = pgTable(
'tasks',
{
id: text('id')
.primaryKey()
.$defaultFn(() => idGenerator('tasks'))
.notNull(),
// Workspace-level identifier (e.g. 'T-1', 'PROJ-42')
identifier: text('identifier').notNull(),
seq: integer('seq').notNull(),
// Creator (user or agent)
createdByUserId: text('created_by_user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
createdByAgentId: text('created_by_agent_id').references(() => agents.id, {
onDelete: 'set null',
}),
// Assignee (user and agent can coexist, both nullable)
assigneeUserId: text('assignee_user_id').references(() => users.id, { onDelete: 'set null' }),
assigneeAgentId: text('assignee_agent_id').references(() => agents.id, {
onDelete: 'set null',
}),
// Tree structure (self-referencing, no depth limit)
parentTaskId: text('parent_task_id'),
// Task definition
name: text('name'),
description: varchar255('description'),
instruction: text('instruction').notNull(),
// Lifecycle (same state machine for user and agent)
// 'backlog' | 'running' | 'paused' | 'completed' | 'failed' | 'canceled'
status: text('status').notNull().default('backlog'),
priority: integer('priority').default(0), // 'no' | 'urgent' | 'high' | 'normal' | 'low'
sortOrder: integer('sort_order').default(0), // manual sort within parent, lower = higher
// Heartbeat
heartbeatInterval: integer('heartbeat_interval').default(300), // seconds
heartbeatTimeout: integer('heartbeat_timeout'), // seconds, null = disabled (default off)
lastHeartbeatAt: timestamptz('last_heartbeat_at'),
// Schedule (optional)
schedulePattern: text('schedule_pattern'),
scheduleTimezone: text('schedule_timezone').default('UTC'),
// Topic management
totalTopics: integer('total_topics').default(0),
maxTopics: integer('max_topics'), // null = unlimited
currentTopicId: text('current_topic_id').references(() => topics.id, { onDelete: 'set null' }),
// Context & config (each task independent, no inheritance from parent)
context: jsonb('context').default({}),
config: jsonb('config').default({}), // CheckpointConfig, ReviewConfig, etc.
error: text('error'),
// Timestamps
startedAt: timestamptz('started_at'),
completedAt: timestamptz('completed_at'),
...timestamps,
},
(t) => [
// Self-referential FK (defined here to avoid TS circular inference)
foreignKey({
columns: [t.parentTaskId],
foreignColumns: [t.id],
name: 'tasks_parent_task_id_tasks_id_fk',
}).onDelete('set null'),
uniqueIndex('tasks_identifier_idx').on(t.identifier, t.createdByUserId),
index('tasks_created_by_user_id_idx').on(t.createdByUserId),
index('tasks_created_by_agent_id_idx').on(t.createdByAgentId),
index('tasks_assignee_user_id_idx').on(t.assigneeUserId),
index('tasks_assignee_agent_id_idx').on(t.assigneeAgentId),
index('tasks_parent_task_id_idx').on(t.parentTaskId),
index('tasks_status_idx').on(t.status),
index('tasks_priority_idx').on(t.priority),
index('tasks_heartbeat_idx').on(t.status, t.lastHeartbeatAt),
],
);
export type NewTask = typeof tasks.$inferInsert;
export type TaskItem = typeof tasks.$inferSelect;
// ── Task Dependencies ────────────────────────────────────
export const taskDependencies = pgTable(
'task_dependencies',
{
id: uuid('id').defaultRandom().primaryKey().notNull(),
taskId: text('task_id')
.references(() => tasks.id, { onDelete: 'cascade' })
.notNull(),
dependsOnId: text('depends_on_id')
.references(() => tasks.id, { onDelete: 'cascade' })
.notNull(),
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
// 'blocks' | 'relates'
type: text('type').notNull().default('blocks'),
// Reserved for conditional dependencies: {"on": "success"} / {"on": "failure"}
condition: jsonb('condition'),
createdAt: createdAt(),
},
(t) => [
uniqueIndex('task_deps_unique_idx').on(t.taskId, t.dependsOnId),
index('task_deps_task_id_idx').on(t.taskId),
index('task_deps_depends_on_id_idx').on(t.dependsOnId),
index('task_deps_user_id_idx').on(t.userId),
],
);
export type NewTaskDependency = typeof taskDependencies.$inferInsert;
export type TaskDependencyItem = typeof taskDependencies.$inferSelect;
// ── Task Documents (MVP Workspace) ───────────────────────
export const taskDocuments = pgTable(
'task_documents',
{
id: uuid('id').defaultRandom().primaryKey().notNull(),
taskId: text('task_id')
.references(() => tasks.id, { onDelete: 'cascade' })
.notNull(),
documentId: text('document_id')
.references(() => documents.id, { onDelete: 'cascade' })
.notNull(),
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
// 'agent' | 'user' | 'system'
pinnedBy: text('pinned_by').notNull().default('agent'),
createdAt: createdAt(),
},
(t) => [
uniqueIndex('task_docs_unique_idx').on(t.taskId, t.documentId),
index('task_docs_task_id_idx').on(t.taskId),
index('task_docs_document_id_idx').on(t.documentId),
index('task_docs_user_id_idx').on(t.userId),
],
);
export type NewTaskDocument = typeof taskDocuments.$inferInsert;
export type TaskDocumentItem = typeof taskDocuments.$inferSelect;
// ── Task Topics ─────────────────────────────────────────
export const taskTopics = pgTable(
'task_topics',
{
id: uuid('id').defaultRandom().primaryKey().notNull(),
taskId: text('task_id')
.references(() => tasks.id, { onDelete: 'cascade' })
.notNull(),
topicId: text('topic_id').references(() => topics.id, { onDelete: 'cascade' }),
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
seq: integer('seq').notNull(), // topic sequence within task (1, 2, 3...)
operationId: text('operation_id'), // agent execution operation ID
// 'running' | 'completed' | 'failed' | 'timeout' | 'canceled'
status: text('status').notNull().default('running'),
// Handoff (populated after topic completes via LLM summarization)
// { title, summary, keyFindings: string[], nextAction }
handoff: jsonb('handoff'),
// Review results (populated after topic completes + review runs)
reviewPassed: integer('review_passed'), // 1 = passed, 0 = failed, null = not reviewed
reviewScore: integer('review_score'), // overall score 0-100
reviewScores: jsonb('review_scores'), // [{rubricId, score, passed, reason}]
reviewIteration: integer('review_iteration'), // which iteration (1, 2, 3...)
reviewedAt: timestamptz('reviewed_at'),
...timestamps,
},
(t) => [
uniqueIndex('task_topics_unique_idx').on(t.taskId, t.topicId),
index('task_topics_task_id_idx').on(t.taskId),
index('task_topics_topic_id_idx').on(t.topicId),
index('task_topics_user_id_idx').on(t.userId),
index('task_topics_status_idx').on(t.taskId, t.status),
],
);
export type NewTaskTopic = typeof taskTopics.$inferInsert;
export type TaskTopicItem = typeof taskTopics.$inferSelect;
// ── Briefs ─────────────────────────────────────────────
export const briefs = pgTable(
'briefs',
{
id: text('id')
.primaryKey()
.$defaultFn(() => idGenerator('briefs'))
.notNull(),
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
// Source (polymorphic, fill as needed)
taskId: text('task_id').references(() => tasks.id, { onDelete: 'cascade' }),
cronJobId: text('cron_job_id').references(() => agentCronJobs.id, { onDelete: 'cascade' }),
topicId: text('topic_id'),
agentId: text('agent_id'),
// Content
type: text('type').notNull(), // 'decision' | 'result' | 'insight' | 'error'
priority: text('priority').default('info'), // 'urgent' | 'normal' | 'info'
title: text('title').notNull(),
summary: text('summary').notNull(),
artifacts: jsonb('artifacts'), // document ids
actions: jsonb('actions'), // BriefAction[]
// Resolution
resolvedAction: text('resolved_action'),
resolvedComment: text('resolved_comment'),
readAt: timestamptz('read_at'),
resolvedAt: timestamptz('resolved_at'),
createdAt: createdAt(),
},
(t) => [
index('briefs_user_id_idx').on(t.userId),
index('briefs_task_id_idx').on(t.taskId),
index('briefs_cron_job_id_idx').on(t.cronJobId),
index('briefs_agent_id_idx').on(t.agentId),
index('briefs_type_idx').on(t.type),
index('briefs_priority_idx').on(t.priority),
index('briefs_unresolved_idx').on(t.userId, t.resolvedAt),
],
);
export type NewBrief = typeof briefs.$inferInsert;
export type BriefItem = typeof briefs.$inferSelect;
// ── Task Comments ───────────────────────────────────────
export const taskComments = pgTable(
'task_comments',
{
id: text('id')
.primaryKey()
.$defaultFn(() => idGenerator('taskComments'))
.notNull(),
taskId: text('task_id')
.references(() => tasks.id, { onDelete: 'cascade' })
.notNull(),
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
// Author (user or agent, both nullable)
authorUserId: text('author_user_id').references(() => users.id, { onDelete: 'set null' }),
authorAgentId: text('author_agent_id').references(() => agents.id, { onDelete: 'set null' }),
// Content
content: text('content').notNull(),
editorData: jsonb('editor_data'),
// Optional references
briefId: text('brief_id').references(() => briefs.id, { onDelete: 'set null' }),
topicId: text('topic_id').references(() => topics.id, { onDelete: 'set null' }),
...timestamps,
},
(t) => [
index('task_comments_task_id_idx').on(t.taskId),
index('task_comments_user_id_idx').on(t.userId),
index('task_comments_author_user_id_idx').on(t.authorUserId),
index('task_comments_agent_id_idx').on(t.authorAgentId),
index('task_comments_brief_id_idx').on(t.briefId),
index('task_comments_topic_id_idx').on(t.topicId),
],
);
export type NewTaskComment = typeof taskComments.$inferInsert;
export type TaskCommentItem = typeof taskComments.$inferSelect;

View File

@@ -8,6 +8,9 @@ export const createNanoId = (size = 8) =>
const prefixes = {
agentCronJobs: 'cron',
agentSkills: 'skl',
briefs: 'brf',
taskComments: 'cmt',
tasks: 'task',
agents: 'agt',
budget: 'bgt',
chatGroups: 'cg',

View File

@@ -26,7 +26,7 @@ const FailedPage = () => {
<Flexbox gap={8}>
<Text fontSize={16} type="secondary">
{t('error.desc', {
reason: t(`error.reason.${reason}` as any, { defaultValue: reason }),
reason: t(`error.reason.${reason}` as any, { defaultValue: reason ?? '' }),
})}
</Text>
{!!errorMessage && <Highlighter language={'log'}>{errorMessage}</Highlighter>}

View File

@@ -64,7 +64,8 @@ export const usePluginContext = (): PluginContext => {
return findTopicAcrossAllSessions(state.topicDataMap, topicId);
},
t: (key: string, options?: Record<string, unknown>) => t(key as any, options) as string,
t: (key: string, options?: Record<string, unknown>) =>
t(key as any, options as any) as string,
}),
[agentMap, topicDataMap, sessionGroups, documents, t],
);

View File

@@ -69,7 +69,10 @@ export const useCronJobDropdownMenu = (
modal.confirm({
cancelText: t('cancel', { ns: 'common' }),
centered: true,
content: t('agentCronJobs.confirmClearTopics' as any, { count: topics.length }),
content: t(
'agentCronJobs.confirmClearTopics' as any,
{ count: topics.length } as any,
),
okButtonProps: { danger: true },
okText: t('ok', { ns: 'common' }),
onOk: handleClearTopics,

View File

@@ -217,7 +217,7 @@ const Body = memo<BodyProps>(
components={{ bold: <strong /> }}
i18nKey="channel.endpointUrlHint"
ns="agent"
values={{ fieldName: provider.fieldTags.webhook, name: provider.name }}
values={{ fieldName: provider.fieldTags.webhook ?? '', name: provider.name }}
/>
}
/>