ultravisor 1.3.19 → 1.3.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultravisor",
3
- "version": "1.3.19",
3
+ "version": "1.3.21",
4
4
  "description": "Cyclic process execution with ai integration.",
5
5
  "main": "source/Ultravisor.cjs",
6
6
  "bin": {
@@ -55,18 +55,18 @@
55
55
  "homepage": "https://github.com/stevenvelozo/ultravisor#readme",
56
56
  "dependencies": {
57
57
  "cron": "^4.4.0",
58
- "meadow": "^2.0.43",
58
+ "meadow": "^2.0.44",
59
59
  "meadow-connection-sqlite": "^1.0.20",
60
- "meadow-migrationmanager": "^1.0.4",
60
+ "meadow-migrationmanager": "^1.0.5",
61
61
  "orator": "^6.1.2",
62
- "orator-authentication": "^1.0.4",
62
+ "orator-authentication": "^1.0.6",
63
63
  "orator-serviceserver-restify": "^2.0.11",
64
- "pict": "^1.0.372",
65
- "pict-service-commandlineutility": "^1.0.19",
64
+ "pict": "^1.0.381",
65
+ "pict-service-commandlineutility": "^1.0.20",
66
66
  "pict-serviceproviderbase": "^1.0.4",
67
- "ultravisor-beacon": "^1.0.4",
67
+ "ultravisor-beacon": "^1.0.5",
68
68
  "ultravisor-file-stream": "^1.0.0",
69
- "ws": "^8.20.0"
69
+ "ws": "^8.21.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "pict-docuserve": "^1.4.19",
@@ -640,8 +640,10 @@ class UltravisorBeaconCoordinator extends libPictService
640
640
 
641
641
  // Add standard beacon dispatch settings
642
642
  tmpSettingsInputs.push({ Name: 'AffinityKey', DataType: 'String', Required: false, Description: 'Sticky routing key for beacon affinity.' });
643
+ tmpSettingsInputs.push({ Name: 'RequireAffinityMatch', DataType: 'Boolean', Required: false, Description: 'Strict routing: AffinityKey must match a registered beacon Name; the item waits for that beacon instead of falling back to any capable one.' });
643
644
  tmpSettingsInputs.push({ Name: 'TimeoutMs', DataType: 'Number', Required: false, Description: 'Work item timeout in milliseconds.' });
644
645
  tmpDefaultSettings.AffinityKey = '';
646
+ tmpDefaultSettings.RequireAffinityMatch = false;
645
647
  tmpDefaultSettings.TimeoutMs = 300000;
646
648
 
647
649
  // Derive a display name from the action
@@ -713,6 +715,7 @@ class UltravisorBeaconCoordinator extends libPictService
713
715
  // Build settings object from resolved settings, excluding beacon dispatch meta-fields
714
716
  let tmpSettings = Object.assign({}, pResolvedSettings);
715
717
  delete tmpSettings.AffinityKey;
718
+ delete tmpSettings.RequireAffinityMatch;
716
719
  delete tmpSettings.TimeoutMs;
717
720
 
718
721
  // Coerce types based on schema
@@ -742,6 +745,7 @@ class UltravisorBeaconCoordinator extends libPictService
742
745
  Action: pAction,
743
746
  Settings: tmpSettings,
744
747
  AffinityKey: pResolvedSettings.AffinityKey || '',
748
+ RequireAffinityMatch: !!pResolvedSettings.RequireAffinityMatch,
745
749
  TimeoutMs: pResolvedSettings.TimeoutMs || 300000
746
750
  }, pExecutionContext, fCallback);
747
751
  };
@@ -1115,6 +1119,7 @@ class UltravisorBeaconCoordinator extends libPictService
1115
1119
  Action: pWorkItemInfo.Action || 'Execute',
1116
1120
  Settings: tmpSettings,
1117
1121
  AffinityKey: pWorkItemInfo.AffinityKey || '',
1122
+ RequireAffinityMatch: !!pWorkItemInfo.RequireAffinityMatch,
1118
1123
  AssignedBeaconID: null,
1119
1124
  Status: 'Pending',
