ultravisor 1.0.3 → 1.0.8

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 (248) hide show
  1. package/.claude/launch.json +11 -0
  2. package/.claude/ultravisor-dev-config.json +3 -0
  3. package/card-audit.json +1545 -0
  4. package/docs/_sidebar.md +17 -0
  5. package/docs/css/docuserve.css +73 -0
  6. package/docs/features/beacon-authentication.md +209 -0
  7. package/docs/features/beacon-providers.md +1299 -0
  8. package/docs/features/beacons.md +530 -0
  9. package/docs/features/capabilities.md +193 -0
  10. package/docs/features/llm-model-setup.md +505 -0
  11. package/docs/features/llm.md +262 -0
  12. package/docs/features/tasks-content-system.md +110 -0
  13. package/docs/features/tasks-data-transform.md +233 -0
  14. package/docs/features/tasks-extension.md +36 -0
  15. package/docs/features/tasks-file-system.md +203 -0
  16. package/docs/features/tasks-flow-control.md +125 -0
  17. package/docs/features/tasks-http-client.md +119 -0
  18. package/docs/features/tasks-llm.md +118 -0
  19. package/docs/features/tasks-meadow-api.md +163 -0
  20. package/docs/features/tasks-user-interaction.md +49 -0
  21. package/docs/features/tasks.md +14 -0
  22. package/docs/index.html +1 -1
  23. package/docs/retold-catalog.json +1 -1
  24. package/docs/retold-keyword-index.json +17374 -5
  25. package/operation-library/api-data-pipeline.json +143 -0
  26. package/operation-library/beacon-remote-command.json +72 -0
  27. package/operation-library/conditional-file-backup.json +143 -0
  28. package/operation-library/config-processor.json +136 -0
  29. package/operation-library/csv-data-analysis.json +228 -0
  30. package/operation-library/csv-to-json.json +160 -0
  31. package/operation-library/directory-inventory.json +128 -0
  32. package/operation-library/expression-calculator.json +114 -0
  33. package/operation-library/file-search-replace.json +175 -0
  34. package/operation-library/git-status-report.json +368 -0
  35. package/operation-library/health-check-ping.json +329 -0
  36. package/operation-library/json-config-merge.json +313 -0
  37. package/operation-library/llm-embedding-tool-use.json +314 -0
  38. package/operation-library/llm-multi-turn-analysis.json +322 -0
  39. package/operation-library/llm-summarize.json +264 -0
  40. package/operation-library/log-line-counter.json +185 -0
  41. package/operation-library/meadow-crud-lifecycle.json +264 -0
  42. package/operation-library/npm-project-validator.json +193 -0
  43. package/operation-library/rest-api-orchestrator.json +218 -0
  44. package/operation-library/shell-system-info.json +137 -0
  45. package/operation-library/simple-echo.json +95 -0
  46. package/operation-library/template-processor.json +299 -0
  47. package/operation-library/text-sanitizer.json +176 -0
  48. package/package.json +12 -8
  49. package/source/Ultravisor.cjs +30 -3
  50. package/source/beacon/Ultravisor-Beacon-CLI.cjs +143 -0
  51. package/source/beacon/Ultravisor-Beacon-CapabilityProvider.cjs +129 -0
  52. package/source/beacon/Ultravisor-Beacon-Client.cjs +568 -0
  53. package/source/beacon/Ultravisor-Beacon-Executor.cjs +500 -0
  54. package/source/beacon/Ultravisor-Beacon-ProviderRegistry.cjs +330 -0
  55. package/source/beacon/providers/Ultravisor-Beacon-Provider-FileSystem.cjs +331 -0
  56. package/source/beacon/providers/Ultravisor-Beacon-Provider-LLM.cjs +966 -0
  57. package/source/beacon/providers/Ultravisor-Beacon-Provider-Shell.cjs +95 -0
  58. package/source/cli/Ultravisor-CLIProgram.cjs +44 -22
  59. package/source/cli/commands/Ultravisor-Command-SingleOperation.cjs +29 -18
  60. package/source/cli/commands/Ultravisor-Command-SingleTask.cjs +62 -19
  61. package/source/cli/commands/Ultravisor-Command-UpdateTask.cjs +27 -15
  62. package/source/config/Ultravisor-Default-Command-Configuration.cjs +12 -3
  63. package/source/services/Ultravisor-Beacon-Coordinator.cjs +966 -0
  64. package/source/services/Ultravisor-ExecutionEngine.cjs +1093 -0
  65. package/source/services/Ultravisor-ExecutionManifest.cjs +629 -0
  66. package/source/services/Ultravisor-Hypervisor-State.cjs +270 -97
  67. package/source/services/Ultravisor-Hypervisor.cjs +185 -75
  68. package/source/services/Ultravisor-Schedule-Persistence-Base.cjs +45 -0
  69. package/source/services/Ultravisor-StateManager.cjs +253 -0
  70. package/source/services/Ultravisor-TaskTypeRegistry.cjs +213 -0
  71. package/source/services/persistence/Ultravisor-Schedule-Persistence-JSONFile.cjs +140 -0
  72. package/source/services/tasks/Ultravisor-BuiltIn-TaskConfigs.cjs +19 -0
  73. package/source/services/tasks/Ultravisor-TaskHelper-BeaconDispatch.cjs +107 -0
  74. package/source/services/tasks/Ultravisor-TaskType-Base.cjs +125 -0
  75. package/source/services/tasks/content-system/Ultravisor-TaskConfigs-ContentSystem.cjs +90 -0
  76. package/source/services/tasks/content-system/definitions/content-create-folder.json +24 -0
  77. package/source/services/tasks/content-system/definitions/content-list-files.json +27 -0
  78. package/source/services/tasks/content-system/definitions/content-read-file.json +26 -0
  79. package/source/services/tasks/content-system/definitions/content-save-file.json +26 -0
  80. package/source/services/tasks/data-transform/Ultravisor-TaskConfigs-DataTransform.cjs +577 -0
  81. package/source/services/tasks/data-transform/Ultravisor-TaskType-ReplaceString.cjs +80 -0
  82. package/source/services/tasks/data-transform/Ultravisor-TaskType-SetValues.cjs +74 -0
  83. package/source/services/tasks/data-transform/Ultravisor-TaskType-StringAppender.cjs +90 -0
  84. package/source/services/tasks/data-transform/definitions/comprehension-intersect.json +24 -0
  85. package/source/services/tasks/data-transform/definitions/csv-transform.json +24 -0
  86. package/source/services/tasks/data-transform/definitions/expression-solver.json +20 -0
  87. package/source/services/tasks/data-transform/definitions/histogram.json +23 -0
  88. package/source/services/tasks/data-transform/definitions/parse-csv.json +27 -0
  89. package/source/services/tasks/data-transform/definitions/replace-string.json +27 -0
  90. package/source/services/tasks/data-transform/definitions/set-values.json +17 -0
  91. package/source/services/tasks/data-transform/definitions/string-appender.json +22 -0
  92. package/source/services/tasks/data-transform/definitions/template-string.json +20 -0
  93. package/source/services/tasks/extension/Ultravisor-TaskConfigs-Extension.cjs +87 -0
  94. package/source/services/tasks/extension/definitions/beacon-dispatch.json +32 -0
  95. package/source/services/tasks/file-system/Ultravisor-TaskConfigs-FileSystem.cjs +509 -0
  96. package/source/services/tasks/file-system/Ultravisor-TaskType-ReadFile.cjs +96 -0
  97. package/source/services/tasks/file-system/Ultravisor-TaskType-ReadFileBuffered.cjs +123 -0
  98. package/source/services/tasks/file-system/Ultravisor-TaskType-WriteFile.cjs +106 -0
  99. package/source/services/tasks/file-system/definitions/copy-file.json +27 -0
  100. package/source/services/tasks/file-system/definitions/list-files.json +27 -0
  101. package/source/services/tasks/file-system/definitions/read-file-buffered.json +31 -0
  102. package/source/services/tasks/file-system/definitions/read-file.json +26 -0
  103. package/source/services/tasks/file-system/definitions/read-json.json +23 -0
  104. package/source/services/tasks/file-system/definitions/write-file.json +29 -0
  105. package/source/services/tasks/file-system/definitions/write-json.json +30 -0
  106. package/source/services/tasks/flow-control/Ultravisor-TaskConfigs-FlowControl.cjs +353 -0
  107. package/source/services/tasks/flow-control/Ultravisor-TaskType-IfConditional.cjs +125 -0
  108. package/source/services/tasks/flow-control/Ultravisor-TaskType-LaunchOperation.cjs +186 -0
  109. package/source/services/tasks/flow-control/Ultravisor-TaskType-SplitExecute.cjs +164 -0
  110. package/source/services/tasks/flow-control/definitions/if-conditional.json +25 -0
  111. package/source/services/tasks/flow-control/definitions/launch-operation.json +27 -0
  112. package/source/services/tasks/flow-control/definitions/split-execute.json +32 -0
  113. package/source/services/tasks/http-client/Ultravisor-TaskConfigs-HttpClient.cjs +281 -0
  114. package/source/services/tasks/http-client/definitions/get-json.json +26 -0
  115. package/source/services/tasks/http-client/definitions/get-text.json +26 -0
  116. package/source/services/tasks/http-client/definitions/rest-request.json +32 -0
  117. package/source/services/tasks/http-client/definitions/send-json.json +28 -0
  118. package/source/services/tasks/llm/Ultravisor-TaskConfigs-LLM.cjs +605 -0
  119. package/source/services/tasks/llm/definitions/llm-chat-completion.json +46 -0
  120. package/source/services/tasks/llm/definitions/llm-embedding.json +31 -0
  121. package/source/services/tasks/llm/definitions/llm-tool-use.json +42 -0
  122. package/source/services/tasks/meadow-api/Ultravisor-TaskConfigs-MeadowApi.cjs +341 -0
  123. package/source/services/tasks/meadow-api/definitions/meadow-count.json +26 -0
  124. package/source/services/tasks/meadow-api/definitions/meadow-create.json +26 -0
  125. package/source/services/tasks/meadow-api/definitions/meadow-delete.json +23 -0
  126. package/source/services/tasks/meadow-api/definitions/meadow-read.json +26 -0
  127. package/source/services/tasks/meadow-api/definitions/meadow-reads.json +29 -0
  128. package/source/services/tasks/meadow-api/definitions/meadow-update.json +26 -0
  129. package/source/services/tasks/shell/Ultravisor-TaskConfigs-Shell.cjs +63 -0
  130. package/source/services/tasks/shell/definitions/command.json +29 -0
  131. package/source/services/tasks/user-interaction/Ultravisor-TaskConfigs-UserInteraction.cjs +64 -0
  132. package/source/services/tasks/user-interaction/Ultravisor-TaskType-ErrorMessage.cjs +55 -0
  133. package/source/services/tasks/user-interaction/Ultravisor-TaskType-ValueInput.cjs +47 -0
  134. package/source/services/tasks/user-interaction/definitions/error-message.json +18 -0
  135. package/source/services/tasks/user-interaction/definitions/value-input.json +23 -0
  136. package/source/web_server/Ultravisor-API-Server.cjs +849 -124
  137. package/test/Ultravisor_browser_tests.js +2226 -0
  138. package/test/Ultravisor_operation_library_tests.js +894 -0
  139. package/test/Ultravisor_tests.js +4946 -5044
  140. package/test/workflows/test-content-concatenate-all.json +339 -0
  141. package/test/workflows/test-content-full-pipeline.json +159 -0
  142. package/test/workflows/test-content-list-files.json +76 -0
  143. package/test/workflows/test-content-save-and-read.json +106 -0
  144. package/webinterface/css/ultravisor-themes.css +668 -0
  145. package/webinterface/css/ultravisor.css +37 -14
  146. package/webinterface/docs/card-help/beacon-dispatch.md +30 -0
  147. package/webinterface/docs/card-help/command.md +27 -0
  148. package/webinterface/docs/card-help/comprehension-intersect.md +24 -0
  149. package/webinterface/docs/card-help/content-create-folder.md +22 -0
  150. package/webinterface/docs/card-help/content-list-files.md +25 -0
  151. package/webinterface/docs/card-help/content-read-file.md +24 -0
  152. package/webinterface/docs/card-help/content-save-file.md +24 -0
  153. package/webinterface/docs/card-help/copy-file.md +25 -0
  154. package/webinterface/docs/card-help/csv-transform.md +24 -0
  155. package/webinterface/docs/card-help/end.md +11 -0
  156. package/webinterface/docs/card-help/error-message.md +16 -0
  157. package/webinterface/docs/card-help/expression-solver.md +21 -0
  158. package/webinterface/docs/card-help/get-json.md +24 -0
  159. package/webinterface/docs/card-help/get-text.md +24 -0
  160. package/webinterface/docs/card-help/histogram.md +23 -0
  161. package/webinterface/docs/card-help/if-conditional.md +23 -0
  162. package/webinterface/docs/card-help/launch-operation.md +25 -0
  163. package/webinterface/docs/card-help/list-files.md +25 -0
  164. package/webinterface/docs/card-help/llm-chat-completion.md +43 -0
  165. package/webinterface/docs/card-help/llm-embedding.md +24 -0
  166. package/webinterface/docs/card-help/llm-tool-use.md +39 -0
  167. package/webinterface/docs/card-help/meadow-count.md +24 -0
  168. package/webinterface/docs/card-help/meadow-create.md +24 -0
  169. package/webinterface/docs/card-help/meadow-delete.md +19 -0
  170. package/webinterface/docs/card-help/meadow-read.md +24 -0
  171. package/webinterface/docs/card-help/meadow-reads.md +27 -0
  172. package/webinterface/docs/card-help/meadow-update.md +24 -0
  173. package/webinterface/docs/card-help/parse-csv.md +27 -0
  174. package/webinterface/docs/card-help/read-file-buffered.md +29 -0
  175. package/webinterface/docs/card-help/read-file.md +24 -0
  176. package/webinterface/docs/card-help/read-json.md +21 -0
  177. package/webinterface/docs/card-help/replace-string.md +25 -0
  178. package/webinterface/docs/card-help/rest-request.md +30 -0
  179. package/webinterface/docs/card-help/send-json.md +26 -0
  180. package/webinterface/docs/card-help/set-values.md +16 -0
  181. package/webinterface/docs/card-help/split-execute.md +35 -0
  182. package/webinterface/docs/card-help/start.md +11 -0
  183. package/webinterface/docs/card-help/string-appender.md +22 -0
  184. package/webinterface/docs/card-help/template-string.md +21 -0
  185. package/webinterface/docs/card-help/value-input.md +24 -0
  186. package/webinterface/docs/card-help/write-file.md +27 -0
  187. package/webinterface/docs/card-help/write-json.md +28 -0
  188. package/webinterface/html/index.html +5 -0
  189. package/webinterface/package.json +13 -4
  190. package/webinterface/source/Pict-Application-Ultravisor.js +564 -70
  191. package/webinterface/source/Ultravisor-TimingUtils.js +99 -0
  192. package/webinterface/source/card-help-content.js +46 -0
  193. package/webinterface/source/cards/Ultravisor-BuiltIn-CardConfigs.js +174 -0
  194. package/webinterface/source/cards/Ultravisor-CardConfigGenerator.js +431 -0
  195. package/webinterface/source/data/ExampleFlow-CSVPipeline.js +231 -0
  196. package/webinterface/source/data/ExampleFlow-FileProcessor.js +315 -0
  197. package/webinterface/source/data/ExampleFlow-MeadowPipeline.js +328 -0
  198. package/webinterface/source/panels/Ultravisor-CardSettingsPanel.js +902 -0
  199. package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +20 -8
  200. package/webinterface/source/views/PictView-Ultravisor-BeaconList.js +817 -0
  201. package/webinterface/source/views/PictView-Ultravisor-BottomBar.js +5 -5
  202. package/webinterface/source/views/PictView-Ultravisor-Dashboard.js +25 -25
  203. package/webinterface/source/views/PictView-Ultravisor-Documentation.js +856 -0
  204. package/webinterface/source/views/PictView-Ultravisor-FlowEditor.js +777 -0
  205. package/webinterface/source/views/PictView-Ultravisor-ManifestList.js +321 -58
  206. package/webinterface/source/views/PictView-Ultravisor-OperationEdit.js +36 -91
  207. package/webinterface/source/views/PictView-Ultravisor-OperationList.js +388 -16
  208. package/webinterface/source/views/PictView-Ultravisor-PendingInput.js +314 -0
  209. package/webinterface/source/views/PictView-Ultravisor-Schedule.js +521 -65
  210. package/webinterface/source/views/PictView-Ultravisor-TimingView.js +333 -71
  211. package/webinterface/source/views/PictView-Ultravisor-TopBar.js +257 -21
  212. package/webinterface/theme-sampler.html +645 -0
  213. package/debug/Harness.js +0 -6
  214. package/source/services/Ultravisor-Operation-Manifest.cjs +0 -160
  215. package/source/services/Ultravisor-Operation.cjs +0 -200
  216. package/source/services/Ultravisor-Task.cjs +0 -349
  217. package/source/services/events/Ultravisor-Hypervisor-Event-Solver.cjs +0 -11
  218. package/source/services/tasks/Ultravisor-Task-Base.cjs +0 -264
  219. package/source/services/tasks/Ultravisor-Task-CollectValues.cjs +0 -188
  220. package/source/services/tasks/Ultravisor-Task-Command.cjs +0 -65
  221. package/source/services/tasks/Ultravisor-Task-CommandEach.cjs +0 -190
  222. package/source/services/tasks/Ultravisor-Task-Conditional.cjs +0 -104
  223. package/source/services/tasks/Ultravisor-Task-DateWindow.cjs +0 -72
  224. package/source/services/tasks/Ultravisor-Task-GeneratePagedOperation.cjs +0 -336
  225. package/source/services/tasks/Ultravisor-Task-LaunchOperation.cjs +0 -143
  226. package/source/services/tasks/Ultravisor-Task-LaunchTask.cjs +0 -146
  227. package/source/services/tasks/Ultravisor-Task-LineMatch.cjs +0 -158
  228. package/source/services/tasks/Ultravisor-Task-Request.cjs +0 -56
  229. package/source/services/tasks/Ultravisor-Task-Solver.cjs +0 -89
  230. package/source/services/tasks/Ultravisor-Task-TemplateString.cjs +0 -93
  231. package/source/services/tasks/rest/Ultravisor-Task-GetBinary.cjs +0 -127
  232. package/source/services/tasks/rest/Ultravisor-Task-GetJSON.cjs +0 -119
  233. package/source/services/tasks/rest/Ultravisor-Task-GetText.cjs +0 -109
  234. package/source/services/tasks/rest/Ultravisor-Task-GetXML.cjs +0 -112
  235. package/source/services/tasks/rest/Ultravisor-Task-RestRequest.cjs +0 -499
  236. package/source/services/tasks/rest/Ultravisor-Task-SendJSON.cjs +0 -150
  237. package/source/services/tasks/stagingfiles/Ultravisor-Task-CopyFile.cjs +0 -110
  238. package/source/services/tasks/stagingfiles/Ultravisor-Task-ListFiles.cjs +0 -89
  239. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadBinary.cjs +0 -87
  240. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadJSON.cjs +0 -67
  241. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadText.cjs +0 -66
  242. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadXML.cjs +0 -69
  243. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteBinary.cjs +0 -95
  244. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteJSON.cjs +0 -96
  245. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteText.cjs +0 -99
  246. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteXML.cjs +0 -102
  247. package/webinterface/source/views/PictView-Ultravisor-TaskEdit.js +0 -220
  248. package/webinterface/source/views/PictView-Ultravisor-TaskList.js +0 -248
