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
|
@@ -8,6 +8,84 @@ module Collavre
|
|
|
8
8
|
# matches the same prefix to remove them once the waiter is dequeued.
|
|
9
9
|
WAITING_NOTICE_PREFIX = "⏳"
|
|
10
10
|
|
|
11
|
+
# What a waiting notice speaks for, written by the door that posted it.
|
|
12
|
+
# "topic" is the deduplicated notice coalescing allows a topic exactly one
|
|
13
|
+
# of; "task" is the per-deferral notice the opt-out posts, which names its
|
|
14
|
+
# waiter in waiting_notice_task_id. nil is a notice from before this was
|
|
15
|
+
# recorded — see #cancel_queued_tasks_for_waiting_notice.
|
|
16
|
+
WAITING_NOTICE_TOPIC = "topic"
|
|
17
|
+
WAITING_NOTICE_TASK = "task"
|
|
18
|
+
|
|
19
|
+
# Take down the per-deferral notice that speaks for this waiter, if it had
|
|
20
|
+
# one.
|
|
21
|
+
#
|
|
22
|
+
# A "task" notice is on screen for exactly as long as its waiter is queued:
|
|
23
|
+
# it names that waiter in waiting_notice_task_id and
|
|
24
|
+
# #cancel_queued_tasks_for_waiting_notice stops that waiter and nothing
|
|
25
|
+
# else. So the moment the waiter leaves the queue the notice is a stop
|
|
26
|
+
# button for work that cannot be stopped — and no sweep will collect it,
|
|
27
|
+
# since the drained sweep only runs when the topic queue empties and the
|
|
28
|
+
# waiter itself will never be promoted again.
|
|
29
|
+
#
|
|
30
|
+
# It therefore comes down wherever the waiter leaves, which is why this
|
|
31
|
+
# lives here beside the columns rather than in one of those callers: the
|
|
32
|
+
# promotion (AgentOrchestrator.cleanup_waiter_notice!) and the fold
|
|
33
|
+
# (Orchestration::TaskCoalescer) are two doors onto the same rule, and a
|
|
34
|
+
# third would otherwise write its own copy or forget.
|
|
35
|
+
#
|
|
36
|
+
# The destroy is marked as a system removal: the waiter is being promoted or
|
|
37
|
+
# folded, not abandoned by the user, so the cancellation callback must not
|
|
38
|
+
# fire on top of it.
|
|
39
|
+
def self.remove_waiter_notices!(creative_id:, topic_id:, task_ids:)
|
|
40
|
+
task_ids = Array(task_ids).compact
|
|
41
|
+
return if task_ids.empty?
|
|
42
|
+
|
|
43
|
+
where(creative_id: creative_id, topic_id: topic_id, user_id: nil,
|
|
44
|
+
waiting_notice_scope: WAITING_NOTICE_TASK,
|
|
45
|
+
waiting_notice_task_id: task_ids).find_each do |notice|
|
|
46
|
+
notice.suppress_waiter_cancellation = true
|
|
47
|
+
notice.destroy
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Take down every "⏳" defer notice in the topic that no longer speaks for a
|
|
52
|
+
# queued waiter.
|
|
53
|
+
#
|
|
54
|
+
# Asked per notice, not per topic. "Is anything queued here?" is a topic-wide
|
|
55
|
+
# question and no notice asks it: a "task" notice speaks for the one waiter
|
|
56
|
+
# it names, and a shared one for every queued waiter *except* those a
|
|
57
|
+
# surviving "task" notice claims — see #represented_queued_waiters, which
|
|
58
|
+
# this and the stop button both go through so the two cannot drift. A topic
|
|
59
|
+
# can therefore hold waiters while a notice standing in it represents none
|
|
60
|
+
# of them, and that notice is a "⏳" line whose stop button selects nothing.
|
|
61
|
+
#
|
|
62
|
+
# Nothing else collects it. A cancelled waiter is never promoted, so the
|
|
63
|
+
# promotion's cleanup never runs for it, and a promotion that leaves only
|
|
64
|
+
# claimed waiters behind is exactly the case a topic-wide drain check reads
|
|
65
|
+
# as "still busy".
|
|
66
|
+
#
|
|
67
|
+
# Scoped to topic_concurrency_defer notices. A :delayed notice shares the
|
|
68
|
+
# prefix but explains a rate-limited or busy dispatch that is still going to
|
|
69
|
+
# run, so it speaks for no waiter by design and is not this sweep's to take.
|
|
70
|
+
#
|
|
71
|
+
# Asked under the same lock the notice doors take, so "nobody is queued" is
|
|
72
|
+
# a deferral's own before-or-after rather than a guess: it either commits
|
|
73
|
+
# its waiter before this reads, and keeps its notice, or after, and posts
|
|
74
|
+
# one of its own.
|
|
75
|
+
def self.remove_stranded_waiting_notices!(creative_id:, topic_id:)
|
|
76
|
+
transaction do
|
|
77
|
+
Collavre::Orchestration::TopicSlot.lock!(topic_id, creative_id)
|
|
78
|
+
|
|
79
|
+
where(creative_id: creative_id, topic_id: topic_id, user_id: nil,
|
|
80
|
+
topic_concurrency_defer: true).find_each do |notice|
|
|
81
|
+
next if notice.represented_queued_waiters.any?
|
|
82
|
+
|
|
83
|
+
notice.suppress_waiter_cancellation = true
|
|
84
|
+
notice.destroy
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
11
89
|
# Use non-namespaced partial path for backward compatibility
|
|
12
90
|
def to_partial_path
|
|
13
91
|
"comments/comment"
|
|
@@ -83,7 +161,6 @@ module Collavre
|
|
|
83
161
|
has_many :comment_reactions, class_name: "Collavre::CommentReaction", dependent: :destroy
|
|
84
162
|
has_many :comment_versions, class_name: "Collavre::CommentVersion", dependent: :destroy
|
|
85
163
|
has_many :review_versions, class_name: "Collavre::CommentVersion", foreign_key: :review_comment_id, dependent: :nullify
|
|
86
|
-
has_many :inbox_items, class_name: "Collavre::InboxItem", dependent: :nullify
|
|
87
164
|
has_many :quoting_comments, class_name: "Collavre::Comment", foreign_key: :quoted_comment_id, dependent: :destroy
|
|
88
165
|
has_one :snapshot_as_result, class_name: "Collavre::CommentSnapshot", foreign_key: :result_comment_id, dependent: :nullify
|
|
89
166
|
belongs_to :selected_version, class_name: "Collavre::CommentVersion", optional: true
|
|
@@ -97,6 +174,8 @@ module Collavre
|
|
|
97
174
|
|
|
98
175
|
attribute :skip_default_user, :boolean, default: false
|
|
99
176
|
attribute :skip_dispatch, :boolean, default: false
|
|
177
|
+
attribute :skip_link_preview, :boolean, default: false
|
|
178
|
+
attribute :skip_notification_revision, :boolean, default: false
|
|
100
179
|
# Set by AgentOrchestrator.cleanup_waiting_notices! so destroying a notice as
|
|
101
180
|
# part of *promoting* a waiter does not run the user-delete cancel cascade
|
|
102
181
|
# (which would cancel other still-queued waiters in the same topic).
|
|
@@ -104,8 +183,8 @@ module Collavre
|
|
|
104
183
|
|
|
105
184
|
before_validation :use_origin_creative
|
|
106
185
|
before_validation :assign_default_user, on: :create
|
|
107
|
-
|
|
108
|
-
|
|
186
|
+
include TopicMembership
|
|
187
|
+
after_commit :enqueue_link_preview, on: [ :create, :update ], if: :link_preview_enqueue_required?
|
|
109
188
|
after_create_commit :dispatch_to_orchestration
|
|
110
189
|
after_create_commit :resume_trigger_loop_if_awaiting
|
|
111
190
|
|
|
@@ -123,6 +202,24 @@ module Collavre
|
|
|
123
202
|
quoted_comment_id.present? && !review_type_question?
|
|
124
203
|
end
|
|
125
204
|
|
|
205
|
+
# Which of `ids` are review requests, in one query. The orchestration paths
|
|
206
|
+
# that decide whether a trigger may be folded away or moved forward hold
|
|
207
|
+
# comment *ids*, not records, and run over a whole burst at once.
|
|
208
|
+
#
|
|
209
|
+
# `review_type: [nil, review]` rather than `where.not(question)`: the column
|
|
210
|
+
# is NULL for an ordinary review (only /compress-style questions set it), and
|
|
211
|
+
# SQL's `review_type != 1` drops NULL rows — which is every real review.
|
|
212
|
+
def self.review_message_ids(ids)
|
|
213
|
+
ids = Array(ids).compact.map(&:to_i).uniq
|
|
214
|
+
return [] if ids.empty?
|
|
215
|
+
|
|
216
|
+
review_messages.where(id: ids).pluck(:id)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
scope :review_messages, -> {
|
|
220
|
+
where.not(quoted_comment_id: nil).where(review_type: [ nil, review_types[:review] ])
|
|
221
|
+
}
|
|
222
|
+
|
|
126
223
|
# public for db migration
|
|
127
224
|
def creative_snippet
|
|
128
225
|
creative.creative_snippet
|
|
@@ -160,6 +257,8 @@ module Collavre
|
|
|
160
257
|
end
|
|
161
258
|
|
|
162
259
|
def cancel_pending_tasks
|
|
260
|
+
stranded_scopes = []
|
|
261
|
+
|
|
163
262
|
# Cancel tasks triggered by this comment (no creative_id scoping —
|
|
164
263
|
# CommentMoveService can change comment.creative_id without updating
|
|
165
264
|
# existing tasks, so scoping would miss moved-comment tasks).
|
|
@@ -169,9 +268,35 @@ module Collavre
|
|
|
169
268
|
Task.where(status: %w[pending running queued delegated]).find_each do |task|
|
|
170
269
|
next unless task.trigger_event_payload&.dig("comment", "id") == id
|
|
171
270
|
|
|
271
|
+
# An un-started task can be the survivor of a coalesced burst, answering
|
|
272
|
+
# several comments at once. Cancelling it because its anchor was deleted
|
|
273
|
+
# would throw away the absorbed comments too — they have no task of their
|
|
274
|
+
# own left (TaskCoalescer cancelled those) and no other delivery path
|
|
275
|
+
# (session-backed agents receive only the trigger). Re-anchor onto the
|
|
276
|
+
# newest surviving merged comment instead; only a task with nothing left
|
|
277
|
+
# to say is cancelled.
|
|
278
|
+
next if reanchor_coalesced_task(task)
|
|
279
|
+
|
|
172
280
|
was_delegated = task.status == "delegated"
|
|
173
281
|
task.update!(status: "cancelled")
|
|
174
282
|
|
|
283
|
+
# A waiter cancelled here leaves the queue without ever being promoted,
|
|
284
|
+
# exactly as a folded one does — so the notice that spoke for it is left
|
|
285
|
+
# naming a task no longer queued, and its stop button cancels nothing.
|
|
286
|
+
# This door needs no policy change at all: the opt-out posts the notice,
|
|
287
|
+
# deleting the prompt cancels the waiter, and a sibling still parked
|
|
288
|
+
# keeps the drained sweep from ever running.
|
|
289
|
+
Comment.remove_waiter_notices!(
|
|
290
|
+
creative_id: task.creative_id, topic_id: task.topic_id, task_ids: task.id
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# …and with coalescing on there is no per-deferral notice to take down:
|
|
294
|
+
# the waiter's signal is the topic's *shared* one, which the call above
|
|
295
|
+
# deliberately leaves alone. If this cancellation was the last thing that
|
|
296
|
+
# notice spoke for, nothing else will ever collect it. Swept after the
|
|
297
|
+
# loop, once every task this deletion cancels has left the queue.
|
|
298
|
+
stranded_scopes << [ task.creative_id, task.topic_id ]
|
|
299
|
+
|
|
175
300
|
# Delegated tasks live past their job: the AiAgentJob already returned,
|
|
176
301
|
# holding the agent slot under task.id and counting against the per-topic
|
|
177
302
|
# serializer. Mirror the cancel path used elsewhere to free both.
|
|
@@ -179,17 +304,6 @@ module Collavre
|
|
|
179
304
|
if task.agent
|
|
180
305
|
Collavre::Orchestration::ResourceTracker.for(task.agent).release!(task.id)
|
|
181
306
|
end
|
|
182
|
-
if task.parent_task_id.present?
|
|
183
|
-
begin
|
|
184
|
-
Collavre::Comments::WorkflowExecutor.new(task.parent_task).fail_subtask!(
|
|
185
|
-
task, error_message: "Triggering comment was deleted"
|
|
186
|
-
)
|
|
187
|
-
rescue StandardError => e
|
|
188
|
-
Rails.logger.error(
|
|
189
|
-
"[Comment#cancel_pending_tasks] fail_subtask! failed for task #{task.id}: #{e.message}"
|
|
190
|
-
)
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
307
|
Collavre::Orchestration::AgentOrchestrator.dequeue_next_for_topic(task.topic_id, task.creative_id)
|
|
194
308
|
end
|
|
195
309
|
|
|
@@ -202,16 +316,215 @@ module Collavre
|
|
|
202
316
|
# waiter is being advanced, not abandoned, and cancelling other still-queued
|
|
203
317
|
# waiters would drop their work (multi-slot orphan recovery must not cancel
|
|
204
318
|
# the rest).
|
|
319
|
+
stranded_scopes.uniq.each do |cancelled_creative_id, cancelled_topic_id|
|
|
320
|
+
next unless cancelled_creative_id
|
|
321
|
+
|
|
322
|
+
Comment.remove_stranded_waiting_notices!(
|
|
323
|
+
creative_id: cancelled_creative_id, topic_id: cancelled_topic_id
|
|
324
|
+
)
|
|
325
|
+
end
|
|
326
|
+
|
|
205
327
|
if waiting_notice? && topic_concurrency_defer? && !suppress_waiter_cancellation
|
|
206
328
|
cancel_queued_tasks_for_waiting_notice
|
|
207
329
|
end
|
|
208
330
|
end
|
|
209
331
|
|
|
332
|
+
# Move a coalesced task off this (deleted) comment and onto the newest of
|
|
333
|
+
# the comments it had absorbed. Returns true when the task was rescued, so
|
|
334
|
+
# the caller skips cancellation.
|
|
335
|
+
#
|
|
336
|
+
# Only un-started tasks: a `running`/`delegated` task has already been handed
|
|
337
|
+
# its payload, so re-anchoring changes nothing and deleting the prompt must
|
|
338
|
+
# still stop the turn.
|
|
339
|
+
def reanchor_coalesced_task(task)
|
|
340
|
+
return false unless %w[queued pending].include?(task.status)
|
|
341
|
+
|
|
342
|
+
Task.transaction { reanchor_locked_task(task) }
|
|
343
|
+
rescue ActiveRecord::RecordNotFound
|
|
344
|
+
# The task went away between the scan and the lock (a cascading delete).
|
|
345
|
+
# Nothing to rescue, and nothing left to cancel either.
|
|
346
|
+
false
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# The lock body. `task` is the object cancel_pending_tasks' scan loaded, and
|
|
350
|
+
# everything derived here has to come from the row as it stands *now*:
|
|
351
|
+
# TaskCoalescer folds a sibling into this same task under a lock this path
|
|
352
|
+
# never took, so a fold committing between that scan and the write below
|
|
353
|
+
# would be overwritten by the pre-fold payload — and the absorbed sibling is
|
|
354
|
+
# already cancelled, so its comment has no task left to answer it.
|
|
355
|
+
#
|
|
356
|
+
# lock! reloads in place rather than into a second object: the caller keeps
|
|
357
|
+
# using this instance (it cancels the task when we return false), so swapping
|
|
358
|
+
# identity here would hand it stale attributes.
|
|
359
|
+
#
|
|
360
|
+
# Status is re-read for the same reason. A snapshot that said `queued` may
|
|
361
|
+
# have been promoted and started since, and a started turn has already been
|
|
362
|
+
# handed its payload — deleting the prompt must stop it, not re-target it.
|
|
363
|
+
def reanchor_locked_task(task)
|
|
364
|
+
task.lock!
|
|
365
|
+
return false unless %w[queued pending].include?(task.status)
|
|
366
|
+
|
|
367
|
+
payload = task.trigger_event_payload || {}
|
|
368
|
+
merged = Array(payload[Collavre::Orchestration::TaskCoalescer::PAYLOAD_KEY])
|
|
369
|
+
.compact.map(&:to_i).uniq - [ id ]
|
|
370
|
+
return false if merged.empty?
|
|
371
|
+
|
|
372
|
+
# Anything else in the merge window may have been deleted too. Newest by id:
|
|
373
|
+
# created_at is stamped per writing process, so a burst can carry skewed or
|
|
374
|
+
# tied timestamps (same reason MergedTriggerComments orders by id).
|
|
375
|
+
#
|
|
376
|
+
# Scoped to this turn's creative/topic: a merged comment moved elsewhere
|
|
377
|
+
# while the task waited (CommentMoveService#perform_move reassigns
|
|
378
|
+
# creative_id) is no longer part of it, and adopting it as the anchor would
|
|
379
|
+
# point the reply at a creative the agent may have no share on.
|
|
380
|
+
#
|
|
381
|
+
# The turn is the TASK's scope, not this comment's. cancel_pending_tasks
|
|
382
|
+
# looks tasks up with no creative scoping precisely because a move rewrites
|
|
383
|
+
# the comment without touching the task it triggered — so an anchor moved
|
|
384
|
+
# away and then deleted would be searching the creative it was moved *to*,
|
|
385
|
+
# find nothing there, and cancel a turn whose absorbed comments are all
|
|
386
|
+
# still sitting in the creative the task belongs to.
|
|
387
|
+
#
|
|
388
|
+
# Asked through MergedTriggerComments.in_turn, the same predicate that
|
|
389
|
+
# decides what the turn delivers. Eligibility is not only about *where* a
|
|
390
|
+
# comment is: a comment made private (or turned into an approval surface)
|
|
391
|
+
# after it was absorbed is one dispatch_to_orchestration would refuse to
|
|
392
|
+
# trigger on, and in_turn already drops it from the merged blocks. The
|
|
393
|
+
# anchor is delivered as the trigger itself, with no filter in front of
|
|
394
|
+
# it, so a lookup of its own here is how withdrawn content reaches the
|
|
395
|
+
# agent through the one door that never filters.
|
|
396
|
+
in_scope = Collavre::AiAgent::MergedTriggerComments
|
|
397
|
+
.in_turn(merged, "creative" => { "id" => task.creative_id },
|
|
398
|
+
"topic" => { "id" => task.topic_id })
|
|
399
|
+
.order(:id).to_a
|
|
400
|
+
replacement = in_scope.last
|
|
401
|
+
return false unless replacement
|
|
402
|
+
|
|
403
|
+
# Through the same door the refresh uses, so the promotion is recorded as
|
|
404
|
+
# an acquired anchor either way: this task was created to answer the
|
|
405
|
+
# comment being deleted, not this one.
|
|
406
|
+
payload = Collavre::Orchestration::TaskCoalescer.reanchor_payload(payload, replacement)
|
|
407
|
+
payload = payload.merge(
|
|
408
|
+
# Rebuilt from the in-scope ids rather than merged through
|
|
409
|
+
# absorb_into_payload, which would fold the out-of-scope ones straight
|
|
410
|
+
# back in from the payload it starts from. The new anchor is excluded so
|
|
411
|
+
# the promoted comment is not delivered twice.
|
|
412
|
+
Collavre::Orchestration::TaskCoalescer::PAYLOAD_KEY =>
|
|
413
|
+
(in_scope.map(&:id) - [ replacement.id ]).sort
|
|
414
|
+
)
|
|
415
|
+
task.update!(trigger_event_payload: payload)
|
|
416
|
+
|
|
417
|
+
Rails.logger.info(
|
|
418
|
+
"[Comment#cancel_pending_tasks] Re-anchored coalesced task #{task.id} from deleted " \
|
|
419
|
+
"comment #{id} to comment #{replacement.id}"
|
|
420
|
+
)
|
|
421
|
+
true
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Cancel *every* queued waiter this notice speaks for, not just the newest.
|
|
425
|
+
#
|
|
426
|
+
# Cancelling one was right while each deferral posted its own notice: the
|
|
427
|
+
# newest queued task was the one that notice belonged to. A topic now gets
|
|
428
|
+
# exactly one deduplicated notice
|
|
429
|
+
# (Orchestration::AgentOrchestrator.with_deduped_topic_notice), and with
|
|
430
|
+
# topic_max_concurrent_jobs > 1 it can stand for waiters from several agents
|
|
431
|
+
# — coalescing folds same-agent siblings only. Deleting it while cancelling
|
|
432
|
+
# one of them leaves the rest queued with nothing on screen representing
|
|
433
|
+
# them and no stop control, and nothing reposts a notice until the next
|
|
434
|
+
# deferral happens by.
|
|
435
|
+
#
|
|
436
|
+
# …but only a notice that actually *was* deduplicated. With
|
|
437
|
+
# coalesce_pending_tasks off, post_waiting_notice deliberately skips the
|
|
438
|
+
# dedup path and posts one notice per deferral: there the 1:1 still holds and
|
|
439
|
+
# cancelling every queued task would let a user discard unrelated waiters by
|
|
440
|
+
# dismissing one notice, defeating the opt-out.
|
|
441
|
+
#
|
|
442
|
+
# Which kind this is comes off the row (waiting_notice_scope), written by
|
|
443
|
+
# whichever door posted it. It used to be inferred from whether a sibling
|
|
444
|
+
# notice still stood, on the reasoning that the dedup path allows a topic
|
|
445
|
+
# exactly one — but the two kinds coexist as soon as the policy differs
|
|
446
|
+
# between agents or changes while a topic still has waiters, and then the
|
|
447
|
+
# inference inverts: the shared notice reads as a per-deferral one and takes
|
|
448
|
+
# down the newest queued waiter, which is very likely the one the *other*
|
|
449
|
+
# notice speaks for. A row cannot be classified by its neighbours when the
|
|
450
|
+
# neighbours are a different kind.
|
|
451
|
+
#
|
|
452
|
+
# Queued only. A task holding the topic slot (pending/running/…) is not
|
|
453
|
+
# waiting on anything; the notice surfaces it separately as
|
|
454
|
+
# topic_blocking_task, with its own stop button.
|
|
210
455
|
def cancel_queued_tasks_for_waiting_notice
|
|
456
|
+
represented_queued_waiters.each { |task| task.update!(status: "cancelled") }
|
|
457
|
+
|
|
458
|
+
# Cancelling is the other way a topic queue empties, and it has no
|
|
459
|
+
# promotion behind it to run the drained check. The notices that spoke for
|
|
460
|
+
# the waiters just cancelled went with them — this one is being destroyed,
|
|
461
|
+
# and a per-deferral sibling names a task that is now cancelled — but a
|
|
462
|
+
# *shared* notice is only ever taken down by that check, so without this
|
|
463
|
+
# it is left on screen describing a wait that is over.
|
|
464
|
+
Comment.remove_stranded_waiting_notices!(creative_id: creative_id, topic_id: topic_id)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# The queued waiters this notice speaks for — what its stop button cancels,
|
|
468
|
+
# and equally what keeps it on screen. Public because
|
|
469
|
+
# .remove_stranded_waiting_notices! asks the same question from the other
|
|
470
|
+
# side: a notice representing nobody is a stop control for work that can no
|
|
471
|
+
# longer be stopped. Two answers to one question is how the button and the
|
|
472
|
+
# sweep would come to disagree about what a notice is for.
|
|
473
|
+
public def represented_queued_waiters
|
|
474
|
+
case waiting_notice_scope
|
|
475
|
+
when WAITING_NOTICE_TASK
|
|
476
|
+
# Speaks for exactly the waiter it was posted with — and for nothing at
|
|
477
|
+
# all once that waiter has been folded away or promoted.
|
|
478
|
+
queued_topic_waiters.select { |task| task.id == waiting_notice_task_id }
|
|
479
|
+
when WAITING_NOTICE_TOPIC
|
|
480
|
+
# Every queued waiter in the topic except those a per-deferral notice
|
|
481
|
+
# speaks for: the shared notice was never their signal, and their own
|
|
482
|
+
# stop control is — or is about to be — on screen.
|
|
483
|
+
#
|
|
484
|
+
# Asked of the waiter, not of the notices standing beside it. A waiter and
|
|
485
|
+
# its notice commit in two steps, so an opted-out waiter is queued before
|
|
486
|
+
# any notice names it; classifying it by what survives in that window
|
|
487
|
+
# cancels a turn that was only deferred, and leaves no notice behind to
|
|
488
|
+
# say so. What speaks for a waiter is settled when it is parked.
|
|
489
|
+
claimed = sibling_notice_waiter_ids
|
|
490
|
+
queued_topic_waiters.reject { |task| task_claims_own_notice?(task, claimed) }
|
|
491
|
+
else
|
|
492
|
+
# Posted before this was recorded. Nothing on the row says which kind it
|
|
493
|
+
# was, so keep the behaviour those notices were created under rather than
|
|
494
|
+
# guess: widening it would discard work, narrowing it would disarm the
|
|
495
|
+
# only stop control an in-flight wait has.
|
|
496
|
+
queued_topic_waiters.first(1)
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def queued_topic_waiters
|
|
211
501
|
scope = Task.where(status: "queued", creative_id: creative_id)
|
|
212
502
|
scope = topic_id ? scope.where(topic_id: topic_id) : scope.where(topic_id: nil)
|
|
213
|
-
|
|
214
|
-
|
|
503
|
+
scope.order(created_at: :desc).to_a
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Does this waiter have a per-deferral notice of its own, rather than being
|
|
507
|
+
# one the topic's shared notice speaks for?
|
|
508
|
+
#
|
|
509
|
+
# The waiter says so itself, recorded when it was parked. Rows parked before
|
|
510
|
+
# that was recorded say nothing, so they keep the answer they were parked
|
|
511
|
+
# under — the surviving sibling notices — rather than being guessed at in
|
|
512
|
+
# either direction: widening this discards work, narrowing it disarms the only
|
|
513
|
+
# stop control an in-flight wait has.
|
|
514
|
+
def task_claims_own_notice?(task, claimed)
|
|
515
|
+
case task.waiting_notice_scope
|
|
516
|
+
when WAITING_NOTICE_TASK then true
|
|
517
|
+
when WAITING_NOTICE_TOPIC then false
|
|
518
|
+
else claimed.include?(task.id)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Waiter ids a still-standing per-deferral notice represents. Runs
|
|
523
|
+
# after_destroy_commit, so this notice is already out of the query.
|
|
524
|
+
def sibling_notice_waiter_ids
|
|
525
|
+
Comment.where(creative_id: creative_id, topic_id: topic_id, user_id: nil,
|
|
526
|
+
waiting_notice_scope: WAITING_NOTICE_TASK)
|
|
527
|
+
.pluck(:waiting_notice_task_id).compact
|
|
215
528
|
end
|
|
216
529
|
|
|
217
530
|
def dispatch_to_orchestration
|
|
@@ -252,7 +565,7 @@ module Collavre
|
|
|
252
565
|
# when the local Claude TUI answered the prompt, leaving pending_tool_call
|
|
253
566
|
# set on the server for the rest of a locally-approved tool run.
|
|
254
567
|
|
|
255
|
-
SystemEvents::Dispatcher.dispatch("comment_created", dispatch_payload)
|
|
568
|
+
SystemEvents::Dispatcher.dispatch("comment_created", dispatch_payload, source: "comment_callback")
|
|
256
569
|
rescue StandardError => e
|
|
257
570
|
Rails.logger.error(
|
|
258
571
|
"[Comment#dispatch_to_orchestration] Failed for comment #{id}: " \
|
|
@@ -332,14 +645,6 @@ module Collavre
|
|
|
332
645
|
)
|
|
333
646
|
end
|
|
334
647
|
|
|
335
|
-
def assign_main_topic
|
|
336
|
-
return if topic_id.present?
|
|
337
|
-
return unless creative
|
|
338
|
-
|
|
339
|
-
fallback = user || Collavre.current_user || creative.user
|
|
340
|
-
self.topic = creative.main_topic(fallback_user: fallback)
|
|
341
|
-
end
|
|
342
|
-
|
|
343
648
|
def use_origin_creative
|
|
344
649
|
return unless creative
|
|
345
650
|
self.creative = creative.effective_origin
|
|
@@ -352,14 +657,14 @@ module Collavre
|
|
|
352
657
|
errors.add(:creative, "must be an origin creative")
|
|
353
658
|
end
|
|
354
659
|
|
|
355
|
-
def
|
|
356
|
-
|
|
660
|
+
def enqueue_link_preview
|
|
661
|
+
return if content.blank?
|
|
662
|
+
|
|
663
|
+
CommentLinkPreviewJob.perform_later(id, content, notification_revision)
|
|
357
664
|
end
|
|
358
665
|
|
|
359
|
-
def
|
|
360
|
-
|
|
361
|
-
rescue StandardError => e
|
|
362
|
-
Rails.logger.warn("Comment link preview formatting failed: #{e.class} #{e.message}")
|
|
666
|
+
def link_preview_enqueue_required?
|
|
667
|
+
saved_change_to_content? && !skip_link_preview
|
|
363
668
|
end
|
|
364
669
|
|
|
365
670
|
def images_must_be_images
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
class CommentNotificationDelivery < ApplicationRecord
|
|
5
|
+
self.table_name = "comment_notification_deliveries"
|
|
6
|
+
|
|
7
|
+
CLAIM_TIMEOUT = 5.minutes
|
|
8
|
+
|
|
9
|
+
validates :delivery_key, :recipient_id, :message, presence: true
|
|
10
|
+
|
|
11
|
+
scope :ready_for_push, -> {
|
|
12
|
+
where(push_enqueued_at: nil)
|
|
13
|
+
.where("push_claimed_at IS NULL OR push_claimed_at < ?", CLAIM_TIMEOUT.ago)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def enqueue_push!
|
|
17
|
+
claim_token = SecureRandom.uuid
|
|
18
|
+
claimed_at = Time.current
|
|
19
|
+
claimed = self.class.ready_for_push
|
|
20
|
+
.where(id: id)
|
|
21
|
+
.update_all(
|
|
22
|
+
push_claim_token: claim_token,
|
|
23
|
+
push_claimed_at: claimed_at,
|
|
24
|
+
updated_at: claimed_at
|
|
25
|
+
)
|
|
26
|
+
return false unless claimed == 1
|
|
27
|
+
|
|
28
|
+
job = PushNotificationJob.perform_later(recipient_id, message: message, link: link)
|
|
29
|
+
enqueue_succeeded = job && (!job.respond_to?(:successfully_enqueued?) || job.successfully_enqueued?)
|
|
30
|
+
unless enqueue_succeeded
|
|
31
|
+
enqueue_error = job.respond_to?(:enqueue_error) ? job.enqueue_error : nil
|
|
32
|
+
raise enqueue_error || ActiveJob::EnqueueError.new("Push notification enqueue failed")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
acknowledged = self.class
|
|
36
|
+
.where(id: id, push_claim_token: claim_token, push_enqueued_at: nil)
|
|
37
|
+
.update_all(
|
|
38
|
+
push_enqueued_at: Time.current,
|
|
39
|
+
push_claim_token: nil,
|
|
40
|
+
push_claimed_at: nil,
|
|
41
|
+
updated_at: Time.current
|
|
42
|
+
)
|
|
43
|
+
raise ActiveRecord::StaleObjectError.new(self, "enqueue push") unless acknowledged == 1
|
|
44
|
+
|
|
45
|
+
true
|
|
46
|
+
rescue StandardError
|
|
47
|
+
release_claim(claim_token)
|
|
48
|
+
raise
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def release_claim(claim_token)
|
|
54
|
+
self.class.where(id: id, push_claim_token: claim_token).update_all(
|
|
55
|
+
push_claim_token: nil,
|
|
56
|
+
push_claimed_at: nil,
|
|
57
|
+
updated_at: Time.current
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
class CommentPresenceStore
|
|
5
|
+
module CacheBatches
|
|
6
|
+
def active_subscriptions_for_creatives(ids, user_id, subscription_keys, subscriptions)
|
|
7
|
+
lease_keys = subscription_lease_keys(ids, user_id, subscription_keys, subscriptions)
|
|
8
|
+
leases = Rails.cache.read_multi(*lease_keys.values)
|
|
9
|
+
ids.to_h do |creative_id|
|
|
10
|
+
subscription_ids = Array(subscriptions[subscription_keys.fetch(creative_id)])
|
|
11
|
+
active_ids = subscription_ids.select { |id| leases[lease_keys.fetch([ creative_id, id ])] }
|
|
12
|
+
[ creative_id, active_ids ]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def subscription_lease_keys(ids, user_id, subscription_keys, subscriptions)
|
|
17
|
+
ids.flat_map do |creative_id|
|
|
18
|
+
Array(subscriptions[subscription_keys.fetch(creative_id)]).map do |subscription_id|
|
|
19
|
+
[ [ creative_id, subscription_id ], subscription_lease_key(creative_id, user_id, subscription_id) ]
|
|
20
|
+
end
|
|
21
|
+
end.to_h
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def viewing_topics_from_subscriptions(subscriptions_by_creative_id, user_id)
|
|
25
|
+
topic_keys = subscriptions_by_creative_id.to_h do |creative_id, subscription_ids|
|
|
26
|
+
[ creative_id, subscription_ids.map { |id| topic_key(creative_id, user_id, id) } ]
|
|
27
|
+
end
|
|
28
|
+
cached_topics = Rails.cache.read_multi(*topic_keys.values.flatten)
|
|
29
|
+
topic_keys.transform_values { |keys| keys.flat_map { |key| Array(cached_topics[key]) }.compact.uniq }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def cached_user_ids(ids)
|
|
33
|
+
cached = Rails.cache.read_multi(*ids.map { |creative_id| key(creative_id) })
|
|
34
|
+
ids.to_h { |creative_id| [ creative_id, cached[key(creative_id)] || [] ] }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def cached_subscriptions_for(user_ids_by_creative_id)
|
|
38
|
+
keys = subscription_keys_by_pair(user_ids_by_creative_id)
|
|
39
|
+
Rails.cache.read_multi(*keys.values)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cached_subscription_leases(user_ids_by_creative_id, subscriptions)
|
|
43
|
+
keys = subscription_keys_by_pair(user_ids_by_creative_id)
|
|
44
|
+
lease_keys = keys.flat_map do |(creative_id, user_id), subscription_key|
|
|
45
|
+
Array(subscriptions[subscription_key]).map { |id| subscription_lease_key(creative_id, user_id, id) }
|
|
46
|
+
end
|
|
47
|
+
Rails.cache.read_multi(*lease_keys)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def present_users_from_cache(user_ids_by_creative_id, subscriptions, leases)
|
|
51
|
+
user_ids_by_creative_id.to_h do |creative_id, user_ids|
|
|
52
|
+
users = user_ids.select { |user_id| active_subscription_ids_from_cache(creative_id, user_id, subscriptions, leases).any? }
|
|
53
|
+
[ creative_id, users ]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def subscription_keys_by_pair(user_ids_by_creative_id)
|
|
58
|
+
user_ids_by_creative_id.flat_map do |creative_id, user_ids|
|
|
59
|
+
user_ids.map { |user_id| [ [ creative_id, user_id ], subscriptions_key(creative_id, user_id) ] }
|
|
60
|
+
end.to_h
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|