machinaos 0.0.76 → 0.0.78
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.
- package/README.md +143 -107
- package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
- package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
- package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
- package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
- package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
- package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
- package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
- package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
- package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
- package/client/dist/assets/index-DQ0nwhec.js +257 -0
- package/client/dist/assets/index-DxmbVskS.css +1 -0
- package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
- package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
- package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
- package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
- package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
- package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
- package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
- package/client/dist/index.html +37 -3
- package/client/index.html +28 -1
- package/client/package.json +44 -40
- package/client/src/App.tsx +2 -0
- package/client/src/Dashboard.tsx +157 -45
- package/client/src/ParameterPanel.tsx +3 -5
- package/client/src/adapters/nodeSpecToDescription.ts +1 -0
- package/client/src/assets/icons/NodeIcon.tsx +32 -0
- package/client/src/assets/icons/index.ts +4 -0
- package/client/src/assets/icons/stripe.svg +1 -0
- package/client/src/assets/icons/themedGlyphs.ts +404 -0
- package/client/src/components/AIAgentNode.tsx +77 -53
- package/client/src/components/GenericNode.tsx +34 -52
- package/client/src/components/OutputPanel.tsx +64 -147
- package/client/src/components/ParameterRenderer.tsx +5 -3
- package/client/src/components/SkillEditorModal.tsx +9 -18
- package/client/src/components/SquareNode.tsx +97 -115
- package/client/src/components/StartNode.tsx +32 -42
- package/client/src/components/SvgFilterDefs.tsx +54 -0
- package/client/src/components/TeamMonitorNode.tsx +12 -14
- package/client/src/components/ToolkitNode.tsx +35 -60
- package/client/src/components/TriggerNode.tsx +43 -77
- package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
- package/client/src/components/credentials/CredentialsModal.tsx +98 -30
- package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
- package/client/src/components/credentials/catalogueAdapter.ts +17 -1
- package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
- package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
- package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
- package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
- package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
- package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
- package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
- package/client/src/components/credentials/types.ts +12 -2
- package/client/src/components/credentials/useCredentialPanel.ts +43 -19
- package/client/src/components/icons/AIProviderIcons.tsx +16 -0
- package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
- package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
- package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
- package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
- package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
- package/client/src/components/parameterPanel/InputSection.tsx +9 -7
- package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
- package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
- package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
- package/client/src/components/ui/AIResultModal.tsx +1 -1
- package/client/src/components/ui/CollapsibleSection.tsx +9 -5
- package/client/src/components/ui/CommandPalette.tsx +147 -0
- package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
- package/client/src/components/ui/ComponentItem.tsx +13 -7
- package/client/src/components/ui/ComponentPalette.tsx +24 -13
- package/client/src/components/ui/ConsolePanel.tsx +19 -11
- package/client/src/components/ui/DropCap.tsx +28 -0
- package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
- package/client/src/components/ui/InputNodesPanel.tsx +1 -1
- package/client/src/components/ui/Modal.tsx +38 -6
- package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
- package/client/src/components/ui/SettingsPanel.tsx +42 -13
- package/client/src/components/ui/StatusBar.tsx +108 -0
- package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
- package/client/src/components/ui/TopToolbar.tsx +42 -25
- package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
- package/client/src/components/ui/action-button.tsx +40 -15
- package/client/src/components/ui/button.tsx +24 -1
- package/client/src/components/ui/dropdown-menu.tsx +24 -2
- package/client/src/components/ui/input.tsx +19 -2
- package/client/src/components/ui/select.tsx +15 -0
- package/client/src/components/ui/textarea.tsx +15 -2
- package/client/src/contexts/AuthContext.tsx +148 -109
- package/client/src/contexts/ThemeContext.tsx +93 -17
- package/client/src/contexts/WebSocketContext.tsx +373 -206
- package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
- package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
- package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
- package/client/src/hooks/useAppTheme.ts +209 -7
- package/client/src/hooks/useAutoSkillEdges.ts +7 -2
- package/client/src/hooks/useCatalogueQuery.ts +67 -1
- package/client/src/hooks/useDragVariable.ts +1 -1
- package/client/src/hooks/useNodeAllowlist.ts +115 -8
- package/client/src/hooks/useOnboarding.ts +20 -8
- package/client/src/hooks/useParameterPanel.ts +2 -1
- package/client/src/hooks/useReactFlowNodes.ts +2 -1
- package/client/src/hooks/useSound.ts +185 -0
- package/client/src/hooks/useWorkflowManagement.ts +6 -8
- package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
- package/client/src/index.css +65 -3
- package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
- package/client/src/lib/aiModelProviders.ts +8 -0
- package/client/src/lib/connectionConfig.ts +107 -0
- package/client/src/lib/queryPersist.ts +13 -5
- package/client/src/lib/sound.ts +393 -0
- package/client/src/main.tsx +20 -0
- package/client/src/store/useAppStore.ts +26 -0
- package/client/src/styles/canvasAnimations.ts +37 -36
- package/client/src/styles/theme.ts +36 -20
- package/client/src/test/setup.ts +1 -0
- package/client/src/themes/atomic.css +253 -0
- package/client/src/themes/base.css +373 -0
- package/client/src/themes/cyber.css +890 -0
- package/client/src/themes/dark.css +70 -0
- package/client/src/themes/edo.css +246 -0
- package/client/src/themes/greek.css +293 -0
- package/client/src/themes/light.css +78 -0
- package/client/src/themes/plague.css +253 -0
- package/client/src/themes/renaissance.css +727 -0
- package/client/src/themes/rot.css +249 -0
- package/client/src/themes/steampunk.css +272 -0
- package/client/src/themes/surveillance.css +289 -0
- package/client/src/themes/wasteland.css +250 -0
- package/client/src/types/INodeProperties.ts +5 -0
- package/client/src/types/NodeTypes.ts +11 -1
- package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
- package/client/src/types/cloudEvents.ts +78 -0
- package/client/src/vite-env.d.ts +7 -0
- package/client/tsconfig.json +1 -1
- package/client/vite.config.js +62 -2
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/machina/commands/build.py +51 -7
- package/machina/pyproject.toml +4 -0
- package/machina/supervisor.py +12 -2
- package/machina/tree.py +71 -21
- package/package.json +4 -4
- package/scripts/install.js +16 -1
- package/server/config/ai_cli_providers.json +54 -0
- package/server/config/credential_providers.json +109 -2
- package/server/config/llm_defaults.json +24 -0
- package/server/config/model_registry.json +338 -499
- package/server/config/node_allowlist.json +16 -1
- package/server/config/pricing.json +8 -0
- package/server/constants.py +38 -15
- package/server/core/container.py +2 -2
- package/server/core/credentials_database.py +35 -2
- package/server/core/logging.py +4 -3
- package/server/main.py +99 -13
- package/server/models/node_metadata.py +1 -0
- package/server/nodejs/package.json +8 -6
- package/server/nodejs/src/index.ts +22 -5
- package/server/nodes/README.md +31 -4
- package/server/nodes/agent/_inline.py +2 -0
- package/server/nodes/agent/_specialized.py +6 -3
- package/server/nodes/agent/ai_agent.py +13 -3
- package/server/nodes/agent/chat_agent.py +6 -3
- package/server/nodes/agent/claude_code_agent.py +287 -75
- package/server/nodes/agent/codex_agent.py +239 -0
- package/server/nodes/agent/deep_agent.py +3 -3
- package/server/nodes/agent/rlm_agent.py +3 -3
- package/server/nodes/android/__init__.py +31 -1
- package/server/nodes/android/_base.py +9 -5
- package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
- package/server/nodes/android/_handlers.py +154 -0
- package/server/nodes/android/_option_loaders.py +44 -0
- package/server/nodes/android/_refresh.py +127 -0
- package/server/{services/android → nodes/android/_relay}/client.py +4 -4
- package/server/{routers/android.py → nodes/android/_router.py} +27 -8
- package/server/nodes/browser/browser.py +2 -2
- package/server/nodes/code/_base.py +6 -2
- package/server/nodes/code/_claude_code.py +134 -0
- package/server/nodes/document/embedding_generator.py +3 -3
- package/server/nodes/document/http_scraper.py +3 -3
- package/server/nodes/document/vector_store.py +5 -5
- package/server/nodes/email/__init__.py +11 -1
- package/server/nodes/email/_filters.py +21 -0
- package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
- package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
- package/server/nodes/email/email_read.py +1 -1
- package/server/nodes/email/email_receive.py +54 -5
- package/server/nodes/email/email_send.py +1 -1
- package/server/nodes/filesystem/shell.py +24 -1
- package/server/nodes/google/__init__.py +55 -1
- package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
- package/server/nodes/google/_base.py +2 -2
- package/server/nodes/google/_credentials.py +5 -5
- package/server/nodes/google/_filters.py +25 -0
- package/server/nodes/google/_handlers.py +57 -0
- package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
- package/server/nodes/google/_option_loaders.py +107 -0
- package/server/nodes/google/_refresh.py +66 -0
- package/server/nodes/google/_router.py +131 -0
- package/server/nodes/google/gmail_receive.py +41 -4
- package/server/nodes/groups.py +1 -0
- package/server/nodes/location/_credentials.py +45 -1
- package/server/{services/maps.py → nodes/location/_service.py} +18 -3
- package/server/nodes/location/gmaps_create.py +4 -4
- package/server/nodes/location/gmaps_locations.py +4 -4
- package/server/nodes/location/gmaps_nearby_places.py +4 -4
- package/server/nodes/model/_base.py +8 -3
- package/server/nodes/model/_credentials.py +96 -8
- package/server/nodes/model/_local_validator.py +345 -0
- package/server/nodes/model/lmstudio_chat_model.py +23 -0
- package/server/nodes/model/ollama_chat_model.py +25 -0
- package/server/nodes/proxy/_usage.py +2 -2
- package/server/nodes/proxy/proxy_config.py +14 -14
- package/server/nodes/proxy/proxy_request.py +4 -4
- package/server/nodes/scraper/_credentials.py +29 -1
- package/server/nodes/scraper/apify_actor.py +9 -9
- package/server/nodes/scraper/crawlee_scraper.py +5 -5
- package/server/nodes/search/brave_search.py +4 -0
- package/server/nodes/search/perplexity_search.py +9 -0
- package/server/nodes/search/serper_search.py +3 -0
- package/server/nodes/skill/simple_memory.py +12 -0
- package/server/nodes/social/_base.py +2 -2
- package/server/nodes/stripe/__init__.py +46 -0
- package/server/nodes/stripe/_credentials.py +33 -0
- package/server/nodes/stripe/_handlers.py +270 -0
- package/server/nodes/stripe/_install.py +127 -0
- package/server/nodes/stripe/_source.py +174 -0
- package/server/nodes/stripe/stripe_action.py +81 -0
- package/server/nodes/stripe/stripe_receive.py +92 -0
- package/server/nodes/telegram/_credentials.py +52 -1
- package/server/nodes/telegram/_handlers.py +19 -18
- package/server/nodes/telegram/_service.py +134 -32
- package/server/nodes/telegram/telegram_send.py +5 -6
- package/server/nodes/text/file_handler.py +2 -2
- package/server/nodes/text/text_generator.py +2 -2
- package/server/nodes/tool/agent_builder.py +630 -0
- package/server/nodes/tool/task_manager.py +144 -2
- package/server/nodes/twitter/__init__.py +38 -1
- package/server/nodes/twitter/_base.py +7 -7
- package/server/nodes/twitter/_credentials.py +1 -1
- package/server/nodes/twitter/_filters.py +37 -0
- package/server/nodes/twitter/_handlers.py +77 -0
- package/server/nodes/twitter/_oauth.py +124 -0
- package/server/nodes/twitter/_refresh.py +78 -0
- package/server/nodes/twitter/_router.py +29 -0
- package/server/nodes/twitter/twitter_receive.py +4 -0
- package/server/nodes/visuals.json +64 -19
- package/server/nodes/whatsapp/__init__.py +45 -5
- package/server/nodes/whatsapp/_base.py +3 -3
- package/server/nodes/whatsapp/_filters.py +137 -0
- package/server/nodes/whatsapp/_handlers.py +167 -0
- package/server/nodes/whatsapp/_option_loaders.py +68 -0
- package/server/nodes/whatsapp/_refresh.py +62 -0
- package/server/nodes/whatsapp/_runtime.py +1 -1
- package/server/pyproject.toml +29 -7
- package/server/routers/schemas.py +2 -2
- package/server/routers/webhook.py +26 -9
- package/server/routers/websocket.py +149 -810
- package/server/services/ai.py +89 -8
- package/server/services/auth.py +220 -43
- package/server/services/claude_oauth.py +126 -100
- package/server/services/cli_agent/__init__.py +78 -0
- package/server/services/cli_agent/_handlers.py +237 -0
- package/server/services/cli_agent/config.py +112 -0
- package/server/services/cli_agent/factory.py +48 -0
- package/server/services/cli_agent/lockfile.py +141 -0
- package/server/services/cli_agent/mcp_server.py +482 -0
- package/server/services/cli_agent/protocol.py +173 -0
- package/server/services/cli_agent/providers/__init__.py +9 -0
- package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
- package/server/services/cli_agent/providers/google_gemini.py +80 -0
- package/server/services/cli_agent/providers/openai_codex.py +310 -0
- package/server/services/cli_agent/service.py +607 -0
- package/server/services/cli_agent/session.py +618 -0
- package/server/services/cli_agent/types.py +227 -0
- package/server/services/cli_agent/workflow_tools.py +233 -0
- package/server/services/credential_registry.py +26 -1
- package/server/services/deployment/manager.py +26 -145
- package/server/services/deployment/poll_registry.py +59 -0
- package/server/services/event_waiter.py +76 -246
- package/server/services/events/__init__.py +54 -0
- package/server/services/events/cli.py +78 -0
- package/server/services/events/daemon.py +163 -0
- package/server/services/events/envelope.py +281 -0
- package/server/services/events/lifecycle.py +99 -0
- package/server/services/events/oauth_lifecycle.py +534 -0
- package/server/services/events/polling.py +60 -0
- package/server/services/events/push.py +36 -0
- package/server/services/events/source.py +63 -0
- package/server/services/events/triggers.py +118 -0
- package/server/services/events/verifiers/__init__.py +25 -0
- package/server/services/events/verifiers/base.py +28 -0
- package/server/services/events/verifiers/github.py +25 -0
- package/server/services/events/verifiers/hmac_basic.py +32 -0
- package/server/services/events/verifiers/standard_webhooks.py +47 -0
- package/server/services/events/verifiers/stripe.py +42 -0
- package/server/services/events/webhook.py +105 -0
- package/server/services/handlers/tools.py +28 -186
- package/server/services/llm/config.py +7 -0
- package/server/services/llm/factory.py +8 -2
- package/server/services/memory/__init__.py +52 -0
- package/server/services/memory/jsonl.py +80 -0
- package/server/services/memory/markdown.py +65 -0
- package/server/services/memory/state.py +112 -0
- package/server/services/memory/vector_store.py +40 -0
- package/server/services/model_registry.py +76 -0
- package/server/services/node_allowlist.py +71 -15
- package/server/services/node_executor.py +2 -2
- package/server/services/node_output_schemas.py +21 -10
- package/server/services/node_spec.py +1 -1
- package/server/services/oauth_utils.py +1 -1
- package/server/services/plugin/__init__.py +2 -0
- package/server/services/plugin/base.py +44 -2
- package/server/services/plugin/credential.py +288 -1
- package/server/services/plugin/deps.py +105 -0
- package/server/services/plugin/edge_walker.py +12 -4
- package/server/services/plugin/oauth.py +381 -0
- package/server/services/plugin/polling.py +247 -0
- package/server/services/plugin/registry.py +145 -0
- package/server/services/plugin/singleton.py +65 -0
- package/server/services/plugin/ws.py +81 -0
- package/server/services/process_service.py +31 -2
- package/server/services/status_broadcaster.py +155 -238
- package/server/services/temporal/workflow.py +7 -7
- package/server/services/workflow.py +21 -3
- package/server/services/ws_handler_registry.py +111 -28
- package/server/skills/GUIDE.md +16 -1
- package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
- package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
- package/server/tests/credentials/test_auth_service.py +16 -9
- package/server/tests/credentials/test_credential_broadcasts.py +219 -0
- package/server/tests/credentials/test_google_oauth.py +6 -6
- package/server/tests/credentials/test_oauth_utils.py +1 -1
- package/server/tests/credentials/test_twitter_oauth.py +2 -2
- package/server/tests/credentials/test_websocket_handlers.py +44 -20
- package/server/tests/llm/test_factory.py +1 -0
- package/server/tests/llm/test_wiring.py +5 -1
- package/server/tests/nodes/_compat.py +24 -24
- package/server/tests/nodes/test_agent_builder.py +439 -0
- package/server/tests/nodes/test_ai_tools.py +18 -14
- package/server/tests/nodes/test_code_fs_process.py +17 -8
- package/server/tests/nodes/test_email.py +10 -9
- package/server/tests/nodes/test_google_workspace.py +2 -2
- package/server/tests/nodes/test_specialized_agents.py +100 -53
- package/server/tests/nodes/test_stripe_plugin.py +293 -0
- package/server/tests/nodes/test_telegram_social.py +4 -4
- package/server/tests/nodes/test_twitter.py +1 -1
- package/server/tests/nodes/test_web_automation.py +2 -2
- package/server/tests/nodes/test_whatsapp.py +9 -9
- package/server/tests/services/cli_agent/__init__.py +0 -0
- package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
- package/server/tests/services/cli_agent/test_providers.py +358 -0
- package/server/tests/services/cli_agent/test_service.py +298 -0
- package/server/tests/services/memory/__init__.py +0 -0
- package/server/tests/services/memory/test_jsonl.py +188 -0
- package/server/tests/services/test_events.py +333 -0
- package/server/tests/test_node_spec.py +56 -16
- package/server/tests/test_plugin_helpers.py +116 -0
- package/server/tests/test_plugin_self_containment.py +486 -0
- package/server/tests/test_status_broadcasts.py +425 -0
- package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
- package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
- package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
- package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
- package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
- package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
- package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
- package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
- package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
- package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
- package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
- package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
- package/client/dist/assets/index-D9tZfgvi.js +0 -363
- package/client/dist/assets/index-al7snTkG.css +0 -1
- package/client/src/components/credentials/providers.tsx +0 -177
- package/server/routers/google.py +0 -277
- package/server/routers/maps.py +0 -142
- package/server/routers/twitter.py +0 -365
- package/server/services/claude_code_service.py +0 -106
- package/server/services/memory.py +0 -159
- package/server/services/node_option_loaders/__init__.py +0 -77
- package/server/services/node_option_loaders/android_loaders.py +0 -55
- package/server/services/node_option_loaders/google_loaders.py +0 -97
- package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
- package/server/services/twitter_oauth.py +0 -411
- package/server/services/websocket_client.py +0 -29
- /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
- /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
- /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
- /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
|
@@ -96,15 +96,22 @@ class StatusBroadcaster:
|
|
|
96
96
|
async def connect(self, websocket: WebSocket):
|
|
97
97
|
"""Accept a new WebSocket connection.
|
|
98
98
|
|
|
99
|
-
Sends cached status immediately
|
|
100
|
-
|
|
99
|
+
Sends the cached status snapshot immediately. Status changes
|
|
100
|
+
flow in via the originating code path (WhatsApp Go-service
|
|
101
|
+
events, Telegram bot connect/disconnect, OAuth callbacks,
|
|
102
|
+
Android relay events) -- the cache is kept fresh by those
|
|
103
|
+
event-driven broadcasts, so the connecting client doesn't need
|
|
104
|
+
a per-connect refresh-all storm.
|
|
105
|
+
|
|
106
|
+
Initial cache population (and load-bearing auto-reconnects for
|
|
107
|
+
Telegram + Android relay) happens in a one-time lifespan-startup
|
|
108
|
+
invocation of :meth:`_refresh_all_services` from ``main.py``.
|
|
101
109
|
"""
|
|
102
110
|
await websocket.accept()
|
|
103
111
|
async with self._lock:
|
|
104
112
|
self._connections.add(websocket)
|
|
105
113
|
logger.info(f"[StatusBroadcaster] Client connected. Total: {len(self._connections)}")
|
|
106
114
|
|
|
107
|
-
# Send cached status immediately -- don't block on service refreshes
|
|
108
115
|
try:
|
|
109
116
|
await websocket.send_json({
|
|
110
117
|
"type": "initial_status",
|
|
@@ -113,8 +120,45 @@ class StatusBroadcaster:
|
|
|
113
120
|
except Exception as e:
|
|
114
121
|
logger.error(f"[StatusBroadcaster] Failed to send initial status: {e}")
|
|
115
122
|
|
|
116
|
-
#
|
|
117
|
-
|
|
123
|
+
# Reconcile snapshot — every fresh client gets the
|
|
124
|
+
# currently-running-deployments truth so a stale FE
|
|
125
|
+
# `deploymentStatus.isRunning=true` (carried forward through a
|
|
126
|
+
# backend restart that wiped DeploymentManager._deployments)
|
|
127
|
+
# gets cleared. CloudEvents-shaped envelope; empty list is
|
|
128
|
+
# meaningful and triggers FE-side reset.
|
|
129
|
+
try:
|
|
130
|
+
await self._send_deployment_snapshot(websocket)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error(f"[StatusBroadcaster] Failed to send deployment snapshot: {e}")
|
|
133
|
+
|
|
134
|
+
async def _send_deployment_snapshot(self, websocket: WebSocket) -> None:
|
|
135
|
+
"""Build + send a CloudEvents deployment_snapshot to one client.
|
|
136
|
+
|
|
137
|
+
Lazy container resolution because the broadcaster is constructed
|
|
138
|
+
before WorkflowService and we do not want a circular dependency
|
|
139
|
+
at module-load time.
|
|
140
|
+
"""
|
|
141
|
+
try:
|
|
142
|
+
from core.container import container
|
|
143
|
+
from services.events import WorkflowEvent
|
|
144
|
+
|
|
145
|
+
workflow_service = container.workflow_service()
|
|
146
|
+
dm = workflow_service._get_deployment_manager()
|
|
147
|
+
running_ids = dm.get_deployed_workflows()
|
|
148
|
+
except Exception as e:
|
|
149
|
+
# Backend startup race or DI not wired yet -- skip the snapshot
|
|
150
|
+
# rather than fail the connect entirely. The empty-list path
|
|
151
|
+
# would still be safer than crashing here.
|
|
152
|
+
logger.debug(
|
|
153
|
+
f"[StatusBroadcaster] DeploymentManager unavailable for snapshot: {e}",
|
|
154
|
+
)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
event = WorkflowEvent.deployment_snapshot(running_ids)
|
|
158
|
+
await websocket.send_json({
|
|
159
|
+
"type": "deployment_snapshot",
|
|
160
|
+
"data": event.model_dump(mode="json"),
|
|
161
|
+
})
|
|
118
162
|
|
|
119
163
|
async def disconnect(self, websocket: WebSocket):
|
|
120
164
|
"""Remove a WebSocket connection."""
|
|
@@ -123,20 +167,21 @@ class StatusBroadcaster:
|
|
|
123
167
|
logger.info(f"[StatusBroadcaster] Client disconnected. Total: {len(self._connections)}")
|
|
124
168
|
|
|
125
169
|
async def _refresh_all_services(self):
|
|
126
|
-
"""
|
|
127
|
-
its own status is ready.
|
|
170
|
+
"""Fan out plugin-registered refresh callbacks.
|
|
128
171
|
|
|
129
172
|
Uses ``asyncio.TaskGroup`` (Python 3.11+) for structured
|
|
130
173
|
concurrency: every refresh runs as an independent task and
|
|
131
174
|
publishes its own ``<service>_status`` message the moment its
|
|
132
175
|
cache slot is populated. The slowest service no longer gates
|
|
133
|
-
the others
|
|
176
|
+
the others -- the credentials/status UI hydrates incrementally.
|
|
134
177
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
178
|
+
Wave 11.I, milestone J: every refresher now lives in its
|
|
179
|
+
plugin folder (``nodes/<plugin>/_refresh.py``) and registers
|
|
180
|
+
via :func:`register_service_refresh`. The broadcaster has zero
|
|
181
|
+
per-plugin knowledge in this dispatch loop. Wave 11.I, V: this
|
|
182
|
+
method runs ONCE at FastAPI lifespan startup (no longer per
|
|
183
|
+
WS-client connect). State changes after that flow through the
|
|
184
|
+
originating code path's event-driven broadcasts.
|
|
140
185
|
|
|
141
186
|
Each callback swallows its own exceptions, so TaskGroup never
|
|
142
187
|
sees one. Kept inside try/except* defensively in case a future
|
|
@@ -145,11 +190,6 @@ class StatusBroadcaster:
|
|
|
145
190
|
with tracer.start_as_current_span("broadcaster.refresh_all_services"):
|
|
146
191
|
try:
|
|
147
192
|
async with asyncio.TaskGroup() as tg:
|
|
148
|
-
tg.create_task(self._refresh_whatsapp_status())
|
|
149
|
-
tg.create_task(self._refresh_twitter_status())
|
|
150
|
-
tg.create_task(self._refresh_google_status())
|
|
151
|
-
tg.create_task(self._auto_reconnect_android_relay())
|
|
152
|
-
# Plugin-registered refreshes (telegram, future ones)
|
|
153
193
|
for callback in list(_SERVICE_REFRESH_CALLBACKS):
|
|
154
194
|
tg.create_task(callback(self))
|
|
155
195
|
except* Exception as eg:
|
|
@@ -230,6 +270,87 @@ class StatusBroadcaster:
|
|
|
230
270
|
"""Get API key validation status for a provider."""
|
|
231
271
|
return self._status["api_keys"].get(provider)
|
|
232
272
|
|
|
273
|
+
# =========================================================================
|
|
274
|
+
# Credential Mutation Broadcasts (CloudEvents v1.0)
|
|
275
|
+
# =========================================================================
|
|
276
|
+
#
|
|
277
|
+
# Every backend handler that mutates credential state (store / remove API
|
|
278
|
+
# keys, OAuth login / logout) MUST emit a credential event via this helper
|
|
279
|
+
# so the frontend `useCatalogueQuery` cache stays coherent across all
|
|
280
|
+
# connected clients. Wire-format key stays `credential_catalogue_updated`
|
|
281
|
+
# for FE back-compat; the body is the CloudEvents-shaped `WorkflowEvent`
|
|
282
|
+
# so future EventBridge / Knative interop is a JSON-schema swap.
|
|
283
|
+
#
|
|
284
|
+
# Pytest invariant `test_credential_broadcasts.py` locks the contract.
|
|
285
|
+
|
|
286
|
+
async def broadcast_credential_event(
|
|
287
|
+
self,
|
|
288
|
+
event_type: str,
|
|
289
|
+
*,
|
|
290
|
+
provider: str,
|
|
291
|
+
customer_id: Optional[str] = None,
|
|
292
|
+
) -> None:
|
|
293
|
+
"""Emit a CloudEvents-typed credential-mutation broadcast.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
event_type: CloudEvents `type` field. Convention:
|
|
297
|
+
``"credential.api_key.saved"`` / ``".deleted"``,
|
|
298
|
+
``"credential.oauth.disconnected"``.
|
|
299
|
+
provider: Provider id (e.g. ``"openai"``, ``"twitter"``).
|
|
300
|
+
customer_id: For multi-tenant OAuth flows. Default omitted.
|
|
301
|
+
"""
|
|
302
|
+
# Local import keeps the broadcaster module independent of the
|
|
303
|
+
# event framework's load order during startup.
|
|
304
|
+
from services.events import WorkflowEvent
|
|
305
|
+
|
|
306
|
+
event = WorkflowEvent(
|
|
307
|
+
source="machinaos://services/credentials",
|
|
308
|
+
type=event_type,
|
|
309
|
+
subject=provider,
|
|
310
|
+
data={
|
|
311
|
+
"provider": provider,
|
|
312
|
+
**({"customer_id": customer_id} if customer_id else {}),
|
|
313
|
+
},
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
await self.broadcast({
|
|
317
|
+
"type": "credential_catalogue_updated",
|
|
318
|
+
"data": event.model_dump(mode="json"),
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
async def broadcast_agent_progress(
|
|
322
|
+
self,
|
|
323
|
+
node_id: str,
|
|
324
|
+
*,
|
|
325
|
+
workflow_id: Optional[str],
|
|
326
|
+
iteration: int,
|
|
327
|
+
max_iterations: int,
|
|
328
|
+
phase: Optional[str] = None,
|
|
329
|
+
) -> None:
|
|
330
|
+
"""Emit a CloudEvents-typed agent-progress event.
|
|
331
|
+
|
|
332
|
+
Wire key ``agent_progress`` is a parallel channel to
|
|
333
|
+
``node_status``: same per-node scope, but the inner payload is a
|
|
334
|
+
full CloudEvents envelope instead of a raw dict. The FE routes
|
|
335
|
+
envelope.data into ``nodeStatusStore`` so the existing
|
|
336
|
+
``useNodeStatus`` consumers see ``iteration`` /
|
|
337
|
+
``max_iterations`` without a separate store.
|
|
338
|
+
"""
|
|
339
|
+
from services.events import WorkflowEvent
|
|
340
|
+
|
|
341
|
+
event = WorkflowEvent.agent_progress(
|
|
342
|
+
node_id,
|
|
343
|
+
workflow_id=workflow_id,
|
|
344
|
+
iteration=iteration,
|
|
345
|
+
max_iterations=max_iterations,
|
|
346
|
+
phase=phase,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
await self.broadcast({
|
|
350
|
+
"type": "agent_progress",
|
|
351
|
+
"data": event.model_dump(mode="json"),
|
|
352
|
+
})
|
|
353
|
+
|
|
233
354
|
# =========================================================================
|
|
234
355
|
# Android Status Updates
|
|
235
356
|
# =========================================================================
|
|
@@ -263,218 +384,14 @@ class StatusBroadcaster:
|
|
|
263
384
|
})
|
|
264
385
|
|
|
265
386
|
# =========================================================================
|
|
266
|
-
#
|
|
387
|
+
# Status updates -- per-service refresh bodies live in their plugin
|
|
388
|
+
# folders (Wave 11.I, milestone J): nodes/<plugin>/_refresh.py.
|
|
389
|
+
# The plugin's __init__.py self-registers via
|
|
390
|
+
# services.status_broadcaster.register_service_refresh; the central
|
|
391
|
+
# _refresh_all_services fans out to every registered callback with
|
|
392
|
+
# zero per-plugin knowledge.
|
|
267
393
|
# =========================================================================
|
|
268
394
|
|
|
269
|
-
async def _refresh_whatsapp_status(self):
|
|
270
|
-
"""Fetch fresh WhatsApp status from Go service and update cache.
|
|
271
|
-
|
|
272
|
-
Called on client connect to ensure initial_status has accurate data.
|
|
273
|
-
Silently fails if WhatsApp service is unavailable. Span emitted
|
|
274
|
-
via OTel for cold-start benchmarking.
|
|
275
|
-
"""
|
|
276
|
-
with tracer.start_as_current_span("broadcaster.refresh_whatsapp") as span:
|
|
277
|
-
try:
|
|
278
|
-
from services.whatsapp_service import get_client
|
|
279
|
-
import time
|
|
280
|
-
|
|
281
|
-
client = await get_client()
|
|
282
|
-
status_data = await client.call("status")
|
|
283
|
-
|
|
284
|
-
self._status["whatsapp"] = {
|
|
285
|
-
"connected": status_data.get("connected", False),
|
|
286
|
-
"has_session": status_data.get("has_session", False),
|
|
287
|
-
"running": status_data.get("running", False),
|
|
288
|
-
"pairing": status_data.get("pairing", False),
|
|
289
|
-
"device_id": status_data.get("device_id"),
|
|
290
|
-
"qr": None,
|
|
291
|
-
"timestamp": time.time()
|
|
292
|
-
}
|
|
293
|
-
logger.debug(f"[StatusBroadcaster] Refreshed WhatsApp status: connected={status_data.get('connected')}")
|
|
294
|
-
await self.broadcast({
|
|
295
|
-
"type": "whatsapp_status",
|
|
296
|
-
"data": self._status["whatsapp"],
|
|
297
|
-
})
|
|
298
|
-
span.set_attribute("connected", bool(status_data.get("connected", False)))
|
|
299
|
-
except Exception as e:
|
|
300
|
-
span.record_exception(e)
|
|
301
|
-
# Don't fail client connection if WhatsApp service is down
|
|
302
|
-
logger.debug(f"[StatusBroadcaster] Could not refresh WhatsApp status: {e}")
|
|
303
|
-
|
|
304
|
-
async def _refresh_twitter_status(self):
|
|
305
|
-
"""Fetch Twitter status from stored OAuth tokens in database.
|
|
306
|
-
|
|
307
|
-
Called on client connect to check if user is authenticated with Twitter.
|
|
308
|
-
Uses OAuth token system (matches REST endpoint and node handlers).
|
|
309
|
-
Span emitted via OTel for cold-start benchmarking.
|
|
310
|
-
"""
|
|
311
|
-
with tracer.start_as_current_span("broadcaster.refresh_twitter") as span:
|
|
312
|
-
try:
|
|
313
|
-
from core.container import container
|
|
314
|
-
auth_service = container.auth_service()
|
|
315
|
-
|
|
316
|
-
# Get tokens from OAuth system (NOT api_key system)
|
|
317
|
-
tokens = await auth_service.get_oauth_tokens("twitter", customer_id="owner")
|
|
318
|
-
if not tokens or not tokens.get("access_token"):
|
|
319
|
-
self._status["twitter"] = {
|
|
320
|
-
"connected": False,
|
|
321
|
-
"username": None,
|
|
322
|
-
"user_id": None,
|
|
323
|
-
"name": None,
|
|
324
|
-
"profile_image_url": None
|
|
325
|
-
}
|
|
326
|
-
else:
|
|
327
|
-
# User info is stored in the OAuth token record
|
|
328
|
-
email = tokens.get("email", "") # Stored as "@username"
|
|
329
|
-
name = tokens.get("name", "")
|
|
330
|
-
username = email.lstrip("@") if email.startswith("@") else email
|
|
331
|
-
|
|
332
|
-
self._status["twitter"] = {
|
|
333
|
-
"connected": True,
|
|
334
|
-
"username": username or None,
|
|
335
|
-
"user_id": None,
|
|
336
|
-
"name": name or None,
|
|
337
|
-
"profile_image_url": None
|
|
338
|
-
}
|
|
339
|
-
logger.debug(f"[StatusBroadcaster] Twitter status: connected as @{username}")
|
|
340
|
-
|
|
341
|
-
await self.broadcast({
|
|
342
|
-
"type": "twitter_status",
|
|
343
|
-
"data": self._status["twitter"],
|
|
344
|
-
})
|
|
345
|
-
span.set_attribute("connected", bool(self._status["twitter"]["connected"]))
|
|
346
|
-
except Exception as e:
|
|
347
|
-
span.record_exception(e)
|
|
348
|
-
logger.debug(f"[StatusBroadcaster] Could not refresh Twitter status: {e}")
|
|
349
|
-
|
|
350
|
-
async def _refresh_google_status(self):
|
|
351
|
-
"""Fetch Google Workspace status from stored OAuth tokens.
|
|
352
|
-
|
|
353
|
-
Called on client connect to check if user is authenticated with Google.
|
|
354
|
-
Span emitted via OTel for cold-start benchmarking.
|
|
355
|
-
"""
|
|
356
|
-
with tracer.start_as_current_span("broadcaster.refresh_google") as span:
|
|
357
|
-
try:
|
|
358
|
-
from core.container import container
|
|
359
|
-
auth_service = container.auth_service()
|
|
360
|
-
|
|
361
|
-
tokens = await auth_service.get_oauth_tokens("google", customer_id="owner")
|
|
362
|
-
if not tokens or not tokens.get("access_token"):
|
|
363
|
-
self._status["google"] = {
|
|
364
|
-
"connected": False,
|
|
365
|
-
"email": None,
|
|
366
|
-
"name": None,
|
|
367
|
-
}
|
|
368
|
-
else:
|
|
369
|
-
self._status["google"] = {
|
|
370
|
-
"connected": True,
|
|
371
|
-
"email": tokens.get("email"),
|
|
372
|
-
"name": tokens.get("name"),
|
|
373
|
-
}
|
|
374
|
-
logger.debug(f"[StatusBroadcaster] Google status: connected as {tokens.get('email')}")
|
|
375
|
-
|
|
376
|
-
await self.broadcast({
|
|
377
|
-
"type": "google_status",
|
|
378
|
-
"data": self._status["google"],
|
|
379
|
-
})
|
|
380
|
-
span.set_attribute("connected", bool(self._status["google"]["connected"]))
|
|
381
|
-
except Exception as e:
|
|
382
|
-
span.record_exception(e)
|
|
383
|
-
logger.debug(f"[StatusBroadcaster] Could not refresh Google status: {e}")
|
|
384
|
-
|
|
385
|
-
async def _auto_reconnect_android_relay(self):
|
|
386
|
-
"""Auto-reconnect to Android relay if there's a stored pairing session.
|
|
387
|
-
|
|
388
|
-
Called on client connect to re-establish relay connection after server restart.
|
|
389
|
-
The stored session contains relay URL, API key, and paired device info.
|
|
390
|
-
Span emitted via OTel for cold-start benchmarking.
|
|
391
|
-
"""
|
|
392
|
-
with tracer.start_as_current_span("broadcaster.refresh_android") as span:
|
|
393
|
-
await self._auto_reconnect_android_relay_body(span)
|
|
394
|
-
|
|
395
|
-
async def _auto_reconnect_android_relay_body(self, span):
|
|
396
|
-
try:
|
|
397
|
-
# Check if already connected
|
|
398
|
-
from services.android.manager import get_current_relay_client
|
|
399
|
-
existing = get_current_relay_client()
|
|
400
|
-
if existing and existing.is_connected():
|
|
401
|
-
# Already connected, just refresh status
|
|
402
|
-
self._status["android"] = {
|
|
403
|
-
"connected": True,
|
|
404
|
-
"paired": existing.is_paired(),
|
|
405
|
-
"device_id": existing.paired_device_id,
|
|
406
|
-
"device_name": existing.paired_device_name,
|
|
407
|
-
"connected_devices": list(existing.get_connected_devices()),
|
|
408
|
-
"connection_type": "relay",
|
|
409
|
-
"qr_data": existing.qr_data,
|
|
410
|
-
"session_token": existing.session_token
|
|
411
|
-
}
|
|
412
|
-
logger.debug("[StatusBroadcaster] Android relay already connected")
|
|
413
|
-
await self.broadcast({
|
|
414
|
-
"type": "android_status",
|
|
415
|
-
"data": self._status["android"],
|
|
416
|
-
})
|
|
417
|
-
span.set_attribute("path", "already_connected")
|
|
418
|
-
return
|
|
419
|
-
|
|
420
|
-
# Check for stored session
|
|
421
|
-
from core.container import container
|
|
422
|
-
database = container.database()
|
|
423
|
-
|
|
424
|
-
session = await database.get_android_relay_session()
|
|
425
|
-
if not session:
|
|
426
|
-
span.set_attribute("path", "no_session")
|
|
427
|
-
logger.debug("[StatusBroadcaster] No stored Android relay session")
|
|
428
|
-
return
|
|
429
|
-
|
|
430
|
-
relay_url = session.get("relay_url")
|
|
431
|
-
api_key = session.get("api_key")
|
|
432
|
-
device_id = session.get("device_id")
|
|
433
|
-
session.get("device_name")
|
|
434
|
-
|
|
435
|
-
if not relay_url or not api_key:
|
|
436
|
-
span.set_attribute("path", "session_missing_creds")
|
|
437
|
-
logger.debug("[StatusBroadcaster] Stored session missing relay URL or API key")
|
|
438
|
-
return
|
|
439
|
-
|
|
440
|
-
span.set_attribute("path", "auto_reconnect")
|
|
441
|
-
logger.info("[StatusBroadcaster] Auto-reconnecting to Android relay...",
|
|
442
|
-
relay_url=relay_url, device_id=device_id)
|
|
443
|
-
|
|
444
|
-
# Attempt to reconnect
|
|
445
|
-
from services.android.manager import get_relay_client
|
|
446
|
-
client, error = await get_relay_client(relay_url, api_key)
|
|
447
|
-
|
|
448
|
-
if client and client.is_connected():
|
|
449
|
-
logger.info("[StatusBroadcaster] Android relay reconnected successfully")
|
|
450
|
-
# Update status - connected to relay but need to check if still paired
|
|
451
|
-
# The relay server creates a new session on each connect, so pairing is lost
|
|
452
|
-
# Update the cached status to reflect the current state
|
|
453
|
-
self._status["android"] = {
|
|
454
|
-
"connected": True,
|
|
455
|
-
"paired": client.is_paired(),
|
|
456
|
-
"device_id": client.paired_device_id,
|
|
457
|
-
"device_name": client.paired_device_name,
|
|
458
|
-
"connected_devices": list(client.get_connected_devices()),
|
|
459
|
-
"connection_type": "relay",
|
|
460
|
-
"qr_data": client.qr_data,
|
|
461
|
-
"session_token": client.session_token
|
|
462
|
-
}
|
|
463
|
-
await self.broadcast({
|
|
464
|
-
"type": "android_status",
|
|
465
|
-
"data": self._status["android"],
|
|
466
|
-
})
|
|
467
|
-
span.set_attribute("reconnect_ok", True)
|
|
468
|
-
else:
|
|
469
|
-
span.set_attribute("reconnect_ok", False)
|
|
470
|
-
logger.warning(f"[StatusBroadcaster] Failed to reconnect Android relay: {error}")
|
|
471
|
-
# Clear the stored session since reconnect failed
|
|
472
|
-
await database.clear_android_relay_session()
|
|
473
|
-
|
|
474
|
-
except Exception as e:
|
|
475
|
-
span.record_exception(e)
|
|
476
|
-
logger.debug(f"[StatusBroadcaster] Could not auto-reconnect Android relay: {e}")
|
|
477
|
-
|
|
478
395
|
async def update_whatsapp_status(
|
|
479
396
|
self,
|
|
480
397
|
connected: bool,
|
|
@@ -501,10 +418,6 @@ class StatusBroadcaster:
|
|
|
501
418
|
"data": self._status["whatsapp"]
|
|
502
419
|
})
|
|
503
420
|
|
|
504
|
-
def get_whatsapp_status(self) -> Dict[str, Any]:
|
|
505
|
-
"""Get WhatsApp connection status."""
|
|
506
|
-
return self._status["whatsapp"].copy()
|
|
507
|
-
|
|
508
421
|
# =========================================================================
|
|
509
422
|
# Telegram Status Updates
|
|
510
423
|
# =========================================================================
|
|
@@ -533,10 +446,6 @@ class StatusBroadcaster:
|
|
|
533
446
|
"data": self._status["telegram"]
|
|
534
447
|
})
|
|
535
448
|
|
|
536
|
-
def get_telegram_status(self) -> Dict[str, Any]:
|
|
537
|
-
"""Get Telegram bot connection status."""
|
|
538
|
-
return self._status["telegram"].copy()
|
|
539
|
-
|
|
540
449
|
# =========================================================================
|
|
541
450
|
# Node Status Updates
|
|
542
451
|
# =========================================================================
|
|
@@ -1221,6 +1130,15 @@ _ServiceRefreshCallback = _typing.Callable[
|
|
|
1221
1130
|
]
|
|
1222
1131
|
_SERVICE_REFRESH_CALLBACKS: _typing.List[_ServiceRefreshCallback] = []
|
|
1223
1132
|
|
|
1133
|
+
from services.plugin.registry import IdempotentList as _IdempotentList # noqa: E402
|
|
1134
|
+
|
|
1135
|
+
# Backed by the module-level _SERVICE_REFRESH_CALLBACKS list so the
|
|
1136
|
+
# existing iterator at line 153 (``for callback in list(_SERVICE_REFRESH_CALLBACKS)``)
|
|
1137
|
+
# keeps working unchanged.
|
|
1138
|
+
_SERVICE_REFRESH_FANOUT: _IdempotentList[_ServiceRefreshCallback] = _IdempotentList(
|
|
1139
|
+
"service_refresh", items=_SERVICE_REFRESH_CALLBACKS
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1224
1142
|
|
|
1225
1143
|
def register_service_refresh(callback: _ServiceRefreshCallback) -> None:
|
|
1226
1144
|
"""Register a per-service refresh callback.
|
|
@@ -1229,8 +1147,7 @@ def register_service_refresh(callback: _ServiceRefreshCallback) -> None:
|
|
|
1229
1147
|
callback runs once per ``_refresh_all_services()`` cycle (i.e. on
|
|
1230
1148
|
every WebSocket client connect).
|
|
1231
1149
|
"""
|
|
1232
|
-
|
|
1233
|
-
_SERVICE_REFRESH_CALLBACKS.append(callback)
|
|
1150
|
+
_SERVICE_REFRESH_FANOUT.register(callback)
|
|
1234
1151
|
|
|
1235
1152
|
|
|
1236
1153
|
# Global singleton instance
|
|
@@ -25,13 +25,13 @@ CONFIG_HANDLES = {"input-tools", "input-memory", "input-model", "input-skill", "
|
|
|
25
25
|
# Trigger node types — event listeners that should never be scheduled
|
|
26
26
|
# as blocking activities. Imported from constants to avoid drift (was
|
|
27
27
|
# previously redefined here with a "keep in sync" comment — Wave 11.E.2).
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
# Android service types follow the same pattern: imported from constants
|
|
29
|
+
# so the canonical 16-entry list (Wave 11.I, milestone P -- the local
|
|
30
|
+
# 6-entry copy that lived here was a stale subset).
|
|
31
|
+
from constants import (
|
|
32
|
+
ANDROID_SERVICE_NODE_TYPES as ANDROID_SERVICE_TYPES,
|
|
33
|
+
WORKFLOW_TRIGGER_TYPES as TRIGGER_NODE_TYPES,
|
|
34
|
+
)
|
|
35
35
|
|
|
36
36
|
# Skill node types (connect to Zeenie's input-skill, not executed directly)
|
|
37
37
|
SKILL_NODE_TYPES = {
|
|
@@ -27,9 +27,9 @@ if TYPE_CHECKING:
|
|
|
27
27
|
from core.database import Database
|
|
28
28
|
from core.cache import CacheService
|
|
29
29
|
from services.ai import AIService
|
|
30
|
-
from
|
|
30
|
+
from nodes.location._service import MapsService
|
|
31
31
|
from services.text import TextService
|
|
32
|
-
from
|
|
32
|
+
from nodes.android._dispatcher import AndroidService
|
|
33
33
|
from services.temporal import TemporalExecutor
|
|
34
34
|
|
|
35
35
|
logger = get_logger(__name__)
|
|
@@ -461,7 +461,25 @@ class WorkflowService:
|
|
|
461
461
|
workflow_id: Specific workflow to cancel. If None, cancels first running deployment.
|
|
462
462
|
"""
|
|
463
463
|
manager = self._get_deployment_manager()
|
|
464
|
-
|
|
464
|
+
result = await manager.cancel(workflow_id)
|
|
465
|
+
|
|
466
|
+
# Also cancel any in-flight CLI agent batches (claude_code_agent /
|
|
467
|
+
# codex_agent) for this workflow. Best-effort — failure to cancel
|
|
468
|
+
# CLI sessions must not block the deployment cancel.
|
|
469
|
+
if workflow_id:
|
|
470
|
+
try:
|
|
471
|
+
from services.cli_agent.service import get_ai_cli_service
|
|
472
|
+
cli_svc = get_ai_cli_service()
|
|
473
|
+
cancelled = await cli_svc.cancel_workflow(workflow_id)
|
|
474
|
+
if cancelled:
|
|
475
|
+
logger.info(
|
|
476
|
+
"[workflow] cancelled %d CLI agent session(s) for workflow %s",
|
|
477
|
+
cancelled, workflow_id,
|
|
478
|
+
)
|
|
479
|
+
except Exception as exc:
|
|
480
|
+
logger.debug("[workflow] CLI agent cancel: %s", exc)
|
|
481
|
+
|
|
482
|
+
return result
|
|
465
483
|
|
|
466
484
|
def get_deployment_status(self, workflow_id: Optional[str] = None) -> Dict[str, Any]:
|
|
467
485
|
"""Get deployment status.
|