machinaos 0.0.1

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.
Files changed (288) hide show
  1. package/.env.template +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +87 -0
  4. package/bin/cli.js +159 -0
  5. package/client/.dockerignore +45 -0
  6. package/client/Dockerfile +68 -0
  7. package/client/eslint.config.js +29 -0
  8. package/client/index.html +13 -0
  9. package/client/nginx.conf +66 -0
  10. package/client/package.json +48 -0
  11. package/client/src/App.tsx +27 -0
  12. package/client/src/Dashboard.tsx +1173 -0
  13. package/client/src/ParameterPanel.tsx +301 -0
  14. package/client/src/components/AIAgentNode.tsx +321 -0
  15. package/client/src/components/APIKeyValidator.tsx +118 -0
  16. package/client/src/components/ClaudeChatModelNode.tsx +18 -0
  17. package/client/src/components/ConditionalEdge.tsx +189 -0
  18. package/client/src/components/CredentialsModal.tsx +306 -0
  19. package/client/src/components/EdgeConditionEditor.tsx +443 -0
  20. package/client/src/components/GeminiChatModelNode.tsx +18 -0
  21. package/client/src/components/GenericNode.tsx +357 -0
  22. package/client/src/components/LocationParameterPanel.tsx +154 -0
  23. package/client/src/components/ModelNode.tsx +286 -0
  24. package/client/src/components/OpenAIChatModelNode.tsx +18 -0
  25. package/client/src/components/OutputPanel.tsx +471 -0
  26. package/client/src/components/ParameterRenderer.tsx +1874 -0
  27. package/client/src/components/SkillEditorModal.tsx +417 -0
  28. package/client/src/components/SquareNode.tsx +797 -0
  29. package/client/src/components/StartNode.tsx +250 -0
  30. package/client/src/components/ToolkitNode.tsx +365 -0
  31. package/client/src/components/TriggerNode.tsx +463 -0
  32. package/client/src/components/auth/LoginPage.tsx +247 -0
  33. package/client/src/components/auth/ProtectedRoute.tsx +59 -0
  34. package/client/src/components/base/BaseChatModelNode.tsx +271 -0
  35. package/client/src/components/icons/AIProviderIcons.tsx +50 -0
  36. package/client/src/components/maps/GoogleMapsPicker.tsx +137 -0
  37. package/client/src/components/maps/MapsPreviewPanel.tsx +110 -0
  38. package/client/src/components/maps/index.ts +26 -0
  39. package/client/src/components/parameterPanel/InputSection.tsx +1094 -0
  40. package/client/src/components/parameterPanel/LocationPanelLayout.tsx +65 -0
  41. package/client/src/components/parameterPanel/MapsSection.tsx +92 -0
  42. package/client/src/components/parameterPanel/MiddleSection.tsx +571 -0
  43. package/client/src/components/parameterPanel/OutputSection.tsx +81 -0
  44. package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +82 -0
  45. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -0
  46. package/client/src/components/parameterPanel/index.ts +42 -0
  47. package/client/src/components/shared/DataPanel.tsx +142 -0
  48. package/client/src/components/shared/JSONTreeRenderer.tsx +106 -0
  49. package/client/src/components/ui/AIResultModal.tsx +204 -0
  50. package/client/src/components/ui/AndroidSettingsPanel.tsx +401 -0
  51. package/client/src/components/ui/CodeEditor.tsx +81 -0
  52. package/client/src/components/ui/CollapsibleSection.tsx +88 -0
  53. package/client/src/components/ui/ComponentItem.tsx +154 -0
  54. package/client/src/components/ui/ComponentPalette.tsx +321 -0
  55. package/client/src/components/ui/ConsolePanel.tsx +1074 -0
  56. package/client/src/components/ui/ErrorBoundary.tsx +196 -0
  57. package/client/src/components/ui/InputNodesPanel.tsx +204 -0
  58. package/client/src/components/ui/MapSelector.tsx +314 -0
  59. package/client/src/components/ui/Modal.tsx +149 -0
  60. package/client/src/components/ui/NodeContextMenu.tsx +192 -0
  61. package/client/src/components/ui/NodeOutputPanel.tsx +1150 -0
  62. package/client/src/components/ui/OutputDisplayPanel.tsx +381 -0
  63. package/client/src/components/ui/SettingsPanel.tsx +243 -0
  64. package/client/src/components/ui/TopToolbar.tsx +736 -0
  65. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +345 -0
  66. package/client/src/components/ui/WorkflowSidebar.tsx +294 -0
  67. package/client/src/config/antdTheme.ts +186 -0
  68. package/client/src/config/api.ts +54 -0
  69. package/client/src/contexts/AuthContext.tsx +221 -0
  70. package/client/src/contexts/ThemeContext.tsx +42 -0
  71. package/client/src/contexts/WebSocketContext.tsx +1971 -0
  72. package/client/src/factories/baseChatModelFactory.ts +256 -0
  73. package/client/src/hooks/useAndroidOperations.ts +164 -0
  74. package/client/src/hooks/useApiKeyValidation.ts +107 -0
  75. package/client/src/hooks/useApiKeys.ts +238 -0
  76. package/client/src/hooks/useAppTheme.ts +17 -0
  77. package/client/src/hooks/useComponentPalette.ts +51 -0
  78. package/client/src/hooks/useCopyPaste.ts +155 -0
  79. package/client/src/hooks/useDragAndDrop.ts +124 -0
  80. package/client/src/hooks/useDragVariable.ts +88 -0
  81. package/client/src/hooks/useExecution.ts +313 -0
  82. package/client/src/hooks/useParameterPanel.ts +176 -0
  83. package/client/src/hooks/useReactFlowNodes.ts +189 -0
  84. package/client/src/hooks/useToolSchema.ts +209 -0
  85. package/client/src/hooks/useWhatsApp.ts +196 -0
  86. package/client/src/hooks/useWorkflowManagement.ts +46 -0
  87. package/client/src/index.css +315 -0
  88. package/client/src/main.tsx +19 -0
  89. package/client/src/nodeDefinitions/aiAgentNodes.ts +336 -0
  90. package/client/src/nodeDefinitions/aiModelNodes.ts +340 -0
  91. package/client/src/nodeDefinitions/androidDeviceNodes.ts +140 -0
  92. package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -0
  93. package/client/src/nodeDefinitions/chatNodes.ts +135 -0
  94. package/client/src/nodeDefinitions/codeNodes.ts +54 -0
  95. package/client/src/nodeDefinitions/documentNodes.ts +379 -0
  96. package/client/src/nodeDefinitions/index.ts +15 -0
  97. package/client/src/nodeDefinitions/locationNodes.ts +463 -0
  98. package/client/src/nodeDefinitions/schedulerNodes.ts +220 -0
  99. package/client/src/nodeDefinitions/skillNodes.ts +211 -0
  100. package/client/src/nodeDefinitions/toolNodes.ts +198 -0
  101. package/client/src/nodeDefinitions/utilityNodes.ts +284 -0
  102. package/client/src/nodeDefinitions/whatsappNodes.ts +865 -0
  103. package/client/src/nodeDefinitions/workflowNodes.ts +41 -0
  104. package/client/src/nodeDefinitions.ts +104 -0
  105. package/client/src/schemas/workflowSchema.ts +264 -0
  106. package/client/src/services/dynamicParameterService.ts +96 -0
  107. package/client/src/services/execution/aiAgentExecutionService.ts +35 -0
  108. package/client/src/services/executionService.ts +232 -0
  109. package/client/src/services/workflowApi.ts +91 -0
  110. package/client/src/store/useAppStore.ts +582 -0
  111. package/client/src/styles/theme.ts +508 -0
  112. package/client/src/styles/zIndex.ts +17 -0
  113. package/client/src/types/ComponentTypes.ts +39 -0
  114. package/client/src/types/EdgeCondition.ts +231 -0
  115. package/client/src/types/INodeProperties.ts +288 -0
  116. package/client/src/types/NodeTypes.ts +28 -0
  117. package/client/src/utils/formatters.ts +33 -0
  118. package/client/src/utils/googleMapsLoader.ts +140 -0
  119. package/client/src/utils/locationUtils.ts +85 -0
  120. package/client/src/utils/nodeUtils.ts +31 -0
  121. package/client/src/utils/workflow.ts +30 -0
  122. package/client/src/utils/workflowExport.ts +120 -0
  123. package/client/src/vite-env.d.ts +12 -0
  124. package/client/tailwind.config.js +60 -0
  125. package/client/tsconfig.json +25 -0
  126. package/client/tsconfig.node.json +11 -0
  127. package/client/vite.config.js +35 -0
  128. package/docker-compose.prod.yml +107 -0
  129. package/docker-compose.yml +104 -0
  130. package/docs-MachinaOs/README.md +85 -0
  131. package/docs-MachinaOs/deployment/docker.mdx +228 -0
  132. package/docs-MachinaOs/deployment/production.mdx +345 -0
  133. package/docs-MachinaOs/docs.json +75 -0
  134. package/docs-MachinaOs/faq.mdx +309 -0
  135. package/docs-MachinaOs/favicon.svg +5 -0
  136. package/docs-MachinaOs/installation.mdx +160 -0
  137. package/docs-MachinaOs/introduction.mdx +114 -0
  138. package/docs-MachinaOs/logo/dark.svg +6 -0
  139. package/docs-MachinaOs/logo/light.svg +6 -0
  140. package/docs-MachinaOs/nodes/ai-agent.mdx +216 -0
  141. package/docs-MachinaOs/nodes/ai-models.mdx +240 -0
  142. package/docs-MachinaOs/nodes/android.mdx +411 -0
  143. package/docs-MachinaOs/nodes/overview.mdx +181 -0
  144. package/docs-MachinaOs/nodes/schedulers.mdx +316 -0
  145. package/docs-MachinaOs/nodes/webhooks.mdx +330 -0
  146. package/docs-MachinaOs/nodes/whatsapp.mdx +305 -0
  147. package/docs-MachinaOs/quickstart.mdx +119 -0
  148. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +177 -0
  149. package/docs-MachinaOs/tutorials/android-automation.mdx +242 -0
  150. package/docs-MachinaOs/tutorials/first-workflow.mdx +134 -0
  151. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +185 -0
  152. package/nul +0 -0
  153. package/package.json +70 -0
  154. package/scripts/build.js +158 -0
  155. package/scripts/check-ports.ps1 +33 -0
  156. package/scripts/clean.js +40 -0
  157. package/scripts/docker.js +93 -0
  158. package/scripts/kill-port.ps1 +154 -0
  159. package/scripts/start.js +210 -0
  160. package/scripts/stop.js +325 -0
  161. package/server/.dockerignore +44 -0
  162. package/server/Dockerfile +45 -0
  163. package/server/constants.py +249 -0
  164. package/server/core/__init__.py +1 -0
  165. package/server/core/cache.py +461 -0
  166. package/server/core/config.py +128 -0
  167. package/server/core/container.py +99 -0
  168. package/server/core/database.py +1211 -0
  169. package/server/core/logging.py +314 -0
  170. package/server/main.py +289 -0
  171. package/server/middleware/__init__.py +5 -0
  172. package/server/middleware/auth.py +89 -0
  173. package/server/models/__init__.py +1 -0
  174. package/server/models/auth.py +52 -0
  175. package/server/models/cache.py +24 -0
  176. package/server/models/database.py +211 -0
  177. package/server/models/nodes.py +455 -0
  178. package/server/package.json +9 -0
  179. package/server/pyproject.toml +72 -0
  180. package/server/requirements.txt +83 -0
  181. package/server/routers/__init__.py +1 -0
  182. package/server/routers/android.py +294 -0
  183. package/server/routers/auth.py +203 -0
  184. package/server/routers/database.py +151 -0
  185. package/server/routers/maps.py +142 -0
  186. package/server/routers/nodejs_compat.py +289 -0
  187. package/server/routers/webhook.py +90 -0
  188. package/server/routers/websocket.py +2127 -0
  189. package/server/routers/whatsapp.py +761 -0
  190. package/server/routers/workflow.py +200 -0
  191. package/server/services/__init__.py +1 -0
  192. package/server/services/ai.py +2415 -0
  193. package/server/services/android/__init__.py +27 -0
  194. package/server/services/android/broadcaster.py +114 -0
  195. package/server/services/android/client.py +608 -0
  196. package/server/services/android/manager.py +78 -0
  197. package/server/services/android/protocol.py +165 -0
  198. package/server/services/android_service.py +588 -0
  199. package/server/services/auth.py +131 -0
  200. package/server/services/chat_client.py +160 -0
  201. package/server/services/deployment/__init__.py +12 -0
  202. package/server/services/deployment/manager.py +706 -0
  203. package/server/services/deployment/state.py +47 -0
  204. package/server/services/deployment/triggers.py +275 -0
  205. package/server/services/event_waiter.py +785 -0
  206. package/server/services/execution/__init__.py +77 -0
  207. package/server/services/execution/cache.py +769 -0
  208. package/server/services/execution/conditions.py +373 -0
  209. package/server/services/execution/dlq.py +132 -0
  210. package/server/services/execution/executor.py +1351 -0
  211. package/server/services/execution/models.py +531 -0
  212. package/server/services/execution/recovery.py +235 -0
  213. package/server/services/handlers/__init__.py +126 -0
  214. package/server/services/handlers/ai.py +355 -0
  215. package/server/services/handlers/android.py +260 -0
  216. package/server/services/handlers/code.py +278 -0
  217. package/server/services/handlers/document.py +598 -0
  218. package/server/services/handlers/http.py +193 -0
  219. package/server/services/handlers/polyglot.py +105 -0
  220. package/server/services/handlers/tools.py +845 -0
  221. package/server/services/handlers/triggers.py +107 -0
  222. package/server/services/handlers/utility.py +822 -0
  223. package/server/services/handlers/whatsapp.py +476 -0
  224. package/server/services/maps.py +289 -0
  225. package/server/services/memory_store.py +103 -0
  226. package/server/services/node_executor.py +375 -0
  227. package/server/services/parameter_resolver.py +218 -0
  228. package/server/services/polyglot_client.py +169 -0
  229. package/server/services/scheduler.py +155 -0
  230. package/server/services/skill_loader.py +417 -0
  231. package/server/services/status_broadcaster.py +826 -0
  232. package/server/services/temporal/__init__.py +23 -0
  233. package/server/services/temporal/activities.py +344 -0
  234. package/server/services/temporal/client.py +76 -0
  235. package/server/services/temporal/executor.py +147 -0
  236. package/server/services/temporal/worker.py +251 -0
  237. package/server/services/temporal/workflow.py +355 -0
  238. package/server/services/temporal/ws_client.py +236 -0
  239. package/server/services/text.py +111 -0
  240. package/server/services/user_auth.py +172 -0
  241. package/server/services/websocket_client.py +29 -0
  242. package/server/services/workflow.py +597 -0
  243. package/server/skills/android-skill/SKILL.md +82 -0
  244. package/server/skills/assistant-personality/SKILL.md +45 -0
  245. package/server/skills/code-skill/SKILL.md +140 -0
  246. package/server/skills/http-skill/SKILL.md +161 -0
  247. package/server/skills/maps-skill/SKILL.md +170 -0
  248. package/server/skills/memory-skill/SKILL.md +154 -0
  249. package/server/skills/scheduler-skill/SKILL.md +84 -0
  250. package/server/skills/whatsapp-skill/SKILL.md +283 -0
  251. package/server/uv.lock +2916 -0
  252. package/server/whatsapp-rpc/.dockerignore +30 -0
  253. package/server/whatsapp-rpc/Dockerfile +44 -0
  254. package/server/whatsapp-rpc/Dockerfile.web +17 -0
  255. package/server/whatsapp-rpc/README.md +139 -0
  256. package/server/whatsapp-rpc/cli.js +95 -0
  257. package/server/whatsapp-rpc/configs/config.yaml +7 -0
  258. package/server/whatsapp-rpc/docker-compose.yml +35 -0
  259. package/server/whatsapp-rpc/docs/API.md +410 -0
  260. package/server/whatsapp-rpc/go.mod +67 -0
  261. package/server/whatsapp-rpc/go.sum +203 -0
  262. package/server/whatsapp-rpc/package.json +30 -0
  263. package/server/whatsapp-rpc/schema.json +1294 -0
  264. package/server/whatsapp-rpc/scripts/clean.cjs +66 -0
  265. package/server/whatsapp-rpc/scripts/cli.js +162 -0
  266. package/server/whatsapp-rpc/src/go/cmd/server/main.go +91 -0
  267. package/server/whatsapp-rpc/src/go/config/config.go +49 -0
  268. package/server/whatsapp-rpc/src/go/rpc/rpc.go +446 -0
  269. package/server/whatsapp-rpc/src/go/rpc/server.go +112 -0
  270. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -0
  271. package/server/whatsapp-rpc/src/go/whatsapp/messages.go +390 -0
  272. package/server/whatsapp-rpc/src/go/whatsapp/service.go +2130 -0
  273. package/server/whatsapp-rpc/src/go/whatsapp/types.go +261 -0
  274. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -0
  275. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -0
  276. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -0
  277. package/server/whatsapp-rpc/web/app.py +609 -0
  278. package/server/whatsapp-rpc/web/requirements.txt +6 -0
  279. package/server/whatsapp-rpc/web/rpc_client.py +427 -0
  280. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -0
  281. package/server/whatsapp-rpc/web/templates/base.html +150 -0
  282. package/server/whatsapp-rpc/web/templates/contacts.html +240 -0
  283. package/server/whatsapp-rpc/web/templates/dashboard.html +320 -0
  284. package/server/whatsapp-rpc/web/templates/groups.html +328 -0
  285. package/server/whatsapp-rpc/web/templates/messages.html +465 -0
  286. package/server/whatsapp-rpc/web/templates/messaging.html +681 -0
  287. package/server/whatsapp-rpc/web/templates/send.html +259 -0
  288. package/server/whatsapp-rpc/web/templates/settings.html +459 -0
