From 8aae388ac7fb35f7360f257e79102ee56e046391 Mon Sep 17 00:00:00 2001 From: Bestony Date: Wed, 29 Jul 2026 01:09:51 +0800 Subject: [PATCH 1/5] perf(inbox): assemble preceding context in a single query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preceding-context assembly issued one query per claimed trigger plus one per chat, all inside the delivery transaction, so a 50-entry drain held its locks across 50+ sequential round-trips. Each trigger's lower bound is statically derivable β€” within a chat it is the previous trigger's id, and only the first trigger has to look outside the batch β€” so nothing depends on a previous iteration's result. Collapse the fan-out into one statement: `jsonb_to_recordset` for the batch, `lag()` for the in-batch bound, a short-circuiting `COALESCE` scalar subquery for the out-of-batch bound, and `CROSS JOIN LATERAL` for each trigger's window. The lock stays inside the LATERAL subquery. PostgreSQL rejects `FOR UPDATE` at any query level that also carries a window function, and on the nullable side of an outer join, so this is the only shape that keeps `FOR UPDATE OF e SKIP LOCKED` intact. Per-trigger ranges are disjoint by construction, so one statement locks exactly the rows the loop would have. Extend `idx_inbox_chat_silent` with a trailing `id`. The window bounds are correlated values inside the LATERAL, so the planner cannot estimate their width and would scan every silent row in the chat before discarding almost all of them. With `id` indexed the range collapses into the index scan. The new shape is a superset of the old one, so it replaces rather than adds an index. Ordering is unchanged: the LATERAL keeps `messages.created_at DESC` + LIMIT so the rows nearest the trigger survive the cap, and the outer query orders ascending for the prompt. The outer ORDER BY is required β€” a subquery's ordering is not carried to the outer level. Timestamps are rendered to ISO 8601 in SQL because a raw statement returns PostgreSQL's own timestamp text rather than a Date. --- .../server/drizzle/0084_colorful_gargoyle.sql | 2 + packages/server/drizzle/LATEST | 2 +- .../server/drizzle/meta/0084_snapshot.json | 5856 +++++++++++++++++ packages/server/drizzle/meta/_journal.json | 7 + .../inbox-preceding-context-batch.test.ts | 307 + .../server/src/db/schema/inbox-entries.ts | 11 +- packages/server/src/services/inbox.ts | 291 +- 7 files changed, 6368 insertions(+), 108 deletions(-) create mode 100644 packages/server/drizzle/0084_colorful_gargoyle.sql create mode 100644 packages/server/drizzle/meta/0084_snapshot.json create mode 100644 packages/server/src/__tests__/inbox-preceding-context-batch.test.ts diff --git a/packages/server/drizzle/0084_colorful_gargoyle.sql b/packages/server/drizzle/0084_colorful_gargoyle.sql new file mode 100644 index 000000000..00f2aabbb --- /dev/null +++ b/packages/server/drizzle/0084_colorful_gargoyle.sql @@ -0,0 +1,2 @@ +DROP INDEX "idx_inbox_chat_silent";--> statement-breakpoint +CREATE INDEX "idx_inbox_chat_silent" ON "inbox_entries" USING btree ("inbox_id","chat_id","notify","status","id"); \ No newline at end of file diff --git a/packages/server/drizzle/LATEST b/packages/server/drizzle/LATEST index 0a280305d..69f42d505 100644 --- a/packages/server/drizzle/LATEST +++ b/packages/server/drizzle/LATEST @@ -1 +1 @@ -0083_tiresome_bucky +0084_colorful_gargoyle diff --git a/packages/server/drizzle/meta/0084_snapshot.json b/packages/server/drizzle/meta/0084_snapshot.json new file mode 100644 index 000000000..7b90576e2 --- /dev/null +++ b/packages/server/drizzle/meta/0084_snapshot.json @@ -0,0 +1,5856 @@ +{ + "id": "8ccbbcc3-bcc7-4799-b50c-5734dbf91c92", + "prevId": "a85cf597-fe4f-431a-941b-dd1b309f6f0f", + "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 + }, + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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 + }, + "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": {} + } + }, + "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": {}, + "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 + }, + "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": true + }, + "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()" + } + }, + "indexes": { + "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_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" + }, + { + "expression": "id", + "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": {} + } + }, + "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": {} + } + }, + "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 + }, + "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 + }, + "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": {} + }, + "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": {} + } + }, + "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": {}, + "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 84b8bc89b..c2ccaf9dd 100644 --- a/packages/server/drizzle/meta/_journal.json +++ b/packages/server/drizzle/meta/_journal.json @@ -589,6 +589,13 @@ "when": 1784805906735, "tag": "0083_tiresome_bucky", "breakpoints": true + }, + { + "idx": 84, + "version": "7", + "when": 1785258368232, + "tag": "0084_colorful_gargoyle", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/server/src/__tests__/inbox-preceding-context-batch.test.ts b/packages/server/src/__tests__/inbox-preceding-context-batch.test.ts new file mode 100644 index 000000000..21c423121 --- /dev/null +++ b/packages/server/src/__tests__/inbox-preceding-context-batch.test.ts @@ -0,0 +1,307 @@ +import { and, asc, eq } from "drizzle-orm"; +import type { FastifyInstance } from "fastify"; +import { describe, expect, it } from "vitest"; +import { inboxEntries } from "../db/schema/inbox-entries.js"; +import { messages } from "../db/schema/messages.js"; +import { createChat } from "../services/chat.js"; +import * as inboxService from "../services/inbox.js"; +import { sendMessage } from "../services/message.js"; +import { createTestAgent, useTestApp } from "./helpers.js"; + +/** + * Preceding-context assembly is built from one set-based statement rather than + * one query per claimed trigger. The invariants below are the ones a batched + * implementation can silently break while single-trigger tests stay green: + * per-trigger window boundaries inside a single drain, chat partitioning, + * and the ordering contract. + */ +describe("batched preceding-context assembly", () => { + const getApp = useTestApp(); + + async function seedChatWithObserver(app: FastifyInstance, uid: string) { + const human = await createTestAgent(app, { type: "human", name: `pcb-h-${uid}` }); + const observer = await createTestAgent(app, { type: "agent", name: `pcb-obs-${uid}` }); + const chat = await createChat(app.db, human.agent.uuid, { + type: "group", + participantIds: [observer.agent.uuid], + }); + return { human, observer, chat }; + } + + /** Message nobody is addressed in β€” lands as a silent (notify=false) row. */ + async function sendSilent(app: FastifyInstance, chatId: string, senderId: string, content: string) { + return sendMessage( + app.db, + chatId, + senderId, + { source: "api", format: "text", content }, + { allowRecipientlessSend: true }, + ); + } + + /** Message that @mentions the observer β€” lands as a notify=true trigger. */ + async function sendTrigger( + app: FastifyInstance, + chatId: string, + senderId: string, + observerId: string, + content: string, + ) { + return sendMessage(app.db, chatId, senderId, { + source: "api", + format: "text", + content, + metadata: { mentions: [observerId] }, + }); + } + + function precedingContents(delivery: { message: { precedingMessages: Array<{ content: unknown }> } }): unknown[] { + return delivery.message.precedingMessages.map((p) => p.content); + } + + it("gives each trigger in one drain its own window instead of a shared lower bound", async () => { + const app = getApp(); + const uid = crypto.randomUUID().slice(0, 6); + const { human, observer, chat } = await seedChatWithObserver(app, uid); + + // silent-a | T1 | silent-b | T2 | silent-c | T3 β€” all claimed in one drain. + await sendSilent(app, chat.id, human.agent.uuid, "silent-a"); + await sendTrigger(app, chat.id, human.agent.uuid, observer.agent.uuid, "trigger-1"); + await sendSilent(app, chat.id, human.agent.uuid, "silent-b"); + await sendTrigger(app, chat.id, human.agent.uuid, observer.agent.uuid, "trigger-2"); + await sendSilent(app, chat.id, human.agent.uuid, "silent-c"); + await sendTrigger(app, chat.id, human.agent.uuid, observer.agent.uuid, "trigger-3"); + + const claimed = await inboxService.claimBacklogForPush(app.db, observer.agent.inboxId, 10); + expect(claimed.map((entry) => entry.message.content)).toEqual(["trigger-1", "trigger-2", "trigger-3"]); + + // A shared lower bound would leak earlier silent rows into later triggers. + expect(claimed.map(precedingContents)).toEqual([["silent-a"], ["silent-b"], ["silent-c"]]); + }); + + it("keeps per-chat windows separate when one drain spans several chats", async () => { + const app = getApp(); + const uid = crypto.randomUUID().slice(0, 6); + const human = await createTestAgent(app, { type: "human", name: `pcb-mh-${uid}` }); + const observer = await createTestAgent(app, { type: "agent", name: `pcb-mobs-${uid}` }); + const chatOne = await createChat(app.db, human.agent.uuid, { + type: "group", + participantIds: [observer.agent.uuid], + }); + const chatTwo = await createChat(app.db, human.agent.uuid, { + type: "group", + participantIds: [observer.agent.uuid], + }); + + await sendSilent(app, chatOne.id, human.agent.uuid, "one-silent"); + await sendSilent(app, chatTwo.id, human.agent.uuid, "two-silent"); + await sendTrigger(app, chatOne.id, human.agent.uuid, observer.agent.uuid, "one-trigger"); + await sendTrigger(app, chatTwo.id, human.agent.uuid, observer.agent.uuid, "two-trigger"); + + const claimed = await inboxService.claimBacklogForPush(app.db, observer.agent.inboxId, 10); + const byContent = new Map(claimed.map((entry) => [entry.message.content, entry])); + + const one = byContent.get("one-trigger"); + const two = byContent.get("two-trigger"); + if (!one || !two) throw new Error("expected both chat triggers in one drain"); + // Chat two's silent row sits between chat one's silent row and its + // trigger by inbox id, so an unpartitioned window would capture it. + expect(precedingContents(one)).toEqual(["one-silent"]); + expect(precedingContents(two)).toEqual(["two-silent"]); + }); + + it("orders preceding context by message time, not by inbox row order", async () => { + const app = getApp(); + const uid = crypto.randomUUID().slice(0, 6); + const { human, observer, chat } = await seedChatWithObserver(app, uid); + + const first = await sendSilent(app, chat.id, human.agent.uuid, "written-first"); + const second = await sendSilent(app, chat.id, human.agent.uuid, "written-second"); + const third = await sendSilent(app, chat.id, human.agent.uuid, "written-third"); + + // Make message chronology disagree with inbox insertion order: the row + // written second is now the oldest message. Ordering by the inbox row + // would return written-first/second/third; ordering by message time β€” + // the actual contract β€” returns written-second/first/third. + const base = Date.now() - 60_000; + await app.db + .update(messages) + .set({ createdAt: new Date(base) }) + .where(eq(messages.id, second.message.id)); + await app.db + .update(messages) + .set({ createdAt: new Date(base + 1_000) }) + .where(eq(messages.id, first.message.id)); + await app.db + .update(messages) + .set({ createdAt: new Date(base + 2_000) }) + .where(eq(messages.id, third.message.id)); + + await sendTrigger(app, chat.id, human.agent.uuid, observer.agent.uuid, "order-trigger"); + + const claimed = await inboxService.claimBacklogForPush(app.db, observer.agent.inboxId, 10); + const delivery = claimed[0]; + if (!delivery) throw new Error("expected trigger delivery"); + expect(precedingContents(delivery)).toEqual(["written-second", "written-first", "written-third"]); + // Rendered timestamps must stay ISO 8601 β€” the statement formats them in + // SQL because a raw execute returns PostgreSQL's own text form. + for (const preceding of delivery.message.precedingMessages) { + expect(preceding.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + } + }); + + it("closes the window at an already-acked earlier notify row", async () => { + const app = getApp(); + const uid = crypto.randomUUID().slice(0, 6); + const { human, observer, chat } = await seedChatWithObserver(app, uid); + + await sendSilent(app, chat.id, human.agent.uuid, "before-acked-trigger"); + await sendTrigger(app, chat.id, human.agent.uuid, observer.agent.uuid, "acked-trigger"); + + const firstClaim = await inboxService.claimBacklogForPush(app.db, observer.agent.inboxId, 10); + const firstDelivery = firstClaim[0]; + if (!firstDelivery) throw new Error("expected first delivery"); + const acked = await inboxService.ackEntryByIdForBoundAgents(app.db, firstDelivery.id, [observer.agent.inboxId]); + expect(acked.ok).toBe(true); + + await sendSilent(app, chat.id, human.agent.uuid, "after-acked-trigger"); + await sendTrigger(app, chat.id, human.agent.uuid, observer.agent.uuid, "second-trigger"); + + const secondClaim = await inboxService.claimBacklogForPush(app.db, observer.agent.inboxId, 10); + const secondDelivery = secondClaim[0]; + if (!secondDelivery) throw new Error("expected second delivery"); + // The bound query has no status filter on purpose: an acked notify row + // still closes the window, so the pre-trigger silent row stays out. + expect(precedingContents(secondDelivery)).toEqual(["after-acked-trigger"]); + }); + + it("skips rows with no chat while still bundling chat-scoped triggers", async () => { + const app = getApp(); + const uid = crypto.randomUUID().slice(0, 6); + const { human, observer, chat } = await seedChatWithObserver(app, uid); + + await sendSilent(app, chat.id, human.agent.uuid, "scoped-silent"); + const trigger = await sendTrigger(app, chat.id, human.agent.uuid, observer.agent.uuid, "scoped-trigger"); + + // A legacy null-chat row claimed in the same drain must not break the + // batch, and must carry no context of its own. + await app.db.insert(inboxEntries).values({ + inboxId: observer.agent.inboxId, + messageId: trigger.message.id, + chatId: null, + status: "pending", + notify: true, + }); + + const claimed = await inboxService.claimBacklogForPush(app.db, observer.agent.inboxId, 10); + expect(claimed).toHaveLength(2); + const scoped = claimed.find((entry) => entry.chatId === chat.id); + const unscoped = claimed.find((entry) => entry.chatId === null); + if (!scoped || !unscoped) throw new Error("expected both scoped and null-chat entries"); + expect(precedingContents(scoped)).toEqual(["scoped-silent"]); + expect(unscoped.message.precedingMessages).toEqual([]); + }); + + it("assembles context in one statement regardless of how many triggers a drain claims", async () => { + const app = getApp(); + const uid = crypto.randomUUID().slice(0, 6); + const { human, observer, chat } = await seedChatWithObserver(app, uid); + + const triggerCount = 8; + for (let i = 0; i < triggerCount; i++) { + await sendSilent(app, chat.id, human.agent.uuid, `silent-${i}`); + await sendTrigger(app, chat.id, human.agent.uuid, observer.agent.uuid, `trigger-${i}`); + } + + const probe = countingDb(app.db); + const claimed = await inboxService.claimBacklogForPush(probe.db, observer.agent.inboxId, 50); + expect(claimed).toHaveLength(triggerCount); + expect(claimed.map(precedingContents)).toEqual(Array.from({ length: triggerCount }, (_, i) => [`silent-${i}`])); + + // The batched preceding-context query is the only raw statement on this + // path, so this pins the cost at O(1) rather than O(triggers). The old + // per-trigger loop would report `triggerCount` here. + expect(probe.executeCalls()).toBe(1); + }); + + it("skips silent rows another transaction holds instead of blocking on them", async () => { + const app = getApp(); + const uid = crypto.randomUUID().slice(0, 6); + const { human, observer, chat } = await seedChatWithObserver(app, uid); + + const held = await sendSilent(app, chat.id, human.agent.uuid, "held-silent"); + await sendSilent(app, chat.id, human.agent.uuid, "free-silent"); + await sendTrigger(app, chat.id, human.agent.uuid, observer.agent.uuid, "concurrent-trigger"); + + const [heldRow] = await app.db + .select({ id: inboxEntries.id }) + .from(inboxEntries) + .where(and(eq(inboxEntries.inboxId, observer.agent.inboxId), eq(inboxEntries.messageId, held.message.id))) + .orderBy(asc(inboxEntries.id)) + .limit(1); + if (!heldRow) throw new Error("expected the silent row to exist"); + + // Hold a row lock in a parallel transaction, then drain. SKIP LOCKED must + // step over the held row and still return promptly. + let releaseLock: () => void = () => {}; + const lockReleased = new Promise((resolve) => { + releaseLock = resolve; + }); + const lockHeld = new Promise((resolve, reject) => { + app.db + .transaction(async (tx) => { + await tx.select().from(inboxEntries).where(eq(inboxEntries.id, heldRow.id)).for("update"); + resolve(); + await lockReleased; + }) + .catch(reject); + }); + await lockHeld; + + try { + const claimed = await inboxService.claimBacklogForPush(app.db, observer.agent.inboxId, 10); + const delivery = claimed[0]; + if (!delivery) throw new Error("expected trigger delivery"); + expect(precedingContents(delivery)).toEqual(["free-silent"]); + } finally { + releaseLock(); + } + }); +}); + +/** + * Wrap a database so raw `execute` calls issued inside a delivery transaction + * can be counted. + * + * The casts are unavoidable: Drizzle's transaction callback is generically + * typed over the schema, and a `Proxy` cannot preserve that relationship. The + * proxy only intercepts `transaction` and `execute` and forwards everything + * else untouched, so runtime behaviour is identical. + */ +function countingDb(db: FastifyInstance["db"]): { db: FastifyInstance["db"]; executeCalls: () => number } { + let executeCalls = 0; + const wrapped = new Proxy(db, { + get(target, prop, receiver) { + if (prop !== "transaction") return Reflect.get(target, prop, receiver); + return (callback: (tx: unknown) => Promise, ...rest: unknown[]) => + target.transaction( + async (tx) => { + const txProxy = new Proxy(tx, { + get(txTarget, txProp, txReceiver) { + const value = Reflect.get(txTarget, txProp, txReceiver); + if (txProp !== "execute" || typeof value !== "function") return value; + return (...args: unknown[]) => { + executeCalls += 1; + return value.apply(txTarget, args); + }; + }, + }); + return callback(txProxy); + }, + // biome-ignore lint/suspicious/noExplicitAny: Proxy cannot carry Drizzle's schema-generic transaction signature + ...(rest as any), + ); + }, + }); + return { db: wrapped, executeCalls: () => executeCalls }; +} diff --git a/packages/server/src/db/schema/inbox-entries.ts b/packages/server/src/db/schema/inbox-entries.ts index 3e8fa5ef6..536f97680 100644 --- a/packages/server/src/db/schema/inbox-entries.ts +++ b/packages/server/src/db/schema/inbox-entries.ts @@ -47,8 +47,17 @@ export const inboxEntries = pgTable( * Bundling lookup: given a notify=true trigger, find all silent pending * rows in the same chat that should be attached as preceding context. * Composite shape mirrors the actual WHERE clause used in pollInbox. + * + * `id` is the trailing column because preceding-context assembly bounds + * each trigger's window by `id` (`> previous notify cursor`, `< trigger`). + * Those bounds are correlated values inside a LATERAL, so the planner + * cannot estimate their width and would otherwise scan every silent row + * in the chat before discarding almost all of them. Keeping `id` in the + * index lets the range collapse into the index scan itself. It is a + * superset of the previous four-column shape, so every lookup that used + * the old index still resolves here. */ - index("idx_inbox_chat_silent").on(table.inboxId, table.chatId, table.notify, table.status), + index("idx_inbox_chat_silent").on(table.inboxId, table.chatId, table.notify, table.status, table.id), /** * Message-history delivery status lookup. The chat messages API checks * whether any inbox row for a message is acked, delivered, or still diff --git a/packages/server/src/services/inbox.ts b/packages/server/src/services/inbox.ts index 62ab8d04a..a813d409a 100644 --- a/packages/server/src/services/inbox.ts +++ b/packages/server/src/services/inbox.ts @@ -4,7 +4,7 @@ import { messageSourceSchema, type PrecedingMessage, } from "@first-tree/shared"; -import { and, asc, desc, eq, gt, inArray, isNull, lt, sql } from "drizzle-orm"; +import { and, asc, desc, eq, inArray, isNull, sql } from "drizzle-orm"; import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core"; import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; import type { Database } from "../db/connection.js"; @@ -28,8 +28,10 @@ export type AckEntryResult = } | { ok: false; reason: "not_found_or_not_bound" | "non_notify" | "prefix_gap" }; -/** Structurally-typed DB so both `Database` and transaction clients work. */ -type TxLike = Pick>, "select" | "update" | "delete" | "insert">; +/** Structurally-typed DB so both `Database` and transaction clients work. + * `execute` is included because the set-based preceding-context query below + * needs a raw statement (`LATERAL` has no Drizzle query-builder form). */ +type TxLike = Pick>, "select" | "update" | "delete" | "insert" | "execute">; /** Wider DB shape that matches both the concrete `Database` and the * `PgDatabase` widening used by sibling services (e.g. participant-mode). @@ -358,6 +360,20 @@ function fairBudgetKey(chatId: string | null): string { return chatId === null ? "null" : `chat:${chatId}`; } +/** + * Narrow a raw `inbox_entries.id` from `tx.execute` back to a number. + * + * Drizzle's `bigserial({ mode: "number" })` conversion only applies to + * query-builder selects; a raw statement hands the driver's own + * representation through, and postgres-js returns `bigint` columns as + * strings. `source` names the statement so a corrupt id is traceable. + */ +function toInboxEntryId(raw: string | number, source: string): number { + const id = typeof raw === "number" ? raw : Number(raw); + if (!Number.isSafeInteger(id)) throw new Error(`Unexpected inbox entry id from ${source}: ${raw}`); + return id; +} + /** * Fair WS backlog path for normal agent drains. It keeps the old backlog * helper's same-chat id order, but chooses across chats by chat-local rank so @@ -435,11 +451,7 @@ export async function claimBacklogForPushFair( ORDER BY locked.chat_rank, locked.id `); - const targetIds = selected.map((row) => { - const id = typeof row.id === "number" ? row.id : Number(row.id); - if (!Number.isSafeInteger(id)) throw new Error(`Unexpected inbox entry id from fair claim: ${row.id}`); - return id; - }); + const targetIds = selected.map((row) => toInboxEntryId(row.id, "fair claim")); if (targetIds.length === 0) return []; const claimed = await tx @@ -453,6 +465,27 @@ export async function claimBacklogForPushFair( ); } +/** + * One bundled silent row from the batched preceding-context query, keyed back + * to the trigger it belongs to. + * + * The shape mirrors raw driver output because `tx.execute` bypasses Drizzle's + * column mapping: `bigserial` arrives as a string (see `toInboxEntryId`) and + * `timestamptz` as PostgreSQL's own text form, not a `Date`. `jsonb` is the + * one type the driver still parses. `created_at` is therefore formatted to + * ISO 8601 in SQL rather than re-parsed here β€” see the statement below. + */ +type PrecedingContextRow = { + trigger_entry_id: string | number; + message_id: string; + sender_id: string; + format: string; + content: unknown; + metadata: Record | null; + source: string | null; + created_at: string; +}; + /** * Per claimed trigger: SELECT silent (notify=false) pending rows in the same * chat that occurred between the previous trigger in this batch (or beginning @@ -463,6 +496,13 @@ export async function claimBacklogForPushFair( * consumption: recovery must be able to reset the notify trigger and rebuild * the same trigger-relative context window. Silent rows are drained only when * the client ACKs the consumed notify entry. + * + * **One statement, not one per trigger.** Each trigger's lower bound is + * statically derivable: within a chat, `trigger[i]`'s bound is + * `trigger[i-1].id`, and only `trigger[0]` has to look outside the batch for + * the preceding notify row. Nothing depends on a previous iteration's *result*, + * so the whole fan-out collapses into a single set-based statement. A 50-entry + * drain used to hold its transaction open across 50+ sequential round-trips. */ async function collectPrecedingContext( tx: TxLike, @@ -471,105 +511,144 @@ async function collectPrecedingContext( ): Promise> { const result = new Map(); - // Group triggers by chatId so we can split the silent timeline per chat. - const byChat = new Map>>(); - for (const t of triggers) { - if (t.chatId === null) continue; // defensive: legacy / null-chatId rows can't have context - const list = byChat.get(t.chatId) ?? []; - list.push(t); - byChat.set(t.chatId, list); - } + // Defensive: legacy / null-chatId rows can't have chat-scoped context. + const scopedTriggers = triggers.filter((t) => t.chatId !== null); + if (scopedTriggers.length === 0) return result; + + // Pre-seed every in-scope trigger so a trigger whose window is empty still + // maps to `[]` rather than being absent. + for (const trigger of scopedTriggers) result.set(trigger.id, []); + + const triggerInput = JSON.stringify( + scopedTriggers.map((trigger) => ({ + entry_id: trigger.id, + chat_id: trigger.chatId, + // Computed here rather than as a SQL `interval` so the 24h boundary + // stays bit-identical to the per-trigger implementation this replaced. + window_start: new Date(trigger.createdAt.getTime() - PRECEDING_CONTEXT_WINDOW_SECONDS * 1000).toISOString(), + })), + ); - for (const [chatId, chatTriggers] of byChat) { - chatTriggers.sort((a, b) => a.id - b.id); - - const firstTrigger = chatTriggers[0]; - if (!firstTrigger) continue; - const [previousNotify] = await tx - .select({ id: inboxEntries.id }) - .from(inboxEntries) - .where( - and( - eq(inboxEntries.inboxId, inboxId), - eq(inboxEntries.chatId, chatId), - eq(inboxEntries.notify, true), - lt(inboxEntries.id, firstTrigger.id), - ), - ) - .orderBy(desc(inboxEntries.id)) - .limit(1); - - // For each trigger, fetch silent context strictly before it (and after - // the previous notify trigger cursor, even if that trigger was delivered - // in an earlier unacked batch). Window: 24h before the trigger. - // - // Order matters: when there are MORE than `PRECEDING_CONTEXT_MAX_ENTRIES` - // candidates, we want to keep the rows CLOSEST to the trigger (most - // contextually relevant) and drop the oldest. So select DESC + LIMIT, - // then reverse in JS to get chronological prompt-ready output. Selecting - // ASC + LIMIT would drop the recent rows; ACK-through later drains every - // silent row behind the consumed notify cursor, including rows excluded by - // this cap, so this delivery must choose the most relevant window now. - // - // We sort by `messages.createdAt` rather than `inboxEntries.createdAt` - // because `addParticipant`'s backfill writes 50 inbox rows in one - // `INSERT VALUES (...)` β€” they all share `statement_timestamp()`. The - // message rows themselves have distinct, monotonic timestamps (uuidv7 - // ids are time-ordered, `messages.created_at` is the authoritative - // chronology), so ordering by the joined message timestamp is the only - // stable contract the prompt-rendered context can rely on. - // - // Concurrency: `FOR UPDATE OF inboxEntries SKIP LOCKED` prevents two - // parallel polls on the same inbox from bundling the same silent row - // twice. Without it, poll A picking trigger T1 and poll B picking T2 - // (T2 > T1) would both include silent rows < T1 in their preceding - // context. With SKIP LOCKED, the second poll skips the rows the first - // has reserved. - let previousNotifyId: number | null = previousNotify?.id ?? null; - for (const trigger of chatTriggers) { - const windowStart = new Date(trigger.createdAt.getTime() - PRECEDING_CONTEXT_WINDOW_SECONDS * 1000); - const rows = await tx - .select({ - messageId: messages.id, - senderId: messages.senderId, - format: messages.format, - content: messages.content, - metadata: messages.metadata, - source: messages.source, - createdAt: messages.createdAt, - }) - .from(inboxEntries) - .innerJoin(messages, eq(messages.id, inboxEntries.messageId)) - .where( - and( - eq(inboxEntries.inboxId, inboxId), - eq(inboxEntries.chatId, chatId), - eq(inboxEntries.status, "pending"), - eq(inboxEntries.notify, false), - lt(inboxEntries.id, trigger.id), - previousNotifyId === null ? undefined : gt(inboxEntries.id, previousNotifyId), - gt(inboxEntries.createdAt, windowStart), - ), - ) - .orderBy(desc(messages.createdAt)) - .limit(PRECEDING_CONTEXT_MAX_ENTRIES) - .for("update", { of: inboxEntries, skipLocked: true }); - - // Reverse so the prompt-rendered block reads oldest β†’ newest. - const preceding: PrecedingMessage[] = rows - .map((r) => ({ - id: r.messageId, - senderId: r.senderId, - format: r.format, - content: r.content, - metadata: (r.metadata ?? {}) as Record, - source: messageSourceSchema.nullable().catch(null).parse(r.source), - createdAt: r.createdAt.toISOString(), - })) - .reverse(); - result.set(trigger.id, preceding); - previousNotifyId = trigger.id; - } + // `jsonb_to_recordset` mirrors the batching idiom already used by + // `claimBacklogForPushFair` above. + // + // `batch_lower_bound_id` β€” `lag()` reproduces the old loop's + // `previousNotifyId = trigger.id` carry-over. The window function lives in + // its own CTE on purpose: PostgreSQL rejects `FOR UPDATE` in any query level + // that also has a window function, and rejects it on the nullable side of an + // outer join. Keeping the lock *inside* the `CROSS JOIN LATERAL` subquery is + // the only shape that satisfies both restrictions. + // + // The scalar subquery resolves the bound for the first trigger of each chat + // only β€” `COALESCE` short-circuits for the rest. It deliberately has no + // status filter: any earlier notify row, including an already-acked one, + // closes the window. + // + // Concurrency: `FOR UPDATE OF e SKIP LOCKED` prevents two parallel polls on + // the same inbox from bundling the same silent row twice. Batching does not + // weaken this β€” the per-trigger ranges `(trigger[i-1].id, trigger[i].id)` + // are disjoint by construction, so a single statement locks exactly the rows + // the sequential loop would have. + // + // Ordering (unchanged): the LATERAL takes `messages.created_at DESC` + + // `LIMIT` so that when candidates exceed the cap we keep the rows CLOSEST to + // the trigger; ACK-through later drains the overflow anyway, so this delivery + // must pick the most relevant window now. The outer `ASC` then renders the + // prompt oldest β†’ newest. Sorting by `messages.created_at` rather than + // `inbox_entries.created_at` is required because `addParticipant`'s backfill + // writes its rows in one `INSERT VALUES (...)`, sharing a single + // `statement_timestamp()`; only the message timestamps are distinct and + // monotonic. `m.id` (uuidv7, time-ordered) breaks ties so the batch is + // deterministic. The 24h floor still compares `inbox_entries.created_at` β€” + // that asymmetry is intentional and predates this change. + const rows = await tx.execute(sql` + WITH trigger_input AS ( + SELECT + t.entry_id, + t.chat_id, + t.window_start, + lag(t.entry_id) OVER (PARTITION BY t.chat_id ORDER BY t.entry_id) AS batch_lower_bound_id + FROM jsonb_to_recordset(${triggerInput}::jsonb) + AS t(entry_id bigint, chat_id text, window_start timestamptz) + ), + bounded AS ( + SELECT + ti.entry_id, + ti.chat_id, + ti.window_start, + COALESCE( + ti.batch_lower_bound_id, + ( + SELECT prev.id + FROM inbox_entries prev + WHERE prev.inbox_id = ${inboxId} + AND prev.chat_id = ti.chat_id + AND prev.notify = true + AND prev.id < ti.entry_id + ORDER BY prev.id DESC + LIMIT 1 + ) + ) AS lower_bound_id + FROM trigger_input ti + ) + SELECT + b.entry_id AS trigger_entry_id, + ctx.message_id, + ctx.sender_id, + ctx.format, + ctx.content, + ctx.metadata, + ctx.source, + ctx.created_at + FROM bounded b + CROSS JOIN LATERAL ( + SELECT + m.id AS message_id, + m.sender_id, + m.format, + m.content, + m.metadata, + m.source, + -- Rendered in SQL because a raw statement hands back PostgreSQL's own + -- timestamp text (2026-07-20 00:01:00.123456+00), which is not + -- ISO 8601. Formatting here matches Date#toISOString() exactly -- + -- millisecond precision, T separator, Z suffix -- instead of relying + -- on JS to parse a non-standard shape. + to_char(m.created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS created_at, + m.created_at AS sort_created_at + FROM inbox_entries e + INNER JOIN messages m ON m.id = e.message_id + WHERE e.inbox_id = ${inboxId} + AND e.chat_id = b.chat_id + AND e.status = 'pending' + AND e.notify = false + AND e.id < b.entry_id + AND (b.lower_bound_id IS NULL OR e.id > b.lower_bound_id) + AND e.created_at > b.window_start + ORDER BY m.created_at DESC, m.id DESC + LIMIT ${PRECEDING_CONTEXT_MAX_ENTRIES} + FOR UPDATE OF e SKIP LOCKED + ) ctx + -- The LATERAL's inner ORDER BY only decides which rows LIMIT keeps; that + -- order is not carried to the outer query. Order explicitly here (on the + -- raw timestamp, not the rendered text) so the prompt block reads + -- oldest β†’ newest regardless of the plan chosen. + ORDER BY b.entry_id, ctx.sort_created_at ASC, ctx.message_id ASC + `); + + for (const row of rows) { + const bucket = result.get(toInboxEntryId(row.trigger_entry_id, "preceding context")); + // The statement only ever echoes trigger ids we sent in; skip defensively. + if (!bucket) continue; + bucket.push({ + id: row.message_id, + senderId: row.sender_id, + format: row.format, + content: row.content, + metadata: row.metadata ?? {}, + source: messageSourceSchema.nullable().catch(null).parse(row.source), + createdAt: row.created_at, + }); } return result; From e0caff99fbbbefccaa61b981f7f4690df3040f88 Mon Sep 17 00:00:00 2001 From: Bestony Date: Wed, 29 Jul 2026 01:10:01 +0800 Subject: [PATCH 2/5] perf(ws-client): validate bound agent routes in one query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every `inbox:ack` and every heartbeat walked the socket's bound agents and issued one single-row `agents` lookup each. Heartbeat did it twice, so a client holding N bindings cost 2N queries per heartbeat, and a client has no hard agent-count cap. Add `ensureAgentsStillRoutedHere`, which resolves the whole set in one query and applies the unchanged per-agent judgement β€” including the runtime-switch claim branch that parks a binding instead of dropping it. The single-agent helper now delegates to it so there is exactly one implementation of the judgement and its drop side effects. Candidates are snapshotted before the query, so the outcome no longer depends on the order in which `dropLocalAgentBinding` mutates `boundAgents`. Drop the heartbeat's second route check entirely. `restoredAgentIds` is already a subset of the ids the first check returned β€” it is filtered by an `EXISTS` on the same client and active status β€” and the repair path re-validates anyway: `maybeRepairInboxBacklog` gates on the in-memory route, then `drainBacklogForAgent` opens with its own `ensureAgentStillRoutedHere`, a strictly later and therefore fresher authoritative check. The only residual effect is that an agent that just moved away can still consume one repair-throttle slot, which is meaningless for a binding that is going away. The fake-db harness now supplies `agents.uuid`, which batched validation needs to map each returned row back to its agent; a single-row lookup left that mapping implicit. --- .../__tests__/ws-client-branch-fake.test.ts | 3 + packages/server/src/api/agent/ws-client.ts | 106 ++++++++++++------ 2 files changed, 76 insertions(+), 33 deletions(-) diff --git a/packages/server/src/__tests__/ws-client-branch-fake.test.ts b/packages/server/src/__tests__/ws-client-branch-fake.test.ts index b12fc549f..bd9034ea6 100644 --- a/packages/server/src/__tests__/ws-client-branch-fake.test.ts +++ b/packages/server/src/__tests__/ws-client-branch-fake.test.ts @@ -326,6 +326,9 @@ describe("Agent client WS branch fakes", () => { function activeAgentRow(overrides: Record = {}): Record { return { id: "agent_1", + // Batched route validation reads `agents.uuid` to map each returned row + // back to its agent; a single-row lookup could leave it implicit. + uuid: "agent_1", displayName: "Agent", type: "agent", organizationId: "org_1", diff --git a/packages/server/src/api/agent/ws-client.ts b/packages/server/src/api/agent/ws-client.ts index 3762af026..bdfeeb756 100644 --- a/packages/server/src/api/agent/ws-client.ts +++ b/packages/server/src/api/agent/ws-client.ts @@ -438,34 +438,69 @@ export function clientWsRoutes(notifier: Notifier, instanceId: string) { app.log.info({ clientId, agentId, reason }, "dropped stale local agent binding"); } - async function ensureAgentStillRoutedHere(agentId: string): Promise { - if (!isAgentStillRoutedHere(agentId)) return false; - const info = boundAgents.get(agentId); - if (!info || !clientId) return false; - const [row] = await app.db + /** + * Authoritative route check for several bindings in one round-trip. + * + * A socket can hold many bindings and there is no hard per-client agent + * cap, so validating them one at a time made ACK and heartbeat cost grow + * with fleet size. The judgement per agent is unchanged; only the + * fetch is batched. + * + * Returns the ids still routed to this socket. Agents whose + * authoritative route moved away are dropped with the usual side + * effects (`dropLocalAgentBinding`). + */ + async function ensureAgentsStillRoutedHere(agentIds: readonly string[]): Promise> { + const routed = new Set(); + if (!clientId) return routed; + // Snapshot before querying: `dropLocalAgentBinding` mutates + // `boundAgents`, so deciding the candidate set up front keeps the + // outcome independent of drop order. The in-memory gate is free, so + // only survivors reach the database. + const candidates = agentIds.filter((agentId) => isAgentStillRoutedHere(agentId)); + if (candidates.length === 0) return routed; + + const rows = await app.db .select({ + uuid: agents.uuid, clientId: agents.clientId, runtimeProvider: agents.runtimeProvider, status: agents.status, metadata: agents.metadata, }) .from(agents) - .where(eq(agents.uuid, agentId)) - .limit(1); - if (row?.clientId === clientId && row.status === "active" && row.runtimeProvider === info.runtimeProvider) { - return true; - } - const switchClaim = agentRuntimeSwitchService.getRuntimeSwitchClaim(row?.metadata); - if ( - row?.status === "suspended" && - switchClaim?.phase === "claimed" && - switchClaim.oldClientId === clientId && - switchClaim.oldRuntimeProvider === info.runtimeProvider - ) { - return false; + .where(inArray(agents.uuid, candidates)); + const rowByAgentId = new Map(rows.map((row) => [row.uuid, row])); + + for (const agentId of candidates) { + const info = boundAgents.get(agentId); + if (!info) continue; + const row = rowByAgentId.get(agentId); + if (row?.clientId === clientId && row.status === "active" && row.runtimeProvider === info.runtimeProvider) { + routed.add(agentId); + continue; + } + // A claimed runtime switch parks the binding rather than dropping + // it: the agent is no longer routed here, but the local binding has + // to survive so the switch can still be completed or aborted. + const switchClaim = agentRuntimeSwitchService.getRuntimeSwitchClaim(row?.metadata); + if ( + row?.status === "suspended" && + switchClaim?.phase === "claimed" && + switchClaim.oldClientId === clientId && + switchClaim.oldRuntimeProvider === info.runtimeProvider + ) { + continue; + } + dropLocalAgentBinding(agentId, "authoritative_route_changed"); } - dropLocalAgentBinding(agentId, "authoritative_route_changed"); - return false; + return routed; + } + + /** Single-agent form. Delegates so there is exactly one implementation + * of the route judgement and its drop side effects. */ + async function ensureAgentStillRoutedHere(agentId: string): Promise { + return (await ensureAgentsStillRoutedHere([agentId])).has(agentId); } function inboxInFlightCount(agentId: string): number { @@ -1630,12 +1665,11 @@ export function clientWsRoutes(notifier: Notifier, instanceId: string) { await chainInboxDelivery("__socket", async () => { try { - const routedBoundAgents = []; - for (const agent of boundAgents.values()) { - if (await ensureAgentStillRoutedHere(agent.agentId)) { - routedBoundAgents.push(agent); - } - } + // Keep whole binding records, not just ids: the ack result + // is mapped back to its owning agent by `inboxId` below. + const boundSnapshot = [...boundAgents.values()]; + const routedIds = await ensureAgentsStillRoutedHere(boundSnapshot.map((agent) => agent.agentId)); + const routedBoundAgents = boundSnapshot.filter((agent) => routedIds.has(agent.agentId)); const ackResult = await inboxService.ackEntryByIdForBoundAgents( app.db, entryId, @@ -1768,10 +1802,7 @@ export function clientWsRoutes(notifier: Notifier, instanceId: string) { }); } else if (type === "heartbeat") { if (clientId && connectionManager.isActiveClientConnection(clientId, socket)) { - const routedAgentIds = []; - for (const id of boundAgents.keys()) { - if (await ensureAgentStillRoutedHere(id)) routedAgentIds.push(id); - } + const routedAgentIds = [...(await ensureAgentsStillRoutedHere([...boundAgents.keys()]))]; const pausedReason = msg && typeof msg === "object" && "pausedReason" in msg ? ((msg as { pausedReason?: "auth_rejected" | "auth_refresh_failed" | null }).pausedReason ?? null) @@ -1782,11 +1813,20 @@ export function clientWsRoutes(notifier: Notifier, instanceId: string) { routedAgentIds, pausedReason, }); + // No second route check here. `restoredAgentIds` is already a + // subset of `routedAgentIds` (it is filtered by an `EXISTS` on + // the same client + active status), and the repair itself + // re-validates: `maybeRepairInboxBacklog` gates on the + // in-memory route, then `drainBacklogForAgent` opens with its + // own `ensureAgentStillRoutedHere` β€” a strictly later, and so + // strictly fresher, authoritative check. Re-reading here would + // add one query per bound agent on every heartbeat and close + // no race. The only residual effect is that an agent that just + // moved away can still consume one repair-throttle slot, which + // is meaningless for a binding that is going away. const repairableAgentIds = new Set(liveness.restoredAgentIds); for (const info of boundAgents.values()) { - if (repairableAgentIds.has(info.agentId) && (await ensureAgentStillRoutedHere(info.agentId))) { - maybeRepairInboxBacklog(info.agentId, info.inboxId); - } + if (repairableAgentIds.has(info.agentId)) maybeRepairInboxBacklog(info.agentId, info.inboxId); } await reconcilePinnedAgentsForClient(); } From e1e543385e990a5561ca3f28bcf8bcc15b0c20fb Mon Sep 17 00:00:00 2001 From: Bestony Date: Wed, 29 Jul 2026 01:23:13 +0800 Subject: [PATCH 3/5] perf(inbox): use partial indexes for preceding-context lookups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the composite index this branch first proposed. Appending `id` to `(inbox_id, chat_id, notify, status)` fixed the scan but defeated B-tree deduplication: the four-column form compresses near-duplicate keys to ~7 bytes per row, while a key ending in the unique `id` is ~49 bytes per row. Measured on 240k rows that is 1.7 MB against 11 MB. Moving `status`/`notify` from the key into a predicate keeps `id` indexed without the size cost β€” the pair totals ~2.6 MB and is slightly faster than the composite. It also bounds growth: `inbox_entries` is append-only, and the silent index now only covers rows still pending rather than all history. The notify-cursor index is deliberately not filtered on `status`. Both its callers must see notify rows in any state β€” an already-acked trigger still closes a preceding-context window, and ACK-through walks acked rows to prove its prefix has no gap. Verified the other `(inbox_id, chat_id)` query shapes in this service do not regress: the chat-scoped claim and the claim-prefix scan are unchanged at the median, while ACK-through's prefix walk and its silent drain get ~4x faster now that `id` is in the index. The migration follows the runbook established by 0025 and 0064: `IF NOT EXISTS` guards plus an operator note carrying the out-of-transaction `CONCURRENTLY` form. The new indexes take new names so that runbook can create them before dropping the old one β€” reusing the old name would leave a window with the silent-row lookup unindexed. `DROP INDEX` takes ACCESS EXCLUSIVE, and this table sits in the delivery hot path. Assert the index shapes in inbox-delivery-indexes.test.ts so `id` in the key and the absence of a status filter on the notify cursor are contracts rather than incidental. --- .../server/drizzle/0084_colorful_gargoyle.sql | 2 - .../server/drizzle/0084_sloppy_masque.sql | 54 +++++++++++++++++++ packages/server/drizzle/LATEST | 2 +- .../server/drizzle/meta/0084_snapshot.json | 27 ++++++++-- packages/server/drizzle/meta/_journal.json | 4 +- .../__tests__/inbox-delivery-indexes.test.ts | 32 +++++++++++ .../server/src/db/schema/inbox-entries.ts | 41 +++++++++----- 7 files changed, 140 insertions(+), 22 deletions(-) delete mode 100644 packages/server/drizzle/0084_colorful_gargoyle.sql create mode 100644 packages/server/drizzle/0084_sloppy_masque.sql diff --git a/packages/server/drizzle/0084_colorful_gargoyle.sql b/packages/server/drizzle/0084_colorful_gargoyle.sql deleted file mode 100644 index 00f2aabbb..000000000 --- a/packages/server/drizzle/0084_colorful_gargoyle.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX "idx_inbox_chat_silent";--> statement-breakpoint -CREATE INDEX "idx_inbox_chat_silent" ON "inbox_entries" USING btree ("inbox_id","chat_id","notify","status","id"); \ No newline at end of file diff --git a/packages/server/drizzle/0084_sloppy_masque.sql b/packages/server/drizzle/0084_sloppy_masque.sql new file mode 100644 index 000000000..3fee405cd --- /dev/null +++ b/packages/server/drizzle/0084_sloppy_masque.sql @@ -0,0 +1,54 @@ +-- Replace the chat-silent composite with two partial indexes keyed on `id`. +-- +-- Preceding-context assembly now runs as one set-based statement instead of +-- one query per claimed trigger. Inside the LATERAL, each trigger's window +-- bounds (`> previous notify cursor`, `< trigger`) are correlated values, so +-- the planner cannot estimate their width. Without `id` in the index it scans +-- every silent row in the chat and discards almost all of them. +-- +-- idx_inbox_chat_silent_pending β€” the silent rows a trigger bundles as +-- preceding context, and ACK-through's silent drain. +-- +-- idx_inbox_chat_notify β€” the previous-trigger cursor and ACK-through's +-- contiguous notify prefix. Deliberately not filtered on `status`: an +-- already-acked trigger still closes a preceding-context window, and the +-- prefix check has to walk acked rows to prove there is no gap. +-- +-- Partial rather than appending `status`/`notify` to the key: a key ending in +-- the unique `id` defeats B-tree deduplication. Measured on 240k rows, the old +-- four-column form costs ~7 bytes per row because near-duplicate keys compress; +-- appending `id` makes every key unique and costs ~49 bytes per row (1.7 MB -> +-- 11 MB). Restricting the row set keeps the pair at ~2.6 MB, and since +-- `inbox_entries` is append-only, excluding consumed rows matters more as the +-- table grows. +-- +-- Operator note: +-- Drizzle migrator executes migration files inside a transaction, so this file +-- cannot use CREATE INDEX CONCURRENTLY. A plain CREATE INDEX takes a SHARE lock +-- and DROP INDEX takes ACCESS EXCLUSIVE; on a large `inbox_entries` β€” which +-- sits in the message delivery hot path β€” that is a visible write stall. For a +-- large production table, run the concurrent forms manually outside a +-- transaction before applying migrations, creating the new indexes BEFORE +-- dropping the old one so no lookup is ever left unindexed: +-- +-- 1. CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_inbox_chat_silent_pending" +-- ON "inbox_entries" ("inbox_id", "chat_id", "id") +-- WHERE status = 'pending' AND notify = false; +-- 2. CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_inbox_chat_notify" +-- ON "inbox_entries" ("inbox_id", "chat_id", "id") +-- WHERE notify = true; +-- 3. Verify both are valid (indisvalid in pg_index) and being used, then: +-- DROP INDEX CONCURRENTLY IF EXISTS "idx_inbox_chat_silent"; +-- 4. Re-run `pnpm db:migrate`. The IF NOT EXISTS / IF EXISTS clauses below +-- detect the manual work and skip it. +-- +-- The new indexes carry different names from the one being dropped precisely so +-- this ordering is possible; reusing `idx_inbox_chat_silent` would force a +-- window with no index backing the silent-row lookup. +CREATE INDEX IF NOT EXISTS "idx_inbox_chat_silent_pending" + ON "inbox_entries" USING btree ("inbox_id","chat_id","id") + WHERE status = 'pending' AND notify = false;--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "idx_inbox_chat_notify" + ON "inbox_entries" USING btree ("inbox_id","chat_id","id") + WHERE notify = true;--> statement-breakpoint +DROP INDEX IF EXISTS "idx_inbox_chat_silent"; diff --git a/packages/server/drizzle/LATEST b/packages/server/drizzle/LATEST index 69f42d505..9de1c5c20 100644 --- a/packages/server/drizzle/LATEST +++ b/packages/server/drizzle/LATEST @@ -1 +1 @@ -0084_colorful_gargoyle +0084_sloppy_masque diff --git a/packages/server/drizzle/meta/0084_snapshot.json b/packages/server/drizzle/meta/0084_snapshot.json index 7b90576e2..55162bf91 100644 --- a/packages/server/drizzle/meta/0084_snapshot.json +++ b/packages/server/drizzle/meta/0084_snapshot.json @@ -1,5 +1,5 @@ { - "id": "8ccbbcc3-bcc7-4799-b50c-5734dbf91c92", + "id": "6a4962ef-db07-4d53-b005-8460238614b0", "prevId": "a85cf597-fe4f-431a-941b-dd1b309f6f0f", "version": "7", "dialect": "postgresql", @@ -4212,8 +4212,8 @@ "method": "btree", "with": {} }, - "idx_inbox_chat_silent": { - "name": "idx_inbox_chat_silent", + "idx_inbox_chat_silent_pending": { + "name": "idx_inbox_chat_silent_pending", "columns": [ { "expression": "inbox_id", @@ -4228,13 +4228,29 @@ "nulls": "last" }, { - "expression": "notify", + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'pending' AND notify = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_inbox_chat_notify": { + "name": "idx_inbox_chat_notify", + "columns": [ + { + "expression": "inbox_id", "isExpression": false, "asc": true, "nulls": "last" }, { - "expression": "status", + "expression": "chat_id", "isExpression": false, "asc": true, "nulls": "last" @@ -4247,6 +4263,7 @@ } ], "isUnique": false, + "where": "notify = true", "concurrently": false, "method": "btree", "with": {} diff --git a/packages/server/drizzle/meta/_journal.json b/packages/server/drizzle/meta/_journal.json index c2ccaf9dd..7d5b05562 100644 --- a/packages/server/drizzle/meta/_journal.json +++ b/packages/server/drizzle/meta/_journal.json @@ -593,8 +593,8 @@ { "idx": 84, "version": "7", - "when": 1785258368232, - "tag": "0084_colorful_gargoyle", + "when": 1785259021331, + "tag": "0084_sloppy_masque", "breakpoints": true } ] diff --git a/packages/server/src/__tests__/inbox-delivery-indexes.test.ts b/packages/server/src/__tests__/inbox-delivery-indexes.test.ts index 3828658e3..aacab2ca3 100644 --- a/packages/server/src/__tests__/inbox-delivery-indexes.test.ts +++ b/packages/server/src/__tests__/inbox-delivery-indexes.test.ts @@ -28,6 +28,38 @@ describe("inbox delivery indexes", () => { expect(rows[0]?.indexdef).toContain("USING btree (message_id, status)"); }); + it("keeps the preceding-context indexes partial and keyed on entry id", async () => { + const rows = await getDb().execute<{ indexname: string; indexdef: string }>(sql` + SELECT indexname, indexdef + FROM pg_indexes + WHERE schemaname = 'public' + AND tablename = 'inbox_entries' + AND indexname IN ('idx_inbox_chat_silent_pending', 'idx_inbox_chat_notify') + ORDER BY indexname + `); + + expect(rows).toHaveLength(2); + const byName = new Map(rows.map((row) => [row.indexname, row.indexdef])); + + // `id` must stay in the key: the preceding-context window bounds are + // correlated values inside a LATERAL, so without it the planner scans + // every silent row in the chat. + const silent = byName.get("idx_inbox_chat_silent_pending"); + expect(silent).toContain("(inbox_id, chat_id, id)"); + // Partial, not a wider composite β€” a key ending in the unique `id` + // defeats B-tree deduplication and inflates the index several-fold. + expect(silent).toContain("WHERE"); + expect(silent).toContain("notify = false"); + + // The notify cursor must NOT be filtered on status: an already-acked + // trigger still closes a preceding-context window, and ACK-through walks + // acked rows to prove its prefix has no gap. + const notify = byName.get("idx_inbox_chat_notify"); + expect(notify).toContain("(inbox_id, chat_id, id)"); + expect(notify).toContain("WHERE (notify"); + expect(notify).not.toContain("status"); + }); + 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/db/schema/inbox-entries.ts b/packages/server/src/db/schema/inbox-entries.ts index 536f97680..53164b60c 100644 --- a/packages/server/src/db/schema/inbox-entries.ts +++ b/packages/server/src/db/schema/inbox-entries.ts @@ -44,20 +44,37 @@ export const inboxEntries = pgTable( .on(table.inboxId, table.createdAt) .where(sql`status = 'pending' AND notify = true`), /** - * Bundling lookup: given a notify=true trigger, find all silent pending - * rows in the same chat that should be attached as preceding context. - * Composite shape mirrors the actual WHERE clause used in pollInbox. + * Bundling lookup: given a notify=true trigger, find the silent pending + * rows in the same chat to attach as preceding context. * - * `id` is the trailing column because preceding-context assembly bounds - * each trigger's window by `id` (`> previous notify cursor`, `< trigger`). - * Those bounds are correlated values inside a LATERAL, so the planner - * cannot estimate their width and would otherwise scan every silent row - * in the chat before discarding almost all of them. Keeping `id` in the - * index lets the range collapse into the index scan itself. It is a - * superset of the previous four-column shape, so every lookup that used - * the old index still resolves here. + * `id` is indexed because every caller bounds the window by id β€” the + * preceding-context window (`> previous notify cursor`, `< trigger`) and + * ACK-through's `id <= cursor` drain. Inside a LATERAL those bounds are + * correlated values whose width the planner cannot estimate, so without + * `id` it scans every silent row in the chat and discards almost all of + * them. + * + * Partial rather than a wider composite: `status`/`notify` move into the + * predicate instead of the key. A key ending in the unique `id` defeats + * B-tree deduplication β€” the four-column form compresses ~240k rows into + * ~7 bytes each, while appending `id` makes every key unique and costs + * ~49 bytes each. Restricting the row set instead keeps the index small, + * and `inbox_entries` is append-only, so excluding consumed rows matters + * more over time than it does today. + */ + index("idx_inbox_chat_silent_pending") + .on(table.inboxId, table.chatId, table.id) + .where(sql`status = 'pending' AND notify = false`), + /** + * Notify-cursor lookup: the previous trigger before a given entry, and + * ACK-through's contiguous notify prefix. + * + * Deliberately not filtered on `status` β€” both callers must see notify + * rows in any state. An already-acked trigger still closes a preceding- + * context window, and ACK-through has to walk acked rows to prove the + * prefix has no gap. */ - index("idx_inbox_chat_silent").on(table.inboxId, table.chatId, table.notify, table.status, table.id), + index("idx_inbox_chat_notify").on(table.inboxId, table.chatId, table.id).where(sql`notify = true`), /** * Message-history delivery status lookup. The chat messages API checks * whether any inbox row for a message is acked, delivered, or still From 67b96c2b186c73b04f614b1a134aec1946c74bba Mon Sep 17 00:00:00 2001 From: Bestony Date: Wed, 29 Jul 2026 01:23:13 +0800 Subject: [PATCH 4/5] test(inbox): pin query count across trigger counts Counting only raw statements would still read as 1 if a per-trigger `select` were reintroduced alongside the batched one. Count both kinds and compare two drains of different sizes, so the assertion states what it means: the query count does not grow with the number of triggers. Also document why the route check reads its local binding after the query rather than before. A rebind updates the row and the binding together, so the other order compares a pre-query binding against a post-query row and drops an agent that had just rebound successfully. --- .../inbox-preceding-context-batch.test.ts | 60 ++++++++++++------- packages/server/src/api/agent/ws-client.ts | 6 ++ 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/server/src/__tests__/inbox-preceding-context-batch.test.ts b/packages/server/src/__tests__/inbox-preceding-context-batch.test.ts index 21c423121..eb37640ae 100644 --- a/packages/server/src/__tests__/inbox-preceding-context-batch.test.ts +++ b/packages/server/src/__tests__/inbox-preceding-context-batch.test.ts @@ -202,26 +202,34 @@ describe("batched preceding-context assembly", () => { expect(unscoped.message.precedingMessages).toEqual([]); }); - it("assembles context in one statement regardless of how many triggers a drain claims", async () => { + it("issues the same number of queries no matter how many triggers a drain claims", async () => { const app = getApp(); - const uid = crypto.randomUUID().slice(0, 6); - const { human, observer, chat } = await seedChatWithObserver(app, uid); - const triggerCount = 8; - for (let i = 0; i < triggerCount; i++) { - await sendSilent(app, chat.id, human.agent.uuid, `silent-${i}`); - await sendTrigger(app, chat.id, human.agent.uuid, observer.agent.uuid, `trigger-${i}`); + async function drainAndCount(triggerCount: number): Promise<{ execute: number; select: number }> { + const uid = crypto.randomUUID().slice(0, 6); + const { human, observer, chat } = await seedChatWithObserver(app, uid); + for (let i = 0; i < triggerCount; i++) { + await sendSilent(app, chat.id, human.agent.uuid, `silent-${i}`); + await sendTrigger(app, chat.id, human.agent.uuid, observer.agent.uuid, `trigger-${i}`); + } + + const probe = countingDb(app.db); + const claimed = await inboxService.claimBacklogForPush(probe.db, observer.agent.inboxId, 50); + expect(claimed).toHaveLength(triggerCount); + expect(claimed.map(precedingContents)).toEqual(Array.from({ length: triggerCount }, (_, i) => [`silent-${i}`])); + return probe.counts(); } - const probe = countingDb(app.db); - const claimed = await inboxService.claimBacklogForPush(probe.db, observer.agent.inboxId, 50); - expect(claimed).toHaveLength(triggerCount); - expect(claimed.map(precedingContents)).toEqual(Array.from({ length: triggerCount }, (_, i) => [`silent-${i}`])); + const small = await drainAndCount(2); + const large = await drainAndCount(8); + // Counting both statement kinds matters: `execute` alone would still read + // as 1 if someone reintroduced a per-trigger `select` alongside the + // batched statement. + expect(large).toEqual(small); // The batched preceding-context query is the only raw statement on this - // path, so this pins the cost at O(1) rather than O(triggers). The old - // per-trigger loop would report `triggerCount` here. - expect(probe.executeCalls()).toBe(1); + // path. The per-trigger loop this replaced would report `triggerCount`. + expect(large.execute).toBe(1); }); it("skips silent rows another transaction holds instead of blocking on them", async () => { @@ -270,16 +278,21 @@ describe("batched preceding-context assembly", () => { }); /** - * Wrap a database so raw `execute` calls issued inside a delivery transaction - * can be counted. + * Wrap a database so statements issued inside a delivery transaction can be + * counted. Both `execute` and `select` are tracked β€” counting only raw + * statements would miss a per-trigger `select` reintroduced next to the + * batched one. * * The casts are unavoidable: Drizzle's transaction callback is generically * typed over the schema, and a `Proxy` cannot preserve that relationship. The - * proxy only intercepts `transaction` and `execute` and forwards everything - * else untouched, so runtime behaviour is identical. + * proxy only intercepts `transaction`, `execute`, and `select`, forwarding + * everything else untouched, so runtime behaviour is identical. */ -function countingDb(db: FastifyInstance["db"]): { db: FastifyInstance["db"]; executeCalls: () => number } { - let executeCalls = 0; +function countingDb(db: FastifyInstance["db"]): { + db: FastifyInstance["db"]; + counts: () => { execute: number; select: number }; +} { + const tally = { execute: 0, select: 0 }; const wrapped = new Proxy(db, { get(target, prop, receiver) { if (prop !== "transaction") return Reflect.get(target, prop, receiver); @@ -289,9 +302,10 @@ function countingDb(db: FastifyInstance["db"]): { db: FastifyInstance["db"]; exe const txProxy = new Proxy(tx, { get(txTarget, txProp, txReceiver) { const value = Reflect.get(txTarget, txProp, txReceiver); - if (txProp !== "execute" || typeof value !== "function") return value; + const counted = txProp === "execute" || txProp === "select"; + if (!counted || typeof value !== "function") return value; return (...args: unknown[]) => { - executeCalls += 1; + tally[txProp] += 1; return value.apply(txTarget, args); }; }, @@ -303,5 +317,5 @@ function countingDb(db: FastifyInstance["db"]): { db: FastifyInstance["db"]; exe ); }, }); - return { db: wrapped, executeCalls: () => executeCalls }; + return { db: wrapped, counts: () => ({ ...tally }) }; } diff --git a/packages/server/src/api/agent/ws-client.ts b/packages/server/src/api/agent/ws-client.ts index bdfeeb756..1de3cc931 100644 --- a/packages/server/src/api/agent/ws-client.ts +++ b/packages/server/src/api/agent/ws-client.ts @@ -473,6 +473,12 @@ export function clientWsRoutes(notifier: Notifier, instanceId: string) { const rowByAgentId = new Map(rows.map((row) => [row.uuid, row])); for (const agentId of candidates) { + // Read the binding *after* the query, not before. A rebind updates + // the row and the local binding together, so comparing a binding + // read before the query against a row read after it reports a + // provider mismatch and drops an agent that just rebound + // successfully. Both orders leave a window β€” this one is a loop + // iteration rather than a database round-trip. const info = boundAgents.get(agentId); if (!info) continue; const row = rowByAgentId.get(agentId); From 57c2c2924bfd3a255916b03a9defd762e0557450 Mon Sep 17 00:00:00 2001 From: Bestony Date: Wed, 29 Jul 2026 01:30:46 +0800 Subject: [PATCH 5/5] docs(ws-client): describe the route-check window as relocated, not narrowed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous wording called the exposure "a loop iteration", which reads as microseconds. The row is a snapshot from when Postgres executed the SELECT, so reading the binding after the query exposes result delivery plus an event loop turn β€” the other half of the same round-trip, not a smaller one. Reading the binding after the query is still the right order, but the comment should not imply a guarantee that is not there: the socket's message handler is not awaited by the emitter and bind frames are not serialised against heartbeat frames, so the interleaving is reachable either way. --- packages/server/src/api/agent/ws-client.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/server/src/api/agent/ws-client.ts b/packages/server/src/api/agent/ws-client.ts index 1de3cc931..416df2a95 100644 --- a/packages/server/src/api/agent/ws-client.ts +++ b/packages/server/src/api/agent/ws-client.ts @@ -474,11 +474,18 @@ export function clientWsRoutes(notifier: Notifier, instanceId: string) { for (const agentId of candidates) { // Read the binding *after* the query, not before. A rebind updates - // the row and the local binding together, so comparing a binding - // read before the query against a row read after it reports a - // provider mismatch and drops an agent that just rebound - // successfully. Both orders leave a window β€” this one is a loop - // iteration rather than a database round-trip. + // the row and the local binding together, so whichever side is read + // first can go stale and report a provider mismatch that drops an + // agent which had just rebound successfully. + // + // This relocates that window rather than closing it. `row` is a + // snapshot from the moment Postgres executed the SELECT, so reading + // the binding afterwards exposes [snapshot .. result delivery + + // event loop turn] where reading it first exposed [binding read .. + // snapshot] β€” two halves of the same round-trip, comparable in + // width. Neither is negligible: the socket's message handler is not + // awaited by the emitter, and bind frames are not serialised + // against heartbeat frames, so the interleaving is reachable. const info = boundAgents.get(agentId); if (!info) continue; const row = rowByAgentId.get(agentId);