diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f45de2ddc..8b93af865 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -327,17 +327,28 @@ jobs: # dbmate keys migrations by their version filename. Once a migration # has shipped to main, editing it silently no-ops in any environment # whose schema_migrations row already lists that version (i.e. prod). - # The fresh-Postgres `dbmate up` below applies the latest text of every - # file and stays green, so edits-after-apply are invisible to that job. # Fail the PR when a migration file changes (rather than is added) - # relative to main; force authors to ship a new dated file instead. + # relative to main; force authors to ship a new dated migration instead. + # + # Exceptions: + # 1. The baseline at 00000000000000_baseline.sql may be regenerated + # by re-running the squash workflow (always modifiable). + # 2. PRs whose commit message contains the explicit sentinel + # `[squash-baseline]` may delete the forward-delta migration + # files that were collapsed into the baseline. This is a + # one-time-per-squash exception and code review is the gate. if: github.event_name == 'pull_request' env: BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | - edited=$(git diff --name-only --diff-filter=MRD "$BASE_SHA"...HEAD -- db/migrations/ || true) + if git log "$BASE_SHA"..HEAD --format=%B | grep -qF '[squash-baseline]'; then + echo "[squash-baseline] sentinel present; immutability check is skipped for this PR." + exit 0 + fi + edited=$(git diff --name-only --diff-filter=MRD "$BASE_SHA"...HEAD -- db/migrations/ \ + | grep -v '^db/migrations/00000000000000_baseline\.sql$' || true) if [ -n "$edited" ]; then - echo "::error::Migration files were modified or renamed — applied migrations are immutable. Add a new dated migration instead." + echo "::error::Migration files were modified or renamed — applied migrations are immutable (only db/migrations/00000000000000_baseline.sql is excluded). Add a new dated migration instead." echo "$edited" exit 1 fi @@ -351,10 +362,11 @@ jobs: - name: Apply all migrations against an empty Postgres env: DATABASE_URL: postgres://postgres:postgres@localhost:5432/lobu_ci?sslmode=disable - # `dbmate up` dumps `db/schema.sql` after running. The drift check below - # diffs that file against the committed version after stripping volatile - # lines (see `scripts/normalize-schema.sh`). - run: dbmate --migrations-dir db/migrations --schema-file db/schema.sql up + # The baseline migration plus any forward deltas must apply cleanly + # to a fresh DB. That's the only schema check left after the squash — + # db/schema.sql + the drift-gate-vs-baseline diff are gone since the + # baseline IS the canonical schema. + run: dbmate --migrations-dir db/migrations up - name: Verify migrations are listed in the schema_migrations table env: @@ -368,31 +380,6 @@ jobs: exit 1 fi - - name: Normalize generated schema.sql for comparison - # pg_dump emits non-deterministic / env-dependent lines that vary - # across pg_dump versions and runs: `\restrict `, - # `\unrestrict `, `-- Dumped from database version ...`, - # `-- Dumped by pg_dump version ...`, and `SET transaction_timeout = 0;` - # (pg17+ only). Strip them so the drift check is purely about the - # actual schema. The committed `db/schema.sql` is in the same stripped - # form; devs reproducing locally should run `scripts/normalize-schema.sh - # db/schema.sql` after `dbmate ... dump`. - run: scripts/normalize-schema.sh db/schema.sql - - - name: Schema snapshot drift — db/schema.sql must match `dbmate up` output - # Independent backstop against editing applied migrations. The - # immutability check above looks at the migration file set; this looks - # at the end-state schema. They catch the same class of bug from - # opposite sides — a buggy edit could in principle pass one and trip - # the other (e.g. rename + edit). Failure mode is concrete: - # `make db-schema` locally (requires DATABASE_URL with pgvector) and - # commit the regenerated db/schema.sql. - run: | - if ! git diff --exit-code -- db/schema.sql; then - echo "::error::db/schema.sql is stale. Regenerate locally with \`make db-schema\` (requires DATABASE_URL with pgvector) and commit the result." - exit 1 - fi - # Path-filter for the Mac smoke build, isolated to a cheap ubuntu runner # so skipped PRs don't allocate a macOS runner just to evaluate the diff. # `github.event.pull_request.changed_files` in the webhook payload is an diff --git a/Makefile b/Makefile index 3d7e664a0..1adf9ad0b 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Development Makefile for Lobu -.PHONY: help setup build test eval clean dev build-packages ensure-submodule clean-workers test-unit test-integration test-e2e typecheck task-setup task-clean task-use db-schema +.PHONY: help setup build test eval clean dev build-packages ensure-submodule clean-workers test-unit test-integration test-e2e typecheck task-setup task-clean task-use # Default target help: @@ -18,7 +18,6 @@ help: @echo " make task-setup NAME= - Create a paired worktree at .claude/worktrees/ (lobu + submodule on real branch, .env copied, ports auto-assigned, Lobu context registered)" @echo " make task-clean NAME= [FORCE=1] - Remove the worktree, both branches, and the Lobu context (refuses if there's uncommitted/unpushed work unless FORCE=1)" @echo " make task-use NAME= - Point Chrome ext / Mac app symlinks at this worktree (or 'main' for the canonical checkout)" - @echo " make db-schema - Run dbmate up + normalize db/schema.sql (after adding a migration; auto-patches the varchar(128) gotcha)" # Strict typecheck — mirrors the Dockerfile so local matches CI. Catches # what `build-packages` (relaxed, bundler-only) misses. @@ -91,20 +90,6 @@ task-use: @: $${NAME?Usage: make task-use NAME=} @./scripts/task-use.sh "$(NAME)" -# --- DB schema regen --------------------------------------------------------- -# After adding a file under db/migrations/, regenerate the end-state snapshot -# at db/schema.sql so CI's drift check passes. Mirrors exactly what CI runs, -# then normalizes the output (strips pg18 noise + restores the varchar(128) -# length pg18 silently drops from schema_migrations.version). - -db-schema: - @: $${DATABASE_URL?Set DATABASE_URL=postgres://... (with pgvector) before running} - @echo "→ dbmate up (migrations → db/schema.sql)..." - @dbmate --migrations-dir db/migrations --schema-file db/schema.sql up - @echo "→ normalizing db/schema.sql..." - @./scripts/normalize-schema.sh db/schema.sql - @echo "✓ db/schema.sql regenerated and normalized (varchar(128) auto-patched)" - # --- Test pipelines --------------------------------------------------------- # These mirror what CI runs (.github/workflows/ci.yml) so a passing local run # is a strong signal CI will pass. diff --git a/db/migrations/00000000000000_baseline.sql b/db/migrations/00000000000000_baseline.sql index 158cccd66..fda15b859 100644 --- a/db/migrations/00000000000000_baseline.sql +++ b/db/migrations/00000000000000_baseline.sql @@ -1,61 +1,275 @@ -- migrate:up - +-- ============================================================================= +-- Lobu schema baseline (2026-05-19) +-- ============================================================================= +-- +-- Single source of truth for the schema. Replaces 82 prior migration files +-- (the old 00000000000000_baseline.sql + 81 forward deltas). +-- +-- Generated by: +-- 1. Apply all 82 historical migrations to a fresh pgvector/pgvector:pg16 DB +-- 2. Drop dead schema flagged by audit: +-- - tables: mcp_proxy_sessions, organization_lobu_links, and four +-- migration_* temp artifacts (entity_type_org_backfill, +-- created_entity_types, deleted_default_entity_types, events_kind_backup) +-- - columns: agents.skill_auto_granted_domains +-- 3. Annotate the load-bearing tables with COMMENT ON +-- 4. pg_dump --schema-only +-- 5. Strip pg_dump noise + the schema_migrations table CREATE (dbmate +-- creates it itself) + the schema_migrations data dump +-- +-- Companion changes in this commit: +-- - db/schema.sql deleted (the baseline IS the schema) +-- - scripts/normalize-schema.sh deleted (no schema.sql to normalize) +-- - packages/server/src/db/embedded-schema-patches.ts deleted (embedded +-- path now runs migrations the same way prod does) +-- - Makefile: removed `make db-schema` +-- - CI: removed drift-gate; immutability check honors [squash-baseline] +-- +-- Prod rollout — data-safe procedure +-- ----------------------------------- +-- +-- Safety stance: nothing is dropped. Tables get renamed (data preserved +-- in-place under a date-stamped name). Columns get snapshotted into a +-- side table before being removed from the live table. If the audit +-- missed a reader, recovery is `SELECT * FROM ` away — no +-- pg_restore round-trip. +-- +-- Layered safety: +-- Layer A (cheap, default rollback): CNPG point-in-time recovery via +-- the existing `barmanObjectStore` to Cloudflare R2 with 30-day +-- retention + 15-min archive_timeout (see Cluster spec in +-- packages/owletto/deploy/k8s/apps/lobu/base/helmrelease.yaml). +-- Pattern proof-point: db-recovery.yaml ran successfully on +-- 2026-03-15 after the Reddit re-sync incident. +-- Layer B (in-DB safety net): the renamed tables + column-snapshot +-- tables this surgery leaves behind. Queryable any time; restore +-- in a single ALTER TABLE/UPDATE. +-- +-- STEP 0: pre-flight. Record the surgery start timestamp — this is the +-- `targetTime` for any future PITR rollback. CNPG can recover to any +-- transaction-level point within the 30-day retention window. +-- +-- psql "$PROD_DATABASE_URL" -c "SELECT now()" | tee /tmp/pre-surgery-ts.txt +-- +-- STEP 1: full-DB dump as belt-and-suspenders (CNPG PITR is the real +-- safety net; this is paranoia in case of WAL archive corruption): +-- +-- pg_dump --format=custom --no-owner --no-privileges \ +-- "$PROD_DATABASE_URL" > /tmp/lobu-pre-squash-$(date +%Y%m%d-%H%M%S).dump +-- +-- STEP 2: dump the schema_migrations ledger as CSV. Rollback restores +-- the 82 applied-version rows the surgery is about to reset: +-- +-- psql "$PROD_DATABASE_URL" \ +-- -c "\\copy public.schema_migrations TO '/tmp/schema-migrations-pre-squash.csv' CSV HEADER" +-- +-- STEP 3: sanity-check the named droppee tables. The audit says they +-- have 0 rows; the counts let us catch surprise data before proceeding: +-- +-- psql "$PROD_DATABASE_URL" \ +-- -c "SELECT 'mcp_proxy_sessions' AS t, COUNT(*) FROM public.mcp_proxy_sessions +-- UNION ALL SELECT 'organization_lobu_links', COUNT(*) FROM public.organization_lobu_links +-- UNION ALL SELECT 'migration_20260315300000_entity_type_org_backfill', COUNT(*) FROM public.migration_20260315300000_entity_type_org_backfill +-- UNION ALL SELECT 'migration_20260316100000_created_entity_types', COUNT(*) FROM public.migration_20260316100000_created_entity_types +-- UNION ALL SELECT 'migration_20260316100000_deleted_default_entity_types', COUNT(*) FROM public.migration_20260316100000_deleted_default_entity_types +-- UNION ALL SELECT 'migration_20260316100000_events_kind_backup', COUNT(*) FROM public.migration_20260316100000_events_kind_backup" +-- # If any count is unexpectedly non-zero, ABORT and investigate. The +-- # rename in Step 4 will preserve the rows either way, but the +-- # surprise itself is signal. +-- +-- STEP 4: surgery. Single transaction; ROLLBACK on any error: +-- +-- BEGIN; +-- +-- -- Rename the 2 named tables. Suffix is short to stay within +-- -- Postgres's 63-char identifier limit. +-- ALTER TABLE IF EXISTS public.mcp_proxy_sessions RENAME TO mcp_proxy_sessions_d20260519; +-- ALTER TABLE IF EXISTS public.organization_lobu_links RENAME TO organization_lobu_links_d20260519; +-- +-- -- Rename the 4 migration_* artifact tables. Names approach the 63-char +-- -- identifier limit; use a 10-char suffix `_d20260519` to fit. +-- ALTER TABLE IF EXISTS public.migration_20260315300000_entity_type_org_backfill RENAME TO migration_20260315300000_entity_type_org_backfill_d20260519; +-- ALTER TABLE IF EXISTS public.migration_20260316100000_created_entity_types RENAME TO migration_20260316100000_created_entity_types_d20260519; +-- ALTER TABLE IF EXISTS public.migration_20260316100000_deleted_default_entity_types RENAME TO migration_20260316100000_deleted_default_entity_types_d20260519; +-- ALTER TABLE IF EXISTS public.migration_20260316100000_events_kind_backup RENAME TO migration_20260316100000_events_kind_backup_d20260519; +-- +-- -- Snapshot the 1 dead column into a backup table BEFORE dropping it +-- -- from the live table. The snapshot includes the FULL primary key of +-- -- the parent so the restore UPDATE is unambiguous: +-- -- agents.PK = (organization_id, id) → snapshot keeps both +-- CREATE TABLE public.agents_d20260519_skill_auto_granted_domains AS +-- SELECT organization_id, id AS agent_id, skill_auto_granted_domains +-- FROM public.agents +-- WHERE skill_auto_granted_domains IS NOT NULL +-- AND skill_auto_granted_domains <> '[]'::jsonb; +-- ALTER TABLE public.agents DROP COLUMN IF EXISTS skill_auto_granted_domains; +-- +-- -- NOTE: `runs.retry_delay_seconds` was originally proposed for drop +-- -- (audit flagged it as comment-only) but the column is in fact +-- -- load-bearing for RunsQueue (see packages/server/src/gateway/ +-- -- infrastructure/queue/runs-queue.ts lines 301, 368, 386, 575, 584, +-- -- 603, 617-620). KEPT. No surgery needed. +-- +-- -- Reset the migration ledger to baseline-only. New code's dbmate-up +-- -- skips the baseline body (it's already applied: the renames + drops +-- -- above did the diff against pre-squash state). +-- DELETE FROM public.schema_migrations; +-- INSERT INTO public.schema_migrations (version) VALUES ('00000000000000'); +-- +-- COMMIT; +-- +-- STEP 5: deploy the new image. The app's `dbmate up` on boot sees +-- '00000000000000' applied → skips the baseline. assertSchemaUpToDate +-- passes (expected = applied = baseline version). +-- +-- Verification post-deploy: +-- -- Renamed tables still queryable: +-- SELECT count(*) FROM public.mcp_proxy_sessions_d20260519; +-- -- Dropped column data still queryable: +-- SELECT count(*) FROM public.agents_d20260519_skill_auto_granted_domains; +-- +-- Rollback procedures +-- ------------------- +-- +-- Recovery path A (preferred — CNPG PITR; full restore): +-- +-- The Cluster spec in helmrelease.yaml has `backup.barmanObjectStore` +-- wired to R2 with `retentionPolicy: 30d` and `archive_timeout: 900`. +-- To recover to the pre-surgery timestamp from STEP 0: +-- +-- 1. Pause the broken app (scale to 0 or pause Flux reconcile). +-- 2. Apply a new Cluster CR patterned on +-- packages/owletto/deploy/k8s/apps/lobu/base/db-recovery.yaml: +-- +-- apiVersion: postgresql.cnpg.io/v1 +-- kind: Cluster +-- metadata: +-- name: db-recovery- +-- spec: +-- instances: 1 +-- storage: { size: 30Gi, storageClass: hcloud-volumes } +-- bootstrap: +-- recovery: +-- source: summaries-db-backup +-- recoveryTarget: +-- targetTime: "" +-- externalClusters: +-- - name: summaries-db-backup +-- barmanObjectStore: +-- serverName: summaries-db-v2 +-- destinationPath: s3://summaries-db-backup +-- # ... (same s3Credentials block as helmrelease.yaml) +-- +-- 3. Wait for CNPG to fetch the latest base backup + replay WAL. +-- 4. ONE OF the following two reconciliation paths, depending on which +-- image you want serving traffic after recovery: +-- (a) "Roll back to the pre-squash code" — revert this PR + redeploy +-- the old image, then repoint at the recovered cluster. The old +-- image's migrations dir has all 82 forward deltas; the recovered +-- DB has all 82 ledger rows; dbmate sees nothing pending. This is +-- the safest path and the one to take if the squash itself is +-- suspect. +-- (b) "Keep the new code, just rewind the data" — repoint at the +-- recovered cluster AND insert the baseline ledger row before any +-- new-image migration job runs: +-- psql -v ON_ERROR_STOP=1 <<'SQL' +-- DELETE FROM public.schema_migrations; +-- INSERT INTO public.schema_migrations (version) VALUES ('00000000000000'); +-- SQL +-- (Without that, the new image's dbmate-up sees 82 old applied +-- versions and 1 unapplied baseline → tries to CREATE TABLE +-- against existing tables → errors.) +-- 5. Resume Flux / scale the app back up. +-- +-- Why this works: WAL is archived every 15 minutes; PITR restores the +-- DB content to transaction-level precision within the retention window. +-- Reconciling the ledger to whichever image's migrations dir is +-- authoritative completes the rollback. +-- +-- Recovery path B (lightweight — just the surgery, not the whole DB): +-- +-- If only the squash-induced state needs reverting: +-- +-- -- 1. Restore the ledger: +-- psql "$PROD_DATABASE_URL" -c "DELETE FROM public.schema_migrations" +-- psql "$PROD_DATABASE_URL" \ +-- -c "\\copy public.schema_migrations FROM '/tmp/schema-migrations-pre-squash.csv' CSV HEADER" +-- -- 2. Restore renamed tables to original names: +-- psql "$PROD_DATABASE_URL" <<'SQL' +-- ALTER TABLE public.mcp_proxy_sessions_d20260519 RENAME TO mcp_proxy_sessions; +-- ALTER TABLE public.organization_lobu_links_d20260519 RENAME TO organization_lobu_links; +-- ALTER TABLE public.migration_20260315300000_entity_type_org_backfill_d20260519 RENAME TO migration_20260315300000_entity_type_org_backfill; +-- ALTER TABLE public.migration_20260316100000_created_entity_types_d20260519 RENAME TO migration_20260316100000_created_entity_types; +-- ALTER TABLE public.migration_20260316100000_deleted_default_entity_types_d20260519 RENAME TO migration_20260316100000_deleted_default_entity_types; +-- ALTER TABLE public.migration_20260316100000_events_kind_backup_d20260519 RENAME TO migration_20260316100000_events_kind_backup; +-- SQL +-- -- 3. Restore dropped columns + values (only if needed): +-- psql "$PROD_DATABASE_URL" <<'SQL' +-- ALTER TABLE public.agents ADD COLUMN skill_auto_granted_domains jsonb DEFAULT '[]'::jsonb; +-- -- agents.PK is composite (organization_id, id); join on both to +-- -- avoid restoring the wrong org's row when an id is reused across orgs. +-- UPDATE public.agents a SET skill_auto_granted_domains = b.skill_auto_granted_domains +-- FROM public.agents_d20260519_skill_auto_granted_domains b +-- WHERE a.organization_id = b.organization_id AND a.id = b.agent_id; +-- SQL +-- +-- Cleanup follow-up (separate PR, a week or two after this lands): +-- Once nothing in code or queries reads the renamed tables / snapshot +-- tables, drop them in a small focused PR: +-- +-- DROP TABLE public.mcp_proxy_sessions_d20260519; +-- DROP TABLE public.organization_lobu_links_d20260519; +-- -- ... etc. +-- +-- Fresh DBs (local dev, PGlite, CI): no surgery, no backups; dbmate +-- applies this file from scratch. Wipe local PGlite dirs +-- (`rm -rf /data`) to take advantage of the squash. +-- ============================================================================= + +-- +-- PostgreSQL database dump +-- SET statement_timeout = 0; SET lock_timeout = 0; SET idle_in_transaction_session_timeout = 0; SET client_encoding = 'UTF8'; SET standard_conforming_strings = on; --- pg_dump emits this with `false` (session-wide) to make every CREATE --- statement below schema-unambiguous via fully-qualified names. dbmate --- reuses one connection across migrations, so a session-wide blank --- `search_path` persists into the NEXT migration — which uses bare --- table names (e.g. `INSERT INTO connector_definitions ...`) and fails --- with `relation does not exist` on a fresh DB. Setting `true` scopes --- the change to this migration's transaction; subsequent migrations get --- the default `"$user", public` again. +-- Transaction-scoped (true) — without this, pg_dump's default session-scoped +-- value would leak past this migration's apply and any later forward delta +-- using unqualified table names would fail under CI's `dbmate up`. SELECT pg_catalog.set_config('search_path', '', true); SET check_function_bodies = false; SET xmloption = content; SET client_min_messages = warning; SET row_security = off; --- --- Name: public; Type: SCHEMA; Schema: -; Owner: - --- - --- *not* creating schema, since initdb creates it - - -- -- Name: SCHEMA public; Type: COMMENT; Schema: -; Owner: - -- COMMENT ON SCHEMA public IS ''; - -- -- Name: pg_trgm; Type: EXTENSION; Schema: -; Owner: - -- CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public; - -- -- Name: EXTENSION pg_trgm; Type: COMMENT; Schema: -; Owner: - -- COMMENT ON EXTENSION pg_trgm IS 'text similarity measurement and index searching based on trigrams'; - -- -- Name: vector; Type: EXTENSION; Schema: -; Owner: - -- CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA public; - -- -- Name: EXTENSION vector; Type: COMMENT; Schema: -; Owner: - -- @@ -103,7 +317,6 @@ BEGIN END; $$; - SET default_tablespace = ''; SET default_table_access_method = heap; @@ -128,7 +341,6 @@ CREATE TABLE public.account ( "updatedAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL ); - -- -- Name: agent_channel_bindings; Type: TABLE; Schema: public; Owner: - -- @@ -138,10 +350,10 @@ CREATE TABLE public.agent_channel_bindings ( platform text NOT NULL, channel_id text NOT NULL, team_id text, - created_at timestamp with time zone DEFAULT now() NOT NULL + created_at timestamp with time zone DEFAULT now() NOT NULL, + organization_id text NOT NULL ); - -- -- Name: agent_connections; Type: TABLE; Schema: public; Owner: - -- @@ -157,10 +369,10 @@ CREATE TABLE public.agent_connections ( error_message text, created_at timestamp with time zone DEFAULT now() NOT NULL, updated_at timestamp with time zone DEFAULT now() NOT NULL, + organization_id text NOT NULL, CONSTRAINT agent_connections_status_check CHECK ((status = ANY (ARRAY['active'::text, 'stopped'::text, 'error'::text]))) ); - -- -- Name: agent_grants; Type: TABLE; Schema: public; Owner: - -- @@ -171,10 +383,10 @@ CREATE TABLE public.agent_grants ( pattern text NOT NULL, expires_at timestamp with time zone, granted_at timestamp with time zone DEFAULT now() NOT NULL, - denied boolean DEFAULT false + denied boolean DEFAULT false, + organization_id text NOT NULL ); - -- -- Name: agent_grants_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -188,6 +400,58 @@ ALTER TABLE public.agent_grants ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY CACHE 1 ); +-- +-- Name: agent_secrets; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.agent_secrets ( + name text NOT NULL, + ciphertext text NOT NULL, + expires_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + organization_id text DEFAULT ''::text NOT NULL +); + +-- +-- Name: TABLE agent_secrets; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.agent_secrets IS 'Encrypted secret values referenced via secret:// refs. Backs the PostgresSecretStore implementation of @lobu/gateway WritableSecretStore.'; + +-- +-- Name: agent_transcript_snapshot; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.agent_transcript_snapshot ( + id bigint NOT NULL, + organization_id text NOT NULL, + agent_id text NOT NULL, + conversation_id text NOT NULL, + run_id bigint NOT NULL, + snapshot_jsonl text NOT NULL, + byte_size integer NOT NULL, + terminal_status text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT agent_transcript_snapshot_terminal_status_check CHECK ((terminal_status = ANY (ARRAY['completed'::text, 'failed'::text, 'timeout'::text, 'cancelled'::text]))) +); + +-- +-- Name: agent_transcript_snapshot_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.agent_transcript_snapshot_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- +-- Name: agent_transcript_snapshot_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.agent_transcript_snapshot_id_seq OWNED BY public.agent_transcript_snapshot.id; -- -- Name: agent_users; Type: TABLE; Schema: public; Owner: - @@ -197,10 +461,10 @@ CREATE TABLE public.agent_users ( agent_id text NOT NULL, platform text NOT NULL, user_id text NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL + created_at timestamp with time zone DEFAULT now() NOT NULL, + organization_id text NOT NULL ); - -- -- Name: agents; Type: TABLE; Schema: public; Owner: - -- @@ -212,8 +476,6 @@ CREATE TABLE public.agents ( description text, owner_platform text, owner_user_id text, - template_agent_id text, - parent_connection_id text, is_workspace_agent boolean DEFAULT false, workspace_id text, model text, @@ -222,24 +484,29 @@ CREATE TABLE public.agents ( network_config jsonb DEFAULT '{}'::jsonb, nix_config jsonb DEFAULT '{}'::jsonb, mcp_servers jsonb DEFAULT '{}'::jsonb, - mcp_install_notified jsonb DEFAULT '{}'::jsonb, agent_integrations jsonb DEFAULT '{}'::jsonb, soul_md text DEFAULT ''::text, user_md text DEFAULT ''::text, identity_md text DEFAULT ''::text, skills_config jsonb DEFAULT '{"skills": []}'::jsonb, - skill_auto_granted_domains jsonb DEFAULT '[]'::jsonb, tools_config jsonb DEFAULT '{}'::jsonb, plugins_config jsonb DEFAULT '{}'::jsonb, - auth_profiles jsonb DEFAULT '[]'::jsonb, installed_providers jsonb DEFAULT '[]'::jsonb, skill_registries jsonb DEFAULT '[]'::jsonb, verbose_logging boolean DEFAULT false, created_at timestamp with time zone DEFAULT now() NOT NULL, updated_at timestamp with time zone DEFAULT now() NOT NULL, - last_used_at timestamp with time zone + last_used_at timestamp with time zone, + egress_config jsonb DEFAULT '{}'::jsonb, + pre_approved_tools jsonb DEFAULT '[]'::jsonb, + guardrails jsonb DEFAULT '[]'::jsonb ); +-- +-- Name: TABLE agents; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.agents IS 'Top-level agent definitions per organization. Each row is one configured AI agent: model selection, system prompt (soulMd/identityMd/userMd), MCP tool surface, network/nix/plugins config, secret refs, and tool-approval policy.'; -- -- Name: auth_profiles; Type: TABLE; Schema: public; Owner: - @@ -250,7 +517,7 @@ CREATE TABLE public.auth_profiles ( organization_id text NOT NULL, slug text NOT NULL, display_name text NOT NULL, - connector_key text NOT NULL, + connector_key text, profile_kind text NOT NULL, status text DEFAULT 'active'::text NOT NULL, auth_data jsonb DEFAULT '{}'::jsonb NOT NULL, @@ -259,10 +526,24 @@ CREATE TABLE public.auth_profiles ( created_by text, created_at timestamp with time zone DEFAULT now() NOT NULL, updated_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT auth_profiles_profile_kind_check CHECK ((profile_kind = ANY (ARRAY['env'::text, 'oauth_app'::text, 'oauth_account'::text, 'browser_session'::text]))), + metadata jsonb DEFAULT '{}'::jsonb NOT NULL, + device_worker_id uuid, + browser_kind text, + user_data_dir text, + cdp_url text, + is_default_for_connector boolean DEFAULT false NOT NULL, + CONSTRAINT auth_profiles_browser_kind_check CHECK (((browser_kind IS NULL) OR (browser_kind = ANY (ARRAY['chrome'::text, 'brave'::text, 'arc'::text, 'edge'::text])))), + CONSTRAINT auth_profiles_connector_key_required CHECK (((connector_key IS NOT NULL) OR (profile_kind = 'browser_session'::text))), + CONSTRAINT auth_profiles_device_browser_path_mutex CHECK (((device_worker_id IS NULL) OR (profile_kind <> 'browser_session'::text) OR (user_data_dir IS NULL) OR (cdp_url IS NULL))), + CONSTRAINT auth_profiles_profile_kind_check CHECK ((profile_kind = ANY (ARRAY['env'::text, 'oauth_app'::text, 'oauth_account'::text, 'browser_session'::text, 'interactive'::text]))), CONSTRAINT auth_profiles_status_check CHECK ((status = ANY (ARRAY['active'::text, 'pending_auth'::text, 'error'::text, 'revoked'::text]))) ); +-- +-- Name: TABLE auth_profiles; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.auth_profiles IS 'Per-user-per-agent OAuth / auth state for connector-mediated user identities. Holds tokens, refresh tokens, expiry, and the pending-auth workflow state. Bound to a device_workers.worker_id when minted via /api/me/devices/mint-child-token.'; -- -- Name: auth_profiles_id_seq; Type: SEQUENCE; Schema: public; Owner: - @@ -277,6 +558,110 @@ ALTER TABLE public.auth_profiles ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDE CACHE 1 ); +-- +-- Name: chat_state_cache; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.chat_state_cache ( + key_prefix text NOT NULL, + cache_key text NOT NULL, + value text NOT NULL, + expires_at timestamp with time zone, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + +-- +-- Name: chat_state_lists; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.chat_state_lists ( + key_prefix text NOT NULL, + list_key text NOT NULL, + seq bigint NOT NULL, + value text NOT NULL, + expires_at timestamp with time zone +); + +-- +-- Name: chat_state_lists_seq_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.chat_state_lists_seq_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- +-- Name: chat_state_lists_seq_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.chat_state_lists_seq_seq OWNED BY public.chat_state_lists.seq; + +-- +-- Name: chat_state_locks; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.chat_state_locks ( + key_prefix text NOT NULL, + thread_id text NOT NULL, + token text NOT NULL, + expires_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + +-- +-- Name: chat_state_queues; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.chat_state_queues ( + key_prefix text NOT NULL, + thread_id text NOT NULL, + seq bigint NOT NULL, + value text NOT NULL, + expires_at timestamp with time zone NOT NULL +); + +-- +-- Name: chat_state_queues_seq_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.chat_state_queues_seq_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- +-- Name: chat_state_queues_seq_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.chat_state_queues_seq_seq OWNED BY public.chat_state_queues.seq; + +-- +-- Name: chat_state_subscriptions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.chat_state_subscriptions ( + key_prefix text NOT NULL, + thread_id text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + +-- +-- Name: chat_user_identities; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.chat_user_identities ( + platform text NOT NULL, + team_id text DEFAULT ''::text NOT NULL, + platform_user_id text NOT NULL, + lobu_user_id text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); -- -- Name: connect_tokens; Type: TABLE; Schema: public; Owner: - @@ -300,7 +685,6 @@ CREATE TABLE public.connect_tokens ( CONSTRAINT connect_tokens_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'completed'::text, 'expired'::text]))) ); - -- -- Name: connect_tokens_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -312,14 +696,12 @@ CREATE SEQUENCE public.connect_tokens_id_seq NO MAXVALUE CACHE 1; - -- -- Name: connect_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.connect_tokens_id_seq OWNED BY public.connect_tokens.id; - -- -- Name: connections; Type: TABLE; Schema: public; Owner: - -- @@ -343,9 +725,16 @@ CREATE TABLE public.connections ( visibility text DEFAULT 'org'::text NOT NULL, deleted_at timestamp with time zone, agent_id text, + device_worker_id uuid, + slug text NOT NULL, CONSTRAINT connections_status_check CHECK ((status = ANY (ARRAY['active'::text, 'paused'::text, 'error'::text, 'revoked'::text, 'pending_auth'::text]))) ); +-- +-- Name: TABLE connections; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.connections IS 'Per-organization credentialed connections to external platforms / connectors (Slack, Telegram, GitHub, Linear, …). Stores platform-specific config + secret:// refs. Replaces the legacy chat_connections + per-platform tables.'; -- -- Name: connections_id_seq; Type: SEQUENCE; Schema: public; Owner: - @@ -358,14 +747,12 @@ CREATE SEQUENCE public.connections_id_seq NO MAXVALUE CACHE 1; - -- -- Name: connections_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.connections_id_seq OWNED BY public.connections.id; - -- -- Name: connector_definitions; Type: TABLE; Schema: public; Owner: - -- @@ -390,9 +777,19 @@ CREATE TABLE public.connector_definitions ( api_type text DEFAULT 'api'::text NOT NULL, favicon_domain text, openapi_config jsonb, + default_connection_config jsonb, + entity_link_overrides jsonb, + default_repair_agent_id text, + required_capability text, + runtime jsonb, CONSTRAINT connector_definitions_status_check CHECK ((status = ANY (ARRAY['active'::text, 'archived'::text, 'draft'::text]))) ); +-- +-- Name: COLUMN connector_definitions.entity_link_overrides; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON COLUMN public.connector_definitions.entity_link_overrides IS 'Per-install override of connector entityLinks rules. See resolveEntityLinkRules() for merge semantics.'; -- -- Name: connector_definitions_id_seq; Type: SEQUENCE; Schema: public; Owner: - @@ -406,14 +803,12 @@ CREATE SEQUENCE public.connector_definitions_id_seq NO MAXVALUE CACHE 1; - -- -- Name: connector_definitions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.connector_definitions_id_seq OWNED BY public.connector_definitions.id; - -- -- Name: connector_versions; Type: TABLE; Schema: public; Owner: - -- @@ -422,17 +817,13 @@ CREATE TABLE public.connector_versions ( id integer NOT NULL, connector_key text NOT NULL, version text NOT NULL, - compiled_code text NOT NULL, + compiled_code text, compiled_code_hash text, created_at timestamp with time zone DEFAULT now() NOT NULL, source_code text, - source_repository text, - source_ref text, - source_commit_sha text, source_path text ); - -- -- Name: connector_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -445,23 +836,20 @@ CREATE SEQUENCE public.connector_versions_id_seq NO MAXVALUE CACHE 1; - -- -- Name: connector_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.connector_versions_id_seq OWNED BY public.connector_versions.id; - -- -- Name: events; Type: TABLE; Schema: public; Owner: - -- CREATE TABLE public.events ( - id bigint CONSTRAINT event_id_not_null NOT NULL, + id bigint NOT NULL, organization_id text NOT NULL, entity_ids bigint[], - source_id integer, origin_id text, title text, payload_type text DEFAULT 'text'::text NOT NULL, @@ -493,19 +881,18 @@ CREATE TABLE public.events ( interaction_error text, supersedes_event_id bigint, content_length integer GENERATED ALWAYS AS (COALESCE(length(payload_text), 0)) STORED, - CONSTRAINT events_payload_type_check CHECK ((payload_type = ANY (ARRAY['text'::text, 'markdown'::text, 'json_template'::text, 'media'::text, 'empty'::text]))), - CONSTRAINT events_interaction_type_check CHECK ((interaction_type = ANY (ARRAY['none'::text, 'approval'::text]))), + search_tsv tsvector GENERATED ALWAYS AS ((setweight(to_tsvector('english'::regconfig, COALESCE(title, ''::text)), 'A'::"char") || setweight(to_tsvector('english'::regconfig, COALESCE(payload_text, ''::text)), 'B'::"char"))) STORED, CONSTRAINT events_interaction_status_check CHECK ((interaction_status = ANY (ARRAY['pending'::text, 'approved'::text, 'rejected'::text, 'completed'::text, 'failed'::text]))), + CONSTRAINT events_interaction_type_check CHECK ((interaction_type = ANY (ARRAY['none'::text, 'approval'::text]))), + CONSTRAINT events_payload_type_check CHECK ((payload_type = ANY (ARRAY['text'::text, 'markdown'::text, 'json_template'::text, 'media'::text, 'empty'::text]))), CONSTRAINT events_semantic_type_not_empty CHECK ((length(btrim(semantic_type)) > 0)) ); - -- -- Name: TABLE events; Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON TABLE public.events IS 'Append-only event log for source ingests, user-authored knowledge, and operation history'; - +COMMENT ON TABLE public.events IS 'Append-only event log — the primary data substrate. Each row is a discrete observation from a connector or user action. Watchers, semantic search, and entity extraction all derive from here. Never DELETE; supersede with a tombstone row that points at the original via supersedes_event_id.'; -- -- Name: COLUMN events.id; Type: COMMENT; Schema: public; Owner: - @@ -513,13 +900,17 @@ COMMENT ON TABLE public.events IS 'Append-only event log for source ingests, use COMMENT ON COLUMN public.events.id IS 'Primary key (BIGSERIAL) - exposed to MCP tools'; - -- -- Name: COLUMN events.score; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.events.score IS 'Normalized 0-100 score for ranking'; +-- +-- Name: COLUMN events.origin_type; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON COLUMN public.events.origin_type IS 'Source-native item type (post, comment, review, issue, etc.)'; -- -- Name: COLUMN events.run_id; Type: COMMENT; Schema: public; Owner: - @@ -527,13 +918,22 @@ COMMENT ON COLUMN public.events.score IS 'Normalized 0-100 score for ranking'; COMMENT ON COLUMN public.events.run_id IS 'Links the event to the run that produced or acted on it'; - -- --- Name: COLUMN events.origin_type; Type: COMMENT; Schema: public; Owner: - +-- Name: content_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- -COMMENT ON COLUMN public.events.origin_type IS 'Source-native item type (post, comment, review, issue, etc.)'; +CREATE SEQUENCE public.content_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +-- +-- Name: content_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.content_id_seq OWNED BY public.events.id; -- -- Name: event_embeddings; Type: TABLE; Schema: public; Owner: - @@ -542,29 +942,74 @@ COMMENT ON COLUMN public.events.origin_type IS 'Source-native item type (post, c CREATE TABLE public.event_embeddings ( event_id bigint NOT NULL, embedding public.vector(768) NOT NULL, - model_key text, created_at timestamp with time zone DEFAULT now() NOT NULL ); - -- --- Name: content_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- Name: current_event_records; Type: VIEW; Schema: public; Owner: - -- -CREATE SEQUENCE public.content_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - +CREATE VIEW public.current_event_records AS + SELECT e.id, + e.organization_id, + e.entity_ids, + e.origin_id, + e.title, + e.payload_type, + e.payload_text, + e.payload_data, + e.payload_template, + e.attachments, + e.metadata, + e.score, + emb.embedding, + e.author_name, + e.source_url, + e.occurred_at, + e.created_at, + e.origin_parent_id, + COALESCE(length(e.payload_text), 0) AS content_length, + e.search_tsv, + e.origin_type, + e.connector_key, + e.connection_id, + e.feed_key, + e.feed_id, + e.run_id, + e.semantic_type, + e.client_id, + e.created_by, + e.interaction_type, + e.interaction_status, + e.interaction_input_schema, + e.interaction_input, + e.interaction_output, + e.interaction_error, + e.supersedes_event_id + FROM (public.events e + LEFT JOIN public.event_embeddings emb ON ((emb.event_id = e.id))) + WHERE (NOT (EXISTS ( SELECT 1 + FROM public.events newer + WHERE (newer.supersedes_event_id = e.id)))); -- --- Name: content_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- Name: device_workers; Type: TABLE; Schema: public; Owner: - -- -ALTER SEQUENCE public.content_id_seq OWNED BY public.events.id; - +CREATE TABLE public.device_workers ( + user_id text NOT NULL, + worker_id text NOT NULL, + platform text, + app_version text, + capabilities jsonb DEFAULT '[]'::jsonb NOT NULL, + label text, + first_seen_at timestamp with time zone DEFAULT now() NOT NULL, + last_seen_at timestamp with time zone DEFAULT now() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, + organization_id text, + notification_budget_per_day integer DEFAULT 10 NOT NULL, + CONSTRAINT device_workers_notification_budget_per_day_nonneg CHECK ((notification_budget_per_day >= 0)) +); -- -- Name: entities; Type: TABLE; Schema: public; Owner: - @@ -572,7 +1017,6 @@ ALTER SEQUENCE public.content_id_seq OWNED BY public.events.id; CREATE TABLE public.entities ( id bigint NOT NULL, - entity_type text NOT NULL, parent_id bigint, name text NOT NULL, metadata jsonb DEFAULT '{}'::jsonb, @@ -587,56 +1031,101 @@ CREATE TABLE public.entities ( embedding public.vector(768), content_tsv tsvector GENERATED ALWAYS AS (to_tsvector('english'::regconfig, ((COALESCE(name, ''::text) || ' '::text) || COALESCE(content, ''::text)))) STORED, content_hash text, - deleted_at timestamp with time zone + deleted_at timestamp with time zone, + entity_type_id integer NOT NULL ); - -- -- Name: TABLE entities; Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON TABLE public.entities IS 'Unified entity table (brands, products, and future entity types)'; +COMMENT ON TABLE public.entities IS 'Domain objects tracked per organization (people, companies, tasks, …). Type definition lives in entity_types; the row body is jsonb keyed by the type''s columns.'; + +-- +-- Name: COLUMN entities.parent_id; Type: COMMENT; Schema: public; Owner: - +-- +COMMENT ON COLUMN public.entities.parent_id IS 'Hierarchical parent (products → brands, brands → parent brands)'; -- --- Name: COLUMN entities.entity_type; Type: COMMENT; Schema: public; Owner: - +-- Name: COLUMN entities.enabled_classifiers; Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON COLUMN public.entities.entity_type IS 'Type of entity: brand, product (future: location, feature, team)'; +COMMENT ON COLUMN public.entities.enabled_classifiers IS 'Classifiers enabled for this entity (inherited from parent if NULL)'; +-- +-- Name: entities_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.entities_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; -- --- Name: COLUMN entities.parent_id; Type: COMMENT; Schema: public; Owner: - +-- Name: entities_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- -COMMENT ON COLUMN public.entities.parent_id IS 'Hierarchical parent (products → brands, brands → parent brands)'; +ALTER SEQUENCE public.entities_id_seq OWNED BY public.entities.id; +-- +-- Name: entity_identities; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.entity_identities ( + id bigint NOT NULL, + organization_id text NOT NULL, + entity_id bigint NOT NULL, + namespace text NOT NULL, + identifier text NOT NULL, + source_connector text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); -- --- Name: COLUMN entities.enabled_classifiers; Type: COMMENT; Schema: public; Owner: - +-- Name: TABLE entity_identities; Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON COLUMN public.entities.enabled_classifiers IS 'Classifiers enabled for this entity (inherited from parent if NULL)'; +COMMENT ON TABLE public.entity_identities IS 'Normalized identifier claims per entity. See docs/identity-linking.md for the full pattern.'; +-- +-- Name: COLUMN entity_identities.namespace; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON COLUMN public.entity_identities.namespace IS 'Identifier kind. Standard values: phone, email, wa_jid, slack_user_id, github_login, auth_user_id, google_contact_id. Custom namespaces allowed but connectors sharing a namespace must agree on its format.'; -- --- Name: entities_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- Name: COLUMN entity_identities.identifier; Type: COMMENT; Schema: public; Owner: - -- -CREATE SEQUENCE public.entities_id_seq +COMMENT ON COLUMN public.entity_identities.identifier IS 'Normalized identifier value (E.164 digits for phone, lowercase for email, etc.). Normalizers in @lobu/connector-sdk own the canonical form.'; + +-- +-- Name: COLUMN entity_identities.source_connector; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON COLUMN public.entity_identities.source_connector IS 'Who claimed this identifier: "connector:whatsapp", "manual", or null when seeded by migration.'; + +-- +-- Name: entity_identities_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.entity_identities_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; - -- --- Name: entities_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- Name: entity_identities_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- -ALTER SEQUENCE public.entities_id_seq OWNED BY public.entities.id; - +ALTER SEQUENCE public.entity_identities_id_seq OWNED BY public.entity_identities.id; -- -- Name: entity_relationship_type_rules; Type: TABLE; Schema: public; Owner: - @@ -652,7 +1141,6 @@ CREATE TABLE public.entity_relationship_type_rules ( updated_at timestamp with time zone DEFAULT now() ); - -- -- Name: entity_relationship_type_rules_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -665,14 +1153,12 @@ CREATE SEQUENCE public.entity_relationship_type_rules_id_seq NO MAXVALUE CACHE 1; - -- -- Name: entity_relationship_type_rules_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.entity_relationship_type_rules_id_seq OWNED BY public.entity_relationship_type_rules.id; - -- -- Name: entity_relationship_types; Type: TABLE; Schema: public; Owner: - -- @@ -691,10 +1177,10 @@ CREATE TABLE public.entity_relationship_types ( deleted_at timestamp with time zone, created_at timestamp with time zone DEFAULT now(), updated_at timestamp with time zone DEFAULT now(), + metadata jsonb, CONSTRAINT entity_relationship_types_status_check CHECK ((status = ANY (ARRAY['active'::text, 'archived'::text]))) ); - -- -- Name: entity_relationship_types_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -707,14 +1193,12 @@ CREATE SEQUENCE public.entity_relationship_types_id_seq NO MAXVALUE CACHE 1; - -- -- Name: entity_relationship_types_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.entity_relationship_types_id_seq OWNED BY public.entity_relationship_types.id; - -- -- Name: entity_relationships; Type: TABLE; Schema: public; Owner: - -- @@ -735,7 +1219,6 @@ CREATE TABLE public.entity_relationships ( updated_at timestamp with time zone DEFAULT now() ); - -- -- Name: entity_relationships_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -748,14 +1231,12 @@ CREATE SEQUENCE public.entity_relationships_id_seq NO MAXVALUE CACHE 1; - -- -- Name: entity_relationships_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.entity_relationships_id_seq OWNED BY public.entity_relationships.id; - -- -- Name: entity_type_audit; Type: TABLE; Schema: public; Owner: - -- @@ -771,7 +1252,6 @@ CREATE TABLE public.entity_type_audit ( CONSTRAINT entity_type_audit_action_check CHECK ((action = ANY (ARRAY['create'::text, 'update'::text, 'delete'::text]))) ); - -- -- Name: entity_type_audit_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -784,14 +1264,12 @@ CREATE SEQUENCE public.entity_type_audit_id_seq NO MAXVALUE CACHE 1; - -- -- Name: entity_type_audit_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.entity_type_audit_id_seq OWNED BY public.entity_type_audit.id; - -- -- Name: entity_types; Type: TABLE; Schema: public; Owner: - -- @@ -814,6 +1292,11 @@ CREATE TABLE public.entity_types ( event_kinds jsonb ); +-- +-- Name: TABLE entity_types; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.entity_types IS 'Per-organization type definitions for the entities table — column schema, defaults, classifiers, and the lifecycle hooks each type triggers.'; -- -- Name: entity_types_id_seq; Type: SEQUENCE; Schema: public; Owner: - @@ -827,14 +1310,12 @@ CREATE SEQUENCE public.entity_types_id_seq NO MAXVALUE CACHE 1; - -- -- Name: entity_types_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.entity_types_id_seq OWNED BY public.entity_types.id; - -- -- Name: event_classifications; Type: TABLE; Schema: public; Owner: - -- @@ -860,6 +1341,11 @@ CREATE TABLE public.event_classifications ( CONSTRAINT event_classifications_values_not_empty CHECK ((cardinality("values") > 0)) ); +-- +-- Name: TABLE event_classifications; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.event_classifications IS 'Per-event labels produced by classifiers (embedding / llm / user-driven). Multiple classifications per event; the latest per (event, type) is exposed via the latest_event_classifications view.'; -- -- Name: event_classifications_id_seq; Type: SEQUENCE; Schema: public; Owner: - @@ -872,14 +1358,12 @@ CREATE SEQUENCE public.event_classifications_id_seq NO MAXVALUE CACHE 1; - -- -- Name: event_classifications_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.event_classifications_id_seq OWNED BY public.event_classifications.id; - -- -- Name: event_classifier_versions; Type: TABLE; Schema: public; Owner: - -- @@ -899,14 +1383,12 @@ CREATE TABLE public.event_classifier_versions ( extraction_config jsonb ); - -- -- Name: COLUMN event_classifier_versions.preferred_model; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.event_classifier_versions.preferred_model IS 'AI model to use for LLM fallback. Use 8B for simple classifiers (cheap), 70B for complex reasoning (expensive). Default: 8B'; - -- -- Name: event_classifier_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -918,14 +1400,12 @@ CREATE SEQUENCE public.event_classifier_versions_id_seq NO MAXVALUE CACHE 1; - -- -- Name: event_classifier_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.event_classifier_versions_id_seq OWNED BY public.event_classifier_versions.id; - -- -- Name: event_classifiers; Type: TABLE; Schema: public; Owner: - -- @@ -936,18 +1416,17 @@ CREATE TABLE public.event_classifiers ( name text NOT NULL, description text, attribute_key text NOT NULL, - status text DEFAULT 'active'::text, + status text DEFAULT 'active'::text NOT NULL, created_at timestamp with time zone DEFAULT now(), updated_at timestamp with time zone DEFAULT now(), created_by text NOT NULL, entity_id bigint, watcher_id bigint, - organization_id text, + organization_id text NOT NULL, entity_ids bigint[], CONSTRAINT event_classifiers_status_check CHECK ((status = ANY (ARRAY['active'::text, 'deprecated'::text]))) ); - -- -- Name: event_classifiers_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -959,79 +1438,12 @@ CREATE SEQUENCE public.event_classifiers_id_seq NO MAXVALUE CACHE 1; - -- -- Name: event_classifiers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.event_classifiers_id_seq OWNED BY public.event_classifiers.id; - -CREATE VIEW public.current_event_records AS - SELECT e.id, - e.organization_id, - e.entity_ids, - e.source_id, - e.origin_id, - e.title, - e.payload_type, - e.payload_text, - e.payload_data, - e.payload_template, - e.attachments, - e.metadata, - e.score, - emb.embedding, - e.author_name, - e.source_url, - e.occurred_at, - e.created_at, - e.origin_parent_id, - COALESCE(length(e.payload_text), 0) AS content_length, - e.origin_type, - e.connector_key, - e.connection_id, - e.feed_key, - e.feed_id, - e.run_id, - e.semantic_type, - e.client_id, - e.created_by, - e.interaction_type, - e.interaction_status, - e.interaction_input_schema, - e.interaction_input, - e.interaction_output, - e.interaction_error, - e.supersedes_event_id - FROM (public.events e - LEFT JOIN public.event_embeddings emb ON ((emb.event_id = e.id))) - WHERE (NOT (EXISTS ( SELECT 1 - FROM public.events newer - WHERE (newer.supersedes_event_id = e.id)))); - - --- --- Name: event_thread_tree; Type: VIEW; Schema: public; Owner: - --- - -CREATE VIEW public.event_thread_tree AS - SELECT e.id, - e.origin_id, - e.origin_parent_id, - e.occurred_at, - COALESCE(parent.origin_id, e.origin_id) AS root_origin_id, - COALESCE(parent.occurred_at, e.occurred_at) AS root_occurred_at, - COALESCE(parent.score, e.score) AS root_score, - CASE - WHEN (e.origin_parent_id IS NULL) THEN 0 - ELSE 1 - END AS depth, - ARRAY[(COALESCE(parent.occurred_at, e.occurred_at))::text, (e.id)::text] AS sort_path - FROM (public.current_event_records e - LEFT JOIN public.current_event_records parent ON (((e.origin_parent_id = parent.origin_id) AND (e.entity_ids && parent.entity_ids)))); - - -- -- Name: feeds; Type: TABLE; Schema: public; Owner: - -- @@ -1057,9 +1469,20 @@ CREATE TABLE public.feeds ( deleted_at timestamp with time zone, schedule text, next_run_at timestamp with time zone, + repair_agent_id text, + repair_thread_id text, + repair_attempt_count integer DEFAULT 0 NOT NULL, + last_repair_at timestamp with time zone, + first_failure_at timestamp with time zone, + last_repair_post_hash text, CONSTRAINT feeds_status_check CHECK ((status = ANY (ARRAY['active'::text, 'paused'::text, 'error'::text]))) ); +-- +-- Name: TABLE feeds; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.feeds IS 'Per-organization content feeds derived from a connection (e.g., Slack channel → feed, RSS source → feed). Watchers and agents subscribe to feeds.'; -- -- Name: feeds_id_seq; Type: SEQUENCE; Schema: public; Owner: - @@ -1072,13 +1495,25 @@ CREATE SEQUENCE public.feeds_id_seq NO MAXVALUE CACHE 1; - -- -- Name: feeds_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.feeds_id_seq OWNED BY public.feeds.id; +-- +-- Name: grants; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.grants ( + agent_id text NOT NULL, + kind text NOT NULL, + pattern text NOT NULL, + expires_at timestamp with time zone, + granted_at timestamp with time zone DEFAULT now() NOT NULL, + denied boolean DEFAULT false NOT NULL, + organization_id text NOT NULL +); -- -- Name: invitation; Type: TABLE; Schema: public; Owner: - @@ -1095,7 +1530,6 @@ CREATE TABLE public.invitation ( "inviterId" text ); - -- -- Name: latest_event_classifications; Type: TABLE; Schema: public; Owner: - -- @@ -1115,70 +1549,46 @@ CREATE TABLE public.latest_event_classifications ( created_at timestamp with time zone NOT NULL ); - --- --- Name: member; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.member ( - id text NOT NULL, - "organizationId" text NOT NULL, - "userId" text NOT NULL, - role text DEFAULT 'member'::text NOT NULL, - "createdAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - "teamId" text -); - - -- --- Name: migration_20260315300000_entity_type_org_backfill; Type: TABLE; Schema: public; Owner: - +-- Name: mcp_sessions; Type: TABLE; Schema: public; Owner: - -- -CREATE TABLE public.migration_20260315300000_entity_type_org_backfill ( - entity_type_id integer CONSTRAINT migration_20260315300000_entity_type_or_entity_type_id_not_null NOT NULL +CREATE TABLE public.mcp_sessions ( + session_id text NOT NULL, + user_id text, + client_id text, + organization_id text, + member_role text, + requested_agent_id text, + is_authenticated boolean DEFAULT false NOT NULL, + scoped_to_org boolean DEFAULT false NOT NULL, + last_accessed_at timestamp with time zone DEFAULT now() NOT NULL, + expires_at timestamp with time zone NOT NULL ); - -- --- Name: migration_20260316100000_created_entity_types; Type: TABLE; Schema: public; Owner: - +-- Name: TABLE mcp_sessions; Type: COMMENT; Schema: public; Owner: - -- -CREATE TABLE public.migration_20260316100000_created_entity_types ( - entity_type_id integer CONSTRAINT migration_20260316100000_created_entity_entity_type_id_not_null NOT NULL -); - +COMMENT ON TABLE public.mcp_sessions IS 'Persisted MCP streamable HTTP sessions for restart and cross-replica recovery'; -- --- Name: migration_20260316100000_deleted_default_entity_types; Type: TABLE; Schema: public; Owner: - +-- Name: member; Type: TABLE; Schema: public; Owner: - -- -CREATE TABLE public.migration_20260316100000_deleted_default_entity_types ( - id integer CONSTRAINT migration_20260316100000_deleted_default_entity_typ_id_not_null NOT NULL, - slug text CONSTRAINT migration_20260316100000_deleted_default_entity_t_slug_not_null NOT NULL, - name text CONSTRAINT migration_20260316100000_deleted_default_entity_t_name_not_null NOT NULL, - description text, - icon text, - color text, - metadata_schema jsonb, - organization_id text CONSTRAINT migration_20260316100000_deleted_defau_organization_id_not_null NOT NULL, - created_by text, - updated_by text, - deleted_at timestamp with time zone, - created_at timestamp with time zone, - updated_at timestamp with time zone, - current_view_template_version_id integer +CREATE TABLE public.member ( + id text NOT NULL, + "organizationId" text NOT NULL, + "userId" text NOT NULL, + role text DEFAULT 'member'::text NOT NULL, + "createdAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL ); - -- --- Name: migration_20260316100000_events_kind_backup; Type: TABLE; Schema: public; Owner: - +-- Name: TABLE member; Type: COMMENT; Schema: public; Owner: - -- -CREATE TABLE public.migration_20260316100000_events_kind_backup ( - event_id bigint NOT NULL, - old_kind text -); - +COMMENT ON TABLE public.member IS 'Junction between user and organization with a role (owner / admin / member). The auth source of truth for permission checks at the org level.'; -- -- Name: namespace; Type: TABLE; Schema: public; Owner: - @@ -1192,46 +1602,17 @@ CREATE TABLE public.namespace ( CONSTRAINT namespace_type_check CHECK ((type = ANY (ARRAY['user'::text, 'organization'::text]))) ); - -- --- Name: notifications; Type: TABLE; Schema: public; Owner: - +-- Name: notification_targets; Type: TABLE; Schema: public; Owner: - -- -CREATE TABLE public.notifications ( - id bigint NOT NULL, - organization_id text NOT NULL, +CREATE TABLE public.notification_targets ( + event_id bigint NOT NULL, user_id text NOT NULL, - type text NOT NULL, - title text NOT NULL, - body text, - resource_type text, - resource_id text, - resource_url text, - is_read boolean DEFAULT false NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT notifications_type_check CHECK ((type = ANY (ARRAY['action_approval_needed'::text, 'connection_permission_request'::text, 'invitation_received'::text, 'generic'::text, 'agent_message'::text]))) + delivered_at timestamp with time zone DEFAULT now() NOT NULL, + read_at timestamp with time zone ); - --- --- Name: notifications_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.notifications_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: notifications_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.notifications_id_seq OWNED BY public.notifications.id; - - -- -- Name: oauth_authorization_codes; Type: TABLE; Schema: public; Owner: - -- @@ -1252,14 +1633,12 @@ CREATE TABLE public.oauth_authorization_codes ( created_at timestamp with time zone DEFAULT now() NOT NULL ); - -- -- Name: TABLE oauth_authorization_codes; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON TABLE public.oauth_authorization_codes IS 'Short-lived authorization codes for PKCE flow'; - -- -- Name: oauth_clients; Type: TABLE; Schema: public; Owner: - -- @@ -1289,14 +1668,12 @@ CREATE TABLE public.oauth_clients ( metadata jsonb DEFAULT '{}'::jsonb NOT NULL ); - -- -- Name: TABLE oauth_clients; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON TABLE public.oauth_clients IS 'OAuth 2.1 dynamic client registration for MCP clients'; - -- -- Name: oauth_device_codes; Type: TABLE; Schema: public; Owner: - -- @@ -1316,13 +1693,23 @@ CREATE TABLE public.oauth_device_codes ( CONSTRAINT oauth_device_codes_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'approved'::text, 'denied'::text, 'expired'::text]))) ); - -- -- Name: TABLE oauth_device_codes; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON TABLE public.oauth_device_codes IS 'Device codes for OAuth Device Authorization Grant (RFC 8628)'; +-- +-- Name: oauth_states; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.oauth_states ( + id text NOT NULL, + scope text NOT NULL, + payload jsonb NOT NULL, + expires_at timestamp with time zone NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); -- -- Name: oauth_tokens; Type: TABLE; Schema: public; Owner: - @@ -1344,13 +1731,11 @@ CREATE TABLE public.oauth_tokens ( CONSTRAINT oauth_tokens_token_type_check CHECK ((token_type = ANY (ARRAY['access'::text, 'refresh'::text]))) ); - -- -- Name: TABLE oauth_tokens; Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON TABLE public.oauth_tokens IS 'Access and refresh tokens issued by OAuth server'; - +COMMENT ON TABLE public.oauth_tokens IS 'OAuth 2.1 access + refresh tokens issued by the gateway. Covers MCP clients (Claude Desktop, Cursor, etc.) and integration consumers. Tied to oauth_clients (the registered app) and the user via subject.'; -- -- Name: organization; Type: TABLE; Schema: public; Owner: - @@ -1365,23 +1750,48 @@ CREATE TABLE public.organization ( metadata text, description text, visibility text DEFAULT 'private'::text NOT NULL, - CONSTRAINT org_slug_not_reserved CHECK ((slug <> ALL (ARRAY['settings'::text, 'auth'::text, 'api'::text, 'templates'::text, 'help'::text, 'account'::text, 'admin'::text, 'health'::text, 'login'::text, 'logout'::text, 'signup'::text, 'register'::text]))) + repair_agents_enabled boolean DEFAULT true NOT NULL, + CONSTRAINT org_slug_not_reserved CHECK ((slug <> ALL (ARRAY['settings'::text, 'auth'::text, 'api'::text, 'templates'::text, 'help'::text, 'account'::text, 'admin'::text, 'health'::text, 'login'::text, 'logout'::text, 'signup'::text, 'register'::text, 'www'::text, 'mcp'::text, 'static'::text, 'assets'::text, 'cdn'::text, 'docs'::text, 'mail'::text]))) ); +-- +-- Name: TABLE organization; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.organization IS 'Tenant root. Every other table scopes to an organization. The slug is the public identifier; id is the immutable key used in joins.'; + +-- +-- Name: passkey; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.passkey ( + id text NOT NULL, + name text, + "publicKey" text NOT NULL, + "userId" text NOT NULL, + "credentialID" text NOT NULL, + counter bigint DEFAULT 0 NOT NULL, + "deviceType" text NOT NULL, + "backedUp" boolean DEFAULT false NOT NULL, + transports text, + "createdAt" timestamp with time zone DEFAULT now() NOT NULL, + aaguid text +); -- --- Name: organization_lobu_links; Type: TABLE; Schema: public; Owner: - +-- Name: pending_interactions; Type: TABLE; Schema: public; Owner: - -- -CREATE TABLE public.organization_lobu_links ( +CREATE TABLE public.pending_interactions ( + id text NOT NULL, organization_id text NOT NULL, - lobu_url text NOT NULL, - created_by text, + connection_id text NOT NULL, + expected_user_id text NOT NULL, + entry_payload jsonb NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL + claimed_at timestamp with time zone ); - -- -- Name: personal_access_tokens; Type: TABLE; Schema: public; Owner: - -- @@ -1399,16 +1809,21 @@ CREATE TABLE public.personal_access_tokens ( last_used_at timestamp with time zone, revoked_at timestamp with time zone, created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL + updated_at timestamp with time zone DEFAULT now() NOT NULL, + worker_id text ); - -- -- Name: TABLE personal_access_tokens; Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON TABLE public.personal_access_tokens IS 'Personal Access Tokens for workers, CLI tools, and MCP clients'; +COMMENT ON TABLE public.personal_access_tokens IS 'PATs for CLI, MCP, and worker bridges. Optional worker_id binding (non-NULL) restricts the token to one device_workers.worker_id, used by child tokens minted via /api/me/devices/mint-child-token. Plain user PATs leave worker_id NULL.'; + +-- +-- Name: COLUMN personal_access_tokens.worker_id; Type: COMMENT; Schema: public; Owner: - +-- +COMMENT ON COLUMN public.personal_access_tokens.worker_id IS 'Optional binding to a specific device_workers.worker_id. Set by /api/me/devices/mint-child-token. When non-NULL, /api/workers/poll requires the request body''s worker_id to match.'; -- -- Name: personal_access_tokens_id_seq; Type: SEQUENCE; Schema: public; Owner: - @@ -1421,26 +1836,32 @@ CREATE SEQUENCE public.personal_access_tokens_id_seq NO MAXVALUE CACHE 1; - -- -- Name: personal_access_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.personal_access_tokens_id_seq OWNED BY public.personal_access_tokens.id; - -- -- Name: rate_limits; Type: TABLE; Schema: public; Owner: - -- CREATE TABLE public.rate_limits ( key text NOT NULL, - updated_at timestamp with time zone DEFAULT now(), - prev_count integer DEFAULT 0, - curr_count integer DEFAULT 0, - window_start bigint DEFAULT 0 + count integer DEFAULT 0 NOT NULL, + window_started_at timestamp with time zone NOT NULL, + expires_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL ); +-- +-- Name: revoked_tokens; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.revoked_tokens ( + jti text NOT NULL, + expires_at timestamp with time zone NOT NULL +); -- -- Name: runs; Type: TABLE; Schema: public; Owner: - @@ -1448,7 +1869,7 @@ CREATE TABLE public.rate_limits ( CREATE TABLE public.runs ( id bigint NOT NULL, - organization_id text NOT NULL, + organization_id text, run_type text NOT NULL, feed_id bigint, connection_id bigint, @@ -1469,11 +1890,34 @@ CREATE TABLE public.runs ( created_at timestamp with time zone DEFAULT now() NOT NULL, watcher_id integer, window_id bigint, + auth_profile_id bigint, + auth_signal jsonb, + created_by_user_id text, + dispatched_message_id text, + output_tail text, + exit_code integer, + exit_signal text, + exit_reason text, + approved_input jsonb, + queue_name text, + idempotency_key text, + attempts integer DEFAULT 0 NOT NULL, + max_attempts integer DEFAULT 3 NOT NULL, + run_at timestamp with time zone DEFAULT now() NOT NULL, + priority integer DEFAULT 0 NOT NULL, + expires_at timestamp with time zone, + retry_delay_seconds integer, CONSTRAINT runs_approval_status_check CHECK ((approval_status = ANY (ARRAY['pending'::text, 'approved'::text, 'rejected'::text, 'auto'::text]))), - CONSTRAINT runs_run_type_check CHECK ((run_type = ANY (ARRAY['sync'::text, 'action'::text, 'code'::text, 'insight'::text, 'watcher'::text, 'embed_backfill'::text]))), + CONSTRAINT runs_legacy_org_required CHECK (((run_type <> ALL (ARRAY['sync'::text, 'action'::text, 'embed_backfill'::text, 'watcher'::text, 'auth'::text])) OR (organization_id IS NOT NULL))), + CONSTRAINT runs_run_type_check CHECK ((run_type = ANY (ARRAY['sync'::text, 'action'::text, 'embed_backfill'::text, 'watcher'::text, 'auth'::text, 'chat_message'::text, 'schedule'::text, 'agent_run'::text, 'internal'::text, 'task'::text]))), CONSTRAINT runs_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'claimed'::text, 'running'::text, 'completed'::text, 'failed'::text, 'cancelled'::text, 'timeout'::text]))) ); +-- +-- Name: TABLE runs; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.runs IS 'Job queue for the gateway. Each row is a single sync / action / embed_backfill / auth / watcher / chat_message / schedule / agent_run / internal / task invocation. status transitions pending → claimed → running → completed | failed | timeout via heartbeat-based reaping.'; -- -- Name: runs_id_seq; Type: SEQUENCE; Schema: public; Owner: - @@ -1486,79 +1930,53 @@ CREATE SEQUENCE public.runs_id_seq NO MAXVALUE CACHE 1; - --- --- Name: runs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.runs_id_seq OWNED BY public.runs.id; - - --- --- --- Name: session; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.session ( - id text NOT NULL, - "expiresAt" timestamp with time zone NOT NULL, - token text NOT NULL, - "createdAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - "updatedAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - "ipAddress" text, - "userAgent" text, - "userId" text NOT NULL, - "activeOrganizationId" text -); - - --- --- Name: source_type_auth_defaults; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.source_type_auth_defaults ( - id bigint NOT NULL, - organization_id text NOT NULL, - crawler_type text NOT NULL, - auth_values jsonb DEFAULT '{}'::jsonb NOT NULL, - created_by text NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL -); - - --- --- Name: source_type_auth_defaults_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- +-- Name: runs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- -CREATE SEQUENCE public.source_type_auth_defaults_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - +ALTER SEQUENCE public.runs_id_seq OWNED BY public.runs.id; -- --- Name: source_type_auth_defaults_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- Name: scheduled_jobs; Type: TABLE; Schema: public; Owner: - -- -ALTER SEQUENCE public.source_type_auth_defaults_id_seq OWNED BY public.source_type_auth_defaults.id; - +CREATE TABLE public.scheduled_jobs ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + organization_id text NOT NULL, + action_type text NOT NULL, + action_args jsonb NOT NULL, + cron text, + next_run_at timestamp with time zone NOT NULL, + last_fired_at timestamp with time zone, + last_fired_run_id bigint, + paused boolean DEFAULT false NOT NULL, + description text NOT NULL, + created_by_user text, + created_by_agent text, + source_run_id bigint, + source_event_id bigint, + source_thread_id text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT scheduled_jobs_attribution_check CHECK (((created_by_user IS NOT NULL) OR (created_by_agent IS NOT NULL))) +); -- --- Name: team; Type: TABLE; Schema: public; Owner: - +-- Name: session; Type: TABLE; Schema: public; Owner: - -- -CREATE TABLE public.team ( +CREATE TABLE public.session ( id text NOT NULL, - "organizationId" text NOT NULL, - name text NOT NULL, + "expiresAt" timestamp with time zone NOT NULL, + token text NOT NULL, "createdAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - "updatedAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL + "updatedAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + "ipAddress" text, + "userAgent" text, + "userId" text NOT NULL, + "activeOrganizationId" text ); - -- -- Name: user; Type: TABLE; Schema: public; Owner: - -- @@ -1577,6 +1995,33 @@ CREATE TABLE public."user" ( CONSTRAINT username_not_reserved CHECK ((username <> ALL (ARRAY['settings'::text, 'auth'::text, 'api'::text, 'templates'::text, 'help'::text, 'account'::text, 'admin'::text, 'health'::text, 'login'::text, 'logout'::text, 'signup'::text, 'register'::text]))) ); +-- +-- Name: TABLE "user"; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public."user" IS 'Better-auth user table — one row per signed-in human. Profile fields (name, email, image) flow from the auth provider. Membership in organizations lives in the member table.'; + +-- +-- Name: user_auth_profiles; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_auth_profiles ( + user_id text NOT NULL, + agent_id text NOT NULL, + profiles jsonb DEFAULT '[]'::jsonb NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + +-- +-- Name: user_model_preferences; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_model_preferences ( + user_id text NOT NULL, + provider_id text NOT NULL, + model text NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); -- -- Name: verification; Type: TABLE; Schema: public; Owner: - @@ -1591,7 +2036,6 @@ CREATE TABLE public.verification ( "updatedAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL ); - -- -- Name: view_template_active_tabs; Type: TABLE; Schema: public; Owner: - -- @@ -1607,7 +2051,6 @@ CREATE TABLE public.view_template_active_tabs ( CONSTRAINT view_template_active_tabs_resource_type_check CHECK ((resource_type = ANY (ARRAY['entity_type'::text, 'entity'::text]))) ); - -- -- Name: view_template_active_tabs_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -1620,14 +2063,12 @@ CREATE SEQUENCE public.view_template_active_tabs_id_seq NO MAXVALUE CACHE 1; - -- -- Name: view_template_active_tabs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.view_template_active_tabs_id_seq OWNED BY public.view_template_active_tabs.id; - -- -- Name: view_template_versions; Type: TABLE; Schema: public; Owner: - -- @@ -1647,7 +2088,6 @@ CREATE TABLE public.view_template_versions ( CONSTRAINT view_template_versions_resource_type_check CHECK ((resource_type = ANY (ARRAY['entity_type'::text, 'entity'::text]))) ); - -- -- Name: view_template_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -1660,14 +2100,12 @@ CREATE SEQUENCE public.view_template_versions_id_seq NO MAXVALUE CACHE 1; - -- -- Name: view_template_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.view_template_versions_id_seq OWNED BY public.view_template_versions.id; - -- -- Name: watcher_reactions; Type: TABLE; Schema: public; Owner: - -- @@ -1686,7 +2124,6 @@ CREATE TABLE public.watcher_reactions ( created_at timestamp with time zone DEFAULT now() ); - -- -- Name: watcher_reactions_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -1698,38 +2135,31 @@ CREATE SEQUENCE public.watcher_reactions_id_seq NO MAXVALUE CACHE 1; - -- -- Name: watcher_reactions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.watcher_reactions_id_seq OWNED BY public.watcher_reactions.id; - -- -- Name: watcher_versions; Type: TABLE; Schema: public; Owner: - -- CREATE TABLE public.watcher_versions ( - id integer CONSTRAINT insight_template_versions_id_not_null NOT NULL, - version integer CONSTRAINT insight_template_versions_version_not_null NOT NULL, - name text CONSTRAINT insight_template_versions_name_not_null NOT NULL, + id integer NOT NULL, + version integer NOT NULL, + name text NOT NULL, description text, change_notes text, - created_by text CONSTRAINT insight_template_versions_created_by_not_null NOT NULL, + created_by text NOT NULL, created_at timestamp with time zone DEFAULT now(), - sources_schema jsonb, keying_config jsonb, json_template jsonb, - prompt text CONSTRAINT insight_template_versions_prompt_not_null NOT NULL, - extraction_schema jsonb CONSTRAINT insight_template_versions_extraction_schema_not_null NOT NULL, + prompt text NOT NULL, + extraction_schema jsonb NOT NULL, classifiers jsonb, - required_source_types text[] DEFAULT '{}'::text[] CONSTRAINT insight_template_versions_required_source_types_not_null NOT NULL, - recommended_source_types text[] DEFAULT '{}'::text[] CONSTRAINT insight_template_versions_recommended_source_types_not_null NOT NULL, - source_repository text, - source_ref text, - source_commit_sha text, - source_path text, + required_source_types text[] DEFAULT '{}'::text[] NOT NULL, + recommended_source_types text[] DEFAULT '{}'::text[] NOT NULL, reactions_guidance text, condensation_prompt text, condensation_window_count integer DEFAULT 4, @@ -1737,77 +2167,60 @@ CREATE TABLE public.watcher_versions ( version_sources jsonb ); - --- --- Name: COLUMN watcher_versions.sources_schema; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watcher_versions.sources_schema IS 'JSON Schema defining expected sources. Example: {required: ["content"], properties: {content: {description: "Main content source"}}}. Validates insight sources match template expectations.'; - - -- -- Name: COLUMN watcher_versions.json_template; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.watcher_versions.json_template IS 'JSON-based template definition for React rendering. Replaces Svelte-based renderer_component.'; - -- -- Name: COLUMN watcher_versions.prompt; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.watcher_versions.prompt IS 'Handlebars prompt template used for insight extraction.'; - -- -- Name: COLUMN watcher_versions.extraction_schema; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.watcher_versions.extraction_schema IS 'JSON Schema defining LLM output structure for this template version.'; - -- -- Name: COLUMN watcher_versions.classifiers; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.watcher_versions.classifiers IS 'Optional classifier definitions attached to this template version.'; - -- -- Name: COLUMN watcher_versions.required_source_types; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.watcher_versions.required_source_types IS 'Source type slugs that must exist for selected source entities before insight creation.'; - -- -- Name: COLUMN watcher_versions.recommended_source_types; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.watcher_versions.recommended_source_types IS 'Source type slugs recommended for better insight quality.'; - -- -- Name: COLUMN watcher_versions.reactions_guidance; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.watcher_versions.reactions_guidance IS 'Optional guidance text for LLM agents on what reactions to take after analyzing a watcher window.'; - -- -- Name: COLUMN watcher_versions.condensation_prompt; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.watcher_versions.condensation_prompt IS 'Handlebars prompt for condensing completed windows into a rollup. Receives {{windows}} array.'; - -- -- Name: COLUMN watcher_versions.condensation_window_count; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.watcher_versions.condensation_window_count IS 'How many leaf windows to condense into one rollup. Default 4.'; - -- -- Name: watcher_template_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -1819,26 +2232,23 @@ CREATE SEQUENCE public.watcher_template_versions_id_seq NO MAXVALUE CACHE 1; - -- -- Name: watcher_template_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.watcher_template_versions_id_seq OWNED BY public.watcher_versions.id; - -- -- Name: watcher_window_events; Type: TABLE; Schema: public; Owner: - -- CREATE TABLE public.watcher_window_events ( - id bigint CONSTRAINT insight_window_content_id_not_null NOT NULL, - window_id bigint CONSTRAINT insight_window_content_window_id_not_null NOT NULL, - event_id bigint CONSTRAINT insight_window_content_content_id_not_null NOT NULL, + id bigint NOT NULL, + window_id bigint NOT NULL, + event_id bigint NOT NULL, created_at timestamp with time zone DEFAULT now() ); - -- -- Name: watcher_window_content_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -1850,27 +2260,60 @@ CREATE SEQUENCE public.watcher_window_content_id_seq NO MAXVALUE CACHE 1; - -- -- Name: watcher_window_content_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.watcher_window_content_id_seq OWNED BY public.watcher_window_events.id; +-- +-- Name: watcher_window_field_feedback; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.watcher_window_field_feedback ( + id bigint NOT NULL, + window_id integer NOT NULL, + watcher_id integer NOT NULL, + organization_id text NOT NULL, + field_path text NOT NULL, + mutation text DEFAULT 'set'::text NOT NULL, + corrected_value jsonb, + note text, + created_by text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT watcher_window_field_feedback_mutation_check CHECK ((mutation = ANY (ARRAY['set'::text, 'remove'::text, 'add'::text]))) +); + +-- +-- Name: watcher_window_field_feedback_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.watcher_window_field_feedback_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- +-- Name: watcher_window_field_feedback_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.watcher_window_field_feedback_id_seq OWNED BY public.watcher_window_field_feedback.id; -- -- Name: watcher_windows; Type: TABLE; Schema: public; Owner: - -- CREATE TABLE public.watcher_windows ( - id integer CONSTRAINT insight_windows_id_not_null NOT NULL, - watcher_id integer CONSTRAINT insight_windows_insight_id_not_null NOT NULL, + id integer NOT NULL, + watcher_id integer NOT NULL, parent_window_id integer, - granularity text CONSTRAINT insight_windows_granularity_not_null NOT NULL, - window_start timestamp with time zone CONSTRAINT insight_windows_window_start_not_null NOT NULL, - window_end timestamp with time zone CONSTRAINT insight_windows_window_end_not_null NOT NULL, - content_analyzed integer CONSTRAINT insight_windows_content_analyzed_not_null NOT NULL, - extracted_data jsonb CONSTRAINT insight_windows_extracted_data_not_null NOT NULL, + granularity text NOT NULL, + window_start timestamp with time zone NOT NULL, + window_end timestamp with time zone NOT NULL, + content_analyzed integer NOT NULL, + extracted_data jsonb NOT NULL, model_used text, execution_time_ms integer, is_rollup boolean DEFAULT false, @@ -1879,31 +2322,28 @@ CREATE TABLE public.watcher_windows ( version_id integer, depth integer DEFAULT 0, client_id text, - run_metadata jsonb DEFAULT '{}'::jsonb NOT NULL + run_metadata jsonb DEFAULT '{}'::jsonb NOT NULL, + run_id bigint ); - -- -- Name: TABLE watcher_windows; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON TABLE public.watcher_windows IS 'Time-series watcher results with hierarchical rollups'; - -- -- Name: COLUMN watcher_windows.parent_window_id; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.watcher_windows.parent_window_id IS 'Rollup hierarchy (daily->weekly->monthly->quarterly)'; - -- -- Name: COLUMN watcher_windows.depth; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.watcher_windows.depth IS 'Condensation depth: 0=leaf, 1+=rollup tiers'; - -- -- Name: watcher_windows_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -1915,47 +2355,58 @@ CREATE SEQUENCE public.watcher_windows_id_seq NO MAXVALUE CACHE 1; - -- -- Name: watcher_windows_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.watcher_windows_id_seq OWNED BY public.watcher_windows.id; - -- -- Name: watchers; Type: TABLE; Schema: public; Owner: - -- CREATE TABLE public.watchers ( - id integer CONSTRAINT insights_id_not_null NOT NULL, + id integer NOT NULL, model_config jsonb DEFAULT '{}'::jsonb, - status text DEFAULT 'active'::text, + status text DEFAULT 'active'::text NOT NULL, created_at timestamp with time zone DEFAULT now(), updated_at timestamp with time zone DEFAULT now(), sources jsonb DEFAULT '[]'::jsonb, - created_by text CONSTRAINT insights_created_by_not_null NOT NULL, + created_by text NOT NULL, entity_ids bigint[], reaction_script text, reaction_script_compiled text, - organization_id text, + organization_id text NOT NULL, name text, slug text, description text, version integer DEFAULT 1, tags text[] DEFAULT '{}'::text[], - registry_type text, - registry_repo text, - registry_ref text, current_version_id integer, schedule text, next_run_at timestamp with time zone, - agent_id text, + agent_id text NOT NULL, connection_id text, scheduler_client_id text, - CONSTRAINT insights_status_check CHECK ((status = ANY (ARRAY['active'::text, 'archived'::text]))) + source_watcher_id integer, + watcher_group_id integer NOT NULL, + device_worker_id uuid, + agent_kind text, + notification_channel text DEFAULT 'canvas'::text NOT NULL, + notification_priority text DEFAULT 'normal'::text NOT NULL, + min_cooldown_seconds integer DEFAULT 0 NOT NULL, + last_fired_at timestamp with time zone, + CONSTRAINT insights_status_check CHECK ((status = ANY (ARRAY['active'::text, 'archived'::text]))), + CONSTRAINT watchers_min_cooldown_seconds_nonneg CHECK ((min_cooldown_seconds >= 0)), + CONSTRAINT watchers_notification_channel_check CHECK ((notification_channel = ANY (ARRAY['canvas'::text, 'notification'::text, 'both'::text]))), + CONSTRAINT watchers_notification_priority_check CHECK ((notification_priority = ANY (ARRAY['low'::text, 'normal'::text, 'high'::text]))) ); +-- +-- Name: TABLE watchers; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.watchers IS 'Per-agent scheduled jobs with a cron-style cadence. Each tick enqueues a runs row of run_type=''watcher''. The runs row is the unit of execution; this table is the schedule definition.'; -- -- Name: COLUMN watchers.status; Type: COMMENT; Schema: public; Owner: - @@ -1963,28 +2414,24 @@ CREATE TABLE public.watchers ( COMMENT ON COLUMN public.watchers.status IS 'Status of insight: active (recurring), paused (recurring), failed (recurring), completed (one-off)'; - -- -- Name: COLUMN watchers.sources; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.watchers.sources IS 'Array of data sources: [{name: string, entity_id: number, filters: {min_score?, platforms?, search_query?}}]. Each source defines an entity and its filters for content fetching.'; - -- -- Name: COLUMN watchers.reaction_script; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.watchers.reaction_script IS 'TypeScript source for automated reaction script.'; - -- -- Name: COLUMN watchers.reaction_script_compiled; Type: COMMENT; Schema: public; Owner: - -- COMMENT ON COLUMN public.watchers.reaction_script_compiled IS 'Compiled JavaScript from reaction_script.'; - -- -- Name: watchers_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -1996,106 +2443,29 @@ CREATE SEQUENCE public.watchers_id_seq NO MAXVALUE CACHE 1; - -- -- Name: watchers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public.watchers_id_seq OWNED BY public.watchers.id; - --- --- Name: workers; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.workers ( - worker_id text NOT NULL, - status text DEFAULT 'idle'::text NOT NULL, - last_heartbeat_at timestamp with time zone DEFAULT now() NOT NULL, - registered_at timestamp with time zone DEFAULT now() NOT NULL, - capabilities jsonb DEFAULT '{}'::jsonb NOT NULL, - region text, - version text, - active_jobs integer DEFAULT 0 NOT NULL, - total_jobs_claimed integer DEFAULT 0 NOT NULL, - total_jobs_completed integer DEFAULT 0 NOT NULL, - total_jobs_failed integer DEFAULT 0 NOT NULL, - created_at timestamp with time zone DEFAULT now(), - updated_at timestamp with time zone DEFAULT now(), - user_id text, - browser_type text DEFAULT 'playwright'::text, - device_name text, - CONSTRAINT workers_browser_type_check CHECK ((browser_type = ANY (ARRAY['playwright'::text, 'extension'::text]))), - CONSTRAINT workers_status_check CHECK ((status = ANY (ARRAY['active'::text, 'idle'::text, 'offline'::text]))) -); - - --- --- Name: TABLE workers; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON TABLE public.workers IS 'Real-time registry of all workers (local, Lambda, external) with heartbeat tracking'; - - --- --- Name: COLUMN workers.worker_id; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.workers.worker_id IS 'Unique worker identifier (e.g., "local-worker-1", "lambda-us-east-1-abc")'; - - --- --- Name: COLUMN workers.status; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.workers.status IS 'Current status: active (has jobs), idle (online but no jobs), offline (stale heartbeat)'; - - --- --- Name: COLUMN workers.capabilities; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.workers.capabilities IS 'Worker capabilities: {"browser": true, "browser_type": "playwright", "max_execution_time_ms": 600000}'; - - --- --- Name: COLUMN workers.active_jobs; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.workers.active_jobs IS 'Current number of jobs being processed by this worker'; - - --- --- Name: COLUMN workers.user_id; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.workers.user_id IS 'User ID for extension workers (NULL for server workers)'; - - -- --- Name: COLUMN workers.browser_type; Type: COMMENT; Schema: public; Owner: - +-- Name: agent_transcript_snapshot id; Type: DEFAULT; Schema: public; Owner: - -- -COMMENT ON COLUMN public.workers.browser_type IS 'Browser type: playwright (server) or extension (user browser)'; - +ALTER TABLE ONLY public.agent_transcript_snapshot ALTER COLUMN id SET DEFAULT nextval('public.agent_transcript_snapshot_id_seq'::regclass); -- --- Name: COLUMN workers.device_name; Type: COMMENT; Schema: public; Owner: - +-- Name: chat_state_lists seq; Type: DEFAULT; Schema: public; Owner: - -- -COMMENT ON COLUMN public.workers.device_name IS 'User-friendly device name for extension workers'; - +ALTER TABLE ONLY public.chat_state_lists ALTER COLUMN seq SET DEFAULT nextval('public.chat_state_lists_seq_seq'::regclass); -- --- Name: workspace_settings; Type: TABLE; Schema: public; Owner: - +-- Name: chat_state_queues seq; Type: DEFAULT; Schema: public; Owner: - -- -CREATE TABLE public.workspace_settings ( - organization_id text NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL -); - +ALTER TABLE ONLY public.chat_state_queues ALTER COLUMN seq SET DEFAULT nextval('public.chat_state_queues_seq_seq'::regclass); -- -- Name: connect_tokens id; Type: DEFAULT; Schema: public; Owner: - @@ -2103,34 +2473,35 @@ CREATE TABLE public.workspace_settings ( ALTER TABLE ONLY public.connect_tokens ALTER COLUMN id SET DEFAULT nextval('public.connect_tokens_id_seq'::regclass); - -- -- Name: connections id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.connections ALTER COLUMN id SET DEFAULT nextval('public.connections_id_seq'::regclass); - -- -- Name: connector_definitions id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.connector_definitions ALTER COLUMN id SET DEFAULT nextval('public.connector_definitions_id_seq'::regclass); - -- -- Name: connector_versions id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.connector_versions ALTER COLUMN id SET DEFAULT nextval('public.connector_versions_id_seq'::regclass); - -- -- Name: entities id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.entities ALTER COLUMN id SET DEFAULT nextval('public.entities_id_seq'::regclass); +-- +-- Name: entity_identities id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.entity_identities ALTER COLUMN id SET DEFAULT nextval('public.entity_identities_id_seq'::regclass); -- -- Name: entity_relationship_type_rules id; Type: DEFAULT; Schema: public; Owner: - @@ -2138,132 +2509,107 @@ ALTER TABLE ONLY public.entities ALTER COLUMN id SET DEFAULT nextval('public.ent ALTER TABLE ONLY public.entity_relationship_type_rules ALTER COLUMN id SET DEFAULT nextval('public.entity_relationship_type_rules_id_seq'::regclass); - -- -- Name: entity_relationship_types id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.entity_relationship_types ALTER COLUMN id SET DEFAULT nextval('public.entity_relationship_types_id_seq'::regclass); - -- -- Name: entity_relationships id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.entity_relationships ALTER COLUMN id SET DEFAULT nextval('public.entity_relationships_id_seq'::regclass); - -- -- Name: entity_type_audit id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.entity_type_audit ALTER COLUMN id SET DEFAULT nextval('public.entity_type_audit_id_seq'::regclass); - -- -- Name: entity_types id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.entity_types ALTER COLUMN id SET DEFAULT nextval('public.entity_types_id_seq'::regclass); - -- -- Name: event_classifications id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.event_classifications ALTER COLUMN id SET DEFAULT nextval('public.event_classifications_id_seq'::regclass); - -- -- Name: event_classifier_versions id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.event_classifier_versions ALTER COLUMN id SET DEFAULT nextval('public.event_classifier_versions_id_seq'::regclass); - -- -- Name: event_classifiers id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.event_classifiers ALTER COLUMN id SET DEFAULT nextval('public.event_classifiers_id_seq'::regclass); - -- -- Name: events id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.events ALTER COLUMN id SET DEFAULT nextval('public.content_id_seq'::regclass); - -- -- Name: feeds id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.feeds ALTER COLUMN id SET DEFAULT nextval('public.feeds_id_seq'::regclass); - --- --- Name: notifications id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.notifications ALTER COLUMN id SET DEFAULT nextval('public.notifications_id_seq'::regclass); - - -- -- Name: personal_access_tokens id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.personal_access_tokens ALTER COLUMN id SET DEFAULT nextval('public.personal_access_tokens_id_seq'::regclass); - -- -- Name: runs id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.runs ALTER COLUMN id SET DEFAULT nextval('public.runs_id_seq'::regclass); - --- --- Name: source_type_auth_defaults id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.source_type_auth_defaults ALTER COLUMN id SET DEFAULT nextval('public.source_type_auth_defaults_id_seq'::regclass); - - -- -- Name: view_template_active_tabs id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.view_template_active_tabs ALTER COLUMN id SET DEFAULT nextval('public.view_template_active_tabs_id_seq'::regclass); - -- -- Name: view_template_versions id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.view_template_versions ALTER COLUMN id SET DEFAULT nextval('public.view_template_versions_id_seq'::regclass); - -- -- Name: watcher_reactions id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.watcher_reactions ALTER COLUMN id SET DEFAULT nextval('public.watcher_reactions_id_seq'::regclass); - -- -- Name: watcher_versions id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.watcher_versions ALTER COLUMN id SET DEFAULT nextval('public.watcher_template_versions_id_seq'::regclass); - -- -- Name: watcher_window_events id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.watcher_window_events ALTER COLUMN id SET DEFAULT nextval('public.watcher_window_content_id_seq'::regclass); +-- +-- Name: watcher_window_field_feedback id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.watcher_window_field_feedback ALTER COLUMN id SET DEFAULT nextval('public.watcher_window_field_feedback_id_seq'::regclass); -- -- Name: watcher_windows id; Type: DEFAULT; Schema: public; Owner: - @@ -2271,14 +2617,12 @@ ALTER TABLE ONLY public.watcher_window_events ALTER COLUMN id SET DEFAULT nextva ALTER TABLE ONLY public.watcher_windows ALTER COLUMN id SET DEFAULT nextval('public.watcher_windows_id_seq'::regclass); - -- -- Name: watchers id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.watchers ALTER COLUMN id SET DEFAULT nextval('public.watchers_id_seq'::regclass); - -- -- Name: account account_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2286,7 +2630,6 @@ ALTER TABLE ONLY public.watchers ALTER COLUMN id SET DEFAULT nextval('public.wat ALTER TABLE ONLY public.account ADD CONSTRAINT account_pkey PRIMARY KEY (id); - -- -- Name: agent_channel_bindings agent_channel_bindings_platform_channel_id_team_id_key; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2294,7 +2637,6 @@ ALTER TABLE ONLY public.account ALTER TABLE ONLY public.agent_channel_bindings ADD CONSTRAINT agent_channel_bindings_platform_channel_id_team_id_key UNIQUE (platform, channel_id, team_id); - -- -- Name: agent_connections agent_connections_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2302,46 +2644,110 @@ ALTER TABLE ONLY public.agent_channel_bindings ALTER TABLE ONLY public.agent_connections ADD CONSTRAINT agent_connections_pkey PRIMARY KEY (id); +-- +-- Name: agent_grants agent_grants_org_agent_pattern_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.agent_grants + ADD CONSTRAINT agent_grants_org_agent_pattern_key UNIQUE (organization_id, agent_id, pattern); -- --- Name: agent_grants agent_grants_agent_id_pattern_key; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: agent_grants agent_grants_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.agent_grants - ADD CONSTRAINT agent_grants_agent_id_pattern_key UNIQUE (agent_id, pattern); + ADD CONSTRAINT agent_grants_pkey PRIMARY KEY (id); + +-- +-- Name: agent_secrets agent_secrets_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.agent_secrets + ADD CONSTRAINT agent_secrets_pkey PRIMARY KEY (organization_id, name); + +-- +-- Name: agent_transcript_snapshot agent_transcript_snapshot_organization_id_agent_id_conversa_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.agent_transcript_snapshot + ADD CONSTRAINT agent_transcript_snapshot_organization_id_agent_id_conversa_key UNIQUE (organization_id, agent_id, conversation_id, run_id); + +-- +-- Name: agent_transcript_snapshot agent_transcript_snapshot_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.agent_transcript_snapshot + ADD CONSTRAINT agent_transcript_snapshot_pkey PRIMARY KEY (id); + +-- +-- Name: agent_users agent_users_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.agent_users + ADD CONSTRAINT agent_users_pkey PRIMARY KEY (organization_id, agent_id, platform, user_id); + +-- +-- Name: agents agents_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.agents + ADD CONSTRAINT agents_pkey PRIMARY KEY (organization_id, id); + +-- +-- Name: auth_profiles auth_profiles_org_id_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.auth_profiles + ADD CONSTRAINT auth_profiles_org_id_unique UNIQUE (organization_id, id); + +-- +-- Name: auth_profiles auth_profiles_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.auth_profiles + ADD CONSTRAINT auth_profiles_pkey PRIMARY KEY (id); + +-- +-- Name: chat_state_cache chat_state_cache_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.chat_state_cache + ADD CONSTRAINT chat_state_cache_pkey PRIMARY KEY (key_prefix, cache_key); + +-- +-- Name: chat_state_lists chat_state_lists_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- +ALTER TABLE ONLY public.chat_state_lists + ADD CONSTRAINT chat_state_lists_pkey PRIMARY KEY (key_prefix, list_key, seq); -- --- Name: agent_grants agent_grants_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: chat_state_locks chat_state_locks_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.agent_grants - ADD CONSTRAINT agent_grants_pkey PRIMARY KEY (id); - +ALTER TABLE ONLY public.chat_state_locks + ADD CONSTRAINT chat_state_locks_pkey PRIMARY KEY (key_prefix, thread_id); -- --- Name: agent_users agent_users_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: chat_state_queues chat_state_queues_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.agent_users - ADD CONSTRAINT agent_users_pkey PRIMARY KEY (agent_id, platform, user_id); - +ALTER TABLE ONLY public.chat_state_queues + ADD CONSTRAINT chat_state_queues_pkey PRIMARY KEY (key_prefix, thread_id, seq); -- --- Name: agents agents_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: chat_state_subscriptions chat_state_subscriptions_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.agents - ADD CONSTRAINT agents_pkey PRIMARY KEY (id); - +ALTER TABLE ONLY public.chat_state_subscriptions + ADD CONSTRAINT chat_state_subscriptions_pkey PRIMARY KEY (key_prefix, thread_id); -- --- Name: auth_profiles auth_profiles_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: chat_user_identities chat_user_identities_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.auth_profiles - ADD CONSTRAINT auth_profiles_pkey PRIMARY KEY (id); - +ALTER TABLE ONLY public.chat_user_identities + ADD CONSTRAINT chat_user_identities_pkey PRIMARY KEY (platform, team_id, platform_user_id); -- -- Name: connect_tokens connect_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - @@ -2350,7 +2756,6 @@ ALTER TABLE ONLY public.auth_profiles ALTER TABLE ONLY public.connect_tokens ADD CONSTRAINT connect_tokens_pkey PRIMARY KEY (id); - -- -- Name: connect_tokens connect_tokens_token_key; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2358,7 +2763,6 @@ ALTER TABLE ONLY public.connect_tokens ALTER TABLE ONLY public.connect_tokens ADD CONSTRAINT connect_tokens_token_key UNIQUE (token); - -- -- Name: connections connections_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2366,7 +2770,6 @@ ALTER TABLE ONLY public.connect_tokens ALTER TABLE ONLY public.connections ADD CONSTRAINT connections_pkey PRIMARY KEY (id); - -- -- Name: connector_definitions connector_definitions_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2374,7 +2777,6 @@ ALTER TABLE ONLY public.connections ALTER TABLE ONLY public.connector_definitions ADD CONSTRAINT connector_definitions_pkey PRIMARY KEY (id); - -- -- Name: connector_versions connector_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2382,14 +2784,12 @@ ALTER TABLE ONLY public.connector_definitions ALTER TABLE ONLY public.connector_versions ADD CONSTRAINT connector_versions_pkey PRIMARY KEY (id); - -- --- Name: entities entities_organization_id_entity_type_slug_parent_id_key; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: device_workers device_workers_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.entities - ADD CONSTRAINT entities_organization_id_entity_type_slug_parent_id_key UNIQUE (organization_id, entity_type, slug, parent_id); - +ALTER TABLE ONLY public.device_workers + ADD CONSTRAINT device_workers_pkey PRIMARY KEY (user_id, worker_id); -- -- Name: entities entities_pkey; Type: CONSTRAINT; Schema: public; Owner: - @@ -2398,6 +2798,12 @@ ALTER TABLE ONLY public.entities ALTER TABLE ONLY public.entities ADD CONSTRAINT entities_pkey PRIMARY KEY (id); +-- +-- Name: entity_identities entity_identities_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.entity_identities + ADD CONSTRAINT entity_identities_pkey PRIMARY KEY (id); -- -- Name: entity_relationship_type_rules entity_relationship_type_rules_pkey; Type: CONSTRAINT; Schema: public; Owner: - @@ -2406,7 +2812,6 @@ ALTER TABLE ONLY public.entities ALTER TABLE ONLY public.entity_relationship_type_rules ADD CONSTRAINT entity_relationship_type_rules_pkey PRIMARY KEY (id); - -- -- Name: entity_relationship_types entity_relationship_types_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2414,7 +2819,6 @@ ALTER TABLE ONLY public.entity_relationship_type_rules ALTER TABLE ONLY public.entity_relationship_types ADD CONSTRAINT entity_relationship_types_pkey PRIMARY KEY (id); - -- -- Name: entity_relationships entity_relationships_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2422,7 +2826,6 @@ ALTER TABLE ONLY public.entity_relationship_types ALTER TABLE ONLY public.entity_relationships ADD CONSTRAINT entity_relationships_pkey PRIMARY KEY (id); - -- -- Name: entity_type_audit entity_type_audit_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2430,7 +2833,6 @@ ALTER TABLE ONLY public.entity_relationships ALTER TABLE ONLY public.entity_type_audit ADD CONSTRAINT entity_type_audit_pkey PRIMARY KEY (id); - -- -- Name: entity_types entity_types_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2438,7 +2840,6 @@ ALTER TABLE ONLY public.entity_type_audit ALTER TABLE ONLY public.entity_types ADD CONSTRAINT entity_types_pkey PRIMARY KEY (id); - -- -- Name: event_classifications event_classifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2446,7 +2847,6 @@ ALTER TABLE ONLY public.entity_types ALTER TABLE ONLY public.event_classifications ADD CONSTRAINT event_classifications_pkey PRIMARY KEY (id); - -- -- Name: event_classifier_versions event_classifier_versions_classifier_id_version_key; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2454,7 +2854,6 @@ ALTER TABLE ONLY public.event_classifications ALTER TABLE ONLY public.event_classifier_versions ADD CONSTRAINT event_classifier_versions_classifier_id_version_key UNIQUE (classifier_id, version); - -- -- Name: event_classifier_versions event_classifier_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2462,7 +2861,6 @@ ALTER TABLE ONLY public.event_classifier_versions ALTER TABLE ONLY public.event_classifier_versions ADD CONSTRAINT event_classifier_versions_pkey PRIMARY KEY (id); - -- -- Name: event_classifiers event_classifiers_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2470,22 +2868,12 @@ ALTER TABLE ONLY public.event_classifier_versions ALTER TABLE ONLY public.event_classifiers ADD CONSTRAINT event_classifiers_pkey PRIMARY KEY (id); - -- -- Name: event_classifiers event_classifiers_unique_per_insight; Type: CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.event_classifiers - ADD CONSTRAINT event_classifiers_unique_per_insight UNIQUE (entity_id, watcher_id, slug); - - --- --- Name: events event_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.events - ADD CONSTRAINT event_pkey PRIMARY KEY (id); - + ADD CONSTRAINT event_classifiers_unique_per_insight UNIQUE NULLS NOT DISTINCT (entity_id, watcher_id, slug); -- -- Name: event_embeddings event_embeddings_pkey; Type: CONSTRAINT; Schema: public; Owner: - @@ -2494,6 +2882,12 @@ ALTER TABLE ONLY public.events ALTER TABLE ONLY public.event_embeddings ADD CONSTRAINT event_embeddings_pkey PRIMARY KEY (event_id); +-- +-- Name: events event_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.events + ADD CONSTRAINT event_pkey PRIMARY KEY (id); -- -- Name: feeds feeds_pkey; Type: CONSTRAINT; Schema: public; Owner: - @@ -2502,6 +2896,12 @@ ALTER TABLE ONLY public.event_embeddings ALTER TABLE ONLY public.feeds ADD CONSTRAINT feeds_pkey PRIMARY KEY (id); +-- +-- Name: grants grants_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.grants + ADD CONSTRAINT grants_pkey PRIMARY KEY (organization_id, agent_id, kind, pattern); -- -- Name: watcher_versions insight_template_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: - @@ -2510,7 +2910,6 @@ ALTER TABLE ONLY public.feeds ALTER TABLE ONLY public.watcher_versions ADD CONSTRAINT insight_template_versions_pkey PRIMARY KEY (id); - -- -- Name: watcher_window_events insight_window_content_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2518,7 +2917,6 @@ ALTER TABLE ONLY public.watcher_versions ALTER TABLE ONLY public.watcher_window_events ADD CONSTRAINT insight_window_content_pkey PRIMARY KEY (id); - -- -- Name: watcher_window_events insight_window_content_window_id_content_id_key; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2526,7 +2924,6 @@ ALTER TABLE ONLY public.watcher_window_events ALTER TABLE ONLY public.watcher_window_events ADD CONSTRAINT insight_window_content_window_id_content_id_key UNIQUE (window_id, event_id); - -- -- Name: watcher_windows insight_windows_insight_id_granularity_window_start_key; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2534,7 +2931,6 @@ ALTER TABLE ONLY public.watcher_window_events ALTER TABLE ONLY public.watcher_windows ADD CONSTRAINT insight_windows_insight_id_granularity_window_start_key UNIQUE (watcher_id, granularity, window_start); - -- -- Name: watcher_windows insight_windows_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2542,7 +2938,6 @@ ALTER TABLE ONLY public.watcher_windows ALTER TABLE ONLY public.watcher_windows ADD CONSTRAINT insight_windows_pkey PRIMARY KEY (id); - -- -- Name: watchers insights_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2550,7 +2945,6 @@ ALTER TABLE ONLY public.watcher_windows ALTER TABLE ONLY public.watchers ADD CONSTRAINT insights_pkey PRIMARY KEY (id); - -- -- Name: invitation invitation_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2558,7 +2952,6 @@ ALTER TABLE ONLY public.watchers ALTER TABLE ONLY public.invitation ADD CONSTRAINT invitation_pkey PRIMARY KEY (id); - -- -- Name: latest_event_classifications latest_event_classifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2566,6 +2959,12 @@ ALTER TABLE ONLY public.invitation ALTER TABLE ONLY public.latest_event_classifications ADD CONSTRAINT latest_event_classifications_pkey PRIMARY KEY (event_id, classifier_id); +-- +-- Name: mcp_sessions mcp_sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mcp_sessions + ADD CONSTRAINT mcp_sessions_pkey PRIMARY KEY (session_id); -- -- Name: member member_organizationId_userId_key; Type: CONSTRAINT; Schema: public; Owner: - @@ -2574,7 +2973,6 @@ ALTER TABLE ONLY public.latest_event_classifications ALTER TABLE ONLY public.member ADD CONSTRAINT "member_organizationId_userId_key" UNIQUE ("organizationId", "userId"); - -- -- Name: member member_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2582,39 +2980,6 @@ ALTER TABLE ONLY public.member ALTER TABLE ONLY public.member ADD CONSTRAINT member_pkey PRIMARY KEY (id); - --- --- Name: migration_20260315300000_entity_type_org_backfill migration_20260315300000_entity_type_org_backfill_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.migration_20260315300000_entity_type_org_backfill - ADD CONSTRAINT migration_20260315300000_entity_type_org_backfill_pkey PRIMARY KEY (entity_type_id); - - --- --- Name: migration_20260316100000_created_entity_types migration_20260316100000_created_entity_types_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.migration_20260316100000_created_entity_types - ADD CONSTRAINT migration_20260316100000_created_entity_types_pkey PRIMARY KEY (entity_type_id); - - --- --- Name: migration_20260316100000_deleted_default_entity_types migration_20260316100000_deleted_default_entity_types_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.migration_20260316100000_deleted_default_entity_types - ADD CONSTRAINT migration_20260316100000_deleted_default_entity_types_pkey PRIMARY KEY (id); - - --- --- Name: migration_20260316100000_events_kind_backup migration_20260316100000_events_kind_backup_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.migration_20260316100000_events_kind_backup - ADD CONSTRAINT migration_20260316100000_events_kind_backup_pkey PRIMARY KEY (event_id); - - -- -- Name: namespace namespace_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2622,14 +2987,12 @@ ALTER TABLE ONLY public.migration_20260316100000_events_kind_backup ALTER TABLE ONLY public.namespace ADD CONSTRAINT namespace_pkey PRIMARY KEY (slug); - -- --- Name: notifications notifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: notification_targets notification_targets_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.notifications - ADD CONSTRAINT notifications_pkey PRIMARY KEY (id); - +ALTER TABLE ONLY public.notification_targets + ADD CONSTRAINT notification_targets_pkey PRIMARY KEY (event_id, user_id); -- -- Name: oauth_authorization_codes oauth_authorization_codes_pkey; Type: CONSTRAINT; Schema: public; Owner: - @@ -2638,7 +3001,6 @@ ALTER TABLE ONLY public.notifications ALTER TABLE ONLY public.oauth_authorization_codes ADD CONSTRAINT oauth_authorization_codes_pkey PRIMARY KEY (code); - -- -- Name: oauth_clients oauth_clients_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2646,7 +3008,6 @@ ALTER TABLE ONLY public.oauth_authorization_codes ALTER TABLE ONLY public.oauth_clients ADD CONSTRAINT oauth_clients_pkey PRIMARY KEY (id); - -- -- Name: oauth_device_codes oauth_device_codes_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2654,6 +3015,12 @@ ALTER TABLE ONLY public.oauth_clients ALTER TABLE ONLY public.oauth_device_codes ADD CONSTRAINT oauth_device_codes_pkey PRIMARY KEY (device_code); +-- +-- Name: oauth_states oauth_states_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.oauth_states + ADD CONSTRAINT oauth_states_pkey PRIMARY KEY (id); -- -- Name: oauth_tokens oauth_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - @@ -2662,7 +3029,6 @@ ALTER TABLE ONLY public.oauth_device_codes ALTER TABLE ONLY public.oauth_tokens ADD CONSTRAINT oauth_tokens_pkey PRIMARY KEY (id); - -- -- Name: oauth_tokens oauth_tokens_token_hash_key; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2670,15 +3036,6 @@ ALTER TABLE ONLY public.oauth_tokens ALTER TABLE ONLY public.oauth_tokens ADD CONSTRAINT oauth_tokens_token_hash_key UNIQUE (token_hash); - --- --- Name: organization_lobu_links organization_lobu_links_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.organization_lobu_links - ADD CONSTRAINT organization_lobu_links_pkey PRIMARY KEY (organization_id); - - -- -- Name: organization organization_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2686,7 +3043,6 @@ ALTER TABLE ONLY public.organization_lobu_links ALTER TABLE ONLY public.organization ADD CONSTRAINT organization_pkey PRIMARY KEY (id); - -- -- Name: organization organization_slug_key; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2694,6 +3050,19 @@ ALTER TABLE ONLY public.organization ALTER TABLE ONLY public.organization ADD CONSTRAINT organization_slug_key UNIQUE (slug); +-- +-- Name: passkey passkey_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.passkey + ADD CONSTRAINT passkey_pkey PRIMARY KEY (id); + +-- +-- Name: pending_interactions pending_interactions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.pending_interactions + ADD CONSTRAINT pending_interactions_pkey PRIMARY KEY (id); -- -- Name: personal_access_tokens personal_access_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - @@ -2702,7 +3071,6 @@ ALTER TABLE ONLY public.organization ALTER TABLE ONLY public.personal_access_tokens ADD CONSTRAINT personal_access_tokens_pkey PRIMARY KEY (id); - -- -- Name: personal_access_tokens personal_access_tokens_token_hash_key; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2710,7 +3078,6 @@ ALTER TABLE ONLY public.personal_access_tokens ALTER TABLE ONLY public.personal_access_tokens ADD CONSTRAINT personal_access_tokens_token_hash_key UNIQUE (token_hash); - -- -- Name: rate_limits rate_limits_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2718,6 +3085,12 @@ ALTER TABLE ONLY public.personal_access_tokens ALTER TABLE ONLY public.rate_limits ADD CONSTRAINT rate_limits_pkey PRIMARY KEY (key); +-- +-- Name: revoked_tokens revoked_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.revoked_tokens + ADD CONSTRAINT revoked_tokens_pkey PRIMARY KEY (jti); -- -- Name: runs runs_pkey; Type: CONSTRAINT; Schema: public; Owner: - @@ -2726,8 +3099,13 @@ ALTER TABLE ONLY public.rate_limits ALTER TABLE ONLY public.runs ADD CONSTRAINT runs_pkey PRIMARY KEY (id); - -- +-- Name: scheduled_jobs scheduled_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.scheduled_jobs + ADD CONSTRAINT scheduled_jobs_pkey PRIMARY KEY (id); + -- -- Name: session session_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2735,7 +3113,6 @@ ALTER TABLE ONLY public.runs ALTER TABLE ONLY public.session ADD CONSTRAINT session_pkey PRIMARY KEY (id); - -- -- Name: session session_token_key; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2743,22 +3120,12 @@ ALTER TABLE ONLY public.session ALTER TABLE ONLY public.session ADD CONSTRAINT session_token_key UNIQUE (token); - --- --- Name: source_type_auth_defaults source_type_auth_defaults_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.source_type_auth_defaults - ADD CONSTRAINT source_type_auth_defaults_pkey PRIMARY KEY (id); - - -- --- Name: team team_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: user_auth_profiles user_auth_profiles_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.team - ADD CONSTRAINT team_pkey PRIMARY KEY (id); - +ALTER TABLE ONLY public.user_auth_profiles + ADD CONSTRAINT user_auth_profiles_pkey PRIMARY KEY (user_id, agent_id); -- -- Name: user user_email_key; Type: CONSTRAINT; Schema: public; Owner: - @@ -2767,6 +3134,12 @@ ALTER TABLE ONLY public.team ALTER TABLE ONLY public."user" ADD CONSTRAINT user_email_key UNIQUE (email); +-- +-- Name: user_model_preferences user_model_preferences_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_model_preferences + ADD CONSTRAINT user_model_preferences_pkey PRIMARY KEY (user_id, provider_id); -- -- Name: user user_phoneNumber_key; Type: CONSTRAINT; Schema: public; Owner: - @@ -2775,7 +3148,6 @@ ALTER TABLE ONLY public."user" ALTER TABLE ONLY public."user" ADD CONSTRAINT "user_phoneNumber_key" UNIQUE ("phoneNumber"); - -- -- Name: user user_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2783,7 +3155,6 @@ ALTER TABLE ONLY public."user" ALTER TABLE ONLY public."user" ADD CONSTRAINT user_pkey PRIMARY KEY (id); - -- -- Name: verification verification_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2791,7 +3162,6 @@ ALTER TABLE ONLY public."user" ALTER TABLE ONLY public.verification ADD CONSTRAINT verification_pkey PRIMARY KEY (id); - -- -- Name: view_template_active_tabs view_template_active_tabs_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2799,7 +3169,6 @@ ALTER TABLE ONLY public.verification ALTER TABLE ONLY public.view_template_active_tabs ADD CONSTRAINT view_template_active_tabs_pkey PRIMARY KEY (id); - -- -- Name: view_template_active_tabs view_template_active_tabs_unique; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2807,7 +3176,6 @@ ALTER TABLE ONLY public.view_template_active_tabs ALTER TABLE ONLY public.view_template_active_tabs ADD CONSTRAINT view_template_active_tabs_unique UNIQUE (resource_type, resource_id, organization_id, tab_name); - -- -- Name: view_template_versions view_template_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2815,7 +3183,6 @@ ALTER TABLE ONLY public.view_template_active_tabs ALTER TABLE ONLY public.view_template_versions ADD CONSTRAINT view_template_versions_pkey PRIMARY KEY (id); - -- -- Name: view_template_versions view_template_versions_unique; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2823,7 +3190,6 @@ ALTER TABLE ONLY public.view_template_versions ALTER TABLE ONLY public.view_template_versions ADD CONSTRAINT view_template_versions_unique UNIQUE NULLS NOT DISTINCT (resource_type, resource_id, organization_id, tab_name, version); - -- -- Name: watcher_reactions watcher_reactions_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2831,22 +3197,12 @@ ALTER TABLE ONLY public.view_template_versions ALTER TABLE ONLY public.watcher_reactions ADD CONSTRAINT watcher_reactions_pkey PRIMARY KEY (id); - --- --- Name: workers workers_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.workers - ADD CONSTRAINT workers_pkey PRIMARY KEY (worker_id); - - -- --- Name: workspace_settings workspace_settings_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: watcher_window_field_feedback watcher_window_field_feedback_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.workspace_settings - ADD CONSTRAINT workspace_settings_pkey PRIMARY KEY (organization_id); - +ALTER TABLE ONLY public.watcher_window_field_feedback + ADD CONSTRAINT watcher_window_field_feedback_pkey PRIMARY KEY (id); -- -- Name: account_providerId_accountId_uidx; Type: INDEX; Schema: public; Owner: - @@ -2854,27 +3210,35 @@ ALTER TABLE ONLY public.workspace_settings CREATE UNIQUE INDEX "account_providerId_accountId_uidx" ON public.account USING btree ("providerId", "accountId"); - -- -- Name: account_providerId_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX "account_providerId_idx" ON public.account USING btree ("providerId"); - -- -- Name: account_userId_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX "account_userId_idx" ON public.account USING btree ("userId"); - -- -- Name: agent_channel_bindings_agent_id_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX agent_channel_bindings_agent_id_idx ON public.agent_channel_bindings USING btree (agent_id); +-- +-- Name: agent_channel_bindings_no_team_unique; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX agent_channel_bindings_no_team_unique ON public.agent_channel_bindings USING btree (platform, channel_id) WHERE (team_id IS NULL); + +-- +-- Name: agent_channel_bindings_org_agent_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX agent_channel_bindings_org_agent_idx ON public.agent_channel_bindings USING btree (organization_id, agent_id); -- -- Name: agent_connections_agent_id_idx; Type: INDEX; Schema: public; Owner: - @@ -2882,6 +3246,11 @@ CREATE INDEX agent_channel_bindings_agent_id_idx ON public.agent_channel_binding CREATE INDEX agent_connections_agent_id_idx ON public.agent_connections USING btree (agent_id); +-- +-- Name: agent_connections_org_agent_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX agent_connections_org_agent_idx ON public.agent_connections USING btree (organization_id, agent_id); -- -- Name: agent_connections_platform_idx; Type: INDEX; Schema: public; Owner: - @@ -2889,34 +3258,53 @@ CREATE INDEX agent_connections_agent_id_idx ON public.agent_connections USING bt CREATE INDEX agent_connections_platform_idx ON public.agent_connections USING btree (platform); - -- -- Name: agent_grants_agent_id_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX agent_grants_agent_id_idx ON public.agent_grants USING btree (agent_id); +-- +-- Name: agent_grants_org_agent_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX agent_grants_org_agent_idx ON public.agent_grants USING btree (organization_id, agent_id); -- --- Name: agents_organization_id_idx; Type: INDEX; Schema: public; Owner: - +-- Name: agent_secrets_expires_at_idx; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX agents_organization_id_idx ON public.agents USING btree (organization_id); +CREATE INDEX agent_secrets_expires_at_idx ON public.agent_secrets USING btree (expires_at) WHERE (expires_at IS NOT NULL); + +-- +-- Name: agent_secrets_name_prefix_idx; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX agent_secrets_name_prefix_idx ON public.agent_secrets USING btree (name text_pattern_ops); -- --- Name: agents_parent_connection_id_idx; Type: INDEX; Schema: public; Owner: - +-- Name: agent_secrets_org_id_idx; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX agents_parent_connection_id_idx ON public.agents USING btree (parent_connection_id); +CREATE INDEX agent_secrets_org_id_idx ON public.agent_secrets USING btree (organization_id); + +-- +-- Name: agent_transcript_snapshot_latest; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX agent_transcript_snapshot_latest ON public.agent_transcript_snapshot USING btree (organization_id, agent_id, conversation_id, run_id DESC); -- --- Name: agents_template_agent_id_idx; Type: INDEX; Schema: public; Owner: - +-- Name: agent_users_org_agent_idx; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX agents_template_agent_id_idx ON public.agents USING btree (template_agent_id); +CREATE INDEX agent_users_org_agent_idx ON public.agent_users USING btree (organization_id, agent_id); + +-- +-- Name: agents_organization_id_idx; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX agents_organization_id_idx ON public.agents USING btree (organization_id); -- -- Name: auth_profiles_connector_kind_idx; Type: INDEX; Schema: public; Owner: - @@ -2924,6 +3312,17 @@ CREATE INDEX agents_template_agent_id_idx ON public.agents USING btree (template CREATE INDEX auth_profiles_connector_kind_idx ON public.auth_profiles USING btree (organization_id, connector_key, profile_kind, status); +-- +-- Name: auth_profiles_default_for_connector_unique; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX auth_profiles_default_for_connector_unique ON public.auth_profiles USING btree (organization_id, connector_key) WHERE (is_default_for_connector AND (profile_kind = 'oauth_app'::text)); + +-- +-- Name: auth_profiles_device_worker_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX auth_profiles_device_worker_idx ON public.auth_profiles USING btree (device_worker_id) WHERE (device_worker_id IS NOT NULL); -- -- Name: auth_profiles_org_slug_unique; Type: INDEX; Schema: public; Owner: - @@ -2931,13 +3330,65 @@ CREATE INDEX auth_profiles_connector_kind_idx ON public.auth_profiles USING btre CREATE UNIQUE INDEX auth_profiles_org_slug_unique ON public.auth_profiles USING btree (organization_id, slug); - -- -- Name: auth_profiles_pending_unique; Type: INDEX; Schema: public; Owner: - -- CREATE UNIQUE INDEX auth_profiles_pending_unique ON public.auth_profiles USING btree (organization_id, connector_key, profile_kind, provider) WHERE (status = 'pending_auth'::text); +-- +-- Name: chat_state_cache_expires_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX chat_state_cache_expires_idx ON public.chat_state_cache USING btree (expires_at); + +-- +-- Name: chat_state_lists_expires_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX chat_state_lists_expires_idx ON public.chat_state_lists USING btree (expires_at); + +-- +-- Name: chat_state_locks_expires_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX chat_state_locks_expires_idx ON public.chat_state_locks USING btree (expires_at); + +-- +-- Name: chat_state_queues_expires_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX chat_state_queues_expires_idx ON public.chat_state_queues USING btree (expires_at); + +-- +-- Name: chat_user_identities_lobu_user_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX chat_user_identities_lobu_user_idx ON public.chat_user_identities USING btree (lobu_user_id); + +-- +-- Name: connections_org_slug_unique; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX connections_org_slug_unique ON public.connections USING btree (organization_id, slug) WHERE (deleted_at IS NULL); + +-- +-- Name: connector_definitions_required_capability_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX connector_definitions_required_capability_idx ON public.connector_definitions USING btree (required_capability) WHERE (required_capability IS NOT NULL); + +-- +-- Name: device_workers_id_key; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX device_workers_id_key ON public.device_workers USING btree (id); + +-- +-- Name: device_workers_user_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX device_workers_user_id_idx ON public.device_workers USING btree (user_id); -- -- Name: entities_slug_idx; Type: INDEX; Schema: public; Owner: - @@ -2945,13 +3396,35 @@ CREATE UNIQUE INDEX auth_profiles_pending_unique ON public.auth_profiles USING b CREATE INDEX entities_slug_idx ON public.entities USING btree (slug); - -- -- Name: entities_slug_parent_unique; Type: INDEX; Schema: public; Owner: - -- CREATE UNIQUE INDEX entities_slug_parent_unique ON public.entities USING btree (organization_id, COALESCE(parent_id, (0)::bigint), slug); +-- +-- Name: feeds_open_repair_thread_uniq; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX feeds_open_repair_thread_uniq ON public.feeds USING btree (id) WHERE (repair_thread_id IS NOT NULL); + +-- +-- Name: grants_agent_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX grants_agent_id_idx ON public.grants USING btree (agent_id); + +-- +-- Name: grants_expires_at_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX grants_expires_at_idx ON public.grants USING btree (expires_at) WHERE (expires_at IS NOT NULL); + +-- +-- Name: grants_org_agent_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX grants_org_agent_idx ON public.grants USING btree (organization_id, agent_id); -- -- Name: idx_cc_classifier_version_id; Type: INDEX; Schema: public; Owner: - @@ -2959,111 +3432,113 @@ CREATE UNIQUE INDEX entities_slug_parent_unique ON public.entities USING btree ( CREATE INDEX idx_cc_classifier_version_id ON public.event_classifications USING btree (classifier_version_id); - -- -- Name: idx_cc_content_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_cc_content_id ON public.event_classifications USING btree (event_id); - -- -- Name: idx_cc_source; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_cc_source ON public.event_classifications USING btree (source); - -- -- Name: idx_cc_unique_per_source; Type: INDEX; Schema: public; Owner: - -- CREATE UNIQUE INDEX idx_cc_unique_per_source ON public.event_classifications USING btree (event_id, classifier_version_id, source, COALESCE(watcher_id, (0)::bigint)); - -- -- Name: idx_cc_values_gin; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_cc_values_gin ON public.event_classifications USING gin ("values"); - -- -- Name: idx_cc_watcher_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_cc_watcher_id ON public.event_classifications USING btree (watcher_id) WHERE (watcher_id IS NOT NULL); - -- -- Name: idx_cc_window_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_cc_window_id ON public.event_classifications USING btree (window_id) WHERE (window_id IS NOT NULL); - -- -- Name: idx_connect_tokens_connection_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_connect_tokens_connection_id ON public.connect_tokens USING btree (connection_id); - -- -- Name: idx_connect_tokens_status_expires; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_connect_tokens_status_expires ON public.connect_tokens USING btree (status, expires_at) WHERE (status = 'pending'::text); - -- -- Name: idx_connect_tokens_token; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_connect_tokens_token ON public.connect_tokens USING btree (token); - -- -- Name: idx_connections_account; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_connections_account ON public.connections USING btree (account_id); - -- -- Name: idx_connections_agent_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_connections_agent_id ON public.connections USING btree (agent_id) WHERE (agent_id IS NOT NULL); - -- -- Name: idx_connections_connector_key; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_connections_connector_key ON public.connections USING btree (connector_key); - -- -- Name: idx_connections_deleted_at; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_connections_deleted_at ON public.connections USING btree (deleted_at) WHERE (deleted_at IS NULL); +-- +-- Name: idx_connections_device_worker_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_connections_device_worker_id ON public.connections USING btree (device_worker_id) WHERE (device_worker_id IS NOT NULL); + +-- +-- Name: idx_connections_entity_ids; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_connections_entity_ids ON public.connections USING gin (entity_ids); + +-- +-- Name: idx_connections_org; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_connections_org ON public.connections USING btree (organization_id); -- --- Name: idx_connections_entity_ids; Type: INDEX; Schema: public; Owner: - +-- Name: idx_connections_org_connector_account_live; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_connections_entity_ids ON public.connections USING gin (entity_ids); - +CREATE UNIQUE INDEX idx_connections_org_connector_account_live ON public.connections USING btree (organization_id, connector_key, account_id) WHERE ((deleted_at IS NULL) AND (account_id IS NOT NULL)); -- --- Name: idx_connections_org; Type: INDEX; Schema: public; Owner: - +-- Name: idx_connections_org_connector_device_live; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_connections_org ON public.connections USING btree (organization_id); - +CREATE UNIQUE INDEX idx_connections_org_connector_device_live ON public.connections USING btree (organization_id, connector_key, device_worker_id) WHERE ((deleted_at IS NULL) AND (device_worker_id IS NOT NULL)); -- -- Name: idx_connections_status; Type: INDEX; Schema: public; Owner: - @@ -3071,41 +3546,35 @@ CREATE INDEX idx_connections_org ON public.connections USING btree (organization CREATE INDEX idx_connections_status ON public.connections USING btree (status); - -- -- Name: idx_connections_visibility; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_connections_visibility ON public.connections USING btree (organization_id, visibility); - -- -- Name: idx_connector_defs_org_key; Type: INDEX; Schema: public; Owner: - -- CREATE UNIQUE INDEX idx_connector_defs_org_key ON public.connector_definitions USING btree (organization_id, key) WHERE ((organization_id IS NOT NULL) AND (status = 'active'::text)); - -- -- Name: idx_connector_defs_status; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_connector_defs_status ON public.connector_definitions USING btree (status); - -- --- Name: idx_connector_defs_system_key; Type: INDEX; Schema: public; Owner: - +-- Name: idx_connector_versions_key_version; Type: INDEX; Schema: public; Owner: - -- -CREATE UNIQUE INDEX idx_connector_defs_system_key ON public.connector_definitions USING btree (key) WHERE ((organization_id IS NULL) AND (status = 'active'::text)); - +CREATE UNIQUE INDEX idx_connector_versions_key_version ON public.connector_versions USING btree (connector_key, version); -- --- Name: idx_connector_versions_key_version; Type: INDEX; Schema: public; Owner: - +-- Name: idx_device_workers_organization_id; Type: INDEX; Schema: public; Owner: - -- -CREATE UNIQUE INDEX idx_connector_versions_key_version ON public.connector_versions USING btree (connector_key, version); - +CREATE INDEX idx_device_workers_organization_id ON public.device_workers USING btree (organization_id) WHERE (organization_id IS NOT NULL); -- -- Name: idx_ec_has_excerpts; Type: INDEX; Schema: public; Owner: - @@ -3113,48 +3582,47 @@ CREATE UNIQUE INDEX idx_connector_versions_key_version ON public.connector_versi CREATE INDEX idx_ec_has_excerpts ON public.event_classifications USING btree (((excerpts <> '{}'::jsonb))) WHERE (excerpts <> '{}'::jsonb); - -- -- Name: idx_entities_by_parent; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_entities_by_parent ON public.entities USING btree (parent_id, id) WHERE (parent_id IS NOT NULL); - -- -- Name: idx_entities_classifiers; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_entities_classifiers ON public.entities USING gin (enabled_classifiers) WHERE (enabled_classifiers IS NOT NULL); - -- -- Name: idx_entities_content_hash; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_entities_content_hash ON public.entities USING btree (organization_id, content_hash) WHERE ((content_hash IS NOT NULL) AND (deleted_at IS NULL)); - -- -- Name: idx_entities_content_tsv; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_entities_content_tsv ON public.entities USING gin (content_tsv) WHERE (deleted_at IS NULL); - -- -- Name: idx_entities_created_by; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_entities_created_by ON public.entities USING btree (created_by); - -- -- Name: idx_entities_embedding; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_entities_embedding ON public.entities USING ivfflat (embedding public.vector_cosine_ops) WITH (lists='100') WHERE ((embedding IS NOT NULL) AND (deleted_at IS NULL)); +-- +-- Name: idx_entities_entity_type_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_entities_entity_type_id ON public.entities USING btree (entity_type_id) WHERE (deleted_at IS NULL); -- -- Name: idx_entities_metadata_domain; Type: INDEX; Schema: public; Owner: - @@ -3162,6 +3630,29 @@ CREATE INDEX idx_entities_embedding ON public.entities USING ivfflat (embedding CREATE UNIQUE INDEX idx_entities_metadata_domain ON public.entities USING btree (((metadata ->> 'domain'::text)), organization_id) WHERE ((metadata ->> 'domain'::text) IS NOT NULL); +-- +-- Name: idx_entities_metadata_email; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_entities_metadata_email ON public.entities USING btree (((metadata ->> 'email'::text)), organization_id) WHERE (((metadata ->> 'email'::text) IS NOT NULL) AND (deleted_at IS NULL)); + +-- +-- Name: idx_entities_metadata_github_handle; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_entities_metadata_github_handle ON public.entities USING btree (((metadata ->> 'github_handle'::text)), organization_id) WHERE (((metadata ->> 'github_handle'::text) IS NOT NULL) AND (deleted_at IS NULL)); + +-- +-- Name: idx_entities_metadata_linkedin_url; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_entities_metadata_linkedin_url ON public.entities USING btree (((metadata ->> 'linkedin_url'::text)), organization_id) WHERE (((metadata ->> 'linkedin_url'::text) IS NOT NULL) AND (deleted_at IS NULL)); + +-- +-- Name: idx_entities_metadata_twitter_handle; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_entities_metadata_twitter_handle ON public.entities USING btree (((metadata ->> 'twitter_handle'::text)), organization_id) WHERE (((metadata ->> 'twitter_handle'::text) IS NOT NULL) AND (deleted_at IS NULL)); -- -- Name: idx_entities_name; Type: INDEX; Schema: public; Owner: - @@ -3169,13 +3660,29 @@ CREATE UNIQUE INDEX idx_entities_metadata_domain ON public.entities USING btree CREATE INDEX idx_entities_name ON public.entities USING btree (lower(name)); - -- -- Name: idx_entities_organization_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_entities_organization_id ON public.entities USING btree (organization_id); +-- +-- Name: idx_entity_identities_by_entity; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_entity_identities_by_entity ON public.entity_identities USING btree (entity_id) WHERE (deleted_at IS NULL); + +-- +-- Name: idx_entity_identities_live_unique; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_entity_identities_live_unique ON public.entity_identities USING btree (organization_id, namespace, identifier) WHERE (deleted_at IS NULL); + +-- +-- Name: idx_entity_identities_lookup; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_entity_identities_lookup ON public.entity_identities USING btree (organization_id, namespace, identifier) WHERE (deleted_at IS NULL); -- -- Name: idx_entity_rel_type_rules_type; Type: INDEX; Schema: public; Owner: - @@ -3183,13 +3690,29 @@ CREATE INDEX idx_entities_organization_id ON public.entities USING btree (organi CREATE INDEX idx_entity_rel_type_rules_type ON public.entity_relationship_type_rules USING btree (relationship_type_id); - -- -- Name: idx_entity_rel_types_org_slug; Type: INDEX; Schema: public; Owner: - -- CREATE UNIQUE INDEX idx_entity_rel_types_org_slug ON public.entity_relationship_types USING btree (organization_id, slug) WHERE (status = 'active'::text); +-- +-- Name: idx_entity_relationship_types_has_auto_create; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_entity_relationship_types_has_auto_create ON public.entity_relationship_types USING btree (((metadata -> 'autoCreateWhen'::text))) WHERE ((metadata ? 'autoCreateWhen'::text) AND (deleted_at IS NULL)); + +-- +-- Name: idx_entity_relationships_derived_from_event; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_entity_relationships_derived_from_event ON public.entity_relationships USING btree ((((metadata -> 'derivedFrom'::text) ->> 'sourceEventId'::text))) WHERE (metadata ? 'derivedFrom'::text); + +-- +-- Name: idx_entity_relationships_derived_from_rule; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_entity_relationships_derived_from_rule ON public.entity_relationships USING btree ((((metadata -> 'derivedFrom'::text) ->> 'relationshipTypeId'::text)), (((metadata -> 'derivedFrom'::text) ->> 'ruleVersion'::text))) WHERE (metadata ? 'derivedFrom'::text); -- -- Name: idx_entity_relationships_from; Type: INDEX; Schema: public; Owner: - @@ -3197,6 +3720,11 @@ CREATE UNIQUE INDEX idx_entity_rel_types_org_slug ON public.entity_relationship_ CREATE INDEX idx_entity_relationships_from ON public.entity_relationships USING btree (from_entity_id); +-- +-- Name: idx_entity_relationships_live_triple; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_entity_relationships_live_triple ON public.entity_relationships USING btree (from_entity_id, to_entity_id, relationship_type_id) WHERE (deleted_at IS NULL); -- -- Name: idx_entity_relationships_org; Type: INDEX; Schema: public; Owner: - @@ -3204,158 +3732,125 @@ CREATE INDEX idx_entity_relationships_from ON public.entity_relationships USING CREATE INDEX idx_entity_relationships_org ON public.entity_relationships USING btree (organization_id); - -- -- Name: idx_entity_relationships_to; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_entity_relationships_to ON public.entity_relationships USING btree (to_entity_id); - -- -- Name: idx_entity_relationships_type; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_entity_relationships_type ON public.entity_relationships USING btree (relationship_type_id); - -- -- Name: idx_entity_type_audit_action; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_entity_type_audit_action ON public.entity_type_audit USING btree (action); - -- -- Name: idx_entity_type_audit_type_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_entity_type_audit_type_id ON public.entity_type_audit USING btree (entity_type_id); - -- -- Name: idx_entity_types_active; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_entity_types_active ON public.entity_types USING btree (id) WHERE (deleted_at IS NULL); - -- -- Name: idx_entity_types_org_slug; Type: INDEX; Schema: public; Owner: - -- CREATE UNIQUE INDEX idx_entity_types_org_slug ON public.entity_types USING btree (organization_id, slug) WHERE ((organization_id IS NOT NULL) AND (deleted_at IS NULL)); - -- -- Name: idx_event_classifications_source; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_event_classifications_source ON public.event_classifications USING btree (source); - -- -- Name: idx_event_classifier_versions_classifier; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_event_classifier_versions_classifier ON public.event_classifier_versions USING btree (classifier_id); - -- -- Name: idx_event_classifier_versions_created_by; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_event_classifier_versions_created_by ON public.event_classifier_versions USING btree (created_by); - -- -- Name: idx_event_classifier_versions_current; Type: INDEX; Schema: public; Owner: - -- CREATE UNIQUE INDEX idx_event_classifier_versions_current ON public.event_classifier_versions USING btree (classifier_id) WHERE (is_current = true); - -- -- Name: idx_event_classifiers_created_by; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_event_classifiers_created_by ON public.event_classifiers USING btree (created_by); - -- -- Name: idx_event_classifiers_entity_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_event_classifiers_entity_id ON public.event_classifiers USING btree (entity_id) WHERE (entity_id IS NOT NULL); - -- -- Name: idx_event_classifiers_entity_ids; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_event_classifiers_entity_ids ON public.event_classifiers USING gin (entity_ids); - -- -- Name: idx_event_classifiers_insight_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_event_classifiers_insight_id ON public.event_classifiers USING btree (watcher_id) WHERE (watcher_id IS NOT NULL); - -- -- Name: idx_event_classifiers_organization_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_event_classifiers_organization_id ON public.event_classifiers USING btree (organization_id); - -- -- Name: idx_event_classifiers_slug; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_event_classifiers_slug ON public.event_classifiers USING btree (slug); - -- -- Name: idx_event_classifiers_status; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_event_classifiers_status ON public.event_classifiers USING btree (status); - --- Name: idx_event_length; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_event_length ON public.events USING btree (source_id, (COALESCE(length(payload_text), 0))); - - --- --- Name: idx_event_source_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_event_source_id ON public.events USING btree (source_id); - - -- -- Name: idx_events_client_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_events_client_id ON public.events USING btree (client_id) WHERE (client_id IS NOT NULL); - -- --- Name: idx_events_connection_origin_id; Type: INDEX; Schema: public; Owner: - +-- Name: idx_events_connection_id; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_events_connection_origin_id ON public.events USING btree (connection_id, origin_id, created_at DESC) WHERE (connection_id IS NOT NULL); +CREATE INDEX idx_events_connection_id ON public.events USING btree (connection_id); -- --- Name: idx_events_connection_id; Type: INDEX; Schema: public; Owner: - +-- Name: idx_events_connection_origin_id; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_events_connection_id ON public.events USING btree (connection_id); - +CREATE INDEX idx_events_connection_origin_id ON public.events USING btree (connection_id, origin_id, created_at DESC) WHERE (connection_id IS NOT NULL); -- -- Name: idx_events_connector_key; Type: INDEX; Schema: public; Owner: - @@ -3363,80 +3858,107 @@ CREATE INDEX idx_events_connection_id ON public.events USING btree (connection_i CREATE INDEX idx_events_connector_key ON public.events USING btree (connector_key); - -- -- Name: idx_events_created_at; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_events_created_at ON public.events USING btree (created_at); - -- -- Name: idx_events_created_by; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_events_created_by ON public.events USING btree (created_by) WHERE (created_by IS NOT NULL); - -- -- Name: idx_events_embedding; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_events_embedding ON public.event_embeddings USING ivfflat (embedding public.vector_cosine_ops) WITH (lists='1000'); - -- -- Name: idx_events_entity_ids; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_events_entity_ids ON public.events USING gin (entity_ids); +-- +-- Name: idx_events_feed_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_events_feed_id ON public.events USING btree (feed_id); -- --- Name: idx_events_entity_ids_occurred_at; Type: INDEX; Schema: public; Owner: - +-- Name: idx_events_identity_fact_account; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_events_entity_ids_occurred_at ON public.events USING btree ((entity_ids[1]), occurred_at DESC, id DESC) WHERE ((entity_ids IS NOT NULL) AND (entity_ids <> '{}'::bigint[])); +CREATE INDEX idx_events_identity_fact_account ON public.events USING btree (connector_key, ((metadata ->> 'providerStableId'::text)), ((metadata ->> 'namespace'::text))) WHERE (semantic_type = 'identity_fact'::text); -- --- Name: idx_events_feed_id; Type: INDEX; Schema: public; Owner: - +-- Name: idx_events_identity_fact_lookup; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_events_feed_id ON public.events USING btree (feed_id); +CREATE INDEX idx_events_identity_fact_lookup ON public.events USING btree (organization_id, ((metadata ->> 'namespace'::text)), ((metadata ->> 'normalizedValue'::text))) WHERE (semantic_type = 'identity_fact'::text); +-- +-- Name: idx_events_lifecycle_changes; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_events_lifecycle_changes ON public.events USING btree (organization_id, created_at) WHERE ((semantic_type = 'change'::text) AND ((metadata ->> 'category'::text) = 'lifecycle'::text)); -- --- Name: idx_events_fulltext; Type: INDEX; Schema: public; Owner: - +-- Name: idx_events_metadata_auth_user_id; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_events_fulltext ON public.events USING gin (to_tsvector('english'::regconfig, COALESCE(payload_text, ''::text))); +CREATE INDEX idx_events_metadata_auth_user_id ON public.events USING btree (((metadata ->> 'auth_user_id'::text))) WHERE (metadata ? 'auth_user_id'::text); + +-- +-- Name: idx_events_metadata_email; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX idx_events_metadata_email ON public.events USING btree (((metadata ->> 'email'::text))) WHERE (metadata ? 'email'::text); -- --- Name: idx_events_semantic_type; Type: INDEX; Schema: public; Owner: - +-- Name: idx_events_metadata_github_login; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_events_semantic_type ON public.events USING btree (semantic_type); +CREATE INDEX idx_events_metadata_github_login ON public.events USING btree (((metadata ->> 'github_login'::text))) WHERE (metadata ? 'github_login'::text); -- --- Name: idx_events_missing_embedding_backfill; Type: INDEX; Schema: public; Owner: - +-- Name: idx_events_metadata_google_contact_id; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_events_missing_embedding_backfill ON public.events USING btree (created_at, id) WHERE ((payload_text IS NOT NULL) AND (payload_text <> ''::text)); +CREATE INDEX idx_events_metadata_google_contact_id ON public.events USING btree (((metadata ->> 'google_contact_id'::text))) WHERE (metadata ? 'google_contact_id'::text); + +-- +-- Name: idx_events_metadata_phone; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX idx_events_metadata_phone ON public.events USING btree (((metadata ->> 'phone'::text))) WHERE (metadata ? 'phone'::text); -- --- Name: idx_events_organization_id; Type: INDEX; Schema: public; Owner: - +-- Name: idx_events_metadata_slack_user_id; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_events_organization_id ON public.events USING btree (organization_id) WHERE (organization_id IS NOT NULL); +CREATE INDEX idx_events_metadata_slack_user_id ON public.events USING btree (((metadata ->> 'slack_user_id'::text))) WHERE (metadata ? 'slack_user_id'::text); + +-- +-- Name: idx_events_metadata_wa_jid; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_events_metadata_wa_jid ON public.events USING btree (((metadata ->> 'wa_jid'::text))) WHERE (metadata ? 'wa_jid'::text); + +-- +-- Name: idx_events_missing_embedding_backfill; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX idx_events_missing_embedding_backfill ON public.events USING btree (created_at, id) WHERE ((payload_text IS NOT NULL) AND (payload_text <> ''::text)); -- --- Name: idx_events_origin_parent_id; Type: INDEX; Schema: public; Owner: - +-- Name: idx_events_organization_id; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_events_origin_parent_id ON public.events USING btree (origin_parent_id); +CREATE INDEX idx_events_organization_id ON public.events USING btree (organization_id) WHERE (organization_id IS NOT NULL); -- -- Name: idx_events_raw_content_trgm; Type: INDEX; Schema: public; Owner: - @@ -3444,41 +3966,35 @@ CREATE INDEX idx_events_origin_parent_id ON public.events USING btree (origin_pa CREATE INDEX idx_events_raw_content_trgm ON public.events USING gin (payload_text public.gin_trgm_ops); - -- -- Name: idx_events_run_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_events_run_id ON public.events USING btree (run_id); - -- --- Name: idx_events_source_embedding; Type: INDEX; Schema: public; Owner: - +-- Name: idx_events_search_tsv; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_events_source_embedding ON public.event_embeddings USING btree (event_id); - +CREATE INDEX idx_events_search_tsv ON public.events USING gin (search_tsv); -- --- Name: idx_events_superseded_by; Type: INDEX; Schema: public; Owner: - +-- Name: idx_events_semantic_type; Type: INDEX; Schema: public; Owner: - -- -CREATE UNIQUE INDEX idx_events_superseded_by ON public.events USING btree (supersedes_event_id) WHERE (supersedes_event_id IS NOT NULL); - +CREATE INDEX idx_events_semantic_type ON public.events USING btree (semantic_type); -- --- Name: idx_events_thread_lookup; Type: INDEX; Schema: public; Owner: - +-- Name: idx_events_source_embedding; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_events_thread_lookup ON public.events USING btree (origin_parent_id, occurred_at) WHERE (origin_parent_id IS NOT NULL); - +CREATE INDEX idx_events_source_embedding ON public.event_embeddings USING btree (event_id); -- --- Name: idx_events_type; Type: INDEX; Schema: public; Owner: - +-- Name: idx_events_superseded_by; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_events_type ON public.events USING btree (origin_type) WHERE (origin_type IS NOT NULL); - +CREATE UNIQUE INDEX idx_events_superseded_by ON public.events USING btree (supersedes_event_id) WHERE (supersedes_event_id IS NOT NULL); -- -- Name: idx_feeds_connection; Type: INDEX; Schema: public; Owner: - @@ -3486,97 +4002,101 @@ CREATE INDEX idx_events_type ON public.events USING btree (origin_type) WHERE (o CREATE INDEX idx_feeds_connection ON public.feeds USING btree (connection_id); - -- -- Name: idx_feeds_deleted_at; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_feeds_deleted_at ON public.feeds USING btree (deleted_at) WHERE (deleted_at IS NULL); - -- -- Name: idx_feeds_entity_ids; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_feeds_entity_ids ON public.feeds USING gin (entity_ids); - -- -- Name: idx_feeds_next_run_at; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_feeds_next_run_at ON public.feeds USING btree (next_run_at) WHERE ((status = 'active'::text) AND (deleted_at IS NULL)); - -- -- Name: idx_feeds_org; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_feeds_org ON public.feeds USING btree (organization_id); - -- -- Name: idx_feeds_status; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_feeds_status ON public.feeds USING btree (status); - -- -- Name: idx_latest_ec_classifier_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_latest_ec_classifier_id ON public.latest_event_classifications USING btree (classifier_id); - -- -- Name: idx_latest_ec_event_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_latest_ec_event_id ON public.latest_event_classifications USING btree (event_id); - -- -- Name: idx_latest_ec_id; Type: INDEX; Schema: public; Owner: - -- CREATE UNIQUE INDEX idx_latest_ec_id ON public.latest_event_classifications USING btree (id); - -- -- Name: idx_latest_ec_source; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_latest_ec_source ON public.latest_event_classifications USING btree (source); - -- -- Name: idx_latest_ec_values_gin; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_latest_ec_values_gin ON public.latest_event_classifications USING gin ("values"); - -- --- Name: idx_notifications_listing; Type: INDEX; Schema: public; Owner: - +-- Name: idx_notification_targets_user_all; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_notifications_listing ON public.notifications USING btree (organization_id, user_id, created_at DESC); +CREATE INDEX idx_notification_targets_user_all ON public.notification_targets USING btree (user_id, delivered_at DESC); + +-- +-- Name: idx_notification_targets_user_unread; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX idx_notification_targets_user_unread ON public.notification_targets USING btree (user_id, delivered_at DESC) WHERE (read_at IS NULL); -- --- Name: idx_notifications_unread; Type: INDEX; Schema: public; Owner: - +-- Name: idx_pending_interactions_created_at; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_notifications_unread ON public.notifications USING btree (organization_id, user_id, is_read, created_at DESC); +CREATE INDEX idx_pending_interactions_created_at ON public.pending_interactions USING btree (created_at); +-- +-- Name: idx_pending_interactions_unclaimed; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_pending_interactions_unclaimed ON public.pending_interactions USING btree (id, organization_id, connection_id, expected_user_id) WHERE (claimed_at IS NULL); -- --- Name: idx_rate_limits_updated_at; Type: INDEX; Schema: public; Owner: - +-- Name: idx_personal_access_tokens_worker_id; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_rate_limits_updated_at ON public.rate_limits USING btree (updated_at); +CREATE INDEX idx_personal_access_tokens_worker_id ON public.personal_access_tokens USING btree (worker_id) WHERE (worker_id IS NOT NULL); + +-- +-- Name: idx_runs_active_auth_per_profile; Type: INDEX; Schema: public; Owner: - +-- +CREATE UNIQUE INDEX idx_runs_active_auth_per_profile ON public.runs USING btree (auth_profile_id) WHERE ((run_type = 'auth'::text) AND (auth_profile_id IS NOT NULL) AND (status = ANY (ARRAY['pending'::text, 'claimed'::text, 'running'::text]))); -- -- Name: idx_runs_active_embed_backfill_per_org; Type: INDEX; Schema: public; Owner: - @@ -3584,13 +4104,17 @@ CREATE INDEX idx_rate_limits_updated_at ON public.rate_limits USING btree (updat CREATE UNIQUE INDEX idx_runs_active_embed_backfill_per_org ON public.runs USING btree (organization_id) WHERE ((run_type = 'embed_backfill'::text) AND (status = ANY (ARRAY['pending'::text, 'claimed'::text, 'running'::text]))); - -- -- Name: idx_runs_active_sync_per_feed; Type: INDEX; Schema: public; Owner: - -- CREATE UNIQUE INDEX idx_runs_active_sync_per_feed ON public.runs USING btree (feed_id) WHERE ((run_type = 'sync'::text) AND (feed_id IS NOT NULL) AND (status = ANY (ARRAY['pending'::text, 'claimed'::text, 'running'::text]))); +-- +-- Name: idx_runs_active_watcher_per_watcher; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_runs_active_watcher_per_watcher ON public.runs USING btree (watcher_id) WHERE ((run_type = 'watcher'::text) AND (watcher_id IS NOT NULL) AND (status = ANY (ARRAY['pending'::text, 'claimed'::text, 'running'::text]))); -- -- Name: idx_runs_connection; Type: INDEX; Schema: public; Owner: - @@ -3598,6 +4122,17 @@ CREATE UNIQUE INDEX idx_runs_active_sync_per_feed ON public.runs USING btree (fe CREATE INDEX idx_runs_connection ON public.runs USING btree (connection_id); +-- +-- Name: idx_runs_created_by_user; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_runs_created_by_user ON public.runs USING btree (created_by_user_id) WHERE (created_by_user_id IS NOT NULL); + +-- +-- Name: idx_runs_dispatched_message_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_runs_dispatched_message_id ON public.runs USING btree (dispatched_message_id) WHERE (dispatched_message_id IS NOT NULL); -- -- Name: idx_runs_feed; Type: INDEX; Schema: public; Owner: - @@ -3605,6 +4140,11 @@ CREATE INDEX idx_runs_connection ON public.runs USING btree (connection_id); CREATE INDEX idx_runs_feed ON public.runs USING btree (feed_id); +-- +-- Name: idx_runs_heartbeat_inflight; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_runs_heartbeat_inflight ON public.runs USING btree (last_heartbeat_at) WHERE ((status = ANY (ARRAY['claimed'::text, 'running'::text])) AND (run_type = ANY (ARRAY['sync'::text, 'action'::text, 'embed_backfill'::text, 'auth'::text]))); -- -- Name: idx_runs_org; Type: INDEX; Schema: public; Owner: - @@ -3612,34 +4152,47 @@ CREATE INDEX idx_runs_feed ON public.runs USING btree (feed_id); CREATE INDEX idx_runs_org ON public.runs USING btree (organization_id); - -- -- Name: idx_runs_pending; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_runs_pending ON public.runs USING btree (status, created_at) WHERE (status = 'pending'::text); - -- -- Name: idx_runs_status; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_runs_status ON public.runs USING btree (status); - -- -- Name: idx_runs_type; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_runs_type ON public.runs USING btree (run_type); - -- -- Name: idx_runs_watcher_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_runs_watcher_id ON public.runs USING btree (watcher_id) WHERE (watcher_id IS NOT NULL); +-- +-- Name: idx_scheduled_jobs_due; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_scheduled_jobs_due ON public.scheduled_jobs USING btree (next_run_at) WHERE (NOT paused); + +-- +-- Name: idx_scheduled_jobs_org_agent; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_scheduled_jobs_org_agent ON public.scheduled_jobs USING btree (organization_id, created_by_agent) WHERE (created_by_agent IS NOT NULL); + +-- +-- Name: idx_scheduled_jobs_org_user; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_scheduled_jobs_org_user ON public.scheduled_jobs USING btree (organization_id, created_by_user) WHERE (created_by_user IS NOT NULL); -- -- Name: idx_view_template_versions_resource; Type: INDEX; Schema: public; Owner: - @@ -3647,62 +4200,59 @@ CREATE INDEX idx_runs_watcher_id ON public.runs USING btree (watcher_id) WHERE ( CREATE INDEX idx_view_template_versions_resource ON public.view_template_versions USING btree (resource_type, resource_id, organization_id); - -- -- Name: idx_watcher_reactions_org; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_watcher_reactions_org ON public.watcher_reactions USING btree (organization_id); - -- -- Name: idx_watcher_reactions_window; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_watcher_reactions_window ON public.watcher_reactions USING btree (watcher_id, window_id); - -- -- Name: idx_watcher_template_versions_created_by; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_watcher_template_versions_created_by ON public.watcher_versions USING btree (created_by); - -- -- Name: idx_watcher_versions_watcher_version; Type: INDEX; Schema: public; Owner: - -- CREATE UNIQUE INDEX idx_watcher_versions_watcher_version ON public.watcher_versions USING btree (watcher_id, version); - -- -- Name: idx_watcher_window_events_event; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_watcher_window_events_event ON public.watcher_window_events USING btree (event_id); - -- -- Name: idx_watcher_window_events_unique; Type: INDEX; Schema: public; Owner: - -- CREATE UNIQUE INDEX idx_watcher_window_events_unique ON public.watcher_window_events USING btree (window_id, event_id); - -- -- Name: idx_watcher_window_events_window; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_watcher_window_events_window ON public.watcher_window_events USING btree (window_id); - -- -- Name: idx_watcher_windows_parent; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_watcher_windows_parent ON public.watcher_windows USING btree (parent_window_id) WHERE (parent_window_id IS NOT NULL); +-- +-- Name: idx_watcher_windows_run_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_watcher_windows_run_id ON public.watcher_windows USING btree (run_id) WHERE (run_id IS NOT NULL); -- -- Name: idx_watcher_windows_template_version; Type: INDEX; Schema: public; Owner: - @@ -3710,27 +4260,23 @@ CREATE INDEX idx_watcher_windows_parent ON public.watcher_windows USING btree (p CREATE INDEX idx_watcher_windows_template_version ON public.watcher_windows USING btree (version_id); - -- -- Name: idx_watcher_windows_unique_period; Type: INDEX; Schema: public; Owner: - -- CREATE UNIQUE INDEX idx_watcher_windows_unique_period ON public.watcher_windows USING btree (watcher_id, window_start, window_end) WHERE (is_rollup = false); - -- -- Name: idx_watcher_windows_watcher; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_watcher_windows_watcher ON public.watcher_windows USING btree (watcher_id, granularity, window_start DESC); - -- -- Name: idx_watchers_agent_id; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_watchers_agent_id ON public.watchers USING btree (agent_id) WHERE (agent_id IS NOT NULL); - +CREATE INDEX idx_watchers_agent_id ON public.watchers USING btree (agent_id); -- -- Name: idx_watchers_connection_id; Type: INDEX; Schema: public; Owner: - @@ -3738,13 +4284,17 @@ CREATE INDEX idx_watchers_agent_id ON public.watchers USING btree (agent_id) WHE CREATE INDEX idx_watchers_connection_id ON public.watchers USING btree (connection_id) WHERE (connection_id IS NOT NULL); - -- -- Name: idx_watchers_created_by; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_watchers_created_by ON public.watchers USING btree (created_by); +-- +-- Name: idx_watchers_device_worker_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_watchers_device_worker_id ON public.watchers USING btree (device_worker_id) WHERE (device_worker_id IS NOT NULL); -- -- Name: idx_watchers_entity_ids; Type: INDEX; Schema: public; Owner: - @@ -3752,13 +4302,17 @@ CREATE INDEX idx_watchers_created_by ON public.watchers USING btree (created_by) CREATE INDEX idx_watchers_entity_ids ON public.watchers USING gin (entity_ids); - -- -- Name: idx_watchers_next_run_at; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_watchers_next_run_at ON public.watchers USING btree (next_run_at) WHERE ((schedule IS NOT NULL) AND (status = 'active'::text)); +-- +-- Name: idx_watchers_org_group; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_watchers_org_group ON public.watchers USING btree (organization_id, watcher_group_id); -- -- Name: idx_watchers_org_slug; Type: INDEX; Schema: public; Owner: - @@ -3766,41 +4320,29 @@ CREATE INDEX idx_watchers_next_run_at ON public.watchers USING btree (next_run_a CREATE UNIQUE INDEX idx_watchers_org_slug ON public.watchers USING btree (organization_id, slug) WHERE (slug IS NOT NULL); - -- -- Name: idx_watchers_organization_id; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX idx_watchers_organization_id ON public.watchers USING btree (organization_id) WHERE (organization_id IS NOT NULL); - -- --- Name: idx_workers_heartbeat; Type: INDEX; Schema: public; Owner: - +-- Name: idx_watchers_watcher_group_id; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_workers_heartbeat ON public.workers USING btree (last_heartbeat_at DESC) WHERE (status = ANY (ARRAY['active'::text, 'idle'::text])); - - --- --- Name: idx_workers_region; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_workers_region ON public.workers USING btree (region) WHERE (region IS NOT NULL); - +CREATE INDEX idx_watchers_watcher_group_id ON public.watchers USING btree (watcher_group_id); -- --- Name: idx_workers_status; Type: INDEX; Schema: public; Owner: - +-- Name: idx_wwff_watcher_field_recent; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_workers_status ON public.workers USING btree (status, last_heartbeat_at); - +CREATE INDEX idx_wwff_watcher_field_recent ON public.watcher_window_field_feedback USING btree (watcher_id, field_path, created_at DESC); -- --- Name: idx_workers_user; Type: INDEX; Schema: public; Owner: - +-- Name: idx_wwff_window; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX idx_workers_user ON public.workers USING btree (user_id) WHERE (user_id IS NOT NULL); - +CREATE INDEX idx_wwff_window ON public.watcher_window_field_feedback USING btree (window_id); -- -- Name: invitation_email_idx; Type: INDEX; Schema: public; Owner: - @@ -3808,27 +4350,35 @@ CREATE INDEX idx_workers_user ON public.workers USING btree (user_id) WHERE (use CREATE INDEX invitation_email_idx ON public.invitation USING btree (email); - -- -- Name: invitation_organizationId_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX "invitation_organizationId_idx" ON public.invitation USING btree ("organizationId"); - -- --- Name: member_organizationId_idx; Type: INDEX; Schema: public; Owner: - +-- Name: mcp_sessions_client_id_idx; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX "member_organizationId_idx" ON public.member USING btree ("organizationId"); +CREATE INDEX mcp_sessions_client_id_idx ON public.mcp_sessions USING btree (client_id); +-- +-- Name: mcp_sessions_expires_at_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX mcp_sessions_expires_at_idx ON public.mcp_sessions USING btree (expires_at); -- --- Name: member_teamId_idx; Type: INDEX; Schema: public; Owner: - +-- Name: mcp_sessions_user_id_idx; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX "member_teamId_idx" ON public.member USING btree ("teamId"); +CREATE INDEX mcp_sessions_user_id_idx ON public.mcp_sessions USING btree (user_id); + +-- +-- Name: member_organizationId_idx; Type: INDEX; Schema: public; Owner: - +-- +CREATE INDEX "member_organizationId_idx" ON public.member USING btree ("organizationId"); -- -- Name: member_userId_idx; Type: INDEX; Schema: public; Owner: - @@ -3836,76 +4386,77 @@ CREATE INDEX "member_teamId_idx" ON public.member USING btree ("teamId"); CREATE INDEX "member_userId_idx" ON public.member USING btree ("userId"); - -- -- Name: namespace_ref_id_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX namespace_ref_id_idx ON public.namespace USING btree (ref_id); - -- -- Name: namespace_type_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX namespace_type_idx ON public.namespace USING btree (type); - -- -- Name: oauth_authorization_codes_client_id_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX oauth_authorization_codes_client_id_idx ON public.oauth_authorization_codes USING btree (client_id); - -- -- Name: oauth_authorization_codes_expires_at_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX oauth_authorization_codes_expires_at_idx ON public.oauth_authorization_codes USING btree (expires_at); - -- -- Name: oauth_clients_organization_id_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX oauth_clients_organization_id_idx ON public.oauth_clients USING btree (organization_id); - -- -- Name: oauth_clients_software_id_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX oauth_clients_software_id_idx ON public.oauth_clients USING btree (software_id); - -- -- Name: oauth_clients_user_id_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX oauth_clients_user_id_idx ON public.oauth_clients USING btree (user_id); - -- -- Name: oauth_device_codes_client_id_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX oauth_device_codes_client_id_idx ON public.oauth_device_codes USING btree (client_id); - -- -- Name: oauth_device_codes_expires_at_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX oauth_device_codes_expires_at_idx ON public.oauth_device_codes USING btree (expires_at); - -- -- Name: oauth_device_codes_user_code_idx; Type: INDEX; Schema: public; Owner: - -- CREATE UNIQUE INDEX oauth_device_codes_user_code_idx ON public.oauth_device_codes USING btree (user_code); +-- +-- Name: oauth_states_expires_at_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX oauth_states_expires_at_idx ON public.oauth_states USING btree (expires_at); + +-- +-- Name: oauth_states_scope_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX oauth_states_scope_idx ON public.oauth_states USING btree (scope); -- -- Name: oauth_tokens_active_idx; Type: INDEX; Schema: public; Owner: - @@ -3913,34 +4464,41 @@ CREATE UNIQUE INDEX oauth_device_codes_user_code_idx ON public.oauth_device_code CREATE INDEX oauth_tokens_active_idx ON public.oauth_tokens USING btree (user_id, expires_at) WHERE (revoked_at IS NULL); - -- -- Name: oauth_tokens_client_id_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX oauth_tokens_client_id_idx ON public.oauth_tokens USING btree (client_id); - -- -- Name: oauth_tokens_expires_at_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX oauth_tokens_expires_at_idx ON public.oauth_tokens USING btree (expires_at); - -- -- Name: oauth_tokens_token_hash_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX oauth_tokens_token_hash_idx ON public.oauth_tokens USING btree (token_hash); - -- -- Name: oauth_tokens_user_id_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX oauth_tokens_user_id_idx ON public.oauth_tokens USING btree (user_id); +-- +-- Name: passkey_credential_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX passkey_credential_id_idx ON public.passkey USING btree ("credentialID"); + +-- +-- Name: passkey_user_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX passkey_user_id_idx ON public.passkey USING btree ("userId"); -- -- Name: personal_access_tokens_active_idx; Type: INDEX; Schema: public; Owner: - @@ -3948,34 +4506,59 @@ CREATE INDEX oauth_tokens_user_id_idx ON public.oauth_tokens USING btree (user_i CREATE INDEX personal_access_tokens_active_idx ON public.personal_access_tokens USING btree (user_id, expires_at) WHERE (revoked_at IS NULL); - -- -- Name: personal_access_tokens_organization_id_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX personal_access_tokens_organization_id_idx ON public.personal_access_tokens USING btree (organization_id); - -- -- Name: personal_access_tokens_token_hash_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX personal_access_tokens_token_hash_idx ON public.personal_access_tokens USING btree (token_hash); - -- -- Name: personal_access_tokens_token_prefix_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX personal_access_tokens_token_prefix_idx ON public.personal_access_tokens USING btree (token_prefix); - -- -- Name: personal_access_tokens_user_id_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX personal_access_tokens_user_id_idx ON public.personal_access_tokens USING btree (user_id); +-- +-- Name: rate_limits_expires_at_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX rate_limits_expires_at_idx ON public.rate_limits USING btree (expires_at); + +-- +-- Name: revoked_tokens_expires_at_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX revoked_tokens_expires_at_idx ON public.revoked_tokens USING btree (expires_at); + +-- +-- Name: runs_expires_at_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX runs_expires_at_idx ON public.runs USING btree (expires_at) WHERE (expires_at IS NOT NULL); + +-- +-- Name: runs_idempotency_key_uniq; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX runs_idempotency_key_uniq ON public.runs USING btree (idempotency_key) WHERE ((idempotency_key IS NOT NULL) AND (status = ANY (ARRAY['pending'::text, 'claimed'::text, 'running'::text]))); + +-- +-- Name: runs_lobu_claim_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX runs_lobu_claim_idx ON public.runs USING btree (run_type, queue_name, priority DESC, run_at, id) WHERE ((status = 'pending'::text) AND (run_type = ANY (ARRAY['chat_message'::text, 'schedule'::text, 'agent_run'::text, 'internal'::text, 'task'::text]))); -- -- Name: session_expiresAt_idx; Type: INDEX; Schema: public; Owner: - @@ -3983,27 +4566,23 @@ CREATE INDEX personal_access_tokens_user_id_idx ON public.personal_access_tokens CREATE INDEX "session_expiresAt_idx" ON public.session USING btree ("expiresAt"); - -- -- Name: session_token_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX session_token_idx ON public.session USING btree (token); - -- -- Name: session_userId_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX "session_userId_idx" ON public.session USING btree ("userId"); - -- --- Name: team_organizationId_idx; Type: INDEX; Schema: public; Owner: - +-- Name: user_auth_profiles_agent_id_idx; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX "team_organizationId_idx" ON public.team USING btree ("organizationId"); - +CREATE INDEX user_auth_profiles_agent_id_idx ON public.user_auth_profiles USING btree (agent_id); -- -- Name: user_username_unique; Type: INDEX; Schema: public; Owner: - @@ -4011,28 +4590,24 @@ CREATE INDEX "team_organizationId_idx" ON public.team USING btree ("organization CREATE UNIQUE INDEX user_username_unique ON public."user" USING btree (username); - -- -- Name: verification_expiresAt_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX "verification_expiresAt_idx" ON public.verification USING btree ("expiresAt"); - -- -- Name: verification_identifier_idx; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX verification_identifier_idx ON public.verification USING btree (identifier); - -- -- Name: entities check_entity_cycles; Type: TRIGGER; Schema: public; Owner: - -- CREATE TRIGGER check_entity_cycles BEFORE INSERT OR UPDATE ON public.entities FOR EACH ROW EXECUTE FUNCTION public.prevent_entity_cycles(); - -- -- Name: account account_userId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4040,54 +4615,54 @@ CREATE TRIGGER check_entity_cycles BEFORE INSERT OR UPDATE ON public.entities FO ALTER TABLE ONLY public.account ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES public."user"(id) ON DELETE CASCADE; - -- --- Name: agent_channel_bindings agent_channel_bindings_agent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: agent_channel_bindings agent_channel_bindings_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.agent_channel_bindings - ADD CONSTRAINT agent_channel_bindings_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE; - + ADD CONSTRAINT agent_channel_bindings_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; -- --- Name: agent_connections agent_connections_agent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: agent_connections agent_connections_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.agent_connections - ADD CONSTRAINT agent_connections_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE; - + ADD CONSTRAINT agent_connections_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; -- --- Name: agent_grants agent_grants_agent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: agent_grants agent_grants_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.agent_grants - ADD CONSTRAINT agent_grants_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE; - + ADD CONSTRAINT agent_grants_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; -- --- Name: agent_users agent_users_agent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: agent_transcript_snapshot agent_transcript_snapshot_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.agent_users - ADD CONSTRAINT agent_users_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE; - +ALTER TABLE ONLY public.agent_transcript_snapshot + ADD CONSTRAINT agent_transcript_snapshot_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; -- --- Name: agents agents_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: agent_transcript_snapshot agent_transcript_snapshot_run_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.agents - ADD CONSTRAINT agents_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.agent_transcript_snapshot + ADD CONSTRAINT agent_transcript_snapshot_run_id_fkey FOREIGN KEY (run_id) REFERENCES public.runs(id) ON DELETE CASCADE; +-- +-- Name: agent_users agent_users_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.agent_users + ADD CONSTRAINT agent_users_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; -- --- Name: agents agents_template_agent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: agents agents_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.agents - ADD CONSTRAINT agents_template_agent_id_fkey FOREIGN KEY (template_agent_id) REFERENCES public.agents(id) ON DELETE SET NULL; - + ADD CONSTRAINT agents_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; -- -- Name: auth_profiles auth_profiles_account_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4096,6 +4671,12 @@ ALTER TABLE ONLY public.agents ALTER TABLE ONLY public.auth_profiles ADD CONSTRAINT auth_profiles_account_id_fkey FOREIGN KEY (account_id) REFERENCES public.account(id) ON DELETE SET NULL; +-- +-- Name: auth_profiles auth_profiles_device_worker_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.auth_profiles + ADD CONSTRAINT auth_profiles_device_worker_id_fkey FOREIGN KEY (device_worker_id) REFERENCES public.device_workers(id) ON DELETE CASCADE; -- -- Name: auth_profiles auth_profiles_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4104,6 +4685,12 @@ ALTER TABLE ONLY public.auth_profiles ALTER TABLE ONLY public.auth_profiles ADD CONSTRAINT auth_profiles_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; +-- +-- Name: chat_user_identities chat_user_identities_lobu_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.chat_user_identities + ADD CONSTRAINT chat_user_identities_lobu_user_id_fkey FOREIGN KEY (lobu_user_id) REFERENCES public."user"(id) ON DELETE CASCADE; -- -- Name: connect_tokens connect_tokens_auth_profile_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4112,7 +4699,6 @@ ALTER TABLE ONLY public.auth_profiles ALTER TABLE ONLY public.connect_tokens ADD CONSTRAINT connect_tokens_auth_profile_id_fkey FOREIGN KEY (auth_profile_id) REFERENCES public.auth_profiles(id) ON DELETE SET NULL; - -- -- Name: connect_tokens connect_tokens_connection_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4120,7 +4706,6 @@ ALTER TABLE ONLY public.connect_tokens ALTER TABLE ONLY public.connect_tokens ADD CONSTRAINT connect_tokens_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES public.connections(id) ON DELETE CASCADE; - -- -- Name: connections connections_account_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4128,22 +4713,19 @@ ALTER TABLE ONLY public.connect_tokens ALTER TABLE ONLY public.connections ADD CONSTRAINT connections_account_id_fkey FOREIGN KEY (account_id) REFERENCES public.account(id) ON DELETE SET NULL; - -- -- Name: connections connections_app_auth_profile_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.connections - ADD CONSTRAINT connections_app_auth_profile_id_fkey FOREIGN KEY (app_auth_profile_id) REFERENCES public.auth_profiles(id) ON DELETE SET NULL; - + ADD CONSTRAINT connections_app_auth_profile_id_fkey FOREIGN KEY (organization_id, app_auth_profile_id) REFERENCES public.auth_profiles(organization_id, id) ON DELETE SET NULL; -- -- Name: connections connections_auth_profile_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.connections - ADD CONSTRAINT connections_auth_profile_id_fkey FOREIGN KEY (auth_profile_id) REFERENCES public.auth_profiles(id) ON DELETE SET NULL; - + ADD CONSTRAINT connections_auth_profile_id_fkey FOREIGN KEY (organization_id, auth_profile_id) REFERENCES public.auth_profiles(organization_id, id) ON DELETE SET NULL; -- -- Name: connections connections_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4152,6 +4734,12 @@ ALTER TABLE ONLY public.connections ALTER TABLE ONLY public.connections ADD CONSTRAINT connections_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE SET NULL; +-- +-- Name: connections connections_device_worker_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.connections + ADD CONSTRAINT connections_device_worker_id_fkey FOREIGN KEY (device_worker_id) REFERENCES public.device_workers(id) ON DELETE SET NULL; -- -- Name: connections connections_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4160,7 +4748,6 @@ ALTER TABLE ONLY public.connections ALTER TABLE ONLY public.connections ADD CONSTRAINT connections_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - -- -- Name: connector_definitions connector_definitions_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4168,7 +4755,6 @@ ALTER TABLE ONLY public.connections ALTER TABLE ONLY public.connector_definitions ADD CONSTRAINT connector_definitions_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - -- -- Name: entities entities_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4176,6 +4762,12 @@ ALTER TABLE ONLY public.connector_definitions ALTER TABLE ONLY public.entities ADD CONSTRAINT entities_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE RESTRICT; +-- +-- Name: entities entities_entity_type_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.entities + ADD CONSTRAINT entities_entity_type_id_fkey FOREIGN KEY (entity_type_id) REFERENCES public.entity_types(id); -- -- Name: entities entities_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4184,7 +4776,6 @@ ALTER TABLE ONLY public.entities ALTER TABLE ONLY public.entities ADD CONSTRAINT entities_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - -- -- Name: entities entities_parent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4192,7 +4783,6 @@ ALTER TABLE ONLY public.entities ALTER TABLE ONLY public.entities ADD CONSTRAINT entities_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES public.entities(id) ON DELETE RESTRICT; - -- -- Name: entities entities_view_template_version_fk; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4200,6 +4790,19 @@ ALTER TABLE ONLY public.entities ALTER TABLE ONLY public.entities ADD CONSTRAINT entities_view_template_version_fk FOREIGN KEY (current_view_template_version_id) REFERENCES public.view_template_versions(id); +-- +-- Name: entity_identities entity_identities_entity_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.entity_identities + ADD CONSTRAINT entity_identities_entity_id_fkey FOREIGN KEY (entity_id) REFERENCES public.entities(id) ON DELETE CASCADE; + +-- +-- Name: entity_identities entity_identities_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.entity_identities + ADD CONSTRAINT entity_identities_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; -- -- Name: entity_relationship_type_rules entity_relationship_type_rules_relationship_type_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4208,7 +4811,6 @@ ALTER TABLE ONLY public.entities ALTER TABLE ONLY public.entity_relationship_type_rules ADD CONSTRAINT entity_relationship_type_rules_relationship_type_id_fkey FOREIGN KEY (relationship_type_id) REFERENCES public.entity_relationship_types(id) ON DELETE CASCADE; - -- -- Name: entity_relationship_types entity_relationship_types_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4216,7 +4818,6 @@ ALTER TABLE ONLY public.entity_relationship_type_rules ALTER TABLE ONLY public.entity_relationship_types ADD CONSTRAINT entity_relationship_types_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE SET NULL; - -- -- Name: entity_relationship_types entity_relationship_types_inverse_type_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4224,7 +4825,6 @@ ALTER TABLE ONLY public.entity_relationship_types ALTER TABLE ONLY public.entity_relationship_types ADD CONSTRAINT entity_relationship_types_inverse_type_id_fkey FOREIGN KEY (inverse_type_id) REFERENCES public.entity_relationship_types(id) ON DELETE SET NULL; - -- -- Name: entity_relationship_types entity_relationship_types_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4232,7 +4832,6 @@ ALTER TABLE ONLY public.entity_relationship_types ALTER TABLE ONLY public.entity_relationship_types ADD CONSTRAINT entity_relationship_types_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - -- -- Name: entity_relationships entity_relationships_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4240,7 +4839,6 @@ ALTER TABLE ONLY public.entity_relationship_types ALTER TABLE ONLY public.entity_relationships ADD CONSTRAINT entity_relationships_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE SET NULL; - -- -- Name: entity_relationships entity_relationships_from_entity_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4248,7 +4846,6 @@ ALTER TABLE ONLY public.entity_relationships ALTER TABLE ONLY public.entity_relationships ADD CONSTRAINT entity_relationships_from_entity_id_fkey FOREIGN KEY (from_entity_id) REFERENCES public.entities(id) ON DELETE CASCADE; - -- -- Name: entity_relationships entity_relationships_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4256,7 +4853,6 @@ ALTER TABLE ONLY public.entity_relationships ALTER TABLE ONLY public.entity_relationships ADD CONSTRAINT entity_relationships_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - -- -- Name: entity_relationships entity_relationships_relationship_type_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4264,7 +4860,6 @@ ALTER TABLE ONLY public.entity_relationships ALTER TABLE ONLY public.entity_relationships ADD CONSTRAINT entity_relationships_relationship_type_id_fkey FOREIGN KEY (relationship_type_id) REFERENCES public.entity_relationship_types(id) ON DELETE CASCADE; - -- -- Name: entity_relationships entity_relationships_to_entity_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4272,7 +4867,6 @@ ALTER TABLE ONLY public.entity_relationships ALTER TABLE ONLY public.entity_relationships ADD CONSTRAINT entity_relationships_to_entity_id_fkey FOREIGN KEY (to_entity_id) REFERENCES public.entities(id) ON DELETE CASCADE; - -- -- Name: entity_relationships entity_relationships_updated_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4280,7 +4874,6 @@ ALTER TABLE ONLY public.entity_relationships ALTER TABLE ONLY public.entity_relationships ADD CONSTRAINT entity_relationships_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES public."user"(id) ON DELETE SET NULL; - -- -- Name: entity_type_audit entity_type_audit_actor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4288,7 +4881,6 @@ ALTER TABLE ONLY public.entity_relationships ALTER TABLE ONLY public.entity_type_audit ADD CONSTRAINT entity_type_audit_actor_fkey FOREIGN KEY (actor) REFERENCES public."user"(id) ON DELETE SET NULL; - -- -- Name: entity_type_audit entity_type_audit_entity_type_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4296,7 +4888,6 @@ ALTER TABLE ONLY public.entity_type_audit ALTER TABLE ONLY public.entity_type_audit ADD CONSTRAINT entity_type_audit_entity_type_id_fkey FOREIGN KEY (entity_type_id) REFERENCES public.entity_types(id) ON DELETE CASCADE; - -- -- Name: entity_types entity_types_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4304,7 +4895,6 @@ ALTER TABLE ONLY public.entity_type_audit ALTER TABLE ONLY public.entity_types ADD CONSTRAINT entity_types_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE SET NULL; - -- -- Name: entity_types entity_types_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4312,7 +4902,6 @@ ALTER TABLE ONLY public.entity_types ALTER TABLE ONLY public.entity_types ADD CONSTRAINT entity_types_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - -- -- Name: entity_types entity_types_updated_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4320,7 +4909,6 @@ ALTER TABLE ONLY public.entity_types ALTER TABLE ONLY public.entity_types ADD CONSTRAINT entity_types_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES public."user"(id) ON DELETE SET NULL; - -- -- Name: entity_types entity_types_view_template_version_fk; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4328,7 +4916,6 @@ ALTER TABLE ONLY public.entity_types ALTER TABLE ONLY public.entity_types ADD CONSTRAINT entity_types_view_template_version_fk FOREIGN KEY (current_view_template_version_id) REFERENCES public.view_template_versions(id); - -- -- Name: event_classifications event_classifications_classifier_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4336,7 +4923,6 @@ ALTER TABLE ONLY public.entity_types ALTER TABLE ONLY public.event_classifications ADD CONSTRAINT event_classifications_classifier_id_fkey FOREIGN KEY (classifier_version_id) REFERENCES public.event_classifier_versions(id); - -- -- Name: event_classifications event_classifications_classifier_version_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4344,7 +4930,6 @@ ALTER TABLE ONLY public.event_classifications ALTER TABLE ONLY public.event_classifications ADD CONSTRAINT event_classifications_classifier_version_id_fkey FOREIGN KEY (classifier_version_id) REFERENCES public.event_classifier_versions(id) ON DELETE RESTRICT; - -- -- Name: event_classifications event_classifications_event_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4352,7 +4937,6 @@ ALTER TABLE ONLY public.event_classifications ALTER TABLE ONLY public.event_classifications ADD CONSTRAINT event_classifications_event_id_fkey FOREIGN KEY (event_id) REFERENCES public.events(id) ON DELETE CASCADE; - -- -- Name: event_classifications event_classifications_insight_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4360,7 +4944,6 @@ ALTER TABLE ONLY public.event_classifications ALTER TABLE ONLY public.event_classifications ADD CONSTRAINT event_classifications_insight_id_fkey FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE CASCADE; - -- -- Name: event_classifications event_classifications_window_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4368,7 +4951,6 @@ ALTER TABLE ONLY public.event_classifications ALTER TABLE ONLY public.event_classifications ADD CONSTRAINT event_classifications_window_id_fkey FOREIGN KEY (window_id) REFERENCES public.watcher_windows(id) ON DELETE CASCADE; - -- -- Name: event_classifier_versions event_classifier_versions_classifier_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4376,7 +4958,6 @@ ALTER TABLE ONLY public.event_classifications ALTER TABLE ONLY public.event_classifier_versions ADD CONSTRAINT event_classifier_versions_classifier_id_fkey FOREIGN KEY (classifier_id) REFERENCES public.event_classifiers(id) ON DELETE CASCADE; - -- -- Name: event_classifier_versions event_classifier_versions_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4384,7 +4965,6 @@ ALTER TABLE ONLY public.event_classifier_versions ALTER TABLE ONLY public.event_classifier_versions ADD CONSTRAINT event_classifier_versions_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE RESTRICT; - -- -- Name: event_classifiers event_classifiers_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4392,7 +4972,6 @@ ALTER TABLE ONLY public.event_classifier_versions ALTER TABLE ONLY public.event_classifiers ADD CONSTRAINT event_classifiers_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE RESTRICT; - -- -- Name: event_classifiers event_classifiers_insight_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4400,7 +4979,6 @@ ALTER TABLE ONLY public.event_classifiers ALTER TABLE ONLY public.event_classifiers ADD CONSTRAINT event_classifiers_insight_id_fkey FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE CASCADE; - -- -- Name: event_classifiers event_classifiers_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4408,14 +4986,19 @@ ALTER TABLE ONLY public.event_classifiers ALTER TABLE ONLY public.event_classifiers ADD CONSTRAINT event_classifiers_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; +-- +-- Name: event_embeddings event_embeddings_event_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.event_embeddings + ADD CONSTRAINT event_embeddings_event_id_fkey FOREIGN KEY (event_id) REFERENCES public.events(id) ON DELETE CASCADE; -- -- Name: events events_client_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.events - ADD CONSTRAINT events_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.oauth_clients(id); - + ADD CONSTRAINT events_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.oauth_clients(id) ON DELETE SET NULL; -- -- Name: events events_connection_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4424,6 +5007,12 @@ ALTER TABLE ONLY public.events ALTER TABLE ONLY public.events ADD CONSTRAINT events_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES public.connections(id) ON DELETE SET NULL; +-- +-- Name: events events_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.events + ADD CONSTRAINT events_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE SET NULL; -- -- Name: events events_feed_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4432,14 +5021,12 @@ ALTER TABLE ONLY public.events ALTER TABLE ONLY public.events ADD CONSTRAINT events_feed_id_fkey FOREIGN KEY (feed_id) REFERENCES public.feeds(id) ON DELETE SET NULL; - -- --- Name: event_embeddings event_embeddings_event_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: events events_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.event_embeddings - ADD CONSTRAINT event_embeddings_event_id_fkey FOREIGN KEY (event_id) REFERENCES public.events(id) ON DELETE CASCADE; - +ALTER TABLE ONLY public.events + ADD CONSTRAINT events_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; -- -- Name: events events_run_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4448,7 +5035,6 @@ ALTER TABLE ONLY public.event_embeddings ALTER TABLE ONLY public.events ADD CONSTRAINT events_run_id_fkey FOREIGN KEY (run_id) REFERENCES public.runs(id) ON DELETE SET NULL; - -- -- Name: events events_supersedes_event_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4456,7 +5042,6 @@ ALTER TABLE ONLY public.events ALTER TABLE ONLY public.events ADD CONSTRAINT events_supersedes_event_id_fkey FOREIGN KEY (supersedes_event_id) REFERENCES public.events(id) ON DELETE SET NULL; - -- -- Name: feeds feeds_connection_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4464,7 +5049,6 @@ ALTER TABLE ONLY public.events ALTER TABLE ONLY public.feeds ADD CONSTRAINT feeds_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES public.connections(id) ON DELETE CASCADE; - -- -- Name: feeds feeds_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4472,7 +5056,6 @@ ALTER TABLE ONLY public.feeds ALTER TABLE ONLY public.feeds ADD CONSTRAINT feeds_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - -- -- Name: event_classifiers fk_event_classifiers_entity; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4480,7 +5063,6 @@ ALTER TABLE ONLY public.feeds ALTER TABLE ONLY public.event_classifiers ADD CONSTRAINT fk_event_classifiers_entity FOREIGN KEY (entity_id) REFERENCES public.entities(id) ON DELETE CASCADE; - -- -- Name: event_classifiers fk_event_classifiers_insight; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4488,6 +5070,12 @@ ALTER TABLE ONLY public.event_classifiers ALTER TABLE ONLY public.event_classifiers ADD CONSTRAINT fk_event_classifiers_insight FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE SET NULL; +-- +-- Name: grants grants_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.grants + ADD CONSTRAINT grants_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; -- -- Name: watcher_versions insight_template_versions_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4496,7 +5084,6 @@ ALTER TABLE ONLY public.event_classifiers ALTER TABLE ONLY public.watcher_versions ADD CONSTRAINT insight_template_versions_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE RESTRICT; - -- -- Name: watcher_window_events insight_window_events_event_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4504,7 +5091,6 @@ ALTER TABLE ONLY public.watcher_versions ALTER TABLE ONLY public.watcher_window_events ADD CONSTRAINT insight_window_events_event_id_fkey FOREIGN KEY (event_id) REFERENCES public.events(id) ON DELETE CASCADE; - -- -- Name: watcher_window_events insight_window_events_window_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4512,7 +5098,6 @@ ALTER TABLE ONLY public.watcher_window_events ALTER TABLE ONLY public.watcher_window_events ADD CONSTRAINT insight_window_events_window_id_fkey FOREIGN KEY (window_id) REFERENCES public.watcher_windows(id) ON DELETE CASCADE; - -- -- Name: watcher_windows insight_windows_insight_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4520,7 +5105,6 @@ ALTER TABLE ONLY public.watcher_window_events ALTER TABLE ONLY public.watcher_windows ADD CONSTRAINT insight_windows_insight_id_fkey FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE CASCADE; - -- -- Name: watcher_windows insight_windows_parent_window_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4528,7 +5112,6 @@ ALTER TABLE ONLY public.watcher_windows ALTER TABLE ONLY public.watcher_windows ADD CONSTRAINT insight_windows_parent_window_id_fkey FOREIGN KEY (parent_window_id) REFERENCES public.watcher_windows(id) ON DELETE CASCADE; - -- -- Name: watchers insights_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4536,7 +5119,6 @@ ALTER TABLE ONLY public.watcher_windows ALTER TABLE ONLY public.watchers ADD CONSTRAINT insights_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE RESTRICT; - -- -- Name: invitation invitation_inviterId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4544,7 +5126,6 @@ ALTER TABLE ONLY public.watchers ALTER TABLE ONLY public.invitation ADD CONSTRAINT "invitation_inviterId_fkey" FOREIGN KEY ("inviterId") REFERENCES public."user"(id) ON DELETE SET NULL; - -- -- Name: invitation invitation_organizationId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4552,22 +5133,33 @@ ALTER TABLE ONLY public.invitation ALTER TABLE ONLY public.invitation ADD CONSTRAINT "invitation_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES public.organization(id) ON DELETE CASCADE; +-- +-- Name: mcp_sessions mcp_sessions_client_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.mcp_sessions + ADD CONSTRAINT mcp_sessions_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.oauth_clients(id) ON DELETE CASCADE; -- --- Name: member member_organizationId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: mcp_sessions mcp_sessions_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.member - ADD CONSTRAINT "member_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES public.organization(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.mcp_sessions + ADD CONSTRAINT mcp_sessions_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; + +-- +-- Name: mcp_sessions mcp_sessions_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- +ALTER TABLE ONLY public.mcp_sessions + ADD CONSTRAINT mcp_sessions_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE; -- --- Name: member member_teamId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: member member_organizationId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.member - ADD CONSTRAINT "member_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES public.team(id) ON DELETE SET NULL; - + ADD CONSTRAINT "member_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES public.organization(id) ON DELETE CASCADE; -- -- Name: member member_userId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4576,6 +5168,12 @@ ALTER TABLE ONLY public.member ALTER TABLE ONLY public.member ADD CONSTRAINT "member_userId_fkey" FOREIGN KEY ("userId") REFERENCES public."user"(id) ON DELETE CASCADE; +-- +-- Name: notification_targets notification_targets_event_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notification_targets + ADD CONSTRAINT notification_targets_event_id_fkey FOREIGN KEY (event_id) REFERENCES public.events(id) ON DELETE CASCADE; -- -- Name: oauth_authorization_codes oauth_authorization_codes_client_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4584,7 +5182,6 @@ ALTER TABLE ONLY public.member ALTER TABLE ONLY public.oauth_authorization_codes ADD CONSTRAINT oauth_authorization_codes_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.oauth_clients(id) ON DELETE CASCADE; - -- -- Name: oauth_authorization_codes oauth_authorization_codes_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4592,7 +5189,6 @@ ALTER TABLE ONLY public.oauth_authorization_codes ALTER TABLE ONLY public.oauth_authorization_codes ADD CONSTRAINT oauth_authorization_codes_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE SET NULL; - -- -- Name: oauth_authorization_codes oauth_authorization_codes_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4600,7 +5196,6 @@ ALTER TABLE ONLY public.oauth_authorization_codes ALTER TABLE ONLY public.oauth_authorization_codes ADD CONSTRAINT oauth_authorization_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE; - -- -- Name: oauth_clients oauth_clients_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4608,7 +5203,6 @@ ALTER TABLE ONLY public.oauth_authorization_codes ALTER TABLE ONLY public.oauth_clients ADD CONSTRAINT oauth_clients_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - -- -- Name: oauth_clients oauth_clients_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4616,7 +5210,6 @@ ALTER TABLE ONLY public.oauth_clients ALTER TABLE ONLY public.oauth_clients ADD CONSTRAINT oauth_clients_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE; - -- -- Name: oauth_device_codes oauth_device_codes_client_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4624,7 +5217,6 @@ ALTER TABLE ONLY public.oauth_clients ALTER TABLE ONLY public.oauth_device_codes ADD CONSTRAINT oauth_device_codes_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.oauth_clients(id) ON DELETE CASCADE; - -- -- Name: oauth_device_codes oauth_device_codes_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4632,7 +5224,6 @@ ALTER TABLE ONLY public.oauth_device_codes ALTER TABLE ONLY public.oauth_device_codes ADD CONSTRAINT oauth_device_codes_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE SET NULL; - -- -- Name: oauth_device_codes oauth_device_codes_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4640,7 +5231,6 @@ ALTER TABLE ONLY public.oauth_device_codes ALTER TABLE ONLY public.oauth_device_codes ADD CONSTRAINT oauth_device_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE; - -- -- Name: oauth_tokens oauth_tokens_client_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4648,7 +5238,6 @@ ALTER TABLE ONLY public.oauth_device_codes ALTER TABLE ONLY public.oauth_tokens ADD CONSTRAINT oauth_tokens_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.oauth_clients(id) ON DELETE CASCADE; - -- -- Name: oauth_tokens oauth_tokens_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4656,7 +5245,6 @@ ALTER TABLE ONLY public.oauth_tokens ALTER TABLE ONLY public.oauth_tokens ADD CONSTRAINT oauth_tokens_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE SET NULL; - -- -- Name: oauth_tokens oauth_tokens_parent_token_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4664,7 +5252,6 @@ ALTER TABLE ONLY public.oauth_tokens ALTER TABLE ONLY public.oauth_tokens ADD CONSTRAINT oauth_tokens_parent_token_id_fkey FOREIGN KEY (parent_token_id) REFERENCES public.oauth_tokens(id) ON DELETE SET NULL; - -- -- Name: oauth_tokens oauth_tokens_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4672,22 +5259,19 @@ ALTER TABLE ONLY public.oauth_tokens ALTER TABLE ONLY public.oauth_tokens ADD CONSTRAINT oauth_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE; - -- --- Name: organization_lobu_links organization_lobu_links_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: passkey passkey_userId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.organization_lobu_links - ADD CONSTRAINT organization_lobu_links_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE SET NULL; - +ALTER TABLE ONLY public.passkey + ADD CONSTRAINT "passkey_userId_fkey" FOREIGN KEY ("userId") REFERENCES public."user"(id) ON DELETE CASCADE; -- --- Name: organization_lobu_links organization_lobu_links_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: pending_interactions pending_interactions_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.organization_lobu_links - ADD CONSTRAINT organization_lobu_links_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - +ALTER TABLE ONLY public.pending_interactions + ADD CONSTRAINT pending_interactions_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; -- -- Name: personal_access_tokens personal_access_tokens_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4696,7 +5280,6 @@ ALTER TABLE ONLY public.organization_lobu_links ALTER TABLE ONLY public.personal_access_tokens ADD CONSTRAINT personal_access_tokens_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE SET NULL; - -- -- Name: personal_access_tokens personal_access_tokens_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4704,6 +5287,12 @@ ALTER TABLE ONLY public.personal_access_tokens ALTER TABLE ONLY public.personal_access_tokens ADD CONSTRAINT personal_access_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE; +-- +-- Name: runs runs_auth_profile_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.runs + ADD CONSTRAINT runs_auth_profile_id_fkey FOREIGN KEY (auth_profile_id) REFERENCES public.auth_profiles(id) ON DELETE CASCADE; -- -- Name: runs runs_connection_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4712,6 +5301,12 @@ ALTER TABLE ONLY public.personal_access_tokens ALTER TABLE ONLY public.runs ADD CONSTRAINT runs_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES public.connections(id) ON DELETE SET NULL; +-- +-- Name: runs runs_created_by_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.runs + ADD CONSTRAINT runs_created_by_user_id_fkey FOREIGN KEY (created_by_user_id) REFERENCES public."user"(id) ON DELETE SET NULL; -- -- Name: runs runs_feed_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4720,7 +5315,6 @@ ALTER TABLE ONLY public.runs ALTER TABLE ONLY public.runs ADD CONSTRAINT runs_feed_id_fkey FOREIGN KEY (feed_id) REFERENCES public.feeds(id) ON DELETE SET NULL; - -- -- Name: runs runs_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4728,7 +5322,6 @@ ALTER TABLE ONLY public.runs ALTER TABLE ONLY public.runs ADD CONSTRAINT runs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - -- -- Name: runs runs_watcher_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4736,7 +5329,6 @@ ALTER TABLE ONLY public.runs ALTER TABLE ONLY public.runs ADD CONSTRAINT runs_watcher_id_fkey FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE SET NULL; - -- -- Name: runs runs_window_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4744,30 +5336,47 @@ ALTER TABLE ONLY public.runs ALTER TABLE ONLY public.runs ADD CONSTRAINT runs_window_id_fkey FOREIGN KEY (window_id) REFERENCES public.watcher_windows(id) ON DELETE SET NULL; - -- --- Name: session session_activeOrganizationId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: scheduled_jobs scheduled_jobs_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.session - ADD CONSTRAINT "session_activeOrganizationId_fkey" FOREIGN KEY ("activeOrganizationId") REFERENCES public.organization(id) ON DELETE SET NULL; +ALTER TABLE ONLY public.scheduled_jobs + ADD CONSTRAINT scheduled_jobs_org_agent_fkey FOREIGN KEY (organization_id, created_by_agent) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; +-- +-- Name: scheduled_jobs scheduled_jobs_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.scheduled_jobs + ADD CONSTRAINT scheduled_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; -- --- Name: session session_userId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: scheduled_jobs scheduled_jobs_source_event_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.session - ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES public."user"(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.scheduled_jobs + ADD CONSTRAINT scheduled_jobs_source_event_fkey FOREIGN KEY (source_event_id) REFERENCES public.events(id) ON DELETE SET NULL; + +-- +-- Name: scheduled_jobs scheduled_jobs_source_run_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- +ALTER TABLE ONLY public.scheduled_jobs + ADD CONSTRAINT scheduled_jobs_source_run_fkey FOREIGN KEY (source_run_id) REFERENCES public.runs(id) ON DELETE SET NULL; -- --- Name: team team_organizationId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: session session_activeOrganizationId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.team - ADD CONSTRAINT "team_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES public.organization(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.session + ADD CONSTRAINT "session_activeOrganizationId_fkey" FOREIGN KEY ("activeOrganizationId") REFERENCES public.organization(id) ON DELETE SET NULL; + +-- +-- Name: session session_userId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- +ALTER TABLE ONLY public.session + ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES public."user"(id) ON DELETE CASCADE; -- -- Name: view_template_active_tabs view_template_active_tabs_version_fk; Type: FK CONSTRAINT; Schema: public; Owner: - @@ -4776,7 +5385,6 @@ ALTER TABLE ONLY public.team ALTER TABLE ONLY public.view_template_active_tabs ADD CONSTRAINT view_template_active_tabs_version_fk FOREIGN KEY (current_version_id) REFERENCES public.view_template_versions(id); - -- -- Name: watcher_reactions watcher_reactions_watcher_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4784,7 +5392,6 @@ ALTER TABLE ONLY public.view_template_active_tabs ALTER TABLE ONLY public.watcher_reactions ADD CONSTRAINT watcher_reactions_watcher_id_fkey FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE CASCADE; - -- -- Name: watcher_reactions watcher_reactions_window_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4792,7 +5399,6 @@ ALTER TABLE ONLY public.watcher_reactions ALTER TABLE ONLY public.watcher_reactions ADD CONSTRAINT watcher_reactions_window_id_fkey FOREIGN KEY (window_id) REFERENCES public.watcher_windows(id) ON DELETE CASCADE; - -- -- Name: watcher_versions watcher_versions_watcher_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4800,68 +5406,63 @@ ALTER TABLE ONLY public.watcher_reactions ALTER TABLE ONLY public.watcher_versions ADD CONSTRAINT watcher_versions_watcher_id_fkey FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE CASCADE; - -- --- Name: watcher_windows watcher_windows_version_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: watcher_window_field_feedback watcher_window_field_feedback_watcher_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.watcher_windows - ADD CONSTRAINT watcher_windows_version_id_fkey FOREIGN KEY (version_id) REFERENCES public.watcher_versions(id); - +ALTER TABLE ONLY public.watcher_window_field_feedback + ADD CONSTRAINT watcher_window_field_feedback_watcher_id_fkey FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE CASCADE; -- --- Name: watchers watchers_current_version_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: watcher_window_field_feedback watcher_window_field_feedback_window_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.watchers - ADD CONSTRAINT watchers_current_version_id_fkey FOREIGN KEY (current_version_id) REFERENCES public.watcher_versions(id) ON DELETE SET NULL; - +ALTER TABLE ONLY public.watcher_window_field_feedback + ADD CONSTRAINT watcher_window_field_feedback_window_id_fkey FOREIGN KEY (window_id) REFERENCES public.watcher_windows(id) ON DELETE CASCADE; -- --- Name: workers workers_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: watcher_windows watcher_windows_run_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.workers - ADD CONSTRAINT workers_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id); - +ALTER TABLE ONLY public.watcher_windows + ADD CONSTRAINT watcher_windows_run_id_fkey FOREIGN KEY (run_id) REFERENCES public.runs(id) ON DELETE SET NULL; -- --- Name: workspace_settings workspace_settings_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: watcher_windows watcher_windows_version_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.workspace_settings - ADD CONSTRAINT workspace_settings_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - +ALTER TABLE ONLY public.watcher_windows + ADD CONSTRAINT watcher_windows_version_id_fkey FOREIGN KEY (version_id) REFERENCES public.watcher_versions(id); -- --- PostgreSQL database dump complete +-- Name: watchers watchers_current_version_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- +ALTER TABLE ONLY public.watchers + ADD CONSTRAINT watchers_current_version_id_fkey FOREIGN KEY (current_version_id) REFERENCES public.watcher_versions(id) ON DELETE SET NULL; - +-- +-- Name: watchers watchers_device_worker_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- +ALTER TABLE ONLY public.watchers + ADD CONSTRAINT watchers_device_worker_id_fkey FOREIGN KEY (device_worker_id) REFERENCES public.device_workers(id); -- --- Name: watcher_window_feedback; Type: TABLE; Schema: public; Owner: - +-- Name: watchers watchers_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -CREATE TABLE IF NOT EXISTS public.watcher_window_feedback ( - id bigserial PRIMARY KEY, - window_id integer NOT NULL REFERENCES public.watcher_windows(id) ON DELETE CASCADE, - watcher_id integer NOT NULL REFERENCES public.watchers(id) ON DELETE CASCADE, - organization_id text NOT NULL, - corrections jsonb NOT NULL, - notes text, - created_by text NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_wwf_window ON public.watcher_window_feedback(window_id); -CREATE INDEX IF NOT EXISTS idx_wwf_watcher ON public.watcher_window_feedback(watcher_id); +ALTER TABLE ONLY public.watchers + ADD CONSTRAINT watchers_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; +-- +-- PostgreSQL database dump complete +-- -- migrate:down -DROP SCHEMA public CASCADE; +-- Reverting the baseline drops everything in public. Safe for fresh dev +-- DBs; never run against prod (use CNPG PITR — see header). +DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; +COMMENT ON SCHEMA public IS 'standard public schema'; diff --git a/db/migrations/20260405193000_add_mcp_sessions.sql b/db/migrations/20260405193000_add_mcp_sessions.sql deleted file mode 100644 index fcab3142d..000000000 --- a/db/migrations/20260405193000_add_mcp_sessions.sql +++ /dev/null @@ -1,33 +0,0 @@ --- migrate:up - -CREATE TABLE public.mcp_sessions ( - session_id text PRIMARY KEY, - user_id text, - client_id text, - organization_id text, - member_role text, - requested_agent_id text, - is_authenticated boolean DEFAULT false NOT NULL, - scoped_to_org boolean DEFAULT false NOT NULL, - last_accessed_at timestamp with time zone DEFAULT now() NOT NULL, - expires_at timestamp with time zone NOT NULL -); - -CREATE INDEX mcp_sessions_client_id_idx ON public.mcp_sessions USING btree (client_id); -CREATE INDEX mcp_sessions_expires_at_idx ON public.mcp_sessions USING btree (expires_at); -CREATE INDEX mcp_sessions_user_id_idx ON public.mcp_sessions USING btree (user_id); - -ALTER TABLE ONLY public.mcp_sessions - ADD CONSTRAINT mcp_sessions_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.oauth_clients(id) ON DELETE CASCADE; - -ALTER TABLE ONLY public.mcp_sessions - ADD CONSTRAINT mcp_sessions_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - -ALTER TABLE ONLY public.mcp_sessions - ADD CONSTRAINT mcp_sessions_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE; - -COMMENT ON TABLE public.mcp_sessions IS 'Persisted MCP streamable HTTP sessions for restart and cross-replica recovery'; - --- migrate:down - -DROP TABLE IF EXISTS public.mcp_sessions; diff --git a/db/migrations/20260408120000_remove_system_connectors.sql b/db/migrations/20260408120000_remove_system_connectors.sql deleted file mode 100644 index 5701a8dc4..000000000 --- a/db/migrations/20260408120000_remove_system_connectors.sql +++ /dev/null @@ -1,48 +0,0 @@ --- migrate:up - --- Migrate system connector definitions to org-scoped. --- For every org, copy each active system connector definition that --- the org doesn't already have (regardless of whether connections exist). - -INSERT INTO connector_definitions ( - organization_id, key, name, description, version, - auth_schema, feeds_schema, actions_schema, options_schema, - mcp_config, openapi_config, favicon_domain, status, login_enabled -) -SELECT - o.id, - cd.key, - cd.name, - cd.description, - cd.version, - cd.auth_schema, - cd.feeds_schema, - cd.actions_schema, - cd.options_schema, - cd.mcp_config, - cd.openapi_config, - cd.favicon_domain, - cd.status, - cd.login_enabled -FROM connector_definitions cd -CROSS JOIN "organization" o -WHERE cd.organization_id IS NULL - AND cd.status = 'active' - AND NOT EXISTS ( - SELECT 1 - FROM connector_definitions existing - WHERE existing.organization_id = o.id - AND existing.key = cd.key - AND existing.status = 'active' - ); - --- Archive all system-level connector definitions -UPDATE connector_definitions -SET status = 'archived', updated_at = NOW() -WHERE organization_id IS NULL - AND status = 'active'; - --- Drop orphaned index that only covered system-level (org IS NULL) definitions -DROP INDEX IF EXISTS idx_connector_defs_system_key; - --- migrate:down diff --git a/db/migrations/20260408120001_optional_compiled_code.sql b/db/migrations/20260408120001_optional_compiled_code.sql deleted file mode 100644 index a0de8bd5c..000000000 --- a/db/migrations/20260408120001_optional_compiled_code.sql +++ /dev/null @@ -1,6 +0,0 @@ --- migrate:up -ALTER TABLE connector_versions ALTER COLUMN compiled_code DROP NOT NULL; - --- migrate:down -UPDATE connector_versions SET compiled_code = '' WHERE compiled_code IS NULL; -ALTER TABLE connector_versions ALTER COLUMN compiled_code SET NOT NULL; diff --git a/db/migrations/20260409110000_add_active_watcher_run_index.sql b/db/migrations/20260409110000_add_active_watcher_run_index.sql deleted file mode 100644 index 483212916..000000000 --- a/db/migrations/20260409110000_add_active_watcher_run_index.sql +++ /dev/null @@ -1,9 +0,0 @@ --- migrate:up -CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_active_watcher_per_watcher - ON runs (watcher_id) - WHERE run_type = 'watcher' - AND watcher_id IS NOT NULL - AND status IN ('pending', 'claimed', 'running'); - --- migrate:down -DROP INDEX IF EXISTS idx_runs_active_watcher_per_watcher; diff --git a/db/migrations/20260409130000_connector_default_config.sql b/db/migrations/20260409130000_connector_default_config.sql deleted file mode 100644 index 6dee28c9c..000000000 --- a/db/migrations/20260409130000_connector_default_config.sql +++ /dev/null @@ -1,5 +0,0 @@ --- migrate:up -ALTER TABLE connector_definitions ADD COLUMN IF NOT EXISTS default_connection_config jsonb; - --- migrate:down -ALTER TABLE connector_definitions DROP COLUMN IF EXISTS default_connection_config; diff --git a/db/migrations/20260410120000_add_agent_secrets.sql b/db/migrations/20260410120000_add_agent_secrets.sql deleted file mode 100644 index 13a79cacb..000000000 --- a/db/migrations/20260410120000_add_agent_secrets.sql +++ /dev/null @@ -1,25 +0,0 @@ --- migrate:up - -CREATE TABLE public.agent_secrets ( - name text PRIMARY KEY, - ciphertext text NOT NULL, - expires_at timestamp with time zone, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL -); - --- text_pattern_ops index enables efficient prefix scans used by list(prefix) --- to support cascade deletes (deleteSecretsByPrefix in @lobu/gateway). -CREATE INDEX agent_secrets_name_prefix_idx - ON public.agent_secrets USING btree (name text_pattern_ops); - -CREATE INDEX agent_secrets_expires_at_idx - ON public.agent_secrets USING btree (expires_at) - WHERE expires_at IS NOT NULL; - -COMMENT ON TABLE public.agent_secrets IS - 'Encrypted secret values referenced via secret:// refs. Backs the PostgresSecretStore implementation of @lobu/gateway WritableSecretStore.'; - --- migrate:down - -DROP TABLE IF EXISTS public.agent_secrets; diff --git a/db/migrations/20260413170000_add_watcher_group_id.sql b/db/migrations/20260413170000_add_watcher_group_id.sql deleted file mode 100644 index 63a1268d8..000000000 --- a/db/migrations/20260413170000_add_watcher_group_id.sql +++ /dev/null @@ -1,67 +0,0 @@ --- migrate:up - -ALTER TABLE public.watchers - ADD COLUMN IF NOT EXISTS source_watcher_id integer, - ADD COLUMN IF NOT EXISTS watcher_group_id integer; - --- Backfill source watcher links from cloned watcher versions. -WITH derived_source AS ( - SELECT DISTINCT ON (w.id) - w.id AS watcher_id, - source_versions.watcher_id AS source_watcher_id - FROM public.watchers w - JOIN public.watcher_versions wv ON wv.watcher_id = w.id - JOIN public.watcher_versions source_versions - ON source_versions.id = substring(wv.change_notes FROM 'Created from version ([0-9]+)')::integer - WHERE wv.change_notes ~ 'Created from version [0-9]+' - ORDER BY w.id, wv.version ASC, wv.id ASC -) -UPDATE public.watchers w -SET source_watcher_id = ds.source_watcher_id -FROM derived_source ds -WHERE w.id = ds.watcher_id - AND w.source_watcher_id IS NULL; - --- Build stable watcher group ids by walking source watcher ancestry to the root. -WITH RECURSIVE roots AS ( - SELECT w.id AS watcher_id, w.source_watcher_id, w.id AS root_id - FROM public.watchers w - WHERE w.source_watcher_id IS NULL - - UNION ALL - - SELECT child.id AS watcher_id, child.source_watcher_id, roots.root_id - FROM public.watchers child - JOIN roots ON child.source_watcher_id = roots.watcher_id -) -UPDATE public.watchers w -SET watcher_group_id = roots.root_id -FROM roots -WHERE w.id = roots.watcher_id; - --- Fallback for any rows that did not match the recursive backfill. -UPDATE public.watchers -SET watcher_group_id = id -WHERE watcher_group_id IS NULL; - --- `watchers.current_version_id` has a DEFERRABLE FK. The backfill updates above queue --- deferred trigger events, and PostgreSQL refuses ALTER TABLE while those are pending. -SET CONSTRAINTS ALL IMMEDIATE; - -ALTER TABLE public.watchers - ALTER COLUMN watcher_group_id SET NOT NULL; - -CREATE INDEX IF NOT EXISTS idx_watchers_watcher_group_id - ON public.watchers USING btree (watcher_group_id); - -CREATE INDEX IF NOT EXISTS idx_watchers_org_group - ON public.watchers USING btree (organization_id, watcher_group_id); - --- migrate:down - -DROP INDEX IF EXISTS idx_watchers_org_group; -DROP INDEX IF EXISTS idx_watchers_watcher_group_id; - -ALTER TABLE public.watchers - DROP COLUMN IF EXISTS watcher_group_id, - DROP COLUMN IF EXISTS source_watcher_id; diff --git a/db/migrations/20260416120000_add_entity_wa_jid_index.sql b/db/migrations/20260416120000_add_entity_wa_jid_index.sql deleted file mode 100644 index 23b42b48c..000000000 --- a/db/migrations/20260416120000_add_entity_wa_jid_index.sql +++ /dev/null @@ -1,14 +0,0 @@ --- migrate:up - --- Speeds up the event↔entity join declared by the WhatsApp connector's --- early entityLinks rule: events JOIN entities ON --- entities.metadata->>'wa_jid' = events.metadata->>'chat_jid'. --- Superseded by the entity_identities table (2026-04-17 migration) which --- drops this index; this file is kept so dbmate's history is preserved. -CREATE INDEX IF NOT EXISTS idx_entities_wa_jid - ON public.entities (entity_type, (metadata->>'wa_jid')) - WHERE metadata ? 'wa_jid' AND deleted_at IS NULL; - --- migrate:down - -DROP INDEX IF EXISTS public.idx_entities_wa_jid; diff --git a/db/migrations/20260417100000_add_entity_identities.sql b/db/migrations/20260417100000_add_entity_identities.sql deleted file mode 100644 index e7acb9f45..000000000 --- a/db/migrations/20260417100000_add_entity_identities.sql +++ /dev/null @@ -1,77 +0,0 @@ --- migrate:up - --- Normalized identifier store. One row per (organization_id, namespace, identifier). --- Replaces the earlier scheme of storing identifiers inside entities.metadata JSONB --- arrays. The UNIQUE constraint is the foundation of the whole design: --- * creation races collapse on the UNIQUE (one batch wins, the other links) --- * cross-entity contamination is constraint-blocked --- * accrete is just INSERT ... ON CONFLICT DO NOTHING --- * lookup is a plain BTREE seek (no GIN-on-jsonb) -CREATE TABLE public.entity_identities ( - id bigserial PRIMARY KEY, - organization_id text NOT NULL REFERENCES public.organization(id) ON DELETE CASCADE, - entity_id bigint NOT NULL REFERENCES public.entities(id) ON DELETE CASCADE, - namespace text NOT NULL, - identifier text NOT NULL, - source_connector text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - deleted_at timestamp with time zone -); - --- Relaxed uniqueness: only live rows are constrained, so soft-delete + re-claim --- is a supported repair path (mis-attribution recovery). -CREATE UNIQUE INDEX idx_entity_identities_live_unique - ON public.entity_identities (organization_id, namespace, identifier) - WHERE deleted_at IS NULL; - --- Primary ingestion lookup path: "does anyone in this org own this identifier?" -CREATE INDEX idx_entity_identities_lookup - ON public.entity_identities (organization_id, namespace, identifier) - WHERE deleted_at IS NULL; - --- Reverse lookup: "what identifiers does this entity have?" -CREATE INDEX idx_entity_identities_by_entity - ON public.entity_identities (entity_id) - WHERE deleted_at IS NULL; - -COMMENT ON TABLE public.entity_identities IS - 'Normalized identifier claims per entity. See docs/identity-linking.md for the full pattern.'; -COMMENT ON COLUMN public.entity_identities.namespace IS - 'Identifier kind. Standard values: phone, email, wa_jid, slack_user_id, github_login, auth_user_id, google_contact_id. Custom namespaces allowed but connectors sharing a namespace must agree on its format.'; -COMMENT ON COLUMN public.entity_identities.identifier IS - 'Normalized identifier value (E.164 digits for phone, lowercase for email, etc.). Normalizers in @lobu/connector-sdk own the canonical form.'; -COMMENT ON COLUMN public.entity_identities.source_connector IS - 'Who claimed this identifier: "connector:whatsapp", "manual", or null when seeded by migration.'; - - --- Per-install override surface. A JSONB keyed by entityType; shallow-merged --- onto the connector's declared entityLinks at rule-resolve time. Lets an --- org retarget, disable rules, flip autoCreate, or mask specific identities --- without forking the connector. Null column = use connector defaults verbatim. -ALTER TABLE public.connector_definitions - ADD COLUMN IF NOT EXISTS entity_link_overrides jsonb; - -COMMENT ON COLUMN public.connector_definitions.entity_link_overrides IS - 'Per-install override of connector entityLinks rules. See resolveEntityLinkRules() for merge semantics.'; - - --- The old scalar-JSONB index was built for a shape (entities.metadata->>wa_jid) --- we no longer use — identifiers now live in entity_identities. Drop to avoid --- carrying an unused index and a misleading comment on future reads. -DROP INDEX IF EXISTS public.idx_entities_wa_jid; - - --- migrate:down - -DROP INDEX IF EXISTS public.idx_entity_identities_by_entity; -DROP INDEX IF EXISTS public.idx_entity_identities_lookup; -DROP INDEX IF EXISTS public.idx_entity_identities_live_unique; -DROP TABLE IF EXISTS public.entity_identities; - -ALTER TABLE public.connector_definitions - DROP COLUMN IF EXISTS entity_link_overrides; - -CREATE INDEX IF NOT EXISTS idx_entities_wa_jid - ON public.entities (entity_type, (metadata->>'wa_jid')) - WHERE metadata ? 'wa_jid' AND deleted_at IS NULL; diff --git a/db/migrations/20260418100000_add_auth_runs.sql b/db/migrations/20260418100000_add_auth_runs.sql deleted file mode 100644 index 84be4d842..000000000 --- a/db/migrations/20260418100000_add_auth_runs.sql +++ /dev/null @@ -1,83 +0,0 @@ --- migrate:up - --- Adds the 'auth' run lifecycle: workers run interactive connector.authenticate() --- flows (WhatsApp QR, OAuth redirect, credential prompt) and stream artifacts --- back to the UI via runs.checkpoint. The UI signals back (OAuth callback, --- form submit, cancel) via runs.auth_signal. - -ALTER TABLE public.runs DROP CONSTRAINT IF EXISTS runs_run_type_check; -ALTER TABLE public.runs ADD CONSTRAINT runs_run_type_check CHECK ( - run_type = ANY (ARRAY[ - 'sync'::text, - 'action'::text, - 'code'::text, - 'insight'::text, - 'watcher'::text, - 'embed_backfill'::text, - 'auth'::text - ]) -); - --- Target auth profile for 'auth' runs (null for other run types). -ALTER TABLE public.runs ADD COLUMN IF NOT EXISTS auth_profile_id bigint - REFERENCES public.auth_profiles(id) ON DELETE CASCADE; - --- Reverse channel: UI → connector. The connector pauses on ctx.awaitSignal(name) --- and polls this column; the API writes here when the UI posts a signal. -ALTER TABLE public.runs ADD COLUMN IF NOT EXISTS auth_signal jsonb; - --- One active auth run per auth profile at a time. -CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_active_auth_per_profile - ON public.runs (auth_profile_id) - WHERE run_type = 'auth' - AND auth_profile_id IS NOT NULL - AND status = ANY (ARRAY['pending'::text, 'claimed'::text, 'running'::text]); - --- 'interactive' profile kind: credentials produced by a connector.authenticate() --- run. Examples: WhatsApp Baileys session, connector-managed OAuth. -ALTER TABLE public.auth_profiles DROP CONSTRAINT IF EXISTS auth_profiles_profile_kind_check; -ALTER TABLE public.auth_profiles ADD CONSTRAINT auth_profiles_profile_kind_check CHECK ( - profile_kind = ANY (ARRAY[ - 'env'::text, - 'oauth_app'::text, - 'oauth_account'::text, - 'browser_session'::text, - 'interactive'::text - ]) -); - --- Structured display metadata produced by connector.authenticate() — account_id, --- display_name, paired_at, expires_at, etc. Surfaced in UI next to the connection. -ALTER TABLE public.auth_profiles ADD COLUMN IF NOT EXISTS metadata jsonb DEFAULT '{}'::jsonb NOT NULL; - - --- migrate:down - -ALTER TABLE public.auth_profiles DROP COLUMN IF EXISTS metadata; - -ALTER TABLE public.auth_profiles DROP CONSTRAINT IF EXISTS auth_profiles_profile_kind_check; -ALTER TABLE public.auth_profiles ADD CONSTRAINT auth_profiles_profile_kind_check CHECK ( - profile_kind = ANY (ARRAY[ - 'env'::text, - 'oauth_app'::text, - 'oauth_account'::text, - 'browser_session'::text - ]) -); - -DROP INDEX IF EXISTS public.idx_runs_active_auth_per_profile; - -ALTER TABLE public.runs DROP COLUMN IF EXISTS auth_signal; -ALTER TABLE public.runs DROP COLUMN IF EXISTS auth_profile_id; - -ALTER TABLE public.runs DROP CONSTRAINT IF EXISTS runs_run_type_check; -ALTER TABLE public.runs ADD CONSTRAINT runs_run_type_check CHECK ( - run_type = ANY (ARRAY[ - 'sync'::text, - 'action'::text, - 'code'::text, - 'insight'::text, - 'watcher'::text, - 'embed_backfill'::text - ]) -); diff --git a/db/migrations/20260418110000_add_runs_created_by_user.sql b/db/migrations/20260418110000_add_runs_created_by_user.sql deleted file mode 100644 index 66521a768..000000000 --- a/db/migrations/20260418110000_add_runs_created_by_user.sql +++ /dev/null @@ -1,18 +0,0 @@ --- migrate:up - --- Track which user initiated an interactive auth run so that only that user --- can view the QR / credential artifact. Sensitive artifacts (WhatsApp QR, --- OTP codes, OAuth consent URLs) must never be viewable by other org members. - -ALTER TABLE public.runs ADD COLUMN IF NOT EXISTS created_by_user_id text - REFERENCES public."user"(id) ON DELETE SET NULL; - -CREATE INDEX IF NOT EXISTS idx_runs_created_by_user - ON public.runs (created_by_user_id) - WHERE created_by_user_id IS NOT NULL; - - --- migrate:down - -DROP INDEX IF EXISTS public.idx_runs_created_by_user; -ALTER TABLE public.runs DROP COLUMN IF EXISTS created_by_user_id; diff --git a/db/migrations/20260419120000_add_event_identity_indexes.sql b/db/migrations/20260419120000_add_event_identity_indexes.sql deleted file mode 100644 index e11c50cf2..000000000 --- a/db/migrations/20260419120000_add_event_identity_indexes.sql +++ /dev/null @@ -1,56 +0,0 @@ --- migrate:up - --- Read-time JOIN support for the entity_identities graph. --- --- applyEntityLinks (src/utils/entity-link-upsert.ts) stamps normalized --- identifiers into events.metadata under the namespace key (metadata.email, --- metadata.wa_jid, metadata.phone, …). Entity-scoped content queries then --- JOIN: --- --- events f --- EXISTS (SELECT 1 FROM entity_identities ei --- WHERE ei.entity_id = $X --- AND ei.deleted_at IS NULL --- AND f.metadata ? ei.namespace --- AND f.metadata->>ei.namespace = ei.identifier) --- --- Without these indexes, the plan degenerates into a seq scan of events for --- every entity-scoped content listing. One partial BTREE per namespace in use --- keeps the JOIN on an index. -CREATE INDEX IF NOT EXISTS idx_events_metadata_email - ON public.events ((metadata->>'email')) - WHERE metadata ? 'email'; - -CREATE INDEX IF NOT EXISTS idx_events_metadata_wa_jid - ON public.events ((metadata->>'wa_jid')) - WHERE metadata ? 'wa_jid'; - -CREATE INDEX IF NOT EXISTS idx_events_metadata_phone - ON public.events ((metadata->>'phone')) - WHERE metadata ? 'phone'; - -CREATE INDEX IF NOT EXISTS idx_events_metadata_slack_user_id - ON public.events ((metadata->>'slack_user_id')) - WHERE metadata ? 'slack_user_id'; - -CREATE INDEX IF NOT EXISTS idx_events_metadata_github_login - ON public.events ((metadata->>'github_login')) - WHERE metadata ? 'github_login'; - -CREATE INDEX IF NOT EXISTS idx_events_metadata_auth_user_id - ON public.events ((metadata->>'auth_user_id')) - WHERE metadata ? 'auth_user_id'; - -CREATE INDEX IF NOT EXISTS idx_events_metadata_google_contact_id - ON public.events ((metadata->>'google_contact_id')) - WHERE metadata ? 'google_contact_id'; - --- migrate:down - -DROP INDEX IF EXISTS public.idx_events_metadata_google_contact_id; -DROP INDEX IF EXISTS public.idx_events_metadata_auth_user_id; -DROP INDEX IF EXISTS public.idx_events_metadata_github_login; -DROP INDEX IF EXISTS public.idx_events_metadata_slack_user_id; -DROP INDEX IF EXISTS public.idx_events_metadata_phone; -DROP INDEX IF EXISTS public.idx_events_metadata_wa_jid; -DROP INDEX IF EXISTS public.idx_events_metadata_email; diff --git a/db/migrations/20260420120000_extend_reserved_org_slugs.sql b/db/migrations/20260420120000_extend_reserved_org_slugs.sql deleted file mode 100644 index afc2f876f..000000000 --- a/db/migrations/20260420120000_extend_reserved_org_slugs.sql +++ /dev/null @@ -1,56 +0,0 @@ --- migrate:up - --- Extend reserved org slugs to cover infrastructure subdomains. --- RESERVED_SUBDOMAINS in packages/server/src/index.ts already --- treats www/mcp/static/cdn/... as non-org at the routing layer; this --- mirrors it at the DB layer so those names can never be claimed. --- --- `app` is intentionally NOT reserved — `app.lobu.ai` hosts the auth --- org itself, whose DB row uses slug='app'. - -ALTER TABLE public.organization DROP CONSTRAINT IF EXISTS org_slug_not_reserved; - -ALTER TABLE public.organization ADD CONSTRAINT org_slug_not_reserved CHECK ( - slug <> ALL (ARRAY[ - 'settings', - 'auth', - 'api', - 'templates', - 'help', - 'account', - 'admin', - 'health', - 'login', - 'logout', - 'signup', - 'register', - 'www', - 'mcp', - 'static', - 'assets', - 'cdn', - 'docs', - 'mail' - ]::text[]) -); - --- migrate:down - -ALTER TABLE public.organization DROP CONSTRAINT IF EXISTS org_slug_not_reserved; - -ALTER TABLE public.organization ADD CONSTRAINT org_slug_not_reserved CHECK ( - slug <> ALL (ARRAY[ - 'settings', - 'auth', - 'api', - 'templates', - 'help', - 'account', - 'admin', - 'health', - 'login', - 'logout', - 'signup', - 'register' - ]::text[]) -); diff --git a/db/migrations/20260424030000_add_watcher_run_correlation.sql b/db/migrations/20260424030000_add_watcher_run_correlation.sql deleted file mode 100644 index 40b872f6f..000000000 --- a/db/migrations/20260424030000_add_watcher_run_correlation.sql +++ /dev/null @@ -1,52 +0,0 @@ --- migrate:up - --- Durable correlation between a dispatched watcher run and the agent message --- that executes it, and between a completed watcher window and the run that --- produced it. Replaces payload-based matching in reconcileWatcherRuns. - -ALTER TABLE public.runs - ADD COLUMN IF NOT EXISTS dispatched_message_id text; - -CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_dispatched_message_id - ON public.runs (dispatched_message_id) - WHERE dispatched_message_id IS NOT NULL; - -ALTER TABLE public.watcher_windows - ADD COLUMN IF NOT EXISTS run_id bigint - REFERENCES public.runs(id) ON DELETE SET NULL; - --- Rollout backfill: older windows already stored watcher_run_id in run_metadata, --- but pre-migration rows have NULL run_id. Populate the durable FK before the --- new reconciler/reset path runs so successful pre-deploy executions are not --- re-dispatched on first tick. -WITH correlated_windows AS ( - SELECT ww.id, - (btrim(ww.run_metadata->>'watcher_run_id'))::bigint AS correlated_run_id - FROM public.watcher_windows ww - WHERE ww.run_id IS NULL - AND ww.run_metadata ? 'watcher_run_id' - AND jsonb_typeof(ww.run_metadata->'watcher_run_id') IN ('number', 'string') - AND btrim(ww.run_metadata->>'watcher_run_id') ~ '^[0-9]+$' -) -UPDATE public.watcher_windows ww -SET run_id = cw.correlated_run_id -FROM correlated_windows cw -WHERE ww.id = cw.id - AND EXISTS ( - SELECT 1 - FROM public.runs r - WHERE r.id = cw.correlated_run_id - AND r.run_type = 'watcher' - ); - -CREATE INDEX IF NOT EXISTS idx_watcher_windows_run_id - ON public.watcher_windows (run_id) - WHERE run_id IS NOT NULL; - - --- migrate:down - -DROP INDEX IF EXISTS public.idx_watcher_windows_run_id; -ALTER TABLE public.watcher_windows DROP COLUMN IF EXISTS run_id; -DROP INDEX IF EXISTS public.idx_runs_dispatched_message_id; -ALTER TABLE public.runs DROP COLUMN IF EXISTS dispatched_message_id; diff --git a/db/migrations/20260424130000_relax_events_client_id_fk.sql b/db/migrations/20260424130000_relax_events_client_id_fk.sql deleted file mode 100644 index b642c8c2a..000000000 --- a/db/migrations/20260424130000_relax_events_client_id_fk.sql +++ /dev/null @@ -1,47 +0,0 @@ --- migrate:up - --- The events table tags each row with the OAuth client that produced it via --- `events.client_id -> oauth_clients.id`. The original FK had no ON DELETE --- behaviour, so when an oauth_client row was removed (manual cleanup, e2e --- teardown, expired registration) any in-flight token still issuing inserts --- failed with `events_client_id_fkey` violations (Sentry: LOBU-34). --- --- Match the relaxation already applied to other event-side FKs --- (connection_id, feed_id, run_id) and let stale client references reset --- to NULL instead of breaking inserts. --- --- Add and validate the replacement constraint before the quick final swap so --- existing traffic stays protected and the ACCESS EXCLUSIVE window is short. - -ALTER TABLE public.events - ADD CONSTRAINT events_client_id_fkey_v2 - FOREIGN KEY (client_id) - REFERENCES public.oauth_clients(id) - ON DELETE SET NULL - NOT VALID; - -ALTER TABLE public.events - VALIDATE CONSTRAINT events_client_id_fkey_v2; - -ALTER TABLE public.events - DROP CONSTRAINT IF EXISTS events_client_id_fkey; - -ALTER TABLE public.events - RENAME CONSTRAINT events_client_id_fkey_v2 TO events_client_id_fkey; - --- migrate:down - -ALTER TABLE public.events - ADD CONSTRAINT events_client_id_fkey_v2 - FOREIGN KEY (client_id) - REFERENCES public.oauth_clients(id) - NOT VALID; - -ALTER TABLE public.events - VALIDATE CONSTRAINT events_client_id_fkey_v2; - -ALTER TABLE public.events - DROP CONSTRAINT IF EXISTS events_client_id_fkey; - -ALTER TABLE public.events - RENAME CONSTRAINT events_client_id_fkey_v2 TO events_client_id_fkey; diff --git a/db/migrations/20260425100000_normalize_watcher_feedback.sql b/db/migrations/20260425100000_normalize_watcher_feedback.sql deleted file mode 100644 index 98db0c3da..000000000 --- a/db/migrations/20260425100000_normalize_watcher_feedback.sql +++ /dev/null @@ -1,91 +0,0 @@ --- migrate:up - --- Normalize watcher feedback to one row per corrected field. --- --- The original `watcher_window_feedback` stored every correction batch as a --- single JSONB blob (`corrections`) with one shared `notes` column. That --- shape blocks per-field notes, makes "latest correction per field" a JSONB --- aggregation, and lets duplicate submissions for the same field accumulate --- forever (the prompt summary then injects every historical version). --- --- New table is per-field with explicit mutation kind so structural edits --- (remove an array item, append a new one) live alongside value corrections --- without overloading the value column. Existing rows are migrated by --- expanding the JSONB map into one row per key. - -CREATE TABLE IF NOT EXISTS public.watcher_window_field_feedback ( - id bigserial PRIMARY KEY, - window_id integer NOT NULL REFERENCES public.watcher_windows(id) ON DELETE CASCADE, - watcher_id integer NOT NULL REFERENCES public.watchers(id) ON DELETE CASCADE, - organization_id text NOT NULL, - field_path text NOT NULL, - mutation text NOT NULL DEFAULT 'set' - CHECK (mutation IN ('set', 'remove', 'add')), - corrected_value jsonb, - note text, - created_by text NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT now() -); - -CREATE INDEX IF NOT EXISTS idx_wwff_watcher_field_recent - ON public.watcher_window_field_feedback (watcher_id, field_path, created_at DESC); -CREATE INDEX IF NOT EXISTS idx_wwff_window - ON public.watcher_window_field_feedback (window_id); - -INSERT INTO public.watcher_window_field_feedback ( - window_id, watcher_id, organization_id, - field_path, mutation, corrected_value, note, - created_by, created_at -) -SELECT - f.window_id, - f.watcher_id, - f.organization_id, - kv.key AS field_path, - 'set' AS mutation, - kv.value AS corrected_value, - f.notes AS note, - f.created_by, - f.created_at -FROM public.watcher_window_feedback f, - LATERAL jsonb_each(f.corrections) AS kv; - -DROP TABLE IF EXISTS public.watcher_window_feedback; - --- migrate:down - -CREATE TABLE IF NOT EXISTS public.watcher_window_feedback ( - id bigserial PRIMARY KEY, - window_id integer NOT NULL REFERENCES public.watcher_windows(id) ON DELETE CASCADE, - watcher_id integer NOT NULL REFERENCES public.watchers(id) ON DELETE CASCADE, - organization_id text NOT NULL, - corrections jsonb NOT NULL, - notes text, - created_by text NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT now() -); - -CREATE INDEX IF NOT EXISTS idx_wwf_window ON public.watcher_window_feedback(window_id); -CREATE INDEX IF NOT EXISTS idx_wwf_watcher ON public.watcher_window_feedback(watcher_id); - --- Best-effort rollback: structural mutations ('add' / 'remove') don't fit --- the old shape and are dropped (WHERE mutation = 'set'). When multiple --- contributors edited the same window, MAX(note) and MAX(created_by) pick --- one arbitrarily — this is an emergency recovery path, not a clean inverse. -INSERT INTO public.watcher_window_feedback ( - window_id, watcher_id, organization_id, - corrections, notes, created_by, created_at -) -SELECT - window_id, - watcher_id, - organization_id, - jsonb_object_agg(field_path, COALESCE(corrected_value, 'null'::jsonb)) AS corrections, - MAX(note) AS notes, - MAX(created_by) AS created_by, - MAX(created_at) AS created_at -FROM public.watcher_window_field_feedback -WHERE mutation = 'set' -GROUP BY window_id, watcher_id, organization_id; - -DROP TABLE IF EXISTS public.watcher_window_field_feedback; diff --git a/db/migrations/20260425120000_add_run_diagnostics.sql b/db/migrations/20260425120000_add_run_diagnostics.sql deleted file mode 100644 index eb12f3cdd..000000000 --- a/db/migrations/20260425120000_add_run_diagnostics.sql +++ /dev/null @@ -1,20 +0,0 @@ --- migrate:up - --- Add diagnostic columns to runs so subprocess failures carry enough context --- to diagnose without reading gateway pod logs. --- --- output_tail: redacted tail (~16 KiB) of child stdout+stderr at exit. --- exit_code: subprocess exit code, NULL when terminated by IPC error path. --- exit_signal: signal name (e.g. SIGKILL) when killed. --- exit_reason: categorized — ok | error_message | timeout | oom | crash. -ALTER TABLE public.runs ADD COLUMN output_tail TEXT NULL; -ALTER TABLE public.runs ADD COLUMN exit_code INTEGER NULL; -ALTER TABLE public.runs ADD COLUMN exit_signal TEXT NULL; -ALTER TABLE public.runs ADD COLUMN exit_reason TEXT NULL; - --- migrate:down - -ALTER TABLE public.runs DROP COLUMN IF EXISTS exit_reason; -ALTER TABLE public.runs DROP COLUMN IF EXISTS exit_signal; -ALTER TABLE public.runs DROP COLUMN IF EXISTS exit_code; -ALTER TABLE public.runs DROP COLUMN IF EXISTS output_tail; diff --git a/db/migrations/20260425130000_add_repair_agent_plumbing.sql b/db/migrations/20260425130000_add_repair_agent_plumbing.sql deleted file mode 100644 index 011a0d7e9..000000000 --- a/db/migrations/20260425130000_add_repair_agent_plumbing.sql +++ /dev/null @@ -1,46 +0,0 @@ --- migrate:up - --- Repair-agent plumbing for connector reliability. --- --- When a feed accumulates persistent failures, the worker-completion path --- can open a Lobu agent thread for triage. These columns track the --- per-feed override, the open thread (if any), the lifetime budget of --- repair attempts, the start of the current failure streak, and the --- last-posted content hash for append throttling. -ALTER TABLE public.feeds - ADD COLUMN repair_agent_id text NULL, - ADD COLUMN repair_thread_id text NULL, - ADD COLUMN repair_attempt_count integer NOT NULL DEFAULT 0, - ADD COLUMN last_repair_at timestamp with time zone NULL, - ADD COLUMN first_failure_at timestamp with time zone NULL, - ADD COLUMN last_repair_post_hash text NULL; - --- The unique partial index documents intent: a feed has at most one open --- repair thread at a time. (`feeds.id` is already PK so the constraint is --- redundant for uniqueness; the actual race guard is the conditional --- UPDATE on repair_thread_id IS NULL in the worker-completion path.) -CREATE UNIQUE INDEX feeds_open_repair_thread_uniq - ON public.feeds (id) WHERE repair_thread_id IS NOT NULL; - --- Per-connector default repair agent. When a feed has no explicit --- repair_agent_id, the trigger logic falls back to this value. -ALTER TABLE public.connector_definitions - ADD COLUMN default_repair_agent_id text NULL; - --- Per-org kill switch. When FALSE, no repair threads are opened for any --- feed in the org regardless of per-feed configuration. -ALTER TABLE public.organization - ADD COLUMN repair_agents_enabled boolean NOT NULL DEFAULT TRUE; - --- migrate:down - -ALTER TABLE public.organization DROP COLUMN IF EXISTS repair_agents_enabled; -ALTER TABLE public.connector_definitions DROP COLUMN IF EXISTS default_repair_agent_id; -DROP INDEX IF EXISTS feeds_open_repair_thread_uniq; -ALTER TABLE public.feeds - DROP COLUMN IF EXISTS last_repair_post_hash, - DROP COLUMN IF EXISTS first_failure_at, - DROP COLUMN IF EXISTS last_repair_at, - DROP COLUMN IF EXISTS repair_attempt_count, - DROP COLUMN IF EXISTS repair_thread_id, - DROP COLUMN IF EXISTS repair_agent_id; diff --git a/db/migrations/20260426120000_entities_entity_type_fk.sql b/db/migrations/20260426120000_entities_entity_type_fk.sql deleted file mode 100644 index 264fc56ce..000000000 --- a/db/migrations/20260426120000_entities_entity_type_fk.sql +++ /dev/null @@ -1,101 +0,0 @@ --- migrate:up - --- Convert entities.entity_type from a text slug to an FK on entity_types(id). --- Two motivations folded into one change: --- --- 1. Integrity. Today entity_types renames orphan all referencing entities --- (slug-based reference is silent FK with no enforcement). Hard-deletes --- bypass the validator entirely. With a real FK, Postgres refuses to --- drop a referenced type and renames update for free (the slug becomes --- display only — JOIN to entity_types for it). --- --- 2. Cross-org vocabulary. entity_types.id is globally unique (one sequence --- across all orgs), so an entity in tenant org A can carry a type defined --- in public-catalog org B by FK alone. No additional org_id column on --- entities is needed once the slug-based same-org coupling is gone. --- --- Single-prod-DB migration: add nullable column, backfill, fail loudly on --- orphans, set NOT NULL, drop the text column. Run manually. - --- 1. Add the FK column, nullable for backfill. -ALTER TABLE public.entities - ADD COLUMN entity_type_id integer REFERENCES public.entity_types(id); - --- 2. Backfill from existing (organization_id, entity_type slug) → entity_types.id. --- Prefer live entity_types rows; fall back to soft-deleted ones to preserve --- history. Without the ORDER BY, a slug+org pair with both an active and a --- soft-deleted row would resolve non-deterministically — entity_types' UNIQUE --- index on slug only covers `deleted_at IS NULL` rows, so collisions can exist. -UPDATE public.entities e -SET entity_type_id = ( - SELECT et.id - FROM public.entity_types et - WHERE et.slug = e.entity_type - AND et.organization_id = e.organization_id - ORDER BY (et.deleted_at IS NULL) DESC, et.id DESC - LIMIT 1 -) -WHERE e.entity_type_id IS NULL; - --- 3. Fail loudly on orphans. If any entities reference a slug with no matching --- entity_types row, that's pre-existing data corruption from the slug-based --- regime. Surface it; don't paper over. -DO $$ -DECLARE - orphan_count integer; -BEGIN - SELECT COUNT(*) INTO orphan_count FROM public.entities WHERE entity_type_id IS NULL; - IF orphan_count > 0 THEN - RAISE EXCEPTION - 'entity_type FK migration: % entities have entity_type slugs with no matching entity_types row. Investigate before re-running.', - orphan_count; - END IF; -END $$; - --- 4. Tighten the FK column. -ALTER TABLE public.entities - ALTER COLUMN entity_type_id SET NOT NULL; - --- 5. Index for filter/list queries that previously used entity_type slug. -CREATE INDEX idx_entities_entity_type_id - ON public.entities (entity_type_id) - WHERE deleted_at IS NULL; - --- 6. Drop the redundant UNIQUE constraint that referenced entity_type. The --- stronger `entities_slug_parent_unique` (UNIQUE on org_id, COALESCE(parent_id, --- 0), slug) already enforces slug uniqueness within (org, parent) regardless --- of entity type, with NULL-parent collapsing — so this constraint never --- caught anything the index didn't already catch. Drop it explicitly rather --- than letting DROP COLUMN cascade silently. -ALTER TABLE public.entities - DROP CONSTRAINT IF EXISTS entities_organization_id_entity_type_slug_parent_id_key; - --- 7. Drop the column comment so DROP COLUMN doesn't carry a stale doc string --- if this migration is ever rolled back and re-applied. -COMMENT ON COLUMN public.entities.entity_type IS NULL; - --- 8. Drop the text column. All readers JOIN to entity_types for the slug. -ALTER TABLE public.entities DROP COLUMN entity_type; - - --- migrate:down - -ALTER TABLE public.entities ADD COLUMN entity_type text; - -UPDATE public.entities e -SET entity_type = et.slug -FROM public.entity_types et -WHERE et.id = e.entity_type_id; - -ALTER TABLE public.entities ALTER COLUMN entity_type SET NOT NULL; - -COMMENT ON COLUMN public.entities.entity_type IS - 'Type of entity: brand, product (future: location, feature, team)'; - -ALTER TABLE public.entities - ADD CONSTRAINT entities_organization_id_entity_type_slug_parent_id_key - UNIQUE (organization_id, entity_type, slug, parent_id); - -DROP INDEX IF EXISTS public.idx_entities_entity_type_id; - -ALTER TABLE public.entities DROP COLUMN entity_type_id; diff --git a/db/migrations/20260426130000_db_integrity_cleanup.sql b/db/migrations/20260426130000_db_integrity_cleanup.sql deleted file mode 100644 index 648e06eaa..000000000 --- a/db/migrations/20260426130000_db_integrity_cleanup.sql +++ /dev/null @@ -1,104 +0,0 @@ --- migrate:up - --- DB integrity cleanup pass (transactional half). --- --- This file groups the changes that are safe to run inside dbmate's default --- per-migration transaction: small-table tightenings and a UNIQUE swap that --- only takes a brief ACCESS EXCLUSIVE lock. Operations that would hold a --- long lock on busy tables (events.created_by NOT NULL, runs CHECK swap, --- connections UNIQUE) live in the companion `transaction:false` migration --- 20260426120001_db_integrity_cleanup_concurrent.sql. --- --- Each tightening validates its precondition first and aborts loudly via --- RAISE EXCEPTION if dirty data is present, so a green run on staging is a --- strong signal for production. - --- 1. Drop the legacy events archive (renamed during the append-only cutover --- on 2026-04-06; no application code references it). - -DROP TABLE IF EXISTS public.events_legacy_pre_append_only_20260406; - --- 2. event_classifiers.status / organization_id -> NOT NULL. --- `status` already has DEFAULT 'active' but is nullable; backfill any --- pre-default rows then enforce. `organization_id` should never be null. - -UPDATE public.event_classifiers - SET status = 'active' - WHERE status IS NULL; - -DO $$ -DECLARE - null_org bigint; -BEGIN - SELECT count(*) INTO null_org - FROM public.event_classifiers - WHERE organization_id IS NULL; - IF null_org > 0 THEN - RAISE EXCEPTION - 'event_classifiers has % row(s) with NULL organization_id; backfill before re-running this migration', - null_org; - END IF; -END$$; - -ALTER TABLE public.event_classifiers - ALTER COLUMN status SET NOT NULL, - ALTER COLUMN organization_id SET NOT NULL; - --- 3. watchers.status / organization_id -> NOT NULL. --- `status` has DEFAULT 'active'; `organization_id` should never be null. - -UPDATE public.watchers - SET status = 'active' - WHERE status IS NULL; - -DO $$ -DECLARE - null_org bigint; -BEGIN - SELECT count(*) INTO null_org - FROM public.watchers - WHERE organization_id IS NULL; - IF null_org > 0 THEN - RAISE EXCEPTION - 'watchers has % row(s) with NULL organization_id; backfill before re-running this migration', - null_org; - END IF; -END$$; - -ALTER TABLE public.watchers - ALTER COLUMN status SET NOT NULL, - ALTER COLUMN organization_id SET NOT NULL; - --- 4. event_classifiers UNIQUE -> NULLS NOT DISTINCT. --- The previous (entity_id, watcher_id, slug) UNIQUE silently allowed --- duplicates whenever entity_id or watcher_id were NULL because Postgres --- treats NULLs as distinct. PG15+ supports NULLS NOT DISTINCT for the --- intended "one row per scope+slug" semantic. --- The table is small; the brief ACCESS EXCLUSIVE inside this transaction --- is acceptable. - -ALTER TABLE public.event_classifiers - DROP CONSTRAINT event_classifiers_unique_per_insight; - -ALTER TABLE public.event_classifiers - ADD CONSTRAINT event_classifiers_unique_per_insight - UNIQUE NULLS NOT DISTINCT (entity_id, watcher_id, slug); - --- migrate:down - -ALTER TABLE public.event_classifiers - DROP CONSTRAINT event_classifiers_unique_per_insight; - -ALTER TABLE public.event_classifiers - ADD CONSTRAINT event_classifiers_unique_per_insight - UNIQUE (entity_id, watcher_id, slug); - -ALTER TABLE public.watchers - ALTER COLUMN organization_id DROP NOT NULL, - ALTER COLUMN status DROP NOT NULL; - -ALTER TABLE public.event_classifiers - ALTER COLUMN organization_id DROP NOT NULL, - ALTER COLUMN status DROP NOT NULL; - --- The legacy events table is intentionally not recreated on rollback. diff --git a/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql b/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql deleted file mode 100644 index 1cfbb6888..000000000 --- a/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql +++ /dev/null @@ -1,187 +0,0 @@ --- migrate:up transaction:false - --- DB integrity cleanup pass (non-transactional half). --- --- Statements here would hold ACCESS EXCLUSIVE on busy tables for the --- duration of a table scan or index build if wrapped in dbmate's default --- per-migration transaction. Running with `transaction:false` lets each --- statement release its lock on completion so VALIDATE CONSTRAINT only --- needs SHARE UPDATE EXCLUSIVE (compatible with concurrent reads/writes) --- and CREATE INDEX CONCURRENTLY can do its non-blocking build. --- --- All steps are written to be idempotent: re-running after a partial --- failure converges on the desired state. Validation aborts (RAISE --- EXCEPTION) leave the migration NOT applied so dbmate retries cleanly. --- --- Set a short lock_timeout so any contention surfaces as a fast failure --- instead of a stuck deploy. -SET lock_timeout = '5s'; - --- 1. events.created_by -> NOT NULL. --- ADD CHECK NOT VALID adds the catalog row under a brief ACCESS --- EXCLUSIVE without scanning the table. VALIDATE then takes only --- SHARE UPDATE EXCLUSIVE so concurrent reads/writes are unaffected. --- Once validated, SET NOT NULL is metadata-only because Postgres uses --- the validated CHECK as proof there are no NULLs (PG12+). - -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 - FROM pg_constraint - WHERE conrelid = 'public.events'::regclass - AND conname = 'events_created_by_not_null' - ) THEN - ALTER TABLE public.events - ADD CONSTRAINT events_created_by_not_null - CHECK (created_by IS NOT NULL) NOT VALID; - END IF; -END$$; - --- Backfill historical NULLs with the existing 'system' sentinel before --- VALIDATE. Pre-`created_by` events end up here. --- --- The session's lock_timeout is 5s (set at the top of this migration) --- which is appropriate for DDL but too aggressive for a row-level --- backfill running concurrently with live inserts: the deploying --- gateway and the live old pod can hold per-row locks briefly, and --- our UPDATE waits on each one. Bump locally for the UPDATE and the --- VALIDATE that follows, then restore. -SET lock_timeout = 0; -UPDATE public.events SET created_by = 'system' WHERE created_by IS NULL; - -ALTER TABLE public.events - VALIDATE CONSTRAINT events_created_by_not_null; - -ALTER TABLE public.events - ALTER COLUMN created_by SET NOT NULL; - -ALTER TABLE public.events - DROP CONSTRAINT IF EXISTS events_created_by_not_null; - -SET lock_timeout = '5s'; - --- 2. Prune historical run_type values from the runs CHECK. --- 'embed_backfill' is still in active use and stays. 'insight' and --- 'code' are remnants of earlier orchestration models with no --- production references; abort if any rows still carry them. --- Use the NOT VALID + VALIDATE pattern so the swap doesn't hold --- ACCESS EXCLUSIVE on runs across the table scan. - -DO $$ -DECLARE - historical bigint; -BEGIN - SELECT count(*) INTO historical - FROM public.runs - WHERE run_type IN ('insight', 'code'); - IF historical > 0 THEN - RAISE EXCEPTION - 'runs has % row(s) with deprecated run_type (insight/code); migrate them before re-running', - historical; - END IF; -END$$; - -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 - FROM pg_constraint - WHERE conrelid = 'public.runs'::regclass - AND conname = 'runs_run_type_check_v2' - ) THEN - ALTER TABLE public.runs - ADD CONSTRAINT runs_run_type_check_v2 - CHECK (run_type = ANY (ARRAY[ - 'sync'::text, - 'action'::text, - 'embed_backfill'::text, - 'watcher'::text, - 'auth'::text - ])) NOT VALID; - END IF; -END$$; - -ALTER TABLE public.runs - VALIDATE CONSTRAINT runs_run_type_check_v2; - -ALTER TABLE public.runs - DROP CONSTRAINT IF EXISTS runs_run_type_check; - -ALTER TABLE public.runs - RENAME CONSTRAINT runs_run_type_check_v2 TO runs_run_type_check; - --- 3. Connections natural-key UNIQUE among live, authenticated rows. --- A reconnect should refresh credentials in place, not insert a new --- row. Partial index limits scope to non-deleted rows with an --- account_id so soft-deletes and unauthenticated stubs don't block --- re-creation. CONCURRENTLY avoids blocking writes during the build. - -DO $$ -DECLARE - dup_groups bigint; -BEGIN - SELECT count(*) INTO dup_groups - FROM ( - SELECT 1 - FROM public.connections - WHERE deleted_at IS NULL - AND account_id IS NOT NULL - GROUP BY organization_id, connector_key, account_id - HAVING count(*) > 1 - ) t; - IF dup_groups > 0 THEN - RAISE EXCEPTION - 'connections has % duplicate (organization_id, connector_key, account_id) group(s) among live rows; dedupe before re-running', - dup_groups; - END IF; -END$$; - --- A previous failed run can leave an INVALID index behind. `IF NOT EXISTS` --- alone would skip creation in that case, leaving uniqueness unenforced. --- Drop first (no-op when the index doesn't exist) so the create always --- produces a valid index. --- --- Originally CONCURRENTLY (which is why this migration is in the --- transaction:false half) but dbmate's transaction handling against --- the pq driver still presents these as in-transaction to Postgres, --- failing with "DROP INDEX CONCURRENTLY cannot run inside a --- transaction block". The connections table only has a handful of --- rows matching the partial-index predicate (≈2 in prod), so the --- ACCESS EXCLUSIVE held during a non-concurrent build is sub-second. -DROP INDEX IF EXISTS public.idx_connections_org_connector_account_live; - -CREATE UNIQUE INDEX idx_connections_org_connector_account_live - ON public.connections (organization_id, connector_key, account_id) - WHERE deleted_at IS NULL AND account_id IS NOT NULL; - --- migrate:down transaction:false - -DROP INDEX CONCURRENTLY IF EXISTS public.idx_connections_org_connector_account_live; - -DO $$ -BEGIN - IF EXISTS ( - SELECT 1 - FROM pg_constraint - WHERE conrelid = 'public.runs'::regclass - AND conname = 'runs_run_type_check' - ) THEN - ALTER TABLE public.runs DROP CONSTRAINT runs_run_type_check; - END IF; -END$$; - -ALTER TABLE public.runs - ADD CONSTRAINT runs_run_type_check - CHECK (run_type = ANY (ARRAY[ - 'sync'::text, - 'action'::text, - 'code'::text, - 'insight'::text, - 'embed_backfill'::text, - 'watcher'::text, - 'auth'::text - ])); - -ALTER TABLE public.events - ALTER COLUMN created_by DROP NOT NULL; diff --git a/db/migrations/20260427133000_events_created_by_nullable.sql b/db/migrations/20260427133000_events_created_by_nullable.sql deleted file mode 100644 index bcae4fec9..000000000 --- a/db/migrations/20260427133000_events_created_by_nullable.sql +++ /dev/null @@ -1,74 +0,0 @@ --- migrate:up transaction:false - --- `events.created_by` represents the Lobu user that explicitly saved or --- authored an event. Connector/scheduled/system-produced events have no human --- saver; their provenance is represented by connector_key, connection_id, --- feed_id, run_id, and client_id. Keep user attribution nullable and enforce --- that any non-null value is a real user id instead of a sentinel string. -SET lock_timeout = '5s'; - -ALTER TABLE public.events - ALTER COLUMN created_by DROP NOT NULL; - --- During a rolling deploy, old application pods may still write the historical --- 'system'/'api' sentinel actors. Normalize only those known sentinels to NULL --- before the FK sees them. Unknown non-user values should still be rejected. -CREATE OR REPLACE FUNCTION public.normalize_event_created_by() -RETURNS trigger -LANGUAGE plpgsql -AS $$ -BEGIN - IF NEW.created_by IN ('system', 'api') AND NOT EXISTS ( - SELECT 1 FROM public."user" u WHERE u.id = NEW.created_by - ) THEN - NEW.created_by := NULL; - END IF; - RETURN NEW; -END; -$$; - -DROP TRIGGER IF EXISTS normalize_event_created_by ON public.events; -CREATE TRIGGER normalize_event_created_by - BEFORE INSERT OR UPDATE OF created_by ON public.events - FOR EACH ROW - EXECUTE FUNCTION public.normalize_event_created_by(); - --- The previous integrity migration backfilled NULL event actors to 'system'. --- Convert those sentinel values back to NULL. This uses idx_events_created_by. -SET lock_timeout = 0; -UPDATE public.events e - SET created_by = NULL - WHERE e.created_by IN ('system', 'api') - AND NOT EXISTS ( - SELECT 1 FROM public."user" u WHERE u.id = e.created_by - ); -SET lock_timeout = '5s'; - -ALTER TABLE public.events - ADD CONSTRAINT events_created_by_fkey - FOREIGN KEY (created_by) - REFERENCES public."user"(id) - ON DELETE SET NULL - NOT VALID; - -SET lock_timeout = 0; -ALTER TABLE public.events - VALIDATE CONSTRAINT events_created_by_fkey; -SET lock_timeout = '5s'; - --- migrate:down transaction:false - -ALTER TABLE public.events - DROP CONSTRAINT IF EXISTS events_created_by_fkey; - -DROP TRIGGER IF EXISTS normalize_event_created_by ON public.events; -DROP FUNCTION IF EXISTS public.normalize_event_created_by(); - -SET lock_timeout = 0; -UPDATE public.events - SET created_by = 'system' - WHERE created_by IS NULL; -SET lock_timeout = '5s'; - -ALTER TABLE public.events - ALTER COLUMN created_by SET NOT NULL; diff --git a/db/migrations/20260427140000_identity_engine_indexes.sql b/db/migrations/20260427140000_identity_engine_indexes.sql deleted file mode 100644 index 3869d036a..000000000 --- a/db/migrations/20260427140000_identity_engine_indexes.sql +++ /dev/null @@ -1,140 +0,0 @@ --- migrate:up - --- Identity-engine schema additions. --- --- The follow-up identity engine writes connector-emitted facts as rows in --- `events` with `semantic_type='identity_fact'` and stores derivation --- provenance as metadata on `entity_relationships`. Auto-create rules live --- as JSONB on `entity_relationship_types.metadata` (compiled from YAML by --- the seeder). All three shapes need selective indexes so the hot paths --- don't full-scan. --- --- Pattern matches the existing per-namespace event metadata indexes added --- in 20260419120000_add_event_identity_indexes.sql. - --- ── Rule storage on relationship types ───────────────────────────────── --- The engine reads each relationship type's `metadata.autoCreateWhen[]` to --- decide which rules to fire on each incoming fact. Adding the column up --- front (NULL allowed) keeps the seeder change non-destructive. -ALTER TABLE public.entity_relationship_types - ADD COLUMN IF NOT EXISTS metadata jsonb; - -CREATE INDEX IF NOT EXISTS idx_entity_relationship_types_has_auto_create - ON public.entity_relationship_types ((metadata->'autoCreateWhen')) - WHERE metadata ? 'autoCreateWhen' AND deleted_at IS NULL; - --- ── Identity lookup ───────────────────────────────────────────────────── --- "Find the entity in catalog X whose normalized identity value matches --- this fact's normalizedValue." Composite expression index keyed on the --- (org, namespace, normalizedValue) tuple. Partial: only fact-typed events --- get indexed, so total size scales with fact volume (small) not event --- volume (huge). -CREATE INDEX IF NOT EXISTS idx_events_identity_fact_lookup - ON public.events ( - organization_id, - (metadata->>'namespace'), - (metadata->>'normalizedValue') - ) - WHERE semantic_type = 'identity_fact'; - --- ── Per-account fact diff ─────────────────────────────────────────────── --- "Find every active fact for this provider account." Used by the engine --- to diff prior facts vs current set on refresh — drops fall out of the --- result and get superseded. Keyed on `providerStableId` (not --- `sourceAccountId`): BetterAuth may issue a fresh account row on --- reconnect; we want facts to chain across that boundary. -CREATE INDEX IF NOT EXISTS idx_events_identity_fact_account - ON public.events ( - connector_key, - (metadata->>'providerStableId'), - (metadata->>'namespace') - ) - WHERE semantic_type = 'identity_fact'; - --- ── Provenance reverse-lookup ─────────────────────────────────────────── --- "Find every relationship derived from this fact event." Used at --- revocation: when a fact is superseded, find auto-created relationships --- that referenced its event_id and revoke them. -CREATE INDEX IF NOT EXISTS idx_entity_relationships_derived_from_event - ON public.entity_relationships ( - ((metadata->'derivedFrom'->>'sourceEventId')) - ) - WHERE metadata ? 'derivedFrom'; - --- ── Rule-version drift detection ──────────────────────────────────────── --- "Find every relationship derived from this rule type at this version." --- Used by reconcile when a rule changes — find derivations stamped with --- an older version, revoke or refresh them. -CREATE INDEX IF NOT EXISTS idx_entity_relationships_derived_from_rule - ON public.entity_relationships ( - ((metadata->'derivedFrom'->>'relationshipTypeId')), - ((metadata->'derivedFrom'->>'ruleVersion')) - ) - WHERE metadata ? 'derivedFrom'; - --- ── Idempotent derivation insert ──────────────────────────────────────── --- Pi P0.3 — even with the engine's advisory lock, this partial unique --- constraint catches accidental double-inserts (e.g. when a different code --- path tries to write the same auto-derived edge). ON CONFLICT DO NOTHING --- in the engine relies on this index to fire. --- --- Existing installs may already have duplicate live triples. Collapse those --- first so adding the invariant is safe and deterministic. -WITH duplicate_live_triples AS ( - SELECT id, - ROW_NUMBER() OVER ( - PARTITION BY from_entity_id, to_entity_id, relationship_type_id - ORDER BY created_at ASC NULLS LAST, id ASC - ) AS rn - FROM public.entity_relationships - WHERE deleted_at IS NULL -) -UPDATE public.entity_relationships r -SET deleted_at = NOW(), updated_at = NOW() -FROM duplicate_live_triples d -WHERE r.id = d.id - AND d.rn > 1; - -CREATE UNIQUE INDEX IF NOT EXISTS idx_entity_relationships_live_triple - ON public.entity_relationships (from_entity_id, to_entity_id, relationship_type_id) - WHERE deleted_at IS NULL; - --- ── Per-namespace entity metadata indexes ─────────────────────────────── --- Pi P1.10 — the engine's findEntitiesByMetadataField does --- `WHERE metadata->>$field = $value`. Without per-field expression indexes --- this is a sequential scan on the entities table for every fact it --- processes. Add the common identity namespaces here; future namespaces --- get an entry in this same list when their first rule lands. --- --- `idx_entities_metadata_domain` already exists in the baseline schema. - -CREATE INDEX IF NOT EXISTS idx_entities_metadata_email - ON public.entities ((metadata->>'email'), organization_id) - WHERE (metadata->>'email') IS NOT NULL AND deleted_at IS NULL; - -CREATE INDEX IF NOT EXISTS idx_entities_metadata_linkedin_url - ON public.entities ((metadata->>'linkedin_url'), organization_id) - WHERE (metadata->>'linkedin_url') IS NOT NULL AND deleted_at IS NULL; - -CREATE INDEX IF NOT EXISTS idx_entities_metadata_github_handle - ON public.entities ((metadata->>'github_handle'), organization_id) - WHERE (metadata->>'github_handle') IS NOT NULL AND deleted_at IS NULL; - -CREATE INDEX IF NOT EXISTS idx_entities_metadata_twitter_handle - ON public.entities ((metadata->>'twitter_handle'), organization_id) - WHERE (metadata->>'twitter_handle') IS NOT NULL AND deleted_at IS NULL; - - --- migrate:down - -DROP INDEX IF EXISTS public.idx_entities_metadata_twitter_handle; -DROP INDEX IF EXISTS public.idx_entities_metadata_github_handle; -DROP INDEX IF EXISTS public.idx_entities_metadata_linkedin_url; -DROP INDEX IF EXISTS public.idx_entities_metadata_email; -DROP INDEX IF EXISTS public.idx_entity_relationships_live_triple; -DROP INDEX IF EXISTS public.idx_entity_relationships_derived_from_rule; -DROP INDEX IF EXISTS public.idx_entity_relationships_derived_from_event; -DROP INDEX IF EXISTS public.idx_events_identity_fact_account; -DROP INDEX IF EXISTS public.idx_events_identity_fact_lookup; -DROP INDEX IF EXISTS public.idx_entity_relationship_types_has_auto_create; -ALTER TABLE public.entity_relationship_types DROP COLUMN IF EXISTS metadata; diff --git a/db/migrations/20260427150000_drop_events_source_id.sql b/db/migrations/20260427150000_drop_events_source_id.sql deleted file mode 100644 index e01f0b89a..000000000 --- a/db/migrations/20260427150000_drop_events_source_id.sql +++ /dev/null @@ -1,177 +0,0 @@ --- migrate:up transaction:false - --- `events.source_id` is legacy and duplicates `events.connection_id`. New code --- no longer writes or reads it, and scoring now partitions by connection_id. --- This migration intentionally runs after that code is deployed. -SET lock_timeout = '5s'; - -DROP VIEW IF EXISTS public.event_thread_tree; -DROP VIEW IF EXISTS public.current_event_records; - -DROP INDEX IF EXISTS public.idx_event_length; -DROP INDEX IF EXISTS public.idx_event_source_id; - -ALTER TABLE public.events - DROP COLUMN IF EXISTS source_id; - -DROP TRIGGER IF EXISTS normalize_event_created_by ON public.events; -DROP FUNCTION IF EXISTS public.normalize_event_created_by(); - -CREATE VIEW public.current_event_records AS - SELECT e.id, - e.organization_id, - e.entity_ids, - e.origin_id, - e.title, - e.payload_type, - e.payload_text, - e.payload_data, - e.payload_template, - e.attachments, - e.metadata, - e.score, - emb.embedding, - e.author_name, - e.source_url, - e.occurred_at, - e.created_at, - e.origin_parent_id, - COALESCE(length(e.payload_text), 0) AS content_length, - e.origin_type, - e.connector_key, - e.connection_id, - e.feed_key, - e.feed_id, - e.run_id, - e.semantic_type, - e.client_id, - e.created_by, - e.interaction_type, - e.interaction_status, - e.interaction_input_schema, - e.interaction_input, - e.interaction_output, - e.interaction_error, - e.supersedes_event_id - FROM (public.events e - LEFT JOIN public.event_embeddings emb ON ((emb.event_id = e.id))) - WHERE (NOT (EXISTS ( SELECT 1 - FROM public.events newer - WHERE (newer.supersedes_event_id = e.id)))); - -CREATE VIEW public.event_thread_tree AS - SELECT e.id, - e.origin_id, - e.origin_parent_id, - e.occurred_at, - COALESCE(parent.origin_id, e.origin_id) AS root_origin_id, - COALESCE(parent.occurred_at, e.occurred_at) AS root_occurred_at, - COALESCE(parent.score, e.score) AS root_score, - CASE - WHEN (e.origin_parent_id IS NULL) THEN 0 - ELSE 1 - END AS depth, - ARRAY[(COALESCE(parent.occurred_at, e.occurred_at))::text, (e.id)::text] AS sort_path - FROM (public.current_event_records e - LEFT JOIN public.current_event_records parent ON (((e.origin_parent_id = parent.origin_id) AND (e.entity_ids && parent.entity_ids)))); - --- migrate:down transaction:false - -SET lock_timeout = '5s'; - -DROP VIEW IF EXISTS public.event_thread_tree; -DROP VIEW IF EXISTS public.current_event_records; - -ALTER TABLE public.events - ADD COLUMN IF NOT EXISTS source_id integer; - -UPDATE public.events - SET source_id = connection_id::integer - WHERE source_id IS NULL - AND connection_id IS NOT NULL - AND connection_id <= 2147483647; - -CREATE INDEX IF NOT EXISTS idx_event_length - ON public.events (source_id, (COALESCE(length(payload_text), 0))); - -CREATE INDEX IF NOT EXISTS idx_event_source_id - ON public.events (source_id); - -CREATE OR REPLACE FUNCTION public.normalize_event_created_by() -RETURNS trigger -LANGUAGE plpgsql -AS $$ -BEGIN - IF NEW.created_by IN ('system', 'api') AND NOT EXISTS ( - SELECT 1 FROM public."user" u WHERE u.id = NEW.created_by - ) THEN - NEW.created_by := NULL; - END IF; - RETURN NEW; -END; -$$; - -DROP TRIGGER IF EXISTS normalize_event_created_by ON public.events; -CREATE TRIGGER normalize_event_created_by - BEFORE INSERT OR UPDATE OF created_by ON public.events - FOR EACH ROW - EXECUTE FUNCTION public.normalize_event_created_by(); - -CREATE VIEW public.current_event_records AS - SELECT e.id, - e.organization_id, - e.entity_ids, - e.source_id, - e.origin_id, - e.title, - e.payload_type, - e.payload_text, - e.payload_data, - e.payload_template, - e.attachments, - e.metadata, - e.score, - emb.embedding, - e.author_name, - e.source_url, - e.occurred_at, - e.created_at, - e.origin_parent_id, - COALESCE(length(e.payload_text), 0) AS content_length, - e.origin_type, - e.connector_key, - e.connection_id, - e.feed_key, - e.feed_id, - e.run_id, - e.semantic_type, - e.client_id, - e.created_by, - e.interaction_type, - e.interaction_status, - e.interaction_input_schema, - e.interaction_input, - e.interaction_output, - e.interaction_error, - e.supersedes_event_id - FROM (public.events e - LEFT JOIN public.event_embeddings emb ON ((emb.event_id = e.id))) - WHERE (NOT (EXISTS ( SELECT 1 - FROM public.events newer - WHERE (newer.supersedes_event_id = e.id)))); - -CREATE VIEW public.event_thread_tree AS - SELECT e.id, - e.origin_id, - e.origin_parent_id, - e.occurred_at, - COALESCE(parent.origin_id, e.origin_id) AS root_origin_id, - COALESCE(parent.occurred_at, e.occurred_at) AS root_occurred_at, - COALESCE(parent.score, e.score) AS root_score, - CASE - WHEN (e.origin_parent_id IS NULL) THEN 0 - ELSE 1 - END AS depth, - ARRAY[(COALESCE(parent.occurred_at, e.occurred_at))::text, (e.id)::text] AS sort_path - FROM (public.current_event_records e - LEFT JOIN public.current_event_records parent ON (((e.origin_parent_id = parent.origin_id) AND (e.entity_ids && parent.entity_ids)))); diff --git a/db/migrations/20260427160000_drop_dead_schema.sql b/db/migrations/20260427160000_drop_dead_schema.sql deleted file mode 100644 index e5a736bd8..000000000 --- a/db/migrations/20260427160000_drop_dead_schema.sql +++ /dev/null @@ -1,76 +0,0 @@ --- migrate:up - --- Drop schema objects that have no readers, writers, or references in the --- TypeScript source tree (`packages/server/src` is the source of --- truth — `lobu-cli/runtime` is a generated mirror). --- --- Verification: --- * Strict SQL grep across packages/{server,gateway,worker, --- web,connector-sdk}/src for FROM/JOIN/INSERT/UPDATE/REFERENCES. --- * `query_sql` allowlist (utils/table-schema.ts) does not list any of --- the dropped tables, columns, or the view. --- * better-auth `teams` is not enabled in src/auth/index.tsx, so the --- `team` table and `member.teamId` are unused at runtime. --- * `verification` is intentionally NOT dropped — better-auth's --- magicLink and phoneNumber plugins write OTPs/tokens there. --- --- Idempotent (DROP IF EXISTS) so it is safe whether the object exists or --- was already removed out-of-band. - --- --------------------------------------------------------------------------- --- Tables: empty in prod, no code paths reference them --- --------------------------------------------------------------------------- - -DROP TABLE IF EXISTS public.rate_limits; -DROP TABLE IF EXISTS public.workers; -DROP TABLE IF EXISTS public.source_type_auth_defaults; -DROP TABLE IF EXISTS public.workspace_settings; - --- `team` is referenced by `member.teamId`. Drop the inbound FK + column --- first so the table drop does not depend on CASCADE behavior. -ALTER TABLE public.member DROP CONSTRAINT IF EXISTS "member_teamId_fkey"; -ALTER TABLE public.member DROP COLUMN IF EXISTS "teamId"; -DROP TABLE IF EXISTS public.team; - --- --------------------------------------------------------------------------- --- Views: defined in baseline, not referenced anywhere in code --- --------------------------------------------------------------------------- - -DROP VIEW IF EXISTS public.event_thread_tree; - --- --------------------------------------------------------------------------- --- Columns: always-NULL in prod, never read or written by any code path --- --------------------------------------------------------------------------- - -ALTER TABLE public.watchers - DROP COLUMN IF EXISTS lobu_schedule_id, - DROP COLUMN IF EXISTS registry_ref, - DROP COLUMN IF EXISTS registry_repo, - DROP COLUMN IF EXISTS registry_type; - --- watcher_versions.source_path is unused (the heavily-used `source_path` --- column lives on connector_versions; same name, different table). -ALTER TABLE public.watcher_versions - DROP COLUMN IF EXISTS source_path, - DROP COLUMN IF EXISTS source_repository, - DROP COLUMN IF EXISTS source_ref, - DROP COLUMN IF EXISTS source_commit_sha, - DROP COLUMN IF EXISTS sources_schema; - -ALTER TABLE public.connector_versions - DROP COLUMN IF EXISTS source_repository, - DROP COLUMN IF EXISTS source_ref, - DROP COLUMN IF EXISTS source_commit_sha; - -ALTER TABLE public.event_embeddings - DROP COLUMN IF EXISTS model_key; - - --- migrate:down - --- This cleanup is one-way. Re-creating these objects would require copying --- DDL from the baseline migration (00000000000000_baseline.sql) and the --- column definitions inferred from prior schema dumps. Down is intentionally --- a no-op — recovery is via `git revert` of this migration plus manual DDL, --- not via dbmate rollback. -SELECT 1; diff --git a/db/migrations/20260427170000_market_founder_to_member.sql b/db/migrations/20260427170000_market_founder_to_member.sql deleted file mode 100644 index 8a4dbdf73..000000000 --- a/db/migrations/20260427170000_market_founder_to_member.sql +++ /dev/null @@ -1,364 +0,0 @@ --- migrate:up - --- Migrate `founder` entities in the `market` org to `$member` entities so --- the identity engine can adopt them on OAuth sign-in. --- --- Rationale: the engine binds a signing-in user to an existing `$member` by --- multi-namespace identity lookup (email, linkedin_url, github_username, --- etc.). Pre-curated founders need to BE `$member` rows for that adoption --- to fire. Keeping `founder` as a separate type means the engine can never --- bind a user to their pre-curated profile. --- --- The post-migration shape: --- - `$member` entities in market carry the founder's metadata, including --- `metadata.role='founder'` so the public browse route in --- `lobu-ai/lobu-web` can filter `$member` rows on role. --- - `entity_identities` rows are written for every namespace value the --- migration can extract (email, linkedin_url, twitter_handle). --- - Existing relationship rows (`works_at`, `founded`, `previously_at`, --- `educated_at`, etc.) are re-pointed from founder.id to the new --- $member.id. --- - Relationship-type rules are widened to accept `$member` as the --- source slug for the relationships that pointed at founders. --- - Source `founder` rows are soft-deleted (deleted_at=NOW()) — kept for --- audit history; the application path filters them out. --- --- Idempotent: each step uses ON CONFLICT / WHERE NOT EXISTS, so a re-run --- on a partially-migrated DB completes the unfinished work without --- duplicating rows. Safe to run on a fresh DB (no founders yet) — every --- query returns the empty set. - -DO $$ -DECLARE - v_market_org_id text; - v_market_org_slug text; - v_member_type_id integer; - v_founder_type_id integer; -BEGIN - FOR v_market_org_id, v_market_org_slug IN - SELECT id, slug - FROM public.organization - WHERE slug IN ('market', 'venture-capital') - ORDER BY CASE slug WHEN 'market' THEN 0 ELSE 1 END - LOOP - RAISE NOTICE 'running founder→$member migration for org %', v_market_org_slug; - - SELECT id INTO v_member_type_id - FROM public.entity_types - WHERE organization_id = v_market_org_id AND slug = '$member' AND deleted_at IS NULL - LIMIT 1; - IF v_member_type_id IS NULL THEN - INSERT INTO public.entity_types (organization_id, slug, name, created_at, updated_at) - VALUES (v_market_org_id, '$member', 'Member', NOW(), NOW()) - RETURNING id INTO v_member_type_id; - END IF; - - SELECT id INTO v_founder_type_id - FROM public.entity_types - WHERE organization_id = v_market_org_id AND slug = 'founder' AND deleted_at IS NULL - LIMIT 1; - - IF v_founder_type_id IS NULL THEN - RAISE NOTICE 'no founder entity_type in % — nothing to migrate', v_market_org_slug; - CONTINUE; - END IF; - - -- 1. Create $member rows for each founder. Idempotent via slug uniqueness: - -- we generate a deterministic slug derived from the founder id so a - -- re-run without source data still produces the same slug. - INSERT INTO public.entities ( - name, slug, entity_type_id, organization_id, parent_id, - metadata, created_by, created_at, updated_at - ) - SELECT - f.name, - 'member-from-founder-' || f.id::text AS slug, - v_member_type_id, - v_market_org_id, - NULL, - COALESCE(f.metadata, '{}'::jsonb) || jsonb_build_object('role', 'founder', 'migrated_from_founder_id', f.id), - COALESCE(f.created_by, 'system'), - NOW(), - NOW() - FROM public.entities f - WHERE f.organization_id = v_market_org_id - AND f.entity_type_id = v_founder_type_id - AND f.deleted_at IS NULL - AND NOT EXISTS ( - SELECT 1 FROM public.entities m - WHERE m.organization_id = v_market_org_id - AND m.entity_type_id = v_member_type_id - AND m.metadata->>'migrated_from_founder_id' = f.id::text - AND m.deleted_at IS NULL - ); - - -- 2. Write entity_identities rows for any namespace values present on - -- the founder metadata. Limited to the namespaces the engine consults. - -- email, linkedin_url, twitter_handle. - INSERT INTO public.entity_identities ( - organization_id, entity_id, namespace, identifier, source_connector - ) - SELECT - v_market_org_id, - m.id, - 'email', - LOWER(f.metadata->>'email'), - 'migration:founder_to_member' - FROM public.entities m - JOIN public.entities f - ON f.id = (m.metadata->>'migrated_from_founder_id')::bigint - WHERE m.organization_id = v_market_org_id - AND m.entity_type_id = v_member_type_id - AND m.deleted_at IS NULL - AND f.metadata ? 'email' - AND f.metadata->>'email' IS NOT NULL - AND f.metadata->>'email' <> '' - ON CONFLICT (organization_id, namespace, identifier) WHERE deleted_at IS NULL - DO NOTHING; - - INSERT INTO public.entity_identities ( - organization_id, entity_id, namespace, identifier, source_connector - ) - SELECT - v_market_org_id, - m.id, - 'linkedin_url', - f.metadata->>'linkedin_url', - 'migration:founder_to_member' - FROM public.entities m - JOIN public.entities f - ON f.id = (m.metadata->>'migrated_from_founder_id')::bigint - WHERE m.organization_id = v_market_org_id - AND m.entity_type_id = v_member_type_id - AND m.deleted_at IS NULL - AND f.metadata ? 'linkedin_url' - AND f.metadata->>'linkedin_url' IS NOT NULL - AND f.metadata->>'linkedin_url' <> '' - ON CONFLICT (organization_id, namespace, identifier) WHERE deleted_at IS NULL - DO NOTHING; - - INSERT INTO public.entity_identities ( - organization_id, entity_id, namespace, identifier, source_connector - ) - SELECT - v_market_org_id, - m.id, - 'twitter_handle', - LOWER(REPLACE(f.metadata->>'twitter_handle', '@', '')), - 'migration:founder_to_member' - FROM public.entities m - JOIN public.entities f - ON f.id = (m.metadata->>'migrated_from_founder_id')::bigint - WHERE m.organization_id = v_market_org_id - AND m.entity_type_id = v_member_type_id - AND m.deleted_at IS NULL - AND f.metadata ? 'twitter_handle' - AND f.metadata->>'twitter_handle' IS NOT NULL - AND f.metadata->>'twitter_handle' <> '' - ON CONFLICT (organization_id, namespace, identifier) WHERE deleted_at IS NULL - DO NOTHING; - - -- 3. Re-point existing relationships pointing FROM the founder rows to - -- point FROM the new $member rows. The reverse direction (relationships - -- pointing AT founders) is handled symmetrically. If the destination edge - -- already exists, archive the founder edge first so the live-triple unique - -- index remains satisfied. - UPDATE public.entity_relationships r - SET deleted_at = NOW(), updated_at = NOW() - FROM public.entities f - JOIN public.entities m - ON m.organization_id = f.organization_id - AND m.entity_type_id = v_member_type_id - AND m.metadata->>'migrated_from_founder_id' = f.id::text - AND m.deleted_at IS NULL - WHERE r.from_entity_id = f.id - AND r.deleted_at IS NULL - AND f.organization_id = v_market_org_id - AND f.entity_type_id = v_founder_type_id - AND f.deleted_at IS NULL - AND EXISTS ( - SELECT 1 - FROM public.entity_relationships existing - WHERE existing.from_entity_id = m.id - AND existing.to_entity_id = r.to_entity_id - AND existing.relationship_type_id = r.relationship_type_id - AND existing.deleted_at IS NULL - AND existing.id <> r.id - ); - - UPDATE public.entity_relationships r - SET from_entity_id = m.id, updated_at = NOW() - FROM public.entities f - JOIN public.entities m - ON m.organization_id = f.organization_id - AND m.entity_type_id = v_member_type_id - AND m.metadata->>'migrated_from_founder_id' = f.id::text - AND m.deleted_at IS NULL - WHERE r.from_entity_id = f.id - AND r.deleted_at IS NULL - AND f.organization_id = v_market_org_id - AND f.entity_type_id = v_founder_type_id - AND f.deleted_at IS NULL; - - UPDATE public.entity_relationships r - SET deleted_at = NOW(), updated_at = NOW() - FROM public.entities f - JOIN public.entities m - ON m.organization_id = f.organization_id - AND m.entity_type_id = v_member_type_id - AND m.metadata->>'migrated_from_founder_id' = f.id::text - AND m.deleted_at IS NULL - WHERE r.to_entity_id = f.id - AND r.deleted_at IS NULL - AND f.organization_id = v_market_org_id - AND f.entity_type_id = v_founder_type_id - AND f.deleted_at IS NULL - AND EXISTS ( - SELECT 1 - FROM public.entity_relationships existing - WHERE existing.from_entity_id = r.from_entity_id - AND existing.to_entity_id = m.id - AND existing.relationship_type_id = r.relationship_type_id - AND existing.deleted_at IS NULL - AND existing.id <> r.id - ); - - UPDATE public.entity_relationships r - SET to_entity_id = m.id, updated_at = NOW() - FROM public.entities f - JOIN public.entities m - ON m.organization_id = f.organization_id - AND m.entity_type_id = v_member_type_id - AND m.metadata->>'migrated_from_founder_id' = f.id::text - AND m.deleted_at IS NULL - WHERE r.to_entity_id = f.id - AND r.deleted_at IS NULL - AND f.organization_id = v_market_org_id - AND f.entity_type_id = v_founder_type_id - AND f.deleted_at IS NULL; - - -- 4. Widen relationship_type rules to accept `$member` as a source - -- where they previously accepted `founder`. Keep both slugs for now — - -- if the founder slug ever resurrects (e.g. for catalog imports that - -- still emit the old type), the rules tolerate it. - INSERT INTO public.entity_relationship_type_rules ( - relationship_type_id, source_entity_type_slug, target_entity_type_slug, created_at - ) - SELECT DISTINCT r.relationship_type_id, '$member', r.target_entity_type_slug, NOW() - FROM public.entity_relationship_type_rules r - JOIN public.entity_relationship_types rt ON rt.id = r.relationship_type_id - WHERE r.source_entity_type_slug = 'founder' - AND rt.organization_id = v_market_org_id - AND NOT EXISTS ( - SELECT 1 FROM public.entity_relationship_type_rules existing - WHERE existing.relationship_type_id = r.relationship_type_id - AND existing.source_entity_type_slug = '$member' - AND existing.target_entity_type_slug = r.target_entity_type_slug - ); - - INSERT INTO public.entity_relationship_type_rules ( - relationship_type_id, source_entity_type_slug, target_entity_type_slug, created_at - ) - SELECT DISTINCT r.relationship_type_id, r.source_entity_type_slug, '$member', NOW() - FROM public.entity_relationship_type_rules r - JOIN public.entity_relationship_types rt ON rt.id = r.relationship_type_id - WHERE r.target_entity_type_slug = 'founder' - AND rt.organization_id = v_market_org_id - AND NOT EXISTS ( - SELECT 1 FROM public.entity_relationship_type_rules existing - WHERE existing.relationship_type_id = r.relationship_type_id - AND existing.source_entity_type_slug = r.source_entity_type_slug - AND existing.target_entity_type_slug = '$member' - ); - - -- 5. Repoint any pre-existing entity_identities rows that pointed at - -- a founder. Step 2 wrote *new* identity rows for the $member, but if - -- some other path had already written identity rows on the founder - -- (e.g. an earlier provisioning script), those would dangle once the - -- founder is soft-deleted in step 7 — entity-identity lookups join on - -- entities.deleted_at IS NULL and silently miss the binding. - UPDATE public.entity_identities ei - SET deleted_at = NOW(), updated_at = NOW() - FROM public.entities f - JOIN public.entities m - ON m.organization_id = f.organization_id - AND m.entity_type_id = v_member_type_id - AND m.metadata->>'migrated_from_founder_id' = f.id::text - AND m.deleted_at IS NULL - WHERE ei.entity_id = f.id - AND ei.organization_id = v_market_org_id - AND ei.deleted_at IS NULL - AND f.organization_id = v_market_org_id - AND f.entity_type_id = v_founder_type_id - AND EXISTS ( - SELECT 1 - FROM public.entity_identities existing - WHERE existing.organization_id = ei.organization_id - AND existing.namespace = ei.namespace - AND existing.identifier = ei.identifier - AND existing.entity_id = m.id - AND existing.deleted_at IS NULL - AND existing.id <> ei.id - ); - - UPDATE public.entity_identities ei - SET entity_id = m.id, updated_at = NOW() - FROM public.entities f - JOIN public.entities m - ON m.organization_id = f.organization_id - AND m.entity_type_id = v_member_type_id - AND m.metadata->>'migrated_from_founder_id' = f.id::text - AND m.deleted_at IS NULL - WHERE ei.entity_id = f.id - AND ei.organization_id = v_market_org_id - AND ei.deleted_at IS NULL - AND f.organization_id = v_market_org_id - AND f.entity_type_id = v_founder_type_id; - - -- 6. Rewrite event.entity_ids arrays so historical events on founder - -- rows resolve to the corresponding $member going forward. array_replace - -- is a no-op on a re-run because after the first pass the founder id is - -- gone from the array and the WHERE filter excludes the row. - UPDATE public.events e - SET entity_ids = array_replace(e.entity_ids, f.id, m.id) - FROM public.entities f - JOIN public.entities m - ON m.organization_id = f.organization_id - AND m.entity_type_id = v_member_type_id - AND m.metadata->>'migrated_from_founder_id' = f.id::text - AND m.deleted_at IS NULL - WHERE e.entity_ids @> ARRAY[f.id] - AND e.organization_id = v_market_org_id - AND f.organization_id = v_market_org_id - AND f.entity_type_id = v_founder_type_id; - - -- 7. Soft-delete the source founder rows. Audit trail survives in the - -- entities table itself; queries already filter `deleted_at IS NULL`. - UPDATE public.entities f - SET deleted_at = NOW(), updated_at = NOW() - WHERE f.organization_id = v_market_org_id - AND f.entity_type_id = v_founder_type_id - AND f.deleted_at IS NULL - AND EXISTS ( - SELECT 1 FROM public.entities m - WHERE m.organization_id = v_market_org_id - AND m.entity_type_id = v_member_type_id - AND m.metadata->>'migrated_from_founder_id' = f.id::text - AND m.deleted_at IS NULL - ); - END LOOP; -END $$; - - --- migrate:down - --- Fail loudly. A bare `SELECT 1` would let an automatic rollback (CI, --- incident response, dbmate down) succeed silently while leaving the DB --- in the migrated state, masking the irreversibility. Operators wanting --- to revert run a manual playbook: clear deleted_at on the founder rows, --- re-point relationships using `metadata.migrated_from_founder_id`, then --- delete the new $member rows. -DO $$ -BEGIN - RAISE EXCEPTION 'irreversible: founder→$member is a one-way data migration. Reverse by manual playbook only.'; -END $$; diff --git a/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql b/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql deleted file mode 100644 index b66caadd0..000000000 --- a/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql +++ /dev/null @@ -1,66 +0,0 @@ --- migrate:up transaction:false - --- Add ON DELETE CASCADE FK on events.organization_id and watchers.organization_id. --- --- These two columns were declared as `text NOT NULL` with no FK to organization, --- so dropping an org left orphaned events/watchers behind that had to be DELETE'd --- separately. Every other org-scoped table has a CASCADE FK; these two were --- oversights. Add them so future org deletes are atomic with their event/watcher --- data. --- --- Lock-window strategy: ADD CONSTRAINT NOT VALID adds the catalog row under --- a brief ACCESS EXCLUSIVE without scanning the table. VALIDATE then takes --- only SHARE UPDATE EXCLUSIVE so concurrent reads and writes are unaffected. --- Running with `transaction:false` lets each ALTER release its lock on --- completion — keeping ADD + VALIDATE in a single transaction would hold --- ACCESS EXCLUSIVE through the validate scan and block writers on the --- 575k-row events table. --- --- Idempotency: each ADD is gated on `pg_constraint`. If the FK already --- exists (e.g. it was applied out-of-band before this migration was --- recorded), the ADD is skipped; VALIDATE on a constraint that's already --- validated is a no-op. - -SET lock_timeout = '5s'; - --- 1. events.organization_id → organization(id) ON DELETE CASCADE. -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 - FROM pg_constraint - WHERE conrelid = 'public.events'::regclass - AND conname = 'events_organization_id_fkey' - ) THEN - ALTER TABLE public.events - ADD CONSTRAINT events_organization_id_fkey - FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE - NOT VALID; - END IF; -END$$; - -ALTER TABLE public.events VALIDATE CONSTRAINT events_organization_id_fkey; - --- 2. watchers.organization_id → organization(id) ON DELETE CASCADE. -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 - FROM pg_constraint - WHERE conrelid = 'public.watchers'::regclass - AND conname = 'watchers_organization_id_fkey' - ) THEN - ALTER TABLE public.watchers - ADD CONSTRAINT watchers_organization_id_fkey - FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE - NOT VALID; - END IF; -END$$; - -ALTER TABLE public.watchers VALIDATE CONSTRAINT watchers_organization_id_fkey; - - --- migrate:down transaction:false - -ALTER TABLE public.events DROP CONSTRAINT IF EXISTS events_organization_id_fkey; -ALTER TABLE public.watchers DROP CONSTRAINT IF EXISTS watchers_organization_id_fkey; diff --git a/db/migrations/20260428050000_add_runs_approved_input.sql b/db/migrations/20260428050000_add_runs_approved_input.sql deleted file mode 100644 index cc2d43ef1..000000000 --- a/db/migrations/20260428050000_add_runs_approved_input.sql +++ /dev/null @@ -1,9 +0,0 @@ --- migrate:up - -ALTER TABLE public.runs - ADD COLUMN IF NOT EXISTS approved_input jsonb; - --- migrate:down - -ALTER TABLE public.runs - DROP COLUMN IF EXISTS approved_input; diff --git a/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql b/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql deleted file mode 100644 index bf29a0666..000000000 --- a/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql +++ /dev/null @@ -1,79 +0,0 @@ --- migrate:up - --- Enforce tenant scope on connection.auth_profile FKs. --- --- Background: connections.app_auth_profile_id and connections.auth_profile_id --- referenced auth_profiles(id) without checking that the profile belongs to --- the same organization as the connection. A bug in some prior create path --- left at least one connection (id=212, org buremba) pointing at an auth --- profile in a different org (id=10, org_617e78013e6ff72d). The UI correctly --- refused to load it ("Auth profile 'reddit-oauth-app' not found"), but the --- bad reference blocked setup. --- --- Fix: switch both FKs to multi-column (organization_id, profile_id) so --- references can't escape their tenant. MATCH SIMPLE (the default) keeps --- profile_id-NULL satisfying the constraint, which is what we want — the --- column stays nullable. - --- 1. Clean up any cross-org or dangling references in existing data. --- Idempotent: 0 rows once applied; safe to re-run. -UPDATE connections c -SET app_auth_profile_id = NULL, updated_at = NOW() -WHERE app_auth_profile_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM auth_profiles ap - WHERE ap.id = c.app_auth_profile_id - AND ap.organization_id = c.organization_id - ); - -UPDATE connections c -SET auth_profile_id = NULL, updated_at = NOW() -WHERE auth_profile_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM auth_profiles ap - WHERE ap.id = c.auth_profile_id - AND ap.organization_id = c.organization_id - ); - --- 2. Add (organization_id, id) UNIQUE on auth_profiles to give the new FK a --- target. Cheap on the existing 31-row table. -ALTER TABLE auth_profiles - ADD CONSTRAINT auth_profiles_org_id_unique UNIQUE (organization_id, id); - --- 3. Replace single-column FKs with tenant-scoped multi-column FKs. -ALTER TABLE connections - DROP CONSTRAINT connections_app_auth_profile_id_fkey; -ALTER TABLE connections - ADD CONSTRAINT connections_app_auth_profile_id_fkey - FOREIGN KEY (organization_id, app_auth_profile_id) - REFERENCES auth_profiles (organization_id, id) - ON DELETE SET NULL; - -ALTER TABLE connections - DROP CONSTRAINT connections_auth_profile_id_fkey; -ALTER TABLE connections - ADD CONSTRAINT connections_auth_profile_id_fkey - FOREIGN KEY (organization_id, auth_profile_id) - REFERENCES auth_profiles (organization_id, id) - ON DELETE SET NULL; - --- migrate:down - -ALTER TABLE connections - DROP CONSTRAINT connections_app_auth_profile_id_fkey; -ALTER TABLE connections - ADD CONSTRAINT connections_app_auth_profile_id_fkey - FOREIGN KEY (app_auth_profile_id) - REFERENCES auth_profiles (id) - ON DELETE SET NULL; - -ALTER TABLE connections - DROP CONSTRAINT connections_auth_profile_id_fkey; -ALTER TABLE connections - ADD CONSTRAINT connections_auth_profile_id_fkey - FOREIGN KEY (auth_profile_id) - REFERENCES auth_profiles (id) - ON DELETE SET NULL; - -ALTER TABLE auth_profiles - DROP CONSTRAINT auth_profiles_org_id_unique; diff --git a/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql b/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql deleted file mode 100644 index dd471a50a..000000000 --- a/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql +++ /dev/null @@ -1,108 +0,0 @@ --- migrate:up - --- Phase 5 of Redis -> Postgres migration: route the lobu queue (chat_message, --- thread_message_*, thread_response, schedule, agent_run) through the runs --- table instead of BullMQ. --- --- 1. Extend the run_type CHECK to allow the new lobu-queue lanes alongside --- the existing connector/auth/embed lanes. --- 2. Allow organization_id to be NULL for lobu-queue runs (chat_message, --- schedule, etc.). The original lanes still require it via the partial --- CHECK below. --- 3. Add columns required for in-process claim + retry: --- queue_name — distinguishes thread_message_* sub-queues that all --- share run_type='chat_message'. --- idempotency_key — singletonKey dedup; partial unique index below. --- attempts — current retry count. --- max_attempts — cap for retries before DLQ/failed. --- run_at — when the row becomes claimable; supports delayMs. --- 4. Add the claim index used by the in-process polling loop. Filter to the --- new run types so the connector worker (run_type IN ('sync','action',...)) --- is never woken by lobu-queue inserts and vice versa. - --- Drop the old run_type CHECK and re-add with the new lobu-queue lanes. -ALTER TABLE public.runs - DROP CONSTRAINT IF EXISTS runs_run_type_check; - -ALTER TABLE public.runs - ADD CONSTRAINT runs_run_type_check CHECK (run_type = ANY (ARRAY[ - 'sync'::text, - 'action'::text, - 'embed_backfill'::text, - 'watcher'::text, - 'auth'::text, - 'chat_message'::text, - 'schedule'::text, - 'agent_run'::text, - 'internal'::text - ])); - --- organization_id is required for connector lanes (sync/action/embed/watcher/ --- auth) but optional for lobu-queue lanes. Drop NOT NULL and enforce per-lane. -ALTER TABLE public.runs - ALTER COLUMN organization_id DROP NOT NULL; - -ALTER TABLE public.runs - ADD CONSTRAINT runs_legacy_org_required CHECK ( - run_type NOT IN ( - 'sync', 'action', 'embed_backfill', 'watcher', 'auth' - ) - OR organization_id IS NOT NULL - ); - --- Lobu queue columns. -ALTER TABLE public.runs - ADD COLUMN IF NOT EXISTS queue_name text, - ADD COLUMN IF NOT EXISTS idempotency_key text, - ADD COLUMN IF NOT EXISTS attempts integer NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS max_attempts integer NOT NULL DEFAULT 3, - ADD COLUMN IF NOT EXISTS run_at timestamp with time zone NOT NULL DEFAULT now(); - --- Idempotency: partial unique on (idempotency_key) for runs that are still in --- a non-terminal state. Once a run completes / fails / cancels / times out it --- no longer participates in dedup, so a future enqueue with the same singleton --- key (e.g. a Slack retry that lands minutes after the first attempt --- finished) can insert a fresh row. Connector lanes never set this column. -CREATE UNIQUE INDEX IF NOT EXISTS runs_idempotency_key_uniq - ON public.runs (idempotency_key) - WHERE idempotency_key IS NOT NULL - AND status IN ('pending', 'claimed', 'running'); - --- Claim index for the in-process poll loop. Limited to lobu-queue run types --- so the connector worker's HTTP-poll claim stays on its own indexes. -CREATE INDEX IF NOT EXISTS runs_lobu_claim_idx - ON public.runs (run_type, queue_name, run_at) - WHERE status = 'pending' - AND run_type IN ('chat_message', 'schedule', 'agent_run', 'internal'); - --- migrate:down - -DROP INDEX IF EXISTS public.runs_lobu_claim_idx; -DROP INDEX IF EXISTS public.runs_idempotency_key_uniq; - -ALTER TABLE public.runs - DROP COLUMN IF EXISTS run_at, - DROP COLUMN IF EXISTS max_attempts, - DROP COLUMN IF EXISTS attempts, - DROP COLUMN IF EXISTS idempotency_key, - DROP COLUMN IF EXISTS queue_name; - -ALTER TABLE public.runs - DROP CONSTRAINT IF EXISTS runs_legacy_org_required; - --- Restoring NOT NULL would fail if any lobu-queue rows still exist. Operators --- running `migrate:down` are expected to truncate or migrate those first. -ALTER TABLE public.runs - ALTER COLUMN organization_id SET NOT NULL; - -ALTER TABLE public.runs - DROP CONSTRAINT IF EXISTS runs_run_type_check; - -ALTER TABLE public.runs - ADD CONSTRAINT runs_run_type_check CHECK (run_type = ANY (ARRAY[ - 'sync'::text, - 'action'::text, - 'embed_backfill'::text, - 'watcher'::text, - 'auth'::text - ])); diff --git a/db/migrations/20260429120000_agent_changed_notify.sql b/db/migrations/20260429120000_agent_changed_notify.sql deleted file mode 100644 index 760fba71e..000000000 --- a/db/migrations/20260429120000_agent_changed_notify.sql +++ /dev/null @@ -1,97 +0,0 @@ --- migrate:up - --- Phase 6: NOTIFY trigger for invalidatableCache invalidation. --- --- The agent-related runtime caches (formerly Redis-backed) read agents, --- channel bindings, user-agent associations, and per-(user,agent) auth --- profiles directly from Postgres. Each gateway process keeps a small --- read-through cache invalidated by `pg_notify('agent_changed', )`. --- --- We emit a single channel name and let the cache implementation match --- the payload against the cached key. Channels are deliberately coarse --- (one per logical resource family) to keep the postmaster's notification --- table small — see invalidatable-cache.ts. - -CREATE OR REPLACE FUNCTION public.notify_agent_changed() -RETURNS trigger -LANGUAGE plpgsql AS $$ -BEGIN - PERFORM pg_notify('agent_changed', COALESCE(NEW.id, OLD.id)); - RETURN COALESCE(NEW, OLD); -END; -$$; - -DROP TRIGGER IF EXISTS agents_changed_notify ON public.agents; -CREATE TRIGGER agents_changed_notify - AFTER INSERT OR UPDATE OR DELETE ON public.agents - FOR EACH ROW EXECUTE FUNCTION public.notify_agent_changed(); - - --- Channel bindings: payload is "::" so the --- in-process cache (keyed identically) can drop just the affected entry. -CREATE OR REPLACE FUNCTION public.notify_channel_binding_changed() -RETURNS trigger -LANGUAGE plpgsql AS $$ -DECLARE - rec record; - payload text; -BEGIN - IF TG_OP = 'DELETE' THEN - rec := OLD; - ELSE - rec := NEW; - END IF; - payload := format( - '%s:%s:%s', - rec.platform, - COALESCE(rec.team_id, '-'), - rec.channel_id - ); - PERFORM pg_notify('channel_binding_changed', payload); - RETURN COALESCE(NEW, OLD); -END; -$$; - -DROP TRIGGER IF EXISTS agent_channel_bindings_changed_notify ON public.agent_channel_bindings; -CREATE TRIGGER agent_channel_bindings_changed_notify - AFTER INSERT OR UPDATE OR DELETE ON public.agent_channel_bindings - FOR EACH ROW EXECUTE FUNCTION public.notify_channel_binding_changed(); - - --- User-agent associations: payload is ":" so the --- agent listing cache for a single user can be dropped without affecting --- other users. -CREATE OR REPLACE FUNCTION public.notify_agent_users_changed() -RETURNS trigger -LANGUAGE plpgsql AS $$ -DECLARE - rec record; -BEGIN - IF TG_OP = 'DELETE' THEN - rec := OLD; - ELSE - rec := NEW; - END IF; - PERFORM pg_notify( - 'agent_users_changed', - format('%s:%s', rec.platform, rec.user_id) - ); - RETURN COALESCE(NEW, OLD); -END; -$$; - -DROP TRIGGER IF EXISTS agent_users_changed_notify ON public.agent_users; -CREATE TRIGGER agent_users_changed_notify - AFTER INSERT OR UPDATE OR DELETE ON public.agent_users - FOR EACH ROW EXECUTE FUNCTION public.notify_agent_users_changed(); - --- migrate:down - -DROP TRIGGER IF EXISTS agent_users_changed_notify ON public.agent_users; -DROP FUNCTION IF EXISTS public.notify_agent_users_changed(); - -DROP TRIGGER IF EXISTS agent_channel_bindings_changed_notify ON public.agent_channel_bindings; -DROP FUNCTION IF EXISTS public.notify_channel_binding_changed(); - -DROP TRIGGER IF EXISTS agents_changed_notify ON public.agents; -DROP FUNCTION IF EXISTS public.notify_agent_changed(); diff --git a/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql b/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql deleted file mode 100644 index 3808378ee..000000000 --- a/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql +++ /dev/null @@ -1,36 +0,0 @@ --- migrate:up - --- Phase 6: PG-backed storage for per-user runtime state previously held in Redis. --- --- These tables replace Redis-keyed structures: --- user:auth-profiles:{userId}:{agentId} → user_auth_profiles --- {providerId}:model_preference:{userId} → user_model_preferences --- --- The previous Redis layout kept the credential/refreshToken in the secret --- store and persisted only refs in the cached JSON; we keep that contract --- and store the same JSON document in `profiles` here. - -CREATE TABLE IF NOT EXISTS public.user_auth_profiles ( - user_id text NOT NULL, - agent_id text NOT NULL, - profiles jsonb DEFAULT '[]'::jsonb NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - PRIMARY KEY (user_id, agent_id) -); - -CREATE INDEX IF NOT EXISTS user_auth_profiles_agent_id_idx - ON public.user_auth_profiles (agent_id); - - -CREATE TABLE IF NOT EXISTS public.user_model_preferences ( - user_id text NOT NULL, - provider_id text NOT NULL, - model text NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - PRIMARY KEY (user_id, provider_id) -); - --- migrate:down - -DROP TABLE IF EXISTS public.user_model_preferences; -DROP TABLE IF EXISTS public.user_auth_profiles; diff --git a/db/migrations/20260429120200_fix_notify_old_keys.sql b/db/migrations/20260429120200_fix_notify_old_keys.sql deleted file mode 100644 index 6b81e58ea..000000000 --- a/db/migrations/20260429120200_fix_notify_old_keys.sql +++ /dev/null @@ -1,130 +0,0 @@ --- migrate:up - --- Phase 6 follow-up: emit BOTH the OLD and NEW cache keys when a row's key --- columns change in an UPDATE. The Phase-6 triggers only emitted the NEW --- key, so a process that had the OLD key cached would miss the invalidation --- and serve stale data until the TTL expired. - --- Phase 6 follow-up: add a partial unique index that treats NULL team_id as --- a single equivalence class. Postgres unique constraints treat NULLs as --- distinct, so the existing UNIQUE (platform, channel_id, team_id) lets --- repeated upserts of (platform, channel_id, NULL) insert duplicate rows; --- the matching ON CONFLICT clause never fires and a subsequent getBinding() --- reads an arbitrary row. --- --- Using a partial index lets the existing platform/channel_id/team_id --- constraint stay in place for the team_id-set rows; the new index covers --- the team_id-null branch. -CREATE UNIQUE INDEX IF NOT EXISTS agent_channel_bindings_no_team_unique - ON public.agent_channel_bindings (platform, channel_id) - WHERE team_id IS NULL; - - -CREATE OR REPLACE FUNCTION public.notify_channel_binding_changed() -RETURNS trigger -LANGUAGE plpgsql AS $$ -DECLARE - new_payload text; - old_payload text; -BEGIN - IF TG_OP = 'DELETE' THEN - PERFORM pg_notify( - 'channel_binding_changed', - format('%s:%s:%s', OLD.platform, COALESCE(OLD.team_id, '-'), OLD.channel_id) - ); - RETURN OLD; - END IF; - - new_payload := format( - '%s:%s:%s', NEW.platform, COALESCE(NEW.team_id, '-'), NEW.channel_id - ); - PERFORM pg_notify('channel_binding_changed', new_payload); - - IF TG_OP = 'UPDATE' THEN - old_payload := format( - '%s:%s:%s', OLD.platform, COALESCE(OLD.team_id, '-'), OLD.channel_id - ); - IF old_payload <> new_payload THEN - PERFORM pg_notify('channel_binding_changed', old_payload); - END IF; - END IF; - - RETURN NEW; -END; -$$; - - -CREATE OR REPLACE FUNCTION public.notify_agent_users_changed() -RETURNS trigger -LANGUAGE plpgsql AS $$ -DECLARE - new_payload text; - old_payload text; -BEGIN - IF TG_OP = 'DELETE' THEN - PERFORM pg_notify( - 'agent_users_changed', format('%s:%s', OLD.platform, OLD.user_id) - ); - RETURN OLD; - END IF; - - new_payload := format('%s:%s', NEW.platform, NEW.user_id); - PERFORM pg_notify('agent_users_changed', new_payload); - - IF TG_OP = 'UPDATE' THEN - old_payload := format('%s:%s', OLD.platform, OLD.user_id); - IF old_payload <> new_payload THEN - PERFORM pg_notify('agent_users_changed', old_payload); - END IF; - END IF; - - RETURN NEW; -END; -$$; - --- migrate:down - -DROP INDEX IF EXISTS public.agent_channel_bindings_no_team_unique; - --- Restore the Phase-6 (pre-fix) function bodies, which only emitted the NEW key. -CREATE OR REPLACE FUNCTION public.notify_channel_binding_changed() -RETURNS trigger -LANGUAGE plpgsql AS $$ -DECLARE - rec record; - payload text; -BEGIN - IF TG_OP = 'DELETE' THEN - rec := OLD; - ELSE - rec := NEW; - END IF; - payload := format( - '%s:%s:%s', - rec.platform, - COALESCE(rec.team_id, '-'), - rec.channel_id - ); - PERFORM pg_notify('channel_binding_changed', payload); - RETURN COALESCE(NEW, OLD); -END; -$$; - -CREATE OR REPLACE FUNCTION public.notify_agent_users_changed() -RETURNS trigger -LANGUAGE plpgsql AS $$ -DECLARE - rec record; -BEGIN - IF TG_OP = 'DELETE' THEN - rec := OLD; - ELSE - rec := NEW; - END IF; - PERFORM pg_notify( - 'agent_users_changed', - format('%s:%s', rec.platform, rec.user_id) - ); - RETURN COALESCE(NEW, OLD); -END; -$$; diff --git a/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql b/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql deleted file mode 100644 index 594bd597b..000000000 --- a/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql +++ /dev/null @@ -1,83 +0,0 @@ --- migrate:up - --- Phase 7 of Redis -> Postgres migration: replace the three remaining --- ephemeral-key Redis stores (OAuth state CSRF nonces, CLI auth sessions, --- and the fixed-window rate limiter) with thin Postgres tables. None of --- these need cross-process pub/sub or pipelining; they're cheap row-level --- reads/writes with a TTL column for lazy cleanup on read plus a periodic --- vacuum task. --- --- Design notes: --- - All three tables key on a text id and store an explicit `expires_at`. --- Lazy reads filter `expires_at > now()`; a background sweeper deletes --- stale rows in bulk. --- - `payload` is jsonb so the OAuth state stores can keep their schema --- flexible (PKCE verifier, redirect URI, MCP discovery context, etc.) --- without churning migrations. --- - The rate_limits table is a single counter per (key, window_started_at) --- instead of one row per request — the existing Redis impl is a fixed --- window with INCR + EXPIRE, so a counter row matches the semantics --- exactly. - --- OAuth state nonces: provider PKCE flows, MCP OAuth flow, Slack install, --- CLI browser/device handoff. `scope` mirrors the Redis key-prefix --- (e.g. `claude:oauth_state`, `mcp-oauth:state`, `slack:oauth:state`, --- `cli:auth:request`) so a single lookup tagged by scope+id replaces the --- previous prefix+id Redis lookup. -CREATE TABLE IF NOT EXISTS public.oauth_states ( - id text PRIMARY KEY, - scope text NOT NULL, - payload jsonb NOT NULL, - expires_at timestamptz NOT NULL, - created_at timestamptz NOT NULL DEFAULT now() -); - -CREATE INDEX IF NOT EXISTS oauth_states_scope_idx - ON public.oauth_states (scope); - -CREATE INDEX IF NOT EXISTS oauth_states_expires_at_idx - ON public.oauth_states (expires_at); - --- CLI auth sessions for the `lobu` CLI. Each row is a long-lived (30 day) --- refresh-token-anchored session; the access token is JWT-shaped and --- carries `sessionId` so verifyAccessToken can re-check the row exists --- and hasn't been revoked. -CREATE TABLE IF NOT EXISTS public.cli_sessions ( - session_id text PRIMARY KEY, - user_id text NOT NULL, - email text, - name text, - refresh_token_id text NOT NULL, - expires_at timestamptz NOT NULL, - created_at timestamptz NOT NULL DEFAULT now() -); - -CREATE INDEX IF NOT EXISTS cli_sessions_user_id_idx - ON public.cli_sessions (user_id); - -CREATE INDEX IF NOT EXISTS cli_sessions_expires_at_idx - ON public.cli_sessions (expires_at); - --- Fixed-window rate limit counters. One row per (key, window_started_at); --- a successful consume() does an UPSERT that increments `count`, sets the --- window if missing, and returns the new count. The window expires when --- `expires_at <= now()`. --- --- `key` is the same string the Redis impl used (`rate-limit:cli:admin-login:`, --- etc) so callers don't have to translate. -CREATE TABLE IF NOT EXISTS public.rate_limits ( - key text PRIMARY KEY, - count integer NOT NULL DEFAULT 0, - window_started_at timestamptz NOT NULL, - expires_at timestamptz NOT NULL, - updated_at timestamptz NOT NULL DEFAULT now() -); - -CREATE INDEX IF NOT EXISTS rate_limits_expires_at_idx - ON public.rate_limits (expires_at); - --- migrate:down - -DROP TABLE IF EXISTS public.rate_limits; -DROP TABLE IF EXISTS public.cli_sessions; -DROP TABLE IF EXISTS public.oauth_states; diff --git a/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql b/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql deleted file mode 100644 index 1e03fcea6..000000000 --- a/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql +++ /dev/null @@ -1,84 +0,0 @@ --- migrate:up - --- Phase 8 of Redis -> Postgres migration: replace the remaining Redis-only --- substrates that don't fit cleanly into the existing tables with proper --- typed Postgres tables. --- --- 1. `grants` — per-(agent, kind, pattern) grant rows. Replaces the --- `grant::` Redis key prefix and SCAN-by-prefix list. --- Wildcard expansion happens in the application layer. --- 2. `chat_connections` — chat-platform (Telegram/Slack/Discord/...) connection --- rows. Replaces the `connection:`, `connections:all`, and --- `connections:agent:` Redis keys used by ChatInstanceManager. The --- existing `public.connections` table is for Lobu product connectors, --- not chat platforms. --- 3. `mcp_proxy_sessions` — short-lived MCP server session-id mappings used --- by the MCP proxy. The existing `public.mcp_sessions` table is the --- inbound MCP-server-as-server session table; this is the outbound --- upstream-MCP session-id cache. - --- ============================================================================ --- grants --- ============================================================================ - -CREATE TABLE IF NOT EXISTS public.grants ( - agent_id text NOT NULL REFERENCES public.agents(id) ON DELETE CASCADE, - kind text NOT NULL, - pattern text NOT NULL, - expires_at timestamptz, - granted_at timestamptz NOT NULL DEFAULT now(), - denied boolean NOT NULL DEFAULT false, - PRIMARY KEY (agent_id, kind, pattern) -); - -CREATE INDEX IF NOT EXISTS grants_agent_id_idx - ON public.grants (agent_id); - -CREATE INDEX IF NOT EXISTS grants_expires_at_idx - ON public.grants (expires_at) - WHERE expires_at IS NOT NULL; - --- ============================================================================ --- chat_connections --- ============================================================================ - -CREATE TABLE IF NOT EXISTS public.chat_connections ( - id text PRIMARY KEY, - platform text NOT NULL, - template_agent_id text REFERENCES public.agents(id) ON DELETE CASCADE, - config jsonb NOT NULL, - settings jsonb NOT NULL DEFAULT '{}'::jsonb, - metadata jsonb NOT NULL DEFAULT '{}'::jsonb, - status text NOT NULL DEFAULT 'active', - error_message text, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now() -); - -CREATE INDEX IF NOT EXISTS chat_connections_template_agent_id_idx - ON public.chat_connections (template_agent_id) - WHERE template_agent_id IS NOT NULL; - -CREATE INDEX IF NOT EXISTS chat_connections_platform_idx - ON public.chat_connections (platform); - --- ============================================================================ --- mcp_proxy_sessions (NOT to be confused with public.mcp_sessions which is --- the inbound MCP server's session table) --- ============================================================================ - -CREATE TABLE IF NOT EXISTS public.mcp_proxy_sessions ( - session_key text PRIMARY KEY, - upstream_session_id text NOT NULL, - expires_at timestamptz NOT NULL, - updated_at timestamptz NOT NULL DEFAULT now() -); - -CREATE INDEX IF NOT EXISTS mcp_proxy_sessions_expires_at_idx - ON public.mcp_proxy_sessions (expires_at); - --- migrate:down - -DROP TABLE IF EXISTS public.mcp_proxy_sessions; -DROP TABLE IF EXISTS public.chat_connections; -DROP TABLE IF EXISTS public.grants; diff --git a/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql b/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql deleted file mode 100644 index 31e44a6c7..000000000 --- a/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql +++ /dev/null @@ -1,44 +0,0 @@ --- migrate:up - --- Phase 10 of Redis -> Postgres migration: honor the caller-supplied --- queue options (priority, expireInSeconds, retryDelay) that RunsQueue --- previously dropped on the floor. --- --- 1. priority: int, default 0; claim ORDER BY priority DESC, run_at ASC, id ASC. --- 2. expires_at: row-level TTL. Claim filter excludes expired rows; the --- periodic cleanup task deletes them. --- 3. retry_delay_seconds: when set, scheduleRetry uses fixed-delay backoff --- instead of exponential. NULL falls back to the existing exponential --- cap-300s curve. - -ALTER TABLE public.runs - ADD COLUMN IF NOT EXISTS priority integer NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS expires_at timestamptz, - ADD COLUMN IF NOT EXISTS retry_delay_seconds integer; - --- Refresh the lobu-claim index so priority + run_at decide claim order. -DROP INDEX IF EXISTS public.runs_lobu_claim_idx; - -CREATE INDEX IF NOT EXISTS runs_lobu_claim_idx - ON public.runs (run_type, queue_name, priority DESC, run_at ASC, id ASC) - WHERE status = 'pending' - AND run_type IN ('chat_message', 'schedule', 'agent_run', 'internal'); - -CREATE INDEX IF NOT EXISTS runs_expires_at_idx - ON public.runs (expires_at) - WHERE expires_at IS NOT NULL; - --- migrate:down - -DROP INDEX IF EXISTS public.runs_expires_at_idx; -DROP INDEX IF EXISTS public.runs_lobu_claim_idx; - -CREATE INDEX IF NOT EXISTS runs_lobu_claim_idx - ON public.runs (run_type, queue_name, run_at) - WHERE status = 'pending' - AND run_type IN ('chat_message', 'schedule', 'agent_run', 'internal'); - -ALTER TABLE public.runs - DROP COLUMN IF EXISTS retry_delay_seconds, - DROP COLUMN IF EXISTS expires_at, - DROP COLUMN IF EXISTS priority; diff --git a/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql b/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql deleted file mode 100644 index 815553131..000000000 --- a/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql +++ /dev/null @@ -1,25 +0,0 @@ --- migrate:up - --- Drop the NOTIFY triggers + functions that powered the InvalidatableCache. --- The cache layer was removed: stores read-through to PG directly. Reads sit --- at ~7 SELECTs per chat dispatch — well within PG capacity at current scale. --- The runs-queue's pg_notify('runs_lobu:', ...) wakeup path is --- unaffected (different channel, different trigger). - -DROP TRIGGER IF EXISTS agent_users_changed_notify ON public.agent_users; -DROP FUNCTION IF EXISTS public.notify_agent_users_changed(); - -DROP TRIGGER IF EXISTS agent_channel_bindings_changed_notify ON public.agent_channel_bindings; -DROP FUNCTION IF EXISTS public.notify_channel_binding_changed(); - -DROP TRIGGER IF EXISTS agents_changed_notify ON public.agents; -DROP FUNCTION IF EXISTS public.notify_agent_changed(); - --- migrate:down - --- Restoration mirrors 20260429120000_agent_changed_notify.sql + --- 20260429120200_fix_notify_old_keys.sql; if you need to roll back, replay --- those by hand. We don't reproduce them here because the cache that consumed --- these channels no longer exists — a rollback to the old behavior requires --- restoring the cache code too. -SELECT 1; diff --git a/db/migrations/20260430005614_agents_apply_fields.sql b/db/migrations/20260430005614_agents_apply_fields.sql deleted file mode 100644 index 4a44f94a4..000000000 --- a/db/migrations/20260430005614_agents_apply_fields.sql +++ /dev/null @@ -1,21 +0,0 @@ --- migrate:up - --- Persist three agent settings fields that the file-loader produces from --- lobu.toml but the postgres-backed AgentConfigStore had nowhere to put: --- * egress_config -> AgentSettings.egressConfig --- * pre_approved_tools -> AgentSettings.preApprovedTools --- * guardrails -> AgentSettings.guardrails --- Without these columns, `lobu apply` would silently drop the values on --- every push, producing perpetual drift between local and cloud. - -ALTER TABLE public.agents - ADD COLUMN egress_config jsonb DEFAULT '{}'::jsonb, - ADD COLUMN pre_approved_tools jsonb DEFAULT '[]'::jsonb, - ADD COLUMN guardrails jsonb DEFAULT '[]'::jsonb; - --- migrate:down - -ALTER TABLE public.agents - DROP COLUMN egress_config, - DROP COLUMN pre_approved_tools, - DROP COLUMN guardrails; diff --git a/db/migrations/20260430022231_fix_connection_config_encryption.sql b/db/migrations/20260430022231_fix_connection_config_encryption.sql deleted file mode 100644 index d50dfcf7b..000000000 --- a/db/migrations/20260430022231_fix_connection_config_encryption.sql +++ /dev/null @@ -1,69 +0,0 @@ --- migrate:up - --- Fix connection-config encryption asymmetry in agent_connections.config. --- --- encryptConfig() in postgres-stores.ts historically returned raw --- "iv:tag:ciphertext" output from @lobu/core's `encrypt()`, but --- decryptConfig() only decrypts strings that start with "enc:v1:". So any --- secret-named field that hit encryptConfig was stored as prefixless --- ciphertext and round-tripped as that ciphertext literal on read. --- --- This migration backfills existing prefixless rows by re-prefixing them so --- the now-aligned decryptConfig path can decrypt them. --- --- Identification: AES-GCM in @lobu/core uses a 12-byte IV (24 hex chars) --- and a 16-byte auth tag (32 hex chars), joined with the ciphertext as --- `iv:tag:ciphertext`. We match exactly that shape to avoid touching --- arbitrary `:` separated values. --- --- Idempotent: jsonb_object_agg only rewrites string values that match the --- prefixless shape AND lack the prefix. Re-running the migration is a noop. - -UPDATE public.agent_connections AS ac -SET config = sub.fixed_config -FROM ( - SELECT - id, - jsonb_object_agg( - key, - CASE - WHEN jsonb_typeof(value) = 'string' - AND value #>> '{}' ~ '^[0-9a-f]{24}:[0-9a-f]{32}:[0-9a-f]+$' - AND value #>> '{}' NOT LIKE 'enc:v1:%' - THEN to_jsonb('enc:v1:' || (value #>> '{}')) - ELSE value - END - ) AS fixed_config - FROM public.agent_connections, - LATERAL jsonb_each(config) - GROUP BY id -) AS sub -WHERE ac.id = sub.id - AND ac.config IS DISTINCT FROM sub.fixed_config; - --- migrate:down - --- Strip the "enc:v1:" prefix to restore the prefixless ciphertext shape. --- Same regex: only touch strings whose remainder is `iv:tag:ciphertext`. - -UPDATE public.agent_connections AS ac -SET config = sub.fixed_config -FROM ( - SELECT - id, - jsonb_object_agg( - key, - CASE - WHEN jsonb_typeof(value) = 'string' - AND value #>> '{}' LIKE 'enc:v1:%' - AND substring(value #>> '{}' FROM 8) ~ '^[0-9a-f]{24}:[0-9a-f]{32}:[0-9a-f]+$' - THEN to_jsonb(substring(value #>> '{}' FROM 8)) - ELSE value - END - ) AS fixed_config - FROM public.agent_connections, - LATERAL jsonb_each(config) - GROUP BY id -) AS sub -WHERE ac.id = sub.id - AND ac.config IS DISTINCT FROM sub.fixed_config; diff --git a/db/migrations/20260430151215_add_task_run_type.sql b/db/migrations/20260430151215_add_task_run_type.sql deleted file mode 100644 index b2be5a0e6..000000000 --- a/db/migrations/20260430151215_add_task_run_type.sql +++ /dev/null @@ -1,77 +0,0 @@ --- migrate:up transaction:false - --- Add 'task' to the runs_run_type_check + extend runs_lobu_claim_idx. --- --- Background: the lobu-queue lanes (chat_message, schedule, agent_run, --- internal) are claimed in-process by the gateway's RunsQueue. The 'task' --- lane extends that pattern for platform-side periodic + lazy work --- (token refresh, classification reconciliation, embed backfill, watcher --- maintenance, etc.) that previously ran from a single setInterval-driven --- maintenance scheduler. --- --- Lock-safety: this migration runs `transaction:false` so that --- CREATE INDEX CONCURRENTLY and VALIDATE CONSTRAINT release locks between --- statements. Without it, dbmate's per-migration transaction would force --- ACCESS EXCLUSIVE on the runs table for the duration of a constraint --- validation or index build — visible downtime for a hot queue table. -SET lock_timeout = '5s'; - --- 1. Widen the run_type CHECK constraint without scanning the table. --- NOT VALID adds the catalog row under a brief ACCESS EXCLUSIVE. --- VALIDATE takes only SHARE UPDATE EXCLUSIVE so concurrent reads/writes --- are unaffected. Idempotent: re-runs safely if a prior run died midway. -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 - FROM pg_constraint - WHERE conrelid = 'public.runs'::regclass - AND conname = 'runs_run_type_check_v2' - ) THEN - ALTER TABLE public.runs - ADD CONSTRAINT runs_run_type_check_v2 CHECK (run_type = ANY (ARRAY[ - 'sync'::text, - 'action'::text, - 'embed_backfill'::text, - 'watcher'::text, - 'auth'::text, - 'chat_message'::text, - 'schedule'::text, - 'agent_run'::text, - 'internal'::text, - 'task'::text - ])) NOT VALID; - END IF; -END$$; - -ALTER TABLE public.runs VALIDATE CONSTRAINT runs_run_type_check_v2; - -ALTER TABLE public.runs DROP CONSTRAINT IF EXISTS runs_run_type_check; -ALTER TABLE public.runs RENAME CONSTRAINT runs_run_type_check_v2 TO runs_run_type_check; - --- 2. Replace the lobu claim index. Originally written with CONCURRENTLY, --- but dbmate's transaction wrapper still presents these to PG as --- in-transaction even with `transaction:false`, which breaks --- CREATE/DROP INDEX CONCURRENTLY (see comments in --- 20260426130001_db_integrity_cleanup_concurrent.sql for the same --- workaround). The partial index only covers `status = 'pending'` --- rows in the lobu lanes — typically a small set since pending rows --- are claimed within milliseconds — so the ACCESS EXCLUSIVE held --- during the non-concurrent build is sub-second in practice. -DROP INDEX IF EXISTS public.runs_lobu_claim_idx; - -CREATE INDEX runs_lobu_claim_idx - ON public.runs (run_type, queue_name, priority DESC, run_at ASC, id ASC) - WHERE status = 'pending' - AND run_type IN ('chat_message', 'schedule', 'agent_run', 'internal', 'task'); - --- migrate:down - --- This migration is forward-only. --- --- Reverting would require either deleting all rows with run_type='task' --- (data loss — both pending tasks and historical run records) or leaving --- the constraint widened (the failure mode the up migration was avoiding). --- Neither is safe to do automatically. If you genuinely need to revert, --- write a follow-up migration that explicitly handles the data first. -SELECT 1; diff --git a/db/migrations/20260501000000_drop_cli_sessions.sql b/db/migrations/20260501000000_drop_cli_sessions.sql deleted file mode 100644 index 42a2428c8..000000000 --- a/db/migrations/20260501000000_drop_cli_sessions.sql +++ /dev/null @@ -1,27 +0,0 @@ --- migrate:up - --- The bespoke `lobu login` device-code flow that backed `cli_sessions` has --- been replaced with standard OAuth 2.0 device-code (issued by the existing --- Lobu IdP). The CLI now mints OAuth bearer tokens, so `cli_sessions` is --- dead. -DROP INDEX IF EXISTS public.cli_sessions_user_id_idx; -DROP INDEX IF EXISTS public.cli_sessions_expires_at_idx; -DROP TABLE IF EXISTS public.cli_sessions; - --- migrate:down - -CREATE TABLE IF NOT EXISTS public.cli_sessions ( - session_id text PRIMARY KEY, - user_id text NOT NULL, - email text, - name text, - refresh_token_id text NOT NULL, - expires_at timestamptz NOT NULL, - created_at timestamptz NOT NULL DEFAULT now() -); - -CREATE INDEX IF NOT EXISTS cli_sessions_user_id_idx - ON public.cli_sessions (user_id); - -CREATE INDEX IF NOT EXISTS cli_sessions_expires_at_idx - ON public.cli_sessions (expires_at); diff --git a/db/migrations/20260501133000_lobu_memory_mcp_id.sql b/db/migrations/20260501133000_lobu_memory_mcp_id.sql deleted file mode 100644 index 331179ff5..000000000 --- a/db/migrations/20260501133000_lobu_memory_mcp_id.sql +++ /dev/null @@ -1,117 +0,0 @@ --- migrate:up - -UPDATE public.agents -SET mcp_servers = (mcp_servers - 'lobu') || jsonb_build_object('lobu-memory', mcp_servers->'lobu'), - updated_at = now() -WHERE mcp_servers ? 'lobu' - AND NOT (mcp_servers ? 'lobu-memory'); - -UPDATE public.agents -SET mcp_servers = mcp_servers - 'lobu', - updated_at = now() -WHERE mcp_servers ? 'lobu'; - -UPDATE public.agents a -SET pre_approved_tools = COALESCE(( - SELECT jsonb_agg(DISTINCT mapped.value) - FROM ( - SELECT CASE - WHEN tool.value #>> '{}' LIKE '/mcp/lobu/tools/%' - OR tool.value #>> '{}' LIKE '/mcp/lobu-memory/tools/%' - THEN to_jsonb('/mcp/lobu-memory/tools/*'::text) - ELSE tool.value - END AS value - FROM jsonb_array_elements(COALESCE(a.pre_approved_tools, '[]'::jsonb)) AS tool(value) - - UNION ALL - - SELECT to_jsonb('/mcp/lobu-memory/tools/*'::text) AS value - WHERE a.mcp_servers ? 'lobu-memory' - ) AS mapped -), '[]'::jsonb), - updated_at = now() -WHERE a.mcp_servers ? 'lobu-memory' - OR EXISTS ( - SELECT 1 - FROM jsonb_array_elements(COALESCE(a.pre_approved_tools, '[]'::jsonb)) AS tool(value) - WHERE tool.value #>> '{}' LIKE '/mcp/lobu/tools/%' - OR tool.value #>> '{}' LIKE '/mcp/lobu-memory/tools/%' - ); - -INSERT INTO public.grants (agent_id, kind, pattern, expires_at, granted_at, denied) -SELECT g.agent_id, - g.kind, - '/mcp/lobu-memory/tools/*', - CASE - WHEN bool_or(g.expires_at IS NULL) THEN NULL::timestamptz - ELSE max(g.expires_at) - END, - now(), - bool_or(g.denied) -FROM public.grants g -WHERE g.kind = 'mcp_tool' - AND (g.pattern LIKE '/mcp/lobu/tools/%' OR g.pattern LIKE '/mcp/lobu-memory/tools/%') -GROUP BY g.agent_id, g.kind -ON CONFLICT (agent_id, kind, pattern) DO UPDATE SET - expires_at = CASE - WHEN public.grants.expires_at IS NULL THEN EXCLUDED.expires_at - WHEN EXCLUDED.expires_at IS NULL THEN public.grants.expires_at - ELSE LEAST(public.grants.expires_at, EXCLUDED.expires_at) - END, - granted_at = EXCLUDED.granted_at, - denied = public.grants.denied OR EXCLUDED.denied; - -DELETE FROM public.grants -WHERE kind = 'mcp_tool' - AND pattern LIKE '/mcp/lobu/tools/%'; - --- migrate:down - -UPDATE public.agents -SET mcp_servers = (mcp_servers - 'lobu-memory') || jsonb_build_object('lobu', mcp_servers->'lobu-memory'), - updated_at = now() -WHERE mcp_servers ? 'lobu-memory' - AND NOT (mcp_servers ? 'lobu'); - -UPDATE public.agents a -SET pre_approved_tools = COALESCE(( - SELECT jsonb_agg(DISTINCT CASE - WHEN tool.value #>> '{}' LIKE '/mcp/lobu-memory/tools/%' - THEN to_jsonb('/mcp/lobu/tools/*'::text) - ELSE tool.value - END) - FROM jsonb_array_elements(COALESCE(a.pre_approved_tools, '[]'::jsonb)) AS tool(value) -), '[]'::jsonb), - updated_at = now() -WHERE EXISTS ( - SELECT 1 - FROM jsonb_array_elements(COALESCE(a.pre_approved_tools, '[]'::jsonb)) AS tool(value) - WHERE tool.value #>> '{}' LIKE '/mcp/lobu-memory/tools/%' -); - -INSERT INTO public.grants (agent_id, kind, pattern, expires_at, granted_at, denied) -SELECT g.agent_id, - g.kind, - '/mcp/lobu/tools/*', - CASE - WHEN bool_or(g.expires_at IS NULL) THEN NULL::timestamptz - ELSE max(g.expires_at) - END, - now(), - bool_or(g.denied) -FROM public.grants g -WHERE g.kind = 'mcp_tool' - AND g.pattern LIKE '/mcp/lobu-memory/tools/%' -GROUP BY g.agent_id, g.kind -ON CONFLICT (agent_id, kind, pattern) DO UPDATE SET - expires_at = CASE - WHEN public.grants.expires_at IS NULL THEN EXCLUDED.expires_at - WHEN EXCLUDED.expires_at IS NULL THEN public.grants.expires_at - ELSE LEAST(public.grants.expires_at, EXCLUDED.expires_at) - END, - granted_at = EXCLUDED.granted_at, - denied = public.grants.denied OR EXCLUDED.denied; - -DELETE FROM public.grants -WHERE kind = 'mcp_tool' - AND pattern LIKE '/mcp/lobu-memory/tools/%'; diff --git a/db/migrations/20260502000000_drop_chat_connections.sql b/db/migrations/20260502000000_drop_chat_connections.sql deleted file mode 100644 index e265c17b2..000000000 --- a/db/migrations/20260502000000_drop_chat_connections.sql +++ /dev/null @@ -1,60 +0,0 @@ --- migrate:up --- Drop chat_connections table. Connection state is now unified in --- agent_connections, which ChatInstanceManager reads/writes directly. --- Secret fields (botToken, signingSecret, etc.) live as `secret://` --- refs inside the row's `config` JSON and resolve at runtime through --- SecretStoreRegistry — backed by Postgres by default, pluggable to --- AWS Secrets Manager / Vault / k8s for ops who need it. - --- Copy any existing chat_connections rows into agent_connections so a --- live deployment with provisioned chat bots doesn't lose them. Configs --- carrying the legacy `enc:v1:` ciphertext are handled at read time by --- decryptLegacyEncryptedConfig in postgres-stores.ts; refs pass through --- unchanged. ON CONFLICT DO NOTHING covers the case where rows have --- already been mirrored by an in-flight write through the manager. --- agent_connections.agent_id is NOT NULL, but chat_connections.template_agent_id --- was nullable. Skip orphaned rows (no parent agent) — they could not start --- in the current model anyway. --- Guarded by to_regclass: deployments that never had chat_connections --- (table created with IF NOT EXISTS in 20260429140000 and never used) skip --- the copy entirely instead of erroring on the missing relation. -DO $$ -BEGIN - IF to_regclass('public.chat_connections') IS NOT NULL THEN - INSERT INTO public.agent_connections ( - id, agent_id, platform, config, settings, metadata, - status, error_message, created_at, updated_at - ) - SELECT - id, template_agent_id, platform, config, settings, metadata, - status, error_message, created_at, updated_at - FROM public.chat_connections - WHERE template_agent_id IS NOT NULL - ON CONFLICT (id) DO NOTHING; - END IF; -END $$; - -DROP TABLE IF EXISTS public.chat_connections; - --- migrate:down --- Recreate chat_connections for rollback. Data would need to be re-seeded. - -CREATE TABLE IF NOT EXISTS public.chat_connections ( - id text PRIMARY KEY, - platform text NOT NULL, - template_agent_id text REFERENCES public.agents(id) ON DELETE CASCADE, - config jsonb NOT NULL, - settings jsonb NOT NULL DEFAULT '{}'::jsonb, - metadata jsonb NOT NULL DEFAULT '{}'::jsonb, - status text NOT NULL DEFAULT 'active', - error_message text, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now() -); - -CREATE INDEX IF NOT EXISTS chat_connections_template_agent_id_idx - ON public.chat_connections (template_agent_id) - WHERE template_agent_id IS NOT NULL; - -CREATE INDEX IF NOT EXISTS chat_connections_platform_idx - ON public.chat_connections (platform); diff --git a/db/migrations/20260503000000_agent_secrets_org_scope.sql b/db/migrations/20260503000000_agent_secrets_org_scope.sql deleted file mode 100644 index a275eb12e..000000000 --- a/db/migrations/20260503000000_agent_secrets_org_scope.sql +++ /dev/null @@ -1,56 +0,0 @@ --- migrate:up - --- agent_secrets used a global `(name)` namespace, so two organizations --- storing the same key (e.g. `ANTHROPIC_API_KEY`) would silently overwrite --- each other. Scope rows by organization while keeping legacy rows --- addressable: empty-string organization_id means "global" (used by --- system env-store and any pre-existing rows from the global era). --- --- ROLLOUT NOTE: this migration replaces the primary key on `(name)` with --- `(organization_id, name)` non-atomically. Lobu's deployment model is a --- single embedded Node process with atomic swap (see "Embedded-only --- deployment" in AGENTS.md), so old + new processes never run concurrently --- against the same database. If you're running Lobu under a rolling-deploy --- supervisor, stop traffic before applying this migration — old pods using --- `ON CONFLICT (name)` will fail writes against the new schema. - -ALTER TABLE public.agent_secrets - ADD COLUMN IF NOT EXISTS organization_id text NOT NULL DEFAULT ''; - -ALTER TABLE public.agent_secrets - DROP CONSTRAINT IF EXISTS agent_secrets_pkey; - -ALTER TABLE public.agent_secrets - ADD CONSTRAINT agent_secrets_pkey PRIMARY KEY (organization_id, name); - -CREATE INDEX IF NOT EXISTS agent_secrets_org_id_idx - ON public.agent_secrets (organization_id); - --- migrate:down - -ALTER TABLE public.agent_secrets - DROP CONSTRAINT IF EXISTS agent_secrets_pkey; - -DROP INDEX IF EXISTS public.agent_secrets_org_id_idx; - --- Down-step is destructive by design: collapses per-org rows back to the --- global namespace. The two DELETEs below pick a survivor non-deterministically --- when multiple orgs hold the same name (global wins first; otherwise the --- lexicographically-smaller org_id survives). Acceptable because rollback --- after multi-org writes is already lossy — there is no "right" winner. -DELETE FROM public.agent_secrets a -USING public.agent_secrets b -WHERE a.name = b.name - AND a.organization_id <> '' - AND b.organization_id = ''; - -DELETE FROM public.agent_secrets a -USING public.agent_secrets b -WHERE a.name = b.name - AND a.organization_id > b.organization_id; - -ALTER TABLE public.agent_secrets - ADD CONSTRAINT agent_secrets_pkey PRIMARY KEY (name); - -ALTER TABLE public.agent_secrets - DROP COLUMN IF EXISTS organization_id; diff --git a/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql b/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql deleted file mode 100644 index 01a70e3e9..000000000 --- a/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql +++ /dev/null @@ -1,48 +0,0 @@ --- migrate:up - --- Flatten the agents table: there is no longer a template/sandbox split. --- One agents row = one logical agent. Definitions, providers, settings all --- live on this row and are agent-only (not per-user). --- --- Per-user state has its own homes: --- - user_auth_profiles (per-user-per-agent OAuth tokens) — already canonical --- - agent_channel_bindings (where the agent operates) --- - agent_users (who has interacted) --- --- ROLLOUT NOTE: this migration deletes every sandbox row. There is no --- pre-cutover production data to preserve (per buremba). If that ever --- changes, the rollback below restores the columns but NOT the deleted --- rows. - --- 1. Drop sandbox rows. Anything with template_agent_id or parent_connection_id --- set was a per-user / per-connection sandbox. -DELETE FROM public.agents -WHERE template_agent_id IS NOT NULL - OR parent_connection_id IS NOT NULL; - --- 2. Drop indexes on the going-away columns. -DROP INDEX IF EXISTS public.agents_template_agent_id_idx; -DROP INDEX IF EXISTS public.agents_parent_connection_id_idx; - --- 3. Drop the columns themselves. -ALTER TABLE public.agents DROP COLUMN IF EXISTS template_agent_id; -ALTER TABLE public.agents DROP COLUMN IF EXISTS parent_connection_id; - --- 4. Drop legacy / dead-code columns. --- auth_profiles: superseded by user_auth_profiles table (per-user-per-agent). --- mcp_install_notified: per-user UI dismissal state, never used at runtime. -ALTER TABLE public.agents DROP COLUMN IF EXISTS auth_profiles; -ALTER TABLE public.agents DROP COLUMN IF EXISTS mcp_install_notified; - --- migrate:down - --- Restore columns (data is gone — the DELETE is irreversible). -ALTER TABLE public.agents ADD COLUMN IF NOT EXISTS template_agent_id text; -ALTER TABLE public.agents ADD COLUMN IF NOT EXISTS parent_connection_id text; -ALTER TABLE public.agents ADD COLUMN IF NOT EXISTS auth_profiles jsonb DEFAULT '[]'::jsonb; -ALTER TABLE public.agents ADD COLUMN IF NOT EXISTS mcp_install_notified jsonb DEFAULT '{}'::jsonb; - -CREATE INDEX IF NOT EXISTS agents_template_agent_id_idx - ON public.agents (template_agent_id); -CREATE INDEX IF NOT EXISTS agents_parent_connection_id_idx - ON public.agents (parent_connection_id); diff --git a/db/migrations/20260510220000_connector_required_capability.sql b/db/migrations/20260510220000_connector_required_capability.sql deleted file mode 100644 index 63ee32e40..000000000 --- a/db/migrations/20260510220000_connector_required_capability.sql +++ /dev/null @@ -1,47 +0,0 @@ --- migrate:up - --- Add a per-connector capability gate for worker dispatch. Workers advertise --- their capabilities on poll; the runs scheduler only assigns connector runs --- to workers whose capabilities include the connector's required_capability. --- NULL means "no special capability required" (the default for API/browser --- connectors that the existing fleet can run). --- --- `runtime` carries platform metadata for device-bound connectors (e.g. --- `{"platforms": ["macos"]}` for apple.screen_time / local.directory, which --- only run inside Lobu for Mac — that data is unreachable from a server-side --- worker). NULL = cloud connector. --- --- Initial use case: apple.screen_time and local.directory, served by Lobu for --- Mac polling /api/workers/* as a user-scoped device worker. - -ALTER TABLE public.connector_definitions - ADD COLUMN IF NOT EXISTS required_capability text, - ADD COLUMN IF NOT EXISTS runtime jsonb; - -CREATE INDEX IF NOT EXISTS connector_definitions_required_capability_idx - ON public.connector_definitions (required_capability) - WHERE required_capability IS NOT NULL; - -CREATE TABLE IF NOT EXISTS public.device_workers ( - user_id text NOT NULL, - worker_id text NOT NULL, - platform text, - app_version text, - capabilities jsonb NOT NULL DEFAULT '[]'::jsonb, - label text, - first_seen_at timestamptz NOT NULL DEFAULT now(), - last_seen_at timestamptz NOT NULL DEFAULT now(), - PRIMARY KEY (user_id, worker_id) -); - -CREATE INDEX IF NOT EXISTS device_workers_user_id_idx - ON public.device_workers (user_id); - --- migrate:down - -DROP INDEX IF EXISTS public.device_workers_user_id_idx; -DROP TABLE IF EXISTS public.device_workers; -DROP INDEX IF EXISTS public.connector_definitions_required_capability_idx; -ALTER TABLE public.connector_definitions - DROP COLUMN IF EXISTS runtime, - DROP COLUMN IF EXISTS required_capability; diff --git a/db/migrations/20260512000000_device_worker_connection_binding.sql b/db/migrations/20260512000000_device_worker_connection_binding.sql deleted file mode 100644 index e16bd0643..000000000 --- a/db/migrations/20260512000000_device_worker_connection_binding.sql +++ /dev/null @@ -1,113 +0,0 @@ --- migrate:up - --- Make a connection's execution target explicit, and give every device worker --- a home organization. --- --- connections.device_worker_id (nullable) is the binding: --- NULL -> runs on the cloud connector-worker pool (today's behavior) --- set -> runs are pinned to that device worker --- For device-type connectors the binding is mandatory; for cloud connectors --- it's an optional override. A connection can only be pinned to a device that --- is attached to that connection's organization. --- --- device_workers.organization_id is the device's home org — chosen at setup, --- defaulting to the owner's personal workspace. The device's connectors live --- there; re-attaching the device to a different org (a member of which the --- owner must be) is the only knob. There is no per-connection device→org grant. - --- Surrogate key for device_workers so connections / UI can reference a device --- by a single stable id. The (user_id, worker_id) primary key stays. -ALTER TABLE public.device_workers - ADD COLUMN IF NOT EXISTS id uuid NOT NULL DEFAULT gen_random_uuid(), - ADD COLUMN IF NOT EXISTS organization_id text; - -CREATE UNIQUE INDEX IF NOT EXISTS device_workers_id_key - ON public.device_workers (id); - -CREATE INDEX IF NOT EXISTS idx_device_workers_organization_id - ON public.device_workers (organization_id) - WHERE organization_id IS NOT NULL; - -ALTER TABLE public.connections - ADD COLUMN IF NOT EXISTS device_worker_id uuid; - -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'connections_device_worker_id_fkey' - ) THEN - ALTER TABLE public.connections - ADD CONSTRAINT connections_device_worker_id_fkey - FOREIGN KEY (device_worker_id) - REFERENCES public.device_workers (id) - ON DELETE SET NULL; - END IF; -END$$; - -CREATE INDEX IF NOT EXISTS idx_connections_device_worker_id - ON public.connections (device_worker_id) - WHERE device_worker_id IS NOT NULL; - --- Attach existing devices to their owner's personal workspace (no-op on a --- fresh database — there are no users yet; the device heartbeat sets this for --- new devices either way). -UPDATE public.device_workers dw -SET organization_id = ( - SELECT o.id FROM public.organization o - WHERE (o.metadata::jsonb)->>'personal_org_for_user_id' = dw.user_id - LIMIT 1 -) -WHERE dw.organization_id IS NULL; - --- Backfill: existing auto-wired personal-org device connections (created_by --- set, no auth profile) whose owner has exactly one device get pinned to that --- device — but at most one per (org, connector_key, owner) so the unique index --- created below can never be violated. Ambiguous ones stay NULL and the UI --- prompts for a device. -UPDATE public.connections c -SET device_worker_id = dw.id -FROM ( - -- Users with exactly one device worker (no min(uuid) needed — and Postgres - -- has no aggregate for uuid anyway). - SELECT dw1.user_id, dw1.id - FROM public.device_workers dw1 - WHERE NOT EXISTS ( - SELECT 1 FROM public.device_workers dw2 - WHERE dw2.user_id = dw1.user_id AND dw2.id <> dw1.id - ) -) dw -WHERE c.created_by = dw.user_id - AND c.device_worker_id IS NULL - AND c.deleted_at IS NULL - AND c.auth_profile_id IS NULL - AND c.connector_key IN ( - SELECT key FROM public.connector_definitions WHERE required_capability IS NOT NULL - ) - AND c.id = ( - SELECT min(c2.id) FROM public.connections c2 - WHERE c2.organization_id = c.organization_id - AND c2.connector_key = c.connector_key - AND c2.created_by = c.created_by - AND c2.deleted_at IS NULL - ); - --- One active connection per (org, connector, device). A second device backing --- the same connector is a second connection. Doubles as DB-level idempotency --- for the create-vs-auto-wire race. Created AFTER the backfill above. -DROP INDEX IF EXISTS public.idx_connections_org_connector_device_live; -CREATE UNIQUE INDEX idx_connections_org_connector_device_live - ON public.connections (organization_id, connector_key, device_worker_id) - WHERE deleted_at IS NULL AND device_worker_id IS NOT NULL; - --- migrate:down - -DROP INDEX IF EXISTS public.idx_connections_org_connector_device_live; -DROP INDEX IF EXISTS public.idx_connections_device_worker_id; -ALTER TABLE public.connections - DROP CONSTRAINT IF EXISTS connections_device_worker_id_fkey, - DROP COLUMN IF EXISTS device_worker_id; -DROP INDEX IF EXISTS public.device_workers_id_key; -DROP INDEX IF EXISTS public.idx_device_workers_organization_id; -ALTER TABLE public.device_workers - DROP COLUMN IF EXISTS id, - DROP COLUMN IF EXISTS organization_id; diff --git a/db/migrations/20260512131703_connections_slug.sql b/db/migrations/20260512131703_connections_slug.sql deleted file mode 100644 index 953e1dfc8..000000000 --- a/db/migrations/20260512131703_connections_slug.sql +++ /dev/null @@ -1,131 +0,0 @@ --- migrate:up - --- Add a stable `slug` identity to `connections` so `lobu apply` can diff --- connections by an immutable key instead of the mutable `display_name`. --- --- Mirrors the existing `auth_profiles.slug` design: text slug, unique per org --- among live rows (partial index on `deleted_at IS NULL`), generated from the --- display name when not supplied explicitly. --- --- Backfill MUST produce exactly what `ensureUniqueConnectionSlug` / --- `slugifyConnectionName` in packages/server/src/utils/connections.ts would --- generate (that file is the source of truth): --- 1. base = slugify(display_name); if empty, slugify(connector_key); if still --- empty, the literal 'connection'. slugify = lowercase, every run of --- non-alphanumerics -> '-', trim leading/trailing '-'. --- 2. Collisions per (organization_id) among live (`deleted_at IS NULL`) rows --- are resolved with a deterministic numeric suffix loop: base, base-2, --- base-3, ... assigned in ascending id order. Soft-deleted rows do not --- participate in the unique index, so they keep their base slug freely. - -ALTER TABLE public.connections - ADD COLUMN IF NOT EXISTS slug text; - --- slugify(display_name) -> slugify(connector_key) -> 'connection' -WITH base AS ( - SELECT - c.id, - coalesce( - NULLIF( - regexp_replace( - regexp_replace(lower(coalesce(c.display_name, '')), '[^a-z0-9]+', '-', 'g'), - '(^-+|-+$)', '', 'g' - ), - '' - ), - NULLIF( - regexp_replace( - regexp_replace(lower(coalesce(c.connector_key, '')), '[^a-z0-9]+', '-', 'g'), - '(^-+|-+$)', '', 'g' - ), - '' - ), - 'connection' - ) AS base_slug - FROM public.connections c -) -UPDATE public.connections c -SET slug = b.base_slug -FROM base b -WHERE b.id = c.id - AND c.slug IS NULL; - --- Resolve collisions to base / base-2 / base-3 / ... in ascending id order. --- Loops until no live (deleted_at IS NULL) duplicates remain — a re-assigned --- `base-N` could itself collide with another row whose base slug is already --- `base-N`, so a single pass is not enough. --- --- This produces a deterministic, collision-free assignment with the same --- semantics as the runtime (slugified connector_key fallback, numeric `-N` --- suffixing). It is NOT guaranteed to be byte-identical to what --- `ensureUniqueConnectionSlug` would pick for pathological mixed-name sets --- (the runtime resolves in row-creation order against live DB state, which --- pure SQL can't replay) — `packages/server/src/utils/connections.ts` is the --- source of truth for new rows. -DO $$ -DECLARE - v_changed integer; -BEGIN - LOOP - WITH ranked AS ( - SELECT - id, - organization_id, - slug, - -- strip any suffix we may have appended on a prior pass so the - -- base groups stay stable across iterations - regexp_replace(slug, '-[0-9]+$', '') AS base_slug, - row_number() OVER ( - PARTITION BY organization_id, regexp_replace(slug, '-[0-9]+$', '') - ORDER BY id - ) AS rn - FROM public.connections - WHERE deleted_at IS NULL - ), - target AS ( - SELECT - id, - CASE WHEN rn = 1 THEN base_slug ELSE base_slug || '-' || rn::text END AS desired_slug - FROM ranked - ) - UPDATE public.connections c - SET slug = t.desired_slug - FROM target t - WHERE t.id = c.id - AND c.slug IS DISTINCT FROM t.desired_slug; - - GET DIAGNOSTICS v_changed = ROW_COUNT; - EXIT WHEN v_changed = 0; - END LOOP; -END $$; - --- Guard: there must be no live-slug duplicate per org before the unique index. -DO $$ -DECLARE - v_dups integer; -BEGIN - SELECT count(*) INTO v_dups - FROM ( - SELECT organization_id, slug - FROM public.connections - WHERE deleted_at IS NULL - GROUP BY organization_id, slug - HAVING count(*) > 1 - ) d; - IF v_dups > 0 THEN - RAISE EXCEPTION 'connections.slug backfill left % duplicate (organization_id, slug) group(s) among live rows', v_dups; - END IF; -END $$; - -ALTER TABLE public.connections - ALTER COLUMN slug SET NOT NULL; - -CREATE UNIQUE INDEX IF NOT EXISTS connections_org_slug_unique - ON public.connections (organization_id, slug) - WHERE deleted_at IS NULL; - --- migrate:down - -DROP INDEX IF EXISTS public.connections_org_slug_unique; -ALTER TABLE public.connections - DROP COLUMN IF EXISTS slug; diff --git a/db/migrations/20260513000000_chat_user_identities.sql b/db/migrations/20260513000000_chat_user_identities.sql deleted file mode 100644 index 484767270..000000000 --- a/db/migrations/20260513000000_chat_user_identities.sql +++ /dev/null @@ -1,24 +0,0 @@ --- migrate:up - --- Maps a chat-platform user (Slack `U…`, …) to a Lobu account. Recorded as a --- side effect of `/lobu link ` — the code is minted by an authenticated --- `lobu run`, so `oauth_states.payload.createdBy` is the Lobu user. Once a user --- is linked here, they can re-bind any chat to an agent they can manage via --- `/lobu link ` without minting a fresh code. - -CREATE TABLE IF NOT EXISTS public.chat_user_identities ( - platform text NOT NULL, - team_id text NOT NULL DEFAULT '', -- workspace id; '' for platforms without one - platform_user_id text NOT NULL, - lobu_user_id text NOT NULL REFERENCES public."user"(id) ON DELETE CASCADE, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - PRIMARY KEY (platform, team_id, platform_user_id) -); - -CREATE INDEX IF NOT EXISTS chat_user_identities_lobu_user_idx - ON public.chat_user_identities (lobu_user_id); - --- migrate:down - -DROP TABLE IF EXISTS public.chat_user_identities; diff --git a/db/migrations/20260513120000_auth_profiles_device_binding.sql b/db/migrations/20260513120000_auth_profiles_device_binding.sql deleted file mode 100644 index 66142112a..000000000 --- a/db/migrations/20260513120000_auth_profiles_device_binding.sql +++ /dev/null @@ -1,50 +0,0 @@ --- migrate:up - --- Let an auth_profile of kind 'browser_session' live on a specific device worker --- instead of holding cookies in auth_data. When device_worker_id is set, cookies --- live on disk inside the Mac app's managed --user-data-dir at user_data_dir; --- the server never sees them. Cloud/fleet path (device_worker_id NULL, --- auth_data populated) is unchanged. - -ALTER TABLE public.auth_profiles - ADD COLUMN IF NOT EXISTS device_worker_id uuid, - ADD COLUMN IF NOT EXISTS browser_kind text, - ADD COLUMN IF NOT EXISTS user_data_dir text; - -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'auth_profiles_device_worker_id_fkey' - ) THEN - ALTER TABLE public.auth_profiles - ADD CONSTRAINT auth_profiles_device_worker_id_fkey - FOREIGN KEY (device_worker_id) - REFERENCES public.device_workers (id) - ON DELETE CASCADE; - END IF; -END$$; - -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'auth_profiles_browser_kind_check' - ) THEN - ALTER TABLE public.auth_profiles - ADD CONSTRAINT auth_profiles_browser_kind_check - CHECK (browser_kind IS NULL OR browser_kind = ANY (ARRAY['chrome','brave','arc','edge'])); - END IF; -END$$; - -CREATE INDEX IF NOT EXISTS auth_profiles_device_worker_idx - ON public.auth_profiles (device_worker_id) - WHERE device_worker_id IS NOT NULL; - --- migrate:down - -DROP INDEX IF EXISTS public.auth_profiles_device_worker_idx; -ALTER TABLE public.auth_profiles - DROP CONSTRAINT IF EXISTS auth_profiles_browser_kind_check, - DROP CONSTRAINT IF EXISTS auth_profiles_device_worker_id_fkey, - DROP COLUMN IF EXISTS user_data_dir, - DROP COLUMN IF EXISTS browser_kind, - DROP COLUMN IF EXISTS device_worker_id; diff --git a/db/migrations/20260513150000_auth_profiles_cdp_url.sql b/db/migrations/20260513150000_auth_profiles_cdp_url.sql deleted file mode 100644 index 4794bf6d2..000000000 --- a/db/migrations/20260513150000_auth_profiles_cdp_url.sql +++ /dev/null @@ -1,43 +0,0 @@ --- migrate:up - --- Add `cdp_url` to auth_profiles. For a device-bound `browser_session` --- profile, exactly one of {user_data_dir, cdp_url} should be set: --- user_data_dir → managed Chrome with isolated cookies (default) --- cdp_url → attach to a running Chrome via remote-debugging-port --- The application enforces this invariant; we don't add a CHECK constraint --- because the OR-on-NULL semantics are awkward to express and the column --- is harmless when both are NULL (legacy fleet path with cookies in --- auth_data jsonb). - -ALTER TABLE public.auth_profiles - ADD COLUMN IF NOT EXISTS cdp_url text; - --- A device-bound browser_session profile MUST set exactly one of --- (user_data_dir, cdp_url). Other profile kinds — and non-device-bound --- browser_session profiles (cookies in auth_data, fleet-served) — are --- exempt. Enforcing this at the DB stops a buggy admin tool or a bad --- merge from setting both and then having the connector silently prefer --- whichever code path it sees first. -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'auth_profiles_device_browser_path_xor' - ) THEN - ALTER TABLE public.auth_profiles - ADD CONSTRAINT auth_profiles_device_browser_path_xor - CHECK ( - device_worker_id IS NULL - OR profile_kind <> 'browser_session' - OR ( - (user_data_dir IS NOT NULL AND cdp_url IS NULL) - OR (user_data_dir IS NULL AND cdp_url IS NOT NULL) - ) - ); - END IF; -END$$; - --- migrate:down - -ALTER TABLE public.auth_profiles - DROP CONSTRAINT IF EXISTS auth_profiles_device_browser_path_xor, - DROP COLUMN IF EXISTS cdp_url; diff --git a/db/migrations/20260513200000_notifications_as_events.sql b/db/migrations/20260513200000_notifications_as_events.sql deleted file mode 100644 index 85ac9cbf9..000000000 --- a/db/migrations/20260513200000_notifications_as_events.sql +++ /dev/null @@ -1,86 +0,0 @@ --- migrate:up - --- Unify notifications with events. --- --- A notification was a (org, user, title, body, type, resource_url) row in --- its own table with per-user `is_read`. Conceptually it's an event with a --- particular kind + per-user delivery / read-state. This migration turns --- every notification into: --- 1. an event with semantic_type='notification' (org-wide visibility in --- the events stream — searchable, addressable, links into knowledge); --- 2. a notification_targets row (event_id, user_id, delivered_at, read_at) --- so the inbox still scopes to the targeted user. --- --- After this, "send to admins" inserts one event + N targets; "mark read" --- updates a target row; "unread count" counts target rows without read_at. --- Search across events naturally includes notifications, but a user's --- inbox is still private to them. - -CREATE TABLE public.notification_targets ( - event_id bigint NOT NULL REFERENCES public.events(id) ON DELETE CASCADE, - user_id text NOT NULL, - delivered_at timestamp with time zone NOT NULL DEFAULT now(), - read_at timestamp with time zone, - PRIMARY KEY (event_id, user_id) -); - --- Fast inbox lookups: list a user's unread notifications, newest first. -CREATE INDEX idx_notification_targets_user_unread - ON public.notification_targets (user_id, delivered_at DESC) - WHERE read_at IS NULL; - --- All of a user's notifications, newest first (for read-list pagination). -CREATE INDEX idx_notification_targets_user_all - ON public.notification_targets (user_id, delivered_at DESC); - --- Backfill existing notifications. We keep 1:1 row mapping (one event per --- legacy notification) for safety — at scale the right model is "one event, --- many targets" but the old schema didn't capture that and we can't --- retroactively coalesce without an oracle. -WITH legacy AS ( - SELECT id, organization_id, user_id, type, title, body, - resource_type, resource_id, resource_url, is_read, created_at - FROM public.notifications - ORDER BY id ASC -), -inserted AS ( - INSERT INTO public.events - (organization_id, title, payload_text, payload_type, semantic_type, - occurred_at, created_at, metadata, origin_id) - SELECT - l.organization_id, - l.title, - l.body, - 'text', - 'notification', - l.created_at, - l.created_at, - jsonb_build_object( - 'notification_type', l.type, - 'resource_type', l.resource_type, - 'resource_id', l.resource_id, - 'resource_url', l.resource_url, - 'legacy_notification_id', l.id - ), - 'notification:legacy:' || l.id::text - FROM legacy l - RETURNING id AS event_id, (metadata->>'legacy_notification_id')::bigint AS legacy_id -) -INSERT INTO public.notification_targets (event_id, user_id, delivered_at, read_at) -SELECT - i.event_id, - l.user_id, - l.created_at, - CASE WHEN l.is_read THEN l.created_at ELSE NULL END -FROM inserted i -JOIN public.notifications l ON l.id = i.legacy_id; - --- Drop the legacy table. All readers/writers go through the new service. -DROP TABLE public.notifications; - --- migrate:down - --- One-way migration. Recovery is from backup; events created here stay --- (deleting them would also wipe their notification_targets via CASCADE). --- If you really need to roll back: re-create the table, copy notifications --- back out of events + notification_targets, drop the event rows. diff --git a/db/migrations/20260514000000_scheduled_jobs.sql b/db/migrations/20260514000000_scheduled_jobs.sql deleted file mode 100644 index 1132bc932..000000000 --- a/db/migrations/20260514000000_scheduled_jobs.sql +++ /dev/null @@ -1,97 +0,0 @@ --- migrate:up - --- User-driven scheduled jobs. --- --- Why a separate table: --- * `runs` already holds *fired* / *pending-to-fire* rows via --- scheduler.spawn(). Each scheduled_jobs row is the *definition* of a --- recurring (or one-shot) schedule — its source of truth. --- * The ticker (`scheduled-jobs-tick`) scans this table on cron, spawns --- a runs row per firing via TaskScheduler.spawn, and advances --- next_run_at from `cron`. If the tick or a firing fails, the next --- tick re-reads the same row (next_run_at didn't move forward) and --- retries. Self-healing. --- * Attribution lives here: who scheduled it (user or agent), what run --- was the trigger, what event was the trigger. Lets "why did the --- system act?" become a single JOIN. --- * Cascade-on-delete: when an agent is deleted, all its schedules --- evaporate via the FK — no orphan wake-ups firing into the void. - -CREATE TABLE public.scheduled_jobs ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - organization_id text NOT NULL REFERENCES public.organization(id) ON DELETE CASCADE, - - -- What fires - action_type text NOT NULL, -- 'send_notification' | 'wake_agent' | ... - action_args jsonb NOT NULL, -- handler payload - cron text, -- null = one-shot; cron string = recurring - next_run_at timestamp with time zone NOT NULL, - last_fired_at timestamp with time zone, - last_fired_run_id bigint, -- the runs.id from the most recent firing - paused boolean NOT NULL DEFAULT false, - - description text NOT NULL, -- human summary for the UI / audit - - -- Attribution - created_by_user text, -- user that scheduled it (null when agent did) - created_by_agent text, -- agent that scheduled it (null when user did) - source_run_id bigint, -- runs.id that originated the scheduling, if any - source_event_id bigint, -- events.id that originated, if any - source_thread_id text, -- chat-thread context, if any - - created_at timestamp with time zone NOT NULL DEFAULT now(), - updated_at timestamp with time zone NOT NULL DEFAULT now(), - - CONSTRAINT scheduled_jobs_attribution_check CHECK ( - created_by_user IS NOT NULL OR created_by_agent IS NOT NULL - ) -); - --- Cascade: dropping an agent kills its scheduled jobs (so an agent's --- wake-ups don't outlive the agent itself). Conditional so the migration --- works on installs where the agents table doesn't exist yet (very --- old) — every row already has organization_id which is the harder constraint. -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'agents' AND relkind = 'r') THEN - ALTER TABLE public.scheduled_jobs - ADD CONSTRAINT scheduled_jobs_agent_fkey - FOREIGN KEY (created_by_agent) REFERENCES public.agents(id) ON DELETE CASCADE; - END IF; -END$$; - -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'runs' AND relkind = 'r') THEN - ALTER TABLE public.scheduled_jobs - ADD CONSTRAINT scheduled_jobs_source_run_fkey - FOREIGN KEY (source_run_id) REFERENCES public.runs(id) ON DELETE SET NULL; - END IF; -END$$; - -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'events' AND relkind = 'r') THEN - ALTER TABLE public.scheduled_jobs - ADD CONSTRAINT scheduled_jobs_source_event_fkey - FOREIGN KEY (source_event_id) REFERENCES public.events(id) ON DELETE SET NULL; - END IF; -END$$; - --- Index: the ticker's hot read. -CREATE INDEX idx_scheduled_jobs_due - ON public.scheduled_jobs (next_run_at) - WHERE NOT paused; - --- Index: list per-agent / per-user. -CREATE INDEX idx_scheduled_jobs_org_agent - ON public.scheduled_jobs (organization_id, created_by_agent) - WHERE created_by_agent IS NOT NULL; - -CREATE INDEX idx_scheduled_jobs_org_user - ON public.scheduled_jobs (organization_id, created_by_user) - WHERE created_by_user IS NOT NULL; - --- migrate:down - -DROP TABLE IF EXISTS public.scheduled_jobs; diff --git a/db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql b/db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql deleted file mode 100644 index 618ead818..000000000 --- a/db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql +++ /dev/null @@ -1,42 +0,0 @@ --- migrate:up - --- Drop the NOT NULL on auth_profiles.connector_key so browser_session --- profiles can be device-bound resources without a connector binding. --- --- A browser_session profile is physically (device, browser_kind, user_data_dir --- XOR cdp_url) — the connector_key was always a hint, not a gate. One CDP --- attach on a Mac already has cookies for every site the user is logged into; --- forcing one row per connector against the same cdp_url was bookkeeping for --- the DB's benefit, not the user's. Connection resolution falls back to --- "browser_session on the connection's device_worker_id" when no exact --- connector match exists. --- --- Other profile kinds (env, oauth_app, oauth_account, interactive) remain --- per-connector; the new check constraint keeps them honest. - -ALTER TABLE public.auth_profiles - ALTER COLUMN connector_key DROP NOT NULL; - -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'auth_profiles_connector_key_required' - ) THEN - ALTER TABLE public.auth_profiles - ADD CONSTRAINT auth_profiles_connector_key_required - CHECK ( - connector_key IS NOT NULL - OR profile_kind = 'browser_session' - ); - END IF; -END$$; - --- migrate:down - -ALTER TABLE public.auth_profiles - DROP CONSTRAINT IF EXISTS auth_profiles_connector_key_required; - --- Restoring NOT NULL would fail if any browser_session rows now have --- connector_key = NULL. Backfill with a placeholder before running this. -ALTER TABLE public.auth_profiles - ALTER COLUMN connector_key SET NOT NULL; diff --git a/db/migrations/20260514130000_connection_action_modes.sql b/db/migrations/20260514130000_connection_action_modes.sql deleted file mode 100644 index 02a0e857c..000000000 --- a/db/migrations/20260514130000_connection_action_modes.sql +++ /dev/null @@ -1,103 +0,0 @@ --- migrate:up - --- Collapse `connection.config.auto_approve_actions` (string[]) and --- `connection.config.require_approval_actions` (string[]) into a single --- `action_modes` (Record) map. --- --- The old two-array model couldn't express "agent must not call this op --- at all" — every action the connector defined was always reachable, the --- arrays only flipped approval prompts. The new map adds 'disabled' as the --- third state and gives every op an explicit user-chosen mode. --- --- Backfill rule, per row, for every op listed in either array: --- op in auto_approve_actions → action_modes[op] = 'auto' --- op in require_approval_actions → action_modes[op] = 'approval' --- When an op appears in both, 'approval' wins (it's the stricter signal: --- the user explicitly opted in to seeing an approval prompt). --- --- Ops the user never touched are not stored in action_modes; the server --- falls back to the connector's per-op `requires_approval` default at read --- time, which preserves today's "all on" behavior. --- --- We drop the two old keys in the same statement so the new state is the --- only state on disk after migration. - -UPDATE public.connections -SET config = ( - COALESCE(config, '{}'::jsonb) - - 'auto_approve_actions' - - 'require_approval_actions' - ) - || jsonb_build_object( - 'action_modes', - COALESCE( - ( - -- 'approval' wins over 'auto' when an op appears in both arrays - -- (MIN('approval', 'auto') = 'approval' lexicographically). - SELECT jsonb_object_agg(op_key, mode) - FROM ( - SELECT op_key, MIN(mode) AS mode - FROM ( - SELECT op_key, 'approval'::text AS mode - FROM jsonb_array_elements_text( - CASE - WHEN jsonb_typeof(config->'require_approval_actions') = 'array' - THEN config->'require_approval_actions' - ELSE '[]'::jsonb - END - ) AS op_key - UNION ALL - SELECT op_key, 'auto'::text AS mode - FROM jsonb_array_elements_text( - CASE - WHEN jsonb_typeof(config->'auto_approve_actions') = 'array' - THEN config->'auto_approve_actions' - ELSE '[]'::jsonb - END - ) AS op_key - ) all_modes - GROUP BY op_key - ) collapsed - ), - '{}'::jsonb - ) - ) -WHERE config IS NOT NULL - AND ( - config ? 'auto_approve_actions' - OR config ? 'require_approval_actions' - ); - --- migrate:down - --- Reverse the collapse: split action_modes back into the two arrays. --- 'auto' → auto_approve_actions --- 'approval' → require_approval_actions --- 'disabled' has no pre-refactor equivalent and is silently dropped on --- downgrade — the agent will see the op again as if no override existed. -UPDATE public.connections -SET config = ( - COALESCE(config, '{}'::jsonb) - 'action_modes' - ) - || jsonb_build_object( - 'auto_approve_actions', - COALESCE( - ( - SELECT jsonb_agg(key) - FROM jsonb_each_text(config->'action_modes') - WHERE value = 'auto' - ), - '[]'::jsonb - ), - 'require_approval_actions', - COALESCE( - ( - SELECT jsonb_agg(key) - FROM jsonb_each_text(config->'action_modes') - WHERE value = 'approval' - ), - '[]'::jsonb - ) - ) -WHERE config IS NOT NULL - AND jsonb_typeof(config->'action_modes') = 'object'; diff --git a/db/migrations/20260514160000_auth_profiles_mirror_mode.sql b/db/migrations/20260514160000_auth_profiles_mirror_mode.sql deleted file mode 100644 index 38a8caa93..000000000 --- a/db/migrations/20260514160000_auth_profiles_mirror_mode.sql +++ /dev/null @@ -1,32 +0,0 @@ --- migrate:up --- Relax the device-binding XOR for browser_session profiles to allow --- mirror mode, where neither user_data_dir nor cdp_url is set on the --- row (the source profile dir lives in auth_data.source_profile_dir). --- Keep the mutual exclusion of the two columns so they can't be set --- together; application validation enforces "exactly one of mirror / --- cdp / legacy" per row. - -ALTER TABLE auth_profiles - DROP CONSTRAINT IF EXISTS auth_profiles_device_browser_path_xor; - -ALTER TABLE auth_profiles - ADD CONSTRAINT auth_profiles_device_browser_path_mutex - CHECK ( - device_worker_id IS NULL - OR profile_kind <> 'browser_session' - OR user_data_dir IS NULL - OR cdp_url IS NULL - ); - --- migrate:down -ALTER TABLE auth_profiles - DROP CONSTRAINT IF EXISTS auth_profiles_device_browser_path_mutex; - -ALTER TABLE auth_profiles - ADD CONSTRAINT auth_profiles_device_browser_path_xor - CHECK ( - device_worker_id IS NULL - OR profile_kind <> 'browser_session' - OR ((user_data_dir IS NOT NULL) AND (cdp_url IS NULL)) - OR ((user_data_dir IS NULL) AND (cdp_url IS NOT NULL)) - ); diff --git a/db/migrations/20260515120000_agents_per_org_pk.sql b/db/migrations/20260515120000_agents_per_org_pk.sql deleted file mode 100644 index a1ed6f0f3..000000000 --- a/db/migrations/20260515120000_agents_per_org_pk.sql +++ /dev/null @@ -1,66 +0,0 @@ --- migrate:up --- Phase A of moving `agents` from a globally-unique `id` PK to a per-org --- composite PK `(organization_id, id)`. The application has always treated --- agents as org-scoped (every read/delete/list filters by organization_id), --- so the global PK is a latent footgun: two orgs cannot share an agent ID, --- and a stale agent in one org silently blocks another org from using the --- same name. --- --- This phase is INTENTIONALLY NON-BREAKING. It only: --- 1. Adds an `organization_id` column to each FK-holding child table --- (NULLABLE — backfilled here, set NOT NULL in a later phase once the --- app-code refactor lands so every INSERT writes the value). --- 2. Backfills `organization_id` from agents (no orphan rows in prod). --- 3. Adds a parallel UNIQUE constraint on `agents (organization_id, id)` --- so the schema is ready for the eventual PK swap. --- 4. Adds composite indexes on each child table so the upcoming --- org-scoped query patterns are fast from day one. --- --- The single-column PK on agents and the single-column FKs on child tables --- stay in place. App code keeps working unmodified. The PK swap and FK --- composite migration ship in a separate PR after the storage interfaces --- are plumbed with `organization_id`. - --- ── 1. Add organization_id columns (nullable for now). -ALTER TABLE agent_grants ADD COLUMN organization_id text; -ALTER TABLE agent_connections ADD COLUMN organization_id text; -ALTER TABLE agent_users ADD COLUMN organization_id text; -ALTER TABLE agent_channel_bindings ADD COLUMN organization_id text; -ALTER TABLE grants ADD COLUMN organization_id text; - --- ── 2. Backfill from agents. -UPDATE agent_grants SET organization_id = a.organization_id FROM agents a WHERE agent_grants.agent_id = a.id; -UPDATE agent_connections SET organization_id = a.organization_id FROM agents a WHERE agent_connections.agent_id = a.id; -UPDATE agent_users SET organization_id = a.organization_id FROM agents a WHERE agent_users.agent_id = a.id; -UPDATE agent_channel_bindings SET organization_id = a.organization_id FROM agents a WHERE agent_channel_bindings.agent_id = a.id; -UPDATE grants SET organization_id = a.organization_id FROM agents a WHERE grants.agent_id = a.id; - --- ── 3. Parallel UNIQUE on agents (organization_id, id). The single-column --- PK on (id) stays — this is purely additive and signals to readers --- that org-scoped uniqueness is the eventual model. The PK swap in a --- later migration will drop this UNIQUE and reuse the index for the --- new composite PK. -ALTER TABLE agents - ADD CONSTRAINT agents_organization_id_id_key UNIQUE (organization_id, id); - --- ── 4. Composite indexes on child tables for upcoming org-scoped queries. -CREATE INDEX agent_grants_org_agent_idx ON agent_grants (organization_id, agent_id); -CREATE INDEX agent_connections_org_agent_idx ON agent_connections (organization_id, agent_id); -CREATE INDEX agent_users_org_agent_idx ON agent_users (organization_id, agent_id); -CREATE INDEX agent_channel_bindings_org_agent_idx ON agent_channel_bindings (organization_id, agent_id); -CREATE INDEX grants_org_agent_idx ON grants (organization_id, agent_id); - --- migrate:down -DROP INDEX IF EXISTS grants_org_agent_idx; -DROP INDEX IF EXISTS agent_channel_bindings_org_agent_idx; -DROP INDEX IF EXISTS agent_users_org_agent_idx; -DROP INDEX IF EXISTS agent_connections_org_agent_idx; -DROP INDEX IF EXISTS agent_grants_org_agent_idx; - -ALTER TABLE agents DROP CONSTRAINT IF EXISTS agents_organization_id_id_key; - -ALTER TABLE grants DROP COLUMN IF EXISTS organization_id; -ALTER TABLE agent_channel_bindings DROP COLUMN IF EXISTS organization_id; -ALTER TABLE agent_users DROP COLUMN IF EXISTS organization_id; -ALTER TABLE agent_connections DROP COLUMN IF EXISTS organization_id; -ALTER TABLE agent_grants DROP COLUMN IF EXISTS organization_id; diff --git a/db/migrations/20260515150000_geo_enrichment.sql b/db/migrations/20260515150000_geo_enrichment.sql deleted file mode 100644 index 965bcc0a3..000000000 --- a/db/migrations/20260515150000_geo_enrichment.sql +++ /dev/null @@ -1,208 +0,0 @@ --- migrate:up - --- ============================================================================= --- Geo enrichment: reverse-geocode lat/lng → country / admin1 / place at the --- gateway, once, for every event with coordinates. Used by `apple.photos` --- today; gmaps reviews, github commit metadata, and any future geo-bearing --- connector benefit automatically. --- --- Three system-level reference tables (no organization_id — these are --- read-only geographic facts shared across all tenants) seeded from --- GeoNames (https://www.geonames.org/, CC-BY 4.0): --- --- geo_countries — country_code → name/continent/currency/etc. (~250 rows) --- geo_admin1 — state/province codes per country (~4k rows) --- geo_places — populated places (cities/towns/villages/hamlets); --- seeded from GeoNames cities1000.txt (~150k rows for v1). --- Can be upgraded to the full PPL-class subset of --- allCountries (~5M rows) without schema changes — --- nearest-neighbor query is the same shape. --- --- The `geo_lookup(lat, lng)` function returns the enriched row in one --- call. Nearest-neighbor uses PostGIS `geography(POINT, 4326)` + GiST so --- distance is true geodesic (not L2-on-degrees), and the index keeps it --- sub-millisecond at any table size we'd ever load. --- --- Run `scripts/seed-geo-data.sh` after this migration applies to populate --- the tables. The TS enrichment hook gracefully no-ops if the tables are --- empty or the function is missing, so partially-deployed installs keep --- working — events just don't get the enriched fields until seeding runs. --- --- ENVIRONMENTS WITHOUT POSTGIS: the entire migration is wrapped in a DO --- block that probes for the extension. If PostGIS isn't installable --- (PGlite in tests, restricted hosts), every statement below is skipped --- with a NOTICE. The runtime enrichment hook also fails open, so --- partially-supported environments keep functioning — they just don't --- get geo enrichment. --- ============================================================================= - -DO $migration$ -BEGIN - -- Try to install PostGIS. If it's not available on this host (PGlite - -- without the postgis extension registered, managed Postgres without - -- the extension, etc.), bail out cleanly. - BEGIN - CREATE EXTENSION IF NOT EXISTS postgis; - EXCEPTION - WHEN OTHERS THEN - RAISE NOTICE - 'geo-enrichment: PostGIS unavailable (%), skipping geo schema. Runtime enrichment will no-op.', - SQLERRM; - RETURN; - END; - - -- spatial_ref_sys row for SRID 4326 (WGS-84). Real PostGIS installs - -- bundle ~8000 standard projections; the pglite-postgis WASM build - -- ships an empty table to keep the bundle small. Inserting the one - -- row we use makes nearest-neighbour queries work everywhere; the - -- ON CONFLICT skips on prod where the row already exists. - INSERT INTO spatial_ref_sys (srid, auth_name, auth_srid, srtext, proj4text) - VALUES ( - 4326, - 'EPSG', - 4326, - 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]', - '+proj=longlat +datum=WGS84 +no_defs' - ) - ON CONFLICT (srid) DO NOTHING; - - -- Everything below this point assumes PostGIS is loaded. EXECUTE-wrapping - -- the DDL keeps the SQL parser from choking on `geography(POINT, 4326)` - -- when this whole DO block is parsed before the extension creates the type. - - -- geo_countries — ISO-2 country code → full record. Source: GeoNames - -- countryInfo.txt. - EXECUTE $ddl$ - CREATE TABLE IF NOT EXISTS geo_countries ( - code text PRIMARY KEY, - code3 text, - numeric_code integer, - fips text, - name text NOT NULL, - capital text, - area_sq_km numeric, - population bigint, - continent text, - tld text, - currency_code text, - currency_name text, - phone text, - postal_code_fmt text, - postal_code_re text, - languages text, - geonameid bigint, - neighbours text - ) - $ddl$; - - -- geo_admin1 — first-order administrative subdivisions. - -- Code shape: '.' (e.g. 'IT.07' = Lazio). - EXECUTE $ddl$ - CREATE TABLE IF NOT EXISTS geo_admin1 ( - code text PRIMARY KEY, - country_code text NOT NULL, - name text NOT NULL, - ascii_name text NOT NULL, - geonameid bigint - ) - $ddl$; - - EXECUTE 'CREATE INDEX IF NOT EXISTS geo_admin1_country_idx ON geo_admin1 (country_code)'; - - -- geo_places — populated places. `location` is a generated geography - -- point that the GiST index uses for nearest-neighbour lookup. Stays - -- sub-ms even at 5M+ rows. - EXECUTE $ddl$ - CREATE TABLE IF NOT EXISTS geo_places ( - geonameid bigint PRIMARY KEY, - name text NOT NULL, - ascii_name text NOT NULL, - alt_names text, - latitude double precision NOT NULL, - longitude double precision NOT NULL, - feature_class text, - feature_code text, - country_code text NOT NULL, - admin1_code text, - admin2_code text, - population bigint DEFAULT 0, - elevation_m integer, - timezone text, - location geography(POINT, 4326) - GENERATED ALWAYS AS ( - ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography - ) STORED - ) - $ddl$; - - EXECUTE 'CREATE INDEX IF NOT EXISTS geo_places_location_idx ON geo_places USING GIST (location)'; - EXECUTE 'CREATE INDEX IF NOT EXISTS geo_places_country_idx ON geo_places (country_code)'; - - -- geo_lookup(lat, lng) — single-call enrichment. - -- Returns the nearest populated place plus the country/admin1 join. - -- distance_km is included so callers can apply their own threshold - -- (e.g., reject results > 500 km away — ocean/desert coordinates that - -- would otherwise snap misleadingly to the closest coastal city). - EXECUTE $fn$ - CREATE OR REPLACE FUNCTION geo_lookup(p_lat double precision, p_lng double precision) - RETURNS TABLE ( - place_name text, - place_id bigint, - country_code text, - country_name text, - admin1_code text, - admin1_name text, - timezone text, - population bigint, - distance_km double precision - ) - LANGUAGE sql - STABLE - PARALLEL SAFE - AS $body$ - WITH nearest AS ( - SELECT - p.geonameid, - p.name, - p.country_code, - p.admin1_code, - p.timezone, - p.population, - ST_Distance( - p.location, - ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography - ) / 1000.0 AS distance_km - FROM geo_places p - ORDER BY p.location <-> ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography - LIMIT 1 - ) - SELECT - n.name AS place_name, - n.geonameid AS place_id, - n.country_code AS country_code, - c.name AS country_name, - CASE - WHEN n.admin1_code IS NULL OR n.admin1_code = '' THEN NULL - ELSE n.country_code || '.' || n.admin1_code - END AS admin1_code, - a.name AS admin1_name, - n.timezone AS timezone, - n.population AS population, - n.distance_km AS distance_km - FROM nearest n - LEFT JOIN geo_countries c ON c.code = n.country_code - LEFT JOIN geo_admin1 a ON a.code = n.country_code || '.' || n.admin1_code - $body$ - $fn$; -END -$migration$; - --- migrate:down - -DROP FUNCTION IF EXISTS geo_lookup(double precision, double precision); -DROP TABLE IF EXISTS geo_places; -DROP TABLE IF EXISTS geo_admin1; -DROP TABLE IF EXISTS geo_countries; --- Intentionally do NOT DROP EXTENSION postgis. The extension may be --- shared by other tables / future migrations on the same Postgres --- instance; rolling back this migration shouldn't take that down. diff --git a/db/migrations/20260515160000_drop_agents_org_id_unique.sql b/db/migrations/20260515160000_drop_agents_org_id_unique.sql deleted file mode 100644 index b3ac2ca21..000000000 --- a/db/migrations/20260515160000_drop_agents_org_id_unique.sql +++ /dev/null @@ -1,24 +0,0 @@ --- migrate:up --- Drop the parallel `UNIQUE (organization_id, id)` added in 20260515120000. --- It was meant as schema-prep for the eventual PK swap to (organization_id, --- id), but it actively broke `ON CONFLICT (id) DO NOTHING/UPDATE` callers. --- --- Why: Postgres' `ON CONFLICT (X)` only suppresses violations of the unique --- constraint matching exactly column set X. Adding a second unique constraint --- that overlaps with the PK means inserts can fail on the new constraint --- before reaching the PK conflict — and ON CONFLICT (id) doesn't catch it. --- Surfaced in `__tests__/integration/.../race-mcp` where parallel inserts of --- `(org-a, race-mcp-0)` started throwing `agents_organization_id_id_key` --- duplicates instead of being silently de-duped by the existing --- `ON CONFLICT (id) DO NOTHING` clause. --- --- The PK on `(id)` already enforces global uniqueness, which subsumes --- `(organization_id, id)` uniqueness — the new constraint was logically --- redundant. Phase C of the per-org PK migration will swap the PK directly --- without needing a parallel constraint as a stepping stone. - -ALTER TABLE agents DROP CONSTRAINT IF EXISTS agents_organization_id_id_key; - --- migrate:down -ALTER TABLE agents - ADD CONSTRAINT agents_organization_id_id_key UNIQUE (organization_id, id); diff --git a/db/migrations/20260515170000_auth_profiles_default_for_connector.sql b/db/migrations/20260515170000_auth_profiles_default_for_connector.sql deleted file mode 100644 index ae1241e77..000000000 --- a/db/migrations/20260515170000_auth_profiles_default_for_connector.sql +++ /dev/null @@ -1,23 +0,0 @@ --- migrate:up --- Admin-managed default app profile per (org, connector_key). --- Today getPrimaryAuthProfileForKind picks the most-recently-updated active --- oauth_app profile for the connector — admins have no way to designate --- which profile members should fall through to. The flag lets the admin --- pin a chosen profile; the resolver prefers flagged rows first. --- --- Constrained to oauth_app for now since that's the only kind where --- "default for connector" is meaningful (env / interactive / browser_session --- are picked by other rules — device binding, capture mode, etc.). - -ALTER TABLE auth_profiles - ADD COLUMN is_default_for_connector boolean NOT NULL DEFAULT false; - -CREATE UNIQUE INDEX auth_profiles_default_for_connector_unique - ON auth_profiles (organization_id, connector_key) - WHERE is_default_for_connector AND profile_kind = 'oauth_app'; - --- migrate:down -DROP INDEX IF EXISTS auth_profiles_default_for_connector_unique; - -ALTER TABLE auth_profiles - DROP COLUMN IF EXISTS is_default_for_connector; diff --git a/db/migrations/20260516120000_agents_per_org_pk_swap.sql b/db/migrations/20260516120000_agents_per_org_pk_swap.sql deleted file mode 100644 index 4936e26af..000000000 --- a/db/migrations/20260516120000_agents_per_org_pk_swap.sql +++ /dev/null @@ -1,125 +0,0 @@ --- migrate:up --- Phase C: swap `agents` PK from globally-unique `id` to per-org composite --- `(organization_id, id)`. The application has always treated agents as --- org-scoped (every read/list filters by organization_id) but the global PK --- silently blocked two orgs from sharing an agent ID — for example, a stale --- `food-ordering` in one org would prevent `food-ordering` in another. --- --- Phase A (20260515120000) added the org column + composite indexes on the --- 5 FK-holding child tables and backfilled values from agents. Phase B (the --- application-code refactor in this PR) plumbs `organization_id` through every --- INSERT/UPDATE/DELETE/SELECT touching these tables. This migration is the --- final structural swap: it drops the single-column PK + FKs, adds the --- composite PK + FKs, and widens the per-(agent,kind,pattern) uniques on --- agent_users / agent_grants / grants with organization_id. - --- ── 0. Set NOT NULL on the columns Phase A backfilled. --- Backfill defensively in case any rows snuck in NULL (e.g. embedded PGlite --- installs that bypassed the dbmate runner during a partial state). -UPDATE public.agent_grants c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id; -UPDATE public.agent_connections c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id; -UPDATE public.agent_users c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id; -UPDATE public.agent_channel_bindings c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id; -UPDATE public.grants c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id; - --- Drop any orphan rows (agent_id with no matching agents row). Backfill --- can't recover these. -DELETE FROM public.agent_grants WHERE organization_id IS NULL; -DELETE FROM public.agent_connections WHERE organization_id IS NULL; -DELETE FROM public.agent_users WHERE organization_id IS NULL; -DELETE FROM public.agent_channel_bindings WHERE organization_id IS NULL; -DELETE FROM public.grants WHERE organization_id IS NULL; - -ALTER TABLE public.agent_grants ALTER COLUMN organization_id SET NOT NULL; -ALTER TABLE public.agent_connections ALTER COLUMN organization_id SET NOT NULL; -ALTER TABLE public.agent_users ALTER COLUMN organization_id SET NOT NULL; -ALTER TABLE public.agent_channel_bindings ALTER COLUMN organization_id SET NOT NULL; -ALTER TABLE public.grants ALTER COLUMN organization_id SET NOT NULL; - --- ── 1. Drop the 6 single-column FKs into agents(id). -ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_agent_id_fkey; -ALTER TABLE public.agent_connections DROP CONSTRAINT IF EXISTS agent_connections_agent_id_fkey; -ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_agent_id_fkey; -ALTER TABLE public.agent_channel_bindings DROP CONSTRAINT IF EXISTS agent_channel_bindings_agent_id_fkey; -ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_agent_id_fkey; -ALTER TABLE public.scheduled_jobs DROP CONSTRAINT IF EXISTS scheduled_jobs_agent_fkey; - --- ── 2. Drop the unique/PK constraints on child tables that scope to bare agent_id. -ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_agent_id_pattern_key; -ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_pkey; -ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_pkey; - --- ── 3. Swap the PK on agents from (id) to (organization_id, id). -ALTER TABLE public.agents DROP CONSTRAINT IF EXISTS agents_pkey; -ALTER TABLE public.agents ADD CONSTRAINT agents_pkey PRIMARY KEY (organization_id, id); - --- ── 4. Re-add per-org-scoped uniques on the child tables. -ALTER TABLE public.agent_grants - ADD CONSTRAINT agent_grants_org_agent_pattern_key UNIQUE (organization_id, agent_id, pattern); -ALTER TABLE public.agent_users - ADD CONSTRAINT agent_users_pkey PRIMARY KEY (organization_id, agent_id, platform, user_id); -ALTER TABLE public.grants - ADD CONSTRAINT grants_pkey PRIMARY KEY (organization_id, agent_id, kind, pattern); - --- ── 5. Re-add composite FKs (organization_id, agent_id) → agents(organization_id, id). -ALTER TABLE public.agent_grants - ADD CONSTRAINT agent_grants_org_agent_fkey - FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; -ALTER TABLE public.agent_connections - ADD CONSTRAINT agent_connections_org_agent_fkey - FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; -ALTER TABLE public.agent_users - ADD CONSTRAINT agent_users_org_agent_fkey - FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; -ALTER TABLE public.agent_channel_bindings - ADD CONSTRAINT agent_channel_bindings_org_agent_fkey - FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; -ALTER TABLE public.grants - ADD CONSTRAINT grants_org_agent_fkey - FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; -ALTER TABLE public.scheduled_jobs - ADD CONSTRAINT scheduled_jobs_org_agent_fkey - FOREIGN KEY (organization_id, created_by_agent) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; - --- migrate:down --- Reverse the swap. NOTE: this WILL FAIL if two orgs ended up sharing an --- agent ID after this migration shipped (the previous PK on (id) requires --- global uniqueness). That's by design — this migration's whole purpose is --- to allow per-org agent IDs that the old PK forbids. - -ALTER TABLE public.scheduled_jobs DROP CONSTRAINT IF EXISTS scheduled_jobs_org_agent_fkey; -ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_org_agent_fkey; -ALTER TABLE public.agent_channel_bindings DROP CONSTRAINT IF EXISTS agent_channel_bindings_org_agent_fkey; -ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_org_agent_fkey; -ALTER TABLE public.agent_connections DROP CONSTRAINT IF EXISTS agent_connections_org_agent_fkey; -ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_org_agent_fkey; - -ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_pkey; -ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_pkey; -ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_org_agent_pattern_key; - -ALTER TABLE public.agents DROP CONSTRAINT IF EXISTS agents_pkey; -ALTER TABLE public.agents ADD CONSTRAINT agents_pkey PRIMARY KEY (id); - -ALTER TABLE public.agent_grants ADD CONSTRAINT agent_grants_agent_id_pattern_key UNIQUE (agent_id, pattern); -ALTER TABLE public.agent_users ADD CONSTRAINT agent_users_pkey PRIMARY KEY (agent_id, platform, user_id); -ALTER TABLE public.grants ADD CONSTRAINT grants_pkey PRIMARY KEY (agent_id, kind, pattern); - -ALTER TABLE public.agent_grants - ADD CONSTRAINT agent_grants_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE; -ALTER TABLE public.agent_connections - ADD CONSTRAINT agent_connections_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE; -ALTER TABLE public.agent_users - ADD CONSTRAINT agent_users_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE; -ALTER TABLE public.agent_channel_bindings - ADD CONSTRAINT agent_channel_bindings_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE; -ALTER TABLE public.grants - ADD CONSTRAINT grants_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE; -ALTER TABLE public.scheduled_jobs - ADD CONSTRAINT scheduled_jobs_agent_fkey FOREIGN KEY (created_by_agent) REFERENCES public.agents(id) ON DELETE CASCADE; - -ALTER TABLE public.grants ALTER COLUMN organization_id DROP NOT NULL; -ALTER TABLE public.agent_channel_bindings ALTER COLUMN organization_id DROP NOT NULL; -ALTER TABLE public.agent_users ALTER COLUMN organization_id DROP NOT NULL; -ALTER TABLE public.agent_connections ALTER COLUMN organization_id DROP NOT NULL; -ALTER TABLE public.agent_grants ALTER COLUMN organization_id DROP NOT NULL; diff --git a/db/migrations/20260516200000_events_search_tsv.sql b/db/migrations/20260516200000_events_search_tsv.sql deleted file mode 100644 index 4b65a526e..000000000 --- a/db/migrations/20260516200000_events_search_tsv.sql +++ /dev/null @@ -1,134 +0,0 @@ --- migrate:up - --- Materialize the fulltext search vector as a STORED column. --- --- Why a real column instead of the previous expression-indexed `to_tsvector(payload_text)`: --- 1. It includes both `title` (weight A) and `payload_text` (weight B) — the --- same shape buildSearchDocumentExpr() in content-search.ts uses for --- ranking. Retrieval (@@) and ts_rank_cd now read the same vector, so --- title-only hits surface correctly and ranking doesn't recompute the --- vector per matched row at query time. --- 2. Planner-stable: `search_tsv @@ to_tsquery(...)` is a plain column --- reference — no expression-shape matching, no aliasing risk where the --- GIN gets skipped because the WHERE expression isn't byte-identical to --- the indexed expression. --- 3. The new GIN strictly subsumes the old payload-only one (same lexemes --- plus title's), so we drop the old index and recover its write --- amplification on every events insert. --- --- Operational note: ADD COLUMN ... GENERATED STORED rewrites the events --- table under ACCESS EXCLUSIVE. On a 1M-row table expect on the order of a --- minute; run during a quiet window. CONCURRENTLY does not apply to ADD --- COLUMN; it only applies to CREATE INDEX, which is a separate statement --- below. - -ALTER TABLE public.events - ADD COLUMN search_tsv tsvector GENERATED ALWAYS AS ( - setweight(to_tsvector('english', COALESCE(title, '')), 'A') || - setweight(to_tsvector('english', COALESCE(payload_text, '')), 'B') - ) STORED; - -CREATE INDEX idx_events_search_tsv ON public.events USING gin (search_tsv); - -DROP INDEX IF EXISTS public.idx_events_fulltext; - --- Recreate the current_event_records view so it exposes search_tsv to --- callers that go through the supersession filter (the view materializes --- its column list at create time, so `e.*`-style auto-pickup doesn't --- apply). buildSearchDocumentExpr() reads `f.search_tsv` / `fi.search_tsv` --- where f and fi are this view's aliases, so without this the view-backed --- queries fail with "column ... does not exist". - -DROP VIEW IF EXISTS public.current_event_records; -CREATE VIEW public.current_event_records AS - SELECT e.id, - e.organization_id, - e.entity_ids, - e.origin_id, - e.title, - e.payload_type, - e.payload_text, - e.payload_data, - e.payload_template, - e.attachments, - e.metadata, - e.score, - emb.embedding, - e.author_name, - e.source_url, - e.occurred_at, - e.created_at, - e.origin_parent_id, - COALESCE(length(e.payload_text), 0) AS content_length, - e.search_tsv, - e.origin_type, - e.connector_key, - e.connection_id, - e.feed_key, - e.feed_id, - e.run_id, - e.semantic_type, - e.client_id, - e.created_by, - e.interaction_type, - e.interaction_status, - e.interaction_input_schema, - e.interaction_input, - e.interaction_output, - e.interaction_error, - e.supersedes_event_id - FROM (public.events e - LEFT JOIN public.event_embeddings emb ON ((emb.event_id = e.id))) - WHERE (NOT (EXISTS ( SELECT 1 - FROM public.events newer - WHERE (newer.supersedes_event_id = e.id)))); - --- migrate:down - -DROP VIEW IF EXISTS public.current_event_records; -CREATE VIEW public.current_event_records AS - SELECT e.id, - e.organization_id, - e.entity_ids, - e.origin_id, - e.title, - e.payload_type, - e.payload_text, - e.payload_data, - e.payload_template, - e.attachments, - e.metadata, - e.score, - emb.embedding, - e.author_name, - e.source_url, - e.occurred_at, - e.created_at, - e.origin_parent_id, - COALESCE(length(e.payload_text), 0) AS content_length, - e.origin_type, - e.connector_key, - e.connection_id, - e.feed_key, - e.feed_id, - e.run_id, - e.semantic_type, - e.client_id, - e.created_by, - e.interaction_type, - e.interaction_status, - e.interaction_input_schema, - e.interaction_input, - e.interaction_output, - e.interaction_error, - e.supersedes_event_id - FROM (public.events e - LEFT JOIN public.event_embeddings emb ON ((emb.event_id = e.id))) - WHERE (NOT (EXISTS ( SELECT 1 - FROM public.events newer - WHERE (newer.supersedes_event_id = e.id)))); - -CREATE INDEX idx_events_fulltext ON public.events - USING gin (to_tsvector('english'::regconfig, COALESCE(payload_text, ''::text))); -DROP INDEX IF EXISTS public.idx_events_search_tsv; -ALTER TABLE public.events DROP COLUMN search_tsv; diff --git a/db/migrations/20260516200100_events_lifecycle_changes_index.sql b/db/migrations/20260516200100_events_lifecycle_changes_index.sql deleted file mode 100644 index 68ec828ce..000000000 --- a/db/migrations/20260516200100_events_lifecycle_changes_index.sql +++ /dev/null @@ -1,25 +0,0 @@ --- migrate:up - --- Partial index for the dashboard's 14-day lifecycle-cumulative-stats query --- (lifecycleCumulativeStatsSql in packages/owletto/src/lib/api/metric-series.ts). --- --- The query reads events older than `now() - interval '14 days'` and --- aggregates by date_trunc('day', created_at), with every COUNT FILTER --- already gated on semantic_type='change' AND metadata->>'category'='lifecycle'. --- A partial index whose predicate matches that gate prunes the scan from --- "all events in the last 14 days for the org" to "just the lifecycle --- changes" — typically a small slice of total events. The aggregation --- itself is cheap once the row set is narrow. --- --- Why not a materialized view: at current scale the bottleneck is the row --- scan, not the per-bucket COUNT FILTER work. A partial index removes the --- scan cost without introducing a refresh job or staleness. Revisit if the --- post-prune row count grows into the millions. - -CREATE INDEX idx_events_lifecycle_changes - ON public.events (organization_id, created_at) - WHERE semantic_type = 'change' AND metadata->>'category' = 'lifecycle'; - --- migrate:down - -DROP INDEX IF EXISTS public.idx_events_lifecycle_changes; diff --git a/db/migrations/20260517010000_drop_unused_indexes.sql b/db/migrations/20260517010000_drop_unused_indexes.sql deleted file mode 100644 index 2e9aa35c0..000000000 --- a/db/migrations/20260517010000_drop_unused_indexes.sql +++ /dev/null @@ -1,49 +0,0 @@ --- migrate:up - --- Drop 4 indexes that pg_stat_user_indexes reported `idx_scan = 0` after 28h --- of prod uptime AND are not referenced from any active code path. --- --- A larger set (4 more, ~5 GB combined) was originally bundled here but --- review caught they're not actually unused — they're dormant. The three --- big search indexes (`idx_events_embedding`, `idx_events_raw_content_trgm`, --- `idx_events_search_tsv`) are explicitly used by the ANN/fulltext/trigram --- branches of `approximate_candidate_search` in --- `packages/server/src/utils/content-search.ts:1707-1733`. The `search()` --- agent tool path threads through there; prod just hasn't called it in --- 28h, but a single user-initiated search would now time out at 6s and --- return empty results without those indexes (`content-search.ts:1850-1863`). --- Similarly `idx_events_run_id` backs the "view in memory" filter --- (`content-query-filters.ts:197-201`); rare, but a real path. --- --- Keep those four until either (a) the dormant features are removed in --- code, or (b) measured prod traffic confirms they're abandoned. --- --- What remains is small but still real write amplification: each kept --- INSERT into events updates these btrees. Combined size ~66 MB — --- modest reclaim, but zero downside since the underlying queries don't --- exist anywhere in the codebase today (verified by grep). --- --- Plain `DROP INDEX` (not CONCURRENTLY) is used because dbmate's --- `transaction:false` directive doesn't actually exit the transaction --- block against the `pq` driver — see the comment in --- 20260426130001_db_integrity_cleanup_concurrent.sql. These 4 indexes --- are all small btrees so the ACCESS EXCLUSIVE on `events` during the --- drop is sub-second; no operator runbook needed. - -DROP INDEX IF EXISTS public.idx_events_entity_ids_occurred_at; -DROP INDEX IF EXISTS public.idx_events_origin_parent_id; -DROP INDEX IF EXISTS public.idx_events_thread_lookup; -DROP INDEX IF EXISTS public.idx_events_type; - --- migrate:down - -CREATE INDEX IF NOT EXISTS idx_events_entity_ids_occurred_at - ON public.events USING btree ((entity_ids[1]), occurred_at DESC, id DESC) - WHERE ((entity_ids IS NOT NULL) AND (entity_ids <> '{}'::bigint[])); -CREATE INDEX IF NOT EXISTS idx_events_origin_parent_id - ON public.events USING btree (origin_parent_id); -CREATE INDEX IF NOT EXISTS idx_events_thread_lookup - ON public.events USING btree (origin_parent_id, occurred_at) - WHERE (origin_parent_id IS NOT NULL); -CREATE INDEX IF NOT EXISTS idx_events_type - ON public.events USING btree (origin_type) WHERE (origin_type IS NOT NULL); diff --git a/db/migrations/20260517020000_softdelete_orphan_feeds.sql b/db/migrations/20260517020000_softdelete_orphan_feeds.sql deleted file mode 100644 index c18f67061..000000000 --- a/db/migrations/20260517020000_softdelete_orphan_feeds.sql +++ /dev/null @@ -1,56 +0,0 @@ --- migrate:up - --- Soft-delete feeds whose (connector_key, organization_id) has no active --- connector_definition row. --- --- Why: the 2026-05-16 audit found feeds 117-155 (and others) referencing --- connector_key='website' in orgs that have no active definition for it --- (only one org has `website` active; one definition is archived). Every --- CheckDueFeeds tick (every minute) tried to materialize a sync run for --- these feeds and threw "No active connector definition found for X." — --- producing ~380 error logs / minute that masked real signal in stdout. --- --- The app-side code path now warns + skips (no throw) for the same case --- so future orphans don't spam logs either. This migration is the one-time --- cleanup of the existing data. --- --- Conservative criteria — match exactly the set CheckDueFeeds processes --- (so we only soft-delete feeds that actually produce the error stream). --- - feed has no pinned_version (= would have looked up connector_definitions) --- - feed.deleted_at IS NULL (still considered active) --- - feed.status = 'active' (CheckDueFeeds filters on this — see --- packages/server/src/scheduled/check-due-feeds.ts:36-43) --- - connection.deleted_at IS NULL AND connection.status = 'active' (same) --- - NO active connector_definition exists for that (key, organization) pair --- --- Feeds in paused / pending_auth / error / revoked states are left alone --- — operators may be mid-recovery on them and they don't contribute to --- the error spam (CheckDueFeeds skips them anyway). --- --- The same feed remains recoverable: clearing `deleted_at` + reinstalling --- the connector definition for the org restores it. - -UPDATE public.feeds f -SET deleted_at = now() -FROM public.connections c -WHERE f.connection_id = c.id - AND f.deleted_at IS NULL - AND f.pinned_version IS NULL - AND f.status = 'active' - AND c.deleted_at IS NULL - AND c.status = 'active' - AND NOT EXISTS ( - SELECT 1 - FROM public.connector_definitions cd - WHERE cd.key = c.connector_key - AND cd.organization_id = f.organization_id - AND cd.status = 'active' - ); - --- migrate:down - --- No-op: re-attaching the orphan feeds would require knowing which were --- soft-deleted by this migration vs. by an operator action. The original --- error condition is fixed in code; this migration is a one-shot data --- cleanup. To recover specific feeds in prod, clear `deleted_at` on the --- targeted rows manually and re-install the connector definition. diff --git a/db/migrations/20260517030000_pat_worker_id_binding.sql b/db/migrations/20260517030000_pat_worker_id_binding.sql deleted file mode 100644 index a5c5ec3c9..000000000 --- a/db/migrations/20260517030000_pat_worker_id_binding.sql +++ /dev/null @@ -1,27 +0,0 @@ --- migrate:up - --- Bind PATs minted via /api/me/devices/mint-child-token to a specific --- worker_id. Without this, a chrome-extension child PAT could post any --- worker_id at /api/workers/poll and register a new device_workers row --- under a different platform, bypassing the gateway's capability --- authorization. The poll handler now refuses a request whose body's --- `worker_id` doesn't match the PAT's bound `worker_id` (when non-NULL). --- --- Legacy PATs (CLI tokens, OAuth-issued bearers via dynamic client --- registration on the Mac/iOS bridges) keep worker_id = NULL; the poll --- handler treats NULL as "no binding" and lets the body's value pass. - -ALTER TABLE public.personal_access_tokens - ADD COLUMN IF NOT EXISTS worker_id text; - -CREATE INDEX IF NOT EXISTS idx_personal_access_tokens_worker_id - ON public.personal_access_tokens (worker_id) - WHERE worker_id IS NOT NULL; - -COMMENT ON COLUMN public.personal_access_tokens.worker_id IS - 'Optional binding to a specific device_workers.worker_id. Set by /api/me/devices/mint-child-token. When non-NULL, /api/workers/poll requires the request body''s worker_id to match.'; - --- migrate:down - -DROP INDEX IF EXISTS public.idx_personal_access_tokens_worker_id; -ALTER TABLE public.personal_access_tokens DROP COLUMN IF EXISTS worker_id; diff --git a/db/migrations/20260517040000_archive_orphan_watchers.sql b/db/migrations/20260517040000_archive_orphan_watchers.sql deleted file mode 100644 index 6bddaee33..000000000 --- a/db/migrations/20260517040000_archive_orphan_watchers.sql +++ /dev/null @@ -1,30 +0,0 @@ --- migrate:up - --- Archive watchers that are flagged `status = 'active'` but have no `agent_id`. --- The scheduler (packages/server/src/watchers/automation.ts:469) already --- filters them out with `WHERE w.agent_id IS NOT NULL`, so these rows are --- zombies — visible in the API, redirect-bounced on the watcher detail --- route, never actually executing. --- --- 2026-05-17 prod audit: 28 active orphans across 11 orgs (most originated --- from older create paths before `agent_id` was wired in). Archiving them --- aligns the stored status with their actual execution state and lets the --- `/$owner/watchers/$watcherId` redirect logic stop silently sending users --- to /agents. --- --- The write-time guard (this same PR, manage_watchers.ts) rejects new --- watcher creates/updates that set a schedule without an agent, so the --- orphan set can't grow again. - -UPDATE public.watchers -SET status = 'archived', - updated_at = now() -WHERE status = 'active' - AND agent_id IS NULL; - --- migrate:down - --- No-op: this is a one-shot data cleanup. The original rows can be restored --- manually if needed by assigning an agent_id and flipping status back to --- 'active'; without an agent, status='active' is meaningless to the --- scheduler. diff --git a/db/migrations/20260517050000_watcher_agent_id_not_null.sql b/db/migrations/20260517050000_watcher_agent_id_not_null.sql deleted file mode 100644 index abc45880d..000000000 --- a/db/migrations/20260517050000_watcher_agent_id_not_null.sql +++ /dev/null @@ -1,34 +0,0 @@ --- migrate:up - --- Delete every watcher with `agent_id IS NULL` and enforce the column at --- the schema level. These rows are leftovers from before the watcher --- scheduler required `agent_id` (see watchers/automation.ts:469) — they --- couldn't fire and the prior migration 20260517040000 had already --- archived the active subset. Their dependent rows (windows, reactions, --- versions, classifiers, field feedback) describe runtime state for --- watchers that will never execute; ON DELETE CASCADE clears them. --- `runs.watcher_id` is ON DELETE SET NULL, so the 21k historical run --- records remain — they just lose the watcher linkage. --- --- Application-level guards in manage_watchers.ts (handleCreate + --- handleCreateFromVersion) reject new inserts without `agent_id`; the --- NOT NULL constraint below is the matching DB-level enforcement so --- bypass paths can't reintroduce the zombie state. - -DELETE FROM public.watchers WHERE agent_id IS NULL; - -ALTER TABLE public.watchers - ALTER COLUMN agent_id SET NOT NULL; - --- The existing index is partial (`WHERE agent_id IS NOT NULL`), which is --- a tautology once the column is NOT NULL. Replace it with an unconditional --- index so explain plans don't show the dead predicate. -DROP INDEX IF EXISTS public.idx_watchers_agent_id; -CREATE INDEX idx_watchers_agent_id ON public.watchers USING btree (agent_id); - --- migrate:down - -ALTER TABLE public.watchers - ALTER COLUMN agent_id DROP NOT NULL; - --- No restoring deleted rows — this is forward-only data cleanup. diff --git a/db/migrations/20260517060000_watcher_schema_additions.sql b/db/migrations/20260517060000_watcher_schema_additions.sql deleted file mode 100644 index 1ca536903..000000000 --- a/db/migrations/20260517060000_watcher_schema_additions.sql +++ /dev/null @@ -1,78 +0,0 @@ --- migrate:up - --- Adds the columns needed by the device-aware watcher dispatcher (see --- lobu-ai/lobu#798 / #799): --- --- * device_worker_id — pin a watcher to a specific device worker --- (when its inputs live on that device). --- * agent_kind — overrides the owning agent's default kind for --- this watcher (e.g. "background", "notifier"). --- * notification_channel — where firings surface: canvas, OS notification, --- or both. --- * notification_priority — priority class used by the dispatcher's --- interrupt budget. --- * min_cooldown_seconds — minimum seconds between two firings of the --- same watcher (0 = no cooldown). --- * last_fired_at — last time this watcher actually dispatched --- a notification/canvas item. --- --- Also adds device_workers.notification_budget_per_day for the per-device --- global interrupt budget. 10/day is a placeholder default; tune later. - -ALTER TABLE public.watchers - ADD COLUMN device_worker_id uuid REFERENCES public.device_workers(id), - ADD COLUMN agent_kind text, - ADD COLUMN notification_channel text NOT NULL DEFAULT 'canvas', - ADD COLUMN notification_priority text NOT NULL DEFAULT 'normal', - ADD COLUMN min_cooldown_seconds integer NOT NULL DEFAULT 0, - ADD COLUMN last_fired_at timestamp with time zone; - -ALTER TABLE public.watchers - ADD CONSTRAINT watchers_notification_channel_check - CHECK (notification_channel IN ('canvas', 'notification', 'both')); - -ALTER TABLE public.watchers - ADD CONSTRAINT watchers_notification_priority_check - CHECK (notification_priority IN ('low', 'normal', 'high')); - -ALTER TABLE public.watchers - ADD CONSTRAINT watchers_min_cooldown_seconds_nonneg - CHECK (min_cooldown_seconds >= 0); - -CREATE INDEX IF NOT EXISTS idx_watchers_device_worker_id - ON public.watchers (device_worker_id) - WHERE device_worker_id IS NOT NULL; - -ALTER TABLE public.device_workers - ADD COLUMN notification_budget_per_day integer NOT NULL DEFAULT 10; - -ALTER TABLE public.device_workers - ADD CONSTRAINT device_workers_notification_budget_per_day_nonneg - CHECK (notification_budget_per_day >= 0); - --- migrate:down - -ALTER TABLE public.device_workers - DROP CONSTRAINT IF EXISTS device_workers_notification_budget_per_day_nonneg; - -ALTER TABLE public.device_workers - DROP COLUMN IF EXISTS notification_budget_per_day; - -DROP INDEX IF EXISTS public.idx_watchers_device_worker_id; - -ALTER TABLE public.watchers - DROP CONSTRAINT IF EXISTS watchers_min_cooldown_seconds_nonneg; - -ALTER TABLE public.watchers - DROP CONSTRAINT IF EXISTS watchers_notification_priority_check; - -ALTER TABLE public.watchers - DROP CONSTRAINT IF EXISTS watchers_notification_channel_check; - -ALTER TABLE public.watchers - DROP COLUMN IF EXISTS last_fired_at, - DROP COLUMN IF EXISTS min_cooldown_seconds, - DROP COLUMN IF EXISTS notification_priority, - DROP COLUMN IF EXISTS notification_channel, - DROP COLUMN IF EXISTS agent_kind, - DROP COLUMN IF EXISTS device_worker_id; diff --git a/db/migrations/20260517150000_goals_primitive.sql b/db/migrations/20260517150000_goals_primitive.sql deleted file mode 100644 index 48c17e4ce..000000000 --- a/db/migrations/20260517150000_goals_primitive.sql +++ /dev/null @@ -1,55 +0,0 @@ --- migrate:up - --- Goals primitive — a top-level handle that groups watchers under a single --- user-facing intent (e.g. "Keep my CRM clean", "Watch competitors"). Each --- watcher may optionally point at a goal; goals are the surface the --- canvas/UI (#801) hangs off, while watchers stay the executable unit. --- --- Schema notes: --- - organization_id is `text` (the better-auth `organization.id`) to match --- the rest of the schema; the issue body called it `integer`, but every --- other org-scoped table (watchers, feeds, connections, …) uses text. --- - `(organization_id, slug)` is unique so `lobu apply` can upsert by slug --- the same way it does for watchers/agents. --- - Goal-template loading from YAML is out of scope here; `template_key` --- is just a free-form pointer for the future loader to claim. --- - `metadata` is jsonb for forward-compat (icon, color, owner, etc.) --- without another migration each time the UI grows a knob. - -CREATE TABLE public.goals ( - id bigserial PRIMARY KEY, - organization_id text NOT NULL REFERENCES public.organization(id) ON DELETE CASCADE, - slug text NOT NULL, - name text NOT NULL, - description text, - status text NOT NULL DEFAULT 'active', - template_key text, - metadata jsonb NOT NULL DEFAULT '{}'::jsonb, - created_at timestamp with time zone NOT NULL DEFAULT now(), - updated_at timestamp with time zone NOT NULL DEFAULT now(), - - CONSTRAINT goals_status_check - CHECK (status IN ('active', 'paused', 'archived')), - CONSTRAINT goals_org_slug_unique - UNIQUE (organization_id, slug) -); - -CREATE INDEX idx_goals_organization_id - ON public.goals (organization_id); - --- Watcher → goal link. NULL means "ungrouped" (today's behavior). ON DELETE --- SET NULL keeps the watcher alive when its goal is archived/deleted; the --- watcher just becomes ungrouped. -ALTER TABLE public.watchers - ADD COLUMN goal_id bigint REFERENCES public.goals(id) ON DELETE SET NULL; - -CREATE INDEX idx_watchers_goal_id - ON public.watchers (goal_id) - WHERE goal_id IS NOT NULL; - --- migrate:down - -ALTER TABLE public.watchers - DROP COLUMN IF EXISTS goal_id; - -DROP TABLE IF EXISTS public.goals; diff --git a/db/migrations/20260517160000_drop_goals_primitive.sql b/db/migrations/20260517160000_drop_goals_primitive.sql deleted file mode 100644 index bb6077ad1..000000000 --- a/db/migrations/20260517160000_drop_goals_primitive.sql +++ /dev/null @@ -1,45 +0,0 @@ --- migrate:up - --- Revert the goals primitive added in 20260517150000_goals_primitive.sql. --- Agents already encapsulate the watcher-grouping use case via --- watchers.agent_id; goals were a redundant nullable FK + parallel CRUD with --- zero behavioral value. The primitive never shipped in a release. - -DROP INDEX IF EXISTS idx_watchers_goal_id; - -ALTER TABLE public.watchers - DROP COLUMN IF EXISTS goal_id; - -DROP INDEX IF EXISTS idx_goals_organization_id; - -DROP TABLE IF EXISTS public.goals; - --- migrate:down - -CREATE TABLE public.goals ( - id bigserial PRIMARY KEY, - organization_id text NOT NULL REFERENCES public.organization(id) ON DELETE CASCADE, - slug text NOT NULL, - name text NOT NULL, - description text, - status text NOT NULL DEFAULT 'active', - template_key text, - metadata jsonb NOT NULL DEFAULT '{}'::jsonb, - created_at timestamp with time zone NOT NULL DEFAULT now(), - updated_at timestamp with time zone NOT NULL DEFAULT now(), - - CONSTRAINT goals_status_check - CHECK (status IN ('active', 'paused', 'archived')), - CONSTRAINT goals_org_slug_unique - UNIQUE (organization_id, slug) -); - -CREATE INDEX idx_goals_organization_id - ON public.goals (organization_id); - -ALTER TABLE public.watchers - ADD COLUMN goal_id bigint REFERENCES public.goals(id) ON DELETE SET NULL; - -CREATE INDEX idx_watchers_goal_id - ON public.watchers (goal_id) - WHERE goal_id IS NOT NULL; diff --git a/db/migrations/20260518000000_pending_interactions.sql b/db/migrations/20260518000000_pending_interactions.sql deleted file mode 100644 index b51ecae10..000000000 --- a/db/migrations/20260518000000_pending_interactions.sql +++ /dev/null @@ -1,49 +0,0 @@ --- migrate:up - --- Per-question state for the chat-interaction bridge — moved out of the --- gateway's in-process Map so a button click that lands on pod B can claim --- a question registered on pod A. The bridge keeps a small per-pod cache for --- the platform `SentMessage` (used to edit the original card on click) since --- that's a non-serializable SDK handle; everything that matters for routing --- the click back into the worker (PostedQuestion + connection context) lives --- here. --- --- The claim path scopes by `(id, organization_id, connection_id, --- expected_user_id)` — keying by `id` alone would let a click from one --- connection or one user consume a question registered for another. The --- columns are NOT NULL so the SQL claim is a single index hit with no --- branching for NULL semantics. - -CREATE TABLE public.pending_interactions ( - id text PRIMARY KEY, - organization_id text NOT NULL REFERENCES public.organization(id) ON DELETE CASCADE, - connection_id text NOT NULL, - expected_user_id text NOT NULL, - entry_payload jsonb NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT now(), - claimed_at timestamp with time zone -); - --- Claim path is --- UPDATE pending_interactions --- SET claimed_at = now() --- WHERE id = $1 --- AND organization_id = $2 --- AND connection_id = $3 --- AND expected_user_id = $4 --- AND claimed_at IS NULL --- RETURNING entry_payload --- — a partial index on the unclaimed predicate keeps the lookup index-only. -CREATE INDEX idx_pending_interactions_unclaimed - ON public.pending_interactions (id, organization_id, connection_id, expected_user_id) - WHERE claimed_at IS NULL; - --- Background sweeper drops rows older than 24h; index keeps that scan cheap. -CREATE INDEX idx_pending_interactions_created_at - ON public.pending_interactions (created_at); - --- migrate:down - -DROP INDEX IF EXISTS public.idx_pending_interactions_created_at; -DROP INDEX IF EXISTS public.idx_pending_interactions_unclaimed; -DROP TABLE IF EXISTS public.pending_interactions; diff --git a/db/migrations/20260518010000_runs_heartbeat_reaper_index.sql b/db/migrations/20260518010000_runs_heartbeat_reaper_index.sql deleted file mode 100644 index 05c964002..000000000 --- a/db/migrations/20260518010000_runs_heartbeat_reaper_index.sql +++ /dev/null @@ -1,22 +0,0 @@ --- migrate:up - --- Partial index supporting the connector-lane stale-run reaper. The sweeper --- query in reapStaleRuns() (packages/server/src/scheduled/check-stalled-executions.ts) --- filters runs in the in-progress states (`claimed`, `running`) whose --- `last_heartbeat_at` is older than the configured threshold. Without this --- index every reaper tick does a full scan of `runs`. --- --- Restricted to the connector lanes (sync, action, embed_backfill, auth). The --- lobu-queue lanes (chat_message, schedule, agent_run, internal, task) have --- their own per-claim sweep inside RunsQueue keyed on `claimed_at`, not --- `last_heartbeat_at`. The `watcher` lane has a dedicated 2h-TTL sweep in --- watchers/automation.ts. - -CREATE INDEX IF NOT EXISTS idx_runs_heartbeat_inflight - ON public.runs (last_heartbeat_at) - WHERE status IN ('claimed', 'running') - AND run_type IN ('sync', 'action', 'embed_backfill', 'auth'); - --- migrate:down - -DROP INDEX IF EXISTS public.idx_runs_heartbeat_inflight; diff --git a/db/migrations/20260518020000_runs_heartbeat_inflight_narrow.sql b/db/migrations/20260518020000_runs_heartbeat_inflight_narrow.sql deleted file mode 100644 index f3fd60681..000000000 --- a/db/migrations/20260518020000_runs_heartbeat_inflight_narrow.sql +++ /dev/null @@ -1,36 +0,0 @@ --- migrate:up - --- Narrow the partial index `idx_runs_heartbeat_inflight` to the lanes that --- actually emit `client.heartbeat()` from the connector-worker executor. --- The previous index (20260518010000) covered all four connector lanes --- (sync, action, embed_backfill, auth), but only `sync` and `auth` runs call --- heartbeat() in packages/connector-worker/src/daemon/executor.ts. Action --- runs (executeActionRun) and embed_backfill runs (executeEmbedBackfillRun) --- never heartbeat, so the reaper's heartbeat-based WHERE clause would mark --- any action/embed_backfill run lasting longer than the stale threshold --- (default 120s) as `timeout` while it is still executing. --- --- Drop and recreate to also align the index with the reaper query in --- packages/server/src/scheduled/check-stalled-executions.ts, which is --- updated to filter on the narrower lane set in the same change. --- --- Follow-ups (tracked as separate issues): --- 1. Wire `client.heartbeat()` into executeActionRun + executeEmbedBackfillRun --- so those lanes can be safely reaped. --- 2. Wire heartbeat into the Chrome/Owletto browser-worker run path. - -DROP INDEX IF EXISTS public.idx_runs_heartbeat_inflight; - -CREATE INDEX IF NOT EXISTS idx_runs_heartbeat_inflight - ON public.runs (last_heartbeat_at) - WHERE status IN ('claimed', 'running') - AND run_type IN ('sync', 'auth'); - --- migrate:down - -DROP INDEX IF EXISTS public.idx_runs_heartbeat_inflight; - -CREATE INDEX IF NOT EXISTS idx_runs_heartbeat_inflight - ON public.runs (last_heartbeat_at) - WHERE status IN ('claimed', 'running') - AND run_type IN ('sync', 'action', 'embed_backfill', 'auth'); diff --git a/db/migrations/20260518040000_agent_transcript_snapshot.sql b/db/migrations/20260518040000_agent_transcript_snapshot.sql deleted file mode 100644 index afce19c63..000000000 --- a/db/migrations/20260518040000_agent_transcript_snapshot.sql +++ /dev/null @@ -1,54 +0,0 @@ --- migrate:up - --- Per-run snapshot of OpenClaw's session.jsonl. Replaces the workspaces PVC --- as the source of truth for conversation continuity across pod boundaries. --- Today the helm chart pins `replicaCount: 1` because two pods can't mount --- the RWO volume the session lives on; this table is the prerequisite for --- flipping that. (PVC drop + chart change are Phase 5, separate PR.) --- --- Schema choice notes: --- - One row per (org, agent, conversation, run). The producer is --- pi-coding-agent's `SessionManager`, whose internal entry taxonomy --- evolves between library versions; we store the JSONL verbatim and --- replay it byte-for-byte on the next worker boot. --- - `terminal_status` discriminates success/failure paths so the next worker --- doesn't hydrate from a snapshot that ended in a dangling tool_use trace. --- Hydrate filters `WHERE terminal_status = 'completed'` and falls through --- to older completed snapshots if the latest run failed. --- - `byte_size` is `bytea_length(snapshot_jsonl::bytea)` — kept as a column --- so the dashboard can plot growth without an expensive `length()` scan. --- - No spill to R2: codex measured 633 KB max across 2050 production rows, --- well inside Postgres TOAST's comfortable range. Optimisation deferred --- until production data argues for it. - -CREATE TABLE public.agent_transcript_snapshot ( - id bigserial PRIMARY KEY, - organization_id text NOT NULL REFERENCES public.organization(id) ON DELETE CASCADE, - agent_id text NOT NULL, - conversation_id text NOT NULL, - run_id bigint NOT NULL REFERENCES public.runs(id) ON DELETE CASCADE, - snapshot_jsonl text NOT NULL, - byte_size integer NOT NULL, - terminal_status text NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT now(), - CONSTRAINT agent_transcript_snapshot_terminal_status_check - CHECK (terminal_status IN ('completed', 'failed', 'timeout', 'cancelled')), - UNIQUE (organization_id, agent_id, conversation_id, run_id) -); - --- Hydrate path is --- SELECT snapshot_jsonl FROM agent_transcript_snapshot --- WHERE organization_id = $1 AND agent_id = $2 AND conversation_id = $3 --- AND terminal_status = 'completed' --- ORDER BY run_id DESC LIMIT 1 --- — a descending index on (org, agent, conv, run_id) serves the scan as a --- single index-only seek. Partial-index on `terminal_status='completed'` --- isn't worth it (we still want admin tooling to be able to inspect failed --- snapshots without an index-not-applicable plan). -CREATE INDEX agent_transcript_snapshot_latest - ON public.agent_transcript_snapshot - (organization_id, agent_id, conversation_id, run_id DESC); - --- migrate:down - -DROP TABLE IF EXISTS public.agent_transcript_snapshot; diff --git a/db/migrations/20260518050000_runs_denormalize_agent_conversation.sql b/db/migrations/20260518050000_runs_denormalize_agent_conversation.sql deleted file mode 100644 index 7f0d8cf45..000000000 --- a/db/migrations/20260518050000_runs_denormalize_agent_conversation.sql +++ /dev/null @@ -1,36 +0,0 @@ --- migrate:up - --- Denormalize agent_id + conversation_id out of `runs.action_input` JSONB --- into real columns. Going forward `RunsQueue.send` populates these --- columns on insert; `isRunOwnedByJwtScope` reads them with a --- `COALESCE(scalar_column, action_input->>'key')` fallback so historical --- rows (which still have NULL in the new columns) keep authorizing --- correctly. --- --- Deliberately NO backfill in this migration: --- * The hot verifier path uses `runs_pkey` on `id` regardless of which --- columns the routing keys live in; the JSONB extraction on the --- single PK-matched row is microseconds. --- * A single migrate-time backfill over a multi-million-row hot queue --- table either holds row locks for the full duration (one big --- UPDATE) or scans-with-LIMIT-cursor without an index to anchor it --- (degenerates to O(N²) work). Neither is production-safe. --- * Operators who want the columns populated for diagnostic queries --- can run the chunked-with-keyset backfill from --- `docs/runbooks/runs-backfill-denormalize.md` after the rollout. --- --- ADD COLUMN with no DEFAULT and a nullable type is metadata-only on --- PG11+: brief AccessExclusive, no table rewrite. - -SET lock_timeout = '30s'; - -ALTER TABLE public.runs - ADD COLUMN IF NOT EXISTS agent_id text, - ADD COLUMN IF NOT EXISTS conversation_id text; - --- migrate:down - -SET lock_timeout = '30s'; -ALTER TABLE public.runs - DROP COLUMN IF EXISTS conversation_id, - DROP COLUMN IF EXISTS agent_id; diff --git a/db/migrations/20260518060000_revert_runs_denormalize.sql b/db/migrations/20260518060000_revert_runs_denormalize.sql deleted file mode 100644 index 2c655aaa3..000000000 --- a/db/migrations/20260518060000_revert_runs_denormalize.sql +++ /dev/null @@ -1,29 +0,0 @@ --- migrate:up - --- Revert the speculative `runs.agent_id` + `runs.conversation_id` --- denormalization landed in PR #870 (migration --- `20260518050000_runs_denormalize_agent_conversation.sql`). --- --- The columns had zero consumers — `isRunOwnedByJwtScope` always --- COALESCE-fell back to `action_input->>'key'`, the runs-queue claim --- loop doesn't filter on them, and the reaper doesn't either. The --- original perf justification ("JSONB full-scan in the verifier") was --- wrong: the verifier query is a PK lookup (`WHERE id = $1`) and the --- JSONB extraction on the single matched row is microseconds. --- --- DROP COLUMN is metadata-only on PG11+ (brief AccessExclusive, no --- table rewrite). - -SET lock_timeout = '30s'; - -ALTER TABLE public.runs - DROP COLUMN IF EXISTS conversation_id, - DROP COLUMN IF EXISTS agent_id; - --- migrate:down - -SET lock_timeout = '30s'; - -ALTER TABLE public.runs - ADD COLUMN IF NOT EXISTS agent_id text, - ADD COLUMN IF NOT EXISTS conversation_id text; diff --git a/db/migrations/20260518070000_runs_heartbeat_inflight_widen.sql b/db/migrations/20260518070000_runs_heartbeat_inflight_widen.sql deleted file mode 100644 index bb8bffa9a..000000000 --- a/db/migrations/20260518070000_runs_heartbeat_inflight_widen.sql +++ /dev/null @@ -1,33 +0,0 @@ --- migrate:up - --- Widen the partial index `idx_runs_heartbeat_inflight` back to all four --- connector lanes (sync, action, embed_backfill, auth) now that the --- action + embed_backfill executors in --- packages/connector-worker/src/daemon/executor.ts actually emit --- `client.heartbeat()` (lobu#860). The reaper WHERE clause in --- packages/server/src/scheduled/check-stalled-executions.ts mirrors this --- set so the bulk UPDATE keeps using this partial index. --- --- History: --- 20260518010000 — created with the wide four-lane set. --- 20260518020000 — narrowed to (sync, auth) because action + --- embed_backfill weren't heartbeating; in-flight runs --- were being marked `timeout` mid-flight after 120s. --- 20260518070000 — this — wide again, paired with the executor change --- that makes those lanes heartbeat. - -DROP INDEX IF EXISTS public.idx_runs_heartbeat_inflight; - -CREATE INDEX IF NOT EXISTS idx_runs_heartbeat_inflight - ON public.runs (last_heartbeat_at) - WHERE status IN ('claimed', 'running') - AND run_type IN ('sync', 'action', 'embed_backfill', 'auth'); - --- migrate:down - -DROP INDEX IF EXISTS public.idx_runs_heartbeat_inflight; - -CREATE INDEX IF NOT EXISTS idx_runs_heartbeat_inflight - ON public.runs (last_heartbeat_at) - WHERE status IN ('claimed', 'running') - AND run_type IN ('sync', 'auth'); diff --git a/db/migrations/20260519000000_passkey_table.sql b/db/migrations/20260519000000_passkey_table.sql deleted file mode 100644 index 67fa2a253..000000000 --- a/db/migrations/20260519000000_passkey_table.sql +++ /dev/null @@ -1,32 +0,0 @@ --- migrate:up - --- Adds the `passkey` table required by @better-auth/passkey. Each row is a --- WebAuthn credential: bound to a user, identified by `credential_id` (the --- credential's external id the browser reports), with `public_key` used to --- verify subsequent authentication assertions. `counter` is the WebAuthn --- signCount used to detect cloned authenticators. --- --- Stored separately from `account` because account.providerId is unique-per- --- user-per-provider, but a user can have many passkeys (one per --- device/browser). - -CREATE TABLE IF NOT EXISTS "passkey" ( - id TEXT PRIMARY KEY, - name TEXT, - "publicKey" TEXT NOT NULL, - "userId" TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - "credentialID" TEXT NOT NULL, - counter BIGINT NOT NULL DEFAULT 0, - "deviceType" TEXT NOT NULL, - "backedUp" BOOLEAN NOT NULL DEFAULT FALSE, - transports TEXT, - "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), - aaguid TEXT -); - -CREATE INDEX IF NOT EXISTS passkey_user_id_idx ON "passkey"("userId"); -CREATE INDEX IF NOT EXISTS passkey_credential_id_idx ON "passkey"("credentialID"); - --- migrate:down - -DROP TABLE IF EXISTS "passkey"; diff --git a/db/migrations/20260519020000_chat_state_tables.sql b/db/migrations/20260519020000_chat_state_tables.sql deleted file mode 100644 index cba875a9a..000000000 --- a/db/migrations/20260519020000_chat_state_tables.sql +++ /dev/null @@ -1,82 +0,0 @@ --- migrate:up - --- Chat SDK state-adapter backing tables, previously created at runtime by --- `LobuStateAdapter.ensureSchema()` (packages/server/src/gateway/connections/ --- state-adapter.ts). Promoting them to a real migration so the canonical --- schema source (db/schema.sql via dbmate) reflects production reality and --- the CI drift gate stops failing whenever someone regenerates the snapshot --- against a server-touched DB. --- --- All five tables and four indexes are `IF NOT EXISTS` so this is a no-op --- on any deployment that has already had the gateway boot — the runtime --- ensureSchema() created the same shape with the same names and column --- types. Schema is identical to `@chat-adapter/state-pg`'s (the upstream --- library we replaced with in-house LobuStateAdapter; see the file header --- on state-adapter.ts for why). - -CREATE TABLE IF NOT EXISTS public.chat_state_subscriptions ( - key_prefix text NOT NULL, - thread_id text NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT now(), - PRIMARY KEY (key_prefix, thread_id) -); - -CREATE TABLE IF NOT EXISTS public.chat_state_locks ( - key_prefix text NOT NULL, - thread_id text NOT NULL, - token text NOT NULL, - expires_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL DEFAULT now(), - PRIMARY KEY (key_prefix, thread_id) -); - -CREATE TABLE IF NOT EXISTS public.chat_state_cache ( - key_prefix text NOT NULL, - cache_key text NOT NULL, - value text NOT NULL, - expires_at timestamp with time zone, - updated_at timestamp with time zone NOT NULL DEFAULT now(), - PRIMARY KEY (key_prefix, cache_key) -); - -CREATE TABLE IF NOT EXISTS public.chat_state_lists ( - key_prefix text NOT NULL, - list_key text NOT NULL, - seq bigserial NOT NULL, - value text NOT NULL, - expires_at timestamp with time zone, - PRIMARY KEY (key_prefix, list_key, seq) -); - -CREATE TABLE IF NOT EXISTS public.chat_state_queues ( - key_prefix text NOT NULL, - thread_id text NOT NULL, - seq bigserial NOT NULL, - value text NOT NULL, - expires_at timestamp with time zone NOT NULL, - PRIMARY KEY (key_prefix, thread_id, seq) -); - -CREATE INDEX IF NOT EXISTS chat_state_locks_expires_idx - ON public.chat_state_locks (expires_at); - -CREATE INDEX IF NOT EXISTS chat_state_cache_expires_idx - ON public.chat_state_cache (expires_at); - -CREATE INDEX IF NOT EXISTS chat_state_lists_expires_idx - ON public.chat_state_lists (expires_at); - -CREATE INDEX IF NOT EXISTS chat_state_queues_expires_idx - ON public.chat_state_queues (expires_at); - --- migrate:down - -DROP INDEX IF EXISTS public.chat_state_queues_expires_idx; -DROP INDEX IF EXISTS public.chat_state_lists_expires_idx; -DROP INDEX IF EXISTS public.chat_state_cache_expires_idx; -DROP INDEX IF EXISTS public.chat_state_locks_expires_idx; -DROP TABLE IF EXISTS public.chat_state_queues; -DROP TABLE IF EXISTS public.chat_state_lists; -DROP TABLE IF EXISTS public.chat_state_cache; -DROP TABLE IF EXISTS public.chat_state_locks; -DROP TABLE IF EXISTS public.chat_state_subscriptions; diff --git a/db/migrations/20260519020001_revoked_tokens.sql b/db/migrations/20260519020001_revoked_tokens.sql deleted file mode 100644 index 76165a6f8..000000000 --- a/db/migrations/20260519020001_revoked_tokens.sql +++ /dev/null @@ -1,29 +0,0 @@ --- migrate:up - --- Token revocation kill-switch table, previously created at runtime by --- `RevokedTokenStore.ensureSchema()` (packages/server/src/gateway/auth/ --- revoked-token-store.ts) and again separately by the `lobu token revoke` --- CLI command (packages/cli/src/commands/token.ts). Two runtime sites --- meant two copies of the schema definition; one drifting from the other --- was waiting to happen. --- --- Promoting to a real migration so the schema source (db/schema.sql) --- reflects production reality. Both runtime CREATE blocks are removed in --- the same change set. --- --- `IF NOT EXISTS` so this is a no-op on any deployment where the gateway --- or CLI already created the table at runtime — same name, same column --- types. - -CREATE TABLE IF NOT EXISTS public.revoked_tokens ( - jti text PRIMARY KEY, - expires_at timestamp with time zone NOT NULL -); - -CREATE INDEX IF NOT EXISTS revoked_tokens_expires_at_idx - ON public.revoked_tokens (expires_at); - --- migrate:down - -DROP INDEX IF EXISTS public.revoked_tokens_expires_at_idx; -DROP TABLE IF EXISTS public.revoked_tokens; diff --git a/db/schema.sql b/db/schema.sql deleted file mode 100644 index a8d8a3ccf..000000000 --- a/db/schema.sql +++ /dev/null @@ -1,5392 +0,0 @@ -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - --- --- Name: SCHEMA public; Type: COMMENT; Schema: -; Owner: - --- - -COMMENT ON SCHEMA public IS ''; - --- --- Name: pg_trgm; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public; - --- --- Name: EXTENSION pg_trgm; Type: COMMENT; Schema: -; Owner: - --- - -COMMENT ON EXTENSION pg_trgm IS 'text similarity measurement and index searching based on trigrams'; - --- --- Name: vector; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA public; - --- --- Name: EXTENSION vector; Type: COMMENT; Schema: -; Owner: - --- - -COMMENT ON EXTENSION vector IS 'vector data type and ivfflat and hnsw access methods'; - --- --- Name: prevent_entity_cycles(); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.prevent_entity_cycles() RETURNS trigger - LANGUAGE plpgsql - AS $$ -BEGIN - -- Skip if no parent (top-level entity) - IF NEW.parent_id IS NULL THEN - RETURN NEW; - END IF; - - -- Check for circular reference using recursive CTE (single query) - IF EXISTS ( - WITH RECURSIVE ancestors AS ( - -- Start with the new parent - SELECT parent_id, 1 as depth - FROM entities - WHERE id = NEW.parent_id - - UNION ALL - - -- Recursively walk up the tree - SELECT e.parent_id, a.depth + 1 - FROM entities e - INNER JOIN ancestors a ON e.id = a.parent_id - WHERE a.depth < 10 -- Prevent infinite loops - ) - SELECT 1 - FROM ancestors - WHERE parent_id = NEW.id -- Would create a cycle - OR depth >= 10 -- Too deep - ) THEN - RAISE EXCEPTION 'Circular reference detected or hierarchy too deep (max 10 levels)'; - END IF; - - RETURN NEW; -END; -$$; - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: account; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.account ( - id text NOT NULL, - "accountId" text NOT NULL, - "providerId" text NOT NULL, - "userId" text NOT NULL, - "accessToken" text, - "refreshToken" text, - "idToken" text, - "accessTokenExpiresAt" timestamp with time zone, - "refreshTokenExpiresAt" timestamp with time zone, - scope text, - password text, - "createdAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - "updatedAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL -); - --- --- Name: agent_channel_bindings; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.agent_channel_bindings ( - agent_id text NOT NULL, - platform text NOT NULL, - channel_id text NOT NULL, - team_id text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - organization_id text NOT NULL -); - --- --- Name: agent_connections; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.agent_connections ( - id text NOT NULL, - agent_id text NOT NULL, - platform text NOT NULL, - config jsonb DEFAULT '{}'::jsonb NOT NULL, - settings jsonb DEFAULT '{}'::jsonb NOT NULL, - metadata jsonb DEFAULT '{}'::jsonb NOT NULL, - status text DEFAULT 'active'::text NOT NULL, - error_message text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - organization_id text NOT NULL, - CONSTRAINT agent_connections_status_check CHECK ((status = ANY (ARRAY['active'::text, 'stopped'::text, 'error'::text]))) -); - --- --- Name: agent_grants; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.agent_grants ( - id bigint NOT NULL, - agent_id text NOT NULL, - pattern text NOT NULL, - expires_at timestamp with time zone, - granted_at timestamp with time zone DEFAULT now() NOT NULL, - denied boolean DEFAULT false, - organization_id text NOT NULL -); - --- --- Name: agent_grants_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -ALTER TABLE public.agent_grants ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY ( - SEQUENCE NAME public.agent_grants_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - --- --- Name: agent_secrets; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.agent_secrets ( - name text NOT NULL, - ciphertext text NOT NULL, - expires_at timestamp with time zone, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - organization_id text DEFAULT ''::text NOT NULL -); - --- --- Name: TABLE agent_secrets; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON TABLE public.agent_secrets IS 'Encrypted secret values referenced via secret:// refs. Backs the PostgresSecretStore implementation of @lobu/gateway WritableSecretStore.'; - --- --- Name: agent_transcript_snapshot; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.agent_transcript_snapshot ( - id bigint NOT NULL, - organization_id text NOT NULL, - agent_id text NOT NULL, - conversation_id text NOT NULL, - run_id bigint NOT NULL, - snapshot_jsonl text NOT NULL, - byte_size integer NOT NULL, - terminal_status text NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT agent_transcript_snapshot_terminal_status_check CHECK ((terminal_status = ANY (ARRAY['completed'::text, 'failed'::text, 'timeout'::text, 'cancelled'::text]))) -); - --- --- Name: agent_transcript_snapshot_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.agent_transcript_snapshot_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: agent_transcript_snapshot_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.agent_transcript_snapshot_id_seq OWNED BY public.agent_transcript_snapshot.id; - --- --- Name: agent_users; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.agent_users ( - agent_id text NOT NULL, - platform text NOT NULL, - user_id text NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - organization_id text NOT NULL -); - --- --- Name: agents; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.agents ( - id text NOT NULL, - organization_id text NOT NULL, - name text DEFAULT 'Agent'::text NOT NULL, - description text, - owner_platform text, - owner_user_id text, - is_workspace_agent boolean DEFAULT false, - workspace_id text, - model text, - model_selection jsonb DEFAULT '{}'::jsonb, - provider_model_preferences jsonb DEFAULT '{}'::jsonb, - network_config jsonb DEFAULT '{}'::jsonb, - nix_config jsonb DEFAULT '{}'::jsonb, - mcp_servers jsonb DEFAULT '{}'::jsonb, - agent_integrations jsonb DEFAULT '{}'::jsonb, - soul_md text DEFAULT ''::text, - user_md text DEFAULT ''::text, - identity_md text DEFAULT ''::text, - skills_config jsonb DEFAULT '{"skills": []}'::jsonb, - skill_auto_granted_domains jsonb DEFAULT '[]'::jsonb, - tools_config jsonb DEFAULT '{}'::jsonb, - plugins_config jsonb DEFAULT '{}'::jsonb, - installed_providers jsonb DEFAULT '[]'::jsonb, - skill_registries jsonb DEFAULT '[]'::jsonb, - verbose_logging boolean DEFAULT false, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - last_used_at timestamp with time zone, - egress_config jsonb DEFAULT '{}'::jsonb, - pre_approved_tools jsonb DEFAULT '[]'::jsonb, - guardrails jsonb DEFAULT '[]'::jsonb -); - --- --- Name: auth_profiles; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.auth_profiles ( - id bigint NOT NULL, - organization_id text NOT NULL, - slug text NOT NULL, - display_name text NOT NULL, - connector_key text, - profile_kind text NOT NULL, - status text DEFAULT 'active'::text NOT NULL, - auth_data jsonb DEFAULT '{}'::jsonb NOT NULL, - account_id text, - provider text, - created_by text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - metadata jsonb DEFAULT '{}'::jsonb NOT NULL, - device_worker_id uuid, - browser_kind text, - user_data_dir text, - cdp_url text, - is_default_for_connector boolean DEFAULT false NOT NULL, - CONSTRAINT auth_profiles_browser_kind_check CHECK (((browser_kind IS NULL) OR (browser_kind = ANY (ARRAY['chrome'::text, 'brave'::text, 'arc'::text, 'edge'::text])))), - CONSTRAINT auth_profiles_connector_key_required CHECK (((connector_key IS NOT NULL) OR (profile_kind = 'browser_session'::text))), - CONSTRAINT auth_profiles_device_browser_path_mutex CHECK (((device_worker_id IS NULL) OR (profile_kind <> 'browser_session'::text) OR (user_data_dir IS NULL) OR (cdp_url IS NULL))), - CONSTRAINT auth_profiles_profile_kind_check CHECK ((profile_kind = ANY (ARRAY['env'::text, 'oauth_app'::text, 'oauth_account'::text, 'browser_session'::text, 'interactive'::text]))), - CONSTRAINT auth_profiles_status_check CHECK ((status = ANY (ARRAY['active'::text, 'pending_auth'::text, 'error'::text, 'revoked'::text]))) -); - --- --- Name: auth_profiles_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -ALTER TABLE public.auth_profiles ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY ( - SEQUENCE NAME public.auth_profiles_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - --- --- Name: chat_state_cache; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.chat_state_cache ( - key_prefix text NOT NULL, - cache_key text NOT NULL, - value text NOT NULL, - expires_at timestamp with time zone, - updated_at timestamp with time zone DEFAULT now() NOT NULL -); - --- --- Name: chat_state_lists; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.chat_state_lists ( - key_prefix text NOT NULL, - list_key text NOT NULL, - seq bigint NOT NULL, - value text NOT NULL, - expires_at timestamp with time zone -); - --- --- Name: chat_state_lists_seq_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.chat_state_lists_seq_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: chat_state_lists_seq_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.chat_state_lists_seq_seq OWNED BY public.chat_state_lists.seq; - --- --- Name: chat_state_locks; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.chat_state_locks ( - key_prefix text NOT NULL, - thread_id text NOT NULL, - token text NOT NULL, - expires_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL -); - --- --- Name: chat_state_queues; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.chat_state_queues ( - key_prefix text NOT NULL, - thread_id text NOT NULL, - seq bigint NOT NULL, - value text NOT NULL, - expires_at timestamp with time zone NOT NULL -); - --- --- Name: chat_state_queues_seq_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.chat_state_queues_seq_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: chat_state_queues_seq_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.chat_state_queues_seq_seq OWNED BY public.chat_state_queues.seq; - --- --- Name: chat_state_subscriptions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.chat_state_subscriptions ( - key_prefix text NOT NULL, - thread_id text NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL -); - --- --- Name: chat_user_identities; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.chat_user_identities ( - platform text NOT NULL, - team_id text DEFAULT ''::text NOT NULL, - platform_user_id text NOT NULL, - lobu_user_id text NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL -); - --- --- Name: connect_tokens; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.connect_tokens ( - id bigint NOT NULL, - token text NOT NULL, - connection_id bigint, - organization_id text NOT NULL, - connector_key text NOT NULL, - auth_type text NOT NULL, - auth_config jsonb, - status text DEFAULT 'pending'::text NOT NULL, - created_by text, - expires_at timestamp with time zone DEFAULT (now() + '01:00:00'::interval) NOT NULL, - completed_at timestamp with time zone, - created_at timestamp with time zone DEFAULT now() NOT NULL, - auth_profile_id bigint, - CONSTRAINT connect_tokens_auth_type_check CHECK ((auth_type = ANY (ARRAY['oauth'::text, 'env_keys'::text]))), - CONSTRAINT connect_tokens_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'completed'::text, 'expired'::text]))) -); - --- --- Name: connect_tokens_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.connect_tokens_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: connect_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.connect_tokens_id_seq OWNED BY public.connect_tokens.id; - --- --- Name: connections; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.connections ( - id bigint NOT NULL, - organization_id text NOT NULL, - connector_key text NOT NULL, - display_name text, - status text DEFAULT 'active'::text NOT NULL, - account_id text, - credentials jsonb, - entity_ids bigint[], - config jsonb, - error_message text, - created_by text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - auth_profile_id bigint, - app_auth_profile_id bigint, - visibility text DEFAULT 'org'::text NOT NULL, - deleted_at timestamp with time zone, - agent_id text, - device_worker_id uuid, - slug text NOT NULL, - CONSTRAINT connections_status_check CHECK ((status = ANY (ARRAY['active'::text, 'paused'::text, 'error'::text, 'revoked'::text, 'pending_auth'::text]))) -); - --- --- Name: connections_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.connections_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: connections_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.connections_id_seq OWNED BY public.connections.id; - --- --- Name: connector_definitions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.connector_definitions ( - id integer NOT NULL, - organization_id text, - key text NOT NULL, - name text NOT NULL, - description text, - version text DEFAULT '1.0.0'::text NOT NULL, - auth_schema jsonb, - feeds_schema jsonb, - actions_schema jsonb, - options_schema jsonb, - status text DEFAULT 'active'::text NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - login_enabled boolean DEFAULT false NOT NULL, - mcp_config jsonb, - api_config jsonb, - api_type text DEFAULT 'api'::text NOT NULL, - favicon_domain text, - openapi_config jsonb, - default_connection_config jsonb, - entity_link_overrides jsonb, - default_repair_agent_id text, - required_capability text, - runtime jsonb, - CONSTRAINT connector_definitions_status_check CHECK ((status = ANY (ARRAY['active'::text, 'archived'::text, 'draft'::text]))) -); - --- --- Name: COLUMN connector_definitions.entity_link_overrides; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.connector_definitions.entity_link_overrides IS 'Per-install override of connector entityLinks rules. See resolveEntityLinkRules() for merge semantics.'; - --- --- Name: connector_definitions_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.connector_definitions_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: connector_definitions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.connector_definitions_id_seq OWNED BY public.connector_definitions.id; - --- --- Name: connector_versions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.connector_versions ( - id integer NOT NULL, - connector_key text NOT NULL, - version text NOT NULL, - compiled_code text, - compiled_code_hash text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - source_code text, - source_path text -); - --- --- Name: connector_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.connector_versions_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: connector_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.connector_versions_id_seq OWNED BY public.connector_versions.id; - --- --- Name: events; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.events ( - id bigint NOT NULL, - organization_id text NOT NULL, - entity_ids bigint[], - origin_id text, - title text, - payload_type text DEFAULT 'text'::text NOT NULL, - payload_text text, - payload_data jsonb DEFAULT '{}'::jsonb NOT NULL, - payload_template jsonb, - attachments jsonb DEFAULT '[]'::jsonb NOT NULL, - metadata jsonb DEFAULT '{}'::jsonb NOT NULL, - score numeric(10,2) DEFAULT 0, - author_name text, - source_url text, - occurred_at timestamp with time zone, - created_at timestamp with time zone DEFAULT now() NOT NULL, - origin_parent_id text, - origin_type text, - connector_key text, - connection_id bigint, - feed_key text, - feed_id bigint, - run_id bigint, - semantic_type text DEFAULT 'content'::text NOT NULL, - client_id text, - created_by text, - interaction_type text DEFAULT 'none'::text NOT NULL, - interaction_status text, - interaction_input_schema jsonb, - interaction_input jsonb, - interaction_output jsonb, - interaction_error text, - supersedes_event_id bigint, - content_length integer GENERATED ALWAYS AS (COALESCE(length(payload_text), 0)) STORED, - search_tsv tsvector GENERATED ALWAYS AS ((setweight(to_tsvector('english'::regconfig, COALESCE(title, ''::text)), 'A'::"char") || setweight(to_tsvector('english'::regconfig, COALESCE(payload_text, ''::text)), 'B'::"char"))) STORED, - CONSTRAINT events_interaction_status_check CHECK ((interaction_status = ANY (ARRAY['pending'::text, 'approved'::text, 'rejected'::text, 'completed'::text, 'failed'::text]))), - CONSTRAINT events_interaction_type_check CHECK ((interaction_type = ANY (ARRAY['none'::text, 'approval'::text]))), - CONSTRAINT events_payload_type_check CHECK ((payload_type = ANY (ARRAY['text'::text, 'markdown'::text, 'json_template'::text, 'media'::text, 'empty'::text]))), - CONSTRAINT events_semantic_type_not_empty CHECK ((length(btrim(semantic_type)) > 0)) -); - --- --- Name: TABLE events; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON TABLE public.events IS 'Append-only event log for source ingests, user-authored knowledge, and operation history'; - --- --- Name: COLUMN events.id; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.events.id IS 'Primary key (BIGSERIAL) - exposed to MCP tools'; - --- --- Name: COLUMN events.score; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.events.score IS 'Normalized 0-100 score for ranking'; - --- --- Name: COLUMN events.origin_type; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.events.origin_type IS 'Source-native item type (post, comment, review, issue, etc.)'; - --- --- Name: COLUMN events.run_id; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.events.run_id IS 'Links the event to the run that produced or acted on it'; - --- --- Name: content_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.content_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: content_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.content_id_seq OWNED BY public.events.id; - --- --- Name: event_embeddings; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.event_embeddings ( - event_id bigint NOT NULL, - embedding public.vector(768) NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL -); - --- --- Name: current_event_records; Type: VIEW; Schema: public; Owner: - --- - -CREATE VIEW public.current_event_records AS - SELECT e.id, - e.organization_id, - e.entity_ids, - e.origin_id, - e.title, - e.payload_type, - e.payload_text, - e.payload_data, - e.payload_template, - e.attachments, - e.metadata, - e.score, - emb.embedding, - e.author_name, - e.source_url, - e.occurred_at, - e.created_at, - e.origin_parent_id, - COALESCE(length(e.payload_text), 0) AS content_length, - e.search_tsv, - e.origin_type, - e.connector_key, - e.connection_id, - e.feed_key, - e.feed_id, - e.run_id, - e.semantic_type, - e.client_id, - e.created_by, - e.interaction_type, - e.interaction_status, - e.interaction_input_schema, - e.interaction_input, - e.interaction_output, - e.interaction_error, - e.supersedes_event_id - FROM (public.events e - LEFT JOIN public.event_embeddings emb ON ((emb.event_id = e.id))) - WHERE (NOT (EXISTS ( SELECT 1 - FROM public.events newer - WHERE (newer.supersedes_event_id = e.id)))); - --- --- Name: device_workers; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.device_workers ( - user_id text NOT NULL, - worker_id text NOT NULL, - platform text, - app_version text, - capabilities jsonb DEFAULT '[]'::jsonb NOT NULL, - label text, - first_seen_at timestamp with time zone DEFAULT now() NOT NULL, - last_seen_at timestamp with time zone DEFAULT now() NOT NULL, - id uuid DEFAULT gen_random_uuid() NOT NULL, - organization_id text, - notification_budget_per_day integer DEFAULT 10 NOT NULL, - CONSTRAINT device_workers_notification_budget_per_day_nonneg CHECK ((notification_budget_per_day >= 0)) -); - --- --- Name: entities; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.entities ( - id bigint NOT NULL, - parent_id bigint, - name text NOT NULL, - metadata jsonb DEFAULT '{}'::jsonb, - enabled_classifiers text[], - created_at timestamp with time zone DEFAULT now(), - updated_at timestamp with time zone DEFAULT now(), - organization_id text NOT NULL, - created_by text NOT NULL, - slug text NOT NULL, - current_view_template_version_id integer, - content text, - embedding public.vector(768), - content_tsv tsvector GENERATED ALWAYS AS (to_tsvector('english'::regconfig, ((COALESCE(name, ''::text) || ' '::text) || COALESCE(content, ''::text)))) STORED, - content_hash text, - deleted_at timestamp with time zone, - entity_type_id integer NOT NULL -); - --- --- Name: TABLE entities; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON TABLE public.entities IS 'Unified entity table (brands, products, and future entity types)'; - --- --- Name: COLUMN entities.parent_id; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.entities.parent_id IS 'Hierarchical parent (products → brands, brands → parent brands)'; - --- --- Name: COLUMN entities.enabled_classifiers; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.entities.enabled_classifiers IS 'Classifiers enabled for this entity (inherited from parent if NULL)'; - --- --- Name: entities_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.entities_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: entities_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.entities_id_seq OWNED BY public.entities.id; - --- --- Name: entity_identities; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.entity_identities ( - id bigint NOT NULL, - organization_id text NOT NULL, - entity_id bigint NOT NULL, - namespace text NOT NULL, - identifier text NOT NULL, - source_connector text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - deleted_at timestamp with time zone -); - --- --- Name: TABLE entity_identities; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON TABLE public.entity_identities IS 'Normalized identifier claims per entity. See docs/identity-linking.md for the full pattern.'; - --- --- Name: COLUMN entity_identities.namespace; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.entity_identities.namespace IS 'Identifier kind. Standard values: phone, email, wa_jid, slack_user_id, github_login, auth_user_id, google_contact_id. Custom namespaces allowed but connectors sharing a namespace must agree on its format.'; - --- --- Name: COLUMN entity_identities.identifier; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.entity_identities.identifier IS 'Normalized identifier value (E.164 digits for phone, lowercase for email, etc.). Normalizers in @lobu/connector-sdk own the canonical form.'; - --- --- Name: COLUMN entity_identities.source_connector; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.entity_identities.source_connector IS 'Who claimed this identifier: "connector:whatsapp", "manual", or null when seeded by migration.'; - --- --- Name: entity_identities_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.entity_identities_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: entity_identities_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.entity_identities_id_seq OWNED BY public.entity_identities.id; - --- --- Name: entity_relationship_type_rules; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.entity_relationship_type_rules ( - id integer NOT NULL, - relationship_type_id integer NOT NULL, - source_entity_type_slug text NOT NULL, - target_entity_type_slug text NOT NULL, - deleted_at timestamp with time zone, - created_at timestamp with time zone DEFAULT now(), - updated_at timestamp with time zone DEFAULT now() -); - --- --- Name: entity_relationship_type_rules_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.entity_relationship_type_rules_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: entity_relationship_type_rules_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.entity_relationship_type_rules_id_seq OWNED BY public.entity_relationship_type_rules.id; - --- --- Name: entity_relationship_types; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.entity_relationship_types ( - id integer NOT NULL, - slug text NOT NULL, - name text NOT NULL, - description text, - organization_id text NOT NULL, - created_by text, - metadata_schema jsonb, - is_symmetric boolean DEFAULT false NOT NULL, - inverse_type_id integer, - status text DEFAULT 'active'::text NOT NULL, - deleted_at timestamp with time zone, - created_at timestamp with time zone DEFAULT now(), - updated_at timestamp with time zone DEFAULT now(), - metadata jsonb, - CONSTRAINT entity_relationship_types_status_check CHECK ((status = ANY (ARRAY['active'::text, 'archived'::text]))) -); - --- --- Name: entity_relationship_types_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.entity_relationship_types_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: entity_relationship_types_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.entity_relationship_types_id_seq OWNED BY public.entity_relationship_types.id; - --- --- Name: entity_relationships; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.entity_relationships ( - id integer NOT NULL, - organization_id text NOT NULL, - from_entity_id bigint NOT NULL, - to_entity_id bigint NOT NULL, - relationship_type_id integer NOT NULL, - metadata jsonb, - confidence real, - source text, - created_by text, - updated_by text, - deleted_at timestamp with time zone, - created_at timestamp with time zone DEFAULT now(), - updated_at timestamp with time zone DEFAULT now() -); - --- --- Name: entity_relationships_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.entity_relationships_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: entity_relationships_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.entity_relationships_id_seq OWNED BY public.entity_relationships.id; - --- --- Name: entity_type_audit; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.entity_type_audit ( - id integer NOT NULL, - entity_type_id integer NOT NULL, - action text NOT NULL, - actor text, - before_payload jsonb, - after_payload jsonb, - created_at timestamp with time zone DEFAULT now(), - CONSTRAINT entity_type_audit_action_check CHECK ((action = ANY (ARRAY['create'::text, 'update'::text, 'delete'::text]))) -); - --- --- Name: entity_type_audit_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.entity_type_audit_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: entity_type_audit_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.entity_type_audit_id_seq OWNED BY public.entity_type_audit.id; - --- --- Name: entity_types; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.entity_types ( - id integer NOT NULL, - slug text NOT NULL, - name text NOT NULL, - description text, - icon text, - color text, - metadata_schema jsonb, - organization_id text NOT NULL, - created_by text, - updated_by text, - deleted_at timestamp with time zone, - created_at timestamp with time zone DEFAULT now(), - updated_at timestamp with time zone DEFAULT now(), - current_view_template_version_id integer, - event_kinds jsonb -); - --- --- Name: entity_types_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.entity_types_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: entity_types_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.entity_types_id_seq OWNED BY public.entity_types.id; - --- --- Name: event_classifications; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.event_classifications ( - id bigint NOT NULL, - event_id bigint NOT NULL, - classifier_version_id bigint NOT NULL, - watcher_id bigint, - window_id bigint, - "values" text[] NOT NULL, - confidences jsonb DEFAULT '{}'::jsonb NOT NULL, - source character varying(20) NOT NULL, - is_manual boolean DEFAULT false NOT NULL, - reasoning text, - met_threshold boolean, - threshold numeric(5,4), - best_match_attribute text, - embedding_confidence numeric(6,4), - created_at timestamp with time zone DEFAULT now(), - excerpts jsonb DEFAULT '{}'::jsonb NOT NULL, - CONSTRAINT event_classifications_source_check CHECK (((source)::text = ANY (ARRAY[('embedding'::character varying)::text, ('llm'::character varying)::text, ('user'::character varying)::text]))), - CONSTRAINT event_classifications_values_not_empty CHECK ((cardinality("values") > 0)) -); - --- --- Name: event_classifications_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.event_classifications_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: event_classifications_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.event_classifications_id_seq OWNED BY public.event_classifications.id; - --- --- Name: event_classifier_versions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.event_classifier_versions ( - id integer NOT NULL, - classifier_id integer NOT NULL, - version integer NOT NULL, - is_current boolean DEFAULT false, - attribute_values jsonb NOT NULL, - min_similarity numeric(5,4) DEFAULT 0.7, - fallback_value text, - change_notes text, - created_by text NOT NULL, - created_at timestamp with time zone DEFAULT now(), - preferred_model text DEFAULT '@cf/meta/llama-3.1-8b-instruct'::text, - extraction_config jsonb -); - --- --- Name: COLUMN event_classifier_versions.preferred_model; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.event_classifier_versions.preferred_model IS 'AI model to use for LLM fallback. Use 8B for simple classifiers (cheap), 70B for complex reasoning (expensive). Default: 8B'; - --- --- Name: event_classifier_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.event_classifier_versions_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: event_classifier_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.event_classifier_versions_id_seq OWNED BY public.event_classifier_versions.id; - --- --- Name: event_classifiers; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.event_classifiers ( - id integer NOT NULL, - slug text NOT NULL, - name text NOT NULL, - description text, - attribute_key text NOT NULL, - status text DEFAULT 'active'::text NOT NULL, - created_at timestamp with time zone DEFAULT now(), - updated_at timestamp with time zone DEFAULT now(), - created_by text NOT NULL, - entity_id bigint, - watcher_id bigint, - organization_id text NOT NULL, - entity_ids bigint[], - CONSTRAINT event_classifiers_status_check CHECK ((status = ANY (ARRAY['active'::text, 'deprecated'::text]))) -); - --- --- Name: event_classifiers_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.event_classifiers_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: event_classifiers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.event_classifiers_id_seq OWNED BY public.event_classifiers.id; - --- --- Name: feeds; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.feeds ( - id bigint NOT NULL, - organization_id text NOT NULL, - connection_id bigint NOT NULL, - feed_key text NOT NULL, - display_name text, - status text DEFAULT 'active'::text NOT NULL, - entity_ids bigint[], - config jsonb, - checkpoint jsonb, - last_sync_at timestamp with time zone, - last_sync_status text, - last_error text, - consecutive_failures integer DEFAULT 0 NOT NULL, - items_collected bigint DEFAULT 0 NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - pinned_version text, - deleted_at timestamp with time zone, - schedule text, - next_run_at timestamp with time zone, - repair_agent_id text, - repair_thread_id text, - repair_attempt_count integer DEFAULT 0 NOT NULL, - last_repair_at timestamp with time zone, - first_failure_at timestamp with time zone, - last_repair_post_hash text, - CONSTRAINT feeds_status_check CHECK ((status = ANY (ARRAY['active'::text, 'paused'::text, 'error'::text]))) -); - --- --- Name: feeds_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.feeds_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: feeds_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.feeds_id_seq OWNED BY public.feeds.id; - --- --- Name: grants; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.grants ( - agent_id text NOT NULL, - kind text NOT NULL, - pattern text NOT NULL, - expires_at timestamp with time zone, - granted_at timestamp with time zone DEFAULT now() NOT NULL, - denied boolean DEFAULT false NOT NULL, - organization_id text NOT NULL -); - --- --- Name: invitation; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.invitation ( - id text NOT NULL, - "organizationId" text NOT NULL, - email text NOT NULL, - role text DEFAULT 'member'::text, - status text DEFAULT 'pending'::text NOT NULL, - "expiresAt" timestamp with time zone NOT NULL, - "createdAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - "inviterId" text -); - --- --- Name: latest_event_classifications; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.latest_event_classifications ( - event_id bigint NOT NULL, - classifier_id bigint NOT NULL, - id bigint NOT NULL, - classifier_version_id bigint NOT NULL, - watcher_id bigint, - window_id bigint, - "values" text[] NOT NULL, - confidences jsonb DEFAULT '{}'::jsonb NOT NULL, - source character varying(20) NOT NULL, - is_manual boolean DEFAULT false NOT NULL, - reasoning text, - created_at timestamp with time zone NOT NULL -); - --- --- Name: mcp_proxy_sessions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.mcp_proxy_sessions ( - session_key text NOT NULL, - upstream_session_id text NOT NULL, - expires_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL -); - --- --- Name: mcp_sessions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.mcp_sessions ( - session_id text NOT NULL, - user_id text, - client_id text, - organization_id text, - member_role text, - requested_agent_id text, - is_authenticated boolean DEFAULT false NOT NULL, - scoped_to_org boolean DEFAULT false NOT NULL, - last_accessed_at timestamp with time zone DEFAULT now() NOT NULL, - expires_at timestamp with time zone NOT NULL -); - --- --- Name: TABLE mcp_sessions; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON TABLE public.mcp_sessions IS 'Persisted MCP streamable HTTP sessions for restart and cross-replica recovery'; - --- --- Name: member; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.member ( - id text NOT NULL, - "organizationId" text NOT NULL, - "userId" text NOT NULL, - role text DEFAULT 'member'::text NOT NULL, - "createdAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL -); - --- --- Name: migration_20260315300000_entity_type_org_backfill; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.migration_20260315300000_entity_type_org_backfill ( - entity_type_id integer NOT NULL -); - --- --- Name: migration_20260316100000_created_entity_types; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.migration_20260316100000_created_entity_types ( - entity_type_id integer NOT NULL -); - --- --- Name: migration_20260316100000_deleted_default_entity_types; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.migration_20260316100000_deleted_default_entity_types ( - id integer NOT NULL, - slug text NOT NULL, - name text NOT NULL, - description text, - icon text, - color text, - metadata_schema jsonb, - organization_id text NOT NULL, - created_by text, - updated_by text, - deleted_at timestamp with time zone, - created_at timestamp with time zone, - updated_at timestamp with time zone, - current_view_template_version_id integer -); - --- --- Name: migration_20260316100000_events_kind_backup; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.migration_20260316100000_events_kind_backup ( - event_id bigint NOT NULL, - old_kind text -); - --- --- Name: namespace; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.namespace ( - slug text NOT NULL, - type text NOT NULL, - ref_id text NOT NULL, - created_at timestamp with time zone DEFAULT now(), - CONSTRAINT namespace_type_check CHECK ((type = ANY (ARRAY['user'::text, 'organization'::text]))) -); - --- --- Name: notification_targets; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.notification_targets ( - event_id bigint NOT NULL, - user_id text NOT NULL, - delivered_at timestamp with time zone DEFAULT now() NOT NULL, - read_at timestamp with time zone -); - --- --- Name: oauth_authorization_codes; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.oauth_authorization_codes ( - code text NOT NULL, - client_id text NOT NULL, - user_id text NOT NULL, - organization_id text, - code_challenge text NOT NULL, - code_challenge_method text DEFAULT 'S256'::text NOT NULL, - redirect_uri text NOT NULL, - scope text, - state text, - resource text, - expires_at timestamp with time zone NOT NULL, - used_at timestamp with time zone, - created_at timestamp with time zone DEFAULT now() NOT NULL -); - --- --- Name: TABLE oauth_authorization_codes; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON TABLE public.oauth_authorization_codes IS 'Short-lived authorization codes for PKCE flow'; - --- --- Name: oauth_clients; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.oauth_clients ( - id text NOT NULL, - client_secret text, - client_secret_expires_at timestamp with time zone, - client_id_issued_at timestamp with time zone DEFAULT now() NOT NULL, - redirect_uris text[] NOT NULL, - token_endpoint_auth_method text DEFAULT 'none'::text, - grant_types text[] DEFAULT ARRAY['authorization_code'::text, 'refresh_token'::text], - response_types text[] DEFAULT ARRAY['code'::text], - client_name text, - client_uri text, - logo_uri text, - scope text, - contacts text[], - tos_uri text, - policy_uri text, - software_id text, - software_version text, - user_id text, - organization_id text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - metadata jsonb DEFAULT '{}'::jsonb NOT NULL -); - --- --- Name: TABLE oauth_clients; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON TABLE public.oauth_clients IS 'OAuth 2.1 dynamic client registration for MCP clients'; - --- --- Name: oauth_device_codes; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.oauth_device_codes ( - device_code text NOT NULL, - user_code text NOT NULL, - client_id text NOT NULL, - scope text, - resource text, - user_id text, - organization_id text, - status text DEFAULT 'pending'::text NOT NULL, - poll_interval integer DEFAULT 5 NOT NULL, - expires_at timestamp with time zone NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT oauth_device_codes_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'approved'::text, 'denied'::text, 'expired'::text]))) -); - --- --- Name: TABLE oauth_device_codes; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON TABLE public.oauth_device_codes IS 'Device codes for OAuth Device Authorization Grant (RFC 8628)'; - --- --- Name: oauth_states; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.oauth_states ( - id text NOT NULL, - scope text NOT NULL, - payload jsonb NOT NULL, - expires_at timestamp with time zone NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL -); - --- --- Name: oauth_tokens; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.oauth_tokens ( - id text NOT NULL, - token_type text NOT NULL, - token_hash text NOT NULL, - client_id text NOT NULL, - user_id text NOT NULL, - organization_id text, - scope text, - resource text, - parent_token_id text, - expires_at timestamp with time zone NOT NULL, - revoked_at timestamp with time zone, - created_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT oauth_tokens_token_type_check CHECK ((token_type = ANY (ARRAY['access'::text, 'refresh'::text]))) -); - --- --- Name: TABLE oauth_tokens; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON TABLE public.oauth_tokens IS 'Access and refresh tokens issued by OAuth server'; - --- --- Name: organization; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.organization ( - id text NOT NULL, - name text NOT NULL, - slug text NOT NULL, - logo text, - "createdAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - metadata text, - description text, - visibility text DEFAULT 'private'::text NOT NULL, - repair_agents_enabled boolean DEFAULT true NOT NULL, - CONSTRAINT org_slug_not_reserved CHECK ((slug <> ALL (ARRAY['settings'::text, 'auth'::text, 'api'::text, 'templates'::text, 'help'::text, 'account'::text, 'admin'::text, 'health'::text, 'login'::text, 'logout'::text, 'signup'::text, 'register'::text, 'www'::text, 'mcp'::text, 'static'::text, 'assets'::text, 'cdn'::text, 'docs'::text, 'mail'::text]))) -); - --- --- Name: organization_lobu_links; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.organization_lobu_links ( - organization_id text NOT NULL, - lobu_url text NOT NULL, - created_by text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL -); - --- --- Name: passkey; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.passkey ( - id text NOT NULL, - name text, - "publicKey" text NOT NULL, - "userId" text NOT NULL, - "credentialID" text NOT NULL, - counter bigint DEFAULT 0 NOT NULL, - "deviceType" text NOT NULL, - "backedUp" boolean DEFAULT false NOT NULL, - transports text, - "createdAt" timestamp with time zone DEFAULT now() NOT NULL, - aaguid text -); - --- --- Name: pending_interactions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.pending_interactions ( - id text NOT NULL, - organization_id text NOT NULL, - connection_id text NOT NULL, - expected_user_id text NOT NULL, - entry_payload jsonb NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - claimed_at timestamp with time zone -); - --- --- Name: personal_access_tokens; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.personal_access_tokens ( - id bigint NOT NULL, - token_hash text NOT NULL, - token_prefix character varying(16) NOT NULL, - user_id text NOT NULL, - organization_id text, - name text NOT NULL, - description text, - scope text, - expires_at timestamp with time zone, - last_used_at timestamp with time zone, - revoked_at timestamp with time zone, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - worker_id text -); - --- --- Name: TABLE personal_access_tokens; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON TABLE public.personal_access_tokens IS 'Personal Access Tokens for workers, CLI tools, and MCP clients'; - --- --- Name: COLUMN personal_access_tokens.worker_id; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.personal_access_tokens.worker_id IS 'Optional binding to a specific device_workers.worker_id. Set by /api/me/devices/mint-child-token. When non-NULL, /api/workers/poll requires the request body''s worker_id to match.'; - --- --- Name: personal_access_tokens_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.personal_access_tokens_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: personal_access_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.personal_access_tokens_id_seq OWNED BY public.personal_access_tokens.id; - --- --- Name: rate_limits; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.rate_limits ( - key text NOT NULL, - count integer DEFAULT 0 NOT NULL, - window_started_at timestamp with time zone NOT NULL, - expires_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL -); - --- --- Name: revoked_tokens; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.revoked_tokens ( - jti text NOT NULL, - expires_at timestamp with time zone NOT NULL -); - --- --- Name: runs; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.runs ( - id bigint NOT NULL, - organization_id text, - run_type text NOT NULL, - feed_id bigint, - connection_id bigint, - action_key text, - action_input jsonb, - action_output jsonb, - approval_status text DEFAULT 'auto'::text, - status text DEFAULT 'pending'::text NOT NULL, - claimed_by text, - claimed_at timestamp with time zone, - last_heartbeat_at timestamp with time zone, - completed_at timestamp with time zone, - connector_key text, - connector_version text, - checkpoint jsonb, - items_collected integer DEFAULT 0, - error_message text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - watcher_id integer, - window_id bigint, - auth_profile_id bigint, - auth_signal jsonb, - created_by_user_id text, - dispatched_message_id text, - output_tail text, - exit_code integer, - exit_signal text, - exit_reason text, - approved_input jsonb, - queue_name text, - idempotency_key text, - attempts integer DEFAULT 0 NOT NULL, - max_attempts integer DEFAULT 3 NOT NULL, - run_at timestamp with time zone DEFAULT now() NOT NULL, - priority integer DEFAULT 0 NOT NULL, - expires_at timestamp with time zone, - retry_delay_seconds integer, - CONSTRAINT runs_approval_status_check CHECK ((approval_status = ANY (ARRAY['pending'::text, 'approved'::text, 'rejected'::text, 'auto'::text]))), - CONSTRAINT runs_legacy_org_required CHECK (((run_type <> ALL (ARRAY['sync'::text, 'action'::text, 'embed_backfill'::text, 'watcher'::text, 'auth'::text])) OR (organization_id IS NOT NULL))), - CONSTRAINT runs_run_type_check CHECK ((run_type = ANY (ARRAY['sync'::text, 'action'::text, 'embed_backfill'::text, 'watcher'::text, 'auth'::text, 'chat_message'::text, 'schedule'::text, 'agent_run'::text, 'internal'::text, 'task'::text]))), - CONSTRAINT runs_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'claimed'::text, 'running'::text, 'completed'::text, 'failed'::text, 'cancelled'::text, 'timeout'::text]))) -); - --- --- Name: runs_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.runs_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: runs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.runs_id_seq OWNED BY public.runs.id; - --- --- Name: scheduled_jobs; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.scheduled_jobs ( - id uuid DEFAULT gen_random_uuid() NOT NULL, - organization_id text NOT NULL, - action_type text NOT NULL, - action_args jsonb NOT NULL, - cron text, - next_run_at timestamp with time zone NOT NULL, - last_fired_at timestamp with time zone, - last_fired_run_id bigint, - paused boolean DEFAULT false NOT NULL, - description text NOT NULL, - created_by_user text, - created_by_agent text, - source_run_id bigint, - source_event_id bigint, - source_thread_id text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT scheduled_jobs_attribution_check CHECK (((created_by_user IS NOT NULL) OR (created_by_agent IS NOT NULL))) -); - --- --- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.schema_migrations ( - version character varying(128) NOT NULL -); - --- --- Name: session; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.session ( - id text NOT NULL, - "expiresAt" timestamp with time zone NOT NULL, - token text NOT NULL, - "createdAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - "updatedAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - "ipAddress" text, - "userAgent" text, - "userId" text NOT NULL, - "activeOrganizationId" text -); - --- --- Name: user; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public."user" ( - id text NOT NULL, - name text NOT NULL, - email text NOT NULL, - "emailVerified" boolean DEFAULT false NOT NULL, - image text, - "createdAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - "updatedAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - "phoneNumber" text, - "phoneNumberVerified" boolean DEFAULT false, - username text, - CONSTRAINT username_not_reserved CHECK ((username <> ALL (ARRAY['settings'::text, 'auth'::text, 'api'::text, 'templates'::text, 'help'::text, 'account'::text, 'admin'::text, 'health'::text, 'login'::text, 'logout'::text, 'signup'::text, 'register'::text]))) -); - --- --- Name: user_auth_profiles; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.user_auth_profiles ( - user_id text NOT NULL, - agent_id text NOT NULL, - profiles jsonb DEFAULT '[]'::jsonb NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL -); - --- --- Name: user_model_preferences; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.user_model_preferences ( - user_id text NOT NULL, - provider_id text NOT NULL, - model text NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL -); - --- --- Name: verification; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.verification ( - id text NOT NULL, - identifier text NOT NULL, - value text NOT NULL, - "expiresAt" timestamp with time zone NOT NULL, - "createdAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - "updatedAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL -); - --- --- Name: view_template_active_tabs; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.view_template_active_tabs ( - id integer NOT NULL, - resource_type text NOT NULL, - resource_id text NOT NULL, - organization_id text NOT NULL, - tab_name text NOT NULL, - tab_order integer DEFAULT 0, - current_version_id integer NOT NULL, - CONSTRAINT view_template_active_tabs_resource_type_check CHECK ((resource_type = ANY (ARRAY['entity_type'::text, 'entity'::text]))) -); - --- --- Name: view_template_active_tabs_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.view_template_active_tabs_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: view_template_active_tabs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.view_template_active_tabs_id_seq OWNED BY public.view_template_active_tabs.id; - --- --- Name: view_template_versions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.view_template_versions ( - id integer NOT NULL, - resource_type text NOT NULL, - resource_id text NOT NULL, - organization_id text NOT NULL, - version integer NOT NULL, - tab_name text, - tab_order integer DEFAULT 0, - json_template jsonb NOT NULL, - change_notes text, - created_by text NOT NULL, - created_at timestamp with time zone DEFAULT now(), - CONSTRAINT view_template_versions_resource_type_check CHECK ((resource_type = ANY (ARRAY['entity_type'::text, 'entity'::text]))) -); - --- --- Name: view_template_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.view_template_versions_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: view_template_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.view_template_versions_id_seq OWNED BY public.view_template_versions.id; - --- --- Name: watcher_reactions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.watcher_reactions ( - id bigint NOT NULL, - organization_id text NOT NULL, - watcher_id integer NOT NULL, - window_id bigint NOT NULL, - reaction_type text NOT NULL, - tool_name text NOT NULL, - tool_args jsonb, - tool_result jsonb, - run_id bigint, - entity_id bigint, - created_at timestamp with time zone DEFAULT now() -); - --- --- Name: watcher_reactions_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.watcher_reactions_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: watcher_reactions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.watcher_reactions_id_seq OWNED BY public.watcher_reactions.id; - --- --- Name: watcher_versions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.watcher_versions ( - id integer NOT NULL, - version integer NOT NULL, - name text NOT NULL, - description text, - change_notes text, - created_by text NOT NULL, - created_at timestamp with time zone DEFAULT now(), - keying_config jsonb, - json_template jsonb, - prompt text NOT NULL, - extraction_schema jsonb NOT NULL, - classifiers jsonb, - required_source_types text[] DEFAULT '{}'::text[] NOT NULL, - recommended_source_types text[] DEFAULT '{}'::text[] NOT NULL, - reactions_guidance text, - condensation_prompt text, - condensation_window_count integer DEFAULT 4, - watcher_id integer, - version_sources jsonb -); - --- --- Name: COLUMN watcher_versions.json_template; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watcher_versions.json_template IS 'JSON-based template definition for React rendering. Replaces Svelte-based renderer_component.'; - --- --- Name: COLUMN watcher_versions.prompt; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watcher_versions.prompt IS 'Handlebars prompt template used for insight extraction.'; - --- --- Name: COLUMN watcher_versions.extraction_schema; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watcher_versions.extraction_schema IS 'JSON Schema defining LLM output structure for this template version.'; - --- --- Name: COLUMN watcher_versions.classifiers; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watcher_versions.classifiers IS 'Optional classifier definitions attached to this template version.'; - --- --- Name: COLUMN watcher_versions.required_source_types; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watcher_versions.required_source_types IS 'Source type slugs that must exist for selected source entities before insight creation.'; - --- --- Name: COLUMN watcher_versions.recommended_source_types; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watcher_versions.recommended_source_types IS 'Source type slugs recommended for better insight quality.'; - --- --- Name: COLUMN watcher_versions.reactions_guidance; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watcher_versions.reactions_guidance IS 'Optional guidance text for LLM agents on what reactions to take after analyzing a watcher window.'; - --- --- Name: COLUMN watcher_versions.condensation_prompt; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watcher_versions.condensation_prompt IS 'Handlebars prompt for condensing completed windows into a rollup. Receives {{windows}} array.'; - --- --- Name: COLUMN watcher_versions.condensation_window_count; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watcher_versions.condensation_window_count IS 'How many leaf windows to condense into one rollup. Default 4.'; - --- --- Name: watcher_template_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.watcher_template_versions_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: watcher_template_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.watcher_template_versions_id_seq OWNED BY public.watcher_versions.id; - --- --- Name: watcher_window_events; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.watcher_window_events ( - id bigint NOT NULL, - window_id bigint NOT NULL, - event_id bigint NOT NULL, - created_at timestamp with time zone DEFAULT now() -); - --- --- Name: watcher_window_content_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.watcher_window_content_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: watcher_window_content_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.watcher_window_content_id_seq OWNED BY public.watcher_window_events.id; - --- --- Name: watcher_window_field_feedback; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.watcher_window_field_feedback ( - id bigint NOT NULL, - window_id integer NOT NULL, - watcher_id integer NOT NULL, - organization_id text NOT NULL, - field_path text NOT NULL, - mutation text DEFAULT 'set'::text NOT NULL, - corrected_value jsonb, - note text, - created_by text NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT watcher_window_field_feedback_mutation_check CHECK ((mutation = ANY (ARRAY['set'::text, 'remove'::text, 'add'::text]))) -); - --- --- Name: watcher_window_field_feedback_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.watcher_window_field_feedback_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: watcher_window_field_feedback_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.watcher_window_field_feedback_id_seq OWNED BY public.watcher_window_field_feedback.id; - --- --- Name: watcher_windows; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.watcher_windows ( - id integer NOT NULL, - watcher_id integer NOT NULL, - parent_window_id integer, - granularity text NOT NULL, - window_start timestamp with time zone NOT NULL, - window_end timestamp with time zone NOT NULL, - content_analyzed integer NOT NULL, - extracted_data jsonb NOT NULL, - model_used text, - execution_time_ms integer, - is_rollup boolean DEFAULT false, - source_window_ids integer[], - created_at timestamp with time zone DEFAULT now(), - version_id integer, - depth integer DEFAULT 0, - client_id text, - run_metadata jsonb DEFAULT '{}'::jsonb NOT NULL, - run_id bigint -); - --- --- Name: TABLE watcher_windows; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON TABLE public.watcher_windows IS 'Time-series watcher results with hierarchical rollups'; - --- --- Name: COLUMN watcher_windows.parent_window_id; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watcher_windows.parent_window_id IS 'Rollup hierarchy (daily->weekly->monthly->quarterly)'; - --- --- Name: COLUMN watcher_windows.depth; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watcher_windows.depth IS 'Condensation depth: 0=leaf, 1+=rollup tiers'; - --- --- Name: watcher_windows_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.watcher_windows_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: watcher_windows_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.watcher_windows_id_seq OWNED BY public.watcher_windows.id; - --- --- Name: watchers; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.watchers ( - id integer NOT NULL, - model_config jsonb DEFAULT '{}'::jsonb, - status text DEFAULT 'active'::text NOT NULL, - created_at timestamp with time zone DEFAULT now(), - updated_at timestamp with time zone DEFAULT now(), - sources jsonb DEFAULT '[]'::jsonb, - created_by text NOT NULL, - entity_ids bigint[], - reaction_script text, - reaction_script_compiled text, - organization_id text NOT NULL, - name text, - slug text, - description text, - version integer DEFAULT 1, - tags text[] DEFAULT '{}'::text[], - current_version_id integer, - schedule text, - next_run_at timestamp with time zone, - agent_id text NOT NULL, - connection_id text, - scheduler_client_id text, - source_watcher_id integer, - watcher_group_id integer NOT NULL, - device_worker_id uuid, - agent_kind text, - notification_channel text DEFAULT 'canvas'::text NOT NULL, - notification_priority text DEFAULT 'normal'::text NOT NULL, - min_cooldown_seconds integer DEFAULT 0 NOT NULL, - last_fired_at timestamp with time zone, - CONSTRAINT insights_status_check CHECK ((status = ANY (ARRAY['active'::text, 'archived'::text]))), - CONSTRAINT watchers_min_cooldown_seconds_nonneg CHECK ((min_cooldown_seconds >= 0)), - CONSTRAINT watchers_notification_channel_check CHECK ((notification_channel = ANY (ARRAY['canvas'::text, 'notification'::text, 'both'::text]))), - CONSTRAINT watchers_notification_priority_check CHECK ((notification_priority = ANY (ARRAY['low'::text, 'normal'::text, 'high'::text]))) -); - --- --- Name: COLUMN watchers.status; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watchers.status IS 'Status of insight: active (recurring), paused (recurring), failed (recurring), completed (one-off)'; - --- --- Name: COLUMN watchers.sources; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watchers.sources IS 'Array of data sources: [{name: string, entity_id: number, filters: {min_score?, platforms?, search_query?}}]. Each source defines an entity and its filters for content fetching.'; - --- --- Name: COLUMN watchers.reaction_script; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watchers.reaction_script IS 'TypeScript source for automated reaction script.'; - --- --- Name: COLUMN watchers.reaction_script_compiled; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON COLUMN public.watchers.reaction_script_compiled IS 'Compiled JavaScript from reaction_script.'; - --- --- Name: watchers_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.watchers_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: watchers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.watchers_id_seq OWNED BY public.watchers.id; - --- --- Name: agent_transcript_snapshot id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_transcript_snapshot ALTER COLUMN id SET DEFAULT nextval('public.agent_transcript_snapshot_id_seq'::regclass); - --- --- Name: chat_state_lists seq; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.chat_state_lists ALTER COLUMN seq SET DEFAULT nextval('public.chat_state_lists_seq_seq'::regclass); - --- --- Name: chat_state_queues seq; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.chat_state_queues ALTER COLUMN seq SET DEFAULT nextval('public.chat_state_queues_seq_seq'::regclass); - --- --- Name: connect_tokens id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connect_tokens ALTER COLUMN id SET DEFAULT nextval('public.connect_tokens_id_seq'::regclass); - --- --- Name: connections id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connections ALTER COLUMN id SET DEFAULT nextval('public.connections_id_seq'::regclass); - --- --- Name: connector_definitions id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connector_definitions ALTER COLUMN id SET DEFAULT nextval('public.connector_definitions_id_seq'::regclass); - --- --- Name: connector_versions id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connector_versions ALTER COLUMN id SET DEFAULT nextval('public.connector_versions_id_seq'::regclass); - --- --- Name: entities id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entities ALTER COLUMN id SET DEFAULT nextval('public.entities_id_seq'::regclass); - --- --- Name: entity_identities id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_identities ALTER COLUMN id SET DEFAULT nextval('public.entity_identities_id_seq'::regclass); - --- --- Name: entity_relationship_type_rules id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationship_type_rules ALTER COLUMN id SET DEFAULT nextval('public.entity_relationship_type_rules_id_seq'::regclass); - --- --- Name: entity_relationship_types id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationship_types ALTER COLUMN id SET DEFAULT nextval('public.entity_relationship_types_id_seq'::regclass); - --- --- Name: entity_relationships id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationships ALTER COLUMN id SET DEFAULT nextval('public.entity_relationships_id_seq'::regclass); - --- --- Name: entity_type_audit id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_type_audit ALTER COLUMN id SET DEFAULT nextval('public.entity_type_audit_id_seq'::regclass); - --- --- Name: entity_types id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_types ALTER COLUMN id SET DEFAULT nextval('public.entity_types_id_seq'::regclass); - --- --- Name: event_classifications id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifications ALTER COLUMN id SET DEFAULT nextval('public.event_classifications_id_seq'::regclass); - --- --- Name: event_classifier_versions id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifier_versions ALTER COLUMN id SET DEFAULT nextval('public.event_classifier_versions_id_seq'::regclass); - --- --- Name: event_classifiers id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifiers ALTER COLUMN id SET DEFAULT nextval('public.event_classifiers_id_seq'::regclass); - --- --- Name: events id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.events ALTER COLUMN id SET DEFAULT nextval('public.content_id_seq'::regclass); - --- --- Name: feeds id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.feeds ALTER COLUMN id SET DEFAULT nextval('public.feeds_id_seq'::regclass); - --- --- Name: personal_access_tokens id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.personal_access_tokens ALTER COLUMN id SET DEFAULT nextval('public.personal_access_tokens_id_seq'::regclass); - --- --- Name: runs id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.runs ALTER COLUMN id SET DEFAULT nextval('public.runs_id_seq'::regclass); - --- --- Name: view_template_active_tabs id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.view_template_active_tabs ALTER COLUMN id SET DEFAULT nextval('public.view_template_active_tabs_id_seq'::regclass); - --- --- Name: view_template_versions id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.view_template_versions ALTER COLUMN id SET DEFAULT nextval('public.view_template_versions_id_seq'::regclass); - --- --- Name: watcher_reactions id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_reactions ALTER COLUMN id SET DEFAULT nextval('public.watcher_reactions_id_seq'::regclass); - --- --- Name: watcher_versions id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_versions ALTER COLUMN id SET DEFAULT nextval('public.watcher_template_versions_id_seq'::regclass); - --- --- Name: watcher_window_events id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_window_events ALTER COLUMN id SET DEFAULT nextval('public.watcher_window_content_id_seq'::regclass); - --- --- Name: watcher_window_field_feedback id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_window_field_feedback ALTER COLUMN id SET DEFAULT nextval('public.watcher_window_field_feedback_id_seq'::regclass); - --- --- Name: watcher_windows id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_windows ALTER COLUMN id SET DEFAULT nextval('public.watcher_windows_id_seq'::regclass); - --- --- Name: watchers id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watchers ALTER COLUMN id SET DEFAULT nextval('public.watchers_id_seq'::regclass); - --- --- Name: account account_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.account - ADD CONSTRAINT account_pkey PRIMARY KEY (id); - --- --- Name: agent_channel_bindings agent_channel_bindings_platform_channel_id_team_id_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_channel_bindings - ADD CONSTRAINT agent_channel_bindings_platform_channel_id_team_id_key UNIQUE (platform, channel_id, team_id); - --- --- Name: agent_connections agent_connections_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_connections - ADD CONSTRAINT agent_connections_pkey PRIMARY KEY (id); - --- --- Name: agent_grants agent_grants_org_agent_pattern_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_grants - ADD CONSTRAINT agent_grants_org_agent_pattern_key UNIQUE (organization_id, agent_id, pattern); - --- --- Name: agent_grants agent_grants_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_grants - ADD CONSTRAINT agent_grants_pkey PRIMARY KEY (id); - --- --- Name: agent_secrets agent_secrets_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_secrets - ADD CONSTRAINT agent_secrets_pkey PRIMARY KEY (organization_id, name); - --- --- Name: agent_transcript_snapshot agent_transcript_snapshot_organization_id_agent_id_conversa_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_transcript_snapshot - ADD CONSTRAINT agent_transcript_snapshot_organization_id_agent_id_conversa_key UNIQUE (organization_id, agent_id, conversation_id, run_id); - --- --- Name: agent_transcript_snapshot agent_transcript_snapshot_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_transcript_snapshot - ADD CONSTRAINT agent_transcript_snapshot_pkey PRIMARY KEY (id); - --- --- Name: agent_users agent_users_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_users - ADD CONSTRAINT agent_users_pkey PRIMARY KEY (organization_id, agent_id, platform, user_id); - --- --- Name: agents agents_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agents - ADD CONSTRAINT agents_pkey PRIMARY KEY (organization_id, id); - --- --- Name: auth_profiles auth_profiles_org_id_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.auth_profiles - ADD CONSTRAINT auth_profiles_org_id_unique UNIQUE (organization_id, id); - --- --- Name: auth_profiles auth_profiles_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.auth_profiles - ADD CONSTRAINT auth_profiles_pkey PRIMARY KEY (id); - --- --- Name: chat_state_cache chat_state_cache_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.chat_state_cache - ADD CONSTRAINT chat_state_cache_pkey PRIMARY KEY (key_prefix, cache_key); - --- --- Name: chat_state_lists chat_state_lists_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.chat_state_lists - ADD CONSTRAINT chat_state_lists_pkey PRIMARY KEY (key_prefix, list_key, seq); - --- --- Name: chat_state_locks chat_state_locks_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.chat_state_locks - ADD CONSTRAINT chat_state_locks_pkey PRIMARY KEY (key_prefix, thread_id); - --- --- Name: chat_state_queues chat_state_queues_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.chat_state_queues - ADD CONSTRAINT chat_state_queues_pkey PRIMARY KEY (key_prefix, thread_id, seq); - --- --- Name: chat_state_subscriptions chat_state_subscriptions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.chat_state_subscriptions - ADD CONSTRAINT chat_state_subscriptions_pkey PRIMARY KEY (key_prefix, thread_id); - --- --- Name: chat_user_identities chat_user_identities_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.chat_user_identities - ADD CONSTRAINT chat_user_identities_pkey PRIMARY KEY (platform, team_id, platform_user_id); - --- --- Name: connect_tokens connect_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connect_tokens - ADD CONSTRAINT connect_tokens_pkey PRIMARY KEY (id); - --- --- Name: connect_tokens connect_tokens_token_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connect_tokens - ADD CONSTRAINT connect_tokens_token_key UNIQUE (token); - --- --- Name: connections connections_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connections - ADD CONSTRAINT connections_pkey PRIMARY KEY (id); - --- --- Name: connector_definitions connector_definitions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connector_definitions - ADD CONSTRAINT connector_definitions_pkey PRIMARY KEY (id); - --- --- Name: connector_versions connector_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connector_versions - ADD CONSTRAINT connector_versions_pkey PRIMARY KEY (id); - --- --- Name: device_workers device_workers_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.device_workers - ADD CONSTRAINT device_workers_pkey PRIMARY KEY (user_id, worker_id); - --- --- Name: entities entities_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entities - ADD CONSTRAINT entities_pkey PRIMARY KEY (id); - --- --- Name: entity_identities entity_identities_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_identities - ADD CONSTRAINT entity_identities_pkey PRIMARY KEY (id); - --- --- Name: entity_relationship_type_rules entity_relationship_type_rules_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationship_type_rules - ADD CONSTRAINT entity_relationship_type_rules_pkey PRIMARY KEY (id); - --- --- Name: entity_relationship_types entity_relationship_types_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationship_types - ADD CONSTRAINT entity_relationship_types_pkey PRIMARY KEY (id); - --- --- Name: entity_relationships entity_relationships_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationships - ADD CONSTRAINT entity_relationships_pkey PRIMARY KEY (id); - --- --- Name: entity_type_audit entity_type_audit_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_type_audit - ADD CONSTRAINT entity_type_audit_pkey PRIMARY KEY (id); - --- --- Name: entity_types entity_types_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_types - ADD CONSTRAINT entity_types_pkey PRIMARY KEY (id); - --- --- Name: event_classifications event_classifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifications - ADD CONSTRAINT event_classifications_pkey PRIMARY KEY (id); - --- --- Name: event_classifier_versions event_classifier_versions_classifier_id_version_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifier_versions - ADD CONSTRAINT event_classifier_versions_classifier_id_version_key UNIQUE (classifier_id, version); - --- --- Name: event_classifier_versions event_classifier_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifier_versions - ADD CONSTRAINT event_classifier_versions_pkey PRIMARY KEY (id); - --- --- Name: event_classifiers event_classifiers_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifiers - ADD CONSTRAINT event_classifiers_pkey PRIMARY KEY (id); - --- --- Name: event_classifiers event_classifiers_unique_per_insight; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifiers - ADD CONSTRAINT event_classifiers_unique_per_insight UNIQUE NULLS NOT DISTINCT (entity_id, watcher_id, slug); - --- --- Name: event_embeddings event_embeddings_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_embeddings - ADD CONSTRAINT event_embeddings_pkey PRIMARY KEY (event_id); - --- --- Name: events event_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.events - ADD CONSTRAINT event_pkey PRIMARY KEY (id); - --- --- Name: feeds feeds_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.feeds - ADD CONSTRAINT feeds_pkey PRIMARY KEY (id); - --- --- Name: grants grants_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.grants - ADD CONSTRAINT grants_pkey PRIMARY KEY (organization_id, agent_id, kind, pattern); - --- --- Name: watcher_versions insight_template_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_versions - ADD CONSTRAINT insight_template_versions_pkey PRIMARY KEY (id); - --- --- Name: watcher_window_events insight_window_content_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_window_events - ADD CONSTRAINT insight_window_content_pkey PRIMARY KEY (id); - --- --- Name: watcher_window_events insight_window_content_window_id_content_id_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_window_events - ADD CONSTRAINT insight_window_content_window_id_content_id_key UNIQUE (window_id, event_id); - --- --- Name: watcher_windows insight_windows_insight_id_granularity_window_start_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_windows - ADD CONSTRAINT insight_windows_insight_id_granularity_window_start_key UNIQUE (watcher_id, granularity, window_start); - --- --- Name: watcher_windows insight_windows_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_windows - ADD CONSTRAINT insight_windows_pkey PRIMARY KEY (id); - --- --- Name: watchers insights_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watchers - ADD CONSTRAINT insights_pkey PRIMARY KEY (id); - --- --- Name: invitation invitation_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.invitation - ADD CONSTRAINT invitation_pkey PRIMARY KEY (id); - --- --- Name: latest_event_classifications latest_event_classifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.latest_event_classifications - ADD CONSTRAINT latest_event_classifications_pkey PRIMARY KEY (event_id, classifier_id); - --- --- Name: mcp_proxy_sessions mcp_proxy_sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.mcp_proxy_sessions - ADD CONSTRAINT mcp_proxy_sessions_pkey PRIMARY KEY (session_key); - --- --- Name: mcp_sessions mcp_sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.mcp_sessions - ADD CONSTRAINT mcp_sessions_pkey PRIMARY KEY (session_id); - --- --- Name: member member_organizationId_userId_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.member - ADD CONSTRAINT "member_organizationId_userId_key" UNIQUE ("organizationId", "userId"); - --- --- Name: member member_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.member - ADD CONSTRAINT member_pkey PRIMARY KEY (id); - --- --- Name: migration_20260315300000_entity_type_org_backfill migration_20260315300000_entity_type_org_backfill_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.migration_20260315300000_entity_type_org_backfill - ADD CONSTRAINT migration_20260315300000_entity_type_org_backfill_pkey PRIMARY KEY (entity_type_id); - --- --- Name: migration_20260316100000_created_entity_types migration_20260316100000_created_entity_types_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.migration_20260316100000_created_entity_types - ADD CONSTRAINT migration_20260316100000_created_entity_types_pkey PRIMARY KEY (entity_type_id); - --- --- Name: migration_20260316100000_deleted_default_entity_types migration_20260316100000_deleted_default_entity_types_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.migration_20260316100000_deleted_default_entity_types - ADD CONSTRAINT migration_20260316100000_deleted_default_entity_types_pkey PRIMARY KEY (id); - --- --- Name: migration_20260316100000_events_kind_backup migration_20260316100000_events_kind_backup_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.migration_20260316100000_events_kind_backup - ADD CONSTRAINT migration_20260316100000_events_kind_backup_pkey PRIMARY KEY (event_id); - --- --- Name: namespace namespace_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.namespace - ADD CONSTRAINT namespace_pkey PRIMARY KEY (slug); - --- --- Name: notification_targets notification_targets_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.notification_targets - ADD CONSTRAINT notification_targets_pkey PRIMARY KEY (event_id, user_id); - --- --- Name: oauth_authorization_codes oauth_authorization_codes_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_authorization_codes - ADD CONSTRAINT oauth_authorization_codes_pkey PRIMARY KEY (code); - --- --- Name: oauth_clients oauth_clients_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_clients - ADD CONSTRAINT oauth_clients_pkey PRIMARY KEY (id); - --- --- Name: oauth_device_codes oauth_device_codes_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_device_codes - ADD CONSTRAINT oauth_device_codes_pkey PRIMARY KEY (device_code); - --- --- Name: oauth_states oauth_states_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_states - ADD CONSTRAINT oauth_states_pkey PRIMARY KEY (id); - --- --- Name: oauth_tokens oauth_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_tokens - ADD CONSTRAINT oauth_tokens_pkey PRIMARY KEY (id); - --- --- Name: oauth_tokens oauth_tokens_token_hash_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_tokens - ADD CONSTRAINT oauth_tokens_token_hash_key UNIQUE (token_hash); - --- --- Name: organization_lobu_links organization_lobu_links_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.organization_lobu_links - ADD CONSTRAINT organization_lobu_links_pkey PRIMARY KEY (organization_id); - --- --- Name: organization organization_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.organization - ADD CONSTRAINT organization_pkey PRIMARY KEY (id); - --- --- Name: organization organization_slug_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.organization - ADD CONSTRAINT organization_slug_key UNIQUE (slug); - --- --- Name: passkey passkey_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.passkey - ADD CONSTRAINT passkey_pkey PRIMARY KEY (id); - --- --- Name: pending_interactions pending_interactions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.pending_interactions - ADD CONSTRAINT pending_interactions_pkey PRIMARY KEY (id); - --- --- Name: personal_access_tokens personal_access_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.personal_access_tokens - ADD CONSTRAINT personal_access_tokens_pkey PRIMARY KEY (id); - --- --- Name: personal_access_tokens personal_access_tokens_token_hash_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.personal_access_tokens - ADD CONSTRAINT personal_access_tokens_token_hash_key UNIQUE (token_hash); - --- --- Name: rate_limits rate_limits_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.rate_limits - ADD CONSTRAINT rate_limits_pkey PRIMARY KEY (key); - --- --- Name: revoked_tokens revoked_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.revoked_tokens - ADD CONSTRAINT revoked_tokens_pkey PRIMARY KEY (jti); - --- --- Name: runs runs_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.runs - ADD CONSTRAINT runs_pkey PRIMARY KEY (id); - --- --- Name: scheduled_jobs scheduled_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.scheduled_jobs - ADD CONSTRAINT scheduled_jobs_pkey PRIMARY KEY (id); - --- --- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.schema_migrations - ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); - --- --- Name: session session_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.session - ADD CONSTRAINT session_pkey PRIMARY KEY (id); - --- --- Name: session session_token_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.session - ADD CONSTRAINT session_token_key UNIQUE (token); - --- --- Name: user_auth_profiles user_auth_profiles_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.user_auth_profiles - ADD CONSTRAINT user_auth_profiles_pkey PRIMARY KEY (user_id, agent_id); - --- --- Name: user user_email_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."user" - ADD CONSTRAINT user_email_key UNIQUE (email); - --- --- Name: user_model_preferences user_model_preferences_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.user_model_preferences - ADD CONSTRAINT user_model_preferences_pkey PRIMARY KEY (user_id, provider_id); - --- --- Name: user user_phoneNumber_key; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."user" - ADD CONSTRAINT "user_phoneNumber_key" UNIQUE ("phoneNumber"); - --- --- Name: user user_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."user" - ADD CONSTRAINT user_pkey PRIMARY KEY (id); - --- --- Name: verification verification_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.verification - ADD CONSTRAINT verification_pkey PRIMARY KEY (id); - --- --- Name: view_template_active_tabs view_template_active_tabs_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.view_template_active_tabs - ADD CONSTRAINT view_template_active_tabs_pkey PRIMARY KEY (id); - --- --- Name: view_template_active_tabs view_template_active_tabs_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.view_template_active_tabs - ADD CONSTRAINT view_template_active_tabs_unique UNIQUE (resource_type, resource_id, organization_id, tab_name); - --- --- Name: view_template_versions view_template_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.view_template_versions - ADD CONSTRAINT view_template_versions_pkey PRIMARY KEY (id); - --- --- Name: view_template_versions view_template_versions_unique; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.view_template_versions - ADD CONSTRAINT view_template_versions_unique UNIQUE NULLS NOT DISTINCT (resource_type, resource_id, organization_id, tab_name, version); - --- --- Name: watcher_reactions watcher_reactions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_reactions - ADD CONSTRAINT watcher_reactions_pkey PRIMARY KEY (id); - --- --- Name: watcher_window_field_feedback watcher_window_field_feedback_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_window_field_feedback - ADD CONSTRAINT watcher_window_field_feedback_pkey PRIMARY KEY (id); - --- --- Name: account_providerId_accountId_uidx; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX "account_providerId_accountId_uidx" ON public.account USING btree ("providerId", "accountId"); - --- --- Name: account_providerId_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX "account_providerId_idx" ON public.account USING btree ("providerId"); - --- --- Name: account_userId_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX "account_userId_idx" ON public.account USING btree ("userId"); - --- --- Name: agent_channel_bindings_agent_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX agent_channel_bindings_agent_id_idx ON public.agent_channel_bindings USING btree (agent_id); - --- --- Name: agent_channel_bindings_no_team_unique; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX agent_channel_bindings_no_team_unique ON public.agent_channel_bindings USING btree (platform, channel_id) WHERE (team_id IS NULL); - --- --- Name: agent_channel_bindings_org_agent_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX agent_channel_bindings_org_agent_idx ON public.agent_channel_bindings USING btree (organization_id, agent_id); - --- --- Name: agent_connections_agent_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX agent_connections_agent_id_idx ON public.agent_connections USING btree (agent_id); - --- --- Name: agent_connections_org_agent_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX agent_connections_org_agent_idx ON public.agent_connections USING btree (organization_id, agent_id); - --- --- Name: agent_connections_platform_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX agent_connections_platform_idx ON public.agent_connections USING btree (platform); - --- --- Name: agent_grants_agent_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX agent_grants_agent_id_idx ON public.agent_grants USING btree (agent_id); - --- --- Name: agent_grants_org_agent_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX agent_grants_org_agent_idx ON public.agent_grants USING btree (organization_id, agent_id); - --- --- Name: agent_secrets_expires_at_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX agent_secrets_expires_at_idx ON public.agent_secrets USING btree (expires_at) WHERE (expires_at IS NOT NULL); - --- --- Name: agent_secrets_name_prefix_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX agent_secrets_name_prefix_idx ON public.agent_secrets USING btree (name text_pattern_ops); - --- --- Name: agent_secrets_org_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX agent_secrets_org_id_idx ON public.agent_secrets USING btree (organization_id); - --- --- Name: agent_transcript_snapshot_latest; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX agent_transcript_snapshot_latest ON public.agent_transcript_snapshot USING btree (organization_id, agent_id, conversation_id, run_id DESC); - --- --- Name: agent_users_org_agent_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX agent_users_org_agent_idx ON public.agent_users USING btree (organization_id, agent_id); - --- --- Name: agents_organization_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX agents_organization_id_idx ON public.agents USING btree (organization_id); - --- --- Name: auth_profiles_connector_kind_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX auth_profiles_connector_kind_idx ON public.auth_profiles USING btree (organization_id, connector_key, profile_kind, status); - --- --- Name: auth_profiles_default_for_connector_unique; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX auth_profiles_default_for_connector_unique ON public.auth_profiles USING btree (organization_id, connector_key) WHERE (is_default_for_connector AND (profile_kind = 'oauth_app'::text)); - --- --- Name: auth_profiles_device_worker_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX auth_profiles_device_worker_idx ON public.auth_profiles USING btree (device_worker_id) WHERE (device_worker_id IS NOT NULL); - --- --- Name: auth_profiles_org_slug_unique; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX auth_profiles_org_slug_unique ON public.auth_profiles USING btree (organization_id, slug); - --- --- Name: auth_profiles_pending_unique; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX auth_profiles_pending_unique ON public.auth_profiles USING btree (organization_id, connector_key, profile_kind, provider) WHERE (status = 'pending_auth'::text); - --- --- Name: chat_state_cache_expires_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX chat_state_cache_expires_idx ON public.chat_state_cache USING btree (expires_at); - --- --- Name: chat_state_lists_expires_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX chat_state_lists_expires_idx ON public.chat_state_lists USING btree (expires_at); - --- --- Name: chat_state_locks_expires_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX chat_state_locks_expires_idx ON public.chat_state_locks USING btree (expires_at); - --- --- Name: chat_state_queues_expires_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX chat_state_queues_expires_idx ON public.chat_state_queues USING btree (expires_at); - --- --- Name: chat_user_identities_lobu_user_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX chat_user_identities_lobu_user_idx ON public.chat_user_identities USING btree (lobu_user_id); - --- --- Name: connections_org_slug_unique; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX connections_org_slug_unique ON public.connections USING btree (organization_id, slug) WHERE (deleted_at IS NULL); - --- --- Name: connector_definitions_required_capability_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX connector_definitions_required_capability_idx ON public.connector_definitions USING btree (required_capability) WHERE (required_capability IS NOT NULL); - --- --- Name: device_workers_id_key; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX device_workers_id_key ON public.device_workers USING btree (id); - --- --- Name: device_workers_user_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX device_workers_user_id_idx ON public.device_workers USING btree (user_id); - --- --- Name: entities_slug_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX entities_slug_idx ON public.entities USING btree (slug); - --- --- Name: entities_slug_parent_unique; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX entities_slug_parent_unique ON public.entities USING btree (organization_id, COALESCE(parent_id, (0)::bigint), slug); - --- --- Name: feeds_open_repair_thread_uniq; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX feeds_open_repair_thread_uniq ON public.feeds USING btree (id) WHERE (repair_thread_id IS NOT NULL); - --- --- Name: grants_agent_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX grants_agent_id_idx ON public.grants USING btree (agent_id); - --- --- Name: grants_expires_at_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX grants_expires_at_idx ON public.grants USING btree (expires_at) WHERE (expires_at IS NOT NULL); - --- --- Name: grants_org_agent_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX grants_org_agent_idx ON public.grants USING btree (organization_id, agent_id); - --- --- Name: idx_cc_classifier_version_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_cc_classifier_version_id ON public.event_classifications USING btree (classifier_version_id); - --- --- Name: idx_cc_content_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_cc_content_id ON public.event_classifications USING btree (event_id); - --- --- Name: idx_cc_source; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_cc_source ON public.event_classifications USING btree (source); - --- --- Name: idx_cc_unique_per_source; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_cc_unique_per_source ON public.event_classifications USING btree (event_id, classifier_version_id, source, COALESCE(watcher_id, (0)::bigint)); - --- --- Name: idx_cc_values_gin; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_cc_values_gin ON public.event_classifications USING gin ("values"); - --- --- Name: idx_cc_watcher_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_cc_watcher_id ON public.event_classifications USING btree (watcher_id) WHERE (watcher_id IS NOT NULL); - --- --- Name: idx_cc_window_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_cc_window_id ON public.event_classifications USING btree (window_id) WHERE (window_id IS NOT NULL); - --- --- Name: idx_connect_tokens_connection_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_connect_tokens_connection_id ON public.connect_tokens USING btree (connection_id); - --- --- Name: idx_connect_tokens_status_expires; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_connect_tokens_status_expires ON public.connect_tokens USING btree (status, expires_at) WHERE (status = 'pending'::text); - --- --- Name: idx_connect_tokens_token; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_connect_tokens_token ON public.connect_tokens USING btree (token); - --- --- Name: idx_connections_account; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_connections_account ON public.connections USING btree (account_id); - --- --- Name: idx_connections_agent_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_connections_agent_id ON public.connections USING btree (agent_id) WHERE (agent_id IS NOT NULL); - --- --- Name: idx_connections_connector_key; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_connections_connector_key ON public.connections USING btree (connector_key); - --- --- Name: idx_connections_deleted_at; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_connections_deleted_at ON public.connections USING btree (deleted_at) WHERE (deleted_at IS NULL); - --- --- Name: idx_connections_device_worker_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_connections_device_worker_id ON public.connections USING btree (device_worker_id) WHERE (device_worker_id IS NOT NULL); - --- --- Name: idx_connections_entity_ids; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_connections_entity_ids ON public.connections USING gin (entity_ids); - --- --- Name: idx_connections_org; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_connections_org ON public.connections USING btree (organization_id); - --- --- Name: idx_connections_org_connector_account_live; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_connections_org_connector_account_live ON public.connections USING btree (organization_id, connector_key, account_id) WHERE ((deleted_at IS NULL) AND (account_id IS NOT NULL)); - --- --- Name: idx_connections_org_connector_device_live; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_connections_org_connector_device_live ON public.connections USING btree (organization_id, connector_key, device_worker_id) WHERE ((deleted_at IS NULL) AND (device_worker_id IS NOT NULL)); - --- --- Name: idx_connections_status; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_connections_status ON public.connections USING btree (status); - --- --- Name: idx_connections_visibility; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_connections_visibility ON public.connections USING btree (organization_id, visibility); - --- --- Name: idx_connector_defs_org_key; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_connector_defs_org_key ON public.connector_definitions USING btree (organization_id, key) WHERE ((organization_id IS NOT NULL) AND (status = 'active'::text)); - --- --- Name: idx_connector_defs_status; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_connector_defs_status ON public.connector_definitions USING btree (status); - --- --- Name: idx_connector_versions_key_version; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_connector_versions_key_version ON public.connector_versions USING btree (connector_key, version); - --- --- Name: idx_device_workers_organization_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_device_workers_organization_id ON public.device_workers USING btree (organization_id) WHERE (organization_id IS NOT NULL); - --- --- Name: idx_ec_has_excerpts; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_ec_has_excerpts ON public.event_classifications USING btree (((excerpts <> '{}'::jsonb))) WHERE (excerpts <> '{}'::jsonb); - --- --- Name: idx_entities_by_parent; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entities_by_parent ON public.entities USING btree (parent_id, id) WHERE (parent_id IS NOT NULL); - --- --- Name: idx_entities_classifiers; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entities_classifiers ON public.entities USING gin (enabled_classifiers) WHERE (enabled_classifiers IS NOT NULL); - --- --- Name: idx_entities_content_hash; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entities_content_hash ON public.entities USING btree (organization_id, content_hash) WHERE ((content_hash IS NOT NULL) AND (deleted_at IS NULL)); - --- --- Name: idx_entities_content_tsv; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entities_content_tsv ON public.entities USING gin (content_tsv) WHERE (deleted_at IS NULL); - --- --- Name: idx_entities_created_by; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entities_created_by ON public.entities USING btree (created_by); - --- --- Name: idx_entities_embedding; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entities_embedding ON public.entities USING ivfflat (embedding public.vector_cosine_ops) WITH (lists='100') WHERE ((embedding IS NOT NULL) AND (deleted_at IS NULL)); - --- --- Name: idx_entities_entity_type_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entities_entity_type_id ON public.entities USING btree (entity_type_id) WHERE (deleted_at IS NULL); - --- --- Name: idx_entities_metadata_domain; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_entities_metadata_domain ON public.entities USING btree (((metadata ->> 'domain'::text)), organization_id) WHERE ((metadata ->> 'domain'::text) IS NOT NULL); - --- --- Name: idx_entities_metadata_email; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entities_metadata_email ON public.entities USING btree (((metadata ->> 'email'::text)), organization_id) WHERE (((metadata ->> 'email'::text) IS NOT NULL) AND (deleted_at IS NULL)); - --- --- Name: idx_entities_metadata_github_handle; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entities_metadata_github_handle ON public.entities USING btree (((metadata ->> 'github_handle'::text)), organization_id) WHERE (((metadata ->> 'github_handle'::text) IS NOT NULL) AND (deleted_at IS NULL)); - --- --- Name: idx_entities_metadata_linkedin_url; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entities_metadata_linkedin_url ON public.entities USING btree (((metadata ->> 'linkedin_url'::text)), organization_id) WHERE (((metadata ->> 'linkedin_url'::text) IS NOT NULL) AND (deleted_at IS NULL)); - --- --- Name: idx_entities_metadata_twitter_handle; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entities_metadata_twitter_handle ON public.entities USING btree (((metadata ->> 'twitter_handle'::text)), organization_id) WHERE (((metadata ->> 'twitter_handle'::text) IS NOT NULL) AND (deleted_at IS NULL)); - --- --- Name: idx_entities_name; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entities_name ON public.entities USING btree (lower(name)); - --- --- Name: idx_entities_organization_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entities_organization_id ON public.entities USING btree (organization_id); - --- --- Name: idx_entity_identities_by_entity; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entity_identities_by_entity ON public.entity_identities USING btree (entity_id) WHERE (deleted_at IS NULL); - --- --- Name: idx_entity_identities_live_unique; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_entity_identities_live_unique ON public.entity_identities USING btree (organization_id, namespace, identifier) WHERE (deleted_at IS NULL); - --- --- Name: idx_entity_identities_lookup; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entity_identities_lookup ON public.entity_identities USING btree (organization_id, namespace, identifier) WHERE (deleted_at IS NULL); - --- --- Name: idx_entity_rel_type_rules_type; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entity_rel_type_rules_type ON public.entity_relationship_type_rules USING btree (relationship_type_id); - --- --- Name: idx_entity_rel_types_org_slug; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_entity_rel_types_org_slug ON public.entity_relationship_types USING btree (organization_id, slug) WHERE (status = 'active'::text); - --- --- Name: idx_entity_relationship_types_has_auto_create; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entity_relationship_types_has_auto_create ON public.entity_relationship_types USING btree (((metadata -> 'autoCreateWhen'::text))) WHERE ((metadata ? 'autoCreateWhen'::text) AND (deleted_at IS NULL)); - --- --- Name: idx_entity_relationships_derived_from_event; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entity_relationships_derived_from_event ON public.entity_relationships USING btree ((((metadata -> 'derivedFrom'::text) ->> 'sourceEventId'::text))) WHERE (metadata ? 'derivedFrom'::text); - --- --- Name: idx_entity_relationships_derived_from_rule; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entity_relationships_derived_from_rule ON public.entity_relationships USING btree ((((metadata -> 'derivedFrom'::text) ->> 'relationshipTypeId'::text)), (((metadata -> 'derivedFrom'::text) ->> 'ruleVersion'::text))) WHERE (metadata ? 'derivedFrom'::text); - --- --- Name: idx_entity_relationships_from; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entity_relationships_from ON public.entity_relationships USING btree (from_entity_id); - --- --- Name: idx_entity_relationships_live_triple; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_entity_relationships_live_triple ON public.entity_relationships USING btree (from_entity_id, to_entity_id, relationship_type_id) WHERE (deleted_at IS NULL); - --- --- Name: idx_entity_relationships_org; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entity_relationships_org ON public.entity_relationships USING btree (organization_id); - --- --- Name: idx_entity_relationships_to; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entity_relationships_to ON public.entity_relationships USING btree (to_entity_id); - --- --- Name: idx_entity_relationships_type; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entity_relationships_type ON public.entity_relationships USING btree (relationship_type_id); - --- --- Name: idx_entity_type_audit_action; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entity_type_audit_action ON public.entity_type_audit USING btree (action); - --- --- Name: idx_entity_type_audit_type_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entity_type_audit_type_id ON public.entity_type_audit USING btree (entity_type_id); - --- --- Name: idx_entity_types_active; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_entity_types_active ON public.entity_types USING btree (id) WHERE (deleted_at IS NULL); - --- --- Name: idx_entity_types_org_slug; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_entity_types_org_slug ON public.entity_types USING btree (organization_id, slug) WHERE ((organization_id IS NOT NULL) AND (deleted_at IS NULL)); - --- --- Name: idx_event_classifications_source; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_event_classifications_source ON public.event_classifications USING btree (source); - --- --- Name: idx_event_classifier_versions_classifier; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_event_classifier_versions_classifier ON public.event_classifier_versions USING btree (classifier_id); - --- --- Name: idx_event_classifier_versions_created_by; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_event_classifier_versions_created_by ON public.event_classifier_versions USING btree (created_by); - --- --- Name: idx_event_classifier_versions_current; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_event_classifier_versions_current ON public.event_classifier_versions USING btree (classifier_id) WHERE (is_current = true); - --- --- Name: idx_event_classifiers_created_by; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_event_classifiers_created_by ON public.event_classifiers USING btree (created_by); - --- --- Name: idx_event_classifiers_entity_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_event_classifiers_entity_id ON public.event_classifiers USING btree (entity_id) WHERE (entity_id IS NOT NULL); - --- --- Name: idx_event_classifiers_entity_ids; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_event_classifiers_entity_ids ON public.event_classifiers USING gin (entity_ids); - --- --- Name: idx_event_classifiers_insight_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_event_classifiers_insight_id ON public.event_classifiers USING btree (watcher_id) WHERE (watcher_id IS NOT NULL); - --- --- Name: idx_event_classifiers_organization_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_event_classifiers_organization_id ON public.event_classifiers USING btree (organization_id); - --- --- Name: idx_event_classifiers_slug; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_event_classifiers_slug ON public.event_classifiers USING btree (slug); - --- --- Name: idx_event_classifiers_status; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_event_classifiers_status ON public.event_classifiers USING btree (status); - --- --- Name: idx_events_client_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_client_id ON public.events USING btree (client_id) WHERE (client_id IS NOT NULL); - --- --- Name: idx_events_connection_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_connection_id ON public.events USING btree (connection_id); - --- --- Name: idx_events_connection_origin_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_connection_origin_id ON public.events USING btree (connection_id, origin_id, created_at DESC) WHERE (connection_id IS NOT NULL); - --- --- Name: idx_events_connector_key; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_connector_key ON public.events USING btree (connector_key); - --- --- Name: idx_events_created_at; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_created_at ON public.events USING btree (created_at); - --- --- Name: idx_events_created_by; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_created_by ON public.events USING btree (created_by) WHERE (created_by IS NOT NULL); - --- --- Name: idx_events_embedding; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_embedding ON public.event_embeddings USING ivfflat (embedding public.vector_cosine_ops) WITH (lists='1000'); - --- --- Name: idx_events_entity_ids; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_entity_ids ON public.events USING gin (entity_ids); - --- --- Name: idx_events_feed_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_feed_id ON public.events USING btree (feed_id); - --- --- Name: idx_events_identity_fact_account; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_identity_fact_account ON public.events USING btree (connector_key, ((metadata ->> 'providerStableId'::text)), ((metadata ->> 'namespace'::text))) WHERE (semantic_type = 'identity_fact'::text); - --- --- Name: idx_events_identity_fact_lookup; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_identity_fact_lookup ON public.events USING btree (organization_id, ((metadata ->> 'namespace'::text)), ((metadata ->> 'normalizedValue'::text))) WHERE (semantic_type = 'identity_fact'::text); - --- --- Name: idx_events_lifecycle_changes; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_lifecycle_changes ON public.events USING btree (organization_id, created_at) WHERE ((semantic_type = 'change'::text) AND ((metadata ->> 'category'::text) = 'lifecycle'::text)); - --- --- Name: idx_events_metadata_auth_user_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_metadata_auth_user_id ON public.events USING btree (((metadata ->> 'auth_user_id'::text))) WHERE (metadata ? 'auth_user_id'::text); - --- --- Name: idx_events_metadata_email; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_metadata_email ON public.events USING btree (((metadata ->> 'email'::text))) WHERE (metadata ? 'email'::text); - --- --- Name: idx_events_metadata_github_login; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_metadata_github_login ON public.events USING btree (((metadata ->> 'github_login'::text))) WHERE (metadata ? 'github_login'::text); - --- --- Name: idx_events_metadata_google_contact_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_metadata_google_contact_id ON public.events USING btree (((metadata ->> 'google_contact_id'::text))) WHERE (metadata ? 'google_contact_id'::text); - --- --- Name: idx_events_metadata_phone; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_metadata_phone ON public.events USING btree (((metadata ->> 'phone'::text))) WHERE (metadata ? 'phone'::text); - --- --- Name: idx_events_metadata_slack_user_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_metadata_slack_user_id ON public.events USING btree (((metadata ->> 'slack_user_id'::text))) WHERE (metadata ? 'slack_user_id'::text); - --- --- Name: idx_events_metadata_wa_jid; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_metadata_wa_jid ON public.events USING btree (((metadata ->> 'wa_jid'::text))) WHERE (metadata ? 'wa_jid'::text); - --- --- Name: idx_events_missing_embedding_backfill; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_missing_embedding_backfill ON public.events USING btree (created_at, id) WHERE ((payload_text IS NOT NULL) AND (payload_text <> ''::text)); - --- --- Name: idx_events_organization_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_organization_id ON public.events USING btree (organization_id) WHERE (organization_id IS NOT NULL); - --- --- Name: idx_events_raw_content_trgm; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_raw_content_trgm ON public.events USING gin (payload_text public.gin_trgm_ops); - --- --- Name: idx_events_run_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_run_id ON public.events USING btree (run_id); - --- --- Name: idx_events_search_tsv; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_search_tsv ON public.events USING gin (search_tsv); - --- --- Name: idx_events_semantic_type; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_semantic_type ON public.events USING btree (semantic_type); - --- --- Name: idx_events_source_embedding; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_events_source_embedding ON public.event_embeddings USING btree (event_id); - --- --- Name: idx_events_superseded_by; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_events_superseded_by ON public.events USING btree (supersedes_event_id) WHERE (supersedes_event_id IS NOT NULL); - --- --- Name: idx_feeds_connection; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_feeds_connection ON public.feeds USING btree (connection_id); - --- --- Name: idx_feeds_deleted_at; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_feeds_deleted_at ON public.feeds USING btree (deleted_at) WHERE (deleted_at IS NULL); - --- --- Name: idx_feeds_entity_ids; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_feeds_entity_ids ON public.feeds USING gin (entity_ids); - --- --- Name: idx_feeds_next_run_at; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_feeds_next_run_at ON public.feeds USING btree (next_run_at) WHERE ((status = 'active'::text) AND (deleted_at IS NULL)); - --- --- Name: idx_feeds_org; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_feeds_org ON public.feeds USING btree (organization_id); - --- --- Name: idx_feeds_status; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_feeds_status ON public.feeds USING btree (status); - --- --- Name: idx_latest_ec_classifier_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_latest_ec_classifier_id ON public.latest_event_classifications USING btree (classifier_id); - --- --- Name: idx_latest_ec_event_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_latest_ec_event_id ON public.latest_event_classifications USING btree (event_id); - --- --- Name: idx_latest_ec_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_latest_ec_id ON public.latest_event_classifications USING btree (id); - --- --- Name: idx_latest_ec_source; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_latest_ec_source ON public.latest_event_classifications USING btree (source); - --- --- Name: idx_latest_ec_values_gin; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_latest_ec_values_gin ON public.latest_event_classifications USING gin ("values"); - --- --- Name: idx_notification_targets_user_all; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_notification_targets_user_all ON public.notification_targets USING btree (user_id, delivered_at DESC); - --- --- Name: idx_notification_targets_user_unread; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_notification_targets_user_unread ON public.notification_targets USING btree (user_id, delivered_at DESC) WHERE (read_at IS NULL); - --- --- Name: idx_pending_interactions_created_at; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_pending_interactions_created_at ON public.pending_interactions USING btree (created_at); - --- --- Name: idx_pending_interactions_unclaimed; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_pending_interactions_unclaimed ON public.pending_interactions USING btree (id, organization_id, connection_id, expected_user_id) WHERE (claimed_at IS NULL); - --- --- Name: idx_personal_access_tokens_worker_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_personal_access_tokens_worker_id ON public.personal_access_tokens USING btree (worker_id) WHERE (worker_id IS NOT NULL); - --- --- Name: idx_runs_active_auth_per_profile; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_runs_active_auth_per_profile ON public.runs USING btree (auth_profile_id) WHERE ((run_type = 'auth'::text) AND (auth_profile_id IS NOT NULL) AND (status = ANY (ARRAY['pending'::text, 'claimed'::text, 'running'::text]))); - --- --- Name: idx_runs_active_embed_backfill_per_org; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_runs_active_embed_backfill_per_org ON public.runs USING btree (organization_id) WHERE ((run_type = 'embed_backfill'::text) AND (status = ANY (ARRAY['pending'::text, 'claimed'::text, 'running'::text]))); - --- --- Name: idx_runs_active_sync_per_feed; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_runs_active_sync_per_feed ON public.runs USING btree (feed_id) WHERE ((run_type = 'sync'::text) AND (feed_id IS NOT NULL) AND (status = ANY (ARRAY['pending'::text, 'claimed'::text, 'running'::text]))); - --- --- Name: idx_runs_active_watcher_per_watcher; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_runs_active_watcher_per_watcher ON public.runs USING btree (watcher_id) WHERE ((run_type = 'watcher'::text) AND (watcher_id IS NOT NULL) AND (status = ANY (ARRAY['pending'::text, 'claimed'::text, 'running'::text]))); - --- --- Name: idx_runs_connection; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_runs_connection ON public.runs USING btree (connection_id); - --- --- Name: idx_runs_created_by_user; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_runs_created_by_user ON public.runs USING btree (created_by_user_id) WHERE (created_by_user_id IS NOT NULL); - --- --- Name: idx_runs_dispatched_message_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_runs_dispatched_message_id ON public.runs USING btree (dispatched_message_id) WHERE (dispatched_message_id IS NOT NULL); - --- --- Name: idx_runs_feed; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_runs_feed ON public.runs USING btree (feed_id); - --- --- Name: idx_runs_heartbeat_inflight; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_runs_heartbeat_inflight ON public.runs USING btree (last_heartbeat_at) WHERE ((status = ANY (ARRAY['claimed'::text, 'running'::text])) AND (run_type = ANY (ARRAY['sync'::text, 'action'::text, 'embed_backfill'::text, 'auth'::text]))); - --- --- Name: idx_runs_org; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_runs_org ON public.runs USING btree (organization_id); - --- --- Name: idx_runs_pending; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_runs_pending ON public.runs USING btree (status, created_at) WHERE (status = 'pending'::text); - --- --- Name: idx_runs_status; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_runs_status ON public.runs USING btree (status); - --- --- Name: idx_runs_type; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_runs_type ON public.runs USING btree (run_type); - --- --- Name: idx_runs_watcher_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_runs_watcher_id ON public.runs USING btree (watcher_id) WHERE (watcher_id IS NOT NULL); - --- --- Name: idx_scheduled_jobs_due; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_scheduled_jobs_due ON public.scheduled_jobs USING btree (next_run_at) WHERE (NOT paused); - --- --- Name: idx_scheduled_jobs_org_agent; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_scheduled_jobs_org_agent ON public.scheduled_jobs USING btree (organization_id, created_by_agent) WHERE (created_by_agent IS NOT NULL); - --- --- Name: idx_scheduled_jobs_org_user; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_scheduled_jobs_org_user ON public.scheduled_jobs USING btree (organization_id, created_by_user) WHERE (created_by_user IS NOT NULL); - --- --- Name: idx_view_template_versions_resource; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_view_template_versions_resource ON public.view_template_versions USING btree (resource_type, resource_id, organization_id); - --- --- Name: idx_watcher_reactions_org; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watcher_reactions_org ON public.watcher_reactions USING btree (organization_id); - --- --- Name: idx_watcher_reactions_window; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watcher_reactions_window ON public.watcher_reactions USING btree (watcher_id, window_id); - --- --- Name: idx_watcher_template_versions_created_by; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watcher_template_versions_created_by ON public.watcher_versions USING btree (created_by); - --- --- Name: idx_watcher_versions_watcher_version; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_watcher_versions_watcher_version ON public.watcher_versions USING btree (watcher_id, version); - --- --- Name: idx_watcher_window_events_event; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watcher_window_events_event ON public.watcher_window_events USING btree (event_id); - --- --- Name: idx_watcher_window_events_unique; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_watcher_window_events_unique ON public.watcher_window_events USING btree (window_id, event_id); - --- --- Name: idx_watcher_window_events_window; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watcher_window_events_window ON public.watcher_window_events USING btree (window_id); - --- --- Name: idx_watcher_windows_parent; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watcher_windows_parent ON public.watcher_windows USING btree (parent_window_id) WHERE (parent_window_id IS NOT NULL); - --- --- Name: idx_watcher_windows_run_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watcher_windows_run_id ON public.watcher_windows USING btree (run_id) WHERE (run_id IS NOT NULL); - --- --- Name: idx_watcher_windows_template_version; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watcher_windows_template_version ON public.watcher_windows USING btree (version_id); - --- --- Name: idx_watcher_windows_unique_period; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_watcher_windows_unique_period ON public.watcher_windows USING btree (watcher_id, window_start, window_end) WHERE (is_rollup = false); - --- --- Name: idx_watcher_windows_watcher; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watcher_windows_watcher ON public.watcher_windows USING btree (watcher_id, granularity, window_start DESC); - --- --- Name: idx_watchers_agent_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watchers_agent_id ON public.watchers USING btree (agent_id); - --- --- Name: idx_watchers_connection_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watchers_connection_id ON public.watchers USING btree (connection_id) WHERE (connection_id IS NOT NULL); - --- --- Name: idx_watchers_created_by; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watchers_created_by ON public.watchers USING btree (created_by); - --- --- Name: idx_watchers_device_worker_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watchers_device_worker_id ON public.watchers USING btree (device_worker_id) WHERE (device_worker_id IS NOT NULL); - --- --- Name: idx_watchers_entity_ids; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watchers_entity_ids ON public.watchers USING gin (entity_ids); - --- --- Name: idx_watchers_next_run_at; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watchers_next_run_at ON public.watchers USING btree (next_run_at) WHERE ((schedule IS NOT NULL) AND (status = 'active'::text)); - --- --- Name: idx_watchers_org_group; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watchers_org_group ON public.watchers USING btree (organization_id, watcher_group_id); - --- --- Name: idx_watchers_org_slug; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX idx_watchers_org_slug ON public.watchers USING btree (organization_id, slug) WHERE (slug IS NOT NULL); - --- --- Name: idx_watchers_organization_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watchers_organization_id ON public.watchers USING btree (organization_id) WHERE (organization_id IS NOT NULL); - --- --- Name: idx_watchers_watcher_group_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_watchers_watcher_group_id ON public.watchers USING btree (watcher_group_id); - --- --- Name: idx_wwff_watcher_field_recent; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_wwff_watcher_field_recent ON public.watcher_window_field_feedback USING btree (watcher_id, field_path, created_at DESC); - --- --- Name: idx_wwff_window; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX idx_wwff_window ON public.watcher_window_field_feedback USING btree (window_id); - --- --- Name: invitation_email_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX invitation_email_idx ON public.invitation USING btree (email); - --- --- Name: invitation_organizationId_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX "invitation_organizationId_idx" ON public.invitation USING btree ("organizationId"); - --- --- Name: mcp_proxy_sessions_expires_at_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX mcp_proxy_sessions_expires_at_idx ON public.mcp_proxy_sessions USING btree (expires_at); - --- --- Name: mcp_sessions_client_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX mcp_sessions_client_id_idx ON public.mcp_sessions USING btree (client_id); - --- --- Name: mcp_sessions_expires_at_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX mcp_sessions_expires_at_idx ON public.mcp_sessions USING btree (expires_at); - --- --- Name: mcp_sessions_user_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX mcp_sessions_user_id_idx ON public.mcp_sessions USING btree (user_id); - --- --- Name: member_organizationId_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX "member_organizationId_idx" ON public.member USING btree ("organizationId"); - --- --- Name: member_userId_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX "member_userId_idx" ON public.member USING btree ("userId"); - --- --- Name: namespace_ref_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX namespace_ref_id_idx ON public.namespace USING btree (ref_id); - --- --- Name: namespace_type_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX namespace_type_idx ON public.namespace USING btree (type); - --- --- Name: oauth_authorization_codes_client_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX oauth_authorization_codes_client_id_idx ON public.oauth_authorization_codes USING btree (client_id); - --- --- Name: oauth_authorization_codes_expires_at_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX oauth_authorization_codes_expires_at_idx ON public.oauth_authorization_codes USING btree (expires_at); - --- --- Name: oauth_clients_organization_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX oauth_clients_organization_id_idx ON public.oauth_clients USING btree (organization_id); - --- --- Name: oauth_clients_software_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX oauth_clients_software_id_idx ON public.oauth_clients USING btree (software_id); - --- --- Name: oauth_clients_user_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX oauth_clients_user_id_idx ON public.oauth_clients USING btree (user_id); - --- --- Name: oauth_device_codes_client_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX oauth_device_codes_client_id_idx ON public.oauth_device_codes USING btree (client_id); - --- --- Name: oauth_device_codes_expires_at_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX oauth_device_codes_expires_at_idx ON public.oauth_device_codes USING btree (expires_at); - --- --- Name: oauth_device_codes_user_code_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX oauth_device_codes_user_code_idx ON public.oauth_device_codes USING btree (user_code); - --- --- Name: oauth_states_expires_at_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX oauth_states_expires_at_idx ON public.oauth_states USING btree (expires_at); - --- --- Name: oauth_states_scope_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX oauth_states_scope_idx ON public.oauth_states USING btree (scope); - --- --- Name: oauth_tokens_active_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX oauth_tokens_active_idx ON public.oauth_tokens USING btree (user_id, expires_at) WHERE (revoked_at IS NULL); - --- --- Name: oauth_tokens_client_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX oauth_tokens_client_id_idx ON public.oauth_tokens USING btree (client_id); - --- --- Name: oauth_tokens_expires_at_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX oauth_tokens_expires_at_idx ON public.oauth_tokens USING btree (expires_at); - --- --- Name: oauth_tokens_token_hash_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX oauth_tokens_token_hash_idx ON public.oauth_tokens USING btree (token_hash); - --- --- Name: oauth_tokens_user_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX oauth_tokens_user_id_idx ON public.oauth_tokens USING btree (user_id); - --- --- Name: passkey_credential_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX passkey_credential_id_idx ON public.passkey USING btree ("credentialID"); - --- --- Name: passkey_user_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX passkey_user_id_idx ON public.passkey USING btree ("userId"); - --- --- Name: personal_access_tokens_active_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX personal_access_tokens_active_idx ON public.personal_access_tokens USING btree (user_id, expires_at) WHERE (revoked_at IS NULL); - --- --- Name: personal_access_tokens_organization_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX personal_access_tokens_organization_id_idx ON public.personal_access_tokens USING btree (organization_id); - --- --- Name: personal_access_tokens_token_hash_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX personal_access_tokens_token_hash_idx ON public.personal_access_tokens USING btree (token_hash); - --- --- Name: personal_access_tokens_token_prefix_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX personal_access_tokens_token_prefix_idx ON public.personal_access_tokens USING btree (token_prefix); - --- --- Name: personal_access_tokens_user_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX personal_access_tokens_user_id_idx ON public.personal_access_tokens USING btree (user_id); - --- --- Name: rate_limits_expires_at_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX rate_limits_expires_at_idx ON public.rate_limits USING btree (expires_at); - --- --- Name: revoked_tokens_expires_at_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX revoked_tokens_expires_at_idx ON public.revoked_tokens USING btree (expires_at); - --- --- Name: runs_expires_at_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX runs_expires_at_idx ON public.runs USING btree (expires_at) WHERE (expires_at IS NOT NULL); - --- --- Name: runs_idempotency_key_uniq; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX runs_idempotency_key_uniq ON public.runs USING btree (idempotency_key) WHERE ((idempotency_key IS NOT NULL) AND (status = ANY (ARRAY['pending'::text, 'claimed'::text, 'running'::text]))); - --- --- Name: runs_lobu_claim_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX runs_lobu_claim_idx ON public.runs USING btree (run_type, queue_name, priority DESC, run_at, id) WHERE ((status = 'pending'::text) AND (run_type = ANY (ARRAY['chat_message'::text, 'schedule'::text, 'agent_run'::text, 'internal'::text, 'task'::text]))); - --- --- Name: session_expiresAt_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX "session_expiresAt_idx" ON public.session USING btree ("expiresAt"); - --- --- Name: session_token_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX session_token_idx ON public.session USING btree (token); - --- --- Name: session_userId_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX "session_userId_idx" ON public.session USING btree ("userId"); - --- --- Name: user_auth_profiles_agent_id_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX user_auth_profiles_agent_id_idx ON public.user_auth_profiles USING btree (agent_id); - --- --- Name: user_username_unique; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX user_username_unique ON public."user" USING btree (username); - --- --- Name: verification_expiresAt_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX "verification_expiresAt_idx" ON public.verification USING btree ("expiresAt"); - --- --- Name: verification_identifier_idx; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX verification_identifier_idx ON public.verification USING btree (identifier); - --- --- Name: entities check_entity_cycles; Type: TRIGGER; Schema: public; Owner: - --- - -CREATE TRIGGER check_entity_cycles BEFORE INSERT OR UPDATE ON public.entities FOR EACH ROW EXECUTE FUNCTION public.prevent_entity_cycles(); - --- --- Name: account account_userId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.account - ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES public."user"(id) ON DELETE CASCADE; - --- --- Name: agent_channel_bindings agent_channel_bindings_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_channel_bindings - ADD CONSTRAINT agent_channel_bindings_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; - --- --- Name: agent_connections agent_connections_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_connections - ADD CONSTRAINT agent_connections_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; - --- --- Name: agent_grants agent_grants_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_grants - ADD CONSTRAINT agent_grants_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; - --- --- Name: agent_transcript_snapshot agent_transcript_snapshot_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_transcript_snapshot - ADD CONSTRAINT agent_transcript_snapshot_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: agent_transcript_snapshot agent_transcript_snapshot_run_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_transcript_snapshot - ADD CONSTRAINT agent_transcript_snapshot_run_id_fkey FOREIGN KEY (run_id) REFERENCES public.runs(id) ON DELETE CASCADE; - --- --- Name: agent_users agent_users_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agent_users - ADD CONSTRAINT agent_users_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; - --- --- Name: agents agents_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.agents - ADD CONSTRAINT agents_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: auth_profiles auth_profiles_account_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.auth_profiles - ADD CONSTRAINT auth_profiles_account_id_fkey FOREIGN KEY (account_id) REFERENCES public.account(id) ON DELETE SET NULL; - --- --- Name: auth_profiles auth_profiles_device_worker_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.auth_profiles - ADD CONSTRAINT auth_profiles_device_worker_id_fkey FOREIGN KEY (device_worker_id) REFERENCES public.device_workers(id) ON DELETE CASCADE; - --- --- Name: auth_profiles auth_profiles_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.auth_profiles - ADD CONSTRAINT auth_profiles_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: chat_user_identities chat_user_identities_lobu_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.chat_user_identities - ADD CONSTRAINT chat_user_identities_lobu_user_id_fkey FOREIGN KEY (lobu_user_id) REFERENCES public."user"(id) ON DELETE CASCADE; - --- --- Name: connect_tokens connect_tokens_auth_profile_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connect_tokens - ADD CONSTRAINT connect_tokens_auth_profile_id_fkey FOREIGN KEY (auth_profile_id) REFERENCES public.auth_profiles(id) ON DELETE SET NULL; - --- --- Name: connect_tokens connect_tokens_connection_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connect_tokens - ADD CONSTRAINT connect_tokens_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES public.connections(id) ON DELETE CASCADE; - --- --- Name: connections connections_account_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connections - ADD CONSTRAINT connections_account_id_fkey FOREIGN KEY (account_id) REFERENCES public.account(id) ON DELETE SET NULL; - --- --- Name: connections connections_app_auth_profile_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connections - ADD CONSTRAINT connections_app_auth_profile_id_fkey FOREIGN KEY (organization_id, app_auth_profile_id) REFERENCES public.auth_profiles(organization_id, id) ON DELETE SET NULL; - --- --- Name: connections connections_auth_profile_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connections - ADD CONSTRAINT connections_auth_profile_id_fkey FOREIGN KEY (organization_id, auth_profile_id) REFERENCES public.auth_profiles(organization_id, id) ON DELETE SET NULL; - --- --- Name: connections connections_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connections - ADD CONSTRAINT connections_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE SET NULL; - --- --- Name: connections connections_device_worker_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connections - ADD CONSTRAINT connections_device_worker_id_fkey FOREIGN KEY (device_worker_id) REFERENCES public.device_workers(id) ON DELETE SET NULL; - --- --- Name: connections connections_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connections - ADD CONSTRAINT connections_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: connector_definitions connector_definitions_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.connector_definitions - ADD CONSTRAINT connector_definitions_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: entities entities_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entities - ADD CONSTRAINT entities_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE RESTRICT; - --- --- Name: entities entities_entity_type_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entities - ADD CONSTRAINT entities_entity_type_id_fkey FOREIGN KEY (entity_type_id) REFERENCES public.entity_types(id); - --- --- Name: entities entities_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entities - ADD CONSTRAINT entities_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: entities entities_parent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entities - ADD CONSTRAINT entities_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES public.entities(id) ON DELETE RESTRICT; - --- --- Name: entities entities_view_template_version_fk; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entities - ADD CONSTRAINT entities_view_template_version_fk FOREIGN KEY (current_view_template_version_id) REFERENCES public.view_template_versions(id); - --- --- Name: entity_identities entity_identities_entity_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_identities - ADD CONSTRAINT entity_identities_entity_id_fkey FOREIGN KEY (entity_id) REFERENCES public.entities(id) ON DELETE CASCADE; - --- --- Name: entity_identities entity_identities_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_identities - ADD CONSTRAINT entity_identities_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: entity_relationship_type_rules entity_relationship_type_rules_relationship_type_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationship_type_rules - ADD CONSTRAINT entity_relationship_type_rules_relationship_type_id_fkey FOREIGN KEY (relationship_type_id) REFERENCES public.entity_relationship_types(id) ON DELETE CASCADE; - --- --- Name: entity_relationship_types entity_relationship_types_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationship_types - ADD CONSTRAINT entity_relationship_types_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE SET NULL; - --- --- Name: entity_relationship_types entity_relationship_types_inverse_type_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationship_types - ADD CONSTRAINT entity_relationship_types_inverse_type_id_fkey FOREIGN KEY (inverse_type_id) REFERENCES public.entity_relationship_types(id) ON DELETE SET NULL; - --- --- Name: entity_relationship_types entity_relationship_types_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationship_types - ADD CONSTRAINT entity_relationship_types_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: entity_relationships entity_relationships_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationships - ADD CONSTRAINT entity_relationships_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE SET NULL; - --- --- Name: entity_relationships entity_relationships_from_entity_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationships - ADD CONSTRAINT entity_relationships_from_entity_id_fkey FOREIGN KEY (from_entity_id) REFERENCES public.entities(id) ON DELETE CASCADE; - --- --- Name: entity_relationships entity_relationships_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationships - ADD CONSTRAINT entity_relationships_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: entity_relationships entity_relationships_relationship_type_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationships - ADD CONSTRAINT entity_relationships_relationship_type_id_fkey FOREIGN KEY (relationship_type_id) REFERENCES public.entity_relationship_types(id) ON DELETE CASCADE; - --- --- Name: entity_relationships entity_relationships_to_entity_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationships - ADD CONSTRAINT entity_relationships_to_entity_id_fkey FOREIGN KEY (to_entity_id) REFERENCES public.entities(id) ON DELETE CASCADE; - --- --- Name: entity_relationships entity_relationships_updated_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_relationships - ADD CONSTRAINT entity_relationships_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES public."user"(id) ON DELETE SET NULL; - --- --- Name: entity_type_audit entity_type_audit_actor_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_type_audit - ADD CONSTRAINT entity_type_audit_actor_fkey FOREIGN KEY (actor) REFERENCES public."user"(id) ON DELETE SET NULL; - --- --- Name: entity_type_audit entity_type_audit_entity_type_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_type_audit - ADD CONSTRAINT entity_type_audit_entity_type_id_fkey FOREIGN KEY (entity_type_id) REFERENCES public.entity_types(id) ON DELETE CASCADE; - --- --- Name: entity_types entity_types_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_types - ADD CONSTRAINT entity_types_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE SET NULL; - --- --- Name: entity_types entity_types_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_types - ADD CONSTRAINT entity_types_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: entity_types entity_types_updated_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_types - ADD CONSTRAINT entity_types_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES public."user"(id) ON DELETE SET NULL; - --- --- Name: entity_types entity_types_view_template_version_fk; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.entity_types - ADD CONSTRAINT entity_types_view_template_version_fk FOREIGN KEY (current_view_template_version_id) REFERENCES public.view_template_versions(id); - --- --- Name: event_classifications event_classifications_classifier_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifications - ADD CONSTRAINT event_classifications_classifier_id_fkey FOREIGN KEY (classifier_version_id) REFERENCES public.event_classifier_versions(id); - --- --- Name: event_classifications event_classifications_classifier_version_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifications - ADD CONSTRAINT event_classifications_classifier_version_id_fkey FOREIGN KEY (classifier_version_id) REFERENCES public.event_classifier_versions(id) ON DELETE RESTRICT; - --- --- Name: event_classifications event_classifications_event_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifications - ADD CONSTRAINT event_classifications_event_id_fkey FOREIGN KEY (event_id) REFERENCES public.events(id) ON DELETE CASCADE; - --- --- Name: event_classifications event_classifications_insight_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifications - ADD CONSTRAINT event_classifications_insight_id_fkey FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE CASCADE; - --- --- Name: event_classifications event_classifications_window_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifications - ADD CONSTRAINT event_classifications_window_id_fkey FOREIGN KEY (window_id) REFERENCES public.watcher_windows(id) ON DELETE CASCADE; - --- --- Name: event_classifier_versions event_classifier_versions_classifier_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifier_versions - ADD CONSTRAINT event_classifier_versions_classifier_id_fkey FOREIGN KEY (classifier_id) REFERENCES public.event_classifiers(id) ON DELETE CASCADE; - --- --- Name: event_classifier_versions event_classifier_versions_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifier_versions - ADD CONSTRAINT event_classifier_versions_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE RESTRICT; - --- --- Name: event_classifiers event_classifiers_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifiers - ADD CONSTRAINT event_classifiers_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE RESTRICT; - --- --- Name: event_classifiers event_classifiers_insight_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifiers - ADD CONSTRAINT event_classifiers_insight_id_fkey FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE CASCADE; - --- --- Name: event_classifiers event_classifiers_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifiers - ADD CONSTRAINT event_classifiers_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: event_embeddings event_embeddings_event_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_embeddings - ADD CONSTRAINT event_embeddings_event_id_fkey FOREIGN KEY (event_id) REFERENCES public.events(id) ON DELETE CASCADE; - --- --- Name: events events_client_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.events - ADD CONSTRAINT events_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.oauth_clients(id) ON DELETE SET NULL; - --- --- Name: events events_connection_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.events - ADD CONSTRAINT events_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES public.connections(id) ON DELETE SET NULL; - --- --- Name: events events_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.events - ADD CONSTRAINT events_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE SET NULL; - --- --- Name: events events_feed_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.events - ADD CONSTRAINT events_feed_id_fkey FOREIGN KEY (feed_id) REFERENCES public.feeds(id) ON DELETE SET NULL; - --- --- Name: events events_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.events - ADD CONSTRAINT events_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: events events_run_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.events - ADD CONSTRAINT events_run_id_fkey FOREIGN KEY (run_id) REFERENCES public.runs(id) ON DELETE SET NULL; - --- --- Name: events events_supersedes_event_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.events - ADD CONSTRAINT events_supersedes_event_id_fkey FOREIGN KEY (supersedes_event_id) REFERENCES public.events(id) ON DELETE SET NULL; - --- --- Name: feeds feeds_connection_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.feeds - ADD CONSTRAINT feeds_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES public.connections(id) ON DELETE CASCADE; - --- --- Name: feeds feeds_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.feeds - ADD CONSTRAINT feeds_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: event_classifiers fk_event_classifiers_entity; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifiers - ADD CONSTRAINT fk_event_classifiers_entity FOREIGN KEY (entity_id) REFERENCES public.entities(id) ON DELETE CASCADE; - --- --- Name: event_classifiers fk_event_classifiers_insight; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.event_classifiers - ADD CONSTRAINT fk_event_classifiers_insight FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE SET NULL; - --- --- Name: grants grants_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.grants - ADD CONSTRAINT grants_org_agent_fkey FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; - --- --- Name: watcher_versions insight_template_versions_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_versions - ADD CONSTRAINT insight_template_versions_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE RESTRICT; - --- --- Name: watcher_window_events insight_window_events_event_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_window_events - ADD CONSTRAINT insight_window_events_event_id_fkey FOREIGN KEY (event_id) REFERENCES public.events(id) ON DELETE CASCADE; - --- --- Name: watcher_window_events insight_window_events_window_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_window_events - ADD CONSTRAINT insight_window_events_window_id_fkey FOREIGN KEY (window_id) REFERENCES public.watcher_windows(id) ON DELETE CASCADE; - --- --- Name: watcher_windows insight_windows_insight_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_windows - ADD CONSTRAINT insight_windows_insight_id_fkey FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE CASCADE; - --- --- Name: watcher_windows insight_windows_parent_window_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_windows - ADD CONSTRAINT insight_windows_parent_window_id_fkey FOREIGN KEY (parent_window_id) REFERENCES public.watcher_windows(id) ON DELETE CASCADE; - --- --- Name: watchers insights_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watchers - ADD CONSTRAINT insights_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE RESTRICT; - --- --- Name: invitation invitation_inviterId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.invitation - ADD CONSTRAINT "invitation_inviterId_fkey" FOREIGN KEY ("inviterId") REFERENCES public."user"(id) ON DELETE SET NULL; - --- --- Name: invitation invitation_organizationId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.invitation - ADD CONSTRAINT "invitation_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: mcp_sessions mcp_sessions_client_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.mcp_sessions - ADD CONSTRAINT mcp_sessions_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.oauth_clients(id) ON DELETE CASCADE; - --- --- Name: mcp_sessions mcp_sessions_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.mcp_sessions - ADD CONSTRAINT mcp_sessions_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: mcp_sessions mcp_sessions_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.mcp_sessions - ADD CONSTRAINT mcp_sessions_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE; - --- --- Name: member member_organizationId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.member - ADD CONSTRAINT "member_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: member member_userId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.member - ADD CONSTRAINT "member_userId_fkey" FOREIGN KEY ("userId") REFERENCES public."user"(id) ON DELETE CASCADE; - --- --- Name: notification_targets notification_targets_event_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.notification_targets - ADD CONSTRAINT notification_targets_event_id_fkey FOREIGN KEY (event_id) REFERENCES public.events(id) ON DELETE CASCADE; - --- --- Name: oauth_authorization_codes oauth_authorization_codes_client_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_authorization_codes - ADD CONSTRAINT oauth_authorization_codes_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.oauth_clients(id) ON DELETE CASCADE; - --- --- Name: oauth_authorization_codes oauth_authorization_codes_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_authorization_codes - ADD CONSTRAINT oauth_authorization_codes_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE SET NULL; - --- --- Name: oauth_authorization_codes oauth_authorization_codes_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_authorization_codes - ADD CONSTRAINT oauth_authorization_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE; - --- --- Name: oauth_clients oauth_clients_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_clients - ADD CONSTRAINT oauth_clients_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: oauth_clients oauth_clients_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_clients - ADD CONSTRAINT oauth_clients_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE; - --- --- Name: oauth_device_codes oauth_device_codes_client_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_device_codes - ADD CONSTRAINT oauth_device_codes_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.oauth_clients(id) ON DELETE CASCADE; - --- --- Name: oauth_device_codes oauth_device_codes_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_device_codes - ADD CONSTRAINT oauth_device_codes_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE SET NULL; - --- --- Name: oauth_device_codes oauth_device_codes_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_device_codes - ADD CONSTRAINT oauth_device_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE; - --- --- Name: oauth_tokens oauth_tokens_client_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_tokens - ADD CONSTRAINT oauth_tokens_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.oauth_clients(id) ON DELETE CASCADE; - --- --- Name: oauth_tokens oauth_tokens_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_tokens - ADD CONSTRAINT oauth_tokens_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE SET NULL; - --- --- Name: oauth_tokens oauth_tokens_parent_token_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_tokens - ADD CONSTRAINT oauth_tokens_parent_token_id_fkey FOREIGN KEY (parent_token_id) REFERENCES public.oauth_tokens(id) ON DELETE SET NULL; - --- --- Name: oauth_tokens oauth_tokens_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.oauth_tokens - ADD CONSTRAINT oauth_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE; - --- --- Name: organization_lobu_links organization_lobu_links_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.organization_lobu_links - ADD CONSTRAINT organization_lobu_links_created_by_fkey FOREIGN KEY (created_by) REFERENCES public."user"(id) ON DELETE SET NULL; - --- --- Name: organization_lobu_links organization_lobu_links_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.organization_lobu_links - ADD CONSTRAINT organization_lobu_links_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: passkey passkey_userId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.passkey - ADD CONSTRAINT "passkey_userId_fkey" FOREIGN KEY ("userId") REFERENCES public."user"(id) ON DELETE CASCADE; - --- --- Name: pending_interactions pending_interactions_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.pending_interactions - ADD CONSTRAINT pending_interactions_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: personal_access_tokens personal_access_tokens_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.personal_access_tokens - ADD CONSTRAINT personal_access_tokens_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE SET NULL; - --- --- Name: personal_access_tokens personal_access_tokens_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.personal_access_tokens - ADD CONSTRAINT personal_access_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE; - --- --- Name: runs runs_auth_profile_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.runs - ADD CONSTRAINT runs_auth_profile_id_fkey FOREIGN KEY (auth_profile_id) REFERENCES public.auth_profiles(id) ON DELETE CASCADE; - --- --- Name: runs runs_connection_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.runs - ADD CONSTRAINT runs_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES public.connections(id) ON DELETE SET NULL; - --- --- Name: runs runs_created_by_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.runs - ADD CONSTRAINT runs_created_by_user_id_fkey FOREIGN KEY (created_by_user_id) REFERENCES public."user"(id) ON DELETE SET NULL; - --- --- Name: runs runs_feed_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.runs - ADD CONSTRAINT runs_feed_id_fkey FOREIGN KEY (feed_id) REFERENCES public.feeds(id) ON DELETE SET NULL; - --- --- Name: runs runs_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.runs - ADD CONSTRAINT runs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: runs runs_watcher_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.runs - ADD CONSTRAINT runs_watcher_id_fkey FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE SET NULL; - --- --- Name: runs runs_window_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.runs - ADD CONSTRAINT runs_window_id_fkey FOREIGN KEY (window_id) REFERENCES public.watcher_windows(id) ON DELETE SET NULL; - --- --- Name: scheduled_jobs scheduled_jobs_org_agent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.scheduled_jobs - ADD CONSTRAINT scheduled_jobs_org_agent_fkey FOREIGN KEY (organization_id, created_by_agent) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE; - --- --- Name: scheduled_jobs scheduled_jobs_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.scheduled_jobs - ADD CONSTRAINT scheduled_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- Name: scheduled_jobs scheduled_jobs_source_event_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.scheduled_jobs - ADD CONSTRAINT scheduled_jobs_source_event_fkey FOREIGN KEY (source_event_id) REFERENCES public.events(id) ON DELETE SET NULL; - --- --- Name: scheduled_jobs scheduled_jobs_source_run_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.scheduled_jobs - ADD CONSTRAINT scheduled_jobs_source_run_fkey FOREIGN KEY (source_run_id) REFERENCES public.runs(id) ON DELETE SET NULL; - --- --- Name: session session_activeOrganizationId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.session - ADD CONSTRAINT "session_activeOrganizationId_fkey" FOREIGN KEY ("activeOrganizationId") REFERENCES public.organization(id) ON DELETE SET NULL; - --- --- Name: session session_userId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.session - ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES public."user"(id) ON DELETE CASCADE; - --- --- Name: view_template_active_tabs view_template_active_tabs_version_fk; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.view_template_active_tabs - ADD CONSTRAINT view_template_active_tabs_version_fk FOREIGN KEY (current_version_id) REFERENCES public.view_template_versions(id); - --- --- Name: watcher_reactions watcher_reactions_watcher_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_reactions - ADD CONSTRAINT watcher_reactions_watcher_id_fkey FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE CASCADE; - --- --- Name: watcher_reactions watcher_reactions_window_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_reactions - ADD CONSTRAINT watcher_reactions_window_id_fkey FOREIGN KEY (window_id) REFERENCES public.watcher_windows(id) ON DELETE CASCADE; - --- --- Name: watcher_versions watcher_versions_watcher_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_versions - ADD CONSTRAINT watcher_versions_watcher_id_fkey FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE CASCADE; - --- --- Name: watcher_window_field_feedback watcher_window_field_feedback_watcher_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_window_field_feedback - ADD CONSTRAINT watcher_window_field_feedback_watcher_id_fkey FOREIGN KEY (watcher_id) REFERENCES public.watchers(id) ON DELETE CASCADE; - --- --- Name: watcher_window_field_feedback watcher_window_field_feedback_window_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_window_field_feedback - ADD CONSTRAINT watcher_window_field_feedback_window_id_fkey FOREIGN KEY (window_id) REFERENCES public.watcher_windows(id) ON DELETE CASCADE; - --- --- Name: watcher_windows watcher_windows_run_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_windows - ADD CONSTRAINT watcher_windows_run_id_fkey FOREIGN KEY (run_id) REFERENCES public.runs(id) ON DELETE SET NULL; - --- --- Name: watcher_windows watcher_windows_version_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watcher_windows - ADD CONSTRAINT watcher_windows_version_id_fkey FOREIGN KEY (version_id) REFERENCES public.watcher_versions(id); - --- --- Name: watchers watchers_current_version_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watchers - ADD CONSTRAINT watchers_current_version_id_fkey FOREIGN KEY (current_version_id) REFERENCES public.watcher_versions(id) ON DELETE SET NULL; - --- --- Name: watchers watchers_device_worker_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watchers - ADD CONSTRAINT watchers_device_worker_id_fkey FOREIGN KEY (device_worker_id) REFERENCES public.device_workers(id); - --- --- Name: watchers watchers_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.watchers - ADD CONSTRAINT watchers_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE; - --- --- PostgreSQL database dump complete --- - --- --- Dbmate schema migrations --- - -INSERT INTO public.schema_migrations (version) VALUES - ('00000000000000'), - ('20260405193000'), - ('20260408120000'), - ('20260408120001'), - ('20260409110000'), - ('20260409130000'), - ('20260410120000'), - ('20260413170000'), - ('20260416120000'), - ('20260417100000'), - ('20260418100000'), - ('20260418110000'), - ('20260419120000'), - ('20260420120000'), - ('20260424030000'), - ('20260424130000'), - ('20260425100000'), - ('20260425120000'), - ('20260425130000'), - ('20260426120000'), - ('20260426130000'), - ('20260426130001'), - ('20260427133000'), - ('20260427140000'), - ('20260427150000'), - ('20260427160000'), - ('20260427170000'), - ('20260428040000'), - ('20260428050000'), - ('20260429010000'), - ('20260429060000'), - ('20260429120000'), - ('20260429120100'), - ('20260429120200'), - ('20260429130000'), - ('20260429140000'), - ('20260429140100'), - ('20260429180000'), - ('20260430005614'), - ('20260430022231'), - ('20260430151215'), - ('20260501000000'), - ('20260501133000'), - ('20260502000000'), - ('20260503000000'), - ('20260504000000'), - ('20260510220000'), - ('20260512000000'), - ('20260512131703'), - ('20260513000000'), - ('20260513120000'), - ('20260513150000'), - ('20260513200000'), - ('20260514000000'), - ('20260514120000'), - ('20260514130000'), - ('20260514160000'), - ('20260515120000'), - ('20260515150000'), - ('20260515160000'), - ('20260515170000'), - ('20260516120000'), - ('20260516200000'), - ('20260516200100'), - ('20260517010000'), - ('20260517020000'), - ('20260517030000'), - ('20260517040000'), - ('20260517050000'), - ('20260517060000'), - ('20260517150000'), - ('20260517160000'), - ('20260518000000'), - ('20260518010000'), - ('20260518020000'), - ('20260518040000'), - ('20260518050000'), - ('20260518060000'), - ('20260518070000'), - ('20260519000000'), - ('20260519020000'), - ('20260519020001'); diff --git a/packages/server/src/__tests__/integration/embedded-schema-patches.test.ts b/packages/server/src/__tests__/integration/embedded-schema-patches.test.ts deleted file mode 100644 index c8f8ee36c..000000000 --- a/packages/server/src/__tests__/integration/embedded-schema-patches.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Regression test for the `scheduled-jobs` embedded schema patch. - * - * Issue lobu#787: on a real PGlite restart, the patch tries to re-add the - * single-column FK `scheduled_jobs_agent_fkey → agents(id)` even though the - * earlier `agents-per-org-pk-phase-c` patch has already swapped `agents` PK - * to the composite `(organization_id, id)` and replaced this FK with - * `scheduled_jobs_org_agent_fkey`. The second boot crashes with 42830 - * ("there is no unique constraint matching given keys for referenced table"). - * - * This test mirrors the boot sequence: migrations run (via the global setup), - * then EMBEDDED_SCHEMA_PATCHES run twice in a row against the same DB. The - * second pass must be a no-op — no crash, no constraint drift. - */ - -import { describe, expect, it } from 'vitest'; -import { EMBEDDED_SCHEMA_PATCHES } from '../../db/embedded-schema-patches'; -import { cleanupTestDatabase, getTestDb } from '../setup/test-db'; - -describe('embedded schema patches', () => { - it('are idempotent across two consecutive boots (regression: lobu#787)', async () => { - await cleanupTestDatabase(); - const sql = getTestDb(); - - // Migrations have already run via global-setup. The dbmate runner mirrors - // the patch DDL, so the constraint landscape post-migration matches what - // a long-lived embedded DB looks like after the first boot. - - // Sanity: after migrations, agents PK is composite (org, id) and - // scheduled_jobs has the composite FK, not the single-column one. - const constraintsBefore = (await sql.unsafe(` - SELECT conname FROM pg_constraint - WHERE conname IN ('scheduled_jobs_agent_fkey', 'scheduled_jobs_org_agent_fkey') - `)) as Array<{ conname: string }>; - const namesBefore = new Set(constraintsBefore.map((r) => r.conname)); - expect(namesBefore.has('scheduled_jobs_org_agent_fkey')).toBe(true); - expect(namesBefore.has('scheduled_jobs_agent_fkey')).toBe(false); - - // First post-migration run of embedded patches. With the fix, the - // scheduled-jobs patch detects the composite FK is already installed and - // skips re-adding the single-column FK. - for (const patch of EMBEDDED_SCHEMA_PATCHES) { - await patch.apply(sql as unknown as { unsafe: (...a: unknown[]) => Promise }); - } - - // Second run — simulates a server restart against the same DB. This is - // the path that crashes before the fix. - for (const patch of EMBEDDED_SCHEMA_PATCHES) { - await patch.apply(sql as unknown as { unsafe: (...a: unknown[]) => Promise }); - } - - // Post-condition: composite FK still in place, single-column FK absent, - // and agents PK is still the composite. - const constraintsAfter = (await sql.unsafe(` - SELECT conname FROM pg_constraint - WHERE conname IN ('scheduled_jobs_agent_fkey', 'scheduled_jobs_org_agent_fkey') - `)) as Array<{ conname: string }>; - const namesAfter = new Set(constraintsAfter.map((r) => r.conname)); - expect(namesAfter.has('scheduled_jobs_org_agent_fkey')).toBe(true); - expect(namesAfter.has('scheduled_jobs_agent_fkey')).toBe(false); - - const pkDef = (await sql.unsafe(` - SELECT pg_get_constraintdef(c.oid) AS def - FROM pg_constraint c - JOIN pg_class t ON t.oid = c.conrelid - JOIN pg_namespace n ON n.oid = t.relnamespace - WHERE n.nspname = 'public' - AND t.relname = 'agents' - AND c.contype = 'p' - LIMIT 1 - `)) as Array<{ def: string }>; - expect(pkDef[0]?.def ?? '').toMatch(/organization_id/); - expect(pkDef[0]?.def ?? '').toMatch(/\bid\b/); - }); -}); diff --git a/packages/server/src/__tests__/integration/identity/founder-to-member-migration.test.ts b/packages/server/src/__tests__/integration/identity/founder-to-member-migration.test.ts deleted file mode 100644 index 815fae55f..000000000 --- a/packages/server/src/__tests__/integration/identity/founder-to-member-migration.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Founder → $member migration replay test. - * - * Reads the committed SQL file, runs it against a fresh fixture state with a - * `market` org plus a couple of founder entities + cross-references, runs it - * a SECOND time to confirm idempotency, and verifies: - * - $member rows exist for each former founder - * - entity_identities rows that pointed at the founder now point at $member - * - events.entity_ids arrays no longer reference the soft-deleted founder - * - relationship-type rules accept `$member` as source/target where they - * previously accepted `founder` - * - source founder rows are soft-deleted - * - second run produces zero net changes (same row counts, same entity ids). - */ - -import { readFileSync } from "node:fs"; -import { join } from "node:path"; -import { beforeEach, describe, expect, it } from "vitest"; -import { cleanupTestDatabase, getTestDb } from "../../setup/test-db"; -import { - createTestEntity, - createTestOrganization, - createTestUser, -} from "../../setup/test-fixtures"; - -const MIGRATION_PATH = join( - __dirname, - "../../../../../../db/migrations/20260427170000_market_founder_to_member.sql", -); - -function loadMigrationUp(): string { - const raw = readFileSync(MIGRATION_PATH, "utf-8"); - // The dbmate file format has `-- migrate:up` and `-- migrate:down` markers. - const upStart = raw.indexOf("-- migrate:up"); - const downStart = raw.indexOf("-- migrate:down"); - if (upStart < 0 || downStart < 0) { - throw new Error( - `migration file missing up/down markers: ${MIGRATION_PATH}`, - ); - } - return raw.slice(upStart + "-- migrate:up".length, downStart).trim(); -} - -describe("founder→$member migration", () => { - beforeEach(async () => { - await cleanupTestDatabase(); - }); - - it("repoints identities, rewrites event entity_ids, and is idempotent on re-run", async () => { - const sql = getTestDb(); - const market = await createTestOrganization({ - name: "venture-capital", - visibility: "public", - }); - // Legacy installs used slug='venture-capital'; migration must still find them. - await sql`UPDATE organization SET slug = 'venture-capital' WHERE id = ${market.id}`; - const user = await createTestUser({ email: "op@market.test" }); - - const founder = await createTestEntity({ - name: "Old Founder", - entity_type: "founder", - organization_id: market.id, - created_by: user.id, - }); - await sql` - UPDATE entities SET metadata = ${sql.json({ - email: "founder@example.com", - linkedin_url: "linkedin.com/in/founder", - twitter_handle: "@founder", - })} - WHERE id = ${founder.id} - `; - - // Pre-existing identity row pointing at the founder (simulates an - // earlier provisioning script). Migration must repoint this. - await sql` - INSERT INTO entity_identities (organization_id, entity_id, namespace, identifier, source_connector) - VALUES (${market.id}, ${founder.id}, 'auth_user_id', 'preexisting_user_42', 'auth:signup') - `; - - // Pre-existing event referencing the founder in entity_ids. - const [historicalEvent] = await sql<{ id: number }[]>` - INSERT INTO events ( - organization_id, entity_ids, origin_id, semantic_type, payload_type, created_by - ) - VALUES ( - ${market.id}, ARRAY[${founder.id}::bigint], 'historical-1', 'note', 'text', ${user.id} - ) - RETURNING id - `; - - const sqlText = loadMigrationUp(); - await sql.unsafe(sqlText); - // Second run — must be a no-op for correctness. - await sql.unsafe(sqlText); - - const memberRows = await sql< - { id: number; metadata: Record }[] - >` - SELECT e.id, e.metadata - FROM entities e - JOIN entity_types et ON et.id = e.entity_type_id - WHERE e.organization_id = ${market.id} - AND et.slug = '$member' - AND e.deleted_at IS NULL - `; - expect(memberRows).toHaveLength(1); - const memberId = Number(memberRows[0].id); - expect(memberRows[0].metadata).toMatchObject({ - role: "founder", - migrated_from_founder_id: founder.id, - }); - - // Pre-existing identity row was repointed. - const repointedIdentity = await sql<{ entity_id: number }[]>` - SELECT entity_id FROM entity_identities - WHERE organization_id = ${market.id} - AND namespace = 'auth_user_id' - AND identifier = 'preexisting_user_42' - AND deleted_at IS NULL - `; - expect(repointedIdentity).toHaveLength(1); - expect(Number(repointedIdentity[0].entity_id)).toBe(memberId); - - // Migration also wrote new identity rows from founder metadata. - const writtenIdentities = await sql< - { namespace: string; entity_id: number }[] - >` - SELECT namespace, entity_id FROM entity_identities - WHERE organization_id = ${market.id} - AND source_connector = 'migration:founder_to_member' - AND deleted_at IS NULL - ORDER BY namespace - `; - expect(writtenIdentities.map((r) => r.namespace).sort()).toEqual([ - "email", - "linkedin_url", - "twitter_handle", - ]); - for (const r of writtenIdentities) { - expect(Number(r.entity_id)).toBe(memberId); - } - - // Historical event was rewritten. - const [eventAfter] = await sql<{ entity_ids: number[] }[]>` - SELECT entity_ids FROM events WHERE id = ${historicalEvent.id} - `; - expect(eventAfter.entity_ids.map(Number)).toEqual([memberId]); - - // Founder row is soft-deleted. - const [founderAfter] = await sql<{ deleted_at: string | null }[]>` - SELECT deleted_at FROM entities WHERE id = ${founder.id} - `; - expect(founderAfter.deleted_at).not.toBeNull(); - }); -}); diff --git a/packages/server/src/db/embedded-schema-patches.ts b/packages/server/src/db/embedded-schema-patches.ts deleted file mode 100644 index 897c34778..000000000 --- a/packages/server/src/db/embedded-schema-patches.ts +++ /dev/null @@ -1,1066 +0,0 @@ -/** - * Embedded (PGlite) schema patches. - * - * Already-initialized embedded/PGlite databases skip the dbmate migrations - * directory runner, so schema changes that ship as `db/migrations/*.sql` are - * mirrored here as idempotent ALTER/CREATE statements and replayed on every - * boot. Pure data — the runner lives in `start-local.ts`. - */ - -export type MigrationSqlClient = { - unsafe: (...args: any[]) => Promise; -}; - -interface EmbeddedSchemaPatch { - id: string; - apply: (sql: MigrationSqlClient) => Promise; -} - -export const EMBEDDED_SCHEMA_PATCHES: EmbeddedSchemaPatch[] = [ - { - id: 'feeds-display-name', - apply: async (sql) => { - await sql.unsafe(` - ALTER TABLE public.feeds - ADD COLUMN IF NOT EXISTS display_name text - `); - }, - }, - { - id: 'watcher-run-correlation', - apply: async (sql) => { - await sql.unsafe(` - ALTER TABLE public.runs - ADD COLUMN IF NOT EXISTS dispatched_message_id text - `); - await sql.unsafe(` - CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_dispatched_message_id - ON public.runs (dispatched_message_id) - WHERE dispatched_message_id IS NOT NULL - `); - await sql.unsafe(` - ALTER TABLE public.watcher_windows - ADD COLUMN IF NOT EXISTS run_id bigint - REFERENCES public.runs(id) ON DELETE SET NULL - `); - await sql.unsafe(` - WITH correlated_windows AS ( - SELECT ww.id, - (btrim(ww.run_metadata->>'watcher_run_id'))::bigint AS correlated_run_id - FROM public.watcher_windows ww - WHERE ww.run_id IS NULL - AND ww.run_metadata ? 'watcher_run_id' - AND jsonb_typeof(ww.run_metadata->'watcher_run_id') IN ('number', 'string') - AND btrim(ww.run_metadata->>'watcher_run_id') ~ '^[0-9]+$' - ) - UPDATE public.watcher_windows ww - SET run_id = cw.correlated_run_id - FROM correlated_windows cw - WHERE ww.id = cw.id - AND EXISTS ( - SELECT 1 - FROM public.runs r - WHERE r.id = cw.correlated_run_id - AND r.run_type = 'watcher' - ) - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS idx_watcher_windows_run_id - ON public.watcher_windows (run_id) - WHERE run_id IS NOT NULL - `); - }, - }, - { - id: 'mcp-sessions-table', - apply: async (sql) => { - await sql.unsafe(` - CREATE TABLE IF NOT EXISTS public.mcp_sessions ( - session_id text PRIMARY KEY, - user_id text, - client_id text, - organization_id text, - member_role text, - requested_agent_id text, - is_authenticated boolean DEFAULT false NOT NULL, - scoped_to_org boolean DEFAULT false NOT NULL, - last_accessed_at timestamp with time zone DEFAULT now() NOT NULL, - expires_at timestamp with time zone NOT NULL - ) - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS mcp_sessions_client_id_idx - ON public.mcp_sessions USING btree (client_id) - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS mcp_sessions_expires_at_idx - ON public.mcp_sessions USING btree (expires_at) - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS mcp_sessions_user_id_idx - ON public.mcp_sessions USING btree (user_id) - `); - await sql.unsafe(` - ALTER TABLE public.mcp_sessions - DROP CONSTRAINT IF EXISTS mcp_sessions_client_id_fkey - `); - await sql.unsafe(` - ALTER TABLE public.mcp_sessions - ADD CONSTRAINT mcp_sessions_client_id_fkey - FOREIGN KEY (client_id) REFERENCES public.oauth_clients(id) ON DELETE CASCADE - `); - await sql.unsafe(` - ALTER TABLE public.mcp_sessions - DROP CONSTRAINT IF EXISTS mcp_sessions_organization_id_fkey - `); - await sql.unsafe(` - ALTER TABLE public.mcp_sessions - ADD CONSTRAINT mcp_sessions_organization_id_fkey - FOREIGN KEY (organization_id) REFERENCES public.organization(id) ON DELETE CASCADE - `); - await sql.unsafe(` - ALTER TABLE public.mcp_sessions - DROP CONSTRAINT IF EXISTS mcp_sessions_user_id_fkey - `); - await sql.unsafe(` - ALTER TABLE public.mcp_sessions - ADD CONSTRAINT mcp_sessions_user_id_fkey - FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE - `); - }, - }, - { - id: 'agent-secrets-org-scope', - apply: async (sql) => { - // Mirror of db/migrations/20260503000000_agent_secrets_org_scope.sql - // for already-initialized PGlite installs that skip the migrations - // dir runner. - await sql.unsafe(` - ALTER TABLE public.agent_secrets - ADD COLUMN IF NOT EXISTS organization_id text NOT NULL DEFAULT '' - `); - await sql.unsafe(` - ALTER TABLE public.agent_secrets - DROP CONSTRAINT IF EXISTS agent_secrets_pkey - `); - await sql.unsafe(` - ALTER TABLE public.agent_secrets - ADD CONSTRAINT agent_secrets_pkey PRIMARY KEY (organization_id, name) - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS agent_secrets_org_id_idx - ON public.agent_secrets (organization_id) - `); - }, - }, - { - id: 'drop-chat-connections', - apply: async (sql) => { - // Mirror of db/migrations/20260502000000_drop_chat_connections.sql - // for already-initialized PGlite installs that skip the migrations - // dir runner. ChatInstanceManager now reads/writes agent_connections - // directly. Copy any rows from chat_connections (if it exists) into - // agent_connections, then drop the legacy table. INSERT runs only - // when chat_connections is present so fresh PGlite installs (which - // never had the table) skip cleanly. - await sql.unsafe(` - DO $$ - BEGIN - IF EXISTS ( - SELECT 1 FROM pg_tables - WHERE schemaname = 'public' AND tablename = 'chat_connections' - ) THEN - INSERT INTO public.agent_connections ( - id, agent_id, platform, config, settings, metadata, - status, error_message, created_at, updated_at - ) - SELECT - id, template_agent_id, platform, config, settings, metadata, - status, error_message, created_at, updated_at - FROM public.chat_connections - WHERE template_agent_id IS NOT NULL - ON CONFLICT (id) DO NOTHING; - - DROP TABLE public.chat_connections; - END IF; - END $$; - `); - }, - }, - { - id: 'connector-required-capability', - apply: async (sql) => { - await sql.unsafe(` - ALTER TABLE public.connector_definitions - ADD COLUMN IF NOT EXISTS required_capability text - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS connector_definitions_required_capability_idx - ON public.connector_definitions (required_capability) - WHERE required_capability IS NOT NULL - `); - }, - }, - { - id: 'connector-runtime', - apply: async (sql) => { - // `runtime` carries platform metadata for device-bound connectors - // (e.g. apple.screen_time / local.directory, which run inside the Lobu - // Lobu for Mac). NULL = embedded server. - await sql.unsafe(` - ALTER TABLE public.connector_definitions - ADD COLUMN IF NOT EXISTS runtime jsonb - `); - }, - }, - { - id: 'device-workers', - apply: async (sql) => { - await sql.unsafe(` - CREATE TABLE IF NOT EXISTS public.device_workers ( - user_id text NOT NULL, - worker_id text NOT NULL, - platform text, - app_version text, - capabilities jsonb NOT NULL DEFAULT '[]'::jsonb, - label text, - first_seen_at timestamptz NOT NULL DEFAULT now(), - last_seen_at timestamptz NOT NULL DEFAULT now(), - PRIMARY KEY (user_id, worker_id) - ) - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS device_workers_user_id_idx - ON public.device_workers (user_id) - `); - }, - }, - { - // Mirrors db/migrations/20260512000000_device_worker_connection_binding.sql - // for already-initialized embedded/PGlite databases (where dbmate - // migrations don't run). - id: 'device-worker-connection-binding', - apply: async (sql) => { - await sql.unsafe(` - ALTER TABLE public.device_workers - ADD COLUMN IF NOT EXISTS id uuid NOT NULL DEFAULT gen_random_uuid() - `); - await sql.unsafe(` - ALTER TABLE public.device_workers - ADD COLUMN IF NOT EXISTS organization_id text - `); - await sql.unsafe(` - CREATE UNIQUE INDEX IF NOT EXISTS device_workers_id_key - ON public.device_workers (id) - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS idx_device_workers_organization_id - ON public.device_workers (organization_id) - WHERE organization_id IS NOT NULL - `); - await sql.unsafe(` - ALTER TABLE public.connections - ADD COLUMN IF NOT EXISTS device_worker_id uuid - `); - await sql.unsafe(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'connections_device_worker_id_fkey' - ) THEN - ALTER TABLE public.connections - ADD CONSTRAINT connections_device_worker_id_fkey - FOREIGN KEY (device_worker_id) - REFERENCES public.device_workers (id) - ON DELETE SET NULL; - END IF; - END $$; - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS idx_connections_device_worker_id - ON public.connections (device_worker_id) - WHERE device_worker_id IS NOT NULL - `); - await sql.unsafe(` - CREATE UNIQUE INDEX IF NOT EXISTS idx_connections_org_connector_device_live - ON public.connections (organization_id, connector_key, device_worker_id) - WHERE deleted_at IS NULL AND device_worker_id IS NOT NULL - `); - // Older embedded DBs may have the dropped device_worker_org_grants table. - await sql.unsafe(`DROP TABLE IF EXISTS public.device_worker_org_grants`); - }, - }, - { - // Mirrors db/migrations/20260513120000_auth_profiles_device_binding.sql. - // Lets a 'browser_session' auth_profile live on a device worker (cookies on - // disk in user_data_dir, auth_data empty) instead of in server-side - // auth_data jsonb. - id: 'auth-profiles-device-binding', - apply: async (sql) => { - await sql.unsafe(` - ALTER TABLE public.auth_profiles - ADD COLUMN IF NOT EXISTS device_worker_id uuid - `); - await sql.unsafe(` - ALTER TABLE public.auth_profiles - ADD COLUMN IF NOT EXISTS browser_kind text - `); - await sql.unsafe(` - ALTER TABLE public.auth_profiles - ADD COLUMN IF NOT EXISTS user_data_dir text - `); - await sql.unsafe(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'auth_profiles_device_worker_id_fkey' - ) THEN - ALTER TABLE public.auth_profiles - ADD CONSTRAINT auth_profiles_device_worker_id_fkey - FOREIGN KEY (device_worker_id) - REFERENCES public.device_workers (id) - ON DELETE CASCADE; - END IF; - END $$; - `); - await sql.unsafe(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'auth_profiles_browser_kind_check' - ) THEN - ALTER TABLE public.auth_profiles - ADD CONSTRAINT auth_profiles_browser_kind_check - CHECK (browser_kind IS NULL OR browser_kind = ANY (ARRAY['chrome','brave','arc','edge'])); - END IF; - END $$; - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS auth_profiles_device_worker_idx - ON public.auth_profiles (device_worker_id) - WHERE device_worker_id IS NOT NULL - `); - }, - }, - { - // Mirrors db/migrations/20260513150000_auth_profiles_cdp_url.sql - id: 'auth-profiles-cdp-url', - apply: async (sql) => { - await sql.unsafe(` - ALTER TABLE public.auth_profiles - ADD COLUMN IF NOT EXISTS cdp_url text - `); - await sql.unsafe(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'auth_profiles_device_browser_path_xor' - ) THEN - ALTER TABLE public.auth_profiles - ADD CONSTRAINT auth_profiles_device_browser_path_xor - CHECK ( - device_worker_id IS NULL - OR profile_kind <> 'browser_session' - OR ( - (user_data_dir IS NOT NULL AND cdp_url IS NULL) - OR (user_data_dir IS NULL AND cdp_url IS NOT NULL) - ) - ); - END IF; - END $$; - `); - }, - }, - { - // Mirrors db/migrations/20260513200000_notifications_as_events.sql. - // Idempotent: only migrates rows from `notifications` if the table still - // exists; subsequent boots no-op. - id: 'notifications-as-events', - apply: async (sql) => { - await sql.unsafe(` - CREATE TABLE IF NOT EXISTS public.notification_targets ( - event_id bigint NOT NULL REFERENCES public.events(id) ON DELETE CASCADE, - user_id text NOT NULL, - delivered_at timestamp with time zone NOT NULL DEFAULT now(), - read_at timestamp with time zone, - PRIMARY KEY (event_id, user_id) - ) - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS idx_notification_targets_user_unread - ON public.notification_targets (user_id, delivered_at DESC) - WHERE read_at IS NULL - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS idx_notification_targets_user_all - ON public.notification_targets (user_id, delivered_at DESC) - `); - // Backfill from the legacy table if it still exists. - const legacyExists = (await sql.unsafe( - `SELECT to_regclass('public.notifications') IS NOT NULL AS exists` - )) as Array<{ exists: boolean }>; - if (legacyExists[0]?.exists) { - await sql.unsafe(` - WITH legacy AS ( - SELECT id, organization_id, user_id, type, title, body, - resource_type, resource_id, resource_url, is_read, created_at - FROM public.notifications - ORDER BY id ASC - ), - inserted AS ( - INSERT INTO public.events - (organization_id, title, payload_text, payload_type, semantic_type, - occurred_at, created_at, metadata, origin_id) - SELECT - l.organization_id, l.title, l.body, 'text', 'notification', - l.created_at, l.created_at, - jsonb_build_object( - 'notification_type', l.type, - 'resource_type', l.resource_type, - 'resource_id', l.resource_id, - 'resource_url', l.resource_url, - 'legacy_notification_id', l.id - ), - 'notification:legacy:' || l.id::text - FROM legacy l - RETURNING id AS event_id, - (metadata->>'legacy_notification_id')::bigint AS legacy_id - ) - INSERT INTO public.notification_targets (event_id, user_id, delivered_at, read_at) - SELECT i.event_id, l.user_id, l.created_at, - CASE WHEN l.is_read THEN l.created_at ELSE NULL END - FROM inserted i - JOIN public.notifications l ON l.id = i.legacy_id - `); - await sql.unsafe(`DROP TABLE public.notifications`); - } - }, - }, - { - // Mirrors db/migrations/20260514000000_scheduled_jobs.sql. - id: 'scheduled-jobs', - apply: async (sql) => { - await sql.unsafe(` - CREATE TABLE IF NOT EXISTS public.scheduled_jobs ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - organization_id text NOT NULL REFERENCES public.organization(id) ON DELETE CASCADE, - action_type text NOT NULL, - action_args jsonb NOT NULL, - cron text, - next_run_at timestamp with time zone NOT NULL, - last_fired_at timestamp with time zone, - last_fired_run_id bigint, - paused boolean NOT NULL DEFAULT false, - description text NOT NULL, - created_by_user text, - created_by_agent text, - source_run_id bigint, - source_event_id bigint, - source_thread_id text, - created_at timestamp with time zone NOT NULL DEFAULT now(), - updated_at timestamp with time zone NOT NULL DEFAULT now(), - CONSTRAINT scheduled_jobs_attribution_check CHECK ( - created_by_user IS NOT NULL OR created_by_agent IS NOT NULL - ) - ) - `); - // Add the single-column FK only when `agents.id` still has a unique - // constraint to reference. The later `agents-per-org-pk-phase-c` patch - // swaps the PK to composite `(organization_id, id)` and installs - // `scheduled_jobs_org_agent_fkey` in place of this one — after that - // swap, re-adding the single-column FK fails with 42830 ("no unique - // constraint matching given keys for referenced table"). Skip when the - // composite FK is already present *or* when no single-column unique - // exists on agents(id). Issue lobu#787. - await sql.unsafe(` - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'agents' AND relkind = 'r') THEN - RETURN; - END IF; - IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'scheduled_jobs_agent_fkey') THEN - RETURN; - END IF; - IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'scheduled_jobs_org_agent_fkey') THEN - -- Composite FK already installed by the later phase-c patch. - RETURN; - END IF; - -- Confirm a single-column unique/PK exists on agents(id) before - -- referencing it; otherwise the ADD CONSTRAINT will crash with 42830. - IF NOT EXISTS ( - SELECT 1 - FROM pg_constraint c - JOIN pg_class t ON t.oid = c.conrelid - JOIN pg_namespace n ON n.oid = t.relnamespace - JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY (c.conkey) - WHERE n.nspname = 'public' - AND t.relname = 'agents' - AND c.contype IN ('p', 'u') - AND array_length(c.conkey, 1) = 1 - AND a.attname = 'id' - ) THEN - RETURN; - END IF; - ALTER TABLE public.scheduled_jobs - ADD CONSTRAINT scheduled_jobs_agent_fkey - FOREIGN KEY (created_by_agent) REFERENCES public.agents(id) ON DELETE CASCADE; - END$$; - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS idx_scheduled_jobs_due - ON public.scheduled_jobs (next_run_at) WHERE NOT paused - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS idx_scheduled_jobs_org_agent - ON public.scheduled_jobs (organization_id, created_by_agent) - WHERE created_by_agent IS NOT NULL - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS idx_scheduled_jobs_org_user - ON public.scheduled_jobs (organization_id, created_by_user) - WHERE created_by_user IS NOT NULL - `); - }, - }, - { - // Mirrors db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql. - // Drops NOT NULL on auth_profiles.connector_key so browser_session profiles - // (a device-bound resource) no longer require a per-connector binding. - id: 'auth-profiles-connector-key-nullable', - apply: async (sql) => { - await sql.unsafe(` - ALTER TABLE public.auth_profiles - ALTER COLUMN connector_key DROP NOT NULL - `); - await sql.unsafe(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'auth_profiles_connector_key_required' - ) THEN - ALTER TABLE public.auth_profiles - ADD CONSTRAINT auth_profiles_connector_key_required - CHECK ( - connector_key IS NOT NULL - OR profile_kind = 'browser_session' - ); - END IF; - END $$; - `); - }, - }, - { - // Mirrors db/migrations/20260515120000_agents_per_org_pk.sql — phase A only. - // Adds organization_id (NULLABLE) to the 5 FK-holding child tables, backfills - // from agents, adds a parallel UNIQUE (organization_id, id) on agents, and - // creates composite indexes for upcoming org-scoped queries. The PK swap - // and FK composite migration ship in a later phase once the storage - // interfaces are plumbed with organization_id everywhere. - id: 'agents-per-org-pk-phase-a', - apply: async (sql) => { - for (const t of [ - 'agent_grants', - 'agent_connections', - 'agent_users', - 'agent_channel_bindings', - 'grants', - ]) { - await sql.unsafe(` - ALTER TABLE public.${t} - ADD COLUMN IF NOT EXISTS organization_id text - `); - await sql.unsafe(` - UPDATE public.${t} c - SET organization_id = a.organization_id - FROM public.agents a - WHERE c.agent_id = a.id AND c.organization_id IS NULL - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS ${t}_org_agent_idx - ON public.${t} (organization_id, agent_id) - `); - } - // Note: an earlier revision of this patch added a parallel UNIQUE - // (organization_id, id) on agents. It was reverted in - // db/migrations/20260515160000_drop_agents_org_id_unique.sql because - // it broke `ON CONFLICT (id) DO NOTHING/UPDATE` callers — Postgres - // can violate the new constraint before reaching the PK conflict, - // and ON CONFLICT (id) only suppresses the named constraint. The - // PK on (id) already enforces global uniqueness. Phase C of the - // per-org PK migration will swap the PK directly. - }, - }, - { - // Mirrors db/migrations/20260515160000_drop_agents_org_id_unique.sql. - id: 'drop-agents-org-id-unique', - apply: async (sql) => { - await sql.unsafe( - `ALTER TABLE public.agents DROP CONSTRAINT IF EXISTS agents_organization_id_id_key` - ); - }, - }, - { - // Mirrors db/migrations/20260516120000_agents_per_org_pk_swap.sql. - // Detects whether the swap has already happened by reading the current - // PK definition on `agents`; skips silently when the composite PK is - // already in place. - id: 'agents-per-org-pk-phase-c', - apply: async (sql) => { - const pkDef = (await sql.unsafe(` - SELECT pg_get_constraintdef(c.oid) AS def - FROM pg_constraint c - JOIN pg_class t ON t.oid = c.conrelid - JOIN pg_namespace n ON n.oid = t.relnamespace - WHERE n.nspname = 'public' - AND t.relname = 'agents' - AND c.contype = 'p' - LIMIT 1 - `)) as Array<{ def: string }>; - const def = pkDef[0]?.def ?? ''; - if (def.includes('organization_id') && def.includes('id')) { - // Composite PK already in place — nothing to do. - return; - } - - // Backfill any stragglers and drop orphans. - for (const t of [ - 'agent_grants', - 'agent_connections', - 'agent_users', - 'agent_channel_bindings', - 'grants', - ]) { - await sql.unsafe(` - UPDATE public.${t} c - SET organization_id = a.organization_id - FROM public.agents a - WHERE c.organization_id IS NULL AND c.agent_id = a.id - `); - await sql.unsafe(` - DELETE FROM public.${t} WHERE organization_id IS NULL - `); - await sql.unsafe(` - ALTER TABLE public.${t} ALTER COLUMN organization_id SET NOT NULL - `); - } - - // Drop legacy single-column FKs. - await sql.unsafe( - `ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_agent_id_fkey` - ); - await sql.unsafe( - `ALTER TABLE public.agent_connections DROP CONSTRAINT IF EXISTS agent_connections_agent_id_fkey` - ); - await sql.unsafe( - `ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_agent_id_fkey` - ); - await sql.unsafe( - `ALTER TABLE public.agent_channel_bindings DROP CONSTRAINT IF EXISTS agent_channel_bindings_agent_id_fkey` - ); - await sql.unsafe( - `ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_agent_id_fkey` - ); - await sql.unsafe( - `ALTER TABLE public.scheduled_jobs DROP CONSTRAINT IF EXISTS scheduled_jobs_agent_fkey` - ); - - // Drop legacy uniques/PKs scoped to bare agent_id. - await sql.unsafe( - `ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_agent_id_pattern_key` - ); - await sql.unsafe( - `ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_pkey` - ); - await sql.unsafe( - `ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_pkey` - ); - - // Swap PK on agents. - await sql.unsafe(`ALTER TABLE public.agents DROP CONSTRAINT IF EXISTS agents_pkey`); - await sql.unsafe( - `ALTER TABLE public.agents ADD CONSTRAINT agents_pkey PRIMARY KEY (organization_id, id)` - ); - - // Re-add per-org-scoped uniques. - await sql.unsafe(` - ALTER TABLE public.agent_grants - ADD CONSTRAINT agent_grants_org_agent_pattern_key UNIQUE (organization_id, agent_id, pattern) - `); - await sql.unsafe(` - ALTER TABLE public.agent_users - ADD CONSTRAINT agent_users_pkey PRIMARY KEY (organization_id, agent_id, platform, user_id) - `); - await sql.unsafe(` - ALTER TABLE public.grants - ADD CONSTRAINT grants_pkey PRIMARY KEY (organization_id, agent_id, kind, pattern) - `); - - // Re-add composite FKs into agents(organization_id, id). - await sql.unsafe(` - ALTER TABLE public.agent_grants - ADD CONSTRAINT agent_grants_org_agent_fkey - FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE - `); - await sql.unsafe(` - ALTER TABLE public.agent_connections - ADD CONSTRAINT agent_connections_org_agent_fkey - FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE - `); - await sql.unsafe(` - ALTER TABLE public.agent_users - ADD CONSTRAINT agent_users_org_agent_fkey - FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE - `); - await sql.unsafe(` - ALTER TABLE public.agent_channel_bindings - ADD CONSTRAINT agent_channel_bindings_org_agent_fkey - FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE - `); - await sql.unsafe(` - ALTER TABLE public.grants - ADD CONSTRAINT grants_org_agent_fkey - FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE - `); - await sql.unsafe(` - ALTER TABLE public.scheduled_jobs - ADD CONSTRAINT scheduled_jobs_org_agent_fkey - FOREIGN KEY (organization_id, created_by_agent) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE - `); - }, - }, - { - // Mirrors db/migrations/20260517060000_watcher_schema_additions.sql. - // Adds dispatcher-related columns to watchers (device_worker_id, - // agent_kind, notification_channel, notification_priority, - // min_cooldown_seconds, last_fired_at) plus the per-device daily - // notification budget on device_workers. Idempotent for replay on - // already-initialised embedded/PGlite databases — each ADD COLUMN is - // wrapped in a duplicate_column-tolerant DO block; constraint and index - // creation is gated on pg_constraint / IF NOT EXISTS. - id: 'watcher-schema-additions', - apply: async (sql) => { - const watcherColumns: Array<{ name: string; ddl: string }> = [ - { - name: 'device_worker_id', - ddl: `ALTER TABLE public.watchers - ADD COLUMN device_worker_id uuid REFERENCES public.device_workers(id)`, - }, - { - name: 'agent_kind', - ddl: `ALTER TABLE public.watchers ADD COLUMN agent_kind text`, - }, - { - name: 'notification_channel', - ddl: `ALTER TABLE public.watchers - ADD COLUMN notification_channel text NOT NULL DEFAULT 'canvas'`, - }, - { - name: 'notification_priority', - ddl: `ALTER TABLE public.watchers - ADD COLUMN notification_priority text NOT NULL DEFAULT 'normal'`, - }, - { - name: 'min_cooldown_seconds', - ddl: `ALTER TABLE public.watchers - ADD COLUMN min_cooldown_seconds integer NOT NULL DEFAULT 0`, - }, - { - name: 'last_fired_at', - ddl: `ALTER TABLE public.watchers - ADD COLUMN last_fired_at timestamp with time zone`, - }, - ]; - for (const col of watcherColumns) { - await sql.unsafe(` - DO $$ - BEGIN - ${col.ddl}; - EXCEPTION WHEN duplicate_column THEN NULL; - END $$; - `); - } - - await sql.unsafe(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'watchers_notification_channel_check' - ) THEN - ALTER TABLE public.watchers - ADD CONSTRAINT watchers_notification_channel_check - CHECK (notification_channel IN ('canvas', 'notification', 'both')); - END IF; - END $$; - `); - await sql.unsafe(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'watchers_notification_priority_check' - ) THEN - ALTER TABLE public.watchers - ADD CONSTRAINT watchers_notification_priority_check - CHECK (notification_priority IN ('low', 'normal', 'high')); - END IF; - END $$; - `); - await sql.unsafe(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'watchers_min_cooldown_seconds_nonneg' - ) THEN - ALTER TABLE public.watchers - ADD CONSTRAINT watchers_min_cooldown_seconds_nonneg - CHECK (min_cooldown_seconds >= 0); - END IF; - END $$; - `); - - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS idx_watchers_device_worker_id - ON public.watchers (device_worker_id) - WHERE device_worker_id IS NOT NULL - `); - - await sql.unsafe(` - DO $$ - BEGIN - ALTER TABLE public.device_workers - ADD COLUMN notification_budget_per_day integer NOT NULL DEFAULT 10; - EXCEPTION WHEN duplicate_column THEN NULL; - END $$; - `); - await sql.unsafe(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint - WHERE conname = 'device_workers_notification_budget_per_day_nonneg' - ) THEN - ALTER TABLE public.device_workers - ADD CONSTRAINT device_workers_notification_budget_per_day_nonneg - CHECK (notification_budget_per_day >= 0); - END IF; - END $$; - `); - }, - }, - { - id: 'runs-heartbeat-reaper-index', - apply: async (sql) => { - // Mirrors db/migrations/20260518010000_runs_heartbeat_reaper_index.sql. - // Supports the connector-lane stale-run reaper in - // scheduled/check-stalled-executions.ts. Restricted to the connector - // lanes; the lobu-queue lanes have their own sweep in RunsQueue and - // the watcher lane is handled by watchers/automation.ts. - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS idx_runs_heartbeat_inflight - ON public.runs (last_heartbeat_at) - WHERE status IN ('claimed', 'running') - AND run_type IN ('sync', 'action', 'embed_backfill', 'auth') - `); - }, - }, - { - id: 'runs-heartbeat-inflight-narrow', - apply: async (sql) => { - // Mirrors db/migrations/20260518020000_runs_heartbeat_inflight_narrow.sql. - // Narrows the partial index + reaper WHERE clause to the lanes that - // actually heartbeat today (sync + auth). The previous wide set - // (sync, action, embed_backfill, auth) caused in-flight action and - // embed_backfill runs to be marked `timeout` after 120s because their - // executors never call client.heartbeat(). - await sql.unsafe(`DROP INDEX IF EXISTS public.idx_runs_heartbeat_inflight`); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS idx_runs_heartbeat_inflight - ON public.runs (last_heartbeat_at) - WHERE status IN ('claimed', 'running') - AND run_type IN ('sync', 'auth') - `); - }, - }, - { - id: 'runs-heartbeat-inflight-widen', - apply: async (sql) => { - // Mirrors db/migrations/20260518070000_runs_heartbeat_inflight_widen.sql. - // Widens the partial index + reaper WHERE clause back to all four - // connector lanes now that the action + embed_backfill executors - // emit client.heartbeat() (lobu#860). Same wide predicate as the - // original 20260518010000 patch; the intermediate narrow was a - // hotfix for the heartbeat-blind reaping window. - await sql.unsafe(`DROP INDEX IF EXISTS public.idx_runs_heartbeat_inflight`); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS idx_runs_heartbeat_inflight - ON public.runs (last_heartbeat_at) - WHERE status IN ('claimed', 'running') - AND run_type IN ('sync', 'action', 'embed_backfill', 'auth') - `); - }, - }, - { - id: 'pat-worker-id-binding', - apply: async (sql) => { - // Mirrors db/migrations/20260517030000_pat_worker_id_binding.sql. - // Binds PATs minted via /api/me/devices/mint-child-token to a - // specific worker_id so a child PAT can't post any worker_id at - // /api/workers/poll. Legacy PATs keep worker_id = NULL. - await sql.unsafe(` - ALTER TABLE public.personal_access_tokens - ADD COLUMN IF NOT EXISTS worker_id text - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS idx_personal_access_tokens_worker_id - ON public.personal_access_tokens (worker_id) - WHERE worker_id IS NOT NULL - `); - }, - }, - { - id: 'pending-interactions', - apply: async (sql) => { - // Mirrors db/migrations/20260518000000_pending_interactions.sql. - // Per-question state for the chat-interaction bridge — moved out of - // the gateway's in-process Map so a button click on pod B can claim - // a question registered on pod A. The migration itself omits - // IF NOT EXISTS; we add it here for embedded-patch idempotency. - await sql.unsafe(` - CREATE TABLE IF NOT EXISTS public.pending_interactions ( - id text PRIMARY KEY, - organization_id text NOT NULL REFERENCES public.organization(id) ON DELETE CASCADE, - connection_id text NOT NULL, - expected_user_id text NOT NULL, - entry_payload jsonb NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT now(), - claimed_at timestamp with time zone - ) - `); - }, - }, - { - id: 'agent-transcript-snapshot', - apply: async (sql) => { - // Mirrors db/migrations/20260518040000_agent_transcript_snapshot.sql. - // Per-run snapshot of OpenClaw's session.jsonl. Replaces the - // workspaces PVC as the source of truth for conversation continuity - // across pod boundaries. - await sql.unsafe(` - CREATE TABLE IF NOT EXISTS public.agent_transcript_snapshot ( - id bigserial PRIMARY KEY, - organization_id text NOT NULL REFERENCES public.organization(id) ON DELETE CASCADE, - agent_id text NOT NULL, - conversation_id text NOT NULL, - run_id bigint NOT NULL REFERENCES public.runs(id) ON DELETE CASCADE, - snapshot_jsonl text NOT NULL, - byte_size bigint NOT NULL, - terminal_status text NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT now() - ) - `); - }, - }, - { - id: 'runs-denormalize-agent-conversation', - apply: async (sql) => { - // Mirrors db/migrations/20260518050000_runs_denormalize_agent_conversation.sql. - // Denormalize agent_id + conversation_id out of runs.action_input - // JSONB into real columns. No backfill — readers fall through to - // the JSONB extraction via COALESCE. - await sql.unsafe(` - ALTER TABLE public.runs - ADD COLUMN IF NOT EXISTS agent_id text, - ADD COLUMN IF NOT EXISTS conversation_id text - `); - }, - }, - { - id: 'chat-state-tables', - apply: async (sql) => { - // Mirrors db/migrations/20260519020000_chat_state_tables.sql. - // The Chat SDK state-adapter tables previously created at runtime - // by `LobuStateAdapter.ensureSchema()`. Promoted to a migration so - // db/schema.sql can stay canonical; the runtime ensureSchema() call - // was removed in the same change set. - await sql.unsafe(` - CREATE TABLE IF NOT EXISTS public.chat_state_subscriptions ( - key_prefix text NOT NULL, - thread_id text NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT now(), - PRIMARY KEY (key_prefix, thread_id) - ) - `); - await sql.unsafe(` - CREATE TABLE IF NOT EXISTS public.chat_state_locks ( - key_prefix text NOT NULL, - thread_id text NOT NULL, - token text NOT NULL, - expires_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL DEFAULT now(), - PRIMARY KEY (key_prefix, thread_id) - ) - `); - await sql.unsafe(` - CREATE TABLE IF NOT EXISTS public.chat_state_cache ( - key_prefix text NOT NULL, - cache_key text NOT NULL, - value text NOT NULL, - expires_at timestamp with time zone, - updated_at timestamp with time zone NOT NULL DEFAULT now(), - PRIMARY KEY (key_prefix, cache_key) - ) - `); - await sql.unsafe(` - CREATE TABLE IF NOT EXISTS public.chat_state_lists ( - key_prefix text NOT NULL, - list_key text NOT NULL, - seq bigserial NOT NULL, - value text NOT NULL, - expires_at timestamp with time zone, - PRIMARY KEY (key_prefix, list_key, seq) - ) - `); - await sql.unsafe(` - CREATE TABLE IF NOT EXISTS public.chat_state_queues ( - key_prefix text NOT NULL, - thread_id text NOT NULL, - seq bigserial NOT NULL, - value text NOT NULL, - expires_at timestamp with time zone NOT NULL, - PRIMARY KEY (key_prefix, thread_id, seq) - ) - `); - await sql.unsafe( - `CREATE INDEX IF NOT EXISTS chat_state_locks_expires_idx ON public.chat_state_locks (expires_at)` - ); - await sql.unsafe( - `CREATE INDEX IF NOT EXISTS chat_state_cache_expires_idx ON public.chat_state_cache (expires_at)` - ); - await sql.unsafe( - `CREATE INDEX IF NOT EXISTS chat_state_lists_expires_idx ON public.chat_state_lists (expires_at)` - ); - await sql.unsafe( - `CREATE INDEX IF NOT EXISTS chat_state_queues_expires_idx ON public.chat_state_queues (expires_at)` - ); - }, - }, - { - id: 'revoked-tokens', - apply: async (sql) => { - // Mirrors db/migrations/20260519020001_revoked_tokens.sql. - // Token revocation kill-switch, previously created at runtime in - // RevokedTokenStore.ensureSchema() AND duplicated in the - // `lobu token revoke` CLI command. Both runtime CREATE sites are - // removed in the same change set. - await sql.unsafe(` - CREATE TABLE IF NOT EXISTS public.revoked_tokens ( - jti text PRIMARY KEY, - expires_at timestamp with time zone NOT NULL - ) - `); - await sql.unsafe(` - CREATE INDEX IF NOT EXISTS revoked_tokens_expires_at_idx - ON public.revoked_tokens (expires_at) - `); - }, - }, -]; diff --git a/packages/server/src/gateway/auth/revoked-token-store.ts b/packages/server/src/gateway/auth/revoked-token-store.ts index 2df167d60..4d39ba7b8 100644 --- a/packages/server/src/gateway/auth/revoked-token-store.ts +++ b/packages/server/src/gateway/auth/revoked-token-store.ts @@ -22,9 +22,10 @@ interface CacheEntry { * Backed by `public.revoked_tokens(jti text primary key, expires_at * timestamptz not null)`. Mirrors the shape of `grant-store.ts`: a Postgres * table, a small in-memory TTL cache, and a lazy GC sweep of expired rows. - * Schema lives in `db/migrations/20260519020001_revoked_tokens.sql` and is - * mirrored in `db/embedded-schema-patches.ts` for pre-initialized embedded - * databases. + * Schema lives in `db/migrations/00000000000000_baseline.sql` (the post-2026-05-19 + * squashed baseline that absorbed the original 20260519020001_revoked_tokens + * migration). All boot paths — prod dbmate-up, embedded start-local — apply + * the baseline; no separate mirror to maintain. */ export class RevokedTokenStore { private readonly cache = new Map(); diff --git a/packages/server/src/gateway/connections/state-adapter.ts b/packages/server/src/gateway/connections/state-adapter.ts index a8fa125ed..cfa3283a5 100644 --- a/packages/server/src/gateway/connections/state-adapter.ts +++ b/packages/server/src/gateway/connections/state-adapter.ts @@ -11,11 +11,12 @@ * TTL filtering. Nothing here is upstream IP — keeping it in-tree is * cheaper than the dep weight. * - * Schema lives in `db/migrations/20260519020000_chat_state_tables.sql` and - * is mirrored in `db/embedded-schema-patches.ts` for pre-initialized - * embedded (PGlite) databases. Table names and column types match - * state-pg's so an existing state-pg deployment can swap in this adapter - * without a schema migration. + * Schema lives in `db/migrations/00000000000000_baseline.sql` (the post-2026-05-19 + * squashed baseline that absorbed the original 20260519020000_chat_state_tables + * migration). All boot paths — prod dbmate-up, embedded start-local — apply the + * baseline; no separate mirror to maintain. Table names and column types match + * `@chat-adapter/state-pg`'s so an existing state-pg deployment can swap in + * this adapter without a schema migration. * * Concurrency: every method works on a single statement that round-trips * through the pool; no in-class state. Multiple gateways on the same DB diff --git a/packages/server/src/start-local.ts b/packages/server/src/start-local.ts index d2f73a355..dd610cdfd 100644 --- a/packages/server/src/start-local.ts +++ b/packages/server/src/start-local.ts @@ -48,10 +48,6 @@ import { vector } from '@electric-sql/pglite/vector'; import { PGLiteSocketServer } from '@electric-sql/pglite-socket'; import { getRequestListener } from '@hono/node-server'; import { Hono } from 'hono'; -import { - EMBEDDED_SCHEMA_PATCHES, - type MigrationSqlClient, -} from './db/embedded-schema-patches'; import { listMigrationFiles, loadMigrationUpSection } from './db/migration-loader'; import type { Env } from './index'; import { getEnvFromProcess } from './utils/env'; @@ -270,20 +266,20 @@ async function main() { // ─── Migrations ────────────────────────────────────────────────── async function runMigrations(dbUrl: string) { + // Embedded boot runs the same migrations dbmate uses for prod, applied + // unconditionally. After the schema squash (2026-05-19), the migrations + // dir is a single baseline + any forward deltas; both are idempotent + // enough to replay on a pre-initialized DB: + // - The baseline starts with `CREATE TABLE` against a fresh schema + // and is gated by a `schema_migrations` row insertion. On a DB that + // has the baseline applied, dbmate-style version tracking skips the + // file; we do the same below. + // - Forward deltas use `IF NOT EXISTS` discipline so re-application + // against an already-migrated DB is a no-op. const pg = await import('postgres'); const sql = pg.default(dbUrl, { max: 1 }); try { - const [{ cnt }] = await sql<[{ cnt: number }]>` - SELECT count(*)::int AS cnt FROM pg_tables - WHERE schemaname = 'public' AND tablename = 'organization' - `; - if (cnt > 0) { - logger.info('Database already initialized; applying legacy embedded schema patches'); - await applyEmbeddedSchemaPatches(sql); - return; - } - const migrationsDir = resolveExistingPath( // Published @lobu/cli copies migrations next to start-local.bundle.mjs // under dist/db/migrations. @@ -299,13 +295,35 @@ async function runMigrations(dbUrl: string) { throw new Error('Migrations directory not found.'); } + // Make sure the `schema_migrations` ledger exists before we read it. + await sql.unsafe(` + CREATE TABLE IF NOT EXISTS public.schema_migrations ( + version character varying(128) NOT NULL PRIMARY KEY + ) + `); + + const appliedRows = (await sql.unsafe( + `SELECT version FROM public.schema_migrations` + )) as Array<{ version: string }>; + const applied = new Set(appliedRows.map((r) => r.version)); + logger.info('Running migrations...'); for (const file of listMigrationFiles(migrationsDir)) { + // Filename convention is `_.sql`; the version is the + // leading underscore-separated prefix. + const version = file.split('_')[0] ?? ''; + if (applied.has(version)) { + continue; + } const migrationSql = loadMigrationUpSection(migrationsDir, file); if (!migrationSql) continue; await sql.unsafe('SET search_path TO public'); await sql.unsafe(migrationSql); + await sql` + INSERT INTO public.schema_migrations (version) VALUES (${version}) + ON CONFLICT DO NOTHING + `; } logger.info('Migrations complete'); @@ -314,46 +332,6 @@ async function runMigrations(dbUrl: string) { } } -async function applyEmbeddedSchemaPatches(sql: MigrationSqlClient) { - // Embedded patches mirror DDL from `db/migrations/*.sql`. When a patch - // actually changes the schema (column count delta), that's a drift signal: - // the canonical migration didn't run for this database, either because a - // dev edited an already-applied migration (the #639 footgun) or because - // dbmate isn't wired up. Surface that loudly so it doesn't silently mask - // the same bug class in dev that would crash in prod. - async function schemaSnapshot(): Promise<{ columns: number; indexes: number }> { - try { - const rows = (await sql.unsafe(` - SELECT - (SELECT count(*) FROM information_schema.columns WHERE table_schema = 'public')::int AS columns, - (SELECT count(*) FROM pg_indexes WHERE schemaname = 'public')::int AS indexes - `)) as Array<{ columns: number; indexes: number }>; - return rows[0] ?? { columns: 0, indexes: 0 }; - } catch { - return { columns: 0, indexes: 0 }; - } - } - - for (const patch of EMBEDDED_SCHEMA_PATCHES) { - logger.info({ patch: patch.id }, 'Applying embedded schema patch'); - const before = await schemaSnapshot(); - await patch.apply(sql); - const after = await schemaSnapshot(); - if (after.columns !== before.columns || after.indexes !== before.indexes) { - logger.warn( - { - patch: patch.id, - columnDelta: after.columns - before.columns, - indexDelta: after.indexes - before.indexes, - }, - 'Embedded patch modified schema — the matching db/migrations/*.sql ' + - 'did not run for this database. If you just edited a migration, ' + - 'roll it back and ship a new dated migration file instead.' - ); - } - } -} - // ─── Embeddings (child process) ────────────────────────────────── diff --git a/scripts/normalize-schema.sh b/scripts/normalize-schema.sh deleted file mode 100755 index c7367c367..000000000 --- a/scripts/normalize-schema.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -# Strip non-deterministic / env-dependent lines from a pg_dump output so the -# file is byte-stable across pg_dump versions and runs. Used by the -# `migrations` CI job's schema-drift check. -# -# Lines removed: -# \restrict — pg17+ session token at the top of the dump -# \unrestrict — matching footer -# -- Dumped from database version <...> -# -- Dumped by pg_dump version <...> -# SET transaction_timeout = 0; — emitted only by pg17+ -# -# Lines patched: -# schema_migrations.version — pg18's pg_dump emits this column as bare -# `character varying`, dropping the explicit -# length dbmate declared. The committed form -# (and what CI keeps) is `character varying(128)`. -# Restoring the (128) here lets local regen -# under pg18 produce a byte-identical file. -# Other columns are unaffected — they have -# explicit lengths in migrations and pg18 -# preserves them. -# -# Adjacent blank lines that result from deletions are collapsed and any -# leading blanks at the top of the file are dropped, so the output is the -# same regardless of which lines were originally present. -# -# Usage: scripts/normalize-schema.sh db/schema.sql -set -euo pipefail -file="${1:?usage: $0 }" -tmp="$(mktemp)" -awk ' - /^\\restrict [^[:space:]]+$/ { next } - /^\\unrestrict [^[:space:]]+$/ { next } - /^-- Dumped from database version / { next } - /^-- Dumped by pg_dump version / { next } - /^SET transaction_timeout = 0;$/ { next } - /^$/ { - # Drop leading blanks before any content. Once content has been emitted, - # collapse runs of blank lines: queue one and emit it before the next - # non-blank line. - if (!seen) next - pending_blank = 1 - next - } - # Restore schema_migrations.version length stripped by pg18 pg_dump. - # Table-scoped: only inside `CREATE TABLE public.schema_migrations (...)`. - # Without the scope guard, any future app table whose column happens to be - # exactly ` version character varying NOT NULL` would also get silently - # patched to (128) — which would be wrong. - /^CREATE TABLE public\.schema_migrations \(/ { in_schema_migrations = 1 } - in_schema_migrations && /^ version character varying NOT NULL$/ { - sub(/character varying/, "character varying(128)") - } - in_schema_migrations && /^\);/ { in_schema_migrations = 0 } - { - if (pending_blank) { print ""; pending_blank = 0 } - print - seen = 1 - } -' "$file" >"$tmp" -mv "$tmp" "$file"