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
|
@@ -1,51 +1,91 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class AiAgentJob < ApplicationJob
|
|
3
|
-
queue_as :
|
|
3
|
+
queue_as :ai_agents
|
|
4
4
|
|
|
5
5
|
# Allow resuming a task that was pending approval
|
|
6
6
|
def perform(agent_id_or_task, event_name = nil, context = nil)
|
|
7
7
|
if agent_id_or_task.is_a?(Task)
|
|
8
8
|
# Resume existing task
|
|
9
9
|
task = agent_id_or_task
|
|
10
|
-
|
|
10
|
+
if task.reload.status == "cancelled"
|
|
11
|
+
# A cancelled task can still be holding the topic slot. Promotion moves
|
|
12
|
+
# a waiter queued -> pending and enqueues this job, and the whole gap
|
|
13
|
+
# from that claim until this line is one where deleting the comment it
|
|
14
|
+
# answers cancels it (Comment#cancel_pending_tasks covers `pending`).
|
|
15
|
+
# Returning bare hands the slot to nobody, so the waiters behind it sit
|
|
16
|
+
# queued until orphan recovery. Same reason the fold's own cancelled
|
|
17
|
+
# branch below drains — this is the earlier door onto it.
|
|
18
|
+
if task.trigger_event_payload&.key?("topic")
|
|
19
|
+
Orchestration::AgentOrchestrator.dequeue_next_for_topic(task.topic_id, task.creative_id)
|
|
20
|
+
end
|
|
21
|
+
return
|
|
22
|
+
end
|
|
11
23
|
|
|
12
24
|
agent = task.agent
|
|
13
25
|
|
|
14
26
|
# Guard: same offline-session check as the agent_id branch below.
|
|
15
27
|
# Queued Claude Channel tasks resumed via Orchestration::AgentOrchestrator
|
|
16
28
|
# .dequeue_next_for_topic enter this branch as AiAgentJob.perform_later(task).
|
|
17
|
-
# If AgentChannel#unsubscribed
|
|
29
|
+
# If AgentChannel#unsubscribed removed the last presence row while the
|
|
18
30
|
# task was queued (WS drop without DELETE /agent/:id, e.g. SIGKILL or
|
|
19
31
|
# network blip past the reconnect grace), and another completion later
|
|
20
32
|
# drains the queue, this task would otherwise be promoted to running →
|
|
21
33
|
# delegated and broadcast to a clientless agent:user:<id> stream —
|
|
22
34
|
# held until stuck recovery.
|
|
23
|
-
if agent.claude_channel_agent? && agent.
|
|
35
|
+
if agent.claude_channel_agent? && !agent.claude_channel_online?
|
|
24
36
|
Rails.logger.info(
|
|
25
37
|
"[AiAgentJob] Skipping resumed Claude Channel task #{task.id}: " \
|
|
26
|
-
"session offline (
|
|
38
|
+
"session offline (no live presence)"
|
|
27
39
|
)
|
|
28
|
-
|
|
29
|
-
# no topic. If we only cancel the child and return, the parent workflow
|
|
30
|
-
# stays "running" with its current/pending creative state forever — no
|
|
31
|
-
# rescue path runs because we never raise. Mirror the StandardError
|
|
32
|
-
# rescue below: fail the child and notify the parent so the workflow
|
|
33
|
-
# transitions to "failed" with a failure_reason.
|
|
34
|
-
if task.parent_task_id.present?
|
|
35
|
-
task.update!(status: "failed")
|
|
36
|
-
Collavre::Comments::WorkflowExecutor.new(task.parent_task).fail_subtask!(
|
|
37
|
-
task,
|
|
38
|
-
error_message: "Claude Channel session offline before dispatch"
|
|
39
|
-
)
|
|
40
|
-
else
|
|
41
|
-
task.update!(status: "cancelled")
|
|
42
|
-
end
|
|
40
|
+
task.update!(status: "cancelled")
|
|
43
41
|
if task.trigger_event_payload&.key?("topic")
|
|
44
42
|
Orchestration::AgentOrchestrator.dequeue_next_for_topic(task.topic_id, task.creative_id)
|
|
45
43
|
end
|
|
46
44
|
return
|
|
47
45
|
end
|
|
48
46
|
|
|
47
|
+
# A claimed task holds the topic slot as `pending` from promotion until
|
|
48
|
+
# this line, and a comment arriving in that window parks a waiter that
|
|
49
|
+
# enqueue-time coalescing cannot fold into a non-`queued` sibling. Both
|
|
50
|
+
# turns are still un-started here, so this is the last place the fold
|
|
51
|
+
# can happen — and it must run before the assignment guard below, which
|
|
52
|
+
# reads the payload this may re-anchor.
|
|
53
|
+
Orchestration::AgentOrchestrator.coalesce_at_start!(task)
|
|
54
|
+
if task.reload.status == "cancelled"
|
|
55
|
+
# The fold's re-anchor found no comment left to answer. A `pending`
|
|
56
|
+
# task holds no ResourceTracker reservation yet (reserve! is below),
|
|
57
|
+
# but it does hold the topic slot — drain or the queue stalls.
|
|
58
|
+
Orchestration::AgentOrchestrator.dequeue_next_for_topic(task.topic_id, task.creative_id)
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Guard: an exclusive primary-agent assignment can also land after a task
|
|
63
|
+
# has already been cleared for execution, so checking at enqueue time is
|
|
64
|
+
# not sufficient. AgentOrchestrator.dequeue_next_for_topic revalidates and
|
|
65
|
+
# then enqueues asynchronously — a pin created in that window is missed —
|
|
66
|
+
# and approval resumption (Comments::ActionExecutor) re-enqueues a
|
|
67
|
+
# pending_approval task with no check at all, after a pause that can last
|
|
68
|
+
# as long as the human takes to approve. Re-check at the moment of
|
|
69
|
+
# execution so a demoted agent cannot answer in a topic that is now
|
|
70
|
+
# exclusively someone else's.
|
|
71
|
+
resumed_context = task.trigger_event_payload
|
|
72
|
+
if resumed_context&.key?("topic") &&
|
|
73
|
+
!Orchestration::Matcher.permits_assignment?(resumed_context, agent)
|
|
74
|
+
Rails.logger.info(
|
|
75
|
+
"[AiAgentJob] Cancelling resumed task #{task.id}: topic #{task.topic_id} " \
|
|
76
|
+
"is now assigned to another agent (agent=#{agent.id})"
|
|
77
|
+
)
|
|
78
|
+
task.update!(status: "cancelled")
|
|
79
|
+
# A pending_approval task kept its slot across the pause (the
|
|
80
|
+
# ApprovalPendingError rescue sets should_release = false), and this
|
|
81
|
+
# early return skips the ensure block that would give it back, so
|
|
82
|
+
# release explicitly. release! is idempotent, so a queued task that
|
|
83
|
+
# never reserved one is unaffected.
|
|
84
|
+
Orchestration::ResourceTracker.for(agent).release!(task.id)
|
|
85
|
+
Orchestration::AgentOrchestrator.dequeue_next_for_topic(task.topic_id, task.creative_id)
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
49
89
|
task.update!(status: "running")
|
|
50
90
|
else
|
|
51
91
|
# Create new task
|
|
@@ -53,17 +93,33 @@ module Collavre
|
|
|
53
93
|
|
|
54
94
|
# Guard: skip if the Claude Channel session has unregistered (or its WS
|
|
55
95
|
# dropped) during the window between Scheduler enqueue and this job
|
|
56
|
-
# firing. AgentsController#destroy / AgentChannel#unsubscribed
|
|
57
|
-
#
|
|
58
|
-
#
|
|
96
|
+
# firing. AgentsController#destroy / AgentChannel#unsubscribed remove
|
|
97
|
+
# the last presence row, so no live presence here means there is no MCP
|
|
98
|
+
# client to receive the dispatch. Without
|
|
59
99
|
# this guard, a :delayed (busy / rate-limited) enqueue from
|
|
60
100
|
# Scheduler#evaluate would materialize a fresh Task, flip it to
|
|
61
101
|
# "delegated", and broadcast to a clientless agent:user:<id> stream
|
|
62
102
|
# — holding the topic/agent slot until stuck recovery.
|
|
63
|
-
if agent.claude_channel_agent? && agent.
|
|
103
|
+
if agent.claude_channel_agent? && !agent.claude_channel_online?
|
|
64
104
|
Rails.logger.info(
|
|
65
105
|
"[AiAgentJob] Skipping Claude Channel job for agent #{agent.id}: " \
|
|
66
|
-
"session offline (
|
|
106
|
+
"session offline (no live presence, event=#{event_name})"
|
|
107
|
+
)
|
|
108
|
+
return
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Guard: an exclusive primary-agent assignment created (or moved) after
|
|
112
|
+
# this dispatch was scheduled must still bind it. A :delayed decision
|
|
113
|
+
# (agent busy / rate limited) sits in the job queue with its agent already
|
|
114
|
+
# chosen and is never re-matched, so without this a demoted agent speaks
|
|
115
|
+
# in a topic that now belongs to someone else. Unlike a queued waiter
|
|
116
|
+
# there is no Task yet to cancel, so cancelling on assignment change
|
|
117
|
+
# cannot cover this path — the check has to happen here.
|
|
118
|
+
if context && !Orchestration::Matcher.new(context).assignment_permits?(agent)
|
|
119
|
+
Rails.logger.info(
|
|
120
|
+
"[AiAgentJob] Skipping job for agent #{agent.id}: topic " \
|
|
121
|
+
"#{context.dig('topic', 'id')} is now assigned to another agent " \
|
|
122
|
+
"(event=#{event_name})"
|
|
67
123
|
)
|
|
68
124
|
return
|
|
69
125
|
end
|
|
@@ -78,25 +134,31 @@ module Collavre
|
|
|
78
134
|
return
|
|
79
135
|
end
|
|
80
136
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
from_ai = context&.dig("comment", "from_ai") == true
|
|
95
|
-
topic_id = context&.dig("topic", "id")
|
|
96
|
-
Orchestration::LoopBreaker.new(context).record_task(
|
|
97
|
-
creative_id, agent.id, topic_id: topic_id, triggered_by_user: !from_ai
|
|
137
|
+
# Guard: the same question the orchestrator asks before enqueueing, asked
|
|
138
|
+
# again where the answer is authoritative. This job can sit in the queue
|
|
139
|
+
# while the covering turn assembles its payload, so a dispatch that was
|
|
140
|
+
# not droppable at enqueue time can be droppable by the time it runs —
|
|
141
|
+
# and the enqueue door alone would leave that window open.
|
|
142
|
+
#
|
|
143
|
+
# As at the enqueue door, the drop only happens if the covering turn
|
|
144
|
+
# records it — otherwise this dispatch has nobody to restore it.
|
|
145
|
+
covering = Orchestration::DeliveryRecord.covering_task(agent, comment_id, context, event_name)
|
|
146
|
+
if covering && Orchestration::DeliveryRecord.claim_drop!(covering, comment_id)
|
|
147
|
+
Rails.logger.info(
|
|
148
|
+
"[AiAgentJob] Skipping dispatch: comment #{comment_id} was already delivered to " \
|
|
149
|
+
"agent #{agent.id} by in-flight task #{covering.id} (event=#{event_name})"
|
|
98
150
|
)
|
|
151
|
+
return
|
|
99
152
|
end
|
|
153
|
+
|
|
154
|
+
# Guard: the Scheduler's topic-concurrency check counts Task rows, but
|
|
155
|
+
# the row for an :immediate decision is only created *here*. A burst of
|
|
156
|
+
# comments dispatched before the first job runs therefore all see an
|
|
157
|
+
# empty topic and are all judged :immediate — several turns run at once
|
|
158
|
+
# in a topic limited to one. Re-check at the moment the row is created,
|
|
159
|
+
# where the answer is authoritative, and defer into the queue instead.
|
|
160
|
+
task = admit_or_defer!(agent, event_name, context)
|
|
161
|
+
return if task.nil?
|
|
100
162
|
end
|
|
101
163
|
|
|
102
164
|
# Reserve resources before starting work
|
|
@@ -138,60 +200,188 @@ module Collavre
|
|
|
138
200
|
task.reload
|
|
139
201
|
end
|
|
140
202
|
|
|
141
|
-
|
|
203
|
+
AiAgentService.new(task).call
|
|
142
204
|
|
|
143
205
|
# Claude Channel agents delegate via MCP; no immediate response expected
|
|
144
206
|
if is_claude_channel_agent
|
|
145
207
|
# Hold agent capacity until reply / cancel / stuck-recovery releases it.
|
|
146
208
|
should_release = false
|
|
147
|
-
# Workflow subtasks with empty responses should retry, then fail
|
|
148
|
-
elsif task.parent_task_id.present? && response_content.blank?
|
|
149
|
-
max_retries = 2
|
|
150
|
-
current_retry = task.retry_count || 0
|
|
151
|
-
|
|
152
|
-
if current_retry < max_retries
|
|
153
|
-
task.update!(retry_count: current_retry + 1, status: "pending")
|
|
154
|
-
Rails.logger.warn(
|
|
155
|
-
"[AiAgentJob] Workflow subtask #{task.id} returned empty response, " \
|
|
156
|
-
"retrying (#{current_retry + 1}/#{max_retries})"
|
|
157
|
-
)
|
|
158
|
-
AiAgentJob.set(wait: 5.seconds).perform_later(task)
|
|
159
|
-
else
|
|
160
|
-
task.update!(status: "failed")
|
|
161
|
-
Collavre::Comments::WorkflowExecutor.new(task.parent_task).fail_subtask!(
|
|
162
|
-
task, error_message: "Agent returned empty response after #{max_retries} retries"
|
|
163
|
-
)
|
|
164
|
-
end
|
|
165
209
|
else
|
|
166
|
-
|
|
167
|
-
# Advance workflow (release happens in ensure block)
|
|
168
|
-
if task.parent_task_id.present?
|
|
169
|
-
Collavre::Comments::WorkflowExecutor.new(task.parent_task).complete_subtask!(task)
|
|
170
|
-
end
|
|
210
|
+
transition_running_task!(task, status: "done")
|
|
171
211
|
end
|
|
172
212
|
rescue ApprovalPendingError
|
|
173
213
|
# Task status already set to pending_approval by AiAgentService
|
|
174
214
|
# Don't release resources yet - task will resume
|
|
175
215
|
should_release = false
|
|
176
216
|
Rails.logger.info("AiAgentJob paused for task #{task.id}: awaiting tool approval")
|
|
217
|
+
rescue TurnDeadlineError
|
|
218
|
+
# AgentLifecycleManager already wrote the terminal status inside this
|
|
219
|
+
# call stack. Restore/settle of dropped dispatches needs nothing extra:
|
|
220
|
+
# fail_while_worker_settles! marked the row, and the ensure below
|
|
221
|
+
# settles it.
|
|
222
|
+
Rails.logger.info("AiAgentJob turn deadline exceeded for task #{task.id}")
|
|
177
223
|
rescue CancelledError
|
|
178
224
|
# Task status already set to "cancelled" by Comment callback
|
|
179
225
|
Rails.logger.info("AiAgentJob cancelled for task #{task.id}: trigger message deleted")
|
|
226
|
+
# Where a turn stopped mid-answer settles. The cancel was committed in
|
|
227
|
+
# another process while this worker was inside the provider call, so
|
|
228
|
+
# Task's status callback declined to decide
|
|
229
|
+
# (Task#ended_while_worker_settles?) and this is the first moment either
|
|
230
|
+
# answer exists: AiAgentService's ensure has recorded whether the
|
|
231
|
+
# payload got there on the way up to here.
|
|
232
|
+
# Reloaded because the status was written to the row by somebody else.
|
|
233
|
+
Orchestration::DeliveryRecord.restore_if_undelivered!(task.reload)
|
|
180
234
|
rescue StandardError => e
|
|
181
235
|
task.update!(status: "failed")
|
|
182
|
-
# Fail workflow if this is a sub-task
|
|
183
|
-
if task.parent_task_id.present?
|
|
184
|
-
Collavre::Comments::WorkflowExecutor.new(task.parent_task).fail_subtask!(task, error_message: e.message)
|
|
185
|
-
end
|
|
186
236
|
Rails.logger.error("AiAgentJob failed for task #{task.id}: #{e.message}")
|
|
187
237
|
raise e
|
|
188
238
|
ensure
|
|
189
239
|
# Guarantee resource release for all paths except pending_approval
|
|
190
240
|
tracker.release!(resource_id, tokens_used: 0) if should_release && tracker && resource_id
|
|
191
|
-
|
|
192
|
-
|
|
241
|
+
settled_task = task&.reload
|
|
242
|
+
if settled_task &&
|
|
243
|
+
Orchestration::DeliveryRecord.worker_settling?(settled_task.trigger_event_payload)
|
|
244
|
+
# StuckDetector failed this row while this worker was still in the
|
|
245
|
+
# provider call. The worker is out now, so remove the deferral and ask
|
|
246
|
+
# the restore question from the handoff evidence AiAgentService wrote.
|
|
247
|
+
Orchestration::DeliveryRecord.settle_worker!(settled_task)
|
|
248
|
+
Orchestration::DeliveryRecord.restore_if_undelivered!(settled_task.reload)
|
|
249
|
+
end
|
|
250
|
+
if settled_task&.trigger_event_payload&.key?("topic") &&
|
|
251
|
+
%w[done failed cancelled escalated].include?(settled_task.status)
|
|
252
|
+
Orchestration::AgentOrchestrator.dequeue_next_for_topic(settled_task.topic_id, settled_task.creative_id)
|
|
193
253
|
end
|
|
194
254
|
end
|
|
195
255
|
end
|
|
256
|
+
|
|
257
|
+
private
|
|
258
|
+
|
|
259
|
+
# A terminal status can be written by Stop/StuckDetector after the service's
|
|
260
|
+
# last lifecycle checkpoint but before this job records its outcome. Lock
|
|
261
|
+
# and re-check the row so normal completion/retry cannot overwrite that
|
|
262
|
+
# external winner.
|
|
263
|
+
def transition_running_task!(task, **attributes)
|
|
264
|
+
task.with_lock do
|
|
265
|
+
raise CancelledError unless task.status == "running"
|
|
266
|
+
|
|
267
|
+
task.update!(attributes)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Create this dispatch's Task row, either admitted (`running`) or parked as
|
|
272
|
+
# a `queued` waiter when the topic's concurrency slot is already taken.
|
|
273
|
+
#
|
|
274
|
+
# Returns the admitted task, or nil when the dispatch was deferred.
|
|
275
|
+
def admit_or_defer!(agent, event_name, context)
|
|
276
|
+
attrs = {
|
|
277
|
+
name: "Response to #{event_name}",
|
|
278
|
+
trigger_event_name: event_name,
|
|
279
|
+
trigger_event_payload: context,
|
|
280
|
+
agent: agent,
|
|
281
|
+
topic_id: context&.dig("topic", "id"),
|
|
282
|
+
creative_id: context&.dig("creative", "id")
|
|
283
|
+
}
|
|
284
|
+
unless topic_admission_scoped?(context)
|
|
285
|
+
return Task.create!(attrs.merge(status: "running")).tap { record_loop_breaker_turn(agent, context) }
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
task, admitted, current_context = nil, false, false
|
|
289
|
+
# A queued row here is a waiter, and what speaks for it is settled now, with
|
|
290
|
+
# the row — not later, from whichever notices happen to be visible. Its
|
|
291
|
+
# notice is posted in a separate transaction below, so between the two this
|
|
292
|
+
# waiter is queued with nothing naming it, and a shared notice deleted in
|
|
293
|
+
# that window would read an opted-out waiter as one of its own and cancel a
|
|
294
|
+
# turn that was only deferred.
|
|
295
|
+
# Counting occupants and claiming the slot must be ONE step. Two workers
|
|
296
|
+
# starting within the same millisecond — the exact burst this job defends
|
|
297
|
+
# against — otherwise both read an occupancy below the limit before either
|
|
298
|
+
# inserts its `running` row, and the re-check reproduces the very TOCTOU
|
|
299
|
+
# race it exists to close. Serialize admission on the topic row: every
|
|
300
|
+
# admission for a topic takes the same lock, so the loser reads the
|
|
301
|
+
# winner's committed row.
|
|
302
|
+
Task.transaction do
|
|
303
|
+
next unless Orchestration::TopicSlot.lock_matches_context?(attrs[:topic_id], attrs[:creative_id])
|
|
304
|
+
|
|
305
|
+
current_context = true
|
|
306
|
+
admitted = Orchestration::TopicSlot.available_for?(agent.id, attrs[:topic_id], attrs[:creative_id], context)
|
|
307
|
+
task = Task.create!(attrs.merge(
|
|
308
|
+
status: admitted ? "running" : "queued",
|
|
309
|
+
# Left nil on an admitted row: it is not waiting, so no notice speaks
|
|
310
|
+
# for it and there is nothing for a stop control to represent.
|
|
311
|
+
waiting_notice_scope: admitted ? nil : waiter_notice_scope(agent, context)
|
|
312
|
+
))
|
|
313
|
+
end
|
|
314
|
+
return unless current_context
|
|
315
|
+
|
|
316
|
+
park_deferred_waiter(task, agent, event_name, context, attrs[:topic_id], attrs[:creative_id]) unless admitted
|
|
317
|
+
# Count where the row is created, before an admitted turn starts and after
|
|
318
|
+
# a deferred turn is parked. A stale context creates no row and no count.
|
|
319
|
+
record_loop_breaker_turn(agent, context)
|
|
320
|
+
task if admitted
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Record a created turn for the loop breaker's creative-retry count.
|
|
324
|
+
# User-initiated messages are not loop material — a person typing twice is
|
|
325
|
+
# not a runaway — and LoopBreaker skips those itself.
|
|
326
|
+
def record_loop_breaker_turn(agent, context)
|
|
327
|
+
creative_id = context&.dig("creative", "id")
|
|
328
|
+
return unless creative_id
|
|
329
|
+
|
|
330
|
+
Orchestration::LoopBreaker.new(context).record_task(
|
|
331
|
+
creative_id, agent.id,
|
|
332
|
+
topic_id: context&.dig("topic", "id"),
|
|
333
|
+
triggered_by_user: context&.dig("comment", "from_ai") != true
|
|
334
|
+
)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Which notice will speak for a waiter parked by this dispatch: the topic's
|
|
338
|
+
# shared one when this agent coalesces, its own per-deferral one when it has
|
|
339
|
+
# opted out. Resolved once, at the moment the waiter is created, so the fold,
|
|
340
|
+
# the notice and the shared notice's stop button all read one answer.
|
|
341
|
+
def waiter_notice_scope(agent, context)
|
|
342
|
+
if Orchestration::PolicyResolver.new(context).coalesce_pending_tasks_for?(agent)
|
|
343
|
+
Comment::WAITING_NOTICE_TOPIC
|
|
344
|
+
else
|
|
345
|
+
Comment::WAITING_NOTICE_TASK
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Is this dispatch subject to the topic concurrency limit at all?
|
|
350
|
+
# Topic-less dispatches are not.
|
|
351
|
+
def topic_admission_scoped?(context)
|
|
352
|
+
context.is_a?(Hash) && context.key?("topic")
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Finish parking a waiter created by #admit_or_defer!: fold it together with
|
|
356
|
+
# any waiters already parked for the same agent/topic/creative, and surface
|
|
357
|
+
# one "⏳" notice for the topic.
|
|
358
|
+
def park_deferred_waiter(waiter, agent, event_name, context, topic_id, creative_id)
|
|
359
|
+
# Read off the waiter rather than resolved again: folding and the notice are
|
|
360
|
+
# the same policy answer, and asking twice is how they come to disagree
|
|
361
|
+
# about a wait whose policy changed in between.
|
|
362
|
+
if waiter.waiting_notice_scope == Comment::WAITING_NOTICE_TOPIC
|
|
363
|
+
Orchestration::TaskCoalescer.coalesce!(waiter)
|
|
364
|
+
end
|
|
365
|
+
Orchestration::AgentOrchestrator.post_topic_concurrency_notice(
|
|
366
|
+
creative_id, topic_id, context, agent: agent, waiter: waiter
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
Rails.logger.info(
|
|
370
|
+
"[AiAgentJob] Deferred agent #{agent.id} into topic #{topic_id} queue as task " \
|
|
371
|
+
"#{waiter.id}: slot already occupied (event=#{event_name})"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# A holder may have finished between the admission check and this point,
|
|
375
|
+
# leaving a waiter with nobody left to drain it (its
|
|
376
|
+
# dequeue_next_for_topic already ran). Drain unconditionally: this
|
|
377
|
+
# deferral is not proof the topic is full — with topic_max > 1 an agent
|
|
378
|
+
# defers because *it* is busy while a slot sits free for someone else, and
|
|
379
|
+
# gating on "no occupants at all" would strand that waiter until the
|
|
380
|
+
# unrelated holder finishes. dequeue_next_for_topic re-checks capacity and
|
|
381
|
+
# eligibility under the admission lock, so an over-eager call is a no-op.
|
|
382
|
+
Orchestration::AgentOrchestrator.dequeue_next_for_topic(topic_id, creative_id)
|
|
383
|
+
|
|
384
|
+
nil
|
|
385
|
+
end
|
|
196
386
|
end
|
|
197
387
|
end
|
|
@@ -5,15 +5,15 @@ module Collavre
|
|
|
5
5
|
# session whose WebSocket dropped without DELETE /api/v1/agent/:id.
|
|
6
6
|
#
|
|
7
7
|
# AgentChannel#unsubscribed makes the agent unroutable, but a task already
|
|
8
|
-
# in "delegated" still holds its ResourceTracker slot
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# in "delegated" still holds its ResourceTracker slot. The dispatch was
|
|
9
|
+
# broadcast to a now-dead stream, so no client remains to call /reply —
|
|
10
|
+
# without this job
|
|
11
11
|
# the slot stays held until StuckDetectorJob times out (minutes to hours).
|
|
12
12
|
#
|
|
13
13
|
# The grace delay lets transient WS blips self-heal: if the client
|
|
14
14
|
# reconnects before the job fires, AgentChannel#subscribe_to_agent_stream
|
|
15
|
-
#
|
|
16
|
-
#
|
|
15
|
+
# creates a fresh live presence row and writes a fresh subscription token.
|
|
16
|
+
# The job's online-state recheck then no-ops.
|
|
17
17
|
class CancelOfflineDelegatedTasksJob < ApplicationJob
|
|
18
18
|
queue_as :default
|
|
19
19
|
|
|
@@ -31,10 +31,6 @@ module Collavre
|
|
|
31
31
|
# when a sibling remains; mirrors the destroy path's cancel_tasks_for_topic.
|
|
32
32
|
return cancel_dropped_session_tasks(agent, session_id) if session_id.present?
|
|
33
33
|
|
|
34
|
-
# Agent came back online during the grace window — the same MCP
|
|
35
|
-
# session (or a new one) is subscribed and can answer /reply.
|
|
36
|
-
return if agent.routing_expression.present?
|
|
37
|
-
|
|
38
34
|
# A session (reconnect or a still-live sibling sharing this agent) holds
|
|
39
35
|
# a LIVE presence row — the agent is online, its delegated work is still
|
|
40
36
|
# owned. Presence is the authority now that one agent fans out to many
|
|
@@ -87,21 +83,11 @@ module Collavre
|
|
|
87
83
|
|
|
88
84
|
tracker = Orchestration::ResourceTracker.for(agent)
|
|
89
85
|
tasks.find_each do |task|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
tracker.release!(task.id) if was_running
|
|
86
|
+
previous_status = task.cancel_if_active!(statuses: %w[queued pending running])
|
|
87
|
+
next unless previous_status
|
|
93
88
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
begin
|
|
97
|
-
Collavre::Comments::WorkflowExecutor.new(task.parent_task).fail_subtask!(
|
|
98
|
-
task, error_message: "Claude Channel session lost connection before dispatch"
|
|
99
|
-
)
|
|
100
|
-
rescue StandardError => e
|
|
101
|
-
Rails.logger.error(
|
|
102
|
-
"[CancelOfflineDelegatedTasksJob] fail_subtask! failed for queued task #{task.id}: #{e.message}"
|
|
103
|
-
)
|
|
104
|
-
end
|
|
89
|
+
was_running = previous_status == "running"
|
|
90
|
+
tracker.release!(task.id) if was_running
|
|
105
91
|
end
|
|
106
92
|
end
|
|
107
93
|
|
|
@@ -110,20 +96,9 @@ module Collavre
|
|
|
110
96
|
|
|
111
97
|
tracker = Orchestration::ResourceTracker.for(agent)
|
|
112
98
|
tasks.find_each do |task|
|
|
113
|
-
task.
|
|
114
|
-
tracker.release!(task.id)
|
|
99
|
+
next unless task.cancel_if_active!(statuses: %w[delegated])
|
|
115
100
|
|
|
116
|
-
|
|
117
|
-
begin
|
|
118
|
-
Collavre::Comments::WorkflowExecutor.new(task.parent_task).fail_subtask!(
|
|
119
|
-
task, error_message: "Claude Channel session lost connection before reply"
|
|
120
|
-
)
|
|
121
|
-
rescue StandardError => e
|
|
122
|
-
Rails.logger.error(
|
|
123
|
-
"[CancelOfflineDelegatedTasksJob] fail_subtask! failed for task #{task.id}: #{e.message}"
|
|
124
|
-
)
|
|
125
|
-
end
|
|
126
|
-
end
|
|
101
|
+
tracker.release!(task.id)
|
|
127
102
|
|
|
128
103
|
if task.topic_id.present?
|
|
129
104
|
Orchestration::AgentOrchestrator.dequeue_next_for_topic(task.topic_id, task.creative_id)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
# Recomputes and rebroadcasts the per-participant comment badges of a creative.
|
|
5
|
+
#
|
|
6
|
+
# The arithmetic is O(comment history) x O(participants): a total count, an
|
|
7
|
+
# unread count per distinct read watermark, and a private count per user. Run
|
|
8
|
+
# inline from Comment's after_commit it made writing a message cost more the
|
|
9
|
+
# longer the conversation was — and an inbox notification wrote a second
|
|
10
|
+
# comment into an inbox creative, paying the same price again over that
|
|
11
|
+
# inbox's own history.
|
|
12
|
+
#
|
|
13
|
+
# Badges are eventually-consistent UI: the recipient already got the comment
|
|
14
|
+
# itself over its own Turbo stream, so recounting a beat later is invisible.
|
|
15
|
+
class CommentBadgesBroadcastJob < ApplicationJob
|
|
16
|
+
queue_as :default
|
|
17
|
+
discard_on ActiveJob::DeserializationError
|
|
18
|
+
|
|
19
|
+
def perform(creative_id)
|
|
20
|
+
creative = Creative.find_by(id: creative_id)
|
|
21
|
+
# Deleting a creative destroys its comments, so the enqueued job can
|
|
22
|
+
# outlive its subject. Nothing to recount, and nobody to tell.
|
|
23
|
+
return unless creative
|
|
24
|
+
|
|
25
|
+
Comment.broadcast_badges(creative)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
class CommentLinkPreviewJob < ApplicationJob
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
retry_on StandardError, wait: :polynomially_longer, attempts: 3
|
|
8
|
+
discard_on ActiveRecord::RecordNotFound
|
|
9
|
+
|
|
10
|
+
def perform(comment_id, expected_content, expected_revision)
|
|
11
|
+
comment = Comment.find(comment_id)
|
|
12
|
+
return unless comment.content == expected_content
|
|
13
|
+
return unless comment.notification_revision == expected_revision
|
|
14
|
+
|
|
15
|
+
formatted_content = CommentLinkFormatter.new(expected_content).format
|
|
16
|
+
return if formatted_content == expected_content
|
|
17
|
+
|
|
18
|
+
Comment.transaction do
|
|
19
|
+
comment = Comment.lock.find(comment_id)
|
|
20
|
+
return unless comment.content == expected_content
|
|
21
|
+
return unless comment.notification_revision == expected_revision
|
|
22
|
+
|
|
23
|
+
# This is a derived representation of the same logical comment revision.
|
|
24
|
+
# Save normally so every Comment after_update_commit integration observes
|
|
25
|
+
# the formatted content, while suppressing only notification revision
|
|
26
|
+
# advancement and another preview job.
|
|
27
|
+
comment.skip_link_preview = true
|
|
28
|
+
comment.skip_notification_revision = true
|
|
29
|
+
comment.update!(content: formatted_content)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
class CommentNotificationJob < ApplicationJob
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
retry_on StandardError, wait: :polynomially_longer, attempts: 3
|
|
8
|
+
discard_on ActiveRecord::RecordNotFound
|
|
9
|
+
|
|
10
|
+
def perform(comment_id, kind, event)
|
|
11
|
+
Comment.find(comment_id).deliver_notifications(kind, event)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
class CommentPushDeliverySweepJob < ApplicationJob
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
def perform
|
|
8
|
+
CommentNotificationDelivery.ready_for_push.find_each do |delivery|
|
|
9
|
+
delivery.enqueue_push!
|
|
10
|
+
rescue StandardError => e
|
|
11
|
+
Rails.logger.error(
|
|
12
|
+
"[CommentPushDeliverySweepJob] Failed delivery #{delivery.id}: #{e.class}: #{e.message}"
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|