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
|
@@ -11,6 +11,22 @@ module Collavre
|
|
|
11
11
|
CANCEL_CHECK_INTERVAL = 1.0
|
|
12
12
|
# Interval (in seconds) between agent_status heartbeats during streaming
|
|
13
13
|
AGENT_STATUS_HEARTBEAT_INTERVAL = 3.0
|
|
14
|
+
# Statuses written to the row by another process while this worker is
|
|
15
|
+
# inside the provider call: user Stop -> cancelled, StuckDetectorJob ->
|
|
16
|
+
# failed. Both must stop this worker — a row already settled externally
|
|
17
|
+
# has nobody waiting on this turn, and streaming on holds a worker
|
|
18
|
+
# thread for the rest of the provider call.
|
|
19
|
+
TERMINAL_STATUSES = %w[cancelled failed].freeze
|
|
20
|
+
|
|
21
|
+
def self.topic_id_for(task:, creative:)
|
|
22
|
+
task.topic_id ||
|
|
23
|
+
task.trigger_event_payload&.dig("topic", "id") ||
|
|
24
|
+
Comment.where(
|
|
25
|
+
id: task.trigger_event_payload&.dig("comment", "id"),
|
|
26
|
+
creative_id: creative.id
|
|
27
|
+
).pick(:topic_id) ||
|
|
28
|
+
creative.main_topic.id
|
|
29
|
+
end
|
|
14
30
|
|
|
15
31
|
def initialize(task:, agent:, creative:)
|
|
16
32
|
@task = task
|
|
@@ -18,6 +34,10 @@ module Collavre
|
|
|
18
34
|
@creative = creative
|
|
19
35
|
@last_cancel_check_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
20
36
|
@last_heartbeat_at = @last_cancel_check_at
|
|
37
|
+
# Captured once so a setting changed mid-turn cannot make the deadline
|
|
38
|
+
# and the message that reports crossing it disagree about what fired.
|
|
39
|
+
@turn_deadline_seconds = SystemSetting.ai_agent_turn_deadline_seconds
|
|
40
|
+
@deadline_at = @last_cancel_check_at + @turn_deadline_seconds
|
|
21
41
|
end
|
|
22
42
|
|
|
23
43
|
# Broadcast agent status change
|
|
@@ -30,18 +50,55 @@ module Collavre
|
|
|
30
50
|
agent_id: @agent.id,
|
|
31
51
|
agent_name: @agent.display_name,
|
|
32
52
|
task_id: @task.id,
|
|
53
|
+
topic_id: self.class.topic_id_for(task: @task, creative: @creative),
|
|
33
54
|
content: content,
|
|
34
55
|
source_creative_id: @creative.id
|
|
35
56
|
)
|
|
36
57
|
end
|
|
37
58
|
|
|
38
|
-
# Check if task
|
|
39
|
-
|
|
59
|
+
# Check if task reached a terminal status or overran its turn deadline,
|
|
60
|
+
# raise Collavre::CancelledError / Collavre::TurnDeadlineError if so
|
|
61
|
+
def check_cancelled!(force: false)
|
|
40
62
|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
41
|
-
return if (now - @last_cancel_check_at) < CANCEL_CHECK_INTERVAL
|
|
63
|
+
return if !force && (now - @last_cancel_check_at) < CANCEL_CHECK_INTERVAL
|
|
42
64
|
|
|
43
65
|
@last_cancel_check_at = now
|
|
44
|
-
raise Collavre::CancelledError if @task.reload.status
|
|
66
|
+
raise Collavre::CancelledError if TERMINAL_STATUSES.include?(@task.reload.status)
|
|
67
|
+
|
|
68
|
+
return if now < @deadline_at
|
|
69
|
+
|
|
70
|
+
# This worker is the only process that knows the turn overran, so it
|
|
71
|
+
# writes the terminal status itself before leaving through the
|
|
72
|
+
# cancellation path (which never overwrites a terminal status).
|
|
73
|
+
#
|
|
74
|
+
# Written the same way StuckDetector fails a row from outside its
|
|
75
|
+
# worker: mark_handed_off! has not run yet (it runs later, in
|
|
76
|
+
# execute_llm_conversation's ensure), so a bare `update!` would let
|
|
77
|
+
# Task's status callback decide off evidence this attempt has not
|
|
78
|
+
# written down yet and restore dispatches this turn actually
|
|
79
|
+
# delivered. fail_while_worker_settles! marks the row so the callback
|
|
80
|
+
# defers, and AiAgentJob's ensure settles it once the real handoff
|
|
81
|
+
# evidence exists.
|
|
82
|
+
deadline_transitioned = Orchestration::DeliveryRecord.fail_while_worker_settles!(@task)
|
|
83
|
+
# Another process may have moved the row to a terminal status after
|
|
84
|
+
# the reload above but before the locked transition. Preserve that
|
|
85
|
+
# winner as an ordinary cancellation; classifying it as our deadline
|
|
86
|
+
# would make AiAgentJob fail a parent workflow that was just cancelled.
|
|
87
|
+
raise Collavre::CancelledError unless deadline_transitioned
|
|
88
|
+
|
|
89
|
+
Rails.logger.warn(
|
|
90
|
+
"[AgentLifecycleManager] Task #{@task.id} exceeded turn deadline " \
|
|
91
|
+
"(#{@turn_deadline_seconds}s); failing"
|
|
92
|
+
)
|
|
93
|
+
raise Collavre::TurnDeadlineError.new(@turn_deadline_seconds)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Bound a single silent provider request by the remaining wall-clock
|
|
97
|
+
# budget. Keep a tiny positive floor for transport APIs that reject zero;
|
|
98
|
+
# check_cancelled! remains the authority once that request returns.
|
|
99
|
+
def remaining_deadline_seconds
|
|
100
|
+
remaining = @deadline_at - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
101
|
+
[ remaining, 0.001 ].max
|
|
45
102
|
end
|
|
46
103
|
|
|
47
104
|
# Send heartbeat if interval passed
|
|
@@ -54,7 +111,7 @@ module Collavre
|
|
|
54
111
|
end
|
|
55
112
|
|
|
56
113
|
# Handle cancellation cleanup
|
|
57
|
-
def handle_cancelled(reply_comment:, response_content:)
|
|
114
|
+
def handle_cancelled(reply_comment:, response_content:, action_type: "cancelled", message: "Task cancelled by user")
|
|
58
115
|
if reply_comment
|
|
59
116
|
if response_content.present?
|
|
60
117
|
reply_comment.content_will_change!
|
|
@@ -71,7 +128,7 @@ module Collavre
|
|
|
71
128
|
end
|
|
72
129
|
|
|
73
130
|
broadcast_status("idle")
|
|
74
|
-
log_action(
|
|
131
|
+
log_action(action_type, { message: message })
|
|
75
132
|
end
|
|
76
133
|
|
|
77
134
|
private
|
|
@@ -16,10 +16,10 @@ module Collavre
|
|
|
16
16
|
def handle(error, summary: nil)
|
|
17
17
|
cleanup_placeholder
|
|
18
18
|
|
|
19
|
-
broadcast_idle
|
|
20
|
-
|
|
21
19
|
update_task(error)
|
|
22
20
|
|
|
21
|
+
broadcast_pending_approval
|
|
22
|
+
|
|
23
23
|
log_action(error)
|
|
24
24
|
|
|
25
25
|
create_approval_comment(error, summary: summary)
|
|
@@ -31,15 +31,23 @@ module Collavre
|
|
|
31
31
|
@reply_comment&.destroy! if @reply_comment&.content == Comment::STREAMING_PLACEHOLDER_CONTENT
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
# The agent has stopped producing output, but the task has not finished:
|
|
35
|
+
# it holds its topic slot until the tool call is approved or rejected, and
|
|
36
|
+
# TasksController#cancel still accepts it. Broadcasting "idle" here made
|
|
37
|
+
# the client drop the task and stop polling, so the pending_approval status
|
|
38
|
+
# this method announces was never observed and Stop disappeared on the one
|
|
39
|
+
# turn that is waiting on the user. Sent after update_task so a client that
|
|
40
|
+
# reacts by polling reads the status this payload claims.
|
|
41
|
+
def broadcast_pending_approval
|
|
35
42
|
return unless @creative
|
|
36
43
|
|
|
37
44
|
CommentsPresenceChannel.broadcast_agent_status(
|
|
38
45
|
@creative.effective_origin.id,
|
|
39
|
-
status: "
|
|
46
|
+
status: "pending_approval",
|
|
40
47
|
agent_id: @agent.id,
|
|
41
48
|
agent_name: @agent.display_name,
|
|
42
49
|
task_id: @task.id,
|
|
50
|
+
topic_id: @task.topic_id || @task.trigger_event_payload&.dig("topic", "id"),
|
|
43
51
|
source_creative_id: @creative.id
|
|
44
52
|
)
|
|
45
53
|
end
|
|
@@ -17,10 +17,9 @@ module Collavre
|
|
|
17
17
|
|
|
18
18
|
def deliver
|
|
19
19
|
unless @topic_id
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# parent workflow advances via fail_subtask! instead of hanging.
|
|
20
|
+
# A Claude Channel agent has no place to deliver a dispatch that
|
|
21
|
+
# carries no topic — raise so AiAgentJob fails the task instead of
|
|
22
|
+
# broadcasting into nowhere.
|
|
24
23
|
raise UndeliverableError,
|
|
25
24
|
"Claude Channel delivery requires a topic_id (agent=#{@agent.id})"
|
|
26
25
|
end
|
|
@@ -43,7 +42,13 @@ module Collavre
|
|
|
43
42
|
session_topic: session_topic?,
|
|
44
43
|
comment: {
|
|
45
44
|
id: @context.dig("comment", "id"),
|
|
46
|
-
|
|
45
|
+
# This payload is the agent's ONLY input — the MCP client gets no
|
|
46
|
+
# chat history — so comments that Orchestration::TaskCoalescer
|
|
47
|
+
# folded into this turn have to be rendered inline here or they
|
|
48
|
+
# never reach the agent at all.
|
|
49
|
+
content: MergedTriggerComments.prepend_to(
|
|
50
|
+
@context.dig("comment", "content"), @context, agent: @agent
|
|
51
|
+
),
|
|
47
52
|
author_id: @context.dig("sender", "id") || @context.dig("comment", "user_id"),
|
|
48
53
|
author_name: @context.dig("sender", "name") || comment&.user&.display_name,
|
|
49
54
|
topic_id: @topic_id,
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collavre
|
|
4
|
+
module AiAgent
|
|
5
|
+
# Renders the comments that Orchestration::TaskCoalescer folded into a task.
|
|
6
|
+
#
|
|
7
|
+
# When a burst of comments collapses into one turn, the superseded comments
|
|
8
|
+
# survive only as ids in the payload's "merged_comment_ids". Every delivery
|
|
9
|
+
# path that carries a trigger has to render them, because none of them can
|
|
10
|
+
# fall back to chat history:
|
|
11
|
+
#
|
|
12
|
+
# - ClaudeChannelAdapter sends *only* comment.content to the MCP client.
|
|
13
|
+
# - SessionContextResolver#incremental_payload sends *only* the :trigger
|
|
14
|
+
# message for session-backed agents.
|
|
15
|
+
#
|
|
16
|
+
# Blocks are chronological and labelled with their speaker, matching how
|
|
17
|
+
# MessageBuilder labels chat history.
|
|
18
|
+
class MergedTriggerComments
|
|
19
|
+
Block = Struct.new(:comment, :text, :images, keyword_init: true)
|
|
20
|
+
|
|
21
|
+
TRUNCATION_SUFFIX = "…[truncated]"
|
|
22
|
+
|
|
23
|
+
def self.for(context, agent:)
|
|
24
|
+
new(context, agent: agent).blocks
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The comments a payload's merged ids still refer to *in this turn*.
|
|
28
|
+
#
|
|
29
|
+
# The merged ids were captured when the burst was folded; they are not a
|
|
30
|
+
# standing claim on those rows. CommentMoveService#perform_move reassigns
|
|
31
|
+
# creative_id, so a comment moved away while the task waited is no longer
|
|
32
|
+
# part of the turn — rendering it would hand the agent text and image
|
|
33
|
+
# attachments from a creative it may have no share on.
|
|
34
|
+
#
|
|
35
|
+
# Public because two doors ask the same question and must not diverge:
|
|
36
|
+
# this class decides what is delivered, and Matcher.permits_assignment?
|
|
37
|
+
# decides whether a merged @mention still licenses the turn. A mention
|
|
38
|
+
# excluded from the trigger cannot be the reason the turn is allowed.
|
|
39
|
+
def self.in_turn(ids, context)
|
|
40
|
+
context ||= {}
|
|
41
|
+
relation = Comment.public_only.without_approval_action.where(id: ids)
|
|
42
|
+
creative_id = context.dig("creative", "id")
|
|
43
|
+
relation = relation.where(creative_id: creative_id) if creative_id
|
|
44
|
+
# topic_id nil is a real scope (a creative's Main topic), so key? rather
|
|
45
|
+
# than presence decides whether the turn is topic-scoped at all.
|
|
46
|
+
relation = relation.where(topic_id: context.dig("topic", "id")) if context.key?("topic")
|
|
47
|
+
relation
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Prefix `content` with the merged comments, oldest first.
|
|
51
|
+
def self.prepend_to(content, context, agent:)
|
|
52
|
+
blocks = self.for(context, agent: agent)
|
|
53
|
+
return content if blocks.empty?
|
|
54
|
+
|
|
55
|
+
(blocks.map(&:text) + [ content.to_s ]).join("\n\n")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def initialize(context, agent:)
|
|
59
|
+
@context = context || {}
|
|
60
|
+
@agent = agent
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Ids of the coalesced comments, with the current anchor excluded — the
|
|
64
|
+
# anchor is delivered as the trigger itself and must not be duplicated.
|
|
65
|
+
def comment_ids
|
|
66
|
+
@comment_ids ||= begin
|
|
67
|
+
ids = Array(@context[Orchestration::TaskCoalescer::PAYLOAD_KEY])
|
|
68
|
+
.compact.map(&:to_i).uniq
|
|
69
|
+
anchor_id = @context.dig("comment", "id")
|
|
70
|
+
anchor_id ? ids - [ anchor_id.to_i ] : ids
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def blocks
|
|
75
|
+
return [] if comment_ids.empty?
|
|
76
|
+
|
|
77
|
+
# Memoized: MessageBuilder renders these into the trigger and also scans
|
|
78
|
+
# them for creative references, and one build should not re-query.
|
|
79
|
+
# Ordered by id, not created_at. A burst is exactly the case where the
|
|
80
|
+
# rows are written by different processes, and created_at is stamped by
|
|
81
|
+
# whichever one wrote it — clock skew or a tie can hand the agent
|
|
82
|
+
# "ignore that" ahead of the instruction it retracts. Ids are the app's
|
|
83
|
+
# single monotonic causal sequence (CommentsController orders by id for
|
|
84
|
+
# the same reason).
|
|
85
|
+
@blocks ||= within_budget(
|
|
86
|
+
self.class.in_turn(comment_ids, @context)
|
|
87
|
+
.includes(:user)
|
|
88
|
+
.order(:id)
|
|
89
|
+
.map { |c| Block.new(comment: c, text: label(c), images: image_blobs(c)) }
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# Coalescing turns a burst into one indivisible message, so an oversized
|
|
96
|
+
# burst does not degrade — the whole turn fails. MessageBuilder budgets
|
|
97
|
+
# chat history by the same setting; the trigger these blocks go into had
|
|
98
|
+
# no budget at all, which is the asymmetry a burst is most likely to hit.
|
|
99
|
+
#
|
|
100
|
+
# Shrink, never drop. Dropping the oldest blocks would rest on the history
|
|
101
|
+
# window carrying them instead, and no delivery path guarantees that:
|
|
102
|
+
#
|
|
103
|
+
# - A session-backed agent has no history window at all
|
|
104
|
+
# (SessionContextResolver#incremental_payload keeps only :trigger), and
|
|
105
|
+
# ClaudeChannelAdapter sends only the comment content.
|
|
106
|
+
# - Even with history, MessageBuilder#append_chat_history caps it by
|
|
107
|
+
# chat_history_limit and by *this same* size budget spread across the
|
|
108
|
+
# whole preceding conversation, cutting from its newest end — which is
|
|
109
|
+
# exactly where a just-dropped burst comment sits.
|
|
110
|
+
# - History carries text only. A dropped block's image attachments reach
|
|
111
|
+
# the agent through nothing, whatever the budgets say.
|
|
112
|
+
#
|
|
113
|
+
# So the trigger is the only channel that can promise a merged comment
|
|
114
|
+
# arrives, and "which of the user's comments to delete" is not a choice
|
|
115
|
+
# this class is in a position to make. Every block is kept and truncated to
|
|
116
|
+
# an equal share instead: an oversized burst then degrades in fidelity
|
|
117
|
+
# rather than losing whole messages, and the truncation is visible in the
|
|
118
|
+
# text where a silent drop was not.
|
|
119
|
+
#
|
|
120
|
+
# No floor under the share. A budget too small to say much per comment is
|
|
121
|
+
# still not a reason to delete the other comments outright: keeping them
|
|
122
|
+
# preserves their order, their speakers and — the part the text budget
|
|
123
|
+
# never measures — their image attachments, which are delivered as
|
|
124
|
+
# separate parts and cost nothing here.
|
|
125
|
+
def within_budget(blocks)
|
|
126
|
+
budget = size_limit
|
|
127
|
+
return blocks if budget.nil? || blocks.sum { |b| b.text.length } <= budget
|
|
128
|
+
|
|
129
|
+
blocks.map { |b| truncate_block(b, budget / blocks.size) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def truncate_block(block, limit)
|
|
133
|
+
return block if block.text.length <= limit
|
|
134
|
+
|
|
135
|
+
text = if limit > TRUNCATION_SUFFIX.length
|
|
136
|
+
block.text[0, limit - TRUNCATION_SUFFIX.length] + TRUNCATION_SUFFIX
|
|
137
|
+
else
|
|
138
|
+
block.text[0, limit]
|
|
139
|
+
end
|
|
140
|
+
Block.new(comment: block.comment, text: text, images: block.images)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def size_limit
|
|
144
|
+
@agent.respond_to?(:chat_history_size_limit) ? @agent.chat_history_size_limit : nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def label(comment)
|
|
148
|
+
text = comment.content.to_s
|
|
149
|
+
if @agent && comment.user_id == @agent.id
|
|
150
|
+
# Defensive: an agent's own message should never be coalesced into its
|
|
151
|
+
# own trigger, but if one is, do not relabel it as a human speaker.
|
|
152
|
+
"[#{@agent.name}]: #{text}"
|
|
153
|
+
else
|
|
154
|
+
text = MentionParser.strip_self_mention(text, @agent.name) if @agent
|
|
155
|
+
"[#{comment.user&.name || 'unknown'}]: #{text}"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def image_blobs(comment)
|
|
160
|
+
comment.images.attached? ? comment.images.map(&:blob) : []
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -106,14 +106,18 @@ module Collavre
|
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def append_referenced_creative_contexts(messages)
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
# Comments coalesced into this turn are delivered with the trigger, so
|
|
110
|
+
# their links point at creatives the agent is about to be asked about.
|
|
111
|
+
# Scanning only the surviving anchor would drop the subtree that the
|
|
112
|
+
# absorbed comment's own dispatch would have supplied.
|
|
113
|
+
contents = [ @context.dig("comment", "content") ] + merged_trigger.blocks.map(&:text)
|
|
114
|
+
contents = contents.compact
|
|
115
|
+
return if contents.empty?
|
|
111
116
|
|
|
112
117
|
children_level = @agent.creative_children_level
|
|
113
118
|
max_depth = 1 + children_level
|
|
114
119
|
|
|
115
|
-
|
|
116
|
-
referenced_ids = content.scan(%r{\[[^\]]*\]\(/creatives/(\d+)\)}).flatten.map(&:to_i).uniq
|
|
120
|
+
referenced_ids = contents.flat_map { |c| referenced_creative_ids(c) }.uniq
|
|
117
121
|
referenced_ids.reject! { |cid| @injected_creative_ids.include?(cid) }
|
|
118
122
|
creatives_by_id = Creative.where(id: referenced_ids).index_by(&:id)
|
|
119
123
|
|
|
@@ -134,6 +138,44 @@ module Collavre
|
|
|
134
138
|
end
|
|
135
139
|
end
|
|
136
140
|
|
|
141
|
+
# Creative ids linked from markdown like [title](/creatives/123).
|
|
142
|
+
def referenced_creative_ids(text)
|
|
143
|
+
text.to_s.scan(%r{\[[^\]]*\]\(/creatives/(\d+)\)}).flatten.map(&:to_i).uniq
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Whether a history entry carries its comment whole.
|
|
147
|
+
#
|
|
148
|
+
# A history entry is text: this window renders `content` and nothing
|
|
149
|
+
# else. Two things ride only on the trigger path — the blobs
|
|
150
|
+
# append_trigger_message attaches, and the subtree
|
|
151
|
+
# append_referenced_creative_contexts renders for a linked creative,
|
|
152
|
+
# which scans the anchor and the merged blocks and never this window. A
|
|
153
|
+
# comment carrying either is *partly* delivered here, and the turn that
|
|
154
|
+
# swept it up is not a substitute for its own dispatch.
|
|
155
|
+
#
|
|
156
|
+
# Decided here rather than at Orchestration::DeliveryRecord, which reads
|
|
157
|
+
# the :comment_id tag as a delivery claim: this is the only place that
|
|
158
|
+
# knows what was rendered, and in particular which creatives were
|
|
159
|
+
# already injected. A link to the topic's own creative — the common case
|
|
160
|
+
# — withholds nothing, and a rule written anywhere else could not tell
|
|
161
|
+
# that apart from a link to a subtree the agent never saw.
|
|
162
|
+
#
|
|
163
|
+
# Deliberately the same test append_referenced_creative_contexts applies,
|
|
164
|
+
# @injected_creative_ids and all, so the two answer alike: whatever that
|
|
165
|
+
# path would have injected for this comment is what this window failed to
|
|
166
|
+
# supply. It holds only roots, not the descendants a rendered subtree
|
|
167
|
+
# also covers, so a link to a descendant reads as withheld when its text
|
|
168
|
+
# was in fact rendered. That asymmetry costs a redundant turn; the
|
|
169
|
+
# opposite one costs the message.
|
|
170
|
+
def delivered_whole?(comment)
|
|
171
|
+
return false if comment.images.attached?
|
|
172
|
+
|
|
173
|
+
missing = referenced_creative_ids(comment.content) - @injected_creative_ids.to_a
|
|
174
|
+
return true if missing.empty?
|
|
175
|
+
|
|
176
|
+
!Creative.where(id: missing).exists?
|
|
177
|
+
end
|
|
178
|
+
|
|
137
179
|
# Appends chat history messages and returns the count of messages added.
|
|
138
180
|
def append_chat_history(messages)
|
|
139
181
|
creative_id = @context.dig("creative", "id")
|
|
@@ -146,16 +188,23 @@ module Collavre
|
|
|
146
188
|
history_chars = 0
|
|
147
189
|
count = 0
|
|
148
190
|
|
|
149
|
-
Comment.public_only.without_approval_action.where(creative_id: creative_id)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
191
|
+
scope = Comment.public_only.without_approval_action.where(creative_id: creative_id)
|
|
192
|
+
.where(topic_id: topic_id)
|
|
193
|
+
.where.not(user_id: nil)
|
|
194
|
+
.includes(:user, :images_attachments)
|
|
195
|
+
|
|
196
|
+
# Exclude before limiting, not after. The trigger and the comments folded
|
|
197
|
+
# into it are delivered by append_trigger_message, so leaving them in the
|
|
198
|
+
# limited window lets a burst eat every history slot — a burst as large as
|
|
199
|
+
# the limit would leave no conversation at all and mark the turn
|
|
200
|
+
# first_message. Dropping them first lets older messages backfill.
|
|
201
|
+
excluded_ids = (merged_comment_ids + [ @context.dig("comment", "id") ]).compact.map(&:to_i)
|
|
202
|
+
scope = scope.where.not(id: excluded_ids) if excluded_ids.any?
|
|
203
|
+
|
|
204
|
+
scope.order(created_at: :desc)
|
|
205
|
+
.limit(history_limit)
|
|
206
|
+
.reverse
|
|
207
|
+
.each do |c|
|
|
159
208
|
role = (c.user_id == @agent.id) ? "model" : "user"
|
|
160
209
|
content = c.content.to_s
|
|
161
210
|
|
|
@@ -168,7 +217,18 @@ module Collavre
|
|
|
168
217
|
history_chars += content.length
|
|
169
218
|
break if history_chars > history_size_limit
|
|
170
219
|
|
|
171
|
-
|
|
220
|
+
# Tagged with the comment it came from so Orchestration::DeliveryRecord
|
|
221
|
+
# can read, off the payload the adapter is actually handed, which
|
|
222
|
+
# comments this turn delivered. Consumers key into :role and :parts;
|
|
223
|
+
# the extra key is inert to all of them.
|
|
224
|
+
#
|
|
225
|
+
# Only an entry that carries its comment whole wears the tag — the
|
|
226
|
+
# tag is the delivery claim, and delivered_whole? is what makes it
|
|
227
|
+
# true. An untagged entry still goes to the agent as context; it just
|
|
228
|
+
# does not stand in for the comment's own dispatch.
|
|
229
|
+
entry = { role: role, kind: :chat_history, parts: [ { text: content } ] }
|
|
230
|
+
entry[:comment_id] = c.id if delivered_whole?(c)
|
|
231
|
+
messages << entry
|
|
172
232
|
count += 1
|
|
173
233
|
end
|
|
174
234
|
|
|
@@ -176,7 +236,7 @@ module Collavre
|
|
|
176
236
|
end
|
|
177
237
|
|
|
178
238
|
def append_trigger_message(messages)
|
|
179
|
-
payload_text =
|
|
239
|
+
payload_text = trigger_payload_text
|
|
180
240
|
|
|
181
241
|
if review_eligible?
|
|
182
242
|
quoted_body = @original_comment.quoted_comment&.content
|
|
@@ -192,8 +252,17 @@ module Collavre
|
|
|
192
252
|
payload_text = "[#{sender_name}]: #{payload_text}"
|
|
193
253
|
end
|
|
194
254
|
|
|
255
|
+
# Comments that were folded into this task by Orchestration::TaskCoalescer
|
|
256
|
+
# (a burst of messages answered as one turn). They belong *in* the
|
|
257
|
+
# trigger, not in chat history: a session-backed agent receives only the
|
|
258
|
+
# :trigger message, so history-only placement would lose them entirely.
|
|
259
|
+
merged_blocks = merged_trigger.blocks
|
|
260
|
+
payload_text = (merged_blocks.map(&:text) + [ payload_text ]).join("\n\n") if merged_blocks.any?
|
|
261
|
+
|
|
195
262
|
trigger_parts = [ { text: payload_text } ]
|
|
196
263
|
|
|
264
|
+
merged_blocks.each { |b| b.images.each { |blob| trigger_parts << { image: blob } } }
|
|
265
|
+
|
|
197
266
|
if @original_comment&.images&.attached?
|
|
198
267
|
@original_comment.images.each do |image|
|
|
199
268
|
trigger_parts << { image: image.blob }
|
|
@@ -203,6 +272,24 @@ module Collavre
|
|
|
203
272
|
messages << { role: "user", kind: :trigger, parts: trigger_parts }
|
|
204
273
|
end
|
|
205
274
|
|
|
275
|
+
def trigger_payload_text
|
|
276
|
+
@context.dig("comment", "content") || @context.except(SystemEvents::Envelope::KEY).to_json
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def merged_trigger
|
|
280
|
+
@merged_trigger ||= MergedTriggerComments.new(@context, agent: @agent)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Ids of the coalesced comments this turn actually renders into the
|
|
284
|
+
# trigger (anchor excluded). Deliberately the rendered blocks rather than
|
|
285
|
+
# every merged id: MergedTriggerComments drops the oldest of an oversized
|
|
286
|
+
# burst, and excluding a comment that no longer appears in the trigger
|
|
287
|
+
# would delete it from the turn outright. Leaving it in the history scope
|
|
288
|
+
# lets that separately budgeted window carry it instead.
|
|
289
|
+
def merged_comment_ids
|
|
290
|
+
merged_trigger.blocks.filter_map { |b| b.comment&.id }
|
|
291
|
+
end
|
|
292
|
+
|
|
206
293
|
def trigger_comment
|
|
207
294
|
@trigger_comment ||= begin
|
|
208
295
|
id = @context.dig("comment", "id")
|
|
@@ -32,9 +32,6 @@ module Collavre
|
|
|
32
32
|
existing = User.find_by(email: email)
|
|
33
33
|
return verified_agent(existing) if existing
|
|
34
34
|
|
|
35
|
-
# routing_expression: nil so the new ai_user is not matchable until the
|
|
36
|
-
# client claims the per-agent stream via AgentChannel. See the comment on
|
|
37
|
-
# verified_agent.
|
|
38
35
|
User.create!(
|
|
39
36
|
email: email,
|
|
40
37
|
name: "Claude Channel (#{agent_name})",
|
|
@@ -42,8 +39,7 @@ module Collavre
|
|
|
42
39
|
llm_vendor: "anthropic",
|
|
43
40
|
llm_model: "claude-code",
|
|
44
41
|
created_by_id: @user.id,
|
|
45
|
-
searchable: false
|
|
46
|
-
routing_expression: nil
|
|
42
|
+
searchable: false
|
|
47
43
|
)
|
|
48
44
|
rescue ActiveRecord::RecordNotUnique
|
|
49
45
|
# A concurrent registration for the same (user, agent_name) won the
|
|
@@ -92,9 +88,9 @@ module Collavre
|
|
|
92
88
|
end
|
|
93
89
|
|
|
94
90
|
# Returns the agent only when it is the current user's own Claude Channel
|
|
95
|
-
# agent; nil otherwise (the caller renders :conflict).
|
|
91
|
+
# agent; nil otherwise (the caller renders :conflict). Presence creation
|
|
96
92
|
# stays deferred to AgentChannel#subscribe_to_agent_stream so the agent
|
|
97
|
-
# becomes
|
|
93
|
+
# becomes ambiently routable only once a WebSocket subscriber exists for
|
|
98
94
|
# agent:user:<id> — otherwise comments matched between register returning
|
|
99
95
|
# and the client's subsequent cable subscribe would broadcast into an empty
|
|
100
96
|
# stream, stranding delegated tasks until stuck recovery.
|
|
@@ -14,12 +14,15 @@ module Collavre
|
|
|
14
14
|
# echoes the dispatch's task_id). Without task_id (legacy clients),
|
|
15
15
|
# oldest-first.
|
|
16
16
|
# 2. Inside a transaction: SELECT FOR UPDATE the row, re-check
|
|
17
|
-
# status == 'delegated' under the lock, then update
|
|
17
|
+
# status == 'delegated' under the lock, then update it to 'running'.
|
|
18
18
|
# Concurrent claimers block on the lock; the loser sees the
|
|
19
19
|
# already-flipped status post-lock and returns nil so the caller can
|
|
20
20
|
# refuse the duplicate.
|
|
21
|
-
#
|
|
22
|
-
#
|
|
21
|
+
# running is a transient claimed-reply state. It remains active while the
|
|
22
|
+
# comment transaction commits, so TopicMove cannot slip between claim and
|
|
23
|
+
# finalize; another /reply cannot claim it because only delegated rows are
|
|
24
|
+
# eligible. update_all (NOT update!) is required to skip callbacks at
|
|
25
|
+
# claim time. The callbacks fire check_trigger_loop_completion
|
|
23
26
|
# (which enqueues TriggerLoopCheckJob) and broadcast_stop_button_removal
|
|
24
27
|
# (which reads reply_comment). Both depend on the reply comment already
|
|
25
28
|
# existing — but reply() claims BEFORE comment.save to win the race against
|
|
@@ -43,25 +46,38 @@ module Collavre
|
|
|
43
46
|
locked = Task.lock.find_by(id: candidate.id)
|
|
44
47
|
next unless locked && locked.status == "delegated"
|
|
45
48
|
|
|
46
|
-
Task.where(id: locked.id).update_all(status: "
|
|
49
|
+
Task.where(id: locked.id).update_all(status: "running", pending_tool_call: nil, updated_at: Time.current)
|
|
47
50
|
claimed = locked.reload
|
|
48
51
|
end
|
|
49
52
|
claimed
|
|
50
53
|
end
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
# the comment to the claimed task, releases the ResourceTracker slot the
|
|
54
|
-
# AiAgentJob held under task.id, advances the parent workflow (if any), and
|
|
55
|
-
# drains the topic queue — mirroring AiAgentJob#perform's success path for
|
|
56
|
-
# non-delegated runs.
|
|
57
|
-
def finalize(agent:, task:, comment:)
|
|
55
|
+
def link_reply(task:, comment:)
|
|
58
56
|
comment.update_column(:task_id, task.id)
|
|
57
|
+
end
|
|
59
58
|
|
|
60
|
-
|
|
59
|
+
# Finalize the claimed row inside TaskReplyService's topic-lock
|
|
60
|
+
# transaction. Completion effects are registered after commit, when the
|
|
61
|
+
# linked comment and done status are both visible. This leaves no gap for
|
|
62
|
+
# cancellation or TopicMove between reply persistence and completion.
|
|
63
|
+
# The effects release the ResourceTracker slot the
|
|
64
|
+
# AiAgentJob held under task.id, and drains the topic queue — mirroring AiAgentJob#perform's success path for
|
|
65
|
+
# non-delegated runs.
|
|
66
|
+
def finalize(agent:, task:, comment:)
|
|
67
|
+
completed = Task.where(id: task.id, status: "running")
|
|
68
|
+
.update_all(status: "done", updated_at: Time.current)
|
|
69
|
+
raise ActiveRecord::RecordNotSaved, "claimed reply task was not running" unless completed == 1
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
task.reload
|
|
72
|
+
ActiveRecord.after_all_transactions_commit do
|
|
73
|
+
run_completion_effects(agent, task, comment)
|
|
64
74
|
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def run_completion_effects(agent, task, comment)
|
|
80
|
+
Orchestration::ResourceTracker.for(agent).release!(task.id)
|
|
65
81
|
|
|
66
82
|
Orchestration::AgentOrchestrator.dequeue_next_for_topic(task.topic_id, task.creative_id)
|
|
67
83
|
|
|
@@ -79,8 +95,6 @@ module Collavre
|
|
|
79
95
|
broadcast_claude_idle(agent, task, comment)
|
|
80
96
|
end
|
|
81
97
|
|
|
82
|
-
private
|
|
83
|
-
|
|
84
98
|
# Clear the chat typing indicator via the canonical status broadcaster (the
|
|
85
99
|
# same one AiAgentService uses for every other agent), so the Claude path
|
|
86
100
|
# emits an identical "idle" agent_status payload.
|