diff --git a/packages/junior/migrations/0025_nifty_stepford_cuckoos.sql b/packages/junior/migrations/0025_nifty_stepford_cuckoos.sql new file mode 100644 index 0000000000..3d4a36e927 --- /dev/null +++ b/packages/junior/migrations/0025_nifty_stepford_cuckoos.sql @@ -0,0 +1,3 @@ +ALTER TABLE "junior_conversations" ADD COLUMN "forked_from_conversation_id" text;--> statement-breakpoint +ALTER TABLE "junior_conversations" ADD CONSTRAINT "junior_conversations_forked_from_conversation_id_junior_conversations_conversation_id_fk" FOREIGN KEY ("forked_from_conversation_id") REFERENCES "public"."junior_conversations"("conversation_id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "junior_conversations_forked_from_idx" ON "junior_conversations" USING btree ("forked_from_conversation_id"); \ No newline at end of file diff --git a/packages/junior/migrations/meta/0025_snapshot.json b/packages/junior/migrations/meta/0025_snapshot.json new file mode 100644 index 0000000000..6d8dbc85cb --- /dev/null +++ b/packages/junior/migrations/meta/0025_snapshot.json @@ -0,0 +1,2309 @@ +{ + "id": "97cf5e18-0c38-4f6d-a2c8-cf46074a11de", + "prevId": "5b615b01-1e96-4396-a09d-82c0a70f4a58", + "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 + }, + "forked_from_conversation_id": { + "name": "forked_from_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_forked_from_idx": { + "name": "junior_conversations_forked_from_idx", + "columns": [ + { + "expression": "forked_from_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_forked_from_conversation_id_junior_conversations_conversation_id_fk": { + "name": "junior_conversations_forked_from_conversation_id_junior_conversations_conversation_id_fk", + "tableFrom": "junior_conversations", + "tableTo": "junior_conversations", + "columnsFrom": [ + "forked_from_conversation_id" + ], + "columnsTo": [ + "conversation_id" + ], + "onDelete": "set null", + "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 2fa668a218..9134d10013 100644 --- a/packages/junior/migrations/meta/_journal.json +++ b/packages/junior/migrations/meta/_journal.json @@ -176,6 +176,13 @@ "when": 1786313061094, "tag": "0024_misty_typhoid_mary", "breakpoints": true + }, + { + "idx": 25, + "version": "7", + "when": 1786469764922, + "tag": "0025_nifty_stepford_cuckoos", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/junior/src/chat/conversations/README.md b/packages/junior/src/chat/conversations/README.md index a658caee3c..3b529f964d 100644 --- a/packages/junior/src/chat/conversations/README.md +++ b/packages/junior/src/chat/conversations/README.md @@ -59,6 +59,18 @@ events append to it. The internal `history_version` column makes loading that active history efficient. There is no initial-history event. Database migrations normalize older history shapes before the runtime reads them. +## Conversation Fork + +`fork.ts` owns the internal fork path. A fork creates a **new root** conversation +(not a subagent child via `parent_conversation_id`) and seeds it with the source +conversation's active agent history through a cutoff seq or platform message id. +The fork row stores `forked_from_conversation_id` as an indexed self-reference, +so callers can resolve fork → source and source → forks without scanning events. +It also records a `junior/conversation_forked` structured event with the cutoff +for transcript display. The fork inherits source destination visibility and +never widens private or unknown sources to public. It does not clone execution +state, mailbox, schedules, watches, approvals, or live tool side effects. + Volatile `` bootstrap is kept only in an unfinished turn's session record. It is removed before SQL history is written and restored for an auth or timeout resume, so agent replay does not need an automatic rollback. diff --git a/packages/junior/src/chat/conversations/fork.ts b/packages/junior/src/chat/conversations/fork.ts new file mode 100644 index 0000000000..4b4b993b11 --- /dev/null +++ b/packages/junior/src/chat/conversations/fork.ts @@ -0,0 +1,320 @@ +/** + * Internal conversation fork. + * + * Creates a new root conversation and seeds it with the source conversation's + * active agent history through a cutoff. Does not clone execution state, + * mailbox, schedules, watches, approvals, or live tool side effects. + * + * Fork roots inherit source destination visibility and never widen it. + * They are not subagent children (`parentConversationId` stays for delegation). + */ +import { createHash } from "node:crypto"; +import { + createWebSource, + localDestinationSchema, +} from "@sentry/junior-plugin-api"; +import type { ConversationPrivacy } from "@/chat/conversation-privacy"; +import type { ConversationEvent } from "@/chat/conversations/history"; +import { + commitMessages, + loadTurnProjection, +} from "@/chat/conversations/projection"; +import type { Conversation } from "@/chat/conversations/store"; +import { + conversationForkedEvent, + JUNIOR_NATIVE_EVENT_NAMESPACE, +} from "@/chat/conversations/structured-events"; +import { + getConversationEventStore, + getConversationStore, +} from "@/chat/db"; + +export type ForkConversationCutoff = + | { kind: "seq"; throughSeq: number } + | { kind: "message"; messageId: string }; + +export interface ForkConversationInput { + sourceConversationId: string; + cutoff: ForkConversationCutoff; + /** Client-supplied key; retries with the same key return the same fork. */ + idempotencyKey: string; +} + +export interface ForkConversationResult { + conversationId: string; + sourceConversationId: string; + throughSeq: number; + sourceMessageId?: string; + status: "created" | "duplicate"; +} + +function stableHex(...parts: string[]): string { + return createHash("sha256") + .update(parts.join("\u0000")) + .digest("hex") + .slice(0, 24); +} + +/** Deterministic fork conversation id for one source + idempotency key. */ +export function createForkConversationId(args: { + sourceConversationId: string; + idempotencyKey: string; +}): string { + return `local:fork:${stableHex(args.sourceConversationId, args.idempotencyKey)}`; +} + +function forkIdempotencyKey(args: { + sourceConversationId: string; + idempotencyKey: string; +}): string { + return `fork:${stableHex(args.sourceConversationId, args.idempotencyKey)}`; +} + +function requireLocalDestination(conversationId: string) { + const parsed = localDestinationSchema.safeParse({ + platform: "local", + conversationId, + }); + if (!parsed.success) { + throw new Error(`Invalid local conversation id: ${conversationId}`); + } + return parsed.data; +} + +/** + * Inherit source visibility. Missing or unknown visibility stays private so a + * fork never widens access. + */ +function forkVisibility(source: Conversation): ConversationPrivacy { + return source.visibility === "public" ? "public" : "private"; +} + +type ForkCutoffResolution = { + throughSeq: number; + sourceMessageId?: string; +}; + +/** Resolve a cutoff to the agent-history seq included in the fork. */ +async function resolveForkCutoff(args: { + sourceConversationId: string; + cutoff: ForkConversationCutoff; +}): Promise { + const eventStore = getConversationEventStore(); + if (args.cutoff.kind === "seq") { + if (args.cutoff.throughSeq < 0) { + throw new Error("Fork cutoff seq must be non-negative"); + } + const history = await eventStore.loadHistoryContaining( + args.sourceConversationId, + args.cutoff.throughSeq, + args.cutoff.throughSeq, + ); + if (!history || history.length === 0) { + throw new Error( + `Fork cutoff seq ${args.cutoff.throughSeq} was not found in ${args.sourceConversationId}`, + ); + } + return { throughSeq: args.cutoff.throughSeq }; + } + + const messageId = args.cutoff.messageId.trim(); + if (!messageId) { + throw new Error("Fork cutoff message id must not be empty"); + } + + // Platform messages are not agent-history items. Cut agent history at the + // latest history-bearing seq at or before the selected message. + const events = await eventStore.loadHistory(args.sourceConversationId); + let messageSeq: number | undefined; + for (const event of events) { + if ( + (event.data.type === "message" || + event.data.type === "message_updated") && + event.data.messageId === messageId + ) { + messageSeq = event.seq; + break; + } + } + if (messageSeq === undefined) { + throw new Error( + `Fork cutoff message ${messageId} was not found in ${args.sourceConversationId}`, + ); + } + + const throughSeq = latestAgentHistorySeqAtOrBefore(events, messageSeq); + if (throughSeq === undefined) { + throw new Error( + `Fork cutoff message ${messageId} has no agent history at or before it in ${args.sourceConversationId}`, + ); + } + return { throughSeq, sourceMessageId: messageId }; +} + +function latestAgentHistorySeqAtOrBefore( + events: ConversationEvent[], + messageSeq: number, +): number | undefined { + let throughSeq: number | undefined; + for (const event of events) { + if (event.seq > messageSeq) break; + if ( + event.data.type === "user_message" || + event.data.type === "assistant_message" || + event.data.type === "tool_result" || + event.data.type === "compaction" || + event.data.type === "handoff" || + event.data.type === "authorization_completed" + ) { + throughSeq = event.seq; + } + } + return throughSeq; +} + +type ForkEventContent = { + sourceConversationId: string; + throughSeq: number; + sourceMessageId?: string; +}; + +function parseForkEventContent(content: unknown): ForkEventContent { + return conversationForkedEvent.parse(content) as ForkEventContent; +} + +/** + * Fork a conversation into a new root at an agent-history cutoff. + * + * The fork is a new independent root (not a subagent child). Agent history + * through the cutoff is copied; runtime and task state are not. + */ +export async function forkConversation( + input: ForkConversationInput, +): Promise { + const sourceConversationId = input.sourceConversationId.trim(); + const idempotencyKey = input.idempotencyKey.trim(); + if (!sourceConversationId) { + throw new Error("Fork source conversation id must not be empty"); + } + if (!idempotencyKey) { + throw new Error("Fork idempotency key must not be empty"); + } + + const conversationStore = getConversationStore(); + const eventStore = getConversationEventStore(); + const nowMs = Date.now(); + + const source = await conversationStore.get({ + conversationId: sourceConversationId, + }); + if (!source) { + throw new Error(`Fork source conversation ${sourceConversationId} not found`); + } + if (source.lineage) { + throw new Error("Forking child conversations is not supported"); + } + if (source.transcriptPurgedAtMs !== undefined) { + throw new Error( + `Fork source conversation ${sourceConversationId} has a purged transcript`, + ); + } + + const conversationId = createForkConversationId({ + sourceConversationId, + idempotencyKey, + }); + const cutoff = await resolveForkCutoff({ + sourceConversationId, + cutoff: input.cutoff, + }); + + const prior = await eventStore.loadLatestStructuredEvent( + conversationId, + JUNIOR_NATIVE_EVENT_NAMESPACE, + conversationForkedEvent.eventName, + ); + if (prior?.data.type === "structured_event") { + const content = parseForkEventContent(prior.data.content); + return { + conversationId, + sourceConversationId, + throughSeq: content.throughSeq, + ...(content.sourceMessageId + ? { sourceMessageId: content.sourceMessageId } + : {}), + status: "duplicate", + }; + } + + const projection = await loadTurnProjection({ + conversationId: sourceConversationId, + committedSeq: cutoff.throughSeq, + includeTail: false, + }); + if (!projection) { + throw new Error( + `Fork cutoff seq ${cutoff.throughSeq} is not loadable in ${sourceConversationId}`, + ); + } + + const visibility = forkVisibility(source); + const destination = requireLocalDestination(conversationId); + await conversationStore.recordActivity({ + conversationId, + destination, + forkedFromConversationId: sourceConversationId, + nowMs, + source: "internal", + sessionSource: createWebSource(conversationId, visibility), + visibility, + ...(source.title ? { title: source.title } : {}), + }); + + // Seed agent history before the fork marker. On retry after a partial write, + // skip reseeding when history is already present. The fork marker is the + // durable completion fact. + if (projection.messages.length > 0) { + const existingHistory = await eventStore.loadCurrentHistory(conversationId); + if (existingHistory.length === 0) { + await commitMessages({ + conversationId, + messages: projection.messages, + provenance: projection.provenance, + }); + } + } + + const forkContent = parseForkEventContent({ + sourceConversationId, + throughSeq: cutoff.throughSeq, + ...(cutoff.sourceMessageId + ? { sourceMessageId: cutoff.sourceMessageId } + : {}), + }); + await eventStore.append(conversationId, [ + { + createdAtMs: nowMs, + idempotencyKey: forkIdempotencyKey({ + sourceConversationId, + idempotencyKey, + }), + data: { + type: "structured_event", + namespace: JUNIOR_NATIVE_EVENT_NAMESPACE, + name: conversationForkedEvent.eventName, + version: conversationForkedEvent.version, + content: forkContent, + }, + }, + ]); + + return { + conversationId, + sourceConversationId, + throughSeq: cutoff.throughSeq, + ...(cutoff.sourceMessageId + ? { sourceMessageId: cutoff.sourceMessageId } + : {}), + status: "created", + }; +} diff --git a/packages/junior/src/chat/conversations/sql/store.ts b/packages/junior/src/chat/conversations/sql/store.ts index 8bdeb49b27..30f73c8c3e 100644 --- a/packages/junior/src/chat/conversations/sql/store.ts +++ b/packages/junior/src/chat/conversations/sql/store.ts @@ -282,6 +282,9 @@ function conversationFromRow(readRow: ConversationReadRow): Conversation { }, } : {}), + ...(row.forkedFromConversationId + ? { forkedFromConversationId: row.forkedFromConversationId } + : {}), ...(destination ? { destination } : {}), ...(location ? { location } : {}), ...(actor ? { actor } : {}), @@ -477,6 +480,7 @@ export class SqlStore implements ConversationStore { channelName?: string; conversationId: string; destination?: Destination; + forkedFromConversationId?: string; nowMs?: number; actor?: StoredSlackActor; source?: ConversationSource; @@ -498,6 +502,15 @@ export class SqlStore implements ConversationStore { next: args.destination, }); } + if ( + existing?.forkedFromConversationId && + args.forkedFromConversationId && + existing.forkedFromConversationId !== args.forkedFromConversationId + ) { + throw new Error( + `Conversation fork source changed for ${args.conversationId}`, + ); + } const current = existing ?? emptyConversation({ @@ -519,6 +532,8 @@ export class SqlStore implements ConversationStore { ...currentWithoutPersistedSignals, destination: current.destination ?? args.destination, source: current.source ?? args.source, + forkedFromConversationId: + current.forkedFromConversationId ?? args.forkedFromConversationId, ...(sessionSource ? { sessionSource } : {}), channelName: current.channelName ?? args.channelName, actor: mergeActor(current.actor, args.actor), @@ -796,6 +811,8 @@ export class SqlStore implements ConversationStore { : dateFromMs(conversation.execution.lastEnqueuedAtMs), parentConversationId: conversation.lineage?.parentConversationId ?? null, + forkedFromConversationId: + conversation.forkedFromConversationId ?? null, rootConversationId, }) .onConflictDoUpdate({ @@ -821,6 +838,7 @@ export class SqlStore implements ConversationStore { runId: sql`case when ${incomingExecutionIsFresh} then excluded.run_id else ${juniorConversations.runId} end`, lastCheckpointAt: sql`case when ${incomingExecutionIsFresh} then coalesce(excluded.last_checkpoint_at, ${juniorConversations.lastCheckpointAt}) else ${juniorConversations.lastCheckpointAt} end`, lastEnqueuedAt: sql`case when ${incomingExecutionIsFresh} then coalesce(excluded.last_enqueued_at, ${juniorConversations.lastEnqueuedAt}) else ${juniorConversations.lastEnqueuedAt} end`, + forkedFromConversationId: sql`coalesce(${juniorConversations.forkedFromConversationId}, excluded.forked_from_conversation_id)`, rootConversationId: sql`coalesce(${juniorConversations.rootConversationId}, excluded.root_conversation_id)`, }, }) diff --git a/packages/junior/src/chat/conversations/store.ts b/packages/junior/src/chat/conversations/store.ts index d2aae089c0..f846a0e410 100644 --- a/packages/junior/src/chat/conversations/store.ts +++ b/packages/junior/src/chat/conversations/store.ts @@ -49,6 +49,8 @@ export interface Conversation { }; lastActivityAtMs: number; lineage?: ConversationLineage; + /** Source conversation for an independent fork root. */ + forkedFromConversationId?: string; location?: Location; actor?: StoredSlackActor; schemaVersion: 1; @@ -107,6 +109,8 @@ export interface ConversationStore { channelName?: string; conversationId: string; destination?: Destination; + /** Set only when creating an independent fork root. */ + forkedFromConversationId?: string; nowMs?: number; actor?: StoredSlackActor; source?: ConversationSource; diff --git a/packages/junior/src/chat/conversations/structured-events.ts b/packages/junior/src/chat/conversations/structured-events.ts index 6b329e98d1..9110295ddc 100644 --- a/packages/junior/src/chat/conversations/structured-events.ts +++ b/packages/junior/src/chat/conversations/structured-events.ts @@ -41,6 +41,14 @@ const agentsInstructionsUpdatedContentSchema = z }) .strict(); +const conversationForkedContentSchema = z + .object({ + sourceConversationId: z.string().min(1), + throughSeq: z.number().int().nonnegative(), + sourceMessageId: z.string().min(1).optional(), + }) + .strict(); + function providerTitle(content: { provider: string; providerLabel?: string; @@ -152,10 +160,34 @@ export const agentsInstructionsUpdatedEvent = defineConversationEvent({ }, }); +/** Junior-owned backlink for a conversation forked from an earlier cutoff. */ +export const conversationForkedEvent = defineConversationEvent({ + name: "conversation_forked", + version: 1, + schema: conversationForkedContentSchema, + renderEvent(event) { + return { + icon: "activity", + title: "Forked conversation", + preview: `From ${event.sourceConversationId} through seq ${event.throughSeq}`, + details: [ + { + title: "Forked conversation", + description: event.sourceMessageId + ? `Source message \`${event.sourceMessageId}\` in \`${event.sourceConversationId}\`` + : `Source conversation \`${event.sourceConversationId}\``, + metadata: [`seq ${event.throughSeq}`], + }, + ], + }; + }, +}); + const NATIVE_EVENT_DEFINITIONS: readonly PluginConversationEventDefinition[] = [ authenticationLinkedEvent, authenticationUnlinkedEvent, agentsInstructionsUpdatedEvent, + conversationForkedEvent, ]; /** Resolve a registered Junior-native conversation event definition. */ diff --git a/packages/junior/src/db/schema/conversations.ts b/packages/junior/src/db/schema/conversations.ts index 8f3fb685a6..231c66392f 100644 --- a/packages/junior/src/db/schema/conversations.ts +++ b/packages/junior/src/db/schema/conversations.ts @@ -61,6 +61,12 @@ export const juniorConversations = pgTable( parentConversationId: text("parent_conversation_id").references( (): AnyPgColumn => juniorConversations.conversationId, ), + // Forks are independent roots. This edge supports source/fork lookup and + // does not affect root ownership or child conversation traversal. + forkedFromConversationId: text("forked_from_conversation_id").references( + (): AnyPgColumn => juniorConversations.conversationId, + { onDelete: "set null" }, + ), // Roots reference themselves; descendants reference the root whose actor // and destination own their privacy boundary. rootConversationId: text("root_conversation_id").references( @@ -100,6 +106,9 @@ export const juniorConversations = pgTable( table.lastActivityAt.desc(), ), index("junior_conversations_parent_idx").on(table.parentConversationId), + index("junior_conversations_forked_from_idx").on( + table.forkedFromConversationId, + ), index("junior_conversations_root_idx").on(table.rootConversationId), ], ); diff --git a/packages/junior/tests/component/conversations/fork.test.ts b/packages/junior/tests/component/conversations/fork.test.ts new file mode 100644 index 0000000000..9c1a28e722 --- /dev/null +++ b/packages/junior/tests/component/conversations/fork.test.ts @@ -0,0 +1,215 @@ +import { eq } from "drizzle-orm"; +import { describe, expect, it } from "vitest"; +import { + createForkConversationId, + forkConversation, +} from "@/chat/conversations/fork"; +import { + commitMessages, + openConversationProjection, +} from "@/chat/conversations/projection"; +import { + contextProvenance, + instructionProvenanceFor, +} from "@/chat/conversations/provenance"; +import { + getConversationEventStore, + getConversationStore, + getSqlExecutor, +} from "@/chat/db"; +import type { PiMessage } from "@/chat/pi/messages"; +import { juniorConversations } from "@/db/schema"; + +const SOURCE_ID = "local:web:fork-source-1"; +const instructionProvenance = instructionProvenanceFor(undefined); + +function userMessage(text: string, timestamp: number): PiMessage { + return { + role: "user", + content: [{ type: "text", text }], + timestamp, + }; +} + +function assistantMessage(text: string, timestamp: number): PiMessage { + return { + role: "assistant", + content: [{ type: "text", text }], + api: "openai-responses", + provider: "openai", + model: "test-model", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + total: 0, + }, + }, + stopReason: "stop", + timestamp, + } as PiMessage; +} + +describe("conversation fork", () => { + it("creates a private root with agent history through the cutoff", async () => { + const store = getConversationStore(); + const events = getConversationEventStore(); + + await store.recordActivity({ + conversationId: SOURCE_ID, + nowMs: 1_000, + source: "web", + title: "Source thread", + destination: { + platform: "local", + conversationId: SOURCE_ID, + }, + // Private source must stay private on the fork root. + visibility: "private", + }); + + const first = userMessage("start here", 1_000); + const second = assistantMessage("first answer", 1_100); + const third = userMessage("later branch point", 1_200); + const fourth = assistantMessage("should not copy", 1_300); + + // Interleave the platform message between history items so a UI + // fork-from-message cuts after the first assistant reply only. + await commitMessages({ + conversationId: SOURCE_ID, + messages: [first, second], + provenance: [instructionProvenance, contextProvenance], + }); + await events.append(SOURCE_ID, [ + { + createdAtMs: 1_150, + idempotencyKey: "message:msg-cutoff", + data: { + type: "message", + messageId: "msg-cutoff", + role: "assistant", + text: "first answer", + }, + }, + ]); + await commitMessages({ + conversationId: SOURCE_ID, + messages: [first, second, third, fourth], + provenance: [ + instructionProvenance, + contextProvenance, + instructionProvenance, + contextProvenance, + ], + }); + + const history = await events.loadCurrentHistory(SOURCE_ID); + const secondSeq = history.find( + (event) => event.data.type === "assistant_message", + )?.seq; + expect(secondSeq).toBeTypeOf("number"); + + const forked = await forkConversation({ + sourceConversationId: SOURCE_ID, + cutoff: { kind: "message", messageId: "msg-cutoff" }, + idempotencyKey: "fork-key-1", + }); + + expect(forked.status).toBe("created"); + expect(forked.conversationId).toBe( + createForkConversationId({ + sourceConversationId: SOURCE_ID, + idempotencyKey: "fork-key-1", + }), + ); + expect(forked.throughSeq).toBe(secondSeq); + expect(forked.sourceMessageId).toBe("msg-cutoff"); + + const forkRow = await store.get({ conversationId: forked.conversationId }); + expect(forkRow).toMatchObject({ + conversationId: forked.conversationId, + source: "internal", + forkedFromConversationId: SOURCE_ID, + title: "Source thread", + visibility: "private", + }); + expect(forkRow?.lineage).toBeUndefined(); + + const forks = await getSqlExecutor() + .db() + .select({ conversationId: juniorConversations.conversationId }) + .from(juniorConversations) + .where(eq(juniorConversations.forkedFromConversationId, SOURCE_ID)); + expect(forks).toEqual([{ conversationId: forked.conversationId }]); + + const projection = await openConversationProjection({ + conversationId: forked.conversationId, + }); + expect(projection.messages).toHaveLength(2); + expect(projection.messages[0]).toMatchObject({ + role: "user", + content: [{ type: "text", text: "start here" }], + }); + expect(projection.messages[1]).toMatchObject({ + role: "assistant", + content: [{ type: "text", text: "first answer" }], + }); + + const forkEvents = await events.loadHistory(forked.conversationId); + expect( + forkEvents.some( + (event) => + event.data.type === "structured_event" && + event.data.name === "conversation_forked", + ), + ).toBe(true); + + const retry = await forkConversation({ + sourceConversationId: SOURCE_ID, + cutoff: { kind: "message", messageId: "msg-cutoff" }, + idempotencyKey: "fork-key-1", + }); + expect(retry).toEqual({ + ...forked, + status: "duplicate", + }); + }); + + it("rejects child conversation forks", async () => { + const store = getConversationStore(); + const parentId = "local:web:fork-parent-1"; + const childId = "local:web:fork-child-1"; + + await store.recordActivity({ + conversationId: parentId, + nowMs: 1_000, + source: "web", + destination: { + platform: "local", + conversationId: parentId, + }, + visibility: "public", + }); + await store.createChild({ + parentConversationId: parentId, + childConversationId: childId, + nowMs: 1_100, + source: "internal", + }); + + await expect( + forkConversation({ + sourceConversationId: childId, + cutoff: { kind: "seq", throughSeq: 0 }, + idempotencyKey: "child-fork", + }), + ).rejects.toThrow("Forking child conversations is not supported"); + }); +}); diff --git a/packages/junior/tests/component/scheduled-tasks-sql.test.ts b/packages/junior/tests/component/scheduled-tasks-sql.test.ts index cc00cfd011..6729992cab 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: 9, + migrated: 10, }); const [migrated] = await fixture.sql.query<{ creatorIdentityId: string | null; diff --git a/packages/junior/tests/unit/conversations/native-events.test.ts b/packages/junior/tests/unit/conversations/native-events.test.ts index d59e41bd3a..d4d70c5ddc 100644 --- a/packages/junior/tests/unit/conversations/native-events.test.ts +++ b/packages/junior/tests/unit/conversations/native-events.test.ts @@ -3,6 +3,7 @@ import { agentsInstructionsUpdatedEvent, authenticationLinkedEvent, authenticationUnlinkedEvent, + conversationForkedEvent, renderJuniorNativeConversationEvent, } from "@/chat/conversations/structured-events"; import { @@ -166,4 +167,49 @@ describe("junior native authentication events", () => { }).data, ).toEqual(data); }); + + it("renders and accepts conversation fork backlinks", () => { + expect( + conversationForkedEvent.renderEvent({ + sourceConversationId: "local:web:source-1", + throughSeq: 2, + sourceMessageId: "msg-1", + }), + ).toEqual({ + icon: "activity", + title: "Forked conversation", + preview: "From local:web:source-1 through seq 2", + details: [ + { + title: "Forked conversation", + description: + "Source message `msg-1` in `local:web:source-1`", + metadata: ["seq 2"], + }, + ], + }); + + const data: ConversationEventData = { + type: "structured_event", + namespace: "junior", + name: "conversation_forked", + version: 1, + content: { + sourceConversationId: "local:web:source-1", + throughSeq: 2, + sourceMessageId: "msg-1", + }, + }; + + expect( + conversationEventSchema.parse({ + schemaVersion: 1, + seq: 3, + historyVersion: 0, + idempotencyKey: "native-fork-1", + createdAtMs: 3_000, + data, + }).data, + ).toEqual(data); + }); });