The product needs one main agent that leads a main conversation, knows the
user across spaces, and hands work to visible specialists. Today none of
that exists in the data model: "Chief" is a client-side naming convention,
the "Hauptthread" is a group the web client creates by name, and nothing
stores what the agents know about the user.
This adds the shared foundation the Phase 1 features build on:
- Bot.kind ("main" | "specialist", one main bot per user) plus capability
fields (tags, builtin tool allowlist, model tier).
- ChatGroup.kind ("group" | "main", one main conversation per user) and
ChatGroup.leadBotId.
- UserProfile and UserFact with category, confidence, source, consent
status and per-space visibility; embeddings are stored as float arrays
because none of the Postgres images ship pgvector.
- DelegatedTask for work the main bot hands to a specialist.
- SpaceModelTier to map a model tier to a connected model per space.
The migration backfills each user's onboarding bot in the home space as the
main bot and turns an existing "Hauptthread" group containing it into the
main conversation. CHECK and foreign key constraints on existing tables are
added NOT VALID and validated in a separate migration, as elsewhere in this
repo.
New db helpers ensure the main bot and the main conversation (idempotent and
race-safe, repairing a lost lead), add same-space specialists to the main
conversation and announce it with a group.updated event, and load the user
context a run may see. The main conversation is exempt from the two-member
floor and cannot have its members replaced through a group update. The
contracts expose the new fields and the user model and delegated task
schemas.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
194 lines
8.4 KiB
SQL
194 lines
8.4 KiB
SQL
-- AlterTable
|
|
ALTER TABLE "bots" ADD COLUMN "allowedBuiltinTools" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
|
ADD COLUMN "kind" TEXT NOT NULL DEFAULT 'specialist',
|
|
ADD COLUMN "modelTier" TEXT,
|
|
ADD COLUMN "restrictBuiltinTools" BOOLEAN NOT NULL DEFAULT false,
|
|
ADD COLUMN "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
|
|
|
-- AlterTable
|
|
ALTER TABLE "chat_groups" ADD COLUMN "kind" TEXT NOT NULL DEFAULT 'group',
|
|
ADD COLUMN "leadBotId" TEXT;
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "space_model_tiers" (
|
|
"id" TEXT NOT NULL,
|
|
"spaceId" TEXT NOT NULL,
|
|
"userId" TEXT NOT NULL,
|
|
"tier" TEXT NOT NULL,
|
|
"credentialId" TEXT NOT NULL,
|
|
"modelId" TEXT NOT NULL,
|
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "space_model_tiers_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "user_profiles" (
|
|
"userId" TEXT NOT NULL,
|
|
"timeZone" TEXT,
|
|
"locale" TEXT,
|
|
"language" TEXT,
|
|
"communicationStyle" TEXT NOT NULL DEFAULT '',
|
|
"summary" TEXT NOT NULL DEFAULT '',
|
|
"consolidatedAt" TIMESTAMP(3),
|
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "user_profiles_pkey" PRIMARY KEY ("userId")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "user_facts" (
|
|
"id" TEXT NOT NULL,
|
|
"userId" TEXT NOT NULL,
|
|
"category" TEXT NOT NULL,
|
|
"value" TEXT NOT NULL,
|
|
"confidence" DOUBLE PRECISION NOT NULL DEFAULT 1,
|
|
"source" TEXT NOT NULL,
|
|
"status" TEXT NOT NULL DEFAULT 'active',
|
|
"sensitive" BOOLEAN NOT NULL DEFAULT false,
|
|
"allSpaces" BOOLEAN NOT NULL DEFAULT true,
|
|
"sourceBotId" TEXT,
|
|
"sourceThreadId" TEXT,
|
|
"sourceMessageId" TEXT,
|
|
"embedding" DOUBLE PRECISION[] DEFAULT ARRAY[]::DOUBLE PRECISION[],
|
|
"embeddingModel" TEXT,
|
|
"confirmedAt" TIMESTAMP(3),
|
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "user_facts_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "user_fact_spaces" (
|
|
"factId" TEXT NOT NULL,
|
|
"spaceId" TEXT NOT NULL,
|
|
|
|
CONSTRAINT "user_fact_spaces_pkey" PRIMARY KEY ("factId","spaceId")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "delegated_tasks" (
|
|
"id" TEXT NOT NULL,
|
|
"userId" TEXT NOT NULL,
|
|
"spaceId" TEXT NOT NULL,
|
|
"threadId" TEXT NOT NULL,
|
|
"requesterBotId" TEXT NOT NULL,
|
|
"assigneeBotId" TEXT NOT NULL,
|
|
"assigneeSpaceId" TEXT NOT NULL,
|
|
"title" TEXT NOT NULL,
|
|
"brief" TEXT NOT NULL,
|
|
"status" TEXT NOT NULL DEFAULT 'queued',
|
|
"deadlineAt" TIMESTAMP(3),
|
|
"result" TEXT,
|
|
"artifacts" JSONB NOT NULL DEFAULT '[]',
|
|
"runId" TEXT,
|
|
"messageId" TEXT,
|
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
"completedAt" TIMESTAMP(3),
|
|
|
|
CONSTRAINT "delegated_tasks_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "space_model_tiers_credentialId_idx" ON "space_model_tiers"("credentialId");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "space_model_tiers_spaceId_userId_tier_key" ON "space_model_tiers"("spaceId", "userId", "tier");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "user_facts_userId_status_category_updatedAt_idx" ON "user_facts"("userId", "status", "category", "updatedAt");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "user_fact_spaces_spaceId_idx" ON "user_fact_spaces"("spaceId");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "delegated_tasks_runId_key" ON "delegated_tasks"("runId");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "delegated_tasks_threadId_status_createdAt_idx" ON "delegated_tasks"("threadId", "status", "createdAt");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "delegated_tasks_assigneeBotId_status_idx" ON "delegated_tasks"("assigneeBotId", "status");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "delegated_tasks_userId_status_updatedAt_idx" ON "delegated_tasks"("userId", "status", "updatedAt");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "chat_groups_leadBotId_idx" ON "chat_groups"("leadBotId");
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "space_model_tiers" ADD CONSTRAINT "space_model_tiers_spaceId_userId_fkey" FOREIGN KEY ("spaceId", "userId") REFERENCES "space_members"("spaceId", "userId") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "space_model_tiers" ADD CONSTRAINT "space_model_tiers_credentialId_userId_fkey" FOREIGN KEY ("credentialId", "userId") REFERENCES "user_model_credentials"("id", "userId") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "chat_groups" ADD CONSTRAINT "chat_groups_leadBotId_fkey" FOREIGN KEY ("leadBotId") REFERENCES "bots"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "user_profiles" ADD CONSTRAINT "user_profiles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "user_facts" ADD CONSTRAINT "user_facts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "user_fact_spaces" ADD CONSTRAINT "user_fact_spaces_factId_fkey" FOREIGN KEY ("factId") REFERENCES "user_facts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "user_fact_spaces" ADD CONSTRAINT "user_fact_spaces_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "delegated_tasks" ADD CONSTRAINT "delegated_tasks_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "delegated_tasks" ADD CONSTRAINT "delegated_tasks_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "threads"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- CHECK constraints on existing tables are added NOT VALID and validated in the next migration.
|
|
ALTER TABLE "bots" ADD CONSTRAINT "bots_kind_chk" CHECK ("kind" IN ('main', 'specialist')) NOT VALID;
|
|
ALTER TABLE "bots" ADD CONSTRAINT "bots_modelTier_chk" CHECK ("modelTier" IS NULL OR "modelTier" IN ('fast', 'standard', 'code')) NOT VALID;
|
|
ALTER TABLE "chat_groups" ADD CONSTRAINT "chat_groups_kind_chk" CHECK ("kind" IN ('group', 'main')) NOT VALID;
|
|
|
|
-- New tables are empty, so their constraints apply immediately.
|
|
ALTER TABLE "space_model_tiers" ADD CONSTRAINT "space_model_tiers_tier_chk" CHECK ("tier" IN ('fast', 'standard', 'code'));
|
|
ALTER TABLE "user_facts" ADD CONSTRAINT "user_facts_category_chk" CHECK ("category" IN ('identity', 'work', 'goals', 'people', 'health', 'values', 'habits', 'preferences', 'tools'));
|
|
ALTER TABLE "user_facts" ADD CONSTRAINT "user_facts_source_chk" CHECK ("source" IN ('onboarding', 'user', 'conversation', 'consolidation', 'connector'));
|
|
ALTER TABLE "user_facts" ADD CONSTRAINT "user_facts_status_chk" CHECK ("status" IN ('active', 'pending'));
|
|
ALTER TABLE "user_facts" ADD CONSTRAINT "user_facts_confidence_chk" CHECK ("confidence" >= 0 AND "confidence" <= 1);
|
|
ALTER TABLE "delegated_tasks" ADD CONSTRAINT "delegated_tasks_status_chk" CHECK ("status" IN ('queued', 'running', 'waiting', 'done', 'failed', 'cancelled'));
|
|
|
|
-- Promote one existing first bot per user to the main bot, preferring the user's home space
|
|
-- (the same order requireMembership uses to pick the default space).
|
|
UPDATE "bots" AS b
|
|
SET "kind" = 'main'
|
|
FROM (
|
|
SELECT DISTINCT ON (fb."userId") fb."id"
|
|
FROM "bots" AS fb
|
|
JOIN "spaces" AS s ON s."id" = fb."spaceId"
|
|
LEFT JOIN "space_members" AS sm ON sm."spaceId" = fb."spaceId" AND sm."userId" = fb."userId"
|
|
WHERE fb."spawnKey" = 'onboarding:first' AND fb."archivedAt" IS NULL
|
|
ORDER BY fb."userId", s."isDefault" DESC, sm."createdAt" ASC NULLS LAST, fb."createdAt" ASC, fb."id" ASC
|
|
) AS first_bot
|
|
WHERE b."id" = first_bot."id";
|
|
|
|
-- Turn an existing client-created "Hauptthread" group that includes the main bot into the
|
|
-- main conversation, so its history stays where the user expects it.
|
|
UPDATE "chat_groups" AS g
|
|
SET "kind" = 'main', "leadBotId" = picked."botId"
|
|
FROM (
|
|
SELECT DISTINCT ON (cg."userId") cg."id" AS "groupId", mb."id" AS "botId"
|
|
FROM "chat_groups" AS cg
|
|
JOIN "chat_group_members" AS cm ON cm."groupId" = cg."id"
|
|
JOIN "bots" AS mb ON mb."id" = cm."botId" AND mb."kind" = 'main' AND mb."spaceId" = cg."spaceId"
|
|
WHERE cg."archivedAt" IS NULL AND cg."name" IN ('Hauptthread', 'Team Chat')
|
|
ORDER BY cg."userId", cg."createdAt" ASC, cg."id" ASC
|
|
) AS picked
|
|
WHERE g."id" = picked."groupId";
|
|
|
|
-- One main bot and one main conversation per user.
|
|
CREATE UNIQUE INDEX "bots_userId_main_key" ON "bots"("userId") WHERE "kind" = 'main';
|
|
CREATE UNIQUE INDEX "chat_groups_userId_main_key" ON "chat_groups"("userId") WHERE "kind" = 'main';
|