1120
1125
  TimeoutMs: pWorkItemInfo.TimeoutMs || tmpDefaultTimeout,
@@ -1176,6 +1181,14 @@ class UltravisorBeaconCoordinator extends libPictService
1176
1181
  tmpNamedBeacon.CurrentWorkItems.push(tmpWorkItemHash);
1177
1182
  this.log.info(`BeaconCoordinator: work item [${tmpWorkItemHash}] routed by name to beacon [${tmpNamedBeacon.BeaconID}] (Name=${tmpNamedBeacon.Name}) via AffinityKey [${tmpWorkItem.AffinityKey}].`);
1178
1183
  }
1184
+ else if (tmpWorkItem.RequireAffinityMatch)
1185
+ {
1186
+ // Strict routing: the designated beacon is not registered right
1187
+ // now (offline / mid-restart). The item stays Pending and only
1188
+ // that beacon may claim it when it returns — never another
1189
+ // capable beacon. The normal work-item timeout bounds the wait.
1190
+ this.log.warn(`BeaconCoordinator: work item [${tmpWorkItemHash}] requires beacon [${tmpWorkItem.AffinityKey}], which is not registered — holding pending until it returns.`);
1191
+ }
1179
1192
  else
1180
1193
  {
1181
1194
  let tmpBinding = this._AffinityBindings[tmpWorkItem.AffinityKey];
@@ -1275,23 +1288,101 @@ class UltravisorBeaconCoordinator extends libPictService
1275
1288
  }
1276
1289
  else if (tmpWorkItem.Status === 'Assigned' && tmpWorkItem.AssignedBeaconID && this._WorkItemPushHandler)
1277
1290
  {
1278
- // Affinity pre-assigned — push directly to the assigned beacon via WebSocket
1279
- tmpWorkItem.Status = 'Running';
1280
- let tmpPushed = this._WorkItemPushHandler(tmpWorkItem.AssignedBeaconID,
1281
- this._sanitizeWorkItemForBeacon(tmpWorkItem));
1291
+ // Affinity pre-assigned — push directly to the assigned beacon via
1292
+ // WebSocket, but only within the beacon's RUNNING capacity. The
1293
+ // beacon SDK silently drops frames beyond its concurrency, so an
1294
+ // ungated burst loses work items (they sit "Running" until the
1295
+ // direct-dispatch timeout while the beacon never saw them).
1296
+ // Over-capacity items stay Assigned; the slot-free re-dispatch in
1297
+ // _dispatchPendingWorkItems delivers them as completions land.
1298
+ this._pushAssignedWorkItem(tmpWorkItem);
1299
+ }
1282
1300
 
