From 864a16df40c362453bb1bf53b46745b5d26d39df Mon Sep 17 00:00:00 2001 From: Bestony Date: Fri, 31 Jul 2026 10:27:39 +0800 Subject: [PATCH 1/4] fix(server): scan only the non-acked delta when committing inbox ACKs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ackThroughEntryIdForBoundAgents` selected — and, under `FOR UPDATE`, locked — every notify=true row in the `(inbox_id, chat_id)` partition up to the ACK cursor, including rows acked long ago. No index matched that query, so the planner fell back to the primary key and walked the whole `id <= cursor` range. Cost per ACK was O(chat history) and lifetime cost O(N^2), and a duplicate ACK that commits nothing paid the same price. Already-acked rows cannot affect any of the three decisions the commit makes: they never trigger a prefix gap, are never committable, and are never reset-from-pending. Excluding them in SQL is therefore an exact equivalence rather than an approximation, and it holds because `acked` is terminal — recovery resets `delivered` rows, never `acked` ones. Restricting the predicate alone is not enough: without a matching index PostgreSQL still scans the partition and only skips the locking. This change pairs the clause with a partial index over non-acked rows, which stays sized to the live in-flight window instead of to history (48 kB on a 2.7M-row table whose comparable full index is 26 MB). The clause is spelled as an inline literal on purpose. postgres-js sends named prepared statements, so PostgreSQL may switch to a generic plan after five executions, and a generic plan cannot use a bound parameter to prove a partial-index predicate — the scan would silently revert to whole-partition while constant-parameter benchmarks still looked fast. For the same reason `notify` sits in the index key rather than in the predicate, so the index depends on exactly one inlined literal. Also stops relying on `RETURNING`'s undefined row order: `committableIds` is now built in one ascending pass and the updated rows are sorted before `ackedEntryIds` is derived, which the WS in-flight bookkeeping treats as an ascending cursor list. Measured on PostgreSQL 17 against a 150k-notify-row history, timing the whole ACK transaction: incremental ACK 70.8 ms -> 0.068 ms (150001 rows scanned -> 1) duplicate ACK 78.1 ms -> 0.010 ms (150000 rows scanned -> 0) 500-row backlog 80.1 ms -> 4.34 ms (150500 rows scanned -> 500) Refs #1671 --- .../server/drizzle/0091_green_archangel.sql | 58 + packages/server/drizzle/LATEST | 2 +- .../server/drizzle/meta/0091_snapshot.json | 6274 +++++++++++++++++ packages/server/drizzle/meta/_journal.json | 7 + .../src/__tests__/inbox-ack-scaling.test.ts | 237 + .../__tests__/inbox-delivery-indexes.test.ts | 20 + .../src/__tests__/inbox-ws-push.test.ts | 66 +- .../server/src/db/schema/inbox-entries.ts | 29 + packages/server/src/services/inbox.ts | 74 +- 9 files changed, 6759 insertions(+), 8 deletions(-) create mode 100644 packages/server/drizzle/0091_green_archangel.sql create mode 100644 packages/server/drizzle/meta/0091_snapshot.json create mode 100644 packages/server/src/__tests__/inbox-ack-scaling.test.ts diff --git a/packages/server/drizzle/0091_green_archangel.sql b/packages/server/drizzle/0091_green_archangel.sql new file mode 100644 index 000000000..391b9bbb7 --- /dev/null +++ b/packages/server/drizzle/0091_green_archangel.sql @@ -0,0 +1,58 @@ +-- ACK-through cursor window index for inbox_entries. +-- +-- `ackThroughEntryIdForBoundAgents` commits the contiguous notify=true prefix +-- of an (inbox_id, chat_id) partition up to an acked cursor. Before this index +-- no index matched that query: the planner fell back to the primary key and +-- scanned — and, under FOR UPDATE, locked — every notify row ever written to +-- the chat, including long-acked ones that can never change the outcome. Cost +-- per ACK was therefore O(chat history) and lifetime cost O(N^2), with lock +-- contention growing to match. +-- +-- The index is partial on `status <> 'acked'` because `acked` is terminal: +-- nothing resets an acked row back to pending/delivered, so the non-acked set +-- is exactly the live in-flight window. That keeps the index sized to +-- concurrent traffic rather than to history — measured at 48 kB on a 2.7M-row +-- table whose comparable full index (idx_inbox_chat_silent) was 26 MB. +-- +-- Column order is load-bearing: +-- +-- inbox_id, chat_id — the partition the ACK commits within. +-- notify — kept in the key rather than in the predicate. A +-- partial index only applies when the planner can prove +-- the query implies its predicate, and under a generic +-- plan a bound parameter proves nothing. The service +-- passes `notify` as a parameter, so a `WHERE notify = +-- true` predicate would silently stop matching; as a key +-- column that same parameter is an ordinary index +-- condition. +-- id — last, so `id <= cursor` is a range condition and +-- `ORDER BY id` needs no sort (which also preserves the +-- ascending FOR UPDATE lock order). +-- +-- The predicate is spelled `status <> 'acked'` so the service's WHERE clause +-- can match it verbatim. The service must also pass that clause as a literal +-- rather than a bind parameter, for the same reason `notify` is not in the +-- predicate: under a generic plan PostgreSQL cannot prove a Param-based clause +-- implies a partial-index predicate, and the scan silently degrades back to +-- whole-partition. This index therefore depends on exactly one inlined +-- literal, and that literal is the fix itself. See services/inbox.ts. +-- +-- ──────────────── Operator note ──────────────── +-- +-- Drizzle migrator wraps every migration file in a single transaction (see the +-- comment block in 0020_unified_user_token.sql), so `CREATE INDEX +-- CONCURRENTLY` cannot be used here. The regular form below holds a SHARE lock +-- that blocks writes to `inbox_entries` (including message fan-out) for the +-- duration of one heap scan: measured at 30 ms for 400k rows and 73 ms for +-- 2.7M rows on a warm table, and proportional to table size on a cold one. +-- +-- For a large production table, the runbook is: +-- +-- 1. Stop applying new migrations briefly. +-- 2. Manually run, OUTSIDE a transaction: +-- CREATE INDEX CONCURRENTLY idx_inbox_unacked_cursor +-- ON inbox_entries (inbox_id, chat_id, notify, id) +-- WHERE status <> 'acked'; +-- 3. Re-run `pnpm db:migrate`. The `IF NOT EXISTS` clause below detects the +-- existing index and the statement becomes a no-op. +CREATE INDEX IF NOT EXISTS "idx_inbox_unacked_cursor" ON "inbox_entries" USING btree ("inbox_id","chat_id","notify","id") WHERE status <> 'acked'; diff --git a/packages/server/drizzle/LATEST b/packages/server/drizzle/LATEST index 70f12c5a8..98c1e361d 100644 --- a/packages/server/drizzle/LATEST +++ b/packages/server/drizzle/LATEST @@ -1 +1 @@ -0090_redundant_crystal +0091_green_archangel diff --git a/packages/server/drizzle/meta/0091_snapshot.json b/packages/server/drizzle/meta/0091_snapshot.json new file mode 100644 index 000000000..1a75124c6 --- /dev/null +++ b/packages/server/drizzle/meta/0091_snapshot.json @@ -0,0 +1,6274 @@ +{ + "id": "9318e58a-28c2-4db6-b0b4-7f164a7e95b2", + "prevId": "d0620311-f910-43cb-95a8-c81e740f1ce8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agent_chat_sessions": { + "name": "agent_chat_sessions", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime_state": { + "name": "runtime_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "runtime_state_at": { + "name": "runtime_state_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_agent_chat_sessions_chat_agent": { + "name": "idx_agent_chat_sessions_chat_agent", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_chat_sessions_agent_id_agents_uuid_fk": { + "name": "agent_chat_sessions_agent_id_agents_uuid_fk", + "tableFrom": "agent_chat_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_chat_sessions_chat_id_chats_id_fk": { + "name": "agent_chat_sessions_chat_id_chats_id_fk", + "tableFrom": "agent_chat_sessions", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_chat_sessions_agent_id_chat_id_pk": { + "name": "agent_chat_sessions_agent_id_chat_id_pk", + "columns": [ + "agent_id", + "chat_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_configs": { + "name": "agent_configs", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "template_ids": { + "name": "template_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_agent_configs_template_ids": { + "name": "idx_agent_configs_template_ids", + "columns": [ + { + "expression": "template_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ck_agent_configs_template_ids_cardinality": { + "name": "ck_agent_configs_template_ids_cardinality", + "value": "cardinality(\"agent_configs\".\"template_ids\") <= 3" + } + }, + "isRLSEnabled": false + }, + "public.agent_presence": { + "name": "agent_presence", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'offline'" + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_type": { + "name": "runtime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_version": { + "name": "runtime_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_state": { + "name": "runtime_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_sessions": { + "name": "active_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_sessions": { + "name": "total_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "runtime_updated_at": { + "name": "runtime_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_presence_agent_id_agents_uuid_fk": { + "name": "agent_presence_agent_id_agents_uuid_fk", + "tableFrom": "agent_presence", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_presence_client_id_clients_id_fk": { + "name": "agent_presence_client_id_clients_id_fk", + "tableFrom": "agent_presence", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_resource_bindings": { + "name": "agent_resource_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replaces_resource_id": { + "name": "replaces_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inline_prompt_body": { + "name": "inline_prompt_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_ref": { + "name": "repo_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_local_path": { + "name": "repo_local_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "origin_template_id": { + "name": "origin_template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_component_key": { + "name": "origin_component_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_agent_resource_bindings_agent": { + "name": "idx_agent_resource_bindings_agent", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_resource_bindings_resource": { + "name": "idx_agent_resource_bindings_resource", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_resource_bindings_replaces": { + "name": "idx_agent_resource_bindings_replaces", + "columns": [ + { + "expression": "replaces_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_resource_bindings_template_origin": { + "name": "idx_agent_resource_bindings_template_origin", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_resource_bindings\".\"origin_template_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_resource_bindings_organization_id_organizations_id_fk": { + "name": "agent_resource_bindings_organization_id_organizations_id_fk", + "tableFrom": "agent_resource_bindings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_resource_bindings_agent_id_agents_uuid_fk": { + "name": "agent_resource_bindings_agent_id_agents_uuid_fk", + "tableFrom": "agent_resource_bindings", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_resource_bindings_resource_id_resources_id_fk": { + "name": "agent_resource_bindings_resource_id_resources_id_fk", + "tableFrom": "agent_resource_bindings", + "tableTo": "resources", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_resource_bindings_replaces_resource_id_resources_id_fk": { + "name": "agent_resource_bindings_replaces_resource_id_resources_id_fk", + "tableFrom": "agent_resource_bindings", + "tableTo": "resources", + "columnsFrom": [ + "replaces_resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ck_agent_resource_bindings_template_origin_consistency": { + "name": "ck_agent_resource_bindings_template_origin_consistency", + "value": "(\"agent_resource_bindings\".\"origin_template_id\" IS NULL AND \"agent_resource_bindings\".\"origin_component_key\" IS NULL) OR (\"agent_resource_bindings\".\"origin_template_id\" IS NOT NULL AND \"agent_resource_bindings\".\"origin_component_key\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.agent_templates": { + "name": "agent_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "replacement_template_id": { + "name": "replacement_template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_agent_templates_slug": { + "name": "uq_agent_templates_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_templates_status": { + "name": "idx_agent_templates_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_templates_replacement_template_id_agent_templates_id_fk": { + "name": "agent_templates_replacement_template_id_agent_templates_id_fk", + "tableFrom": "agent_templates", + "tableTo": "agent_templates", + "columnsFrom": [ + "replacement_template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ck_agent_templates_status": { + "name": "ck_agent_templates_status", + "value": "\"agent_templates\".\"status\" IN ('draft', 'active', 'retired')" + } + }, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delegate_mention": { + "name": "delegate_mention", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_id": { + "name": "inbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_provider": { + "name": "runtime_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-code'" + }, + "avatar_color_token": { + "name": "avatar_color_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_image_data": { + "name": "avatar_image_data", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "avatar_image_mime": { + "name": "avatar_image_mime", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_image_updated_at": { + "name": "avatar_image_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_agents_org": { + "name": "idx_agents_org", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agents_manager": { + "name": "idx_agents_manager", + "columns": [ + { + "expression": "manager_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agents_visibility_org": { + "name": "idx_agents_visibility_org", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agents_client": { + "name": "idx_agents_client", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_organization_id_organizations_id_fk": { + "name": "agents_organization_id_organizations_id_fk", + "tableFrom": "agents", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_client_id_clients_id_fk": { + "name": "agents_client_id_clients_id_fk", + "tableFrom": "agents", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agents_inbox_id_unique": { + "name": "agents_inbox_id_unique", + "nullsNotDistinct": false, + "columns": [ + "inbox_id" + ] + }, + "uq_agents_org_name": { + "name": "uq_agents_org_name", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.attachments": { + "name": "attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle_state": { + "name": "lifecycle_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "attachments_org_state_idx": { + "name": "attachments_org_state_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lifecycle_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "attachments_state_updated_idx": { + "name": "attachments_state_updated_idx", + "columns": [ + { + "expression": "lifecycle_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "attachments_uploaded_by_idx": { + "name": "attachments_uploaded_by_idx", + "columns": [ + { + "expression": "uploaded_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "attachments_created_at_idx": { + "name": "attachments_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.attentions": { + "name": "attentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "origin_agent_id": { + "name": "origin_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin_chat_id": { + "name": "origin_chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_human_id": { + "name": "target_human_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "requires_response": { + "name": "requires_response", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "response": { + "name": "response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "responded_by": { + "name": "responded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "responded_at": { + "name": "responded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled": { + "name": "cancelled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cancelled_reason": { + "name": "cancelled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_attentions_target_open": { + "name": "idx_attentions_target_open", + "columns": [ + { + "expression": "target_human_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_attentions_chat_open": { + "name": "idx_attentions_chat_open", + "columns": [ + { + "expression": "origin_chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_attentions_origin": { + "name": "idx_attentions_origin", + "columns": [ + { + "expression": "origin_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_identities": { + "name": "auth_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credential_type": { + "name": "credential_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_payload": { + "name": "credential_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_auth_identities_user": { + "name": "idx_auth_identities_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_auth_identities_email": { + "name": "idx_auth_identities_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_identities_user_id_users_id_fk": { + "name": "auth_identities_user_id_users_id_fk", + "tableFrom": "auth_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_auth_identities_provider_identifier": { + "name": "uq_auth_identities_provider_identifier", + "nullsNotDistinct": false, + "columns": [ + "provider", + "identifier" + ] + }, + "uq_auth_identities_user_provider": { + "name": "uq_auth_identities_user_provider", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "provider" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_membership": { + "name": "chat_membership", + "schema": "", + "columns": { + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "access_mode": { + "name": "access_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_membership_agent": { + "name": "idx_membership_agent", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_membership_chat_role": { + "name": "idx_membership_chat_role", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "access_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "chat_membership_chat_id_agent_id_pk": { + "name": "chat_membership_chat_id_agent_id_pk", + "columns": [ + "chat_id", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_user_state": { + "name": "chat_user_state", + "schema": "", + "columns": { + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "unread_mention_count": { + "name": "unread_mention_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "open_request_count": { + "name": "open_request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "engagement_status": { + "name": "engagement_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_user_state_agent": { + "name": "idx_user_state_agent", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_state_unread": { + "name": "idx_user_state_unread", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "unread_mention_count > 0", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_state_open_req": { + "name": "idx_user_state_open_req", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "open_request_count > 0", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_state_pinned": { + "name": "idx_user_state_pinned", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "pinned_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "chat_user_state_chat_id_agent_id_pk": { + "name": "chat_user_state_chat_id_agent_id_pk", + "columns": [ + "chat_id", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chats": { + "name": "chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'direct'" + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description_updated_at": { + "name": "description_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lifecycle_policy": { + "name": "lifecycle_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'persistent'" + }, + "parent_chat_id": { + "name": "parent_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "onboarding_kickoff_key": { + "name": "onboarding_kickoff_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_message_preview": { + "name": "last_message_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activity_at": { + "name": "activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_chats_org_last_message": { + "name": "idx_chats_org_last_message", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"last_message_at\" desc", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chats_org_activity": { + "name": "idx_chats_org_activity", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"activity_at\" desc", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_chats_onboarding_kickoff_key": { + "name": "uq_chats_onboarding_kickoff_key", + "columns": [ + { + "expression": "onboarding_kickoff_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chats_organization_id_organizations_id_fk": { + "name": "chats_organization_id_organizations_id_fk", + "tableFrom": "chats", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.clients": { + "name": "clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'disconnected'" + }, + "sdk_version": { + "name": "sdk_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "os": { + "name": "os", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paused_reason": { + "name": "paused_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_clients_user": { + "name": "idx_clients_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_clients_org": { + "name": "idx_clients_org", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "clients_user_id_users_id_fk": { + "name": "clients_user_id_users_id_fk", + "tableFrom": "clients", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "clients_organization_id_organizations_id_fk": { + "name": "clients_organization_id_organizations_id_fk", + "tableFrom": "clients", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ck_clients_paused_reason": { + "name": "ck_clients_paused_reason", + "value": "\"clients\".\"paused_reason\" IS NULL OR \"clients\".\"paused_reason\" IN ('auth_rejected', 'auth_refresh_failed')" + } + }, + "isRLSEnabled": false + }, + "public.connect_codes": { + "name": "connect_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_connect_codes_user": { + "name": "idx_connect_codes_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_connect_codes_expires_at": { + "name": "idx_connect_codes_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connect_codes_user_id_users_id_fk": { + "name": "connect_codes_user_id_users_id_fk", + "tableFrom": "connect_codes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "connect_codes_code_hash_unique": { + "name": "connect_codes_code_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "code_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.context_tree_io_events": { + "name": "context_tree_io_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_session_event_id": { + "name": "source_session_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_index": { + "name": "source_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "runtime_provider": { + "name": "runtime_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tree_repo_url": { + "name": "tree_repo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tree_branch": { + "name": "tree_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_path": { + "name": "target_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_context_tree_io_source": { + "name": "uq_context_tree_io_source", + "columns": [ + { + "expression": "source_session_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_context_tree_io_org_recent": { + "name": "idx_context_tree_io_org_recent", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_context_tree_io_org_action_recent": { + "name": "idx_context_tree_io_org_action_recent", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_context_tree_io_org_agent_recent": { + "name": "idx_context_tree_io_org_agent_recent", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_context_tree_io_org_target_recent": { + "name": "idx_context_tree_io_org_target_recent", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "context_tree_io_events_organization_id_organizations_id_fk": { + "name": "context_tree_io_events_organization_id_organizations_id_fk", + "tableFrom": "context_tree_io_events", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "context_tree_io_events_agent_id_agents_uuid_fk": { + "name": "context_tree_io_events_agent_id_agents_uuid_fk", + "tableFrom": "context_tree_io_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "context_tree_io_events_chat_id_chats_id_fk": { + "name": "context_tree_io_events_chat_id_chats_id_fk", + "tableFrom": "context_tree_io_events", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ck_context_tree_io_action": { + "name": "ck_context_tree_io_action", + "value": "\"context_tree_io_events\".\"action\" IN ('read', 'write')" + }, + "ck_context_tree_io_target_kind": { + "name": "ck_context_tree_io_target_kind", + "value": "\"context_tree_io_events\".\"target_kind\" IN ('file', 'directory', 'repo')" + }, + "ck_context_tree_io_target_path_nonempty": { + "name": "ck_context_tree_io_target_path_nonempty", + "value": "\"context_tree_io_events\".\"target_path\" <> ''" + } + }, + "isRLSEnabled": false + }, + "public.cron_jobs": { + "name": "cron_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_member_id": { + "name": "owner_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "control_chat_id": { + "name": "control_chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_mode": { + "name": "chat_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reuse_control_chat'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_reason": { + "name": "state_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_trigger_message_id": { + "name": "last_trigger_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_cron_jobs_due": { + "name": "idx_cron_jobs_due", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cron_jobs\".\"state\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cron_jobs_control_created": { + "name": "idx_cron_jobs_control_created", + "columns": [ + { + "expression": "control_chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cron_jobs_owner_created": { + "name": "idx_cron_jobs_owner_created", + "columns": [ + { + "expression": "owner_member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cron_jobs_owner_member_id_members_id_fk": { + "name": "cron_jobs_owner_member_id_members_id_fk", + "tableFrom": "cron_jobs", + "tableTo": "members", + "columnsFrom": [ + "owner_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cron_jobs_control_chat_id_chats_id_fk": { + "name": "cron_jobs_control_chat_id_chats_id_fk", + "tableFrom": "cron_jobs", + "tableTo": "chats", + "columnsFrom": [ + "control_chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cron_jobs_agent_id_agents_uuid_fk": { + "name": "cron_jobs_agent_id_agents_uuid_fk", + "tableFrom": "cron_jobs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cron_jobs_last_trigger_message_id_messages_id_fk": { + "name": "cron_jobs_last_trigger_message_id_messages_id_fk", + "tableFrom": "cron_jobs", + "tableTo": "messages", + "columnsFrom": [ + "last_trigger_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_cron_jobs_control_agent_name": { + "name": "uq_cron_jobs_control_agent_name", + "nullsNotDistinct": false, + "columns": [ + "control_chat_id", + "agent_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": { + "ck_cron_jobs_state": { + "name": "ck_cron_jobs_state", + "value": "\"cron_jobs\".\"state\" IN ('active', 'paused')" + }, + "ck_cron_jobs_chat_mode": { + "name": "ck_cron_jobs_chat_mode", + "value": "\"cron_jobs\".\"chat_mode\" = 'reuse_control_chat'" + }, + "ck_cron_jobs_revision_positive": { + "name": "ck_cron_jobs_revision_positive", + "value": "\"cron_jobs\".\"revision\" > 0" + }, + "ck_cron_jobs_active_shape": { + "name": "ck_cron_jobs_active_shape", + "value": "(\"cron_jobs\".\"state\" = 'active' AND \"cron_jobs\".\"next_run_at\" IS NOT NULL AND \"cron_jobs\".\"state_reason\" IS NULL) OR (\"cron_jobs\".\"state\" = 'paused' AND \"cron_jobs\".\"next_run_at\" IS NULL AND \"cron_jobs\".\"state_reason\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.doc_comments": { + "name": "doc_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_kind": { + "name": "author_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor": { + "name": "anchor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_comments_document_status_idx": { + "name": "doc_comments_document_status_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_comments_parent_idx": { + "name": "doc_comments_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "doc_comments_document_id_doc_documents_id_fk": { + "name": "doc_comments_document_id_doc_documents_id_fk", + "tableFrom": "doc_comments", + "tableTo": "doc_documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "doc_comments_parent_id_doc_comments_id_fk": { + "name": "doc_comments_parent_id_doc_comments_id_fk", + "tableFrom": "doc_comments", + "tableTo": "doc_comments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.doc_documents": { + "name": "doc_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project": { + "name": "project", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "latest_version": { + "name": "latest_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_name": { + "name": "created_by_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_documents_org_slug_unique": { + "name": "doc_documents_org_slug_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_documents_org_updated_idx": { + "name": "doc_documents_org_updated_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "doc_documents_organization_id_organizations_id_fk": { + "name": "doc_documents_organization_id_organizations_id_fk", + "tableFrom": "doc_documents", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.doc_versions": { + "name": "doc_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_kind": { + "name": "author_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_versions_document_number_unique": { + "name": "doc_versions_document_number_unique", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "doc_versions_document_id_doc_documents_id_fk": { + "name": "doc_versions_document_id_doc_documents_id_fk", + "tableFrom": "doc_versions", + "tableTo": "doc_documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_app_installations": { + "name": "github_app_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_github_id": { + "name": "account_github_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "installer_github_id": { + "name": "installer_github_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "requester_github_id": { + "name": "requester_github_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "hub_organization_id": { + "name": "hub_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "events": { + "name": "events", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_github_app_installations_installation_id": { + "name": "uq_github_app_installations_installation_id", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_github_app_installations_hub_org": { + "name": "uq_github_app_installations_hub_org", + "columns": [ + { + "expression": "hub_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_github_app_installations_account": { + "name": "idx_github_app_installations_account", + "columns": [ + { + "expression": "account_github_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_github_app_installations_installer": { + "name": "idx_github_app_installations_installer", + "columns": [ + { + "expression": "installer_github_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_github_app_installations_requester": { + "name": "idx_github_app_installations_requester", + "columns": [ + { + "expression": "requester_github_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_app_installations_hub_organization_id_organizations_id_fk": { + "name": "github_app_installations_hub_organization_id_organizations_id_fk", + "tableFrom": "github_app_installations", + "tableTo": "organizations", + "columnsFrom": [ + "hub_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ck_github_app_installations_account_type": { + "name": "ck_github_app_installations_account_type", + "value": "\"github_app_installations\".\"account_type\" IN ('User', 'Organization')" + } + }, + "isRLSEnabled": false + }, + "public.github_entity_chat_mappings": { + "name": "github_entity_chat_mappings", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "human_agent_id": { + "name": "human_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delegate_agent_id": { + "name": "delegate_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_key": { + "name": "entity_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bound_at": { + "name": "bound_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "bound_via": { + "name": "bound_via", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_state": { + "name": "entity_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "entity_state_updated_at": { + "name": "entity_state_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_github_entity_chat_mappings_chat": { + "name": "idx_github_entity_chat_mappings_chat", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_github_entity_chat_mappings_chat_state": { + "name": "idx_github_entity_chat_mappings_chat_state", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_entity_chat_mappings_organization_id_organizations_id_fk": { + "name": "github_entity_chat_mappings_organization_id_organizations_id_fk", + "tableFrom": "github_entity_chat_mappings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "github_entity_chat_mappings_human_agent_id_agents_uuid_fk": { + "name": "github_entity_chat_mappings_human_agent_id_agents_uuid_fk", + "tableFrom": "github_entity_chat_mappings", + "tableTo": "agents", + "columnsFrom": [ + "human_agent_id" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_entity_chat_mappings_delegate_agent_id_agents_uuid_fk": { + "name": "github_entity_chat_mappings_delegate_agent_id_agents_uuid_fk", + "tableFrom": "github_entity_chat_mappings", + "tableTo": "agents", + "columnsFrom": [ + "delegate_agent_id" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_entity_chat_mappings_chat_id_chats_id_fk": { + "name": "github_entity_chat_mappings_chat_id_chats_id_fk", + "tableFrom": "github_entity_chat_mappings", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "github_entity_chat_mappings_organization_id_human_agent_id_delegate_agent_id_entity_type_entity_key_pk": { + "name": "github_entity_chat_mappings_organization_id_human_agent_id_delegate_agent_id_entity_type_entity_key_pk", + "columns": [ + "organization_id", + "human_agent_id", + "delegate_agent_id", + "entity_type", + "entity_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitlab_connections": { + "name": "gitlab_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_origin": { + "name": "instance_origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint_first_seen_at": { + "name": "endpoint_first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_valid_inbound_at": { + "name": "last_valid_inbound_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_system_hook_inbound_at": { + "name": "last_system_hook_inbound_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_project_hook_inbound_at": { + "name": "last_project_hook_inbound_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_system_hook_merge_request_inbound_at": { + "name": "last_system_hook_merge_request_inbound_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_processing_failure_at": { + "name": "last_processing_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_processing_failure_code": { + "name": "last_processing_failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stable_delivery_observed_at": { + "name": "stable_delivery_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_observed_version": { + "name": "last_observed_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewer_mode": { + "name": "reviewer_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "last_reviewer_schema_anomaly_at": { + "name": "last_reviewer_schema_anomaly_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_reviewer_schema_anomaly_code": { + "name": "last_reviewer_schema_anomaly_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_member_id": { + "name": "created_by_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_member_id": { + "name": "updated_by_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_gitlab_connections_org": { + "name": "uq_gitlab_connections_org", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_gitlab_connections_token_hash": { + "name": "uq_gitlab_connections_token_hash", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gitlab_connections_organization_id_organizations_id_fk": { + "name": "gitlab_connections_organization_id_organizations_id_fk", + "tableFrom": "gitlab_connections", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gitlab_connections_created_by_member_id_members_id_fk": { + "name": "gitlab_connections_created_by_member_id_members_id_fk", + "tableFrom": "gitlab_connections", + "tableTo": "members", + "columnsFrom": [ + "created_by_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "gitlab_connections_updated_by_member_id_members_id_fk": { + "name": "gitlab_connections_updated_by_member_id_members_id_fk", + "tableFrom": "gitlab_connections", + "tableTo": "members", + "columnsFrom": [ + "updated_by_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ck_gitlab_connections_reviewer_mode": { + "name": "ck_gitlab_connections_reviewer_mode", + "value": "\"gitlab_connections\".\"reviewer_mode\" IN ('unknown', 'assignee', 'reviewers')" + } + }, + "isRLSEnabled": false + }, + "public.gitlab_entity_chat_mappings": { + "name": "gitlab_entity_chat_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "declared_by_agent_id": { + "name": "declared_by_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bound_via": { + "name": "bound_via", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'agent_declared'" + }, + "identity_link_id": { + "name": "identity_link_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "human_agent_id": { + "name": "human_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delegate_agent_id": { + "name": "delegate_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attention_mode": { + "name": "attention_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'legacy_route_only'" + }, + "attention_backfill_version": { + "name": "attention_backfill_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_iid": { + "name": "entity_iid", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "project_path": { + "name": "project_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_path_normalized": { + "name": "project_path_normalized", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_url": { + "name": "entity_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_state": { + "name": "entity_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_gitlab_entity_pending_pair": { + "name": "uq_gitlab_entity_pending_pair", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "human_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delegate_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_path_normalized", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_iid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"gitlab_entity_chat_mappings\".\"project_id\" IS NULL AND \"gitlab_entity_chat_mappings\".\"active\" AND \"gitlab_entity_chat_mappings\".\"bound_via\" <> 'identity_target' AND \"gitlab_entity_chat_mappings\".\"human_agent_id\" IS NOT NULL AND \"gitlab_entity_chat_mappings\".\"delegate_agent_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_gitlab_entity_observed_pair": { + "name": "uq_gitlab_entity_observed_pair", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "human_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delegate_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_iid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"gitlab_entity_chat_mappings\".\"project_id\" IS NOT NULL AND \"gitlab_entity_chat_mappings\".\"active\" AND \"gitlab_entity_chat_mappings\".\"bound_via\" <> 'identity_target' AND \"gitlab_entity_chat_mappings\".\"human_agent_id\" IS NOT NULL AND \"gitlab_entity_chat_mappings\".\"delegate_agent_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_gitlab_entity_pending_legacy_chat": { + "name": "uq_gitlab_entity_pending_legacy_chat", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_path_normalized", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_iid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"gitlab_entity_chat_mappings\".\"project_id\" IS NULL AND \"gitlab_entity_chat_mappings\".\"active\" AND \"gitlab_entity_chat_mappings\".\"bound_via\" <> 'identity_target' AND \"gitlab_entity_chat_mappings\".\"human_agent_id\" IS NULL AND \"gitlab_entity_chat_mappings\".\"delegate_agent_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_gitlab_entity_observed_legacy_chat": { + "name": "uq_gitlab_entity_observed_legacy_chat", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_iid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"gitlab_entity_chat_mappings\".\"project_id\" IS NOT NULL AND \"gitlab_entity_chat_mappings\".\"active\" AND \"gitlab_entity_chat_mappings\".\"bound_via\" <> 'identity_target' AND \"gitlab_entity_chat_mappings\".\"human_agent_id\" IS NULL AND \"gitlab_entity_chat_mappings\".\"delegate_agent_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_gitlab_entity_identity_target": { + "name": "uq_gitlab_entity_identity_target", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "identity_link_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_iid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"gitlab_entity_chat_mappings\".\"project_id\" IS NOT NULL AND \"gitlab_entity_chat_mappings\".\"active\" AND \"gitlab_entity_chat_mappings\".\"bound_via\" = 'identity_target'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_gitlab_entity_observed_lookup": { + "name": "idx_gitlab_entity_observed_lookup", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_iid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_gitlab_entity_pending_lookup": { + "name": "idx_gitlab_entity_pending_lookup", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_path_normalized", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_iid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"gitlab_entity_chat_mappings\".\"project_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_gitlab_entity_chat": { + "name": "idx_gitlab_entity_chat", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gitlab_entity_chat_mappings_organization_id_organizations_id_fk": { + "name": "gitlab_entity_chat_mappings_organization_id_organizations_id_fk", + "tableFrom": "gitlab_entity_chat_mappings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gitlab_entity_chat_mappings_connection_id_gitlab_connections_id_fk": { + "name": "gitlab_entity_chat_mappings_connection_id_gitlab_connections_id_fk", + "tableFrom": "gitlab_entity_chat_mappings", + "tableTo": "gitlab_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gitlab_entity_chat_mappings_chat_id_chats_id_fk": { + "name": "gitlab_entity_chat_mappings_chat_id_chats_id_fk", + "tableFrom": "gitlab_entity_chat_mappings", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gitlab_entity_chat_mappings_declared_by_agent_id_agents_uuid_fk": { + "name": "gitlab_entity_chat_mappings_declared_by_agent_id_agents_uuid_fk", + "tableFrom": "gitlab_entity_chat_mappings", + "tableTo": "agents", + "columnsFrom": [ + "declared_by_agent_id" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gitlab_entity_chat_mappings_identity_link_id_gitlab_identity_links_id_fk": { + "name": "gitlab_entity_chat_mappings_identity_link_id_gitlab_identity_links_id_fk", + "tableFrom": "gitlab_entity_chat_mappings", + "tableTo": "gitlab_identity_links", + "columnsFrom": [ + "identity_link_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gitlab_entity_chat_mappings_human_agent_id_agents_uuid_fk": { + "name": "gitlab_entity_chat_mappings_human_agent_id_agents_uuid_fk", + "tableFrom": "gitlab_entity_chat_mappings", + "tableTo": "agents", + "columnsFrom": [ + "human_agent_id" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gitlab_entity_chat_mappings_delegate_agent_id_agents_uuid_fk": { + "name": "gitlab_entity_chat_mappings_delegate_agent_id_agents_uuid_fk", + "tableFrom": "gitlab_entity_chat_mappings", + "tableTo": "agents", + "columnsFrom": [ + "delegate_agent_id" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ck_gitlab_entity_type": { + "name": "ck_gitlab_entity_type", + "value": "\"gitlab_entity_chat_mappings\".\"entity_type\" IN ('issue', 'pull_request')" + }, + "ck_gitlab_entity_bound_via": { + "name": "ck_gitlab_entity_bound_via", + "value": "\"gitlab_entity_chat_mappings\".\"bound_via\" IN ('agent_declared', 'human_declared', 'identity_target')" + }, + "ck_gitlab_entity_identity_owner": { + "name": "ck_gitlab_entity_identity_owner", + "value": "\"gitlab_entity_chat_mappings\".\"bound_via\" <> 'identity_target' OR (\"gitlab_entity_chat_mappings\".\"identity_link_id\" IS NOT NULL AND \"gitlab_entity_chat_mappings\".\"human_agent_id\" IS NOT NULL AND \"gitlab_entity_chat_mappings\".\"delegate_agent_id\" IS NOT NULL AND \"gitlab_entity_chat_mappings\".\"project_id\" IS NOT NULL)" + }, + "ck_gitlab_entity_attention_pair": { + "name": "ck_gitlab_entity_attention_pair", + "value": "(\"gitlab_entity_chat_mappings\".\"human_agent_id\" IS NULL AND \"gitlab_entity_chat_mappings\".\"delegate_agent_id\" IS NULL) OR (\"gitlab_entity_chat_mappings\".\"human_agent_id\" IS NOT NULL AND \"gitlab_entity_chat_mappings\".\"delegate_agent_id\" IS NOT NULL)" + }, + "ck_gitlab_entity_attention_mode": { + "name": "ck_gitlab_entity_attention_mode", + "value": "\"gitlab_entity_chat_mappings\".\"attention_mode\" IN ('paired', 'legacy_route_only')" + } + }, + "isRLSEnabled": false + }, + "public.gitlab_identity_links": { + "name": "gitlab_identity_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "membership_id": { + "name": "membership_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_username": { + "name": "normalized_username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_gitlab_identity_connection_membership": { + "name": "uq_gitlab_identity_connection_membership", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "membership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_gitlab_identity_connection_username": { + "name": "uq_gitlab_identity_connection_username", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "normalized_username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_gitlab_identity_org_state": { + "name": "idx_gitlab_identity_org_state", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_gitlab_identity_membership_state": { + "name": "idx_gitlab_identity_membership_state", + "columns": [ + { + "expression": "membership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gitlab_identity_links_organization_id_organizations_id_fk": { + "name": "gitlab_identity_links_organization_id_organizations_id_fk", + "tableFrom": "gitlab_identity_links", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gitlab_identity_links_membership_id_members_id_fk": { + "name": "gitlab_identity_links_membership_id_members_id_fk", + "tableFrom": "gitlab_identity_links", + "tableTo": "members", + "columnsFrom": [ + "membership_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "gitlab_identity_links_connection_id_gitlab_connections_id_fk": { + "name": "gitlab_identity_links_connection_id_gitlab_connections_id_fk", + "tableFrom": "gitlab_identity_links", + "tableTo": "gitlab_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ck_gitlab_identity_state": { + "name": "ck_gitlab_identity_state", + "value": "\"gitlab_identity_links\".\"state\" IN ('active', 'suspended')" + } + }, + "isRLSEnabled": false + }, + "public.inbox_entries": { + "name": "inbox_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "inbox_id": { + "name": "inbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "notify": { + "name": "notify", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "acked_at": { + "name": "acked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_inbox_pending": { + "name": "idx_inbox_pending", + "columns": [ + { + "expression": "inbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_inbox_pending_notify": { + "name": "idx_inbox_pending_notify", + "columns": [ + { + "expression": "inbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'pending' AND notify = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_inbox_chat_silent": { + "name": "idx_inbox_chat_silent", + "columns": [ + { + "expression": "inbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "notify", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_inbox_entries_message_status": { + "name": "idx_inbox_entries_message_status", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_inbox_unacked_cursor": { + "name": "idx_inbox_unacked_cursor", + "columns": [ + { + "expression": "inbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "notify", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status <> 'acked'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_entries_message_id_messages_id_fk": { + "name": "inbox_entries_message_id_messages_id_fk", + "tableFrom": "inbox_entries", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_inbox_delivery": { + "name": "uq_inbox_delivery", + "nullsNotDistinct": false, + "columns": [ + "inbox_id", + "message_id", + "chat_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "ck_inbox_entries_status": { + "name": "ck_inbox_entries_status", + "value": "\"inbox_entries\".\"status\" IN ('pending', 'delivered', 'acked')" + } + }, + "isRLSEnabled": false + }, + "public.invitation_redemptions": { + "name": "invitation_redemptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_invitation_redemptions_invitation": { + "name": "idx_invitation_redemptions_invitation", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invitation_redemptions_user": { + "name": "idx_invitation_redemptions_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_redemptions_invitation_id_invitations_id_fk": { + "name": "invitation_redemptions_invitation_id_invitations_id_fk", + "tableFrom": "invitation_redemptions", + "tableTo": "invitations", + "columnsFrom": [ + "invitation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_redemptions_user_id_users_id_fk": { + "name": "invitation_redemptions_user_id_users_id_fk", + "tableFrom": "invitation_redemptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitations": { + "name": "invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_invitations_token": { + "name": "idx_invitations_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invitations_org": { + "name": "idx_invitations_org", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitations_organization_id_organizations_id_fk": { + "name": "invitations_organization_id_organizations_id_fk", + "tableFrom": "invitations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitations_created_by_users_id_fk": { + "name": "invitations_created_by_users_id_fk", + "tableFrom": "invitations", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitations_token_unique": { + "name": "invitations_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "onboarding_suppressed_at": { + "name": "onboarding_suppressed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "onboarding_suppressed_reason": { + "name": "onboarding_suppressed_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "onboarding_completed_at": { + "name": "onboarding_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_members_user": { + "name": "idx_members_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_members_org": { + "name": "idx_members_org", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_user_id_users_id_fk": { + "name": "members_user_id_users_id_fk", + "tableFrom": "members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "members_organization_id_organizations_id_fk": { + "name": "members_organization_id_organizations_id_fk", + "tableFrom": "members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "members_agent_id_agents_uuid_fk": { + "name": "members_agent_id_agents_uuid_fk", + "tableFrom": "members", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "members_agent_id_unique": { + "name": "members_agent_id_unique", + "nullsNotDistinct": false, + "columns": [ + "agent_id" + ] + }, + "uq_members_user_org": { + "name": "uq_members_user_org", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "ck_members_completed_implies_suppressed": { + "name": "ck_members_completed_implies_suppressed", + "value": "\"members\".\"onboarding_completed_at\" IS NULL OR \"members\".\"onboarding_suppressed_at\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_id": { + "name": "sender_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "reply_to_inbox": { + "name": "reply_to_inbox", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reply_to_chat": { + "name": "reply_to_chat", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_messages_chat_time": { + "name": "idx_messages_chat_time", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_messages_in_reply_to": { + "name": "idx_messages_in_reply_to", + "columns": [ + { + "expression": "in_reply_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_messages_chat_source_time": { + "name": "idx_messages_chat_source_time", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_messages_mentions": { + "name": "idx_messages_mentions", + "columns": [ + { + "expression": "((\"metadata\" -> 'mentions')) jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_messages_attachment_image_id": { + "name": "idx_messages_attachment_image_id", + "columns": [ + { + "expression": "(\"content\" ->> 'imageId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_messages_content_attachments": { + "name": "idx_messages_content_attachments", + "columns": [ + { + "expression": "((\"content\" -> 'attachments')) jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_messages_metadata_attachments": { + "name": "idx_messages_metadata_attachments", + "columns": [ + { + "expression": "((\"metadata\" -> 'attachments')) jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "messages_chat_id_chats_id_fk": { + "name": "messages_chat_id_chats_id_fk", + "tableFrom": "messages", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dedup_key": { + "name": "dedup_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_notifications_org_created": { + "name": "idx_notifications_org_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notifications_agent": { + "name": "idx_notifications_agent", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notifications_org_read": { + "name": "idx_notifications_org_read", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_notifications_org_dedup_unread": { + "name": "uq_notifications_org_dedup_unread", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedup_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "read = false AND dedup_key IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_settings": { + "name": "organization_settings", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_org_settings_namespace": { + "name": "idx_org_settings_namespace", + "columns": [ + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_settings_organization_id_organizations_id_fk": { + "name": "organization_settings_organization_id_organizations_id_fk", + "tableFrom": "organization_settings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_settings_updated_by_users_id_fk": { + "name": "organization_settings_updated_by_users_id_fk", + "tableFrom": "organization_settings", + "tableTo": "users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_settings_organization_id_namespace_pk": { + "name": "organization_settings_organization_id_namespace_pk", + "columns": [ + "organization_id", + "namespace" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "max_agents": { + "name": "max_agents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_messages_per_minute": { + "name": "max_messages_per_minute", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "features": { + "name": "features", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_name_unique": { + "name": "organizations_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_questions": { + "name": "pending_questions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "answered_at": { + "name": "answered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "superseded_reason": { + "name": "superseded_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_pending_questions_agent_status": { + "name": "idx_pending_questions_agent_status", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pending_questions_chat_status": { + "name": "idx_pending_questions_chat_status", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resources": { + "name": "resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_canonical_key": { + "name": "repo_canonical_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bundle_attachment_id": { + "name": "bundle_attachment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_enabled": { + "name": "default_enabled", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "origin_template_id": { + "name": "origin_template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_component_key": { + "name": "origin_component_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_content_digest": { + "name": "origin_content_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_resources_org_type_scope": { + "name": "idx_resources_org_type_scope", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_resources_owner_agent": { + "name": "idx_resources_owner_agent", + "columns": [ + { + "expression": "owner_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_resources_repo_key": { + "name": "idx_resources_repo_key", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_canonical_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_resources_bundle_attachment": { + "name": "idx_resources_bundle_attachment", + "columns": [ + { + "expression": "bundle_attachment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_resources_team_repo_canonical_active": { + "name": "uq_resources_team_repo_canonical_active", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_canonical_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"resources\".\"type\" = 'repo' AND \"resources\".\"scope\" = 'team' AND \"resources\".\"status\" IN ('active', 'stale') AND \"resources\".\"repo_canonical_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_resources_agent_repo_canonical_active": { + "name": "uq_resources_agent_repo_canonical_active", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_canonical_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"resources\".\"type\" = 'repo' AND \"resources\".\"scope\" = 'agent' AND \"resources\".\"status\" IN ('active', 'stale') AND \"resources\".\"repo_canonical_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_resources_template_origin_active": { + "name": "uq_resources_template_origin_active", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_component_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"resources\".\"status\" IN ('active', 'stale') AND \"resources\".\"origin_template_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resources_organization_id_organizations_id_fk": { + "name": "resources_organization_id_organizations_id_fk", + "tableFrom": "resources", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resources_owner_agent_id_agents_uuid_fk": { + "name": "resources_owner_agent_id_agents_uuid_fk", + "tableFrom": "resources", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ck_resources_template_origin_consistency": { + "name": "ck_resources_template_origin_consistency", + "value": "(\"resources\".\"origin_template_id\" IS NULL AND \"resources\".\"origin_component_key\" IS NULL AND \"resources\".\"origin_content_digest\" IS NULL) OR (\"resources\".\"origin_template_id\" IS NOT NULL AND \"resources\".\"origin_component_key\" IS NOT NULL AND \"resources\".\"origin_content_digest\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.server_instances": { + "name": "server_instances", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_heartbeat": { + "name": "last_heartbeat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_events": { + "name": "session_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_session_events_chat_seq": { + "name": "uq_session_events_chat_seq", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_events_chat_created": { + "name": "idx_session_events_chat_created", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_events_context_tree_usage_recent": { + "name": "idx_session_events_context_tree_usage_recent", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"session_events\".\"kind\" = 'context_tree_usage'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_events_context_tree_io_agent_recent": { + "name": "idx_session_events_context_tree_io_agent_recent", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"session_events\".\"kind\" IN ('context_tree_usage', 'tool_call')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_events_token_usage_agent_recent": { + "name": "idx_session_events_token_usage_agent_recent", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"session_events\".\"kind\" = 'token_usage'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/server/drizzle/meta/_journal.json b/packages/server/drizzle/meta/_journal.json index 544a8a5bc..2f162ddca 100644 --- a/packages/server/drizzle/meta/_journal.json +++ b/packages/server/drizzle/meta/_journal.json @@ -638,6 +638,13 @@ "when": 1785410740640, "tag": "0090_redundant_crystal", "breakpoints": true + }, + { + "idx": 91, + "version": "7", + "when": 1785464005678, + "tag": "0091_green_archangel", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/server/src/__tests__/inbox-ack-scaling.test.ts b/packages/server/src/__tests__/inbox-ack-scaling.test.ts new file mode 100644 index 000000000..5813438ba --- /dev/null +++ b/packages/server/src/__tests__/inbox-ack-scaling.test.ts @@ -0,0 +1,237 @@ +import { and, asc, eq, sql } from "drizzle-orm"; +import type { FastifyInstance } from "fastify"; +import { describe, expect, it } from "vitest"; +import { inboxEntries } from "../db/schema/inbox-entries.js"; +import * as inboxService from "../services/inbox.js"; +import { createTestAgent, useTestApp } from "./helpers.js"; + +/** + * Regression guards for PERF-008: ACK-through must cost the in-flight delta, + * not the chat's history. + * + * Before the fix, `ackThroughEntryIdForBoundAgents` selected — and, under + * `FOR UPDATE`, locked — every notify row in the partition up to the cursor, + * including long-acked rows that can never change the outcome. These tests pin + * both halves of that defect: the scan volume and the lock footprint. + */ +describe("inbox ACK scales with the delta, not chat history", () => { + const getApp = useTestApp(); + + const HISTORY_ROWS = 3000; + + /** + * Seed `HISTORY_ROWS` already-acked notify rows into one (inbox, chat) + * partition. Written as bulk SQL rather than through the message API on + * purpose: the point is a long history, and 3000 HTTP round trips would + * dominate the test's runtime without changing what is being measured. + */ + async function seedAckedHistory(app: FastifyInstance) { + const uid = crypto.randomUUID().slice(0, 8); + const receiverName = `ackscale-r-${uid}`; + const sender = await createTestAgent(app, { name: `ackscale-s-${uid}` }); + const receiver = await createTestAgent(app, { name: receiverName }); + const chatRes = await sender.request("POST", "/api/v1/agent/chats", { + type: "group", + participantIds: [receiver.agent.uuid], + }); + const chatId: string = chatRes.json().id; + const inboxId = receiver.agent.inboxId; + + await app.db.execute(sql` + INSERT INTO messages (id, chat_id, sender_id, format, content, metadata, source, created_at) + SELECT ${`hist-${uid}-`} || g, ${chatId}, ${sender.agent.uuid}, 'text', + to_jsonb('history ' || g), '{}'::jsonb, 'api', + now() - make_interval(secs => ${HISTORY_ROWS} - g) + FROM generate_series(1, ${HISTORY_ROWS}) g + `); + await app.db.execute(sql` + INSERT INTO inbox_entries (inbox_id, message_id, chat_id, status, notify, created_at, delivered_at, acked_at) + SELECT ${inboxId}, ${`hist-${uid}-`} || g, ${chatId}, 'acked', true, + now() - make_interval(secs => ${HISTORY_ROWS} - g), + now() - make_interval(secs => ${HISTORY_ROWS} - g), + now() - make_interval(secs => ${HISTORY_ROWS} - g) + FROM generate_series(1, ${HISTORY_ROWS}) g + `); + // The planner needs statistics to prefer the partial index over a seq scan. + await app.db.execute(sql`ANALYZE inbox_entries`); + + return { sender, receiverName, chatId, inboxId }; + } + + /** Deliver one fresh notify row on top of the seeded history. */ + async function deliverOneOnTop( + app: FastifyInstance, + sender: Awaited>, + chatId: string, + receiverName: string, + inboxId: string, + ) { + await sender.request("POST", `/api/v1/agent/chats/${chatId}/messages`, { + format: "text", + content: "live trigger", + receiverNames: [receiverName], + }); + const delivered = await inboxService.claimBacklogForPush(app.db, inboxId, 10); + const entry = delivered.at(-1); + if (!entry) throw new Error("expected a delivered entry on top of the seeded history"); + return entry; + } + + /** Sum every row a plan node actually touched, across the whole plan tree. */ + function countTouchedRows(node: Record): number { + const actual = typeof node["Actual Rows"] === "number" ? node["Actual Rows"] : 0; + const filtered = typeof node["Rows Removed by Filter"] === "number" ? node["Rows Removed by Filter"] : 0; + const children = Array.isArray(node.Plans) ? (node.Plans as Array>) : []; + return actual + filtered + children.reduce((sum, child) => sum + countTouchedRows(child), 0); + } + + it("keeps the non-acked clause a literal so the partial index survives a generic plan", () => { + // First of the two guards, and the one that catches an edit to the + // service. `ne(inboxEntries.status, "acked")` reads better and produces a + // bound parameter, which PostgreSQL cannot use to prove the partial-index + // predicate once the statement switches to a generic plan — the scan then + // silently reverts to whole-partition. A plan-level assertion cannot see + // that change (it would be exercising the test's own SQL), so pin the + // property directly on the clause the service uses. + const compiled = getApp() + .db.select({ id: inboxEntries.id }) + .from(inboxEntries) + .where(inboxService.NOT_ACKED_PREFIX_ROW) + .toSQL(); + + expect(compiled.params).toEqual([]); + expect(compiled.sql).toContain("<> 'acked'"); + }); + + it("reads only the non-acked delta under a generic plan, not the whole notify history", async () => { + const app = getApp(); + const { sender, receiverName, chatId, inboxId } = await seedAckedHistory(app); + const entry = await deliverOneOnTop(app, sender, chatId, receiverName, inboxId); + + // Second guard: the planner contract the literal above depends on. It has + // to run under a forced generic plan, because that is the only mode where + // a partial index can stop matching — under a custom plan every spelling + // looks fine and the guard would be decorative. + // + // `notify` and the cursor stay bound parameters here on purpose: that is + // how the service passes them, and keeping `notify` out of the index + // predicate is what lets a parameter still act as an index condition. + const statement = `ack_prefix_guard_${crypto.randomUUID().replace(/-/g, "")}`; + const plan = await app.db.transaction(async (tx) => { + await tx.execute(sql.raw(`SET LOCAL plan_cache_mode = force_generic_plan`)); + await tx.execute( + sql.raw(` + PREPARE ${statement} (text, text, boolean, bigint) AS + SELECT id, status, delivered_at FROM inbox_entries + WHERE inbox_id = $1 AND chat_id = $2 AND notify = $3 + AND status <> 'acked' AND id <= $4 + ORDER BY id + `), + ); + // EXECUTE arguments cannot themselves be bind parameters, so the values + // are inlined. That does not weaken the test: `force_generic_plan` above + // already forced the plan to be built without knowing them. + const literal = (value: string) => `'${value.replace(/'/g, "''")}'`; + const explained = await tx.execute<{ "QUERY PLAN": Array<{ Plan: Record }> }>( + sql.raw( + `EXPLAIN (ANALYZE, FORMAT JSON) EXECUTE ${statement}(${literal(inboxId)}, ${literal(chatId)}, true, ${entry.id})`, + ), + ); + // No DEALLOCATE on the error path: PostgreSQL discards a statement + // prepared inside a transaction when that transaction rolls back. + await tx.execute(sql.raw(`DEALLOCATE ${statement}`)); + return explained[0]?.["QUERY PLAN"]?.[0]?.Plan; + }); + if (!plan) throw new Error("EXPLAIN returned no plan"); + + // One delivered row is the entire committable delta. Slack is generous + // because the exact plan node is the planner's choice; what must hold is + // that the work does not scale with HISTORY_ROWS. Pre-fix, this query + // touched HISTORY_ROWS + 1 rows. + expect(countTouchedRows(plan)).toBeLessThan(50); + expect(JSON.stringify(plan)).toContain("idx_inbox_unacked_cursor"); + }); + + it("does not lock already-acked history, so an ACK cannot queue behind it", async () => { + const app = getApp(); + const { sender, receiverName, chatId, inboxId } = await seedAckedHistory(app); + const entry = await deliverOneOnTop(app, sender, chatId, receiverName, inboxId); + + const [oldestAcked] = await app.db + .select({ id: inboxEntries.id }) + .from(inboxEntries) + .where(and(eq(inboxEntries.inboxId, inboxId), eq(inboxEntries.chatId, chatId), eq(inboxEntries.status, "acked"))) + .orderBy(asc(inboxEntries.id)) + .limit(1); + if (!oldestAcked) throw new Error("expected seeded acked history"); + + // Hold a row lock on one long-acked row, then ACK through a cursor above + // it. The pre-fix prefix scan locked every notify row up to the cursor, so + // it would block here until this transaction released. The delta scan + // never touches acked rows, so the ACK must complete while the lock is + // still held. + let releaseHolder: (() => void) | undefined; + const holderReady = new Promise((resolveReady) => { + const holding = new Promise((resolveRelease) => { + releaseHolder = resolveRelease; + }); + void app.db.transaction(async (tx) => { + await tx + .select({ id: inboxEntries.id }) + .from(inboxEntries) + .where(eq(inboxEntries.id, oldestAcked.id)) + .for("update"); + resolveReady(); + await holding; + }); + }); + await holderReady; + + try { + const ackOrTimeout = await Promise.race([ + inboxService + .ackEntryByIdForBoundAgents(app.db, entry.id, [inboxId]) + .then((res) => ({ kind: "ack", res }) as const), + new Promise<{ kind: "timeout" }>((resolveTimeout) => + setTimeout(() => resolveTimeout({ kind: "timeout" }), 5_000), + ), + ]); + + expect(ackOrTimeout.kind).toBe("ack"); + if (ackOrTimeout.kind !== "ack") throw new Error("ACK blocked on already-acked history"); + expect(ackOrTimeout.res.ok).toBe(true); + if (!ackOrTimeout.res.ok) throw new Error("ack-through unexpectedly rejected"); + expect(ackOrTimeout.res.ackedEntryIds).toEqual([entry.id]); + } finally { + releaseHolder?.(); + } + }); + + it("commits correctly with a long acked prefix in front of the cursor", async () => { + const app = getApp(); + const { sender, receiverName, chatId, inboxId } = await seedAckedHistory(app); + const entry = await deliverOneOnTop(app, sender, chatId, receiverName, inboxId); + + const accepted = await inboxService.ackEntryByIdForBoundAgents(app.db, entry.id, [inboxId]); + expect(accepted.ok).toBe(true); + if (!accepted.ok) throw new Error("ack-through unexpectedly rejected"); + expect(accepted.disposition).toBe("acked"); + expect(accepted.ackedCount).toBe(1); + expect(accepted.ackedEntryIds).toEqual([entry.id]); + expect(accepted.throughEntry.status).toBe("acked"); + + // The seeded history is untouched: ACK commits the delta, not the prefix. + const stillAcked = await app.db + .select({ count: sql`count(*)::int` }) + .from(inboxEntries) + .where(and(eq(inboxEntries.inboxId, inboxId), eq(inboxEntries.chatId, chatId), eq(inboxEntries.status, "acked"))); + expect(stillAcked[0]?.count).toBe(HISTORY_ROWS + 1); + + // A duplicate ACK over the same long prefix stays a no-op. + const duplicate = await inboxService.ackEntryByIdForBoundAgents(app.db, entry.id, [inboxId]); + expect(duplicate.ok).toBe(true); + if (!duplicate.ok) throw new Error("duplicate ack unexpectedly rejected"); + expect(duplicate.disposition).toBe("already_acked"); + expect(duplicate.ackedCount).toBe(0); + }); +}); diff --git a/packages/server/src/__tests__/inbox-delivery-indexes.test.ts b/packages/server/src/__tests__/inbox-delivery-indexes.test.ts index 3828658e3..d1690114a 100644 --- a/packages/server/src/__tests__/inbox-delivery-indexes.test.ts +++ b/packages/server/src/__tests__/inbox-delivery-indexes.test.ts @@ -28,6 +28,26 @@ describe("inbox delivery indexes", () => { expect(rows[0]?.indexdef).toContain("USING btree (message_id, status)"); }); + it("creates the partial index the ACK-through cursor scan depends on", async () => { + const rows = await getDb().execute<{ indexdef: string }>(sql` + SELECT indexdef + FROM pg_indexes + WHERE schemaname = 'public' + AND tablename = 'inbox_entries' + AND indexname = 'idx_inbox_unacked_cursor' + `); + + expect(rows).toHaveLength(1); + // Both halves are load-bearing and pinned deliberately. The key order lets + // `id <= cursor` be a range condition (no sort, ascending FOR UPDATE lock + // order), and the predicate has to stay spelled exactly this way so the + // service's literal `status <> 'acked'` clause can match it — a differently + // spelled predicate would still be correct SQL but would stop the planner + // from proving the implication, silently restoring the O(history) scan. + expect(rows[0]?.indexdef).toContain("USING btree (inbox_id, chat_id, notify, id)"); + expect(rows[0]?.indexdef).toContain("WHERE (status <> 'acked'::text)"); + }); + it("constrains inbox entry status to active delivery states", async () => { const rows = await getDb().execute<{ definition: string }>(sql` SELECT pg_get_constraintdef(oid) AS definition diff --git a/packages/server/src/__tests__/inbox-ws-push.test.ts b/packages/server/src/__tests__/inbox-ws-push.test.ts index e4a3131a6..955719df5 100644 --- a/packages/server/src/__tests__/inbox-ws-push.test.ts +++ b/packages/server/src/__tests__/inbox-ws-push.test.ts @@ -1,4 +1,4 @@ -import { and, asc, eq, inArray } from "drizzle-orm"; +import { and, asc, eq, inArray, sql } from "drizzle-orm"; import type { FastifyInstance } from "fastify"; import { describe, expect, it } from "vitest"; import { chatMembership } from "../db/schema/chat-membership.js"; @@ -728,6 +728,70 @@ describe("inbox WS data-plane claim helpers", () => { expect(accepted.ackedEntryIds).toEqual([second.id]); }); + it("ackEntryByIdForBoundAgents returns ackedEntryIds ascending when delivered and reset rows interleave", async () => { + // The commit list is assembled from two different row classes (delivered, + // and delivered-then-reset-to-pending by recovery). Interleave them so a + // future refactor that appends the classes as separate groups — or that + // trusts RETURNING's undefined row order — produces a visibly wrong order. + // `ws-client.ts` consumes this array as an ascending cursor list. + const app = getApp(); + const { a2, messageIds, rows } = await seedDeliverables(app, 4); + if (rows.length !== 4) throw new Error("expected four inbox rows"); + + for (const messageId of messageIds) { + await inboxService.claimAndBuildForPush(app.db, a2.agent.inboxId, messageId); + } + // Rows 0 and 2 look like recovery-reset deliveries; 1 and 3 stay delivered. + await app.db + .update(inboxEntries) + .set({ status: "pending" }) + .where(inArray(inboxEntries.id, [rows[0]?.id ?? 0, rows[2]?.id ?? 0])); + + const last = rows[3]; + if (!last) throw new Error("expected a fourth inbox row"); + const accepted = await inboxService.ackEntryByIdForBoundAgents(app.db, last.id, [a2.agent.inboxId]); + expect(accepted.ok).toBe(true); + if (!accepted.ok) throw new Error("ack-through unexpectedly rejected"); + expect(accepted.disposition).toBe("accepted_from_pending"); + expect(accepted.ackedEntryIds).toEqual(rows.map((row) => row.id)); + expect(accepted.ackedEntryIds).toEqual([...accepted.ackedEntryIds].sort((a, b) => a - b)); + }); + + it("ackEntryByIdForBoundAgents still rejects a prefix row whose status is outside the delivery enum", async () => { + // `ck_inbox_entries_status` is NOT VALID, so a legacy row can carry a + // status the current enum does not describe. Restricting the prefix scan + // to non-acked rows must not turn such a row into a silently skipped one: + // it is neither committable nor acked, so it still blocks the commit. + const app = getApp(); + const { a2, messageIds, rows } = await seedDeliverables(app, 2); + const first = rows[0]; + const second = rows[1]; + if (!first || !second) throw new Error("expected two inbox rows"); + + await inboxService.claimAndBuildForPush(app.db, a2.agent.inboxId, messageIds[1] ?? ""); + + // NOT VALID exempts pre-existing rows but still polices new writes, so the + // only way to reproduce a legacy row is to lift the constraint for the + // write and restore its exact definition afterwards. + const [constraint] = await app.db.execute<{ definition: string }>(sql` + SELECT pg_get_constraintdef(oid) AS definition + FROM pg_constraint WHERE conrelid = 'inbox_entries'::regclass AND conname = 'ck_inbox_entries_status' + `); + const definition = constraint?.definition; + if (!definition) throw new Error("ck_inbox_entries_status not found"); + + await app.db.execute(sql`ALTER TABLE inbox_entries DROP CONSTRAINT ck_inbox_entries_status`); + try { + await app.db.execute(sql`UPDATE inbox_entries SET status = 'failed' WHERE id = ${first.id}`); + + const rejected = await inboxService.ackEntryByIdForBoundAgents(app.db, second.id, [a2.agent.inboxId]); + expect(rejected).toEqual({ ok: false, reason: "prefix_gap" }); + } finally { + await app.db.execute(sql`UPDATE inbox_entries SET status = 'pending' WHERE id = ${first.id}`); + await app.db.execute(sql.raw(`ALTER TABLE inbox_entries ADD CONSTRAINT ck_inbox_entries_status ${definition}`)); + } + }); + it("ackEntryByIdForBoundAgents acks only entries in the supplied inbox set", async () => { const app = getApp(); const { a2, messageId } = await seedDeliverable(app); diff --git a/packages/server/src/db/schema/inbox-entries.ts b/packages/server/src/db/schema/inbox-entries.ts index 3e8fa5ef6..2d75616ac 100644 --- a/packages/server/src/db/schema/inbox-entries.ts +++ b/packages/server/src/db/schema/inbox-entries.ts @@ -55,6 +55,35 @@ export const inboxEntries = pgTable( * pending; keeping message_id first bounds that lookup by page size. */ index("idx_inbox_entries_message_status").on(table.messageId, table.status), + /** + * ACK-through cursor window. `ackThroughEntryIdForBoundAgents` walks the + * `(inbox_id, chat_id)` partition up to the acked cursor; without this + * index the planner falls back to the primary key and scans — and locks — + * every notify row ever written to the chat, which is what made ACK cost + * O(history) and lifetime cost O(N^2). + * + * `acked` is a terminal state (nothing resets an acked row back to + * `pending`/`delivered`), so restricting the index to non-acked rows keeps + * it sized to the live in-flight window instead of to chat history. + * + * Shape notes, all load-bearing: + * - `notify` sits in the key rather than in the predicate on purpose. + * A partial index is only usable when the planner can prove the query + * implies its predicate, and under a generic plan a bound parameter + * proves nothing. The service passes `notify` as a parameter + * (`eq(inboxEntries.notify, true)`), so a `WHERE notify = true` + * predicate would silently stop matching; as a key column the same + * parameter is just an ordinary index condition. This keeps the index + * dependent on exactly one inlined literal instead of two. + * - `id` is the last key column, so `id <= cursor` is an index range + * condition and `ORDER BY id` needs no sort. + * - The predicate is spelled `status <> 'acked'` to match the query + * clause verbatim; see the `NOT_ACKED_PREFIX_ROW` note in + * services/inbox.ts for why that clause must stay a literal. + */ + index("idx_inbox_unacked_cursor") + .on(table.inboxId, table.chatId, table.notify, table.id) + .where(sql`status <> 'acked'`), check("ck_inbox_entries_status", sql`${table.status} IN ('pending', 'delivered', 'acked')`), ], ); diff --git a/packages/server/src/services/inbox.ts b/packages/server/src/services/inbox.ts index 62ab8d04a..f35718ada 100644 --- a/packages/server/src/services/inbox.ts +++ b/packages/server/src/services/inbox.ts @@ -575,6 +575,40 @@ async function collectPrecedingContext( return result; } +/** Prefix row shape the ACK-through commit reasons about. */ +type AckPrefixRow = Pick; + +/** + * Restricts the ACK prefix scan to rows that can still change state. + * + * Spelled as an inline literal instead of `ne(inboxEntries.status, "acked")` + * on purpose, and this is load-bearing rather than stylistic. A partial index + * is usable only when the planner can prove the query implies its predicate. + * `connectDatabase` leaves postgres-js on its default `prepare: true`, so + * queries become named prepared statements and PostgreSQL may switch to a + * generic plan after five executions; a generic plan keeps a bound parameter + * as a `Param` node, which proves nothing. The scan then falls back to + * `idx_inbox_chat_silent` and filters the whole partition — restoring the + * O(history) behavior in production, silently, while every benchmark that + * passes constants still looks fast. Observed on the default + * `plan_cache_mode = auto` (not only under `force_generic_plan`): + * + * status <> 'acked' -> Index Scan using idx_inbox_unacked_cursor + * status <> $n -> Index Scan using idx_inbox_chat_silent, + * Rows Removed by Filter: + * + * Do not "normalize" this into a Drizzle comparison helper. Exported so + * `inbox-ack-scaling.test.ts` can assert it still compiles to zero bind + * parameters. + * + * The literal also has to stay a plain inequality against a NOT NULL column: + * SQL's `<>` is three-valued and would drop a NULL status, whereas the JS gap + * check below treats an unexpected status as a gap. `inbox_entries.status` is + * NOT NULL, so the two agree; relaxing that column would silently reintroduce + * a divergence here. + */ +export const NOT_ACKED_PREFIX_ROW = sql`${inboxEntries.status} <> 'acked'`; + /** * Commit inbox progress through the supplied entry id from the WS data plane, * scoped to the inboxes the connected socket has bound. @@ -585,6 +619,15 @@ async function collectPrecedingContext( * prefix are atomically marked `acked`; non-committable gaps reject the commit * so the database cannot persist `A pending, B acked`. * + * **Delta scan.** The prefix scan reads only rows that are not yet `acked`. + * Already-acked rows contribute nothing to any of the three decisions made + * below — they never trigger a gap, are never committable, and are never + * reset-from-pending — so excluding them in SQL is an exact equivalence, not + * an approximation. This is what keeps per-ACK cost proportional to the + * in-flight window instead of to chat history, and it holds only because + * `acked` is terminal: `recoverUnackedForScope` and `resetDeliveredForInboxes` + * reset `delivered` rows, never `acked` ones. + * * Trusts only the `inboxId` set the connected socket has bound (no `inboxId` * on the wire), and short-circuits on an empty `inboxIds`. */ @@ -607,27 +650,42 @@ export async function ackThroughEntryIdForBoundAgents( const chatPredicate = entry.chatId === null ? isNull(inboxEntries.chatId) : eq(inboxEntries.chatId, entry.chatId); const prefixRows = await tx - .select() + .select({ id: inboxEntries.id, status: inboxEntries.status, deliveredAt: inboxEntries.deliveredAt }) .from(inboxEntries) .where( and( eq(inboxEntries.inboxId, entry.inboxId), chatPredicate, eq(inboxEntries.notify, true), + NOT_ACKED_PREFIX_ROW, sql`${inboxEntries.id} <= ${entryId}`, ), ) .orderBy(asc(inboxEntries.id)) .for("update"); - const isResetDeliveredRow = (row: ClaimedEntry): boolean => row.status === "pending" && row.deliveredAt !== null; + const isResetDeliveredRow = (row: AckPrefixRow): boolean => row.status === "pending" && row.deliveredAt !== null; + // `status !== "acked"` is redundant against the SQL predicate above and + // deliberately kept: `ck_inbox_entries_status` is NOT VALID, so a legacy + // row could still carry a status outside the current enum. Keeping the + // test verbatim means such a row keeps rejecting the commit exactly as + // it does today instead of being silently skipped. if (prefixRows.some((row) => row.status !== "acked" && row.status !== "delivered" && !isResetDeliveredRow(row))) { return { ok: false, reason: "prefix_gap" }; } - const deliveredIds = prefixRows.filter((row) => row.status === "delivered").map((row) => row.id); - const resetPendingIds = prefixRows.filter(isResetDeliveredRow).map((row) => row.id); - const committableIds = [...deliveredIds, ...resetPendingIds]; + // Single pass over the id-ordered prefix, so `committableIds` is + // ascending by construction rather than by two concatenated filters. + const committableIds: number[] = []; + let resetFromPendingCount = 0; + for (const row of prefixRows) { + if (row.status === "delivered") { + committableIds.push(row.id); + } else if (isResetDeliveredRow(row)) { + committableIds.push(row.id); + resetFromPendingCount++; + } + } const ackedAt = new Date(); const drainPendingSilentRows = async (): Promise => { await tx @@ -661,11 +719,15 @@ export async function ackThroughEntryIdForBoundAgents( .where(and(inArray(inboxEntries.id, committableIds), inArray(inboxEntries.status, ["delivered", "pending"]))) .returning(); await drainPendingSilentRows(); + // UPDATE ... RETURNING has no defined row order, so sort explicitly: + // the WS layer treats `ackedEntryIds` as an ascending cursor list when + // it clears same-socket in-flight accounting. + updated.sort((a, b) => a.id - b.id); const updatedThroughEntry = updated.find((row) => row.id === entryId) ?? entry; return { ok: true, throughEntry: updatedThroughEntry, - disposition: resetPendingIds.length > 0 ? "accepted_from_pending" : "acked", + disposition: resetFromPendingCount > 0 ? "accepted_from_pending" : "acked", ackedCount: updated.length, ackedEntryIds: updated.map((row) => row.id), }; From 626fb104c54ced4ea6143407cda74f38e3899052 Mon Sep 17 00:00:00 2001 From: Bestony Date: Fri, 31 Jul 2026 10:49:10 +0800 Subject: [PATCH 2/4] docs(server): correct what protects legacy inbox statuses during ACK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review caught a misattribution in the previous commit's comments. The retained `status !== "acked"` term in the gap check is unreachable — the SQL predicate already excludes acked rows — so it is defensive redundancy, not the thing that keeps a legacy out-of-enum status from being committed past. That protection comes from the SQL clause being an exclusion rather than an allow-list. `status IN ('pending', 'delivered')` selects the same rows today and matches the same partial index, but it would drop a legacy `'failed'` row out of the prefix entirely, leaving the gap check blind to it. Verified both directions: removing the JS term changes no test outcome, while swapping the clause to the allow-list form makes the legacy-status case return ok:true instead of prefix_gap. Comments only; no logic change. --- .../src/__tests__/inbox-ack-scaling.test.ts | 8 ++++++ .../src/__tests__/inbox-ws-push.test.ts | 9 ++++--- packages/server/src/services/inbox.ts | 27 ++++++++++++------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/packages/server/src/__tests__/inbox-ack-scaling.test.ts b/packages/server/src/__tests__/inbox-ack-scaling.test.ts index 5813438ba..903b757c8 100644 --- a/packages/server/src/__tests__/inbox-ack-scaling.test.ts +++ b/packages/server/src/__tests__/inbox-ack-scaling.test.ts @@ -116,6 +116,14 @@ describe("inbox ACK scales with the delta, not chat history", () => { // `notify` and the cursor stay bound parameters here on purpose: that is // how the service passes them, and keeping `notify` out of the index // predicate is what lets a parameter still act as an index condition. + // + // Known boundary: this exercises an equivalent statement, not the query + // object the service builds. Together with the literal guard above it + // covers the clause and the index contract, but a future restructuring of + // the service's own WHERE (an added OR branch, say) could stop matching the + // index without either guard noticing. Closing that would mean exporting + // the query builder purely for the test; the coupling was judged not worth + // it, so the gap is recorded here instead of left implicit. const statement = `ack_prefix_guard_${crypto.randomUUID().replace(/-/g, "")}`; const plan = await app.db.transaction(async (tx) => { await tx.execute(sql.raw(`SET LOCAL plan_cache_mode = force_generic_plan`)); diff --git a/packages/server/src/__tests__/inbox-ws-push.test.ts b/packages/server/src/__tests__/inbox-ws-push.test.ts index 955719df5..14f1b9d0f 100644 --- a/packages/server/src/__tests__/inbox-ws-push.test.ts +++ b/packages/server/src/__tests__/inbox-ws-push.test.ts @@ -759,9 +759,12 @@ describe("inbox WS data-plane claim helpers", () => { it("ackEntryByIdForBoundAgents still rejects a prefix row whose status is outside the delivery enum", async () => { // `ck_inbox_entries_status` is NOT VALID, so a legacy row can carry a - // status the current enum does not describe. Restricting the prefix scan - // to non-acked rows must not turn such a row into a silently skipped one: - // it is neither committable nor acked, so it still blocks the commit. + // status the current enum does not describe. This pins the prefix scan as + // an *exclusion* (`status <> 'acked'`) rather than an allow-list + // (`status IN ('pending','delivered')`): an allow-list drops such a row + // from the prefix entirely, so the gap check never sees it and the commit + // silently steps over it. Verified — swapping the clause to the allow-list + // form makes this case return ok:true instead of prefix_gap. const app = getApp(); const { a2, messageIds, rows } = await seedDeliverables(app, 2); const first = rows[0]; diff --git a/packages/server/src/services/inbox.ts b/packages/server/src/services/inbox.ts index f35718ada..02372e188 100644 --- a/packages/server/src/services/inbox.ts +++ b/packages/server/src/services/inbox.ts @@ -601,11 +601,19 @@ type AckPrefixRow = Pick; * `inbox-ack-scaling.test.ts` can assert it still compiles to zero bind * parameters. * - * The literal also has to stay a plain inequality against a NOT NULL column: - * SQL's `<>` is three-valued and would drop a NULL status, whereas the JS gap - * check below treats an unexpected status as a gap. `inbox_entries.status` is - * NOT NULL, so the two agree; relaxing that column would silently reintroduce - * a divergence here. + * The *exclusion* spelling is load-bearing too, independently of the planner. + * `status IN ('pending', 'delivered')` selects the same rows today and would + * match the same partial index, but it is an allow-list: `ck_inbox_entries_status` + * is NOT VALID, so a legacy row can carry a status outside today's enum, and an + * allow-list would drop that row from the prefix entirely — the gap check would + * never see it and the commit would silently step over it. Excluding only + * `acked` keeps every unexpected status visible to the gap check, which is + * where it must be handled. + * + * Finally, this has to stay a plain inequality against a NOT NULL column: SQL's + * `<>` is three-valued and would drop a NULL status, whereas the JS gap check + * below treats an unexpected status as a gap. `inbox_entries.status` is NOT + * NULL, so the two agree; relaxing that column would reintroduce a divergence. */ export const NOT_ACKED_PREFIX_ROW = sql`${inboxEntries.status} <> 'acked'`; @@ -665,11 +673,10 @@ export async function ackThroughEntryIdForBoundAgents( .for("update"); const isResetDeliveredRow = (row: AckPrefixRow): boolean => row.status === "pending" && row.deliveredAt !== null; - // `status !== "acked"` is redundant against the SQL predicate above and - // deliberately kept: `ck_inbox_entries_status` is NOT VALID, so a legacy - // row could still carry a status outside the current enum. Keeping the - // test verbatim means such a row keeps rejecting the commit exactly as - // it does today instead of being silently skipped. + // `status !== "acked"` is unreachable now that the SQL excludes acked + // rows, and is kept only as defensive redundancy — it is not what + // protects legacy out-of-enum rows. That protection lives in the SQL + // being an exclusion rather than an allow-list; see NOT_ACKED_PREFIX_ROW. if (prefixRows.some((row) => row.status !== "acked" && row.status !== "delivered" && !isResetDeliveredRow(row))) { return { ok: false, reason: "prefix_gap" }; } From 5f629be9b76811bf4d0d803e9684216bc88e19c6 Mon Sep 17 00:00:00 2001 From: Bestony Date: Fri, 31 Jul 2026 19:42:08 +0800 Subject: [PATCH 3/4] docs(server): scope the generic-plan rationale to what was measured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment on NOT_ACKED_PREFIX_ROW presented the partial-index fallback as observed production behavior. Driving this service through the real postgres-js path (prepare: true, default plan_cache_mode = auto) against a 60k-row history says otherwise: PostgreSQL 16 and 17 both kept planning custom for 20+ executions, and with the clause written as a bound parameter the index was still used on every one of them — a custom plan substitutes the constant, so either spelling matches. Whether a generic plan gets used at all is a cost-based, dataset-dependent decision. It has been observed on other data, and an operator can force it globally, in which case only the literal keeps the index. That is the real argument for the literal: it removes a dependency on a planner decision the code does not control, not that a regression was observed in production. Comments only; no logic change. --- packages/server/src/services/inbox.ts | 34 ++++++++++++++------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/server/src/services/inbox.ts b/packages/server/src/services/inbox.ts index 02372e188..f2c763207 100644 --- a/packages/server/src/services/inbox.ts +++ b/packages/server/src/services/inbox.ts @@ -582,24 +582,26 @@ type AckPrefixRow = Pick; * Restricts the ACK prefix scan to rows that can still change state. * * Spelled as an inline literal instead of `ne(inboxEntries.status, "acked")` - * on purpose, and this is load-bearing rather than stylistic. A partial index - * is usable only when the planner can prove the query implies its predicate. - * `connectDatabase` leaves postgres-js on its default `prepare: true`, so - * queries become named prepared statements and PostgreSQL may switch to a - * generic plan after five executions; a generic plan keeps a bound parameter - * as a `Param` node, which proves nothing. The scan then falls back to - * `idx_inbox_chat_silent` and filters the whole partition — restoring the - * O(history) behavior in production, silently, while every benchmark that - * passes constants still looks fast. Observed on the default - * `plan_cache_mode = auto` (not only under `force_generic_plan`): + * on purpose. A partial index is usable only when the planner can prove the + * query implies its predicate, and a generic plan keeps a bound parameter as a + * `Param` node, which proves nothing — the scan then filters the whole + * partition and the O(history) behavior comes back. * - * status <> 'acked' -> Index Scan using idx_inbox_unacked_cursor - * status <> $n -> Index Scan using idx_inbox_chat_silent, - * Rows Removed by Filter: + * Whether PostgreSQL actually uses a generic plan is its own cost-based + * decision, and it is dataset-dependent rather than guaranteed. Driving this + * service through the real postgres-js path (`prepare: true`, default + * `plan_cache_mode = auto`) against a 60k-row history, PostgreSQL 16 and 17 + * both kept re-planning as custom for 20+ executions, and the bound-parameter + * spelling did *not* degrade there; a custom plan substitutes the constant, so + * either spelling matches the index. The switch has been observed on other + * data, and an operator can force it globally with `plan_cache_mode = + * force_generic_plan`, in which case only the literal keeps the index. * - * Do not "normalize" this into a Drizzle comparison helper. Exported so - * `inbox-ack-scaling.test.ts` can assert it still compiles to zero bind - * parameters. + * So this is not working around an observed production regression: it removes + * the dependency on a planner decision the code does not control, for the cost + * of one inlined constant. Do not "normalize" it into a Drizzle comparison + * helper. Exported so `inbox-ack-scaling.test.ts` can assert it still compiles + * to zero bind parameters. * * The *exclusion* spelling is load-bearing too, independently of the planner. * `status IN ('pending', 'delivered')` selects the same rows today and would From 6578f78d7feb0ffc57eaa95029bdabc70a7c9e2d Mon Sep 17 00:00:00 2001 From: Bestony Date: Fri, 31 Jul 2026 19:53:35 +0800 Subject: [PATCH 4/4] docs(server): state the literal predicate's real justification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified in the driver source rather than inferred from plan_cache_mode experiments: drizzle-orm's postgres-js session issues every query through `client.unsafe(query, params)`, and postgres-js `unsafe()` hardcodes `prepare: false` with no options passed to override it. These statements are therefore unnamed — one-shot plans, always planned as custom, with parameters substituted before planning. `connectDatabase`'s implicit `prepare: true` never reaches a Drizzle query. Two earlier claims fall out as a result. Generic plans are not merely unlikely here, they are unreachable, so "PostgreSQL kept choosing custom plans" described a cost decision that is not being made. And `plan_cache_mode = force_generic_plan` cannot rescue or break this query either, since it only governs cached statements — measured on 16.14 and 17, the bound-parameter spelling keeps the index under it. The literal stays, with the justification it can actually support: it is insurance against this query becoming a named statement (a `.prepare()`, a driver change, a prepare-capable pooler), for the cost of one inlined constant. The scaling guard keeps forcing a generic plan and now says so explicitly — it models that hypothetical, not production. Also separates the exclusion-vs-allow-list argument, which does bite today, from the literal-vs-parameter one, which does not. Comments only; no logic change. --- .../src/__tests__/inbox-ack-scaling.test.ts | 7 ++++ packages/server/src/services/inbox.ts | 39 +++++++++---------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/server/src/__tests__/inbox-ack-scaling.test.ts b/packages/server/src/__tests__/inbox-ack-scaling.test.ts index 903b757c8..97f0e7a5f 100644 --- a/packages/server/src/__tests__/inbox-ack-scaling.test.ts +++ b/packages/server/src/__tests__/inbox-ack-scaling.test.ts @@ -113,6 +113,13 @@ describe("inbox ACK scales with the delta, not chat history", () => { // a partial index can stop matching — under a custom plan every spelling // looks fine and the guard would be decorative. // + // Note this deliberately models a path the application does not currently + // take: Drizzle goes through postgres-js `unsafe()`, so its statements are + // unnamed and always planned as custom, and `plan_cache_mode` does not + // reach them. This is the worst case the inline literal insures against + // (a named statement, from a `.prepare()` or a prepare-capable pooler), + // not a description of production behavior. + // // `notify` and the cursor stay bound parameters here on purpose: that is // how the service passes them, and keeping `notify` out of the index // predicate is what lets a parameter still act as an index condition. diff --git a/packages/server/src/services/inbox.ts b/packages/server/src/services/inbox.ts index f2c763207..d44a29eac 100644 --- a/packages/server/src/services/inbox.ts +++ b/packages/server/src/services/inbox.ts @@ -581,29 +581,28 @@ type AckPrefixRow = Pick; /** * Restricts the ACK prefix scan to rows that can still change state. * - * Spelled as an inline literal instead of `ne(inboxEntries.status, "acked")` - * on purpose. A partial index is usable only when the planner can prove the - * query implies its predicate, and a generic plan keeps a bound parameter as a - * `Param` node, which proves nothing — the scan then filters the whole - * partition and the O(history) behavior comes back. + * Spelled as an inline literal instead of `ne(inboxEntries.status, "acked")`. + * A partial index applies only when the planner can prove the query implies its + * predicate, and a bound parameter in a *cached* (named) statement proves + * nothing. * - * Whether PostgreSQL actually uses a generic plan is its own cost-based - * decision, and it is dataset-dependent rather than guaranteed. Driving this - * service through the real postgres-js path (`prepare: true`, default - * `plan_cache_mode = auto`) against a 60k-row history, PostgreSQL 16 and 17 - * both kept re-planning as custom for 20+ executions, and the bound-parameter - * spelling did *not* degrade there; a custom plan substitutes the constant, so - * either spelling matches the index. The switch has been observed on other - * data, and an operator can force it globally with `plan_cache_mode = - * force_generic_plan`, in which case only the literal keeps the index. + * That does not happen on today's path. Drizzle issues every query through + * postgres-js `unsafe()`, which hardcodes `prepare: false` and is passed no + * options, so this statement is unnamed: a one-shot plan, always planned as + * custom, with the parameter substituted before planning. Measured through the + * real driver on PostgreSQL 16.14 and 17, the bound-parameter spelling keeps + * using `idx_inbox_unacked_cursor` — including under `plan_cache_mode = + * force_generic_plan`, because that setting only governs cached statements. * - * So this is not working around an observed production regression: it removes - * the dependency on a planner decision the code does not control, for the cost - * of one inlined constant. Do not "normalize" it into a Drizzle comparison - * helper. Exported so `inbox-ack-scaling.test.ts` can assert it still compiles - * to zero bind parameters. + * The literal is therefore insurance, not a fix for an observed regression: it + * costs one inlined constant and stays correct if this query ever becomes a + * named statement (a Drizzle `.prepare()`, a driver change, a pooler that + * prepares). Do not "normalize" it into a Drizzle comparison helper. Exported + * so `inbox-ack-scaling.test.ts` can assert it still compiles to zero bind + * parameters. * - * The *exclusion* spelling is load-bearing too, independently of the planner. + * Separately from the literal-vs-parameter question above — and unlike it, + * this one does bite today — the predicate has to stay an *exclusion*. * `status IN ('pending', 'delivered')` selects the same rows today and would * match the same partial index, but it is an allow-list: `ck_inbox_entries_status` * is NOT VALID, so a legacy row can carry a status outside today's enum, and an