collavre 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +0 -1
- data/app/assets/stylesheets/collavre/comments_popup.css +165 -99
- data/app/assets/stylesheets/collavre/creatives.css +346 -37
- data/app/assets/stylesheets/collavre/desktop_setup.css +246 -0
- data/app/assets/stylesheets/collavre/gnb.css +64 -3
- data/app/assets/stylesheets/collavre/landing.css +108 -3
- data/app/assets/stylesheets/collavre/mention_menu.css +32 -0
- data/app/assets/stylesheets/collavre/popup.css +14 -0
- data/app/assets/stylesheets/collavre/print.css +0 -1
- data/app/assets/stylesheets/collavre/search_popup.css +1 -1
- data/app/assets/stylesheets/collavre/secret_fields.css +3 -0
- data/app/assets/stylesheets/collavre/workspace.css +307 -0
- data/app/channels/collavre/agent_channel.rb +14 -21
- data/app/channels/collavre/comments_presence_channel.rb +181 -21
- data/app/channels/collavre/inbox_badge_channel.rb +7 -1
- data/app/components/collavre/inbox/badge_component.rb +6 -13
- data/app/components/collavre/progress_filter_component.html.erb +2 -2
- data/app/components/collavre/progress_filter_component.rb +20 -0
- data/app/controllers/collavre/admin/settings_controller.rb +4 -1
- data/app/controllers/collavre/agent_connections_controller.rb +165 -0
- data/app/controllers/collavre/agent_gateways_controller.rb +132 -0
- data/app/controllers/collavre/agent_provisioning_controller.rb +79 -0
- data/app/controllers/collavre/api/v1/agents_controller.rb +45 -97
- data/app/controllers/collavre/application_controller.rb +13 -2
- data/app/controllers/collavre/comment_read_pointers_controller.rb +69 -40
- data/app/controllers/collavre/comments_controller.rb +45 -131
- data/app/controllers/collavre/concerns/shareable.rb +9 -3
- data/app/controllers/collavre/creatives_controller.rb +144 -11
- data/app/controllers/collavre/desktop_gateway_registrations_controller.rb +173 -0
- data/app/controllers/collavre/desktop_setup_controller.rb +119 -0
- data/app/controllers/collavre/desktop_sidecar_health_controller.rb +35 -0
- data/app/controllers/collavre/features_controller.rb +57 -0
- data/app/controllers/collavre/invites_controller.rb +12 -1
- data/app/controllers/collavre/llm_models_controller.rb +8 -0
- data/app/controllers/collavre/notices_controller.rb +20 -0
- data/app/controllers/collavre/tasks_controller.rb +40 -6
- data/app/controllers/collavre/topics_controller.rb +130 -53
- data/app/controllers/collavre/user_creative_preferences_controller.rb +214 -30
- data/app/controllers/concerns/collavre/comments/batch_operations.rb +15 -3
- data/app/controllers/concerns/collavre/comments/conversion.rb +1 -1
- data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +118 -8
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +1 -15
- data/app/controllers/concerns/collavre/users_controller/registration.rb +2 -2
- data/app/errors/collavre/turn_deadline_error.rb +20 -0
- data/app/helpers/collavre/application_helper.rb +38 -0
- data/app/helpers/collavre/creatives_helper.rb +56 -22
- data/app/helpers/collavre/navigation_helper.rb +12 -0
- data/app/javascript/collavre.js +1 -2
- data/app/javascript/components/InlineLexicalEditor.jsx +30 -21
- data/app/javascript/components/__tests__/creative_tree_row_parent_id.test.js +32 -2
- data/app/javascript/components/__tests__/creative_tree_row_progress_toggle.test.js +185 -0
- data/app/javascript/components/creative_tree_row.js +62 -22
- data/app/javascript/controllers/__tests__/agent_connection_controller.test.js +195 -0
- data/app/javascript/controllers/__tests__/desktop_proxy_setup_controller.test.js +96 -0
- data/app/javascript/controllers/__tests__/index.test.js +42 -0
- data/app/javascript/controllers/__tests__/last_visited_creative_controller.test.js +103 -0
- data/app/javascript/controllers/__tests__/link_creative_controller.test.js +253 -2
- data/app/javascript/controllers/__tests__/progress_filter_controller.test.js +134 -0
- data/app/javascript/controllers/__tests__/search_popup_controller.test.js +310 -0
- data/app/javascript/controllers/__tests__/topic_list_controller.test.js +111 -0
- data/app/javascript/controllers/__tests__/workspace_tree_controller.test.js +1011 -0
- data/app/javascript/controllers/agent_connection_controller.js +357 -0
- data/app/javascript/controllers/agent_vendor_controller.js +23 -0
- data/app/javascript/controllers/comment_controller.js +3 -1
- data/app/javascript/controllers/comments/__tests__/feature_cards_controller.test.js +223 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_default_placeholder.test.js +83 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_draft.test.js +2069 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_ime_send.test.js +324 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +28 -2
- data/app/javascript/controllers/comments/__tests__/form_controller_search_submit.test.js +106 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft.test.js +134 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_creative_switch.test.js +166 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_failure.test.js +183 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_deep_link_topic.test.js +467 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_docked.test.js +115 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_prev_message.test.js +245 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_read_pointer.test.js +276 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_stream_archived.test.js +221 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_stream_search.test.js +104 -0
- data/app/javascript/controllers/comments/__tests__/placeholder_controller.test.js +79 -0
- data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +754 -1
- data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +717 -15
- data/app/javascript/controllers/comments/__tests__/prev_message_navigator.test.js +129 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_action_errors.test.js +184 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_archived.test.js +879 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_creation_container.test.js +154 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_creation_scroll.test.js +92 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +130 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_docked.test.js +32 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_last_topic_echo.test.js +1938 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_primary_agent.test.js +256 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_scroll.test.js +182 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_selection_race.test.js +981 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_topic_list.test.js +313 -0
- data/app/javascript/controllers/comments/feature_cards_controller.js +83 -0
- data/app/javascript/controllers/comments/form_controller.js +795 -14
- data/app/javascript/controllers/comments/list_controller.js +338 -52
- data/app/javascript/controllers/comments/placeholder_controller.js +30 -0
- data/app/javascript/controllers/comments/popup_controller.js +425 -83
- data/app/javascript/controllers/comments/presence_controller.js +317 -55
- data/app/javascript/controllers/comments/prev_message_navigator.js +94 -0
- data/app/javascript/controllers/comments/topics_controller.js +1772 -100
- data/app/javascript/controllers/common_popup_controller.js +2 -2
- data/app/javascript/controllers/creatives/__tests__/select_mode_controller.test.js +139 -0
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +605 -0
- data/app/javascript/controllers/creatives/__tests__/write_access_request_controller.test.js +125 -0
- data/app/javascript/controllers/creatives/select_mode_controller.js +7 -2
- data/app/javascript/controllers/creatives/tree_controller.js +140 -31
- data/app/javascript/controllers/creatives/write_access_request_controller.js +58 -0
- data/app/javascript/controllers/desktop_proxy_setup_controller.js +51 -0
- data/app/javascript/controllers/gateway_check_controller.js +36 -0
- data/app/javascript/controllers/index.js +25 -1
- data/app/javascript/controllers/last_visited_creative_controller.js +58 -0
- data/app/javascript/controllers/link_creative_controller.js +89 -18
- data/app/javascript/controllers/progress_filter_controller.js +12 -22
- data/app/javascript/controllers/search_popup_controller.js +64 -46
- data/app/javascript/controllers/share_user_search_controller.js +2 -2
- data/app/javascript/controllers/topic_list_controller.js +44 -7
- data/app/javascript/controllers/workspace_tree_controller.js +662 -0
- data/app/javascript/creatives/__tests__/tree_renderer.test.js +80 -0
- data/app/javascript/creatives/drag_drop/__tests__/drop_completion_empty_state.test.js +98 -0
- data/app/javascript/creatives/drag_drop/__tests__/topic_move_released_agent.test.js +125 -0
- data/app/javascript/creatives/drag_drop/event_handlers.js +12 -0
- data/app/javascript/creatives/tree_renderer.js +65 -22
- data/app/javascript/lib/__tests__/chat_drafts.test.js +1458 -0
- data/app/javascript/lib/__tests__/common_popup_bounds.test.js +8 -3
- data/app/javascript/lib/__tests__/common_popup_placement.test.js +374 -0
- data/app/javascript/lib/__tests__/common_popup_reshow.test.js +131 -0
- data/app/javascript/lib/__tests__/creative_link_prefetch.test.js +46 -0
- data/app/javascript/lib/__tests__/last_visited_creative.test.js +87 -0
- data/app/javascript/lib/__tests__/skill_html_entities.test.js +23 -0
- data/app/javascript/lib/__tests__/turbo_stream_actions.test.js +190 -0
- data/app/javascript/lib/api/__tests__/creatives.test.js +68 -0
- data/app/javascript/lib/api/__tests__/csrf_fetch.test.js +27 -0
- data/app/javascript/lib/api/__tests__/topics_save_last_topic.test.js +112 -0
- data/app/javascript/lib/api/creatives.js +32 -13
- data/app/javascript/lib/api/csrf_fetch.js +6 -4
- data/app/javascript/lib/api/topics.js +63 -5
- data/app/javascript/lib/chat_drafts.js +786 -0
- data/app/javascript/lib/common_popup.js +164 -25
- data/app/javascript/lib/creative_link_prefetch.js +27 -0
- data/app/javascript/lib/last_visited_creative.js +97 -0
- data/app/javascript/lib/lexical/__tests__/creative_link_node.test.js +162 -0
- data/app/javascript/lib/lexical/__tests__/creative_link_trigger.test.js +133 -0
- data/app/javascript/lib/lexical/__tests__/link_toolbar.test.js +62 -0
- data/app/javascript/lib/lexical/creative_link_node.js +101 -0
- data/app/javascript/lib/lexical/creative_link_trigger.js +98 -0
- data/app/javascript/lib/lexical/link_toolbar.js +23 -0
- data/app/javascript/lib/turbo_stream_actions.js +23 -16
- data/app/javascript/lib/utils/__tests__/filter_navigation.test.js +210 -0
- data/app/javascript/lib/utils/__tests__/markdown.test.js +44 -0
- data/app/javascript/lib/utils/filter_navigation.js +100 -0
- data/app/javascript/modules/__tests__/command_menu_creative_picker.test.js +62 -0
- data/app/javascript/modules/__tests__/command_menu_typeahead.test.js +267 -0
- data/app/javascript/modules/__tests__/creative_link_picker.test.js +85 -0
- data/app/javascript/modules/__tests__/creative_row_editor_archive.test.js +391 -0
- data/app/javascript/modules/__tests__/creative_row_editor_delegated_clicks.test.js +158 -0
- data/app/javascript/modules/__tests__/creative_row_editor_empty_state_recovery.test.js +519 -0
- data/app/javascript/modules/__tests__/creative_row_editor_empty_state_removal.test.js +346 -0
- data/app/javascript/modules/__tests__/creative_row_editor_session.test.js +144 -0
- data/app/javascript/modules/__tests__/creative_tree_dom.test.js +30 -0
- data/app/javascript/modules/__tests__/creative_tree_empty_state.test.js +186 -0
- data/app/javascript/modules/__tests__/dom_listener_registry.test.js +67 -0
- data/app/javascript/modules/__tests__/support/inline_editor_dom.js +159 -0
- data/app/javascript/modules/command_args_form.js +0 -12
- data/app/javascript/modules/command_menu.js +80 -38
- data/app/javascript/modules/creative_link_picker.js +56 -0
- data/app/javascript/modules/creative_row_editor.js +446 -243
- data/app/javascript/modules/creative_row_editor_delegated_clicks.js +87 -0
- data/app/javascript/modules/creative_tree_dom.js +4 -0
- data/app/javascript/modules/creative_tree_empty_state.js +80 -0
- data/app/javascript/modules/dom_listener_registry.js +23 -0
- data/app/javascript/modules/mention_menu.js +2 -3
- data/app/javascript/utils/__tests__/caret_anchor.test.js +75 -0
- data/app/javascript/utils/__tests__/chevron_icons.test.js +15 -0
- data/app/javascript/utils/caret_position.js +10 -0
- data/app/javascript/utils/chevron_icons.js +7 -0
- data/app/jobs/collavre/ai_agent_job.rb +262 -72
- data/app/jobs/collavre/cancel_offline_delegated_tasks_job.rb +11 -36
- data/app/jobs/collavre/comment_badges_broadcast_job.rb +28 -0
- data/app/jobs/collavre/comment_link_preview_job.rb +33 -0
- data/app/jobs/collavre/comment_notification_job.rb +14 -0
- data/app/jobs/collavre/comment_push_delivery_sweep_job.rb +17 -0
- data/app/jobs/collavre/compress_job.rb +33 -22
- data/app/jobs/collavre/creative_broadcast_job.rb +161 -48
- data/app/jobs/collavre/cron_action_job.rb +36 -18
- data/app/jobs/collavre/drop_trigger_job.rb +23 -9
- data/app/jobs/collavre/merge_comments_job.rb +33 -16
- data/app/jobs/collavre/push_notification_job.rb +62 -18
- data/app/jobs/collavre/restore_dropped_dispatches_job.rb +17 -0
- data/app/jobs/collavre/stuck_detector_job.rb +1 -1
- data/app/jobs/collavre/touch_creative_subtree_job.rb +14 -0
- data/app/models/collavre/agent_gateway.rb +200 -0
- data/app/models/collavre/agent_subscription.rb +4 -4
- data/app/models/collavre/agent_workspace.rb +210 -0
- data/app/models/collavre/comment/broadcastable.rb +22 -52
- data/app/models/collavre/comment/notifiable.rb +154 -77
- data/app/models/collavre/comment/topic_membership.rb +41 -0
- data/app/models/collavre/comment.rb +336 -31
- data/app/models/collavre/comment_notification_delivery.rb +61 -0
- data/app/models/collavre/comment_presence_store/cache_batches.rb +64 -0
- data/app/models/collavre/comment_presence_store.rb +272 -12
- data/app/models/collavre/comment_read_pointer.rb +9 -15
- data/app/models/collavre/creative/describable.rb +8 -2
- data/app/models/collavre/creative/permissible.rb +11 -0
- data/app/models/collavre/creative.rb +29 -4
- data/app/models/collavre/creative_share.rb +109 -4
- data/app/models/collavre/current.rb +1 -0
- data/app/models/collavre/llm_model.rb +62 -0
- data/app/models/collavre/system_setting.rb +12 -1
- data/app/models/collavre/task.rb +121 -2
- data/app/models/collavre/topic.rb +51 -0
- data/app/models/collavre/user.rb +101 -41
- data/app/models/collavre/user_creative_preference.rb +3 -1
- data/app/services/collavre/agent_provisioning/archive.rb +108 -0
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +16 -3
- data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +63 -6
- data/app/services/collavre/ai_agent/approval_handler.rb +12 -4
- data/app/services/collavre/ai_agent/claude_channel_adapter.rb +10 -5
- data/app/services/collavre/ai_agent/merged_trigger_comments.rb +164 -0
- data/app/services/collavre/ai_agent/message_builder.rb +103 -16
- data/app/services/collavre/ai_agent/session_provisioner.rb +3 -7
- data/app/services/collavre/ai_agent/task_claim_service.rb +29 -15
- data/app/services/collavre/ai_agent/task_reply_service.rb +67 -0
- data/app/services/collavre/ai_agent_service.rb +108 -20
- data/app/services/collavre/ai_client.rb +211 -21
- data/app/services/collavre/cli_proxy/client.rb +147 -0
- data/app/services/collavre/cli_proxy/endpoint_policy.rb +81 -0
- data/app/services/collavre/cli_proxy/identity.rb +47 -0
- data/app/services/collavre/cli_proxy/safe_net_http_adapter.rb +19 -0
- data/app/services/collavre/command_menu_service.rb +0 -10
- data/app/services/collavre/comment_move_service.rb +96 -7
- data/app/services/collavre/comment_snapshot_restore_service.rb +11 -5
- data/app/services/collavre/comments/action_executor.rb +48 -20
- data/app/services/collavre/comments/command_processor.rb +0 -1
- data/app/services/collavre/comments/list_response.rb +133 -0
- data/app/services/collavre/comments/read_pointer_writer.rb +210 -0
- data/app/services/collavre/comments/read_receipt_broadcaster.rb +157 -0
- data/app/services/collavre/comments/read_receipt_index.rb +152 -0
- data/app/services/collavre/comments/topic_command.rb +72 -1
- data/app/services/collavre/comments/topic_mutation.rb +20 -0
- data/app/services/collavre/creatives/breadcrumb_resolver.rb +1 -1
- data/app/services/collavre/creatives/comment_badge_index.rb +299 -27
- data/app/services/collavre/creatives/reorderer.rb +15 -3
- data/app/services/collavre/creatives/tree_builder.rb +1 -1
- data/app/services/collavre/creatives/tree_formatter.rb +5 -6
- data/app/services/collavre/creatives/workspace_path_resolver.rb +50 -0
- data/app/services/collavre/creatives/workspace_tree_builder.rb +92 -0
- data/app/services/collavre/crons/recurring_topic_tasks.rb +23 -0
- data/app/services/collavre/home_page_path.rb +55 -0
- data/app/services/collavre/html_text.rb +64 -0
- data/app/services/collavre/http_client.rb +11 -2
- data/app/services/collavre/inbox_reply_service.rb +1 -1
- data/app/services/collavre/markdown_converter.rb +13 -1
- data/app/services/collavre/orchestration/agent_orchestrator.rb +776 -81
- data/app/services/collavre/orchestration/arbiter.rb +48 -5
- data/app/services/collavre/orchestration/deferred_trigger_scope.rb +53 -0
- data/app/services/collavre/orchestration/delivery_record.rb +792 -0
- data/app/services/collavre/orchestration/loop_breaker.rb +10 -4
- data/app/services/collavre/orchestration/matcher.rb +132 -17
- data/app/services/collavre/orchestration/policy_resolver.rb +58 -10
- data/app/services/collavre/orchestration/scheduler.rb +10 -4
- data/app/services/collavre/orchestration/scheduling_hooks.rb +10 -0
- data/app/services/collavre/orchestration/selection.rb +43 -0
- data/app/services/collavre/orchestration/stuck_detector.rb +25 -112
- data/app/services/collavre/orchestration/task_coalescer.rb +355 -0
- data/app/services/collavre/orchestration/topic_slot.rb +73 -0
- data/app/services/collavre/system_events/context_builder.rb +60 -11
- data/app/services/collavre/system_events/dispatcher.rb +58 -12
- data/app/services/collavre/system_events/envelope.rb +73 -0
- data/app/services/collavre/system_events/vocabulary.rb +45 -0
- data/app/services/collavre/tools/creative_retrieval_service.rb +1 -1
- data/app/services/collavre/tools/cron_create_service.rb +32 -18
- data/app/services/collavre/tools/id_list.rb +38 -0
- data/app/services/collavre/tools/topic_authorizer.rb +61 -7
- data/app/services/collavre/tools/topic_branch_service.rb +85 -0
- data/app/services/collavre/tools/topic_create_service.rb +117 -0
- data/app/services/collavre/tools/topic_list_service.rb +101 -0
- data/app/services/collavre/tools/topic_message_create_service.rb +192 -0
- data/app/services/collavre/tools/topic_messages_service.rb +346 -0
- data/app/services/collavre/tools/topic_move_service.rb +108 -0
- data/app/services/collavre/tools/topic_selection.rb +59 -0
- data/app/services/collavre/tools/topic_update_service.rb +165 -0
- data/app/services/collavre/topic_branch_service.rb +39 -10
- data/app/services/collavre/topics/agent_resolver.rb +87 -0
- data/app/services/collavre/topics/char_budget.rb +70 -0
- data/app/services/collavre/topics/last_topic_preference_payload.rb +18 -0
- data/app/services/collavre/topics/message_page.rb +380 -0
- data/app/services/collavre/topics/message_page_markdown.rb +102 -0
- data/app/services/collavre/topics/message_scope.rb +61 -0
- data/app/services/collavre/topics/message_stats.rb +92 -0
- data/app/services/collavre/topics/move_blocker.rb +47 -0
- data/app/services/collavre/topics/next_name.rb +35 -0
- data/app/services/collavre/topics/pin_rejection.rb +25 -0
- data/app/services/collavre/topics/read_pointer_relocator.rb +71 -0
- data/app/services/collavre/topics/reserved_name.rb +18 -0
- data/app/services/collavre/topics/serializer.rb +92 -0
- data/app/services/collavre/topics/topic_destroy.rb +30 -0
- data/app/services/collavre/topics/topic_move.rb +80 -0
- data/app/services/collavre/topics/topic_move_effects.rb +35 -0
- data/app/services/collavre/topics/topic_mutation.rb +27 -0
- data/app/views/collavre/admin/integrations/_setting_row.html.erb +5 -8
- data/app/views/collavre/admin/settings/_system_tab.html.erb +7 -0
- data/app/views/collavre/agent_connections/show.html.erb +77 -0
- data/app/views/collavre/agent_gateways/_form.html.erb +71 -0
- data/app/views/collavre/agent_gateways/edit.html.erb +2 -0
- data/app/views/collavre/agent_gateways/index.html.erb +54 -0
- data/app/views/collavre/agent_gateways/new.html.erb +2 -0
- data/app/views/collavre/comments/_comments_popup.html.erb +32 -17
- data/app/views/collavre/comments/_empty_state.html.erb +33 -0
- data/app/views/collavre/comments/_empty_state_minimal.html.erb +7 -0
- data/app/views/collavre/comments/_feature_card.html.erb +35 -0
- data/app/views/collavre/comments/_list.html.erb +3 -2
- data/app/views/collavre/comments/_no_search_results.html.erb +3 -0
- data/app/views/collavre/creatives/_add_button.html.erb +17 -19
- data/app/views/collavre/creatives/_empty_state.html.erb +109 -0
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +5 -5
- data/app/views/collavre/creatives/_tree_loading_placeholder.html.erb +16 -0
- data/app/views/collavre/creatives/_workspace_tree.html.erb +33 -0
- data/app/views/collavre/creatives/index.html.erb +79 -18
- data/app/views/collavre/desktop_setup/show.html.erb +114 -0
- data/app/views/collavre/features/index.html.erb +42 -0
- data/app/views/collavre/features/show.html.erb +52 -0
- data/app/views/collavre/invitation_mailer/invite.html.erb +1 -1
- data/app/views/collavre/invitation_mailer/invite.text.erb +1 -1
- data/app/views/collavre/invites/show.html.erb +14 -1
- data/app/views/collavre/landing/show.html.erb +5 -1
- data/app/views/collavre/shared/_link_creative_modal.html.erb +4 -1
- data/app/views/collavre/shared/navigation/_help_button.html.erb +3 -1
- data/app/views/collavre/shared/navigation/_panels.html.erb +1 -8
- data/app/views/collavre/shared/navigation/_search_form.html.erb +13 -2
- data/app/views/collavre/user_themes/index.html.erb +1 -1
- data/app/views/collavre/users/_llm_fields.html.erb +65 -0
- data/app/views/collavre/users/_org_chart_node.html.erb +1 -1
- data/app/views/collavre/users/edit_ai.html.erb +33 -31
- data/app/views/collavre/users/new.html.erb +4 -2
- data/app/views/collavre/users/new_ai.html.erb +14 -32
- data/app/views/collavre/users/show.html.erb +13 -1
- data/app/views/layouts/collavre/landing.html.erb +9 -4
- data/config/initializers/ruby_llm_openai_reasoning_content.rb +77 -0
- data/config/locales/admin.en.yml +3 -1
- data/config/locales/admin.ko.yml +3 -1
- data/config/locales/agent_gateway.en.yml +217 -0
- data/config/locales/agent_gateway.ko.yml +217 -0
- data/config/locales/ai_agent.en.yml +0 -1
- data/config/locales/ai_agent.ko.yml +0 -1
- data/config/locales/comments.en.yml +72 -30
- data/config/locales/comments.ko.yml +72 -30
- data/config/locales/creatives.en.yml +32 -1
- data/config/locales/creatives.ko.yml +32 -1
- data/config/locales/features.en.yml +140 -0
- data/config/locales/features.ko.yml +140 -0
- data/config/locales/invites.en.yml +3 -0
- data/config/locales/invites.ko.yml +3 -0
- data/config/locales/landing.en.yml +2 -1
- data/config/locales/landing.ko.yml +2 -1
- data/config/locales/tools.en.yml +20 -0
- data/config/locales/tools.ko.yml +20 -0
- data/config/locales/users.en.yml +15 -14
- data/config/locales/users.ko.yml +14 -14
- data/config/routes.rb +45 -19
- data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +9 -1
- data/db/migrate/20260724000000_add_creative_id_index_to_comments.rb +34 -0
- data/db/migrate/20260724000001_add_creative_private_id_index_to_comments.rb +50 -0
- data/db/migrate/20260724090000_add_async_comment_notification_state.rb +64 -0
- data/db/migrate/20260727000000_add_waiting_notice_scope_to_comments.rb +30 -0
- data/db/migrate/20260727010000_add_waiting_notice_scope_to_tasks.rb +26 -0
- data/db/migrate/20260805000000_add_creative_workspace_enabled_to_users.rb +5 -0
- data/db/migrate/20260807050000_add_dismissed_notices_to_users.rb +5 -0
- data/db/migrate/20260809000000_change_creative_workspace_enabled_default_to_true.rb +18 -0
- data/db/migrate/20260809000001_create_llm_models.rb +64 -0
- data/db/migrate/20260809000002_create_agent_gateways_and_workspaces.rb +35 -0
- data/db/migrate/20260809000003_hash_agent_workspace_callback_tokens.rb +44 -0
- data/db/migrate/20260809000004_encrypt_agent_workspace_manifest_tokens.rb +59 -0
- data/db/migrate/20260810000000_split_agent_workspace_identity_axes.rb +58 -0
- data/db/migrate/20260810000001_seed_codex_custom_proxy_llm_models.rb +28 -0
- data/db/migrate/20260810010000_add_last_visited_creative_to_users.rb +8 -0
- data/db/migrate/20260810020000_add_last_visited_creative_at_to_users.rb +5 -0
- data/db/migrate/20260810030000_add_last_visited_creative_navigation_order_to_users.rb +6 -0
- data/db/migrate/20260810060000_add_last_visited_creative_issued_sequence_to_users.rb +5 -0
- data/db/migrate/20260810100000_add_desktop_managed_to_agent_gateways.rb +40 -0
- data/db/migrate/20260810100001_add_desktop_preset_adapter_to_users.rb +38 -0
- data/db/migrate/20260811000000_make_agent_gateway_completion_key_nullable.rb +7 -0
- data/db/migrate/20260812000000_remove_workflow_columns_from_tasks.rb +101 -0
- data/db/migrate/20260812000001_remove_android_devices.rb +24 -0
- data/db/migrate/20260812010000_drop_inbox_items.rb +22 -0
- data/db/migrate/20260813000000_clear_claude_channel_presence_routing_expressions.rb +16 -0
- data/db/migrate/20260813000001_add_last_topic_revision_to_user_creative_preferences.rb +5 -0
- data/db/migrate/20260813000002_add_last_topic_save_order_to_user_creative_preferences.rb +6 -0
- data/db/migrate/20260813000003_add_last_topic_save_sequences_to_user_creative_preferences.rb +5 -0
- data/db/migrate/20260813000004_add_last_topic_save_fences_to_user_creative_preferences.rb +6 -0
- data/db/migrate/20260814000000_add_topic_to_comment_read_pointers.rb +79 -0
- data/db/migrate/20260814000001_add_creative_topic_private_id_index_to_comments.rb +37 -0
- data/db/migrate/20260816000000_add_watermark_index_to_comment_read_pointers.rb +27 -0
- data/db/migrate/20260816000001_add_private_recipient_topic_index_to_comments.rb +48 -0
- data/db/migrate/20260820000000_add_topic_assigned_at_to_comments.rb +13 -0
- data/db/seeds.rb +1 -51
- data/lib/collavre/engine.rb +108 -1
- data/lib/collavre/feature_card_registry.rb +168 -0
- data/lib/collavre/hashed_access_token_lookup.rb +22 -0
- data/lib/collavre/sensitive_request_silencer.rb +23 -0
- data/lib/collavre/user_extensions.rb +0 -2
- data/lib/collavre/version.rb +1 -1
- data/lib/collavre.rb +1 -0
- data/skills/collavre/SKILL.md +165 -0
- data/skills/collavre/package.json +8 -0
- data/skills/collavre/references/tool-reference.md +347 -0
- data/skills/collavre/scripts/collavre +634 -0
- data/skills/collavre/scripts/html_entities.js +24 -0
- metadata +228 -30
- data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +0 -224
- data/app/controllers/collavre/api/v1/mobile/base_controller.rb +0 -95
- data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +0 -31
- data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +0 -25
- data/app/controllers/collavre/inbox_items_controller.rb +0 -70
- data/app/controllers/collavre/typo_corrections_controller.rb +0 -39
- data/app/javascript/lib/__tests__/typo_correction.test.js +0 -192
- data/app/javascript/lib/typo_correction.js +0 -146
- data/app/javascript/modules/__tests__/typo_corrector.test.js +0 -365
- data/app/javascript/modules/creative_guide.js +0 -67
- data/app/javascript/modules/typo_corrector.js +0 -534
- data/app/jobs/collavre/inbox_summary_job.rb +0 -24
- data/app/mailers/collavre/inbox_mailer.rb +0 -19
- data/app/models/collavre/inbox_item.rb +0 -89
- data/app/services/collavre/comments/concerns/workflow_support.rb +0 -115
- data/app/services/collavre/comments/work_command.rb +0 -161
- data/app/services/collavre/comments/workflow_executor.rb +0 -276
- data/app/services/collavre/gemini_parent_recommender.rb +0 -77
- data/app/services/collavre/mobile/event_summarizer.rb +0 -40
- data/app/services/collavre/typo_corrector.rb +0 -188
- data/app/views/collavre/inbox_items/_item.html.erb +0 -18
- data/app/views/collavre/inbox_items/_items.html.erb +0 -3
- data/app/views/collavre/inbox_items/_list.html.erb +0 -7
- data/app/views/collavre/inbox_items/index.html.erb +0 -1
- data/app/views/collavre/inbox_mailer/daily_summary.html.erb +0 -11
- data/app/views/collavre/inbox_mailer/daily_summary.text.erb +0 -8
- data/app/views/collavre/users/typo_correction.html.erb +0 -50
- data/config/locales/mobile.en.yml +0 -16
- data/config/locales/mobile.ko.yml +0 -16
|
@@ -38,12 +38,12 @@ module Collavre
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# Main entry point - checks all loop conditions
|
|
41
|
-
def check
|
|
41
|
+
def check(prospective_interaction: nil)
|
|
42
42
|
return Result.new(should_break: false, reason: nil, details: {}) unless enabled?
|
|
43
43
|
|
|
44
44
|
# Check each condition
|
|
45
45
|
checks = [
|
|
46
|
-
check_ping_pong,
|
|
46
|
+
check_ping_pong(prospective_interaction),
|
|
47
47
|
check_creative_retry,
|
|
48
48
|
check_task_timeout,
|
|
49
49
|
check_token_spike
|
|
@@ -110,13 +110,14 @@ module Collavre
|
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
# Detect ping-pong between agents
|
|
113
|
-
def check_ping_pong
|
|
113
|
+
def check_ping_pong(prospective_interaction)
|
|
114
114
|
creative_id = @context.dig("creative", "id")
|
|
115
115
|
return safe_result unless creative_id
|
|
116
116
|
|
|
117
117
|
# Check the ping-pong tracking key for this creative
|
|
118
118
|
key = "#{CACHE_PREFIX}:ping_pong_history:#{creative_id}"
|
|
119
|
-
interactions = Rails.cache.read(key)
|
|
119
|
+
interactions = Array(Rails.cache.read(key)).dup
|
|
120
|
+
interactions << prospective_interaction_entry(prospective_interaction) if prospective_interaction
|
|
120
121
|
|
|
121
122
|
# Count exchanges for each agent pair
|
|
122
123
|
pair_exchanges = count_pair_exchanges(interactions)
|
|
@@ -139,6 +140,11 @@ module Collavre
|
|
|
139
140
|
safe_result
|
|
140
141
|
end
|
|
141
142
|
|
|
143
|
+
def prospective_interaction_entry(interaction)
|
|
144
|
+
edge = interaction.with_indifferent_access
|
|
145
|
+
{ at: Time.current.to_i, from: edge[:from_agent_id], to: edge[:to_agent_id] }
|
|
146
|
+
end
|
|
147
|
+
|
|
142
148
|
# Detect too many tasks on same topic (per-topic, not per-creative)
|
|
143
149
|
def check_creative_retry
|
|
144
150
|
creative_id = @context.dig("creative", "id")
|
|
@@ -8,13 +8,53 @@ module Collavre
|
|
|
8
8
|
# 1. Mention-based: If a user is @mentioned, route exclusively to that user
|
|
9
9
|
# - If mentioned user is AI agent → route to that agent only
|
|
10
10
|
# - If mentioned user is human → no AI agents respond
|
|
11
|
-
# 2.
|
|
11
|
+
# 2. Primary-agent assignment: If the topic has a primary_agent, that agent is
|
|
12
|
+
# the topic's sole ambient responder (see #match_by_primary_agent)
|
|
13
|
+
# 3. Expression-based: Evaluate each agent's routing_expression (Liquid)
|
|
12
14
|
#
|
|
13
15
|
# Permission checks:
|
|
14
16
|
# - All agents need feedback permission on the creative to respond
|
|
15
17
|
# - searchable only affects discoverability, not response permission
|
|
16
18
|
#
|
|
17
19
|
class Matcher
|
|
20
|
+
# The one entry point for "may this agent answer, given the topic's
|
|
21
|
+
# primary-agent assignment?", taking a raw trigger payload.
|
|
22
|
+
#
|
|
23
|
+
# Three paths re-ask this question about a waiting task — promotion
|
|
24
|
+
# (AgentOrchestrator.revalidate_assignment!), the resumed AiAgentJob, and
|
|
25
|
+
# approval resumption through it — and each one cancels the task on a
|
|
26
|
+
# "no". They must ask the same question, because a direct @mention
|
|
27
|
+
# outranks the assignment and coalescing moves that mention out of the
|
|
28
|
+
# anchor and into "merged_comment_ids": judging the anchor alone cancels a
|
|
29
|
+
# task that WAS explicitly addressed, and discards every comment it had
|
|
30
|
+
# absorbed along with it. The mention still reaches the agent
|
|
31
|
+
# (MergedTriggerComments folds it into the trigger), so it still counts.
|
|
32
|
+
def self.permits_assignment?(context, agent)
|
|
33
|
+
return true if new(SystemEvents::ContextBuilder.new(context).build)
|
|
34
|
+
.assignment_permits?(agent)
|
|
35
|
+
|
|
36
|
+
merged_ids = Array(context[TaskCoalescer::PAYLOAD_KEY]).compact
|
|
37
|
+
return false if merged_ids.empty?
|
|
38
|
+
|
|
39
|
+
# Scoped to the turn, through the same predicate that decides what is
|
|
40
|
+
# delivered. A merged mention moved to another creative while the task
|
|
41
|
+
# waited is deliberately kept out of the trigger, so it cannot also be
|
|
42
|
+
# the reason the turn is permitted: that would let a non-primary agent
|
|
43
|
+
# answer an ambient anchor with no in-scope mention anywhere.
|
|
44
|
+
AiAgent::MergedTriggerComments.in_turn(merged_ids, context).pluck(:content).any? do |content|
|
|
45
|
+
probe = context.merge("chat" => { "content" => content })
|
|
46
|
+
new(SystemEvents::ContextBuilder.new(probe).build).assignment_permits?(agent)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The permission every routing path in #match is gated on, asked on its
|
|
51
|
+
# own — for a caller that names its agent instead of matching one.
|
|
52
|
+
# DeliveryRecord.restore! is that caller: it puts back the dispatch this
|
|
53
|
+
# matcher once produced, and nothing else on its way re-asks this.
|
|
54
|
+
def self.permits_creative_access?(context, agent)
|
|
55
|
+
new(SystemEvents::ContextBuilder.new(context).build).has_creative_permission?(agent)
|
|
56
|
+
end
|
|
57
|
+
|
|
18
58
|
def initialize(context)
|
|
19
59
|
@context = context
|
|
20
60
|
end
|
|
@@ -29,10 +69,60 @@ module Collavre
|
|
|
29
69
|
mentioned_result = match_by_mention
|
|
30
70
|
return mentioned_result unless mentioned_result.nil?
|
|
31
71
|
|
|
32
|
-
# Priority 2:
|
|
72
|
+
# Priority 2: Topic primary agent assignment (exclusive)
|
|
73
|
+
primary_result = match_by_primary_agent
|
|
74
|
+
return primary_result unless primary_result.nil?
|
|
75
|
+
|
|
76
|
+
# Priority 3: Liquid expression routing (fallback)
|
|
33
77
|
match_by_expression
|
|
34
78
|
end
|
|
35
79
|
|
|
80
|
+
# May this agent still take the floor in this topic, given the topic's
|
|
81
|
+
# CURRENT primary-agent assignment?
|
|
82
|
+
#
|
|
83
|
+
# #match runs when an event is dispatched, but the agent it selects can
|
|
84
|
+
# reach execution much later: a :deferred decision parks a queued Task
|
|
85
|
+
# until the topic's blocker finishes, and a :delayed one sleeps in the job
|
|
86
|
+
# queue. Neither path re-matches — AiAgentJob#perform runs the agent the
|
|
87
|
+
# decision named — so an assignment created or moved during that window
|
|
88
|
+
# would be bypassed by work scheduled moments before it, and a demoted
|
|
89
|
+
# agent would speak in a topic that is now exclusively someone else's.
|
|
90
|
+
#
|
|
91
|
+
# Mirrors #match's precedence rather than re-deriving it: review and
|
|
92
|
+
# mention are exclusive routings that outrank the assignment (see
|
|
93
|
+
# #match_by_primary_agent), so those dispatches stay valid. Expression
|
|
94
|
+
# routing does not, so it is revalidated. Only the assignment is rechecked
|
|
95
|
+
# here — re-running the whole match would also re-evaluate each
|
|
96
|
+
# routing_expression against the refreshed comment, silently dropping
|
|
97
|
+
# deferred work for reasons that have nothing to do with the assignment.
|
|
98
|
+
def assignment_permits?(agent)
|
|
99
|
+
primary_id = matched_topic&.primary_agent_id
|
|
100
|
+
return true if primary_id.nil? || primary_id == agent.id
|
|
101
|
+
|
|
102
|
+
# A review can only ever be applied in place by the author of the quoted
|
|
103
|
+
# comment; no assignment can transfer it, so the alternative to running
|
|
104
|
+
# it is not "the primary answers instead" but a dead Review button.
|
|
105
|
+
return true if matched_comment&.review_message? &&
|
|
106
|
+
matched_comment.quoted_comment&.user_id == agent.id
|
|
107
|
+
|
|
108
|
+
mentioned_id = @context.dig("chat", "mentioned_user", "id") ||
|
|
109
|
+
@context.dig(:chat, :mentioned_user, :id)
|
|
110
|
+
mentioned_id.present? && mentioned_id.to_i == agent.id
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Public because #match is not the only door onto a dispatch: a restore
|
|
114
|
+
# names the agent the match once produced, and this is the gate every
|
|
115
|
+
# branch of #match shares. One predicate, so the two doors cannot come to
|
|
116
|
+
# disagree about what "may answer this creative" means.
|
|
117
|
+
def has_creative_permission?(agent)
|
|
118
|
+
# All agents need feedback permission on the creative to respond
|
|
119
|
+
# searchable only affects discoverability, not response permission
|
|
120
|
+
creative = matched_creative
|
|
121
|
+
return false unless creative
|
|
122
|
+
|
|
123
|
+
creative.has_permission?(agent, :feedback)
|
|
124
|
+
end
|
|
125
|
+
|
|
36
126
|
private
|
|
37
127
|
|
|
38
128
|
# Returns [author] for a routable review message, [] for an unroutable one,
|
|
@@ -92,26 +182,60 @@ module Collavre
|
|
|
92
182
|
[ mentioned_user ]
|
|
93
183
|
end
|
|
94
184
|
|
|
185
|
+
# Returns [primary_agent] when the topic has one, nil when it does not.
|
|
186
|
+
#
|
|
187
|
+
# A topic's primary agent is an exclusive assignment: it is the only agent
|
|
188
|
+
# that speaks on ambient events in that topic. This deliberately overrides
|
|
189
|
+
# each agent's own routing_expression in BOTH directions:
|
|
190
|
+
#
|
|
191
|
+
# - The primary speaks even with no routing_expression (or one that
|
|
192
|
+
# evaluates false), so pinning an agent is enough to talk to it.
|
|
193
|
+
# - Every other agent stays silent even when its expression matches, so a
|
|
194
|
+
# project-wide roster of agents does not all pile into one task.
|
|
195
|
+
#
|
|
196
|
+
# Other agents are not muted, only demoted to explicit invitation: an
|
|
197
|
+
# @mention routes to them via #match_by_mention, which runs first.
|
|
198
|
+
#
|
|
199
|
+
# Returning [] (rather than nil) when the primary is ineligible is
|
|
200
|
+
# intentional — falling through to expression routing would let exactly
|
|
201
|
+
# the agents this assignment excludes take the floor instead.
|
|
202
|
+
def match_by_primary_agent
|
|
203
|
+
agent = matched_topic&.primary_agent
|
|
204
|
+
return nil unless agent
|
|
205
|
+
|
|
206
|
+
return [] unless agent.ai_user?
|
|
207
|
+
return [] unless has_creative_permission?(agent)
|
|
208
|
+
return [] unless eligible_in_inbox?(agent)
|
|
209
|
+
|
|
210
|
+
[ agent ]
|
|
211
|
+
end
|
|
212
|
+
|
|
95
213
|
def match_by_expression
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
214
|
+
# A Claude Channel agent is ambiently routable only while its cable
|
|
215
|
+
# subscription has a live presence row. Its routing_expression remains
|
|
216
|
+
# available for an explicitly configured Liquid rule, but is never used
|
|
217
|
+
# as a session-liveness flag.
|
|
218
|
+
live_claude_agent_ids = AgentSubscription.live.pluck(:agent_id)
|
|
219
|
+
expression_agents = User.where.not(llm_vendor: nil)
|
|
220
|
+
.where.not(routing_expression: [ nil, "" ])
|
|
221
|
+
agents = expression_agents.or(User.where(id: live_claude_agent_ids)).order(:id)
|
|
99
222
|
|
|
100
223
|
agents.select do |agent|
|
|
101
224
|
next false unless has_creative_permission?(agent)
|
|
102
225
|
next false unless eligible_in_inbox?(agent)
|
|
226
|
+
next false if agent.claude_channel_agent? && !live_claude_agent_ids.include?(agent.id)
|
|
103
227
|
|
|
104
|
-
evaluate_routing_expression(agent)
|
|
228
|
+
agent.routing_expression.blank? || evaluate_routing_expression(agent)
|
|
105
229
|
end
|
|
106
230
|
end
|
|
107
231
|
|
|
108
232
|
# A Claude Channel session agent holds inbox-wide :feedback +
|
|
109
|
-
#
|
|
233
|
+
# a live presence row, so within the user's Inbox it would otherwise
|
|
110
234
|
# match EVERY topic. Confine it to its own registered session topic (the
|
|
111
235
|
# topic it is primary_agent on, carrying a session_id) so ordinary inbox
|
|
112
236
|
# topics — Main, Content, user threads — stay identical to a normal topic
|
|
113
237
|
# and are never absorbed by a live session. Only the inbox is affected: on
|
|
114
|
-
# work/project creatives the agent still matches
|
|
238
|
+
# work/project creatives the agent still matches while it is live.
|
|
115
239
|
def eligible_in_inbox?(agent)
|
|
116
240
|
return true unless matched_creative&.inbox?
|
|
117
241
|
return true unless agent.claude_channel_agent?
|
|
@@ -139,15 +263,6 @@ module Collavre
|
|
|
139
263
|
false
|
|
140
264
|
end
|
|
141
265
|
|
|
142
|
-
def has_creative_permission?(agent)
|
|
143
|
-
# All agents need feedback permission on the creative to respond
|
|
144
|
-
# searchable only affects discoverability, not response permission
|
|
145
|
-
creative = matched_creative
|
|
146
|
-
return false unless creative
|
|
147
|
-
|
|
148
|
-
creative.has_permission?(agent, :feedback)
|
|
149
|
-
end
|
|
150
|
-
|
|
151
266
|
def matched_creative
|
|
152
267
|
return @matched_creative if defined?(@matched_creative)
|
|
153
268
|
|
|
@@ -28,6 +28,13 @@ module Collavre
|
|
|
28
28
|
"rate_limit_per_minute" => 20,
|
|
29
29
|
"backoff_strategy" => "exponential",
|
|
30
30
|
"topic_max_concurrent_jobs" => 1,
|
|
31
|
+
# Fold un-started tasks for the same agent/topic/creative into one so a
|
|
32
|
+
# burst of comments produces one answer instead of one per comment.
|
|
33
|
+
"coalesce_pending_tasks" => true,
|
|
34
|
+
# Drop a dispatch whose comment an in-flight turn has already been
|
|
35
|
+
# given, instead of parking it as a waiter (see
|
|
36
|
+
# Orchestration::DeliveryRecord).
|
|
37
|
+
"drop_delivered_dispatches" => true,
|
|
31
38
|
# Loop breaker settings
|
|
32
39
|
"loop_breaker_enabled" => true,
|
|
33
40
|
"ping_pong_threshold" => 5, # Max back-and-forth between same agents
|
|
@@ -38,9 +45,8 @@ module Collavre
|
|
|
38
45
|
"token_spike_window_minutes" => 10
|
|
39
46
|
},
|
|
40
47
|
"stuck_detection" => {
|
|
41
|
-
"enabled" =>
|
|
48
|
+
"enabled" => true,
|
|
42
49
|
"task_stuck_threshold_minutes" => 30, # Task running for > N minutes
|
|
43
|
-
"creative_stall_threshold_minutes" => 120, # Creative no progress for > N minutes
|
|
44
50
|
"queued_orphan_threshold_minutes" => 5, # Queued waiter with no live blocker for > N minutes
|
|
45
51
|
"create_system_comment" => true # Create system comment on escalation
|
|
46
52
|
},
|
|
@@ -87,6 +93,43 @@ module Collavre
|
|
|
87
93
|
scheduling_config["topic_max_concurrent_jobs"]
|
|
88
94
|
end
|
|
89
95
|
|
|
96
|
+
# Whether un-started tasks for the same agent/topic/creative are folded
|
|
97
|
+
# into a single one (see Orchestration::TaskCoalescer).
|
|
98
|
+
#
|
|
99
|
+
# Resolved per agent, because that is the granularity of the thing being
|
|
100
|
+
# switched: coalescing folds *same-agent* siblings, and a User-scoped
|
|
101
|
+
# scheduling policy is how an administrator turns it off for one agent
|
|
102
|
+
# without touching the others in the topic. #scheduling_config excludes
|
|
103
|
+
# agent policies by construction, so asking it here silently ignored that
|
|
104
|
+
# opt-out on every enqueue, promotion, start and notice path.
|
|
105
|
+
#
|
|
106
|
+
# There is deliberately no agent-less variant to fall back to: a second
|
|
107
|
+
# door onto this question is how the two answers drift, and every caller
|
|
108
|
+
# has the agent to hand. `nil` is accepted only for a dispatch with no
|
|
109
|
+
# agent resolved yet, where the global answer is the only one there is.
|
|
110
|
+
def coalesce_pending_tasks_for?(agent)
|
|
111
|
+
config = agent ? scheduling_config_for(agent) : scheduling_config
|
|
112
|
+
config["coalesce_pending_tasks"] != false
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Whether a dispatch is dropped outright when an in-flight turn has
|
|
116
|
+
# already delivered its comment (see Orchestration::DeliveryRecord).
|
|
117
|
+
#
|
|
118
|
+
# Resolved per agent for the same reason coalescing is: delivery is a
|
|
119
|
+
# same-agent question, and a User-scoped scheduling policy is how an
|
|
120
|
+
# administrator turns this off for one agent without touching the others
|
|
121
|
+
# in the topic.
|
|
122
|
+
#
|
|
123
|
+
# Only the *drop* is switched. The record itself keeps being written
|
|
124
|
+
# whatever this says — it is evidence of what an agent was sent, and the
|
|
125
|
+
# promotion door reads it to keep a parked waiter from replaying a
|
|
126
|
+
# delivered comment. Gating the write here would make turning the switch
|
|
127
|
+
# off mid-burst leave that door reading a record with a hole in it.
|
|
128
|
+
def drop_delivered_dispatches_for?(agent)
|
|
129
|
+
config = agent ? scheduling_config_for(agent) : scheduling_config
|
|
130
|
+
config["drop_delivered_dispatches"] != false
|
|
131
|
+
end
|
|
132
|
+
|
|
90
133
|
# Convenience methods
|
|
91
134
|
def arbitration_strategy
|
|
92
135
|
return "primary_first" if topic_primary_agent_id.present?
|
|
@@ -102,6 +145,19 @@ module Collavre
|
|
|
102
145
|
topic_primary_agent_id || arbitration_config["primary_agent_id"]
|
|
103
146
|
end
|
|
104
147
|
|
|
148
|
+
# Read primary_agent_id directly from the topics table.
|
|
149
|
+
#
|
|
150
|
+
# Public because the two sources of a primary agent carry different
|
|
151
|
+
# strength: a topic-column assignment is exclusive, while a policy-level
|
|
152
|
+
# primary_agent_id is only a preference. The Arbiter needs to tell them
|
|
153
|
+
# apart even though #primary_agent_id collapses both.
|
|
154
|
+
def topic_primary_agent_id
|
|
155
|
+
topic_id = @context.dig("topic", "id")
|
|
156
|
+
return nil unless topic_id
|
|
157
|
+
|
|
158
|
+
Topic.where(id: topic_id).pick(:primary_agent_id)
|
|
159
|
+
end
|
|
160
|
+
|
|
105
161
|
# Bid strategy specific
|
|
106
162
|
def confidence_threshold
|
|
107
163
|
arbitration_config["confidence_threshold"]
|
|
@@ -163,14 +219,6 @@ module Collavre
|
|
|
163
219
|
|
|
164
220
|
config
|
|
165
221
|
end
|
|
166
|
-
|
|
167
|
-
# Read primary_agent_id directly from the topics table
|
|
168
|
-
def topic_primary_agent_id
|
|
169
|
-
topic_id = @context.dig("topic", "id")
|
|
170
|
-
return nil unless topic_id
|
|
171
|
-
|
|
172
|
-
Topic.where(id: topic_id).pick(:primary_agent_id)
|
|
173
|
-
end
|
|
174
222
|
end
|
|
175
223
|
end
|
|
176
224
|
end
|
|
@@ -31,10 +31,10 @@ module Collavre
|
|
|
31
31
|
# @param agents [Array<User>] Agents selected by Arbiter
|
|
32
32
|
# @return [Array<Hash>] Scheduling decisions
|
|
33
33
|
# Each decision: { agent:, timing:, delay: (optional), reason: (optional) }
|
|
34
|
-
def schedule(agents)
|
|
34
|
+
def schedule(agents, scheduling_hooks: nil)
|
|
35
35
|
topic_immediate_count = 0
|
|
36
36
|
agents.map do |agent|
|
|
37
|
-
decision = evaluate(agent, topic_immediate_count: topic_immediate_count)
|
|
37
|
+
decision = evaluate(agent, topic_immediate_count: topic_immediate_count, scheduling_hooks: scheduling_hooks)
|
|
38
38
|
topic_immediate_count += 1 if decision[:timing] == :immediate
|
|
39
39
|
decision
|
|
40
40
|
end
|
|
@@ -42,13 +42,13 @@ module Collavre
|
|
|
42
42
|
|
|
43
43
|
private
|
|
44
44
|
|
|
45
|
-
def evaluate(agent, topic_immediate_count: 0)
|
|
45
|
+
def evaluate(agent, topic_immediate_count: 0, scheduling_hooks: nil)
|
|
46
46
|
tracker = ResourceTracker.for(agent)
|
|
47
47
|
config = @policy_resolver.scheduling_config_for(agent)
|
|
48
48
|
|
|
49
49
|
# Check 0: Loop breaker
|
|
50
50
|
if @policy_resolver.loop_breaker_enabled?
|
|
51
|
-
loop_result =
|
|
51
|
+
loop_result = loop_breaker_result(agent, scheduling_hooks)
|
|
52
52
|
if loop_result.should_break?
|
|
53
53
|
return loop_broken_decision(agent, loop_result)
|
|
54
54
|
end
|
|
@@ -86,6 +86,12 @@ module Collavre
|
|
|
86
86
|
immediate_decision(agent)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
def loop_breaker_result(agent, scheduling_hooks)
|
|
90
|
+
LoopBreaker.new(@context, policy_resolver: @policy_resolver).check(
|
|
91
|
+
prospective_interaction: scheduling_hooks&.interaction_for(agent)
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
89
95
|
def immediate_decision(agent)
|
|
90
96
|
{
|
|
91
97
|
agent: agent,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module Orchestration
|
|
5
|
+
SchedulingHooks = Data.define(:interaction_callback, :scheduled_callback) do
|
|
6
|
+
def interaction_for(agent) = interaction_callback&.call(agent)
|
|
7
|
+
def scheduled(agents) = scheduled_callback&.call(agents)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module Orchestration
|
|
5
|
+
# Resolves the exact responders for an event without scheduling work.
|
|
6
|
+
class Selection
|
|
7
|
+
attr_reader :agents
|
|
8
|
+
|
|
9
|
+
def initialize(context, policy_resolver:, candidate_overrides: {})
|
|
10
|
+
@context = context
|
|
11
|
+
@policy_resolver = policy_resolver
|
|
12
|
+
@candidate_overrides = candidate_overrides
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
candidates = candidates_for_contexts
|
|
17
|
+
@agents = [] if candidates.empty?
|
|
18
|
+
return self if candidates.empty?
|
|
19
|
+
|
|
20
|
+
@arbiter = Arbiter.new(@context, policy_resolver: @policy_resolver)
|
|
21
|
+
@agents = @arbiter.select(candidates, commit: false)
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def commit!
|
|
26
|
+
@arbiter&.commit_selection!
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def candidates_for_contexts
|
|
33
|
+
candidates = Matcher.new(@context).match
|
|
34
|
+
@candidate_overrides.each do |agent_id, override|
|
|
35
|
+
candidates = candidates.reject { |agent| agent.id == agent_id }
|
|
36
|
+
overridden = Matcher.new(@context.deep_merge(override.deep_stringify_keys)).match
|
|
37
|
+
candidates.concat(overridden.select { |agent| agent.id == agent_id })
|
|
38
|
+
end
|
|
39
|
+
candidates.uniq(&:id).sort_by(&:id)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module Collavre
|
|
4
4
|
module Orchestration
|
|
5
|
-
# StuckDetector detects stuck tasks
|
|
5
|
+
# StuckDetector detects stuck tasks, auto-recovers them, and escalates to admins.
|
|
6
6
|
#
|
|
7
7
|
# Detection conditions:
|
|
8
8
|
# 1. Running tasks that haven't progressed for N minutes
|
|
9
9
|
# 2. Tasks with too many retries (escalated but not handled)
|
|
10
|
-
# 3. Creatives with no progress for extended periods
|
|
11
10
|
#
|
|
12
11
|
# Auto-escalation:
|
|
13
|
-
# - Creates
|
|
12
|
+
# - Creates inbox comments for admin users
|
|
14
13
|
# - Optionally creates a system comment
|
|
15
14
|
#
|
|
16
15
|
class StuckDetector
|
|
@@ -33,7 +32,6 @@ module Collavre
|
|
|
33
32
|
stuck_items = []
|
|
34
33
|
stuck_items.concat(detect_stuck_tasks(config))
|
|
35
34
|
stuck_items.concat(detect_orphaned_queued_tasks(config))
|
|
36
|
-
stuck_items.concat(detect_stalled_creatives(config))
|
|
37
35
|
|
|
38
36
|
auto_recover_stuck_tasks(stuck_items)
|
|
39
37
|
escalated_count = escalate_stuck_items(stuck_items, config)
|
|
@@ -49,7 +47,6 @@ module Collavre
|
|
|
49
47
|
stuck_items = []
|
|
50
48
|
stuck_items.concat(detect_stuck_tasks(config))
|
|
51
49
|
stuck_items.concat(detect_orphaned_queued_tasks(config))
|
|
52
|
-
stuck_items.concat(detect_stalled_creatives(config))
|
|
53
50
|
stuck_items
|
|
54
51
|
end
|
|
55
52
|
|
|
@@ -133,7 +130,14 @@ module Collavre
|
|
|
133
130
|
task = stuck_item.item
|
|
134
131
|
return unless %w[running delegated].include?(task.status)
|
|
135
132
|
|
|
136
|
-
task.
|
|
133
|
+
recovered = if task.status == "running"
|
|
134
|
+
DeliveryRecord.fail_while_worker_settles!(task)
|
|
135
|
+
else
|
|
136
|
+
task.update!(status: "failed")
|
|
137
|
+
true
|
|
138
|
+
end
|
|
139
|
+
return unless recovered
|
|
140
|
+
|
|
137
141
|
Rails.logger.info(
|
|
138
142
|
"[StuckDetector] Auto-recovered task #{task.id} (agent=#{task.agent_id}): " \
|
|
139
143
|
"marked as failed after #{((Time.current - stuck_item.stuck_since) / 60).round} minutes"
|
|
@@ -145,23 +149,6 @@ module Collavre
|
|
|
145
149
|
tracker.release!(task.id)
|
|
146
150
|
end
|
|
147
151
|
|
|
148
|
-
# If this was a workflow subtask, fail the parent so the workflow
|
|
149
|
-
# advances instead of staying running with pending_creative_ids
|
|
150
|
-
# pointing at a child that's been failed underneath it.
|
|
151
|
-
if task.parent_task_id.present?
|
|
152
|
-
begin
|
|
153
|
-
Collavre::Comments::WorkflowExecutor.new(task.parent_task).fail_subtask!(
|
|
154
|
-
task,
|
|
155
|
-
error_message: "Auto-recovered: stuck for " \
|
|
156
|
-
"#{((Time.current - stuck_item.stuck_since) / 60).round} minutes"
|
|
157
|
-
)
|
|
158
|
-
rescue StandardError => e
|
|
159
|
-
Rails.logger.error(
|
|
160
|
-
"[StuckDetector] fail_subtask! failed for task #{task.id}: #{e.message}"
|
|
161
|
-
)
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
|
|
165
152
|
# Drain the queue for the topic so waiting tasks can execute
|
|
166
153
|
AgentOrchestrator.dequeue_next_for_topic(task.topic_id, task.creative_id)
|
|
167
154
|
rescue StandardError => e
|
|
@@ -184,7 +171,6 @@ module Collavre
|
|
|
184
171
|
.where("updated_at < ?", threshold_time)
|
|
185
172
|
|
|
186
173
|
stuck_tasks.filter_map do |task|
|
|
187
|
-
# Skip if already escalated recently
|
|
188
174
|
next if recently_escalated?(task)
|
|
189
175
|
|
|
190
176
|
escalation_targets = find_escalation_targets(task)
|
|
@@ -232,45 +218,6 @@ module Collavre
|
|
|
232
218
|
end
|
|
233
219
|
end
|
|
234
220
|
|
|
235
|
-
# Detect creatives that have stalled (no activity for extended period)
|
|
236
|
-
def detect_stalled_creatives(config)
|
|
237
|
-
threshold_minutes = config["creative_stall_threshold_minutes"] || 120
|
|
238
|
-
threshold_time = threshold_minutes.minutes.ago
|
|
239
|
-
|
|
240
|
-
# Find creatives with incomplete status but no recent activity
|
|
241
|
-
# Only check creatives that have at least one AI agent with access
|
|
242
|
-
stalled = []
|
|
243
|
-
|
|
244
|
-
Creative.where("progress < 1.0")
|
|
245
|
-
.where("updated_at < ?", threshold_time)
|
|
246
|
-
.includes(creative_shares: :user)
|
|
247
|
-
.find_each do |creative|
|
|
248
|
-
# Skip if no AI agents have access
|
|
249
|
-
ai_agents = creative.creative_shares
|
|
250
|
-
.reject { |s| s.permission == "no_access" }
|
|
251
|
-
.map(&:user)
|
|
252
|
-
.select(&:ai_user?)
|
|
253
|
-
|
|
254
|
-
next if ai_agents.empty?
|
|
255
|
-
|
|
256
|
-
# Skip if already escalated recently
|
|
257
|
-
next if recently_escalated_creative?(creative)
|
|
258
|
-
|
|
259
|
-
escalation_targets = find_creative_escalation_targets(creative)
|
|
260
|
-
next if escalation_targets.empty?
|
|
261
|
-
|
|
262
|
-
stalled << StuckItem.new(
|
|
263
|
-
type: :creative,
|
|
264
|
-
item: creative,
|
|
265
|
-
reason: :stalled,
|
|
266
|
-
stuck_since: creative.updated_at,
|
|
267
|
-
escalation_targets: escalation_targets
|
|
268
|
-
)
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
stalled
|
|
272
|
-
end
|
|
273
|
-
|
|
274
221
|
def find_escalation_targets(task)
|
|
275
222
|
# Find admin users for the task's creative
|
|
276
223
|
creative_id = task.trigger_event_payload&.dig("creative", "id")
|
|
@@ -287,7 +234,7 @@ module Collavre
|
|
|
287
234
|
ancestor_ids = [ creative.id ] + creative.ancestor_ids
|
|
288
235
|
admin_users = CreativeShare.where(creative_id: ancestor_ids, permission: "admin")
|
|
289
236
|
.includes(:user)
|
|
290
|
-
.filter_map { |share| share.user
|
|
237
|
+
.filter_map { |share| share.user if share.user && !share.user.ai_user? }
|
|
291
238
|
|
|
292
239
|
# Also include the creative owner
|
|
293
240
|
admin_users << creative.user if creative.user && !creative.user.ai_user?
|
|
@@ -301,18 +248,9 @@ module Collavre
|
|
|
301
248
|
Rails.cache.exist?(cache_key)
|
|
302
249
|
end
|
|
303
250
|
|
|
304
|
-
def recently_escalated_creative?(creative)
|
|
305
|
-
cache_key = "stuck_detector:creative:#{creative.id}"
|
|
306
|
-
Rails.cache.exist?(cache_key)
|
|
307
|
-
end
|
|
308
|
-
|
|
309
251
|
def mark_escalated(item)
|
|
310
|
-
cache_key = case item.type
|
|
311
|
-
when :task then "stuck_detector:task:#{item.item.id}"
|
|
312
|
-
when :creative then "stuck_detector:creative:#{item.item.id}"
|
|
313
|
-
end
|
|
314
252
|
# Don't re-escalate for 1 hour
|
|
315
|
-
Rails.cache.write(
|
|
253
|
+
Rails.cache.write("stuck_detector:task:#{item.item.id}", true, expires_in: 1.hour)
|
|
316
254
|
end
|
|
317
255
|
|
|
318
256
|
def escalate_stuck_items(stuck_items, config)
|
|
@@ -339,9 +277,7 @@ module Collavre
|
|
|
339
277
|
end
|
|
340
278
|
|
|
341
279
|
# Optionally create a system comment
|
|
342
|
-
if config["create_system_comment"]
|
|
343
|
-
create_system_comment(stuck_item)
|
|
344
|
-
end
|
|
280
|
+
create_system_comment(stuck_item) if config["create_system_comment"]
|
|
345
281
|
|
|
346
282
|
true
|
|
347
283
|
rescue StandardError => e
|
|
@@ -352,13 +288,8 @@ module Collavre
|
|
|
352
288
|
def create_inbox_notification(owner, stuck_item)
|
|
353
289
|
message_key, message_params = build_message(stuck_item)
|
|
354
290
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
creative_id = stuck_item.item.trigger_event_payload&.dig("creative", "id")
|
|
358
|
-
Creative.find_by(id: creative_id)
|
|
359
|
-
when :creative
|
|
360
|
-
stuck_item.item
|
|
361
|
-
end
|
|
291
|
+
creative_id = stuck_item.item.trigger_event_payload&.dig("creative", "id")
|
|
292
|
+
creative = Creative.find_by(id: creative_id)
|
|
362
293
|
|
|
363
294
|
inbox_creative = Creative.inbox_for(owner)
|
|
364
295
|
system_topic = inbox_creative.system_topic(fallback_user: owner)
|
|
@@ -377,37 +308,19 @@ module Collavre
|
|
|
377
308
|
end
|
|
378
309
|
|
|
379
310
|
def build_message(stuck_item)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
minutes: minutes_stuck
|
|
391
|
-
}
|
|
392
|
-
]
|
|
393
|
-
when :creative
|
|
394
|
-
creative = stuck_item.item
|
|
395
|
-
hours_stuck = ((Time.current - stuck_item.stuck_since) / 3600).round
|
|
396
|
-
|
|
397
|
-
[
|
|
398
|
-
"collavre.stuck_detection.creative_stalled",
|
|
399
|
-
{
|
|
400
|
-
creative_title: creative.creative_snippet,
|
|
401
|
-
hours: hours_stuck,
|
|
402
|
-
progress: ((creative.progress || 0) * 100).round
|
|
403
|
-
}
|
|
404
|
-
]
|
|
405
|
-
end
|
|
311
|
+
task = stuck_item.item
|
|
312
|
+
minutes_stuck = ((Time.current - stuck_item.stuck_since) / 60).round
|
|
313
|
+
[
|
|
314
|
+
"collavre.stuck_detection.task_stuck",
|
|
315
|
+
{
|
|
316
|
+
task_name: task.name,
|
|
317
|
+
agent_name: task.agent&.display_name || "Unknown",
|
|
318
|
+
minutes: minutes_stuck
|
|
319
|
+
}
|
|
320
|
+
]
|
|
406
321
|
end
|
|
407
322
|
|
|
408
323
|
def create_system_comment(stuck_item)
|
|
409
|
-
return unless stuck_item.type == :task
|
|
410
|
-
|
|
411
324
|
task = stuck_item.item
|
|
412
325
|
creative_id = task.trigger_event_payload&.dig("creative", "id")
|
|
413
326
|
creative = Creative.find_by(id: creative_id)
|