1283
- if (tmpPushed)
1284
- {
1285
- this.log.info(`BeaconCoordinator: pushed affinity-assigned work item [${tmpWorkItemHash}] to WebSocket beacon [${tmpWorkItem.AssignedBeaconID}].`);
1286
- }
1287
- else
1301
+ return tmpWorkItem;
1302
+ }
1303
+
1304
+ /**
1305
+ * Push an Assigned work item to its designated WebSocket beacon when the
1306
+ * beacon has Running capacity. Leaves the item Assigned (HTTP-poll
1307
+ * claimable, slot-free re-deliverable) when the beacon is full, offline,
1308
+ * or the push fails.
1309
+ *
1310
+ * @param {object} pWorkItem
1311
+ * @returns {boolean} True when the item was delivered.
1312
+ */
1313
+ _pushAssignedWorkItem(pWorkItem)
1314
+ {
1315
+ if (!this._WorkItemPushHandler || !pWorkItem.AssignedBeaconID)
1316
+ {
1317
+ return false;
1318
+ }
1319
+ let tmpBeacon = this._Beacons[pWorkItem.AssignedBeaconID];
1320
+ if (!tmpBeacon || (tmpBeacon.Status !== 'Online' && tmpBeacon.Status !== 'Busy'))
1321
+ {
1322
+ return false;
1323
+ }
1324
+ let tmpMaxConcurrent = tmpBeacon.MaxConcurrent || 1;
1325
+ if (this._runningCountForBeacon(pWorkItem.AssignedBeaconID) >= tmpMaxConcurrent)
1326
+ {
1327
+ this.log.info(`BeaconCoordinator: beacon [${pWorkItem.AssignedBeaconID}] at running capacity (${tmpMaxConcurrent}) — holding assigned work item [${pWorkItem.WorkItemHash}] for slot-free delivery.`);
1328
+ return false;
1329
+ }
1330
+ pWorkItem.Status = 'Running';
1331
+ let tmpPushed = this._WorkItemPushHandler(pWorkItem.AssignedBeaconID,
1332
+ this._sanitizeWorkItemForBeacon(pWorkItem));
1333
+ if (tmpPushed)
1334
+ {
1335
+ this.log.info(`BeaconCoordinator: pushed affinity-assigned work item [${pWorkItem.WorkItemHash}] to WebSocket beacon [${pWorkItem.AssignedBeaconID}].`);
1336
+ return true;
1337
+ }
1338
+ pWorkItem.Status = 'Assigned';
1339
+ return false;
1340
+ }
1341
+
1342
+ /**
1343
+ * Count work items currently RUNNING on a beacon. CurrentWorkItems
1344
+ * includes Assigned-but-not-yet-delivered items, so it overstates the
1345
+ * beacon's true in-flight load — pushes must be gated on Running only,
1346
+ * or a burst of affinity-assigned items overruns the beacon SDK's
1347
+ * concurrency and the overflow frames are silently dropped.
1348
+ *
1349
+ * @param {string} pBeaconID
1350
+ * @returns {number}
1351
+ */
1352
+ _runningCountForBeacon(pBeaconID)
1353
+ {
1354
+ let tmpCount = 0;
1355
+ let tmpHashes = Object.keys(this._WorkQueue);
1356
+ for (let i = 0; i < tmpHashes.length; i++)
1357
+ {
1358
+ let tmpWI = this._WorkQueue[tmpHashes[i]];
1359
+ if (tmpWI && tmpWI.AssignedBeaconID === pBeaconID && tmpWI.Status === 'Running')
1288
1360
  {
1289
- // WebSocket push failed — revert to Assigned for HTTP poll pickup
1290
- tmpWorkItem.Status = 'Assigned';
1361
+ tmpCount++;
1291
1362
  }
1292
1363
  }
1364
+ return tmpCount;
1365
+ }
1293
1366
 
1294
- return tmpWorkItem;
1367
+ /**
1368
+ * Whether a beacon is allowed to take a work item under affinity rules.
1369
+ *
1370
+ * RequireAffinityMatch items designate a beacon BY NAME: only that beacon
1371
+ * may be pushed, polled, or sticky-bound the item. Without the flag,
1372
+ * AffinityKey keeps its dual role (name routing when matched, otherwise a
1373
+ * session-stickiness hint any capable beacon can seed).
1374
+ *
1375
+ * @param {object} pWorkItem
1376
+ * @param {object} pBeacon - a registered beacon record
1377
+ * @returns {boolean}
1378
+ */
1379
+ _beaconSatisfiesAffinity(pWorkItem, pBeacon)
1380
+ {
1381
+ if (!pWorkItem || !pWorkItem.RequireAffinityMatch || !pWorkItem.AffinityKey)
1382
+ {
1383
+ return true;
1384
+ }
1385
+ return !!(pBeacon && pBeacon.Name === pWorkItem.AffinityKey);
1295
1386
  }
1296
1387
 