@@ -0,0 +1,47 @@
1
+ """Deployment State - Immutable state snapshot for event-driven deployment."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import Dict, Any, List, Optional
6
+
7
+
8
+ @dataclass
9
+ class DeploymentState:
10
+ """Immutable deployment state."""
11
+ deployment_id: str
12
+ workflow_id: str
13
+ is_running: bool
14
+ nodes: List[Dict]
15
+ edges: List[Dict]
16
+ session_id: str
17
+ settings: Dict[str, Any] = field(default_factory=dict)
18
+ deployed_at: str = field(default_factory=lambda: datetime.now().isoformat())
19
+
20
+ def to_dict(self) -> Dict[str, Any]:
21
+ return {
22
+ "deployment_id": self.deployment_id,
23
+ "workflow_id": self.workflow_id,
24
+ "is_running": self.is_running,
25
+ "session_id": self.session_id,
26
+ "settings": self.settings,
27
+ "deployed_at": self.deployed_at,
28
+ "node_count": len(self.nodes),
29
+ "edge_count": len(self.edges),
30
+ }
31
+
32
+
33
+ @dataclass
34
+ class TriggerInfo:
35
+ """Info about a registered trigger."""
36
+ node_id: str
37
+ node_type: str
38
+ job_id: Optional[str] = None # For cron triggers
39
+ fired: bool = False # For start triggers
40
+
41
+ def to_dict(self) -> Dict[str, Any]:
42
+ d = {"type": self.node_type, "node_id": self.node_id}
43
+ if self.job_id:
44
+ d["job_id"] = self.job_id
45
+ if self.fired:
46
+ d["fired"] = True
47
+ return d
@@ -0,0 +1,275 @@
1
+ """Trigger Management - Setup and teardown of workflow triggers.
2
+
3
+ Handles cron scheduling, event-based triggers (webhook, whatsapp), and cleanup.
4
+ """
5
+
6
+ import asyncio
7
+ from datetime import datetime
8
+ from typing import Dict, Any, Callable, Optional, Set
9
+
10
+ from core.logging import get_logger
11
+ from constants import WORKFLOW_TRIGGER_TYPES
12
+ from services import event_waiter
13
+ from services import scheduler as cron_scheduler
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class TriggerManager:
19
+ """Manages workflow trigger lifecycle."""
20
+
21
+ def __init__(self, main_loop: Optional[asyncio.AbstractEventLoop] = None):
22
+ self._active_cron_jobs: Dict[str, str] = {} # node_id -> job_id
23
+ self._active_listeners: Dict[str, asyncio.Task] = {} # node_id -> task
24
+ self._main_loop = main_loop
25
+ self._is_running = False
26
+
27
+ def set_running(self, running: bool):
28
+ self._is_running = running
29
+
30
+ def set_main_loop(self, loop: asyncio.AbstractEventLoop):
31
+ self._main_loop = loop
32
+
33
+ # =========================================================================
34
+ # CRON TRIGGERS
35
+ # =========================================================================
36
+
37
+ def setup_cron(self, node_id: str, cron_expr: str, timezone: str,
38
+ on_tick: Callable[[], None]) -> str:
39
+ """Setup a cron trigger that calls on_tick on schedule."""
40
+ job_id = f"cron_{node_id}"
41
+
42
+ def tick_callback():
43
+ if not self._is_running:
44
+ return
45
+ on_tick()
46
+
47
+ cron_scheduler.register_cron_job(
48
+ job_id=job_id,
49
+ cron_expression=cron_expr,
50
+ callback=tick_callback,
51
+ timezone=timezone
52
+ )
53
+
54
+ self._active_cron_jobs[node_id] = job_id
55
+ logger.info("Cron trigger setup", job_id=job_id, expr=cron_expr)
56
+ return job_id
57
+
58
+ def teardown_cron(self, node_id: str) -> bool:
59
+ """Remove a specific cron trigger."""
60
+ job_id = self._active_cron_jobs.pop(node_id, None)
61
+ if job_id:
62
+ cron_scheduler.remove_cron_job(job_id)
63
+ logger.debug("Cron trigger removed", job_id=job_id)
64
+ return True
65
+ return False
66
+
67
+ def get_cron_node_ids(self) -> list:
68
+ """Get node IDs of active cron triggers."""
69
+ return list(self._active_cron_jobs.keys())
70
+
71
+ def teardown_all_crons(self) -> int:
72
+ """Remove all cron triggers."""
73
+ count = 0
74
+ for node_id, job_id in list(self._active_cron_jobs.items()):
75
+ cron_scheduler.remove_cron_job(job_id)
76
+ count += 1
77
+ self._active_cron_jobs.clear()
78
+ logger.info("All cron triggers removed", count=count)
79
+ return count
80
+
81
+ # =========================================================================
82
+ # EVENT TRIGGERS (Webhook, WhatsApp, etc.)
83
+ # =========================================================================
84
+
85
+ async def setup_event_trigger(self, node_id: str, node_type: str,
86
+ parameters: Dict[str, Any],
87
+ on_event: Callable[[Dict], Any],
88
+ broadcaster: Any,
89
+ workflow_id: Optional[str] = None) -> None:
90
+ """Setup an event-based trigger with queue-based sequential processing.
91
+
92
+ Args:
93
+ node_id: The trigger node ID
94
+ node_type: Type of trigger (whatsappReceive, webhookTrigger, etc.)
95
+ parameters: Node parameters for filtering
96
+ on_event: Callback when event is received
97
+ broadcaster: Status broadcaster for real-time updates
98
+ workflow_id: Workflow ID for scoped status updates (n8n pattern)
99
+ """
100
+ event_queue: asyncio.Queue = asyncio.Queue()
101
+ is_executing = False
102
+
103
+ async def collector():
104
+ """Continuously collect events into queue."""
105
+ while self._is_running:
106
+ try:
107
+ waiter = await event_waiter.register(node_type, node_id, parameters)
108
+ config = event_waiter.get_trigger_config(node_type)
109
+
110
+ if config and not is_executing:
111
+ queue_size = event_queue.qsize()
112
+ msg = f"Waiting for {config.display_name}..."
113
+ if queue_size > 0:
114
+ msg = f"Waiting... ({queue_size} queued)"
115
+ await broadcaster.update_node_status(node_id, "waiting", {
116
+ "message": msg,
117
+ "event_type": config.event_type,
118
+ "waiter_id": waiter.id,
119
+ "queue_size": queue_size
120
+ }, workflow_id=workflow_id)
121
+
122
+ event_data = await event_waiter.wait_for_event(waiter)
123
+ if self._is_running:
124
+ await event_queue.put(event_data)
125
+
126
+ except asyncio.CancelledError:
127
+ break
128
+ except Exception as e:
129
+ logger.error("Trigger collector error", node_id=node_id, error=str(e))
130
+ if self._is_running:
131
+ await asyncio.sleep(1)
132
+
133
+ async def processor():
134
+ """Process events from queue sequentially."""
135
+ nonlocal is_executing
136
+ while self._is_running:
137
+ try:
138
+ try:
139
+ event_data = await asyncio.wait_for(event_queue.get(), timeout=1.0)
140
+ except asyncio.TimeoutError:
141
+ continue
142
+
143
+ is_executing = True
144
+ config = event_waiter.get_trigger_config(node_type)
145
+
146
+ # Clear waiting indicator during execution
147
+ await broadcaster.update_node_status(node_id, "idle", {
148
+ "message": "Graph executing...",
149
+ "is_processing": True
150
+ }, workflow_id=workflow_id)
151
+
152
+ try:
153
+ await on_event(event_data)
154
+ except Exception as e:
155
+ logger.error("Trigger execution error", node_id=node_id, error=str(e))
156
+
157
+ is_executing = False
158
+
159
+ # Return to waiting state
160
+ queue_size = event_queue.qsize()
161
+ name = config.display_name if config else node_type
162
+ msg = f"Waiting for {name}..." if queue_size == 0 else f"Processing next... ({queue_size} queued)"
163
+ await broadcaster.update_node_status(node_id, "waiting", {
164
+ "message": msg,
165
+ "queue_size": queue_size,
166
+ "is_processing": False
167
+ }, workflow_id=workflow_id)
168
+
169
+ except asyncio.CancelledError:
170
+ break
171
+ except Exception as e:
172
+ logger.error("Trigger processor error", node_id=node_id, error=str(e))
173
+ is_executing = False
174
+
175
+ async def combined():
176
+ collector_task = asyncio.create_task(collector())
177
+ processor_task = asyncio.create_task(processor())
178
+ try:
179
+ await asyncio.gather(collector_task, processor_task)
180
+ except asyncio.CancelledError:
181
+ collector_task.cancel()
182
+ processor_task.cancel()
183
+ await asyncio.gather(collector_task, processor_task, return_exceptions=True)
184
+
185
+ task = asyncio.create_task(combined())
186
+ self._active_listeners[node_id] = task
187
+
188
+ async def teardown_all_listeners(self) -> int:
189
+ """Cancel all event listeners."""
190
+ count = 0
191
+ for node_id, task in list(self._active_listeners.items()):
192
+ if not task.done():
193
+ task.cancel()
194
+ count += 1
195
+
196
+ if self._active_listeners:
197
+ await asyncio.gather(*self._active_listeners.values(), return_exceptions=True)
198
+
199
+ self._active_listeners.clear()
200
+ return count
201
+
202
+ def get_listener_node_ids(self) -> list:
203
+ """Get node IDs of active listeners."""
204
+ return list(self._active_listeners.keys())
205
+
206
+ # =========================================================================
207
+ # UTILITY METHODS
208
+ # =========================================================================
209
+
210
+ @staticmethod
211
+ def build_cron_expression(parameters: Dict[str, Any]) -> Optional[str]:
212
+ """Build cron expression from user-friendly parameters."""
213
+ frequency = parameters.get('frequency', 'minutes')
214
+
215
+ second, minute, hour = '0', '*/5', '*'
216
+ day, month, weekday = '*', '*', '*'
217
+
218
+ if frequency == 'seconds':
219
+ interval = str(parameters.get('interval', 30))
220
+ second, minute = f'*/{interval}', '*'
221
+
222
+ elif frequency == 'minutes':
223
+ interval = str(parameters.get('intervalMinutes', 5))
224
+ minute = f'*/{interval}' if interval != '1' else '*'
225
+
226
+ elif frequency == 'hours':
227
+ interval = str(parameters.get('intervalHours', 1))
228
+ minute = '0'
229
+ hour = f'*/{interval}' if interval != '1' else '*'
230
+
231
+ elif frequency == 'days':
232
+ time_str = parameters.get('dailyTime', '09:00')
233
+ parts = time_str.split(':')
234
+ hour = parts[0] if parts else '9'
235
+ minute = parts[1] if len(parts) > 1 else '0'
236
+
237
+ elif frequency == 'weeks':
238
+ time_str = parameters.get('weeklyTime', '09:00')
239
+ parts = time_str.split(':')
240
+ hour = parts[0] if parts else '9'
241
+ minute = parts[1] if len(parts) > 1 else '0'
242
+ weekday = parameters.get('weekday', '1')
243
+
244
+ elif frequency == 'months':
245
+ time_str = parameters.get('monthlyTime', '09:00')
246
+ parts = time_str.split(':')
247
+ hour = parts[0] if parts else '9'
248
+ minute = parts[1] if len(parts) > 1 else '0'
249
+ day = parameters.get('monthDay', '1')
250
+
251
+ elif frequency == 'once':
252
+ return None
253
+
254
+ return f'{second} {minute} {hour} {day} {month} {weekday}'
255
+
256
+ @staticmethod
257
+ def find_trigger_nodes(nodes: list, edges: list) -> tuple:
258
+ """Find trigger nodes, split into start nodes and event triggers."""
259
+ # Nodes with incoming edges
260
+ nodes_with_inputs = {e.get('target') for e in edges if e.get('target')}
261
+
262
+ trigger_types_no_cron = WORKFLOW_TRIGGER_TYPES - {'cronScheduler'}
263
+ triggers = [n for n in nodes
264
+ if n.get('type') in trigger_types_no_cron
265
+ and n.get('id') not in nodes_with_inputs]
266
+
267
+ start_nodes = [n for n in triggers if n.get('type') == 'start']
268
+ event_triggers = [n for n in triggers if n.get('type') != 'start']
269
+
270
+ return start_nodes, event_triggers
271
+
272
+ @staticmethod
273
+ def find_cron_nodes(nodes: list) -> list:
274
+ """Find all cron scheduler nodes."""
275
+ return [n for n in nodes if n.get('type') == 'cronScheduler']