@@ -0,0 +1,966 @@
1
+ /**
2
+ * Ultravisor Beacon Coordinator
3
+ *
4
+ * Server-side service that manages Beacon worker nodes, the work queue,
5
+ * and affinity bindings for distributed task execution.
6
+ *
7
+ * The coordinator is transport-agnostic — its internal API (enqueueWorkItem,
8
+ * completeWorkItem, failWorkItem) can be driven by any signaling channel
9
+ * (HTTP polling, WebSocket, MQTT, email/SMS webhooks, etc.).
10
+ *
11
+ * @module Ultravisor-Beacon-Coordinator
12
+ */
13
+
14
+ const libPictService = require('pict-serviceproviderbase');
15
+
16
+ class UltravisorBeaconCoordinator extends libPictService
17
+ {
18
+ constructor(pPict, pOptions, pServiceHash)
19
+ {
20
+ super(pPict, pOptions, pServiceHash);
21
+
22
+ // --- Registered Beacons ---
23
+ // Map of BeaconID → Beacon record
24
+ this._Beacons = {};
25
+
26
+ // --- Work Queue ---
27
+ // Map of WorkItemHash → Work item record
28
+ this._WorkQueue = {};
29
+
30
+ // --- Affinity Bindings ---
31
+ // Map of AffinityKey → Binding record
32
+ this._AffinityBindings = {};
33
+
34
+ // --- Timeout Checker ---
35
+ this._TimeoutInterval = null;
36
+ this._TimeoutCheckIntervalMs = 10000; // Check every 10s
37
+
38
+ // --- Direct Dispatch Callbacks ---
39
+ // Map of WorkItemHash → { callback, timeoutHandle }
40
+ // Used by dispatchAndWait() for synchronous HTTP dispatch
41
+ this._DirectDispatchCallbacks = {};
42
+ }
43
+
44
+ // ====================================================================
45
+ // Lifecycle
46
+ // ====================================================================
47
+
48
+ /**
49
+ * Start the timeout checker interval.
50
+ */
51
+ startTimeoutChecker()
52
+ {
53
+ if (this._TimeoutInterval)
54
+ {
55
+ return;
56
+ }
57
+
58
+ this._TimeoutInterval = setInterval(
59
+ () =>
60
+ {
61
+ this._checkTimeouts();
62
+ },
63
+ this._TimeoutCheckIntervalMs);
64
+
65
+ this.log.info('BeaconCoordinator: timeout checker started.');
66
+ }
67
+
68
+ /**
69
+ * Stop the timeout checker interval.
70
+ */
71
+ stopTimeoutChecker()
72
+ {
73
+ if (this._TimeoutInterval)
74
+ {
75
+ clearInterval(this._TimeoutInterval);
76
+ this._TimeoutInterval = null;
77
+ this.log.info('BeaconCoordinator: timeout checker stopped.');
78
+ }
79
+ }
80
+
81
+ // ====================================================================
82
+ // Beacon Registration
83
+ // ====================================================================
84
+
85
+ /**
86
+ * Register a new Beacon worker.
87
+ *
88
+ * If a beacon with the same Name already exists and is Offline,
89
+ * reclaim it (session-aware reconnection) instead of creating a
90
+ * new record.
91
+ *
92
+ * @param {object} pBeaconInfo - { Name, Capabilities, MaxConcurrent?, Tags? }
93
+ * @param {string} pSessionID - Optional session identifier
94
+ * @returns {object} The created or reclaimed Beacon record
95
+ */
96
+ registerBeacon(pBeaconInfo, pSessionID)
97
+ {
98
+ let tmpName = pBeaconInfo.Name || 'unnamed';
99
+
100
+ // Check for an existing offline beacon with the same name to reclaim
101
+ let tmpExistingBeacon = this.findBeaconByName(tmpName);
102
+ if (tmpExistingBeacon && tmpExistingBeacon.Status === 'Offline')
103
+ {
104
+ tmpExistingBeacon.SessionID = pSessionID || null;
105
+ tmpExistingBeacon.LastHeartbeat = new Date().toISOString();
106
+ tmpExistingBeacon.Status = 'Online';
107
+
108
+ if (Array.isArray(pBeaconInfo.Capabilities))
109
+ {
110
+ tmpExistingBeacon.Capabilities = pBeaconInfo.Capabilities;
111
+ }
112
+ if (pBeaconInfo.MaxConcurrent)
113
+ {
114
+ tmpExistingBeacon.MaxConcurrent = pBeaconInfo.MaxConcurrent;
115
+ }
116
+ if (pBeaconInfo.Tags)
117
+ {
118
+ tmpExistingBeacon.Tags = pBeaconInfo.Tags;
119
+ }
120
+
121
+ this.log.info(`BeaconCoordinator: reconnected beacon [${tmpExistingBeacon.BeaconID}] "${tmpName}" with session [${tmpExistingBeacon.SessionID}].`);
122
+ return tmpExistingBeacon;
123
+ }
124
+
125
+ let tmpTimestamp = Date.now();
126
+ let tmpBeaconID = `bcn-${tmpName.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${tmpTimestamp}`;
127
+
128
+ let tmpBeacon = {
129
+ BeaconID: tmpBeaconID,
130
+ Name: tmpName,
131
+ SessionID: pSessionID || null,
132
+ Capabilities: Array.isArray(pBeaconInfo.Capabilities) ? pBeaconInfo.Capabilities : [],
133
+ Status: 'Online',
134
+ LastHeartbeat: new Date().toISOString(),
135
+ CurrentWorkItems: [],
136
+ MaxConcurrent: pBeaconInfo.MaxConcurrent || 1,
137
+ Tags: pBeaconInfo.Tags || {},
138
+ RegisteredAt: new Date().toISOString()
139
+ };
140
+
141
+ this._Beacons[tmpBeaconID] = tmpBeacon;
142
+ this.log.info(`BeaconCoordinator: registered beacon [${tmpBeaconID}] "${tmpName}" with capabilities [${tmpBeacon.Capabilities.join(', ')}].`);
143
+
144
+ return tmpBeacon;
145
+ }
146
+
147
+ /**
148
+ * Find a beacon by its Name property.
149
+ *
150
+ * @param {string} pName - The beacon name to search for
151
+ * @returns {object|null} The first matching Beacon record, or null
152
+ */
153
+ findBeaconByName(pName)
154
+ {
155
+ let tmpBeaconIDs = Object.keys(this._Beacons);
156
+ for (let i = 0; i < tmpBeaconIDs.length; i++)
157
+ {
158
+ let tmpBeacon = this._Beacons[tmpBeaconIDs[i]];
159
+ if (tmpBeacon.Name === pName)
160
+ {
161
+ return tmpBeacon;
162
+ }
163
+ }
164
+ return null;
165
+ }
166
+
167
+ /**
168
+ * Find a beacon by its SessionID property.
169
+ *
170
+ * @param {string} pSessionID - The session identifier to search for
171
+ * @returns {object|null} The first matching Beacon record, or null
172
+ */
173
+ findBeaconBySessionID(pSessionID)
174
+ {
175
+ let tmpBeaconIDs = Object.keys(this._Beacons);
176
+ for (let i = 0; i < tmpBeaconIDs.length; i++)
177
+ {
178
+ let tmpBeacon = this._Beacons[tmpBeaconIDs[i]];
179
+ if (tmpBeacon.SessionID === pSessionID)
180
+ {
181
+ return tmpBeacon;
182
+ }
183
+ }
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * Deregister a Beacon and release any assigned work items.
189
+ *
190
+ * @param {string} pBeaconID
191
+ * @returns {boolean} True if the Beacon was found and removed
192
+ */
193
+ deregisterBeacon(pBeaconID)
194
+ {
195
+ let tmpBeacon = this._Beacons[pBeaconID];
196
+
197
+ if (!tmpBeacon)
198
+ {
199
+ return false;
200
+ }
201
+
202
+ // Release any assigned work items back to Pending
203
+ for (let i = 0; i < tmpBeacon.CurrentWorkItems.length; i++)
204
+ {
205
+ let tmpWorkItem = this._WorkQueue[tmpBeacon.CurrentWorkItems[i]];
206
+ if (tmpWorkItem && (tmpWorkItem.Status === 'Assigned' || tmpWorkItem.Status === 'Running'))
207
+ {
208
+ tmpWorkItem.Status = 'Pending';
209
+ tmpWorkItem.AssignedBeaconID = null;
210
+ tmpWorkItem.ClaimedAt = null;
211
+ this.log.warn(`BeaconCoordinator: released work item [${tmpWorkItem.WorkItemHash}] from deregistered beacon [${pBeaconID}].`);
212
+ }
213
+ }
214
+
215
+ delete this._Beacons[pBeaconID];
216
+ this.log.info(`BeaconCoordinator: deregistered beacon [${pBeaconID}].`);
217
+
218
+ return true;
219
+ }
220
+
221
+ /**
222
+ * Process a heartbeat from a Beacon.
223
+ *
224
+ * @param {string} pBeaconID
225
+ * @returns {object|null} Updated Beacon record, or null if not found
226
+ */
227
+ heartbeat(pBeaconID)
228
+ {
229
+ let tmpBeacon = this._Beacons[pBeaconID];
230
+
231
+ if (!tmpBeacon)
232
+ {
233
+ return null;
234
+ }
235
+
236
+ tmpBeacon.LastHeartbeat = new Date().toISOString();
237
+
238
+ // Update status based on current work load
239
+ if (tmpBeacon.CurrentWorkItems.length >= tmpBeacon.MaxConcurrent)
240
+ {
241
+ tmpBeacon.Status = 'Busy';
242
+ }
243
+ else
244
+ {
245
+ tmpBeacon.Status = 'Online';
246
+ }
247
+
248
+ return tmpBeacon;
249
+ }
250
+
251
+ /**
252
+ * List all registered Beacons.
253
+ *
254
+ * @returns {Array} Array of Beacon records
255
+ */
256
+ listBeacons()
257
+ {
258
+ return Object.values(this._Beacons);
259
+ }
260
+
261
+ /**
262
+ * Get a specific Beacon by ID.
263
+ *
264
+ * @param {string} pBeaconID
265
+ * @returns {object|null} Beacon record or null
266
+ */
267
+ getBeacon(pBeaconID)
268
+ {
269
+ return this._Beacons[pBeaconID] || null;
270
+ }
271
+
272
+ // ====================================================================
273
+ // Work Queue
274
+ // ====================================================================
275
+
276
+ /**
277
+ * Enqueue a work item for Beacon dispatch.
278
+ *
279
+ * Called by the beacon-dispatch task type when a task needs remote execution.
280
+ *
281
+ * @param {object} pWorkItemInfo - Work item details
282
+ * @returns {object} The created work item record
283
+ */
284
+ enqueueWorkItem(pWorkItemInfo)
285
+ {
286
+ let tmpTimestamp = Date.now();
287
+ let tmpWorkItemHash = `wi-${pWorkItemInfo.RunHash || 'unknown'}-${pWorkItemInfo.NodeHash || 'unknown'}-${tmpTimestamp}`;
288
+
289
+ let tmpDefaultTimeout = this.fable.settings.UltravisorBeaconWorkItemTimeoutMs || 300000;
290
+
291
+ let tmpWorkItem = {
292
+ WorkItemHash: tmpWorkItemHash,
293
+ RunHash: pWorkItemInfo.RunHash || '',
294
+ NodeHash: pWorkItemInfo.NodeHash || '',
295
+ OperationHash: pWorkItemInfo.OperationHash || '',
296
+ Capability: pWorkItemInfo.Capability || 'Shell',
297
+ Action: pWorkItemInfo.Action || 'Execute',
298
+ Settings: pWorkItemInfo.Settings || {},
299
+ AffinityKey: pWorkItemInfo.AffinityKey || '',
300
+ AssignedBeaconID: null,
301
+ Status: 'Pending',
302
+ TimeoutMs: pWorkItemInfo.TimeoutMs || tmpDefaultTimeout,
303
+ CreatedAt: new Date(tmpTimestamp).toISOString(),
304
+ ClaimedAt: null,
305
+ CompletedAt: null,
306
+ Result: null
307
+ };
308
+
309
+ // Check for affinity binding — pre-assign to a specific Beacon
310
+ if (tmpWorkItem.AffinityKey)
311
+ {
312
+ let tmpBinding = this._AffinityBindings[tmpWorkItem.AffinityKey];
313
+
314
+ if (tmpBinding && this._Beacons[tmpBinding.BeaconID])
315
+ {
316
+ // Check if the binding has expired
317
+ if (new Date(tmpBinding.ExpiresAt) > new Date())
318
+ {
319
+ tmpWorkItem.AssignedBeaconID = tmpBinding.BeaconID;
320
+ tmpWorkItem.Status = 'Assigned';
321
+ tmpWorkItem.ClaimedAt = new Date().toISOString();
322
+
323
+ let tmpBeacon = this._Beacons[tmpBinding.BeaconID];
324
+ tmpBeacon.CurrentWorkItems.push(tmpWorkItemHash);
325
+
326
+ this.log.info(`BeaconCoordinator: work item [${tmpWorkItemHash}] pre-assigned to beacon [${tmpBinding.BeaconID}] via affinity [${tmpWorkItem.AffinityKey}].`);
327
+ }
328
+ else
329
+ {
330
+ // Binding expired, clean it up
331
+ delete this._AffinityBindings[tmpWorkItem.AffinityKey];
332
+ }
333
+ }
334
+ }
335
+
336
+ this._WorkQueue[tmpWorkItemHash] = tmpWorkItem;
337
+ this.log.info(`BeaconCoordinator: enqueued work item [${tmpWorkItemHash}] (${tmpWorkItem.Capability}/${tmpWorkItem.Action}) status=${tmpWorkItem.Status}.`);
338
+
339
+ return tmpWorkItem;
340
+ }
341
+
342
+ /**
343
+ * Poll for available work matching a Beacon's capabilities.
344
+ *
345
+ * Returns the first matching work item and assigns it to the Beacon.
346
+ * Affinity-assigned items are prioritized for the matching Beacon.
347
+ *
348
+ * @param {string} pBeaconID - The polling Beacon's ID
349
+ * @returns {object|null} A work item, or null if none available
350
+ */
351
+ pollForWork(pBeaconID)
352
+ {
353
+ let tmpBeacon = this._Beacons[pBeaconID];
354
+
355
+ if (!tmpBeacon)
356
+ {
357
+ this.log.warn(`BeaconCoordinator: poll from unknown beacon [${pBeaconID}].`);
358
+ return null;
359
+ }
360
+
361
+ // Update heartbeat on poll
362
+ tmpBeacon.LastHeartbeat = new Date().toISOString();
363
+
364
+ // Check if Beacon is at capacity
365
+ if (tmpBeacon.CurrentWorkItems.length >= tmpBeacon.MaxConcurrent)
366
+ {
367
+ return null;
368
+ }
369
+
370
+ let tmpWorkItemHashes = Object.keys(this._WorkQueue);
371
+
372
+ // First pass: check for affinity-assigned items for this Beacon
373
+ for (let i = 0; i < tmpWorkItemHashes.length; i++)
374
+ {
375
+ let tmpWorkItem = this._WorkQueue[tmpWorkItemHashes[i]];
376
+
377
+ if (tmpWorkItem.Status === 'Assigned' && tmpWorkItem.AssignedBeaconID === pBeaconID)
378
+ {
379
+ // This item was pre-assigned to us via affinity
380
+ tmpWorkItem.Status = 'Running';
381
+ this.log.info(`BeaconCoordinator: beacon [${pBeaconID}] picked up affinity-assigned work item [${tmpWorkItem.WorkItemHash}].`);
382
+ return this._sanitizeWorkItemForBeacon(tmpWorkItem);
383
+ }
384
+ }
385
+
386
+ // Second pass: check for unassigned items matching capabilities
387
+ for (let i = 0; i < tmpWorkItemHashes.length; i++)
388
+ {
389
+ let tmpWorkItem = this._WorkQueue[tmpWorkItemHashes[i]];
390
+
391
+ if (tmpWorkItem.Status !== 'Pending')
392
+ {
393
+ continue;
394
+ }
395
+
396
+ // Skip items assigned to a different Beacon (affinity)
397
+ if (tmpWorkItem.AssignedBeaconID && tmpWorkItem.AssignedBeaconID !== pBeaconID)
398
+ {
399
+ continue;
400
+ }
401
+
402
+ // Check capability match
403
+ if (tmpBeacon.Capabilities.indexOf(tmpWorkItem.Capability) === -1)
404
+ {
405
+ continue;
406
+ }
407
+
408
+ // Claim this work item
409
+ tmpWorkItem.Status = 'Running';
410
+ tmpWorkItem.AssignedBeaconID = pBeaconID;
411
+ tmpWorkItem.ClaimedAt = new Date().toISOString();
412
+
413
+ if (tmpBeacon.CurrentWorkItems.indexOf(tmpWorkItem.WorkItemHash) === -1)
414
+ {
415
+ tmpBeacon.CurrentWorkItems.push(tmpWorkItem.WorkItemHash);
416
+ }
417
+
418
+ // Create affinity binding if the work item has an AffinityKey
419
+ if (tmpWorkItem.AffinityKey && !this._AffinityBindings[tmpWorkItem.AffinityKey])
420
+ {
421
+ let tmpAffinityTTL = this.fable.settings.UltravisorBeaconAffinityTTLMs || 3600000;
422
+ this._AffinityBindings[tmpWorkItem.AffinityKey] = {
423
+ AffinityKey: tmpWorkItem.AffinityKey,
424
+ BeaconID: pBeaconID,
425
+ RunHash: tmpWorkItem.RunHash,
426
+ CreatedAt: new Date().toISOString(),
427
+ ExpiresAt: new Date(Date.now() + tmpAffinityTTL).toISOString()
428
+ };
429
+ this.log.info(`BeaconCoordinator: created affinity binding [${tmpWorkItem.AffinityKey}] → beacon [${pBeaconID}].`);
430
+ }
431
+
432
+ this.log.info(`BeaconCoordinator: beacon [${pBeaconID}] claimed work item [${tmpWorkItem.WorkItemHash}].`);
433
+ return this._sanitizeWorkItemForBeacon(tmpWorkItem);
434
+ }
435
+
436
+ return null;
437
+ }
438
+
439
+ /**
440
+ * Return a sanitized work item for the Beacon (only what it needs to execute).
441
+ *
442
+ * @param {object} pWorkItem
443
+ * @returns {object}
444
+ */
445
+ _sanitizeWorkItemForBeacon(pWorkItem)
446
+ {
447
+ return {
448
+ WorkItemHash: pWorkItem.WorkItemHash,
449
+ Capability: pWorkItem.Capability,
450
+ Action: pWorkItem.Action,
451
+ Settings: pWorkItem.Settings,
452
+ OperationHash: pWorkItem.OperationHash,
453
+ TimeoutMs: pWorkItem.TimeoutMs
454
+ };
455
+ }
456
+
457
+ /**
458
+ * Report successful completion of a work item.
459
+ *
460
+ * Resumes the paused operation via the ExecutionEngine.
461
+ *
462
+ * @param {string} pWorkItemHash
463
+ * @param {object} pResult - { Outputs, Log }
464
+ * @param {function} fCallback - function(pError)
465
+ */
466
+ completeWorkItem(pWorkItemHash, pResult, fCallback)
467
+ {
468
+ let tmpWorkItem = this._WorkQueue[pWorkItemHash];
469
+
470
+ if (!tmpWorkItem)
471
+ {
472
+ return fCallback(new Error(`BeaconCoordinator: work item [${pWorkItemHash}] not found.`));
473
+ }
474
+
475
+ if (tmpWorkItem.Status === 'Complete' || tmpWorkItem.Status === 'Error' || tmpWorkItem.Status === 'Timeout')
476
+ {
477
+ return fCallback(new Error(`BeaconCoordinator: work item [${pWorkItemHash}] already finalized (${tmpWorkItem.Status}).`));
478
+ }
479
+
480
+ tmpWorkItem.Status = 'Complete';
481
+ tmpWorkItem.CompletedAt = new Date().toISOString();
482
+
483
+ // Merge accumulated progress logs with the final completion log
484
+ let tmpFinalResult = pResult || {};
485
+ if (tmpWorkItem.AccumulatedLog && tmpWorkItem.AccumulatedLog.length > 0)
486
+ {
487
+ let tmpCompletionLog = tmpFinalResult.Log || [];
488
+ tmpFinalResult.Log = tmpWorkItem.AccumulatedLog.concat(tmpCompletionLog);
489
+ }
490
+
491
+ tmpWorkItem.Result = tmpFinalResult;
492
+
493
+ // Remove from Beacon's current work list
494
+ this._removeWorkItemFromBeacon(tmpWorkItem.AssignedBeaconID, pWorkItemHash);
495
+
496
+ this.log.info(`BeaconCoordinator: work item [${pWorkItemHash}] completed by beacon [${tmpWorkItem.AssignedBeaconID}].`);
497
+
498
+ // Check for direct dispatch callback (synchronous HTTP dispatch)
499
+ if (this._resolveDirectDispatch(pWorkItemHash, null, {
500
+ Success: true,
501
+ WorkItemHash: pWorkItemHash,
502
+ Outputs: tmpFinalResult.Outputs || {},
503
+ Log: tmpFinalResult.Log || []
504
+ }))
505
+ {
506
+ // Direct dispatch — clean up and done
507
+ delete this._WorkQueue[pWorkItemHash];
508
+ return fCallback(null);
509
+ }
510
+
511
+ // No RunHash means this was a standalone work item (not part of an operation)
512
+ if (!tmpWorkItem.RunHash)
513
+ {
514
+ delete this._WorkQueue[pWorkItemHash];
515
+ return fCallback(null);
516
+ }
517
+
518
+ // Resume the paused operation
519
+ let tmpEngine = this._getService('UltravisorExecutionEngine');
520
+
521
+ if (!tmpEngine)
522
+ {
523
+ return fCallback(new Error('BeaconCoordinator: ExecutionEngine service not found.'));
524
+ }
525
+
526
+ // Build the structured outputs to pass to resumeOperation
527
+ let tmpOutputs = tmpFinalResult.Outputs || {};
528
+
529
+ // Include the BeaconID in the outputs so downstream tasks can reference it
530
+ tmpOutputs.BeaconID = tmpWorkItem.AssignedBeaconID || '';
531
+
532
+ tmpEngine.resumeOperation(tmpWorkItem.RunHash, tmpWorkItem.NodeHash, tmpOutputs,
533
+ (pError, pContext) =>
534
+ {
535
+ if (pError)
536
+ {
537
+ this.log.error(`BeaconCoordinator: error resuming operation for work item [${pWorkItemHash}]: ${pError.message}`);
538
+ return fCallback(pError);
539
+ }
540
+
541
+ this.log.info(`BeaconCoordinator: operation [${tmpWorkItem.RunHash}] resumed after work item [${pWorkItemHash}] completion.`);
542
+
543
+ // Clean up the completed work item from the queue
544
+ delete this._WorkQueue[pWorkItemHash];
545
+
546
+ return fCallback(null);
547
+ });
548
+ }
549
+
550
+ /**
551
+ * Report failure of a work item.
552
+ *
553
+ * Resumes the paused operation with an error result so the graph's
554
+ * error path can handle it.
555
+ *
556
+ * @param {string} pWorkItemHash
557
+ * @param {object} pError - { ErrorMessage, Log }
558
+ * @param {function} fCallback - function(pError)
559
+ */
560
+ failWorkItem(pWorkItemHash, pError, fCallback)
561
+ {
562
+ let tmpWorkItem = this._WorkQueue[pWorkItemHash];
563
+
564
+ if (!tmpWorkItem)
565
+ {
566
+ return fCallback(new Error(`BeaconCoordinator: work item [${pWorkItemHash}] not found.`));
567
+ }
568
+
569
+ if (tmpWorkItem.Status === 'Complete' || tmpWorkItem.Status === 'Error' || tmpWorkItem.Status === 'Timeout')
570
+ {
571
+ return fCallback(new Error(`BeaconCoordinator: work item [${pWorkItemHash}] already finalized (${tmpWorkItem.Status}).`));
572
+ }
573
+
574
+ tmpWorkItem.Status = 'Error';
575
+ tmpWorkItem.CompletedAt = new Date().toISOString();
576
+ tmpWorkItem.Result = { Error: pError.ErrorMessage || 'Unknown error', Log: pError.Log || [] };
577
+
578
+ // Remove from Beacon's current work list
579
+ this._removeWorkItemFromBeacon(tmpWorkItem.AssignedBeaconID, pWorkItemHash);
580
+
581
+ this.log.warn(`BeaconCoordinator: work item [${pWorkItemHash}] failed: ${pError.ErrorMessage || 'Unknown error'}`);
582
+
583
+ // Check for direct dispatch callback (synchronous HTTP dispatch)
584
+ if (this._resolveDirectDispatch(pWorkItemHash,
585
+ new Error(pError.ErrorMessage || 'Beacon work item failed.'), null))
586
+ {
587
+ // Direct dispatch — clean up and done
588
+ delete this._WorkQueue[pWorkItemHash];
589
+ return fCallback(null);
590
+ }
591
+
592
+ // No RunHash means this was a standalone work item (not part of an operation)
593
+ if (!tmpWorkItem.RunHash)
594
+ {
595
+ delete this._WorkQueue[pWorkItemHash];
596
+ return fCallback(null);
597
+ }
598
+
599
+ // Resume the operation — the beacon-dispatch task type fires 'Error' event
600
+ // by storing error info in outputs; the ResumeEventName handles routing
601
+ let tmpEngine = this._getService('UltravisorExecutionEngine');
602
+
603
+ if (!tmpEngine)
604
+ {
605
+ return fCallback(new Error('BeaconCoordinator: ExecutionEngine service not found.'));
606
+ }
607
+
608
+ let tmpErrorOutputs = {
609
+ StdOut: pError.ErrorMessage || 'Beacon work item failed.',
610
+ ExitCode: -1,
611
+ BeaconID: tmpWorkItem.AssignedBeaconID || '',
612
+ _BeaconError: true
613
+ };
614
+
615
+ // Resume with 'Error' event by temporarily modifying the waiting task's ResumeEventName
616
+ let tmpManifest = this._getService('UltravisorExecutionManifest');
617
+ if (tmpManifest)
618
+ {
619
+ let tmpContext = tmpManifest.getRun(tmpWorkItem.RunHash);
620
+ if (tmpContext && tmpContext.WaitingTasks[tmpWorkItem.NodeHash])
621
+ {
622
+ tmpContext.WaitingTasks[tmpWorkItem.NodeHash].ResumeEventName = 'Error';
623
+ }
624
+ }
625
+
626
+ tmpEngine.resumeOperation(tmpWorkItem.RunHash, tmpWorkItem.NodeHash, tmpErrorOutputs,
627
+ (pResumeError, pContext) =>
628
+ {
629
+ if (pResumeError)
630
+ {
631
+ this.log.error(`BeaconCoordinator: error resuming operation for failed work item [${pWorkItemHash}]: ${pResumeError.message}`);
632
+ return fCallback(pResumeError);
633
+ }
634
+
635
+ this.log.info(`BeaconCoordinator: operation [${tmpWorkItem.RunHash}] resumed with error path after work item [${pWorkItemHash}] failure.`);
636
+
637
+ // Clean up the failed work item
638
+ delete this._WorkQueue[pWorkItemHash];
639
+
640
+ return fCallback(null);
641
+ });
642
+ }
643
+
644
+ /**
645
+ * List all work items (for admin view).
646
+ *
647
+ * @returns {Array} Array of work item records
648
+ */
649
+ listWorkItems()
650
+ {
651
+ return Object.values(this._WorkQueue);
652
+ }
653
+
654
+ /**
655
+ * Get a specific work item by hash.
656
+ *
657
+ * @param {string} pWorkItemHash
658
+ * @returns {object|null}
659
+ */
660
+ getWorkItem(pWorkItemHash)
661
+ {
662
+ return this._WorkQueue[pWorkItemHash] || null;
663
+ }
664
+
665
+ // ====================================================================
666
+ // Direct Dispatch (synchronous HTTP dispatch)
667
+ // ====================================================================
668
+
669
+ /**
670
+ * Dispatch a work item and wait for completion.
671
+ *
672
+ * Used by the POST /Beacon/Work/Dispatch endpoint to provide
673
+ * synchronous request/response semantics for external consumers
674
+ * (e.g. retold-remote). The HTTP connection stays open until the
675
+ * beacon completes the work item.
676
+ *
677
+ * @param {object} pWorkItemInfo - Work item details (no RunHash/NodeHash needed)
678
+ * @param {function} fCallback - function(pError, pResult) called when work completes
679
+ */
680
+ dispatchAndWait(pWorkItemInfo, fCallback)
681
+ {
682
+ // Ensure no RunHash — direct dispatch bypasses the operation graph
683
+ pWorkItemInfo.RunHash = '';
684
+ pWorkItemInfo.NodeHash = '';
685
+ pWorkItemInfo.OperationHash = '';
686
+
687
+ // Enqueue the work item
688
+ let tmpWorkItem = this.enqueueWorkItem(pWorkItemInfo);
689
+
690
+ let tmpTimeoutMs = pWorkItemInfo.TimeoutMs || this.fable.settings.UltravisorBeaconWorkItemTimeoutMs || 300000;
691
+
692
+ // Register completion callback
693
+ let tmpTimeoutHandle = setTimeout(
694
+ () =>
695
+ {
696
+ // Timeout — clean up and call back with error
697
+ let tmpEntry = this._DirectDispatchCallbacks[tmpWorkItem.WorkItemHash];
698
+ if (tmpEntry)
699
+ {
700
+ delete this._DirectDispatchCallbacks[tmpWorkItem.WorkItemHash];
701
+
702
+ // Clean up the work item from the queue
703
+ let tmpWI = this._WorkQueue[tmpWorkItem.WorkItemHash];
704
+ if (tmpWI)
705
+ {
706
+ this._removeWorkItemFromBeacon(tmpWI.AssignedBeaconID, tmpWorkItem.WorkItemHash);
707
+ delete this._WorkQueue[tmpWorkItem.WorkItemHash];
708
+ }
709
+
710
+ this.log.warn(`BeaconCoordinator: direct dispatch [${tmpWorkItem.WorkItemHash}] timed out after ${tmpTimeoutMs}ms.`);
711
+ tmpEntry.callback(new Error(`Direct dispatch timed out after ${tmpTimeoutMs}ms.`));
712
+ }
713
+ },
714
+ tmpTimeoutMs);
715
+
716
+ this._DirectDispatchCallbacks[tmpWorkItem.WorkItemHash] =
717
+ {
718
+ callback: fCallback,
719
+ timeoutHandle: tmpTimeoutHandle
720
+ };
721
+
722
+ this.log.info(`BeaconCoordinator: direct dispatch [${tmpWorkItem.WorkItemHash}] waiting for completion (timeout: ${tmpTimeoutMs}ms).`);
723
+ }
724
+
725
+ /**
726
+ * Check if a work item has a direct dispatch callback registered.
727
+ * If so, call it and return true. Otherwise return false.
728
+ *
729
+ * @param {string} pWorkItemHash
730
+ * @param {object|null} pError - Error object (for fail path)
731
+ * @param {object|null} pResult - Result object (for complete path)
732
+ * @returns {boolean} True if a direct dispatch callback was found and called
733
+ */
734
+ _resolveDirectDispatch(pWorkItemHash, pError, pResult)
735
+ {
736
+ let tmpEntry = this._DirectDispatchCallbacks[pWorkItemHash];
737
+
738
+ if (!tmpEntry)
739
+ {
740
+ return false;
741
+ }
742
+
743
+ // Clear the timeout
744
+ clearTimeout(tmpEntry.timeoutHandle);
745
+ delete this._DirectDispatchCallbacks[pWorkItemHash];
746
+
747
+ if (pError)
748
+ {
749
+ tmpEntry.callback(pError);
750
+ }
751
+ else
752
+ {
753
+ tmpEntry.callback(null, pResult);
754
+ }
755
+
756
+ return true;
757
+ }
758
+
759
+ // ====================================================================
760
+ // Progress Reporting
761
+ // ====================================================================
762
+
763
+ /**
764
+ * Update progress for a running work item.
765
+ *
766
+ * Called by the Beacon during execution to report progress and
767
+ * stream log entries. Progress data is stored on the work item
768
+ * record and reflected in the manifest's WaitingTasks so it
769
+ * surfaces via GET /Manifest/:RunHash.
770
+ *
771
+ * @param {string} pWorkItemHash
772
+ * @param {object} pProgressData - { Percent?, Message?, Step?, TotalSteps?, Log? }
773
+ * @returns {boolean} true if progress was updated
774
+ */
775
+ updateProgress(pWorkItemHash, pProgressData)
776
+ {
777
+ let tmpWorkItem = this._WorkQueue[pWorkItemHash];
778
+
779
+ if (!tmpWorkItem)
780
+ {
781
+ return false;
782
+ }
783
+
784
+ if (tmpWorkItem.Status !== 'Running' && tmpWorkItem.Status !== 'Assigned')
785
+ {
786
+ return false;
787
+ }
788
+
789
+ // Update progress fields on the work item
790
+ tmpWorkItem.Progress = {
791
+ Percent: (pProgressData.Percent !== undefined) ? pProgressData.Percent : (tmpWorkItem.Progress ? tmpWorkItem.Progress.Percent : undefined),
792
+ Message: pProgressData.Message || (tmpWorkItem.Progress ? tmpWorkItem.Progress.Message : ''),
793
+ Step: (pProgressData.Step !== undefined) ? pProgressData.Step : (tmpWorkItem.Progress ? tmpWorkItem.Progress.Step : undefined),
794
+ TotalSteps: (pProgressData.TotalSteps !== undefined) ? pProgressData.TotalSteps : (tmpWorkItem.Progress ? tmpWorkItem.Progress.TotalSteps : undefined),
795
+ UpdatedAt: new Date().toISOString()
796
+ };
797
+
798
+ // Accumulate log entries
799
+ if (Array.isArray(pProgressData.Log) && pProgressData.Log.length > 0)
800
+ {
801
+ if (!tmpWorkItem.AccumulatedLog)
802
+ {
803
+ tmpWorkItem.AccumulatedLog = [];
804
+ }
805
+ for (let i = 0; i < pProgressData.Log.length; i++)
806
+ {
807
+ tmpWorkItem.AccumulatedLog.push(pProgressData.Log[i]);
808
+ }
809
+ }
810
+
811
+ // Update the manifest's WaitingTasks entry so progress surfaces
812
+ // via GET /Manifest/:RunHash
813
+ let tmpManifest = this._getService('UltravisorExecutionManifest');
814
+ if (tmpManifest)
815
+ {
816
+ let tmpContext = tmpManifest.getRun(tmpWorkItem.RunHash);
817
+ if (tmpContext && tmpContext.WaitingTasks[tmpWorkItem.NodeHash])
818
+ {
819
+ tmpContext.WaitingTasks[tmpWorkItem.NodeHash].Progress = tmpWorkItem.Progress;
820
+ }
821
+ }
822
+
823
+ return true;
824
+ }
825
+
826
+ // ====================================================================
827
+ // Affinity
828
+ // ====================================================================
829
+
830
+ /**
831
+ * List all active affinity bindings.
832
+ *
833
+ * @returns {Array}
834
+ */
835
+ listAffinityBindings()
836
+ {
837
+ return Object.values(this._AffinityBindings);
838
+ }
839
+
840
+ /**
841
+ * Clear a specific affinity binding.
842
+ *
843
+ * @param {string} pAffinityKey
844
+ * @returns {boolean}
845
+ */
846
+ clearAffinityBinding(pAffinityKey)
847
+ {
848
+ if (this._AffinityBindings[pAffinityKey])
849
+ {
850
+ delete this._AffinityBindings[pAffinityKey];
851
+ return true;
852
+ }
853
+ return false;
854
+ }
855
+
856
+ // ====================================================================
857
+ // Internal Helpers
858
+ // ====================================================================
859
+
860
+ /**
861
+ * Get a service by type name.
862
+ */
863
+ _getService(pTypeName)
864
+ {
865
+ return this.fable.servicesMap[pTypeName]
866
+ ? Object.values(this.fable.servicesMap[pTypeName])[0]
867
+ : null;
868
+ }
869
+
870
+ /**
871
+ * Remove a work item hash from a Beacon's CurrentWorkItems array.
872
+ */
873
+ _removeWorkItemFromBeacon(pBeaconID, pWorkItemHash)
874
+ {
875
+ if (!pBeaconID)
876
+ {
877
+ return;
878
+ }
879
+
880
+ let tmpBeacon = this._Beacons[pBeaconID];
881
+
882
+ if (!tmpBeacon)
883
+ {
884
+ return;
885
+ }
886
+
887
+ let tmpIndex = tmpBeacon.CurrentWorkItems.indexOf(pWorkItemHash);
888
+ if (tmpIndex > -1)
889
+ {
890
+ tmpBeacon.CurrentWorkItems.splice(tmpIndex, 1);
891
+ }
892
+
893
+ // Update status
894
+ if (tmpBeacon.CurrentWorkItems.length < tmpBeacon.MaxConcurrent)
895
+ {
896
+ tmpBeacon.Status = 'Online';
897
+ }
898
+ }
899
+
900
+ /**
901
+ * Check for timed-out work items and dead Beacons.
902
+ */
903
+ _checkTimeouts()
904
+ {
905
+ let tmpNow = Date.now();
906
+
907
+ // Check work item timeouts
908
+ let tmpWorkItemHashes = Object.keys(this._WorkQueue);
909
+ for (let i = 0; i < tmpWorkItemHashes.length; i++)
910
+ {
911
+ let tmpWorkItem = this._WorkQueue[tmpWorkItemHashes[i]];
912
+
913
+ if (tmpWorkItem.Status !== 'Running' && tmpWorkItem.Status !== 'Assigned')
914
+ {
915
+ continue;
916
+ }
917
+
918
+ let tmpClaimedTime = tmpWorkItem.ClaimedAt ? new Date(tmpWorkItem.ClaimedAt).getTime() : tmpNow;
919
+ let tmpElapsed = tmpNow - tmpClaimedTime;
920
+
921
+ if (tmpElapsed > tmpWorkItem.TimeoutMs)
922
+ {
923
+ this.log.warn(`BeaconCoordinator: work item [${tmpWorkItem.WorkItemHash}] timed out after ${tmpElapsed}ms.`);
924
+ this.failWorkItem(tmpWorkItem.WorkItemHash,
925
+ { ErrorMessage: `Work item timed out after ${tmpElapsed}ms.`, Log: ['Timeout'] },
926
+ (pError) =>
927
+ {
928
+ if (pError)
929
+ {
930
+ this.log.error(`BeaconCoordinator: error handling timeout for [${tmpWorkItem.WorkItemHash}]: ${pError.message}`);
931
+ }
932
+ });
933
+ }
934
+ }
935
+
936
+ // Check Beacon heartbeat timeouts
937
+ let tmpHeartbeatTimeout = this.fable.settings.UltravisorBeaconHeartbeatTimeoutMs || 60000;
938
+ let tmpBeaconIDs = Object.keys(this._Beacons);
939
+ for (let i = 0; i < tmpBeaconIDs.length; i++)
940
+ {
941
+ let tmpBeacon = this._Beacons[tmpBeaconIDs[i]];
942
+ let tmpLastHeartbeat = new Date(tmpBeacon.LastHeartbeat).getTime();
943
+ let tmpElapsed = tmpNow - tmpLastHeartbeat;
944
+
945
+ if (tmpElapsed > tmpHeartbeatTimeout && tmpBeacon.Status !== 'Offline')
946
+ {
947
+ this.log.warn(`BeaconCoordinator: beacon [${tmpBeacon.BeaconID}] missed heartbeat (${tmpElapsed}ms since last), marking Offline.`);
948
+ tmpBeacon.Status = 'Offline';
949
+ }
950
+ }
951
+
952
+ // Clean up expired affinity bindings
953
+ let tmpAffinityKeys = Object.keys(this._AffinityBindings);
954
+ for (let i = 0; i < tmpAffinityKeys.length; i++)
955
+ {
956
+ let tmpBinding = this._AffinityBindings[tmpAffinityKeys[i]];
957
+ if (new Date(tmpBinding.ExpiresAt).getTime() < tmpNow)
958
+ {
959
+ delete this._AffinityBindings[tmpAffinityKeys[i]];
960
+ }
961
+ }
962
+ }
963
+ }
964
+
965
+ module.exports = UltravisorBeaconCoordinator;
966
+ module.exports.default_configuration = {};