1297
1388
  /**
@@ -1328,6 +1419,11 @@ class UltravisorBeaconCoordinator extends libPictService
1328
1419
  {
1329
1420
  continue;
1330
1421
  }
1422
+ if (!this._beaconSatisfiesAffinity(pWorkItem, tmpBeacon))
1423
+ {
1424
+ continue;
1425
+ }
1426
+
1331
1427
  if (tmpBeacon.Capabilities.indexOf(pWorkItem.Capability) === -1)
1332
1428
  {
1333
1429
  continue;
@@ -1527,6 +1623,12 @@ class UltravisorBeaconCoordinator extends libPictService
1527
1623
  continue;
1528
1624
  }
1529
1625
 
1626
+ // Strict affinity: only the designated beacon may claim
1627
+ if (!this._beaconSatisfiesAffinity(tmpWorkItem, tmpBeacon))
1628
+ {
1629
+ continue;
1630
+ }
1631
+
1530
1632
  // Check capability match
1531
1633
  if (tmpBeacon.Capabilities.indexOf(tmpWorkItem.Capability) === -1)
1532
1634
  {
@@ -2089,7 +2191,9 @@ class UltravisorBeaconCoordinator extends libPictService
2089
2191
  {
2090
2192
  if (tmpContext.WaitingTasks[tmpWorkItem.NodeHash])
2091
2193
  {
2092
- tmpContext.WaitingTasks[tmpWorkItem.NodeHash].ResumeEventName = 'error';
2194
+ // Must exactly match the EventOutputs Name 'Error' — the engine's
2195
+ // downstream-event match is case-sensitive.
2196
+ tmpContext.WaitingTasks[tmpWorkItem.NodeHash].ResumeEventName = 'Error';
2093
2197
  }
2094
2198
  // Push the failure into the canonical Errors[] log so
2095
2199
  // finalizeExecution's roll-up sees it and the operation
@@ -2668,6 +2772,13 @@ class UltravisorBeaconCoordinator extends libPictService
2668
2772
  {
2669
2773
  let tmpWI = this._WorkQueue[tmpHashes[i]];
2670
2774
  if (!tmpWI) continue;
2775
+ if (tmpWI.Status === 'Assigned' && tmpWI.AssignedBeaconID)
2776
+ {
2777
+ // Held back at enqueue because the designated beacon was at
2778
+ // running capacity — deliver as slots free up.
2779
+ this._pushAssignedWorkItem(tmpWI);
2780
+ continue;
2781
+ }
2671
2782
  if (tmpWI.Status !== 'Pending') continue;
2672
2783
  if (tmpWI.AssignedBeaconID) continue; // affinity-assigned, leave it
2673
2784
  this._tryPushToWebSocketBeacon(tmpWI);
@@ -1541,6 +1541,7 @@ class UltravisorExecutionEngine extends libPictService
1541
1541
  let tmpConnections = pContext._ConnectionMap.eventSources[pSourceNodeHash] || [];
1542
1542
  let tmpPortLabelMap = pContext._PortLabelMap;
1543
1543
 
1544
+ let tmpMatchedCount = 0;
1544
1545
  for (let i = 0; i < tmpConnections.length; i++)
1545
1546
  {
1546
1547
  let tmpConn = tmpConnections[i];
@@ -1548,6 +1549,7 @@ class UltravisorExecutionEngine extends libPictService
1548
1549
 
1549
1550
  if (tmpSourcePortName === pEventName)
1550
1551
  {
1552
+ tmpMatchedCount++;
1551
1553
  let tmpTargetPortName = this._extractPortName(tmpConn.TargetPortHash, tmpPortLabelMap);
1552
1554
  pContext.PendingEvents.push({
1553
1555
  TargetNodeHash: tmpConn.TargetNodeHash,
@@ -1555,6 +1557,14 @@ class UltravisorExecutionEngine extends libPictService
1555
1557
  });
1556
1558
  }
1557
1559
  }
1560
+
1561
+ // An event that matches none of the node's outgoing connections strands
1562
+ // everything downstream while the run can still terminate 'Complete' —
1563
+ // surface it (event-name/port-label matching is case-sensitive).
1564
+ if (tmpConnections.length > 0 && tmpMatchedCount === 0)
1565
+ {
1566
+ this._log(pContext, `Node [${pSourceNodeHash}] fired event [${pEventName}] but none of its ${tmpConnections.length} outgoing event connection(s) match that name — downstream nodes will not run.`, 'warn');
1567
+ }
1558
1568
  }
1559
1569
 
1560
1570
  /**
@@ -421,7 +421,17 @@ module.exports =
421
421
  {
422
422
  try
423
423
  {
424
- tmpResult = pTask.fable.ExpressionParser.resolve(tmpExpression, pExecutionContext);
424
+ // Fable's ExpressionParser API is solve(expression, dataSource,
425
+ // results, manifest, destination). The data source mirrors the
426
+ // StateManager address roots so expressions can reference
427
+ // Operation.X / Global.X / TaskOutput.<node>.<key> directly.
428
+ tmpResult = pTask.fable.ExpressionParser.solve(tmpExpression,
429
+ {
430
+ Operation: pExecutionContext.OperationState || {},
431
+ Global: pExecutionContext.GlobalState || {},
432
+ TaskOutput: pExecutionContext.TaskOutputs || {}
433
+ },
434
+ {}, pTask.fable.manifest, {});
425
435
  }
426
436
  catch (pError)
427
437
  {
@@ -131,10 +131,13 @@ module.exports =
131
131
  pTask.log.info(`Beacon Dispatch: enqueued work item [${tmpWorkItem.WorkItemHash}] for capability [${tmpWorkItemInfo.Capability}/${tmpWorkItemInfo.Action}]` +
132
132
  (tmpWorkItemInfo.AffinityKey ? ` with affinity [${tmpWorkItemInfo.AffinityKey}]` : ''));
133
133
 
134
- // Pause execution — the BeaconCoordinator will call resumeOperation when the Beacon reports back
134
+ // Pause execution — the BeaconCoordinator will call resumeOperation when the Beacon reports back.
135
+ // ResumeEventName must exactly match an EventOutputs Name ('Complete') — the
136
+ // engine's downstream-event match is case-sensitive, so 'complete' silently
137
+ // strands every node wired after this one.
135
138
  return fCallback(null, {
136
139
  WaitingForInput: true,
137
- ResumeEventName: 'complete',
140
+ ResumeEventName: 'Complete',
138
141
  PromptMessage: `Waiting for Beacon worker (${tmpWorkItemInfo.Capability}/${tmpWorkItemInfo.Action})`,
139
142
  OutputAddress: '',
140
143
  Outputs: {},
@@ -211,7 +211,21 @@ module.exports =
211
211
  {
212
212
  if (pTask.fable.ExpressionParser)
213
213
  {
214
- tmpResult = pTask.fable.ExpressionParser.resolve(pResolvedSettings.Expression, pExecutionContext);
214
+ // Fable's ExpressionParser API is solve(expression, dataSource,
215
+ // results, manifest, destination); the data source mirrors the
216
+ // StateManager address roots. solve returns STRINGS — boolean
217
+ // comparisons come back as '1'/'0', and '0' is truthy in JS,
218
+ // so coerce explicitly before branching.
219
+ let tmpSolved = pTask.fable.ExpressionParser.solve(pResolvedSettings.Expression,
220
+ {
221
+ Operation: pExecutionContext.OperationState || {},
222
+ Global: pExecutionContext.GlobalState || {},
223
+ TaskOutput: pExecutionContext.TaskOutputs || {}
224
+ },
225
+ {}, pTask.fable.manifest, {});
226
+ tmpResult = !(tmpSolved === undefined || tmpSolved === null || tmpSolved === false
227
+ || tmpSolved === 0 || tmpSolved === '' || tmpSolved === '0'
228
+ || String(tmpSolved).toLowerCase() === 'false');
215
229
  }
216
230
  else
217
231
  {
@@ -36,7 +36,21 @@ class UltravisorTaskTypeIfConditional extends libTaskTypeBase
36
36
  {
37
37
  if (this.fable.ExpressionParser)
38
38
  {
39
- tmpResult = this.fable.ExpressionParser.resolve(pResolvedSettings.Expression, pExecutionContext);
39
+ // Fable's ExpressionParser API is solve(expression, dataSource,
40
+ // results, manifest, destination); the data source mirrors the
41
+ // StateManager address roots. solve returns STRINGS — boolean
42
+ // comparisons come back as '1'/'0', and '0' is truthy in JS,
43
+ // so coerce explicitly before branching.
44
+ let tmpSolved = this.fable.ExpressionParser.solve(pResolvedSettings.Expression,
45
+ {
46
+ Operation: pExecutionContext.OperationState || {},
47
+ Global: pExecutionContext.GlobalState || {},
48
+ TaskOutput: pExecutionContext.TaskOutputs || {}
49
+ },
50
+ {}, this.fable.manifest, {});
51
+ tmpResult = !(tmpSolved === undefined || tmpSolved === null || tmpSolved === false
52
+ || tmpSolved === 0 || tmpSolved === '' || tmpSolved === '0'
53
+ || String(tmpSolved).toLowerCase() === 'false');
40
54
  }
41
55
  else
42
56
  {
@@ -106,7 +106,7 @@ module.exports =
106
106
  if (tmpExistingValue !== undefined && tmpExistingValue !== null && tmpExistingValue !== '')
107
107
  {
108
108
  return fCallback(null, {
109
- EventToFire: 'complete',
109
+ EventToFire: 'ValueInputComplete',
110
110
  Outputs: { InputValue: tmpExistingValue },
111
111
  Log: [`Auto-resolved from pre-seeded state: "${tmpOutputAddress}" = "${String(tmpExistingValue).substring(0, 100)}"`]
112
112
  });
@@ -123,7 +123,7 @@ module.exports =
123
123
  if (tmpIsProgrammatic && tmpDefaultValue !== undefined && tmpDefaultValue !== null && tmpDefaultValue !== '')
124
124
  {
125
125
  return fCallback(null, {
126
- EventToFire: 'complete',
126
+ EventToFire: 'ValueInputComplete',
127
127
  Outputs: { InputValue: tmpDefaultValue },
128
128
  Log: [`Auto-resolved from default: "${tmpOutputAddress}" = "${String(tmpDefaultValue).substring(0, 100)}"`]
129
129
  });
@@ -135,7 +135,7 @@ module.exports =
135
135
  if (tmpIsProgrammatic && tmpIsOptional)
136
136
  {
137
137
  return fCallback(null, {
138
- EventToFire: 'complete',
138
+ EventToFire: 'ValueInputComplete',
139
139
  Outputs: { InputValue: '' },
140
140
  Log: [`Auto-resolved optional field: "${tmpOutputAddress}" = "" (no value provided)`]
141
141
  });
@@ -41,7 +41,7 @@ class UltravisorTaskTypeValueInput extends libTaskTypeBase
41
41
  if (tmpExistingValue !== undefined && tmpExistingValue !== null && tmpExistingValue !== '')
42
42
  {
43
43
  return fCallback(null, {
44
- EventToFire: 'complete',
44
+ EventToFire: 'ValueInputComplete',
45
45
  Outputs: { InputValue: tmpExistingValue },
46
46
  Log: [`Auto-resolved from pre-seeded state: "${tmpOutputAddress}" = "${String(tmpExistingValue).substring(0, 100)}"`]
47
47
  });
@@ -59,7 +59,7 @@ class UltravisorTaskTypeValueInput extends libTaskTypeBase
59
59
  if (tmpIsProgrammatic && tmpDefaultValue !== undefined && tmpDefaultValue !== null && tmpDefaultValue !== '')
60
60
  {
61
61
  return fCallback(null, {
62
- EventToFire: 'complete',
62
+ EventToFire: 'ValueInputComplete',
63
63
  Outputs: { InputValue: tmpDefaultValue },
64
64
  Log: [`Auto-resolved from default: "${tmpOutputAddress}" = "${String(tmpDefaultValue).substring(0, 100)}"`]
65
65
  });
@@ -71,7 +71,7 @@ class UltravisorTaskTypeValueInput extends libTaskTypeBase
71
71
  if (tmpIsProgrammatic && tmpIsOptional)
72
72
  {
73
73
  return fCallback(null, {
74
- EventToFire: 'complete',
74
+ EventToFire: 'ValueInputComplete',
75
75
  Outputs: { InputValue: '' },
76
76
  Log: [`Auto-resolved optional field: "${tmpOutputAddress}" = "" (no value provided)`]
77
77
  });