diff --git a/packages/junior/migrations/0024_misty_typhoid_mary.sql b/packages/junior/migrations/0024_misty_typhoid_mary.sql new file mode 100644 index 0000000000..152bf30f3c --- /dev/null +++ b/packages/junior/migrations/0024_misty_typhoid_mary.sql @@ -0,0 +1,12 @@ +CREATE TABLE "junior_location_configurations" ( + "location_id" text NOT NULL, + "key" text NOT NULL, + "value" text NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "updated_by" text, + "source" text, + "expires_at" text, + CONSTRAINT "junior_location_configurations_location_id_key_pk" PRIMARY KEY("location_id","key") +); +--> statement-breakpoint +ALTER TABLE "junior_location_configurations" ADD CONSTRAINT "junior_location_configurations_location_id_junior_destinations_id_fk" FOREIGN KEY ("location_id") REFERENCES "public"."junior_destinations"("id") ON DELETE cascade ON UPDATE no action; diff --git a/packages/junior/migrations/meta/0024_snapshot.json b/packages/junior/migrations/meta/0024_snapshot.json new file mode 100644 index 0000000000..ece33a7f1c --- /dev/null +++ b/packages/junior/migrations/meta/0024_snapshot.json @@ -0,0 +1,2275 @@ +{ + "id": "5b615b01-1e96-4396-a09d-82c0a70f4a58", + "prevId": "86dbef33-04cf-4e3d-b324-b321a3af4af8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.junior_agent_bindings": { + "name": "junior_agent_bindings", + "schema": "", + "columns": { + "parent_conversation_id": { + "name": "parent_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_conversation_id": { + "name": "child_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "junior_agent_bindings_child_idx": { + "name": "junior_agent_bindings_child_idx", + "columns": [ + { + "expression": "child_conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "junior_agent_bindings_parent_conversation_id_junior_conversations_conversation_id_fk": { + "name": "junior_agent_bindings_parent_conversation_id_junior_conversations_conversation_id_fk", + "tableFrom": "junior_agent_bindings", + "tableTo": "junior_conversations", + "columnsFrom": [ + "parent_conversation_id" + ], + "columnsTo": [ + "conversation_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "junior_agent_bindings_child_conversation_id_junior_conversations_conversation_id_fk": { + "name": "junior_agent_bindings_child_conversation_id_junior_conversations_conversation_id_fk", + "tableFrom": "junior_agent_bindings", + "tableTo": "junior_conversations", + "columnsFrom": [ + "child_conversation_id" + ], + "columnsTo": [ + "conversation_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "junior_agent_bindings_parent_conversation_id_name_pk": { + "name": "junior_agent_bindings_parent_conversation_id_name_pk", + "columns": [ + "parent_conversation_id", + "name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.junior_agent_invocations": { + "name": "junior_agent_invocations", + "schema": "", + "columns": { + "invocation_id": { + "name": "invocation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "parent_conversation_id": { + "name": "parent_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_conversation_id": { + "name": "child_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input": { + "name": "input", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_json": { + "name": "actor_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "credential_context_json": { + "name": "credential_context_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_json": { + "name": "source_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_json": { + "name": "destination_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_visibility": { + "name": "destination_visibility", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reasoning_level": { + "name": "reasoning_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mailbox_status": { + "name": "mailbox_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "junior_agent_invocations_child_idx": { + "name": "junior_agent_invocations_child_idx", + "columns": [ + { + "expression": "child_conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_agent_invocations_mailbox_idx": { + "name": "junior_agent_invocations_mailbox_idx", + "columns": [ + { + "expression": "mailbox_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "junior_agent_invocations_parent_conversation_id_junior_conversations_conversation_id_fk": { + "name": "junior_agent_invocations_parent_conversation_id_junior_conversations_conversation_id_fk", + "tableFrom": "junior_agent_invocations", + "tableTo": "junior_conversations", + "columnsFrom": [ + "parent_conversation_id" + ], + "columnsTo": [ + "conversation_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "junior_agent_invocations_child_conversation_id_junior_conversations_conversation_id_fk": { + "name": "junior_agent_invocations_child_conversation_id_junior_conversations_conversation_id_fk", + "tableFrom": "junior_agent_invocations", + "tableTo": "junior_conversations", + "columnsFrom": [ + "child_conversation_id" + ], + "columnsTo": [ + "conversation_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.junior_api_tokens": { + "name": "junior_api_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_email_normalized": { + "name": "owner_email_normalized", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_suffix": { + "name": "token_suffix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "junior_api_tokens_token_hash_uidx": { + "name": "junior_api_tokens_token_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_api_tokens_owner_email_idx": { + "name": "junior_api_tokens_owner_email_idx", + "columns": [ + { + "expression": "owner_email_normalized", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.junior_conversation_annotations": { + "name": "junior_conversation_annotations", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin": { + "name": "plugin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "annotation_json": { + "name": "annotation_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "junior_conversation_annotations_conversation_id_fk": { + "name": "junior_conversation_annotations_conversation_id_fk", + "tableFrom": "junior_conversation_annotations", + "tableTo": "junior_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "conversation_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "junior_conversation_annotations_pk": { + "name": "junior_conversation_annotations_pk", + "columns": [ + "conversation_id", + "plugin", + "kind", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.junior_conversation_bindings": { + "name": "junior_conversation_bindings", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_tenant_id": { + "name": "provider_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "provider_destination_id": { + "name": "provider_destination_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_conversation_id": { + "name": "provider_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "junior_conversation_bindings_conversation_idx": { + "name": "junior_conversation_bindings_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "junior_conversation_bindings_conversation_id_junior_conversations_conversation_id_fk": { + "name": "junior_conversation_bindings_conversation_id_junior_conversations_conversation_id_fk", + "tableFrom": "junior_conversation_bindings", + "tableTo": "junior_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "conversation_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "junior_conversation_bindings_provider_conversation_pk": { + "name": "junior_conversation_bindings_provider_conversation_pk", + "columns": [ + "provider", + "provider_tenant_id", + "provider_destination_id", + "provider_conversation_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.junior_conversation_events": { + "name": "junior_conversation_events", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "history_version": { + "name": "history_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "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 + } + }, + "indexes": { + "junior_conversation_events_history_version_idx": { + "name": "junior_conversation_events_history_version_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "history_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_conversation_events_type_idx": { + "name": "junior_conversation_events_type_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_conversation_events_message_search_idx": { + "name": "junior_conversation_events_message_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"payload\"->>'text')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"junior_conversation_events\".\"type\" = 'message'", + "concurrently": false, + "method": "gin", + "with": {} + }, + "junior_conversation_events_idempotency_idx": { + "name": "junior_conversation_events_idempotency_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "junior_conversation_events_conversation_id_junior_conversations_conversation_id_fk": { + "name": "junior_conversation_events_conversation_id_junior_conversations_conversation_id_fk", + "tableFrom": "junior_conversation_events", + "tableTo": "junior_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "conversation_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "junior_conversation_events_conversation_id_seq_pk": { + "name": "junior_conversation_events_conversation_id_seq_pk", + "columns": [ + "conversation_id", + "seq" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.junior_conversations": { + "name": "junior_conversations", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_json": { + "name": "source_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "origin_type": { + "name": "origin_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_id": { + "name": "origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destination_id": { + "name": "destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destination_json": { + "name": "destination_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_identity_id": { + "name": "actor_identity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_identity_id": { + "name": "creator_identity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_subject_identity_id": { + "name": "credential_subject_identity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_json": { + "name": "actor_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "channel_name": { + "name": "channel_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "execution_updated_at": { + "name": "execution_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_status": { + "name": "execution_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_checkpoint_at": { + "name": "last_checkpoint_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_enqueued_at": { + "name": "last_enqueued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "parent_conversation_id": { + "name": "parent_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_conversation_id": { + "name": "root_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transcript_purged_at": { + "name": "transcript_purged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_duration_ms": { + "name": "execution_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "execution_usage_json": { + "name": "execution_usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metric_run_id": { + "name": "metric_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "junior_conversations_last_activity_idx": { + "name": "junior_conversations_last_activity_idx", + "columns": [ + { + "expression": "last_activity_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_conversations_active_idx": { + "name": "junior_conversations_active_idx", + "columns": [ + { + "expression": "coalesce(\"execution_updated_at\", \"updated_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"junior_conversations\".\"execution_status\" <> 'idle'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_conversations_destination_activity_idx": { + "name": "junior_conversations_destination_activity_idx", + "columns": [ + { + "expression": "destination_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_activity_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_conversations_actor_activity_idx": { + "name": "junior_conversations_actor_activity_idx", + "columns": [ + { + "expression": "actor_identity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_activity_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_conversations_origin_idx": { + "name": "junior_conversations_origin_idx", + "columns": [ + { + "expression": "origin_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_activity_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_conversations_parent_idx": { + "name": "junior_conversations_parent_idx", + "columns": [ + { + "expression": "parent_conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_conversations_root_idx": { + "name": "junior_conversations_root_idx", + "columns": [ + { + "expression": "root_conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "junior_conversations_destination_id_junior_destinations_id_fk": { + "name": "junior_conversations_destination_id_junior_destinations_id_fk", + "tableFrom": "junior_conversations", + "tableTo": "junior_destinations", + "columnsFrom": [ + "destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "junior_conversations_actor_identity_id_junior_identities_id_fk": { + "name": "junior_conversations_actor_identity_id_junior_identities_id_fk", + "tableFrom": "junior_conversations", + "tableTo": "junior_identities", + "columnsFrom": [ + "actor_identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "junior_conversations_creator_identity_id_junior_identities_id_fk": { + "name": "junior_conversations_creator_identity_id_junior_identities_id_fk", + "tableFrom": "junior_conversations", + "tableTo": "junior_identities", + "columnsFrom": [ + "creator_identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "junior_conversations_credential_subject_identity_id_junior_identities_id_fk": { + "name": "junior_conversations_credential_subject_identity_id_junior_identities_id_fk", + "tableFrom": "junior_conversations", + "tableTo": "junior_identities", + "columnsFrom": [ + "credential_subject_identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "junior_conversations_parent_conversation_id_junior_conversations_conversation_id_fk": { + "name": "junior_conversations_parent_conversation_id_junior_conversations_conversation_id_fk", + "tableFrom": "junior_conversations", + "tableTo": "junior_conversations", + "columnsFrom": [ + "parent_conversation_id" + ], + "columnsTo": [ + "conversation_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "junior_conversations_root_conversation_id_junior_conversations_conversation_id_fk": { + "name": "junior_conversations_root_conversation_id_junior_conversations_conversation_id_fk", + "tableFrom": "junior_conversations", + "tableTo": "junior_conversations", + "columnsFrom": [ + "root_conversation_id" + ], + "columnsTo": [ + "conversation_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.junior_destinations": { + "name": "junior_destinations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_tenant_id": { + "name": "provider_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "provider_destination_id": { + "name": "provider_destination_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_destination_id": { + "name": "parent_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "junior_destinations_provider_destination_uidx": { + "name": "junior_destinations_provider_destination_uidx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_destination_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_destinations_provider_kind_idx": { + "name": "junior_destinations_provider_kind_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.junior_event_tasks": { + "name": "junior_event_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at_ms": { + "name": "created_at_ms", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_json": { + "name": "task_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "junior_event_tasks_team_idx": { + "name": "junior_event_tasks_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_event_tasks_match_idx": { + "name": "junior_event_tasks_match_idx", + "columns": [ + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.junior_identities": { + "name": "junior_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_tenant_id": { + "name": "provider_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_normalized": { + "name": "email_normalized", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "junior_identities_provider_subject_uidx": { + "name": "junior_identities_provider_subject_uidx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_identities_user_idx": { + "name": "junior_identities_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_identities_verified_email_idx": { + "name": "junior_identities_verified_email_idx", + "columns": [ + { + "expression": "email_normalized", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"junior_identities\".\"email_verified\" = true AND \"junior_identities\".\"email_normalized\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_identities_kind_provider_idx": { + "name": "junior_identities_kind_provider_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "junior_identities_user_id_junior_users_id_fk": { + "name": "junior_identities_user_id_junior_users_id_fk", + "tableFrom": "junior_identities", + "tableTo": "junior_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.junior_location_configurations": { + "name": "junior_location_configurations", + "schema": "", + "columns": { + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "junior_location_configurations_location_id_junior_destinations_id_fk": { + "name": "junior_location_configurations_location_id_junior_destinations_id_fk", + "tableFrom": "junior_location_configurations", + "tableTo": "junior_destinations", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "junior_location_configurations_location_id_key_pk": { + "name": "junior_location_configurations_location_id_key_pk", + "columns": [ + "location_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.junior_scheduler_runs": { + "name": "junior_scheduler_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled_for_ms": { + "name": "scheduled_for_ms", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "record": { + "name": "record", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "junior_scheduler_runs_task_status_idx": { + "name": "junior_scheduler_runs_task_status_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_scheduler_runs_status_idx": { + "name": "junior_scheduler_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.junior_scheduler_tasks": { + "name": "junior_scheduler_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creator_slack_user_id": { + "name": "creator_slack_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_identity_id": { + "name": "creator_identity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "next_run_at_ms": { + "name": "next_run_at_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "run_now_at_ms": { + "name": "run_now_at_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at_ms": { + "name": "created_at_ms", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "record": { + "name": "record", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "junior_scheduler_tasks_creator_idx": { + "name": "junior_scheduler_tasks_creator_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "creator_slack_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"junior_scheduler_tasks\".\"status\" <> 'deleted'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_scheduler_tasks_creator_identity_idx": { + "name": "junior_scheduler_tasks_creator_identity_idx", + "columns": [ + { + "expression": "creator_identity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"junior_scheduler_tasks\".\"status\" <> 'deleted' AND \"junior_scheduler_tasks\".\"creator_identity_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_scheduler_tasks_team_status_idx": { + "name": "junior_scheduler_tasks_team_status_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"junior_scheduler_tasks\".\"status\" <> 'deleted'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_scheduler_tasks_run_now_due_idx": { + "name": "junior_scheduler_tasks_run_now_due_idx", + "columns": [ + { + "expression": "run_now_at_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"junior_scheduler_tasks\".\"status\" = 'active' AND \"junior_scheduler_tasks\".\"run_now_at_ms\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_scheduler_tasks_next_run_due_idx": { + "name": "junior_scheduler_tasks_next_run_due_idx", + "columns": [ + { + "expression": "next_run_at_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"junior_scheduler_tasks\".\"status\" = 'active' AND \"junior_scheduler_tasks\".\"next_run_at_ms\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.junior_stats": { + "name": "junior_stats", + "schema": "", + "columns": { + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "junior_stats_date_namespace_metric_name_pk": { + "name": "junior_stats_date_namespace_metric_name_pk", + "columns": [ + "date", + "namespace", + "metric", + "name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.junior_task_executions": { + "name": "junior_task_executions", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "executed_at_ms": { + "name": "executed_at_ms", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "junior_task_executions_task_time_idx": { + "name": "junior_task_executions_task_time_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "executed_at_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_task_executions_time_kind_idx": { + "name": "junior_task_executions_time_kind_idx", + "columns": [ + { + "expression": "executed_at_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "junior_task_executions_conversation_time_idx": { + "name": "junior_task_executions_conversation_time_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "executed_at_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "junior_task_executions_conversation_id_junior_conversations_conversation_id_fk": { + "name": "junior_task_executions_conversation_id_junior_conversations_conversation_id_fk", + "tableFrom": "junior_task_executions", + "tableTo": "junior_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "conversation_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "junior_task_executions_kind_namespace_execution_id_pk": { + "name": "junior_task_executions_kind_namespace_execution_id_pk", + "columns": [ + "kind", + "namespace", + "execution_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "junior_task_executions_kind_check": { + "name": "junior_task_executions_kind_check", + "value": "\"junior_task_executions\".\"kind\" in ('scheduled', 'event')" + }, + "junior_task_executions_status_check": { + "name": "junior_task_executions_status_check", + "value": "\"junior_task_executions\".\"status\" in ('blocked', 'completed', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.junior_users": { + "name": "junior_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "primary_email": { + "name": "primary_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_email_normalized": { + "name": "primary_email_normalized", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "junior_users_primary_email_normalized_uidx": { + "name": "junior_users_primary_email_normalized_uidx", + "columns": [ + { + "expression": "primary_email_normalized", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/junior/migrations/meta/_journal.json b/packages/junior/migrations/meta/_journal.json index dd44aacd1f..2fa668a218 100644 --- a/packages/junior/migrations/meta/_journal.json +++ b/packages/junior/migrations/meta/_journal.json @@ -169,6 +169,13 @@ "when": 1786153663118, "tag": "0023_task_title_columns", "breakpoints": true + }, + { + "idx": 24, + "version": "7", + "when": 1786313061094, + "tag": "0024_misty_typhoid_mary", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/junior/src/chat/agent-dispatch/types.ts b/packages/junior/src/chat/agent-dispatch/types.ts index 58d98e6bd5..6bb9cf798b 100644 --- a/packages/junior/src/chat/agent-dispatch/types.ts +++ b/packages/junior/src/chat/agent-dispatch/types.ts @@ -12,7 +12,7 @@ import type { } from "@/chat/credentials/context"; import type { AgentRunRouting } from "@/chat/agent/request"; import type { AgentTurnSurface } from "@/chat/task-execution/checkpoint"; -import type { ChannelConfigurationService } from "@/chat/configuration/types"; +import type { LocationConfigurationService } from "@/chat/configuration/types"; export type DispatchStatus = | "pending" @@ -80,7 +80,7 @@ export interface DispatchTurnResult { /** Dispatch-owned authority supplied to the shared turn runtime. */ export interface DispatchTurnContext { disabledFeatures: readonly ["interactive-auth"]; - channelConfiguration: ChannelConfigurationService; + locationConfiguration: LocationConfigurationService; credentialContext: CredentialContext; destinationVisibility: DestinationVisibility; dispatch: NonNullable; diff --git a/packages/junior/src/chat/agent/index.ts b/packages/junior/src/chat/agent/index.ts index ab789d3374..234da8d57a 100644 --- a/packages/junior/src/chat/agent/index.ts +++ b/packages/junior/src/chat/agent/index.ts @@ -449,8 +449,8 @@ async function executeAgentRunInPrivacyContext( }); } }; - const persistedConfigurationValues = policy.channelConfiguration - ? await policy.channelConfiguration.resolveValues() + const persistedConfigurationValues = policy.locationConfiguration + ? await policy.locationConfiguration.resolveValues() : {}; const configurationValues: Record = { ...getConfigDefaults(), diff --git a/packages/junior/src/chat/agent/request.ts b/packages/junior/src/chat/agent/request.ts index 13016ead88..4d15153015 100644 --- a/packages/junior/src/chat/agent/request.ts +++ b/packages/junior/src/chat/agent/request.ts @@ -13,7 +13,7 @@ import type { Source, SystemActor, } from "@sentry/junior-plugin-api"; -import type { ChannelConfigurationService } from "@/chat/configuration/types"; +import type { LocationConfigurationService } from "@/chat/configuration/types"; import type { ConversationPrivacy } from "@/chat/conversation-privacy"; import type { CredentialContext } from "@/chat/credentials/context"; import type { PiMessage } from "@/chat/pi/messages"; @@ -164,7 +164,7 @@ export interface AgentRunPolicy { /** Explicit per-agent reasoning level. When set, adaptive routing is disabled. */ reasoningLevel?: TurnReasoningLevel; configuration?: Record; - channelConfiguration?: ChannelConfigurationService; + locationConfiguration?: LocationConfigurationService; skillDirs?: string[]; /** Per-slice override for app-owned sandbox egress trace propagation. */ sandboxTracePropagation?: SandboxEgressTracePropagationConfig; diff --git a/packages/junior/src/chat/agent/sandbox.ts b/packages/junior/src/chat/agent/sandbox.ts index 549068cdd9..18bd6a5428 100644 --- a/packages/junior/src/chat/agent/sandbox.ts +++ b/packages/junior/src/chat/agent/sandbox.ts @@ -9,7 +9,7 @@ import type { FileUpload } from "chat"; import type { PluginSandbox } from "@sentry/junior-plugin-api"; import { isUserActor, type Actor } from "@/chat/actor"; import { maybeExecuteJrRpcCustomCommand } from "@/chat/capabilities/jr-rpc-command"; -import type { ChannelConfigurationService } from "@/chat/configuration/types"; +import type { LocationConfigurationService } from "@/chat/configuration/types"; import type { CredentialContext } from "@/chat/credentials/context"; import { listReferenceFiles } from "@/chat/discovery"; import type { LogContext } from "@/chat/logging"; @@ -35,7 +35,7 @@ export interface AgentSandboxOptions { egressSignals?: SandboxEgressSignalTransport; credentialEgress?: CredentialContext; actor?: Actor; - channelConfiguration?: ChannelConfigurationService; + locationConfiguration?: LocationConfigurationService; configurationValues: Record; getActiveSkill(): Skill | null; prepareSandbox(workspace: SandboxWorkspace): void | Promise; @@ -165,7 +165,7 @@ export function createAgentSandbox(options: AgentSandboxOptions): AgentSandbox { if (command) { const result = await maybeExecuteJrRpcCustomCommand(command, { activeSkill: options.getActiveSkill(), - channelConfiguration: options.channelConfiguration, + locationConfiguration: options.locationConfiguration, actorId: isUserActor(options.actor) ? options.actor.userId : undefined, diff --git a/packages/junior/src/chat/agent/tools.ts b/packages/junior/src/chat/agent/tools.ts index c5bf79f7c0..8b24e34e8d 100644 --- a/packages/junior/src/chat/agent/tools.ts +++ b/packages/junior/src/chat/agent/tools.ts @@ -182,7 +182,7 @@ export async function wireAgentTools( egressSignals: args.policy.sandboxEgressSignals, credentialEgress: args.routing.credentialContext, actor: args.currentActor, - channelConfiguration: args.policy.channelConfiguration, + locationConfiguration: args.policy.locationConfiguration, configurationValues: args.configurationValues, getActiveSkill: () => args.skillSandbox.getActiveSkill(), prepareSandbox: pluginHooks.prepareSandbox, diff --git a/packages/junior/src/chat/app/factory.ts b/packages/junior/src/chat/app/factory.ts index d0cf1df9ff..60030685d0 100644 --- a/packages/junior/src/chat/app/factory.ts +++ b/packages/junior/src/chat/app/factory.ts @@ -21,7 +21,7 @@ import { stripLeadingBotMention, } from "@/chat/runtime/thread-context"; import { - getChannelConfigurationServiceById, + getLocationConfigurationService, getPersistedThreadState, mergeArtifactsState, persistThreadState, @@ -241,7 +241,7 @@ export function createSlackRuntime(options: CreateSlackRuntimeOptions) { return { ...runtime, runDispatchTurn: createSlackDispatchTurnRunner({ - getChannelConfiguration: getChannelConfigurationServiceById, + getLocationConfiguration: getLocationConfigurationService, getSlackAdapter: options.getSlackAdapter, replyToThread, }), diff --git a/packages/junior/src/chat/capabilities/jr-rpc-command.ts b/packages/junior/src/chat/capabilities/jr-rpc-command.ts index 6b638ca669..eefd58d355 100644 --- a/packages/junior/src/chat/capabilities/jr-rpc-command.ts +++ b/packages/junior/src/chat/capabilities/jr-rpc-command.ts @@ -1,12 +1,12 @@ import { Bash, defineCommand } from "just-bash"; -import type { ChannelConfigurationService } from "@/chat/configuration/types"; +import type { LocationConfigurationService } from "@/chat/configuration/types"; import { logInfo } from "@/chat/logging"; import { pluginCatalogRuntime } from "@/chat/plugins/catalog-runtime"; import type { Skill } from "@/chat/skills"; type JrRpcDeps = { activeSkill: Skill | null; - channelConfiguration?: ChannelConfigurationService; + locationConfiguration?: LocationConfigurationService; actorId?: string; onConfigurationValueChanged?: ( key: string, @@ -32,13 +32,13 @@ function commandResult(input: { }; } -function requireChannelConfiguration( +function requireLocationConfiguration( deps: JrRpcDeps, ): - | { ok: true; configuration: ChannelConfigurationService } + | { ok: true; configuration: LocationConfigurationService } | { ok: false; result: ReturnType } { - if (deps.channelConfiguration) { - return { ok: true, configuration: deps.channelConfiguration }; + if (deps.locationConfiguration) { + return { ok: true, configuration: deps.locationConfiguration }; } return { ok: false, @@ -100,7 +100,7 @@ async function handleConfigCommand( "jr-rpc config list [--prefix ]", ].join("\n"); const subverb = (args[0] ?? "").trim(); - const configurationResult = requireChannelConfiguration(deps); + const configurationResult = requireLocationConfiguration(deps); if (!configurationResult.ok) { return configurationResult.result; } diff --git a/packages/junior/src/chat/configuration/service.ts b/packages/junior/src/chat/configuration/service.ts index 05f4bf7ff7..c1daa1c3e3 100644 --- a/packages/junior/src/chat/configuration/service.ts +++ b/packages/junior/src/chat/configuration/service.ts @@ -1,8 +1,8 @@ +import { configValueSchema } from "@/chat/configuration/types"; import type { - ChannelConfigState, - ChannelConfigurationService, - ChannelConfigurationStorage, ConfigEntry, + LocationConfigurationService, + LocationConfigurationStorage, } from "@/chat/configuration/types"; import { validateConfigKey, @@ -10,39 +10,33 @@ import { } from "@/chat/configuration/validation"; import { isRecord, toOptionalString } from "@/chat/coerce"; -function defaultState(): ChannelConfigState { - return { - schemaVersion: 1, - entries: {}, - }; -} - function sanitizeEntry(value: unknown): ConfigEntry | undefined { if (!isRecord(value)) { return undefined; } const key = toOptionalString(value.key); - if (!key) { + if (!key || validateConfigKey(key)) { return undefined; } - if (validateConfigKey(key)) { - return undefined; - } - const updatedAt = toOptionalString(value.updatedAt); if (!updatedAt) { return undefined; } - // Accept both legacy "channel" and current "conversation" scope from persisted data - if (value.scope !== "channel" && value.scope !== "conversation") { + if ( + value.scope !== "channel" && + value.scope !== "conversation" && + value.scope !== "location" + ) { + return undefined; + } + const parsedValue = configValueSchema.safeParse(value.value); + if (!parsedValue.success) { return undefined; } - const scope = "conversation" as const; - return { key, - value: value.value, - scope, + value: parsedValue.data, + scope: "location", updatedAt, updatedBy: toOptionalString(value.updatedBy), source: toOptionalString(value.source), @@ -50,130 +44,75 @@ function sanitizeEntry(value: unknown): ConfigEntry | undefined { }; } -function coerceState(raw: unknown): ChannelConfigState { - if (!isRecord(raw)) { - return defaultState(); - } - - const rawConfig = isRecord(raw.configuration) ? raw.configuration : {}; +/** Coerce legacy persisted configuration into current Location entries. */ +export function coerceLegacyLocationConfig(raw: unknown): ConfigEntry[] { + const rawConfig = isRecord(raw) && isRecord(raw.configuration) + ? raw.configuration + : {}; const rawEntries = isRecord(rawConfig.entries) ? rawConfig.entries : {}; - const entries: Record = {}; - for (const [key, value] of Object.entries(rawEntries)) { + return Object.values(rawEntries).flatMap((value) => { const entry = sanitizeEntry(value); - if (!entry) { - continue; - } - entries[key] = entry; - } - - return { - schemaVersion: 1, - entries, - }; + return entry ? [entry] : []; + }); } -export function createChannelConfigurationService( - storage: ChannelConfigurationStorage, -): ChannelConfigurationService { - const getState = async (): Promise => { - const loaded = await storage.load(); - return coerceState(loaded); - }; - - const saveState = async (state: ChannelConfigState): Promise => { - await storage.save({ - schemaVersion: 1, - entries: state.entries, - }); - }; - - const get = async (key: string): Promise => { - const normalizedKey = key.trim(); - const state = await getState(); - return state.entries[normalizedKey]; - }; +/** Create a Location configuration service over entry-level storage. */ +export function createLocationConfigurationService( + storage: LocationConfigurationStorage, +): LocationConfigurationService { + const get = async (key: string): Promise => + await storage.get(key.trim()); - const set: ChannelConfigurationService["set"] = async (input) => { + const set: LocationConfigurationService["set"] = async (input) => { const normalizedKey = input.key.trim(); const keyError = validateConfigKey(normalizedKey); if (keyError) { throw new Error(keyError); } - - const valueError = validateConfigValue(input.value); + const value = configValueSchema.parse(input.value); + const valueError = validateConfigValue(value); if (valueError) { throw new Error(valueError); } - - const state = await getState(); - const nextEntry: ConfigEntry = { + const entry: ConfigEntry = { key: normalizedKey, - value: input.value, - scope: "conversation", + value, + scope: "location", updatedAt: new Date().toISOString(), updatedBy: toOptionalString(input.updatedBy), source: toOptionalString(input.source), expiresAt: toOptionalString(input.expiresAt), }; - state.entries[normalizedKey] = nextEntry; - await saveState(state); - return nextEntry; + await storage.set(entry); + return entry; }; - const unset = async (key: string): Promise => { - const normalizedKey = key.trim(); - const state = await getState(); - if (!state.entries[normalizedKey]) { - return false; - } - delete state.entries[normalizedKey]; - await saveState(state); - return true; - }; + const unset = async (key: string): Promise => + await storage.unset(key.trim()); const list = async ( options: { prefix?: string } = {}, ): Promise => { - const state = await getState(); const prefix = options.prefix?.trim(); - return Object.values(state.entries) + return (await storage.list()) .filter((entry) => (prefix ? entry.key.startsWith(prefix) : true)) .sort((a, b) => a.key.localeCompare(b.key)); }; - const resolve = async (key: string): Promise => { - const entry = await get(key); - return entry?.value; - }; + const resolve: LocationConfigurationService["resolve"] = async (key) => + (await get(key))?.value; const resolveValues = async ( options: { keys?: string[]; prefix?: string } = {}, - ): Promise> => { - const keys = Array.isArray(options.keys) - ? options.keys - .map((entry) => entry.trim()) - .filter((entry) => entry.length > 0) - : undefined; - const entries = options.prefix - ? await list({ prefix: options.prefix }) - : await list({}); - - const filtered = keys - ? entries.filter((entry) => keys.includes(entry.key)) - : entries; - const resolved: Record = {}; - for (const entry of filtered) { - resolved[entry.key] = entry.value; - } - return resolved; + ): ReturnType => { + const keys = options.keys?.map((key) => key.trim()).filter(Boolean); + const entries = await list(options.prefix ? { prefix: options.prefix } : {}); + return Object.fromEntries( + entries + .filter((entry) => !keys || keys.includes(entry.key)) + .map((entry) => [entry.key, entry.value]), + ); }; - return { - get, - set, - unset, - list, - resolve, - resolveValues, - }; + return { get, set, unset, list, resolve, resolveValues }; } diff --git a/packages/junior/src/chat/configuration/sql.ts b/packages/junior/src/chat/configuration/sql.ts new file mode 100644 index 0000000000..843734459d --- /dev/null +++ b/packages/junior/src/chat/configuration/sql.ts @@ -0,0 +1,208 @@ +import { randomUUID } from "node:crypto"; +import type { SlackDestination } from "@sentry/junior-plugin-api"; +import { and, eq, sql } from "drizzle-orm"; +import type { JuniorDatabase } from "@/db/db"; +import { juniorDestinations, juniorLocationConfigurations } from "@/db/schema"; +import { + coerceLegacyLocationConfig, + createLocationConfigurationService, +} from "@/chat/configuration/service"; +import { configValueSchema } from "@/chat/configuration/types"; +import type { + ConfigEntry, + LocationConfigurationService, +} from "@/chat/configuration/types"; + +/** Resolve or create the canonical Location for a Slack Destination. */ +async function resolveLocationId( + db: JuniorDatabase, + destination: SlackDestination, +): Promise { + const channelId = destination.channelId; + const now = new Date(); + const rows = await db + .insert(juniorDestinations) + .values({ + id: randomUUID(), + provider: "slack", + providerTenantId: destination.teamId, + providerDestinationId: channelId, + kind: channelId.startsWith("D") + ? "dm" + : channelId.startsWith("G") + ? "group" + : "channel", + parentDestinationId: null, + displayName: null, + visibility: "unknown", + metadata: { platform: "slack" }, + createdAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [ + juniorDestinations.provider, + juniorDestinations.providerTenantId, + juniorDestinations.providerDestinationId, + ], + set: { id: sql`${juniorDestinations.id}` }, + }) + .returning({ id: juniorDestinations.id }); + const location = rows[0]; + if (!location) { + throw new Error("Location upsert returned no row"); + } + return location.id; +} + +/** Persist configuration rows for the Location addressed by a Slack Destination. */ +function createSqlLocationConfigurationStorage( + db: JuniorDatabase, + destination: SlackDestination, +) { + const locationWhere = and( + eq(juniorDestinations.provider, "slack"), + eq(juniorDestinations.providerTenantId, destination.teamId), + eq(juniorDestinations.providerDestinationId, destination.channelId), + ); + + const selectEntries = () => + db + .select({ + key: juniorLocationConfigurations.key, + value: juniorLocationConfigurations.value, + updatedAt: juniorLocationConfigurations.updatedAt, + updatedBy: juniorLocationConfigurations.updatedBy, + source: juniorLocationConfigurations.source, + expiresAt: juniorLocationConfigurations.expiresAt, + }) + .from(juniorLocationConfigurations) + .innerJoin( + juniorDestinations, + eq(juniorDestinations.id, juniorLocationConfigurations.locationId), + ); + const entryFromRow = ( + row: Awaited>[number], + ): ConfigEntry => ({ + key: row.key, + value: configValueSchema.parse(JSON.parse(row.value)), + scope: "location", + updatedAt: row.updatedAt.toISOString(), + updatedBy: row.updatedBy ?? undefined, + source: row.source ?? undefined, + expiresAt: row.expiresAt ?? undefined, + }); + + const list = async (): Promise => + (await selectEntries().where(locationWhere)).map(entryFromRow); + + const set = async (entry: ConfigEntry): Promise => { + const locationId = await resolveLocationId(db, destination); + await db + .insert(juniorLocationConfigurations) + .values({ + locationId, + key: entry.key, + value: JSON.stringify(entry.value), + updatedAt: new Date(entry.updatedAt), + updatedBy: entry.updatedBy, + source: entry.source, + expiresAt: entry.expiresAt, + }) + .onConflictDoUpdate({ + target: [ + juniorLocationConfigurations.locationId, + juniorLocationConfigurations.key, + ], + set: { + value: JSON.stringify(entry.value), + updatedAt: new Date(entry.updatedAt), + updatedBy: entry.updatedBy, + source: entry.source, + expiresAt: entry.expiresAt, + }, + }); + }; + + return { + get: async (key: string): Promise => { + const rows = await selectEntries() + .where(and(locationWhere, eq(juniorLocationConfigurations.key, key))) + .limit(1); + return rows[0] ? entryFromRow(rows[0]) : undefined; + }, + list, + set, + unset: async (key: string): Promise => { + const locationId = await resolveLocationId(db, destination); + const rows = await db + .delete(juniorLocationConfigurations) + .where( + and( + eq(juniorLocationConfigurations.locationId, locationId), + eq(juniorLocationConfigurations.key, key), + ), + ) + .returning({ key: juniorLocationConfigurations.key }); + return rows.length > 0; + }, + insertLegacy: async (entries: ConfigEntry[]): Promise => { + if (entries.length === 0) { + return; + } + const locationId = await resolveLocationId(db, destination); + await db + .insert(juniorLocationConfigurations) + .values( + entries.map((entry) => ({ + locationId, + key: entry.key, + value: JSON.stringify(entry.value), + updatedAt: new Date(entry.updatedAt), + updatedBy: entry.updatedBy, + source: entry.source, + expiresAt: entry.expiresAt, + })), + ) + .onConflictDoNothing(); + }, + }; +} + +/** Resolve SQL-owned Location configuration and copy live legacy entries once. */ +export function createDurableLocationConfigurationService(args: { + destination: SlackDestination; + db: JuniorDatabase; + loadLegacy: () => Promise; +}): LocationConfigurationService { + const storage = createSqlLocationConfigurationStorage(args.db, args.destination); + let cutover: Promise | undefined; + const ensureCutover = () => + (cutover ??= (async () => { + if ((await storage.list()).length > 0) { + return; + } + // TODO(v0.147.0): Remove the 7-day Redis Location configuration import. + const legacy = coerceLegacyLocationConfig(await args.loadLegacy()); + await storage.insertLegacy(legacy); + })()); + + return createLocationConfigurationService({ + get: async (key) => { + await ensureCutover(); + return await storage.get(key); + }, + list: async () => { + await ensureCutover(); + return await storage.list(); + }, + set: async (entry) => { + await ensureCutover(); + await storage.set(entry); + }, + unset: async (key) => { + await ensureCutover(); + return await storage.unset(key); + }, + }); +} diff --git a/packages/junior/src/chat/configuration/types.ts b/packages/junior/src/chat/configuration/types.ts index 6f04591ac1..98b1a4fe85 100644 --- a/packages/junior/src/chat/configuration/types.ts +++ b/packages/junior/src/chat/configuration/types.ts @@ -1,26 +1,30 @@ -export type ConfigScope = "conversation"; +import { z } from "zod"; +/** JSON value accepted by Location configuration. */ +export const configValueSchema = z.json(); +export type ConfigValue = z.output; + +/** One user-authored configuration value scoped to a Location. */ export interface ConfigEntry { key: string; - value: unknown; - scope: ConfigScope; + value: ConfigValue; + scope: "location"; updatedAt: string; updatedBy?: string; source?: string; expiresAt?: string; } -export interface ChannelConfigState { - schemaVersion: 1; - entries: Record; -} - -export interface ChannelConfigurationStorage { - load: () => Promise; - save: (state: ChannelConfigState) => Promise; +/** Entry-level persistence used by Location configuration. */ +export interface LocationConfigurationStorage { + get: (key: string) => Promise; + list: () => Promise; + set: (entry: ConfigEntry) => Promise; + unset: (key: string) => Promise; } -export interface ChannelConfigurationService { +/** Read and update user-authored configuration for one Location. */ +export interface LocationConfigurationService { get: (key: string) => Promise; set: (input: { key: string; @@ -31,9 +35,9 @@ export interface ChannelConfigurationService { }) => Promise; unset: (key: string) => Promise; list: (options?: { prefix?: string }) => Promise; - resolve: (key: string) => Promise; + resolve: (key: string) => Promise; resolveValues: (options?: { keys?: string[]; prefix?: string; - }) => Promise>; + }) => Promise>; } diff --git a/packages/junior/src/chat/runtime/reply-executor.ts b/packages/junior/src/chat/runtime/reply-executor.ts index b783697a02..0afe73ff32 100644 --- a/packages/junior/src/chat/runtime/reply-executor.ts +++ b/packages/junior/src/chat/runtime/reply-executor.ts @@ -579,7 +579,8 @@ export function createReplyToThread(deps: ReplyExecutorDeps) { options.explicitMention || message.isMention, ), queuedMessages: options.queuedMessages, - channelConfiguration: options.execution?.channelConfiguration, + destination, + locationConfiguration: options.execution?.locationConfiguration, context: { threadId, actorId: slackActorId, @@ -870,7 +871,7 @@ export function createReplyToThread(deps: ReplyExecutorDeps) { const configReply = options.execution?.skipProviderDefaultConfig ? undefined : await maybeApplyProviderDefaultConfigRequest({ - channelConfiguration: preparedState.channelConfiguration, + locationConfiguration: preparedState.locationConfiguration, actorId: actor?.userId, text: effectiveUserText, }); @@ -1386,7 +1387,7 @@ export function createReplyToThread(deps: ReplyExecutorDeps) { }, policy: { configuration: preparedState.configuration, - channelConfiguration: preparedState.channelConfiguration, + locationConfiguration: preparedState.locationConfiguration, disabledFeatures: options.execution?.disabledFeatures ?? (message.author.isBot === true diff --git a/packages/junior/src/chat/runtime/slack-resume.ts b/packages/junior/src/chat/runtime/slack-resume.ts index e301f9a9ab..a31288787c 100644 --- a/packages/junior/src/chat/runtime/slack-resume.ts +++ b/packages/junior/src/chat/runtime/slack-resume.ts @@ -8,7 +8,11 @@ import type { ReplyAttribution } from "@sentry/junior-plugin-api"; import { botConfig } from "@/chat/config"; import { standardModelId } from "@/chat/model-profile"; -import type { ChannelConfigurationService } from "@/chat/configuration/types"; +import { configValueSchema } from "@/chat/configuration/types"; +import type { + ConfigValue, + LocationConfigurationService, +} from "@/chat/configuration/types"; import { RetryableDeliveryError, type AgentRunRequest, @@ -117,12 +121,18 @@ async function postSlackMessageBestEffort( /** Create a read-only configuration service from persisted values. */ function createReadOnlyConfigService( - values: Record, -): ChannelConfigurationService { + rawValues: Record, +): LocationConfigurationService { + const values: Record = Object.fromEntries( + Object.entries(rawValues).map(([key, value]) => [ + key, + configValueSchema.parse(value), + ]), + ); const entries = Object.entries(values).map(([key, value]) => ({ key, value, - scope: "conversation" as const, + scope: "location" as const, updatedAt: new Date().toISOString(), })); @@ -136,7 +146,7 @@ function createReadOnlyConfigService( entries.filter((entry) => !prefix || entry.key.startsWith(prefix)), resolve: async (key) => values[key], resolveValues: async ({ keys, prefix } = {}) => { - const filtered: Record = {}; + const filtered: Record = {}; for (const [key, value] of Object.entries(values)) { if (prefix && !key.startsWith(prefix)) continue; if (keys && !keys.includes(key)) continue; @@ -352,8 +362,8 @@ function createResumeReplyContext( const requestDeadline = getTurnRequestDeadline(); const threadId = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs); - const persistedChannelConfiguration = - replyContext.policy?.channelConfiguration ?? + const persistedLocationConfiguration = + replyContext.policy?.locationConfiguration ?? (replyContext.policy?.configuration ? createReadOnlyConfigService(replyContext.policy.configuration) : undefined); @@ -380,7 +390,7 @@ function createResumeReplyContext( ...replyContext.policy, turnDeadlineAtMs: replyContext.policy?.turnDeadlineAtMs ?? requestDeadline?.deadlineAtMs, - channelConfiguration: persistedChannelConfiguration, + locationConfiguration: persistedLocationConfiguration, }, state: replyContext.state, observers: { diff --git a/packages/junior/src/chat/runtime/slack-runtime.ts b/packages/junior/src/chat/runtime/slack-runtime.ts index ef842af222..f8f1751072 100644 --- a/packages/junior/src/chat/runtime/slack-runtime.ts +++ b/packages/junior/src/chat/runtime/slack-runtime.ts @@ -965,6 +965,7 @@ export function createSlackTurnRuntime< message, text: currentText, explicitMention: Boolean(message.isMention), + destination: hooks.destination, context: threadContext, queuedMessages, }); diff --git a/packages/junior/src/chat/runtime/thread-state.ts b/packages/junior/src/chat/runtime/thread-state.ts index c1bf0ffd38..3fc18de9b5 100644 --- a/packages/junior/src/chat/runtime/thread-state.ts +++ b/packages/junior/src/chat/runtime/thread-state.ts @@ -1,7 +1,9 @@ import type { Thread } from "chat"; +import type { Destination } from "@sentry/junior-plugin-api"; import { toOptionalString } from "@/chat/coerce"; -import { createChannelConfigurationService } from "@/chat/configuration/service"; -import type { ChannelConfigurationService } from "@/chat/configuration/types"; +import { createDurableLocationConfigurationService } from "@/chat/configuration/sql"; +import type { LocationConfigurationService } from "@/chat/configuration/types"; +import { getDb } from "@/chat/db"; import { buildConversationStatePatch } from "@/chat/state/conversation"; import type { ThreadConversationState } from "@/chat/state/conversation"; import { persistConversationMessages } from "@/chat/conversations/messages"; @@ -46,7 +48,7 @@ function buildThreadStatePayload( } /** - * Merge a payload into thread or channel scratch with Junior's TTL. + * Merge a payload into thread scratch with Junior's TTL. * * Chat SDK state writes hardcode a 30-day TTL. This boundary owns Junior's * shorter retention policy for the same scratch keys. @@ -157,19 +159,6 @@ export async function getPersistedThreadState( ); } -/** Load the persisted state payload for a channel without constructing a Chat channel. */ -export async function getPersistedChannelState( - channelId: string, -): Promise> { - const stateAdapter = getStateAdapter(); - await stateAdapter.connect(); - return ( - (await stateAdapter.get>( - channelStateKey(channelId), - )) ?? {} - ); -} - /** Persist a thread-state patch by thread id without constructing a Chat thread. */ export async function persistThreadStateById( threadId: string, @@ -191,36 +180,34 @@ export async function persistThreadStateById( ); } -/** Resolve channel configuration from a Chat thread and persist it with Junior's TTL. */ -export function getChannelConfigurationService( - thread: Thread, -): ChannelConfigurationService { - const channel = thread.channel; - const channelId = - toOptionalString(thread.channelId) ?? toOptionalString(channel.id); - if (!channelId) { - throw new Error("channel id is required to load channel configuration"); - } - return createChannelConfigurationService({ - load: async () => await channel.state, - save: async (state) => { - await mergePersistedState(channelStateKey(channelId), { - configuration: state, - }); - }, - }); +/** Load legacy Redis-backed channel state during the SQL cutover. */ +async function getLegacyChannelState( + channelId: string, +): Promise> { + const stateAdapter = getStateAdapter(); + await stateAdapter.connect(); + return ( + (await stateAdapter.get>( + channelStateKey(channelId), + )) ?? {} + ); } -/** Resolve a channel configuration service by channel id without a Chat thread. */ -export function getChannelConfigurationServiceById( - channelId: string, -): ChannelConfigurationService { - return createChannelConfigurationService({ - load: async () => await getPersistedChannelState(channelId), - save: async (state) => { - await mergePersistedState(channelStateKey(channelId), { - configuration: state, - }); - }, +/** + * Resolve durable location configuration. + * + * Slack still reads the old channel-id Redis bag once during cutover. + */ +export function getLocationConfigurationService( + destination: Destination, +): LocationConfigurationService { + if (destination.platform !== "slack") { + throw new Error("Location configuration requires a Slack Location"); + } + return createDurableLocationConfigurationService({ + destination, + db: getDb(), + loadLegacy: async () => + await getLegacyChannelState(destination.channelId), }); } diff --git a/packages/junior/src/chat/runtime/turn-input.ts b/packages/junior/src/chat/runtime/turn-input.ts index 3b44bd2e33..47381cc83e 100644 --- a/packages/junior/src/chat/runtime/turn-input.ts +++ b/packages/junior/src/chat/runtime/turn-input.ts @@ -1,5 +1,6 @@ import type { Message, Thread } from "chat"; -import type { ChannelConfigurationService } from "@/chat/configuration/types"; +import type { Destination } from "@sentry/junior-plugin-api"; +import type { LocationConfigurationService } from "@/chat/configuration/types"; export interface TurnContext { channelId?: string; @@ -24,7 +25,8 @@ export interface QueuedTurnMessage extends TurnMessageText { } export interface PrepareTurnStateInput { - channelConfiguration?: ChannelConfigurationService; + destination: Destination; + locationConfiguration?: LocationConfigurationService; context: TurnContext; explicitMention: boolean; message: Message; diff --git a/packages/junior/src/chat/runtime/turn-preparation.ts b/packages/junior/src/chat/runtime/turn-preparation.ts index f0b0907672..1986a12ec9 100644 --- a/packages/junior/src/chat/runtime/turn-preparation.ts +++ b/packages/junior/src/chat/runtime/turn-preparation.ts @@ -31,7 +31,7 @@ import { isVisionEnabled, } from "@/chat/slack/vision-context"; import { - getChannelConfigurationService, + getLocationConfigurationService, getPersistedSandboxState, } from "@/chat/runtime/thread-state"; import { @@ -39,7 +39,7 @@ import { persistConversationMessages, } from "@/chat/conversations/messages"; import { persistConversationMessageSummaries } from "@/chat/conversations/message-summaries"; -import type { ChannelConfigurationService } from "@/chat/configuration/types"; +import type { LocationConfigurationService } from "@/chat/configuration/types"; import { parseContent } from "@/chat/slack/message/content"; import type { PrepareTurnStateInput, @@ -52,7 +52,7 @@ const BACKFILL_MESSAGE_LIMIT = 80; export interface PreparedTurnState { artifacts: ThreadArtifactsState; configuration?: Record; - channelConfiguration?: ChannelConfigurationService; + locationConfiguration?: LocationConfigurationService; conversation: ThreadConversationState; conversationContext?: string; sandboxRef?: SandboxRef; @@ -170,9 +170,10 @@ export function createPrepareTurnState(deps: PrepareTurnStateDeps) { const conversation = coerceThreadConversationState(existingState); const conversationId = args.context.threadId ?? args.context.runId; await hydrateConversationMessages({ conversation, conversationId }); - const channelConfiguration = - args.channelConfiguration ?? getChannelConfigurationService(args.thread); - const configuration = await channelConfiguration.resolveValues(); + const locationConfiguration = + args.locationConfiguration ?? + getLocationConfigurationService(args.destination); + const configuration = await locationConfiguration.resolveValues(); const backfillSource = args.skipBackfill ? undefined @@ -255,7 +256,7 @@ export function createPrepareTurnState(deps: PrepareTurnStateDeps) { return { artifacts, configuration, - channelConfiguration, + locationConfiguration, conversation, sandboxRef, conversationContext, diff --git a/packages/junior/src/chat/services/provider-default-config.ts b/packages/junior/src/chat/services/provider-default-config.ts index 9607ddfb3a..04605d6211 100644 --- a/packages/junior/src/chat/services/provider-default-config.ts +++ b/packages/junior/src/chat/services/provider-default-config.ts @@ -1,4 +1,4 @@ -import type { ChannelConfigurationService } from "@/chat/configuration/types"; +import type { LocationConfigurationService } from "@/chat/configuration/types"; const GITHUB_REPO_PART = String.raw`[A-Za-z0-9_.-]*[A-Za-z0-9_-]`; const GITHUB_REPO_RE = new RegExp( @@ -8,17 +8,17 @@ const GITHUB_REPO_RE = new RegExp( /** Apply explicit provider-default config requests that do not need agent reasoning. */ export async function maybeApplyProviderDefaultConfigRequest(args: { - channelConfiguration?: ChannelConfigurationService; + locationConfiguration?: LocationConfigurationService; actorId?: string; text: string; }): Promise<{ text: string } | null> { const match = GITHUB_REPO_RE.exec(args.text); const repo = match?.[1]; - if (!repo || !args.channelConfiguration) { + if (!repo || !args.locationConfiguration) { return null; } - await args.channelConfiguration.set({ + await args.locationConfiguration.set({ key: "github.repo", value: repo, updatedBy: args.actorId, diff --git a/packages/junior/src/chat/slack/dispatch-turn.ts b/packages/junior/src/chat/slack/dispatch-turn.ts index 19f1bfe031..80d15aab02 100644 --- a/packages/junior/src/chat/slack/dispatch-turn.ts +++ b/packages/junior/src/chat/slack/dispatch-turn.ts @@ -32,9 +32,9 @@ interface DispatchReplyToThread { /** Build the Slack provider adapter for agent-dispatched conversation turns. */ export function createSlackDispatchTurnRunner(options: { - getChannelConfiguration: ( - channelId: string, - ) => DispatchTurnContext["channelConfiguration"]; + getLocationConfiguration: ( + destination: DispatchRecord["destination"], + ) => DispatchTurnContext["locationConfiguration"]; getSlackAdapter: () => SlackAdapter; replyToThread: DispatchReplyToThread; }) { @@ -95,8 +95,8 @@ export function createSlackDispatchTurnRunner(options: { destination: dispatch.destination, execution: { disabledFeatures: ["interactive-auth"], - channelConfiguration: options.getChannelConfiguration( - dispatch.destination.channelId, + locationConfiguration: options.getLocationConfiguration( + dispatch.destination, ), credentialContext: routing.credentialContext, destinationVisibility: dispatch.destinationVisibility, diff --git a/packages/junior/src/chat/task-execution/paused-turn.ts b/packages/junior/src/chat/task-execution/paused-turn.ts index d1627a61ed..bd043ea53a 100644 --- a/packages/junior/src/chat/task-execution/paused-turn.ts +++ b/packages/junior/src/chat/task-execution/paused-turn.ts @@ -31,7 +31,7 @@ import { getPersistedThreadState, getPersistedSandboxState, persistThreadStateById, - getChannelConfigurationServiceById, + getLocationConfigurationService, } from "@/chat/runtime/thread-state"; import { buildDeliveredTurnStatePatch } from "@/chat/runtime/delivered-turn-state"; import { @@ -473,9 +473,8 @@ async function runPausedTurnInContext( } const { actor, credentialContext } = identity; - const channelConfiguration = getChannelConfigurationServiceById( - destination.channelId, - ); + const locationConfiguration = + getLocationConfigurationService(destination); const conversationContext = dispatchId ? undefined : buildConversationContext(conversation, { @@ -544,7 +543,7 @@ async function runPausedTurnInContext( artifacts.assistantContextChannelId ?? destination.channelId, }, policy: { - channelConfiguration, + locationConfiguration, }, state: { artifactState: artifacts, diff --git a/packages/junior/src/db/schema.ts b/packages/junior/src/db/schema.ts index c66e3032b8..bb329c4822 100644 --- a/packages/junior/src/db/schema.ts +++ b/packages/junior/src/db/schema.ts @@ -7,6 +7,7 @@ import { juniorAgentBindings, juniorAgentInvocations, } from "./schema/agent-invocations"; +import { juniorLocationConfigurations } from "./schema/location-configurations"; import { juniorDestinations } from "./schema/destinations"; import { juniorEventTasks } from "./schema/event-tasks"; import { juniorIdentities } from "./schema/identities"; @@ -26,6 +27,7 @@ export { juniorConversationEvents, juniorConversationBindings, juniorConversations, + juniorLocationConfigurations, juniorDestinations, juniorEventTasks, juniorIdentities, @@ -44,6 +46,7 @@ export const juniorSqlSchema = { juniorConversationEvents, juniorConversationBindings, juniorConversations, + juniorLocationConfigurations, juniorDestinations, juniorEventTasks, juniorIdentities, diff --git a/packages/junior/src/db/schema/location-configurations.ts b/packages/junior/src/db/schema/location-configurations.ts new file mode 100644 index 0000000000..9c516d9208 --- /dev/null +++ b/packages/junior/src/db/schema/location-configurations.ts @@ -0,0 +1,20 @@ +import { pgTable, primaryKey, text } from "drizzle-orm/pg-core"; +import { juniorDestinations } from "./destinations"; +import { timestamptz } from "./timestamps"; + +/** Durable user configuration entries scoped to provider Locations. */ +export const juniorLocationConfigurations = pgTable( + "junior_location_configurations", + { + locationId: text("location_id") + .notNull() + .references(() => juniorDestinations.id, { onDelete: "cascade" }), + key: text("key").notNull(), + value: text("value").notNull(), + updatedAt: timestamptz("updated_at").notNull(), + updatedBy: text("updated_by"), + source: text("source"), + expiresAt: text("expires_at"), + }, + (table) => [primaryKey({ columns: [table.locationId, table.key] })], +); diff --git a/packages/junior/src/handlers/mcp-oauth-callback.ts b/packages/junior/src/handlers/mcp-oauth-callback.ts index 567d2a147b..3192219579 100644 --- a/packages/junior/src/handlers/mcp-oauth-callback.ts +++ b/packages/junior/src/handlers/mcp-oauth-callback.ts @@ -22,7 +22,7 @@ import { logException, logWarn } from "@/chat/logging"; import type { AgentRunResult } from "@/chat/services/turn-result"; import type { AgentRunner } from "@/chat/runtime/agent-runner"; import { - getChannelConfigurationServiceById, + getLocationConfigurationService, getPersistedSandboxState, getPersistedThreadState, persistThreadStateById, @@ -343,9 +343,8 @@ async function resumeAuthorizedMcpTurn(args: { excludeMessageId: lockedUserMessage.id, }, ); - const lockedChannelConfiguration = getChannelConfigurationServiceById( - authSession.channelId!, - ); + const lockedLocationConfiguration = + getLocationConfigurationService(destination); let actor: Actor; try { actor = createSlackResumeActor({ @@ -415,7 +414,7 @@ async function resumeAuthorizedMcpTurn(args: { }, policy: { configuration: authSession.configuration, - channelConfiguration: lockedChannelConfiguration, + locationConfiguration: lockedLocationConfiguration, }, state: { artifactState: lockedArtifacts, diff --git a/packages/junior/src/handlers/oauth-callback.ts b/packages/junior/src/handlers/oauth-callback.ts index 552b37cb1b..7f13c64857 100644 --- a/packages/junior/src/handlers/oauth-callback.ts +++ b/packages/junior/src/handlers/oauth-callback.ts @@ -19,7 +19,7 @@ import { persistAuthPauseTurnState } from "@/chat/runtime/auth-pause-state"; import { logException, logInfo, logWarn, withLogContext } from "@/chat/logging"; import { htmlCallbackResponse } from "@/handlers/oauth-html"; import { - getChannelConfigurationServiceById, + getLocationConfigurationService, getPersistedSandboxState, getPersistedThreadState, persistThreadStateById, @@ -358,9 +358,8 @@ async function resumeOAuthSessionRecordTurn( excludeMessageId: lockedUserMessage.id, }, ); - const lockedChannelConfiguration = getChannelConfigurationServiceById( - stored.channelId!, - ); + const lockedLocationConfiguration = + getLocationConfigurationService(destination); let actor: Actor; try { actor = createSlackResumeActor({ @@ -432,7 +431,7 @@ async function resumeOAuthSessionRecordTurn( lockedArtifacts.assistantContextChannelId ?? stored.channelId!, }, policy: { - channelConfiguration: lockedChannelConfiguration, + locationConfiguration: lockedLocationConfiguration, }, state: { artifactState: lockedArtifacts, diff --git a/packages/junior/tests/component/config/location-configuration-sql.test.ts b/packages/junior/tests/component/config/location-configuration-sql.test.ts new file mode 100644 index 0000000000..c6871861fc --- /dev/null +++ b/packages/junior/tests/component/config/location-configuration-sql.test.ts @@ -0,0 +1,136 @@ +import { describe, expect, it } from "vitest"; +import { createDurableLocationConfigurationService } from "@/chat/configuration/sql"; +import { migrateSchema } from "@/chat/conversations/sql/migrations"; +import { createLocalJuniorSqlFixture } from "../../fixtures/sql"; + +const DESTINATION = { + platform: "slack" as const, + teamId: "T-durable", + channelId: "C-durable", +}; + +function legacyConfiguration(value: string) { + return { + configuration: { + schemaVersion: 1, + entries: { + "github.repo": { + key: "github.repo", + value, + scope: "conversation", + updatedAt: "2026-08-09T00:00:00.000Z", + }, + }, + }, + }; +} + +describe("SQL location configuration", () => { + it("persists configuration independently of the legacy cache", async () => { + const fixture = await createLocalJuniorSqlFixture(); + await migrateSchema(fixture.sql); + + try { + const service = createDurableLocationConfigurationService({ + destination: DESTINATION, + db: fixture.sql.db(), + loadLegacy: async () => null, + }); + await service.set({ + key: "github.repo", + value: "getsentry/junior", + updatedBy: "U123", + }); + + const reloaded = createDurableLocationConfigurationService({ + destination: DESTINATION, + db: fixture.sql.db(), + loadLegacy: async () => { + throw new Error("legacy configuration should not be read"); + }, + }); + await expect(reloaded.resolve("github.repo")).resolves.toBe( + "getsentry/junior", + ); + await expect(reloaded.get("github.repo")).resolves.toMatchObject({ + scope: "location", + }); + } finally { + await fixture.close(); + } + }); + + it("copies a live legacy record into SQL once", async () => { + const fixture = await createLocalJuniorSqlFixture(); + await migrateSchema(fixture.sql); + + try { + const service = createDurableLocationConfigurationService({ + destination: { + platform: "slack", + teamId: "T-cutover", + channelId: "C-cutover", + }, + db: fixture.sql.db(), + loadLegacy: async () => legacyConfiguration("getsentry/legacy"), + }); + await expect(service.resolve("github.repo")).resolves.toBe( + "getsentry/legacy", + ); + + const reloaded = createDurableLocationConfigurationService({ + destination: { + platform: "slack", + teamId: "T-cutover", + channelId: "C-cutover", + }, + db: fixture.sql.db(), + loadLegacy: async () => null, + }); + await expect(reloaded.resolve("github.repo")).resolves.toBe( + "getsentry/legacy", + ); + } finally { + await fixture.close(); + } + }); + + it("keeps a concurrent SQL write over a stale legacy cutover", async () => { + const fixture = await createLocalJuniorSqlFixture(); + await migrateSchema(fixture.sql); + const destination = { + platform: "slack" as const, + teamId: "T-race", + channelId: "C-race", + }; + + try { + const writer = createDurableLocationConfigurationService({ + destination, + db: fixture.sql.db(), + loadLegacy: async () => null, + }); + const reader = createDurableLocationConfigurationService({ + destination, + db: fixture.sql.db(), + loadLegacy: async () => { + await writer.set({ + key: "github.repo", + value: "getsentry/fresh", + updatedBy: "U-fresh", + }); + return legacyConfiguration("getsentry/stale"); + }, + }); + + await expect(reader.resolve("github.repo")).resolves.toBe( + "getsentry/fresh", + ); + await expect(writer.resolve("github.repo")).resolves.toBe( + "getsentry/fresh", + ); + } finally { + await fixture.close(); + } + }); +}); diff --git a/packages/junior/tests/component/runtime/thread-state.test.ts b/packages/junior/tests/component/runtime/thread-state.test.ts index 23197ce498..aaf8cb59d5 100644 --- a/packages/junior/tests/component/runtime/thread-state.test.ts +++ b/packages/junior/tests/component/runtime/thread-state.test.ts @@ -1,8 +1,6 @@ import type { Thread } from "chat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { - getChannelConfigurationService, - getPersistedChannelState, getPersistedSandboxState, getPersistedThreadState, persistThreadRuntimeState, @@ -62,12 +60,10 @@ describe("thread sandbox state", () => { expect(getPersistedSandboxState(state)).toBeUndefined(); }); - it("writes thread and channel scratch with Junior's 7-day TTL", async () => { + it("writes thread scratch with Junior's 7-day TTL", async () => { const stateAdapter = getStateAdapter(); const set = vi.spyOn(stateAdapter, "set"); const conversationId = "local:test:thread-scratch-ttl"; - const channelId = "C-scratch-ttl"; - await persistThreadStateById(conversationId, { sandboxRef: { id: "sandbox-ttl" }, }); @@ -80,8 +76,8 @@ describe("thread sandbox state", () => { set.mockClear(); const thread = { id: conversationId, - channelId, - channel: { id: channelId }, + channelId: "C-scratch-ttl", + channel: { id: "C-scratch-ttl" }, } as Thread; await persistThreadRuntimeState(thread, { artifacts: { lastCanvasId: "Fcanvas" }, @@ -94,38 +90,6 @@ describe("thread sandbox state", () => { }), JUNIOR_THREAD_STATE_TTL_MS, ); - - set.mockClear(); - await getChannelConfigurationService(thread).set({ - key: "github.repo", - value: "getsentry/junior", - updatedBy: "U123", - }); - expect(set).toHaveBeenCalledWith( - `channel-state:${channelId}`, - expect.objectContaining({ - configuration: expect.objectContaining({ - entries: expect.objectContaining({ - "github.repo": expect.objectContaining({ - key: "github.repo", - value: "getsentry/junior", - }), - }), - }), - }), - JUNIOR_THREAD_STATE_TTL_MS, - ); - - await expect(getPersistedChannelState(channelId)).resolves.toMatchObject({ - configuration: { - entries: { - "github.repo": { - key: "github.repo", - value: "getsentry/junior", - }, - }, - }, - }); }); }); diff --git a/packages/junior/tests/component/scheduled-tasks-sql.test.ts b/packages/junior/tests/component/scheduled-tasks-sql.test.ts index 7df72501f7..cc00cfd011 100644 --- a/packages/junior/tests/component/scheduled-tasks-sql.test.ts +++ b/packages/junior/tests/component/scheduled-tasks-sql.test.ts @@ -175,7 +175,7 @@ describe("scheduled-task SQL storage", () => { await expect(migrateSchema(fixture.sql)).resolves.toMatchObject({ existing: 16, - migrated: 8, + migrated: 9, }); const [migrated] = await fixture.sql.query<{ creatorIdentityId: string | null; diff --git a/packages/junior/tests/integration/agent-continue-slack.test.ts b/packages/junior/tests/integration/agent-continue-slack.test.ts index b03b0d724e..ab62abef57 100644 --- a/packages/junior/tests/integration/agent-continue-slack.test.ts +++ b/packages/junior/tests/integration/agent-continue-slack.test.ts @@ -243,11 +243,13 @@ describe("paused turn Slack integration", () => { }, }, }); - await threadStateModule.getChannelConfigurationServiceById("C123").set({ - key: "demo.org", - value: "acme", - source: "test", - }); + await threadStateModule + .getLocationConfigurationService(SLACK_DESTINATION) + .set({ + key: "demo.org", + value: "acme", + source: "test", + }); const continued = await continueAgentRun({ conversationId, @@ -281,7 +283,7 @@ describe("paused turn Slack integration", () => { ); const resumeContext = executeAgentRunMock.mock.calls[0]?.[0] as { policy?: { - channelConfiguration?: { + locationConfiguration?: { resolve: (key: string) => Promise; }; turnDeadlineAtMs?: number; @@ -290,7 +292,7 @@ describe("paused turn Slack integration", () => { expect(resumeContext.policy?.turnDeadlineAtMs).toEqual(expect.any(Number)); expect(resumeContext.policy?.turnDeadlineAtMs).toBeGreaterThan(Date.now()); expect( - await resumeContext.policy?.channelConfiguration?.resolve("demo.org"), + await resumeContext.policy?.locationConfiguration?.resolve("demo.org"), ).toBe("acme"); expect(slackApiOutbox.calls("assistant.threads.setStatus")).toEqual( diff --git a/packages/junior/tests/integration/slack/provider-default-config-behavior.test.ts b/packages/junior/tests/integration/slack/provider-default-config-behavior.test.ts index ace83980e0..6f0d2c5706 100644 --- a/packages/junior/tests/integration/slack/provider-default-config-behavior.test.ts +++ b/packages/junior/tests/integration/slack/provider-default-config-behavior.test.ts @@ -6,7 +6,7 @@ import { createTestDestination, } from "../../fixtures/slack-harness"; import { completedAgentRun } from "@/chat/runtime/agent-run-outcome"; -import { getPersistedChannelState } from "@/chat/runtime/thread-state"; +import { getLocationConfigurationService } from "@/chat/runtime/thread-state"; import { deliverAssistantMessagesForTest } from "../../fixtures/agent-runner"; function toPostedText(value: unknown): string { @@ -50,16 +50,14 @@ describe("Slack behavior: provider default configuration", () => { expect(executeAgentRun).not.toHaveBeenCalled(); expect(thread.posts).toHaveLength(1); expect(toPostedText(thread.posts[0])).toContain("getsentry/junior"); - await expect(getPersistedChannelState(thread.channelId)).resolves.toMatchObject({ - configuration: { - entries: { - "github.repo": { - key: "github.repo", - value: "getsentry/junior", - source: "provider-default-config", - }, - }, - }, + await expect( + getLocationConfigurationService(createTestDestination(thread)).get( + "github.repo", + ), + ).resolves.toMatchObject({ + key: "github.repo", + value: "getsentry/junior", + source: "provider-default-config", }); }); @@ -105,12 +103,10 @@ describe("Slack behavior: provider default configuration", () => { expect(executeAgentRun).toHaveBeenCalledOnce(); expect(toPostedText(thread.posts[0])).toContain("Created the issue."); - await expect(getPersistedChannelState(thread.channelId)).resolves.not.toMatchObject({ - configuration: { - entries: { - "github.repo": expect.anything(), - }, - }, - }); + await expect( + getLocationConfigurationService(createTestDestination(thread)).get( + "github.repo", + ), + ).resolves.toBeUndefined(); }); }); diff --git a/packages/junior/tests/unit/config/configuration-service.test.ts b/packages/junior/tests/unit/config/configuration-service.test.ts index 1737e5da52..6db47269f9 100644 --- a/packages/junior/tests/unit/config/configuration-service.test.ts +++ b/packages/junior/tests/unit/config/configuration-service.test.ts @@ -1,26 +1,23 @@ import { describe, expect, it } from "vitest"; -import { createChannelConfigurationService } from "@/chat/configuration/service"; +import { createLocationConfigurationService } from "@/chat/configuration/service"; +import type { ConfigEntry } from "@/chat/configuration/types"; function createInMemoryService() { - let state: Record | null = null; - const service = createChannelConfigurationService({ - load: async () => state, - save: async (next) => { - state = { - ...(state ?? {}), - configuration: next, - }; + const entries = new Map(); + const service = createLocationConfigurationService({ + get: async (key) => entries.get(key), + list: async () => [...entries.values()], + set: async (entry) => { + entries.set(entry.key, entry); }, + unset: async (key) => entries.delete(key), }); - return { - service, - getState: () => state, - }; + return { service, entries }; } -describe("channel configuration service", () => { +describe("location configuration service", () => { it("sets, gets, lists, resolves, and unsets entries", async () => { - const { service, getState } = createInMemoryService(); + const { service, entries } = createInMemoryService(); const created = await service.set({ key: "github.repo", @@ -28,26 +25,19 @@ describe("channel configuration service", () => { updatedBy: "U123", source: "test", }); - expect(created.key).toBe("github.repo"); - expect(created.scope).toBe("conversation"); - expect(created.updatedBy).toBe("U123"); - expect(created.source).toBe("test"); - - const fetched = await service.get("github.repo"); - expect(fetched?.value).toBe("getsentry/junior"); - - const listed = await service.list(); - expect(listed).toHaveLength(1); - expect(listed[0]?.key).toBe("github.repo"); - - await service.set({ - key: "jira.project", - value: "PLAT", + expect(created).toMatchObject({ + key: "github.repo", + scope: "location", + updatedBy: "U123", + source: "test", + }); + await expect(service.get("github.repo")).resolves.toMatchObject({ + value: "getsentry/junior", }); - const prefixed = await service.list({ prefix: "github." }); - expect(prefixed).toHaveLength(1); - expect(prefixed[0]?.key).toBe("github.repo"); + await expect(service.list()).resolves.toHaveLength(1); + await service.set({ key: "jira.project", value: "PLAT" }); + await expect(service.list({ prefix: "github." })).resolves.toHaveLength(1); await expect(service.resolve("github.repo")).resolves.toBe( "getsentry/junior", ); @@ -57,38 +47,18 @@ describe("channel configuration service", () => { }); await expect( service.resolveValues({ keys: ["jira.project"] }), - ).resolves.toEqual({ - "jira.project": "PLAT", - }); + ).resolves.toEqual({ "jira.project": "PLAT" }); await expect(service.unset("github.repo")).resolves.toBe(true); await expect(service.unset("github.repo")).resolves.toBe(false); - await expect(service.get("github.repo")).resolves.toBeUndefined(); - - expect(getState()).toEqual({ - configuration: { - schemaVersion: 1, - entries: { - "jira.project": expect.objectContaining({ - key: "jira.project", - value: "PLAT", - scope: "conversation", - }), - }, - }, - }); + expect(entries.has("github.repo")).toBe(false); }); it("rejects invalid keys and secret-like values", async () => { const { service } = createInMemoryService(); - await expect( - service.set({ - key: "token.value", - value: "abc", - }), + service.set({ key: "token.value", value: "abc" }), ).rejects.toThrow("secret-related"); - await expect( service.set({ key: "github.repo", diff --git a/packages/junior/tests/unit/handlers/jr-rpc-command.test.ts b/packages/junior/tests/unit/handlers/jr-rpc-command.test.ts index c819b57c4b..a3a341d100 100644 --- a/packages/junior/tests/unit/handlers/jr-rpc-command.test.ts +++ b/packages/junior/tests/unit/handlers/jr-rpc-command.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { maybeExecuteJrRpcCustomCommand } from "@/chat/capabilities/jr-rpc-command"; -import { createChannelConfigurationService } from "@/chat/configuration/service"; +import { createLocationConfigurationService } from "@/chat/configuration/service"; +import type { ConfigEntry } from "@/chat/configuration/types"; import { pluginCatalogRuntime } from "@/chat/plugins/catalog-runtime"; import type { Skill } from "@/chat/skills"; @@ -12,16 +13,15 @@ const activeSkill: Skill = { pluginProvider: "github", }; -function makeChannelConfiguration() { - let state: Record | null = null; - return createChannelConfigurationService({ - load: async () => state, - save: async (next) => { - state = { - ...(state ?? {}), - configuration: next, - }; +function makeLocationConfiguration() { + const entries = new Map(); + return createLocationConfigurationService({ + get: async (key) => entries.get(key), + list: async () => [...entries.values()], + set: async (entry) => { + entries.set(entry.key, entry); }, + unset: async (key) => entries.delete(key), }); } @@ -64,14 +64,14 @@ describe("jr-rpc custom command", () => { }); it("sets and gets configuration values", async () => { - const configuration = makeChannelConfiguration(); + const configuration = makeLocationConfiguration(); const onConfigurationValueChanged = vi.fn(); const setResult = await maybeExecuteJrRpcCustomCommand( "jr-rpc config set github.repo getsentry/junior", { activeSkill, - channelConfiguration: configuration, + locationConfiguration: configuration, actorId: "U123", onConfigurationValueChanged, }, @@ -86,7 +86,7 @@ describe("jr-rpc custom command", () => { "jr-rpc config get github.repo", { activeSkill, - channelConfiguration: configuration, + locationConfiguration: configuration, actorId: "U123", }, ); @@ -100,7 +100,7 @@ describe("jr-rpc custom command", () => { }); it("supports config list with a prefix filter", async () => { - const configuration = makeChannelConfiguration(); + const configuration = makeLocationConfiguration(); await configuration.set({ key: "github.repo", value: "getsentry/junior", @@ -118,7 +118,7 @@ describe("jr-rpc custom command", () => { "jr-rpc config list --prefix github.", { activeSkill, - channelConfiguration: configuration, + locationConfiguration: configuration, actorId: "U123", }, ); @@ -175,7 +175,7 @@ describe("jr-rpc custom command", () => { }); it("unsets configuration values", async () => { - const configuration = makeChannelConfiguration(); + const configuration = makeLocationConfiguration(); const onConfigurationValueChanged = vi.fn(); await configuration.set({ @@ -189,7 +189,7 @@ describe("jr-rpc custom command", () => { "jr-rpc config unset github.repo", { activeSkill, - channelConfiguration: configuration, + locationConfiguration: configuration, actorId: "U123", onConfigurationValueChanged, }, @@ -225,12 +225,12 @@ describe("jr-rpc custom command", () => { ], }); try { - const configuration = makeChannelConfiguration(); + const configuration = makeLocationConfiguration(); const result = await maybeExecuteJrRpcCustomCommand( "jr-rpc config set cloudflare.worker-id sentry-mcp", { activeSkill, - channelConfiguration: configuration, + locationConfiguration: configuration, actorId: "U123", }, ); @@ -265,12 +265,12 @@ describe("jr-rpc custom command", () => { ], }); try { - const configuration = makeChannelConfiguration(); + const configuration = makeLocationConfiguration(); const result = await maybeExecuteJrRpcCustomCommand( "jr-rpc config set cloudflare.worker.name sentry-mcp", { activeSkill, - channelConfiguration: configuration, + locationConfiguration: configuration, actorId: "U123", }, );