ultravisor 1.0.24 → 1.0.26

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 (25) hide show
  1. package/docs/_sidebar.md +1 -0
  2. package/docs/features/persistence-via-databeacon.md +1211 -0
  3. package/package.json +6 -6
  4. package/source/cli/Ultravisor-CLIProgram.cjs +80 -0
  5. package/source/config/Ultravisor-Default-Command-Configuration.cjs +9 -1
  6. package/source/datamodel/Ultravisor-Fleet.json +66 -0
  7. package/source/persistence/UltravisorPersistenceSchema.json +240 -0
  8. package/source/services/Ultravisor-AuthBeaconBridge.cjs +271 -0
  9. package/source/services/Ultravisor-Beacon-Coordinator.cjs +339 -151
  10. package/source/services/Ultravisor-Beacon-Scheduler.cjs +65 -29
  11. package/source/services/Ultravisor-DirectoryDistributor.cjs +280 -0
  12. package/source/services/Ultravisor-ExecutionManifest.cjs +99 -4
  13. package/source/services/Ultravisor-FleetManager.cjs +871 -0
  14. package/source/services/Ultravisor-ManifestStoreBridge.cjs +1134 -0
  15. package/source/services/Ultravisor-QueuePersistenceBridge.cjs +1336 -0
  16. package/source/services/persistence/Ultravisor-Beacon-FleetStore.cjs +570 -0
  17. package/source/web_server/Ultravisor-API-Server.cjs +1185 -90
  18. package/test/fleetstore-smoke.js +152 -0
  19. package/webinterface/package.json +1 -0
  20. package/webinterface/source/Pict-Application-Ultravisor.js +59 -2
  21. package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +12 -0
  22. package/webinterface/source/views/PictView-Ultravisor-Fleet.js +489 -0
  23. package/webinterface/source/views/PictView-Ultravisor-Login.js +74 -0
  24. package/webinterface/source/views/PictView-Ultravisor-TopBar.js +26 -0
  25. package/webinterface/source/views/PictView-Ultravisor-UserManagement.js +159 -0
@@ -113,6 +113,62 @@ class UltravisorBeaconCoordinator extends libPictService
113
113
  return (tmpStore && tmpStore.isEnabled && tmpStore.isEnabled()) ? tmpStore : null;
114
114
  }
115
115
 
116
+ /**
117
+ * Get the queue persistence bridge (lazy). Falls back to direct
118
+ * QueueStore writes when the bridge isn't installed (older
119
+ * deployments, tests).
120
+ *
121
+ * @returns {object|null}
122
+ */
123
+ _getQueuePersistenceBridge()
124
+ {
125
+ let tmpMap = this.fable.servicesMap && this.fable.servicesMap.UltravisorQueuePersistenceBridge;
126
+ if (!tmpMap) return null;
127
+ return Object.values(tmpMap)[0] || null;
128
+ }
129
+
130
+ /**
131
+ * Capabilities the bridge skips persistence for. Their own dispatches
132
+ * are themselves work items; recording them would loop:
133
+ * QueuePersistence → QP_AppendEvent → enqueueWorkItem → QP_AppendEvent → ...
134
+ * ManifestStore → MS_UpsertManifest → enqueueWorkItem → MS_UpsertManifest → ...
135
+ * MeadowProxy → MeadowProxy.Request (the bridge's new dispatch
136
+ * backend) → enqueueWorkItem → MeadowProxy.Request → ...
137
+ * DataBeaconSchema → bootstrap-time EnsureSchema/IntrospectSchema; same loop hazard.
138
+ * DataBeaconManagement → bootstrap-time EnableEndpoint/UpdateProxyConfig; same loop hazard.
139
+ * The set lives on the coordinator because it's the only place that
140
+ * decides "is this work item worth persisting?".
141
+ */
142
+ _isMetaCapability(pCapability)
143
+ {
144
+ return pCapability === 'QueuePersistence'
145
+ || pCapability === 'ManifestStore'
146
+ || pCapability === 'MeadowProxy'
147
+ || pCapability === 'DataBeaconSchema'
148
+ || pCapability === 'DataBeaconManagement';
149
+ }
150
+
151
+ /**
152
+ * Centralized fire-and-forget for bridge calls. Promise rejections
153
+ * land here as warnings instead of unhandled-rejection events,
154
+ * matching the original `try { ... } catch (...)` best-effort
155
+ * shape of every legacy QueueStore call site.
156
+ */
157
+ _persistBest(pPromise, pLabel)
158
+ {
159
+ if (!pPromise || typeof pPromise.then !== 'function') return;
160
+ pPromise.then((pResult) =>
161
+ {
162
+ if (pResult && pResult.Success === false && pResult.Reason)
163
+ {
164
+ this.log.warn(`BeaconCoordinator: ${pLabel}: ${pResult.Reason}`);
165
+ }
166
+ }).catch((pErr) =>
167
+ {
168
+ this.log.warn(`BeaconCoordinator: ${pLabel} threw: ${(pErr && pErr.message) || pErr}`);
169
+ });
170
+ }
171
+
116
172
  /**
117
173
  * Get the scheduler service (lazy lookup).
118
174
  *
@@ -242,9 +298,19 @@ class UltravisorBeaconCoordinator extends libPictService
242
298
  {
243
299
  let tmpName = pBeaconInfo.Name || 'unnamed';
244
300
 
245
- // Check for an existing offline beacon with the same name to reclaim
301
+ // Check for an existing beacon with the same name to reclaim.
302
+ // Status='Offline' is the original reconnect-after-disconnect case.
303
+ // Status='Online' is the post-enable refreshRegistration case
304
+ // (SDK pushed updated capabilities without disconnecting first).
305
+ // Either way, same name → same beacon — update in place rather
306
+ // than creating a duplicate record (which would orphan the
307
+ // FleetStore installation rows keyed on the old BeaconID and
308
+ // confuse the dispatch filter).
246
309
  let tmpExistingBeacon = this.findBeaconByName(tmpName);
247
- if (tmpExistingBeacon && tmpExistingBeacon.Status === 'Offline')
310
+ if (tmpExistingBeacon
311
+ && (tmpExistingBeacon.Status === 'Offline'
312
+ || tmpExistingBeacon.Status === 'Online'
313
+ || tmpExistingBeacon.Status === 'Busy'))
248
314
  {
249
315
  tmpExistingBeacon.SessionID = pSessionID || null;
250
316
  tmpExistingBeacon.LastHeartbeat = new Date().toISOString();
@@ -303,6 +369,12 @@ class UltravisorBeaconCoordinator extends libPictService
303
369
  tmpReachability.onBeaconRegistered(tmpExistingBeacon.BeaconID);
304
370
  }
305
371
 
372
+ // Bootstrap-flush: a reconnect is exactly the case we care
373
+ // about — the beacon was offline, local writes accumulated,
374
+ // now the beacon is back. Fire the same notification path
375
+ // as a fresh register so the bridges sweep their backlog.
376
+ this._notifyPersistenceBridgesOnConnect(tmpExistingBeacon.BeaconID);
377
+
306
378
  return tmpExistingBeacon;
307
379
  }
308
380
 
@@ -352,9 +424,81 @@ class UltravisorBeaconCoordinator extends libPictService
352
424
  tmpReachability.onBeaconRegistered(tmpBeaconID);
353
425
  }
354
426
 
427
+ // Notify the fleet manager so it can:
428
+ // - auto-push any registered runtimes whose AutoPushOnConnect
429
+ // is true and whose hash on the worker is stale
430
+ // - auto-discover models the worker reports via LWM_Inventory
431
+ // and import them into the fleet table at Source='discovered',
432
+ // EnabledForDispatch=true (so existing dispatches keep working
433
+ // without operator intervention)
434
+ //
435
+ // Critical: defer to the next tick. The WebSocket upgrade handler
436
+ // (which sets `_WorkItemPushHandler`) runs in the SAME tick as
437
+ // `/Beacon/Register`. If we kick off LWM_Inventory or runtime
438
+ // pushes synchronously here, those work items get pre-assigned
439
+ // (via the fleet-push affinity) BUT the WS handler isn't wired
440
+ // yet, so they never deliver — they just fill the beacon's
441
+ // CurrentWorkItems slot and starve every subsequent dispatch.
442
+ // setImmediate buys us "after the WS upgrade settles" without
443
+ // adding a real timer.
444
+ let tmpFleet = this._getService('UltravisorFleetManager');
445
+ if (tmpFleet && typeof tmpFleet.onBeaconConnected === 'function')
446
+ {
447
+ setImmediate(() =>
448
+ {
449
+ try { tmpFleet.onBeaconConnected(tmpBeaconID); }
450
+ catch (pErr)
451
+ {
452
+ this.log.warn(
453
+ `BeaconCoordinator: FleetManager.onBeaconConnected threw: ${pErr.message}`);
454
+ }
455
+ });
456
+ }
457
+
458
+ // Bootstrap-flush: notify persistence bridges that a beacon
459
+ // just connected so they can flush locally-buffered writes the
460
+ // beacon doesn't have. Each bridge inspects whether the new
461
+ // beacon supplies its capability (QueuePersistence /
462
+ // ManifestStore) and fires a sweep when relevant. setImmediate
463
+ // for the same reason as the FleetManager hook above — the
464
+ // WebSocket upgrade hasn't fully wired the push channel yet.
465
+ this._notifyPersistenceBridgesOnConnect(tmpBeaconID);
466
+
355
467
  return tmpBeacon;
356
468
  }
357
469
 
470
+ /**
471
+ * Notify both persistence bridges that a beacon just connected.
472
+ * Each bridge filters by capability internally so this is safe to
473
+ * call for every beacon registration. Fire-and-forget; bridges
474
+ * log their own success/failure. Wrapped in setImmediate so flush
475
+ * dispatches don't preempt the WebSocket upgrade settlement.
476
+ */
477
+ _notifyPersistenceBridgesOnConnect(pBeaconID)
478
+ {
479
+ let tmpQB = this._getService('UltravisorQueuePersistenceBridge');
480
+ let tmpMB = this._getService('UltravisorManifestStoreBridge');
481
+ setImmediate(() =>
482
+ {
483
+ if (tmpQB && typeof tmpQB.onBeaconConnected === 'function')
484
+ {
485
+ try { tmpQB.onBeaconConnected(pBeaconID); }
486
+ catch (pErr)
487
+ {
488
+ this.log.warn(`BeaconCoordinator: QueuePersistenceBridge.onBeaconConnected threw: ${pErr.message}`);
489
+ }
490
+ }
491
+ if (tmpMB && typeof tmpMB.onBeaconConnected === 'function')
492
+ {
493
+ try { tmpMB.onBeaconConnected(pBeaconID); }
494
+ catch (pErr)
495
+ {
496
+ this.log.warn(`BeaconCoordinator: ManifestStoreBridge.onBeaconConnected threw: ${pErr.message}`);
497
+ }
498
+ }
499
+ });
500
+ }
501
+
358
502
  // ================================================================
359
503
  // Action Catalog
360
504
  // ================================================================
@@ -1071,31 +1215,27 @@ class UltravisorBeaconCoordinator extends libPictService
1071
1215
  }
1072
1216
  }
1073
1217
 
1074
- // Persist the full record to the SQLite-backed queue store so
1075
- // the /queue view + historical queries + cross-restart recovery
1076
- // have a canonical source of truth.
1077
- let tmpQueueStore = this._getQueueStore();
1078
- if (tmpQueueStore)
1218
+ // Persist via the bridge (beacon when connected, in-process
1219
+ // QueueStore otherwise). Skip QueuePersistence's own dispatches
1220
+ // recording them would create infinite recursion through the
1221
+ // bridge coordinator → bridge → ... loop.
1222
+ let tmpBridge = this._getQueuePersistenceBridge();
1223
+ if (tmpBridge && !this._isMetaCapability(tmpWorkItem.Capability))
1079
1224
  {
1080
- try { tmpQueueStore.upsertWorkItem(tmpWorkItem); }
1081
- catch (pStoreErr) { this.log.warn(`BeaconCoordinator: queue store upsert failed: ${pStoreErr.message}`); }
1082
- try
1083
- {
1084
- tmpQueueStore.appendEvent({
1085
- WorkItemHash: tmpWorkItem.WorkItemHash,
1086
- RunID: tmpWorkItem.RunID,
1087
- EventType: 'enqueued',
1088
- FromStatus: '',
1089
- ToStatus: tmpWorkItem.Status,
1090
- Payload: {
1091
- Capability: tmpWorkItem.Capability,
1092
- Action: tmpWorkItem.Action,
1093
- Priority: tmpWorkItem.Priority,
1094
- AffinityKey: tmpWorkItem.AffinityKey
1095
- }
1096
- });
1097
- }
1098
- catch (pStoreErr2) { /* best effort */ }
1225
+ this._persistBest(tmpBridge.upsertWorkItem(tmpWorkItem), 'queue upsert');
1226
+ this._persistBest(tmpBridge.appendEvent({
1227
+ WorkItemHash: tmpWorkItem.WorkItemHash,
1228
+ RunID: tmpWorkItem.RunID,
1229
+ EventType: 'enqueued',
1230
+ FromStatus: '',
1231
+ ToStatus: tmpWorkItem.Status,
1232
+ Payload: {
1233
+ Capability: tmpWorkItem.Capability,
1234
+ Action: tmpWorkItem.Action,
1235
+ Priority: tmpWorkItem.Priority,
1236
+ AffinityKey: tmpWorkItem.AffinityKey
1237
+ }
1238
+ }), 'queue enqueued event');
1099
1239
  }
1100
1240
 
1101
1241
  // Broadcast the enqueue event. Scheduler owns the ws envelope.
@@ -1171,6 +1311,31 @@ class UltravisorBeaconCoordinator extends libPictService
1171
1311
  continue;
1172
1312
  }
1173
1313
 
1314
+ // Fleet-manager dispatch filter (same logic as pollForWork's
1315
+ // pending-pass). System actions / unknown-model dispatches
1316
+ // fall through; per-(beacon, model) gating only applies when
1317
+ // the work item targets a known-installed model.
1318
+ let tmpFleetWS = this._getService('UltravisorFleetManager');
1319
+ if (tmpFleetWS && typeof tmpFleetWS.checkDispatchAllowed === 'function')
1320
+ {
1321
+ let tmpAllowWS;
1322
+ try { tmpAllowWS = tmpFleetWS.checkDispatchAllowed(tmpBeacon.BeaconID, pWorkItem); }
1323
+ catch (pErr)
1324
+ {
1325
+ this.log.warn(
1326
+ `BeaconCoordinator: WS-push fleet filter threw: ${pErr.message} — allowing`);
1327
+ tmpAllowWS = { Allowed: true };
1328
+ }
1329
+ if (tmpAllowWS && tmpAllowWS.Allowed === false)
1330
+ {
1331
+ this.log.info(
1332
+ `BeaconCoordinator: WS-push fleet filter denied beacon [${tmpBeacon.BeaconID}] `
1333
+ + `for [${pWorkItem.Capability}/${pWorkItem.Action}] `
1334
+ + `(model=${tmpAllowWS.MatchedModelKey}, reason=${tmpAllowWS.Reason}).`);
1335
+ continue;
1336
+ }
1337
+ }
1338
+
1174
1339
  // Assign the work item to this beacon
1175
1340
  pWorkItem.Status = 'Running';
1176
1341
  pWorkItem.AssignedBeaconID = tmpBeacon.BeaconID;
@@ -1282,37 +1447,35 @@ class UltravisorBeaconCoordinator extends libPictService
1282
1447
  if (!isNaN(tmpEnqMs)) tmpWorkItem.QueueWaitMs = Math.max(0, Date.now() - tmpEnqMs);
1283
1448
  }
1284
1449
  tmpWorkItem.AttemptNumber = (tmpWorkItem.AttemptNumber || 0) + 1;
1285
- let tmpPollStoreA = this._getQueueStore();
1286
- if (tmpPollStoreA)
1450
+ // Affinity-poll dispatch path. Bridge handles beacon-or-local
1451
+ // routing; meta-capability skip mirrors the other write sites.
1452
+ let tmpPollBridgeA = this._getQueuePersistenceBridge();
1453
+ if (tmpPollBridgeA && !this._isMetaCapability(tmpWorkItem.Capability))
1287
1454
  {
1288
- try
1289
- {
1290
- tmpPollStoreA.updateWorkItem(tmpWorkItem.WorkItemHash, {
1291
- Status: 'Running',
1292
- StartedAt: tmpWorkItem.StartedAt,
1293
- DispatchedAt: tmpWorkItem.DispatchedAt,
1294
- LastEventAt: tmpPollNowIso,
1295
- QueueWaitMs: tmpWorkItem.QueueWaitMs || 0,
1296
- AttemptNumber: tmpWorkItem.AttemptNumber
1297
- });
1298
- tmpPollStoreA.appendEvent({
1299
- WorkItemHash: tmpWorkItem.WorkItemHash,
1300
- RunID: tmpWorkItem.RunID,
1301
- EventType: 'dispatched',
1302
- FromStatus: 'Assigned',
1303
- ToStatus: 'Running',
1304
- BeaconID: pBeaconID,
1305
- Payload: { QueueWaitMs: tmpWorkItem.QueueWaitMs || 0, Path: 'poll' }
1306
- });
1307
- tmpPollStoreA.insertAttempt({
1308
- WorkItemHash: tmpWorkItem.WorkItemHash,
1309
- AttemptNumber: tmpWorkItem.AttemptNumber,
1310
- BeaconID: pBeaconID,
1311
- DispatchedAt: tmpWorkItem.DispatchedAt,
1312
- Outcome: 'Dispatched'
1313
- });
1314
- }
1315
- catch (pErr) { /* best effort */ }
1455
+ this._persistBest(tmpPollBridgeA.updateWorkItem(tmpWorkItem.WorkItemHash, {
1456
+ Status: 'Running',
1457
+ StartedAt: tmpWorkItem.StartedAt,
1458
+ DispatchedAt: tmpWorkItem.DispatchedAt,
1459
+ LastEventAt: tmpPollNowIso,
1460
+ QueueWaitMs: tmpWorkItem.QueueWaitMs || 0,
1461
+ AttemptNumber: tmpWorkItem.AttemptNumber
1462
+ }), 'queue dispatch update (affinity)');
1463
+ this._persistBest(tmpPollBridgeA.appendEvent({
1464
+ WorkItemHash: tmpWorkItem.WorkItemHash,
1465
+ RunID: tmpWorkItem.RunID,
1466
+ EventType: 'dispatched',
1467
+ FromStatus: 'Assigned',
1468
+ ToStatus: 'Running',
1469
+ BeaconID: pBeaconID,
1470
+ Payload: { QueueWaitMs: tmpWorkItem.QueueWaitMs || 0, Path: 'poll' }
1471
+ }), 'queue dispatched event (affinity)');
1472
+ this._persistBest(tmpPollBridgeA.insertAttempt({
1473
+ WorkItemHash: tmpWorkItem.WorkItemHash,
1474
+ AttemptNumber: tmpWorkItem.AttemptNumber,
1475
+ BeaconID: pBeaconID,
1476
+ DispatchedAt: tmpWorkItem.DispatchedAt,
1477
+ Outcome: 'Dispatched'
1478
+ }), 'queue insert attempt (affinity)');
1316
1479
  }
1317
1480
  this.log.info(`BeaconCoordinator: beacon [${pBeaconID}] picked up affinity-assigned work item [${tmpWorkItem.WorkItemHash}].`);
1318
1481
  return this._sanitizeWorkItemForBeacon(tmpWorkItem);
@@ -1341,6 +1504,35 @@ class UltravisorBeaconCoordinator extends libPictService
1341
1504
  continue;
1342
1505
  }
1343
1506
 
1507
+ // Fleet-manager dispatch filter: gate on per-(beacon, model)
1508
+ // EnabledForDispatch state when the work item targets a
1509
+ // known-installed model. System actions (LabsWorkerManagement,
1510
+ // VideoPipeline/VP_EncodeVideo, etc.) and dispatches with no
1511
+ // extractable model key fall through unchanged. See
1512
+ // FleetManager.checkDispatchAllowed() for policy.
1513
+ let tmpFleetForFilter = this._getService('UltravisorFleetManager');
1514
+ if (tmpFleetForFilter && typeof tmpFleetForFilter.checkDispatchAllowed === 'function')
1515
+ {
1516
+ let tmpAllow;
1517
+ try { tmpAllow = tmpFleetForFilter.checkDispatchAllowed(pBeaconID, tmpWorkItem); }
1518
+ catch (pErr)
1519
+ {
1520
+ this.log.warn(
1521
+ `BeaconCoordinator: fleet filter threw on `
1522
+ + `[${tmpWorkItem.WorkItemHash}]: ${pErr.message} — allowing`);
1523
+ tmpAllow = { Allowed: true };
1524
+ }
1525
+ if (tmpAllow && tmpAllow.Allowed === false)
1526
+ {
1527
+ this.log.info(
1528
+ `BeaconCoordinator: fleet filter denied beacon [${pBeaconID}] `
1529
+ + `for [${tmpWorkItem.Capability}/${tmpWorkItem.Action}] `
1530
+ + `(model=${tmpAllow.MatchedModelKey}, reason=${tmpAllow.Reason}) — `
1531
+ + `keeping work item pending for an enabled beacon.`);
1532
+ continue;
1533
+ }
1534
+ }
1535
+
1344
1536
  // Claim this work item
1345
1537
  let tmpPollClaimIso = new Date().toISOString();
1346
1538
  let tmpPollFromStatus = tmpWorkItem.Status;
@@ -1357,39 +1549,38 @@ class UltravisorBeaconCoordinator extends libPictService
1357
1549
  if (!isNaN(tmpEnqMs2)) tmpWorkItem.QueueWaitMs = Math.max(0, Date.now() - tmpEnqMs2);
1358
1550
  }
1359
1551
  tmpWorkItem.AttemptNumber = (tmpWorkItem.AttemptNumber || 0) + 1;
1360
- let tmpPollStoreB = this._getQueueStore();
1361
- if (tmpPollStoreB)
1362
- {
1363
- try
1364
- {
1365
- tmpPollStoreB.updateWorkItem(tmpWorkItem.WorkItemHash, {
1366
- Status: 'Running',
1367
- AssignedBeaconID: pBeaconID,
1368
- AssignedAt: tmpWorkItem.AssignedAt,
1369
- DispatchedAt: tmpWorkItem.DispatchedAt,
1370
- StartedAt: tmpWorkItem.StartedAt,
1371
- LastEventAt: tmpPollClaimIso,
1372
- QueueWaitMs: tmpWorkItem.QueueWaitMs || 0,
1373
- AttemptNumber: tmpWorkItem.AttemptNumber
1374
- });
1375
- tmpPollStoreB.appendEvent({
1376
- WorkItemHash: tmpWorkItem.WorkItemHash,
1377
- RunID: tmpWorkItem.RunID,
1378
- EventType: 'dispatched',
1379
- FromStatus: tmpPollFromStatus,
1380
- ToStatus: 'Running',
1381
- BeaconID: pBeaconID,
1382
- Payload: { QueueWaitMs: tmpWorkItem.QueueWaitMs || 0, Path: 'poll' }
1383
- });
1384
- tmpPollStoreB.insertAttempt({
1385
- WorkItemHash: tmpWorkItem.WorkItemHash,
1386
- AttemptNumber: tmpWorkItem.AttemptNumber,
1387
- BeaconID: pBeaconID,
1388
- DispatchedAt: tmpWorkItem.DispatchedAt,
1389
- Outcome: 'Dispatched'
1390
- });
1391
- }
1392
- catch (pErr) { /* best effort */ }
1552
+ // Free-pool poll dispatch path. Same bridge + meta-capability
1553
+ // guard as the affinity poll above; the only difference is
1554
+ // the FromStatus carried into the event row.
1555
+ let tmpPollBridgeB = this._getQueuePersistenceBridge();
1556
+ if (tmpPollBridgeB && !this._isMetaCapability(tmpWorkItem.Capability))
1557
+ {
1558
+ this._persistBest(tmpPollBridgeB.updateWorkItem(tmpWorkItem.WorkItemHash, {
1559
+ Status: 'Running',
1560
+ AssignedBeaconID: pBeaconID,
1561
+ AssignedAt: tmpWorkItem.AssignedAt,
1562
+ DispatchedAt: tmpWorkItem.DispatchedAt,
1563
+ StartedAt: tmpWorkItem.StartedAt,
1564
+ LastEventAt: tmpPollClaimIso,
1565
+ QueueWaitMs: tmpWorkItem.QueueWaitMs || 0,
1566
+ AttemptNumber: tmpWorkItem.AttemptNumber
1567
+ }), 'queue dispatch update (poll)');
1568
+ this._persistBest(tmpPollBridgeB.appendEvent({
1569
+ WorkItemHash: tmpWorkItem.WorkItemHash,
1570
+ RunID: tmpWorkItem.RunID,
1571
+ EventType: 'dispatched',
1572
+ FromStatus: tmpPollFromStatus,
1573
+ ToStatus: 'Running',
1574
+ BeaconID: pBeaconID,
1575
+ Payload: { QueueWaitMs: tmpWorkItem.QueueWaitMs || 0, Path: 'poll' }
1576
+ }), 'queue dispatched event (poll)');
1577
+ this._persistBest(tmpPollBridgeB.insertAttempt({
1578
+ WorkItemHash: tmpWorkItem.WorkItemHash,
1579
+ AttemptNumber: tmpWorkItem.AttemptNumber,
1580
+ BeaconID: pBeaconID,
1581
+ DispatchedAt: tmpWorkItem.DispatchedAt,
1582
+ Outcome: 'Dispatched'
1583
+ }), 'queue insert attempt (poll)');
1393
1584
  }
1394
1585
 
1395
1586
  if (tmpBeacon.CurrentWorkItems.indexOf(tmpWorkItem.WorkItemHash) === -1)
@@ -1580,34 +1771,33 @@ class UltravisorBeaconCoordinator extends libPictService
1580
1771
  tmpJournal.appendEntry('complete', { WorkItemHash: pWorkItemHash });
1581
1772
  }
1582
1773
 
1583
- // Persist to the new queue store + append event + update attempt.
1584
- let tmpQueueStore = this._getQueueStore();
1585
- if (tmpQueueStore)
1586
- {
1587
- try
1588
- {
1589
- tmpQueueStore.updateWorkItem(pWorkItemHash, {
1590
- Status: 'Complete',
1591
- CompletedAt: tmpWorkItem.CompletedAt,
1592
- LastEventAt: tmpWorkItem.LastEventAt,
1593
- Result: pResult || null
1594
- });
1595
- tmpQueueStore.appendEvent({
1596
- WorkItemHash: pWorkItemHash,
1597
- RunID: tmpWorkItem.RunID,
1598
- EventType: 'completed',
1599
- FromStatus: tmpFromStatus,
1600
- ToStatus: 'Complete',
1601
- BeaconID: tmpWorkItem.AssignedBeaconID,
1602
- Payload: { DurationMs: tmpDurationMs }
1603
- });
1604
- tmpQueueStore.updateAttemptOutcome(pWorkItemHash, tmpWorkItem.AttemptNumber || 1, {
1774
+ // Persist via the bridge beacon-backed when connected,
1775
+ // in-process QueueStore otherwise. Skip meta-capability
1776
+ // dispatches to avoid the QP_*-on-QP_* recursion.
1777
+ let tmpCompleteBridge = this._getQueuePersistenceBridge();
1778
+ if (tmpCompleteBridge && !this._isMetaCapability(tmpWorkItem.Capability))
1779
+ {
1780
+ this._persistBest(tmpCompleteBridge.updateWorkItem(pWorkItemHash, {
1781
+ Status: 'Complete',
1782
+ CompletedAt: tmpWorkItem.CompletedAt,
1783
+ LastEventAt: tmpWorkItem.LastEventAt,
1784
+ Result: pResult || null
1785
+ }), 'queue complete update');
1786
+ this._persistBest(tmpCompleteBridge.appendEvent({
1787
+ WorkItemHash: pWorkItemHash,
1788
+ RunID: tmpWorkItem.RunID,
1789
+ EventType: 'completed',
1790
+ FromStatus: tmpFromStatus,
1791
+ ToStatus: 'Complete',
1792
+ BeaconID: tmpWorkItem.AssignedBeaconID,
1793
+ Payload: { DurationMs: tmpDurationMs }
1794
+ }), 'queue completed event');
1795
+ this._persistBest(tmpCompleteBridge.updateAttemptOutcome(
1796
+ pWorkItemHash, tmpWorkItem.AttemptNumber || 1, {
1605
1797
  CompletedAt: tmpWorkItem.CompletedAt,
1606
1798
  Outcome: 'Complete',
1607
1799
  DurationMs: tmpDurationMs
1608
- });
1609
- }
1610
- catch (pStoreErr) { /* best effort */ }
1800
+ }), 'queue attempt outcome');
1611
1801
  }
1612
1802
 
1613
1803
  let tmpScheduler = this._getScheduler();
@@ -1777,36 +1967,35 @@ class UltravisorBeaconCoordinator extends libPictService
1777
1967
  tmpJournal.appendEntry('fail', { WorkItemHash: pWorkItemHash });
1778
1968
  }
1779
1969
 
1780
- // Persist to the new queue store + event + attempt outcome.
1781
- let tmpFailStore = this._getQueueStore();
1782
- if (tmpFailStore)
1783
- {
1784
- try
1785
- {
1786
- tmpFailStore.updateWorkItem(pWorkItemHash, {
1787
- Status: 'Error',
1788
- CompletedAt: tmpWorkItem.CompletedAt,
1789
- LastEventAt: tmpWorkItem.LastEventAt,
1790
- LastError: tmpWorkItem.LastError,
1791
- Result: tmpWorkItem.Result
1792
- });
1793
- tmpFailStore.appendEvent({
1794
- WorkItemHash: pWorkItemHash,
1795
- RunID: tmpWorkItem.RunID,
1796
- EventType: 'failed',
1797
- FromStatus: tmpFromStatus,
1798
- ToStatus: 'Error',
1799
- BeaconID: tmpWorkItem.AssignedBeaconID,
1800
- Payload: { Error: tmpWorkItem.LastError, DurationMs: tmpFailDurationMs }
1801
- });
1802
- tmpFailStore.updateAttemptOutcome(pWorkItemHash, tmpWorkItem.AttemptNumber || 1, {
1970
+ // Persist via the bridge beacon-backed when connected,
1971
+ // in-process QueueStore otherwise. Skip meta-capability
1972
+ // dispatches to avoid recursion.
1973
+ let tmpFailBridge = this._getQueuePersistenceBridge();
1974
+ if (tmpFailBridge && !this._isMetaCapability(tmpWorkItem.Capability))
1975
+ {
1976
+ this._persistBest(tmpFailBridge.updateWorkItem(pWorkItemHash, {
1977
+ Status: 'Error',
1978
+ CompletedAt: tmpWorkItem.CompletedAt,
1979
+ LastEventAt: tmpWorkItem.LastEventAt,
1980
+ LastError: tmpWorkItem.LastError,
1981
+ Result: tmpWorkItem.Result
1982
+ }), 'queue fail update');
1983
+ this._persistBest(tmpFailBridge.appendEvent({
1984
+ WorkItemHash: pWorkItemHash,
1985
+ RunID: tmpWorkItem.RunID,
1986
+ EventType: 'failed',
1987
+ FromStatus: tmpFromStatus,
1988
+ ToStatus: 'Error',
1989
+ BeaconID: tmpWorkItem.AssignedBeaconID,
1990
+ Payload: { Error: tmpWorkItem.LastError, DurationMs: tmpFailDurationMs }
1991
+ }), 'queue failed event');
1992
+ this._persistBest(tmpFailBridge.updateAttemptOutcome(
1993
+ pWorkItemHash, tmpWorkItem.AttemptNumber || 1, {
1803
1994
  CompletedAt: tmpWorkItem.CompletedAt,
1804
1995
  Outcome: 'Error',
1805
1996
  ErrorMessage: tmpWorkItem.LastError,
1806
1997
  DurationMs: tmpFailDurationMs
1807
- });
1808
- }
1809
- catch (pStoreErr) { /* best effort */ }
1998
+ }), 'queue fail attempt outcome');
1810
1999
  }
1811
2000
 
1812
2001
  let tmpFailScheduler = this._getScheduler();
@@ -2168,17 +2357,16 @@ class UltravisorBeaconCoordinator extends libPictService
2168
2357
  // Mark the item as freshly heard-from for health scoring.
2169
2358
  tmpWorkItem.LastEventAt = tmpWorkItem.Progress.UpdatedAt;
2170
2359
  if (!tmpWorkItem.StartedAt) tmpWorkItem.StartedAt = tmpWorkItem.Progress.UpdatedAt;
2171
- let tmpProgressStore = this._getQueueStore();
2172
- if (tmpProgressStore)
2173
- {
2174
- try
2175
- {
2176
- tmpProgressStore.updateWorkItem(pWorkItemHash, {
2177
- LastEventAt: tmpWorkItem.LastEventAt,
2178
- StartedAt: tmpWorkItem.StartedAt
2179
- });
2180
- }
2181
- catch (pErr) { /* best effort */ }
2360
+ // Progress updates also land via the bridge so a beacon-backed
2361
+ // persistence backend sees the same heartbeat trail an in-process
2362
+ // store would. Skip meta-capabilities to avoid recursion.
2363
+ let tmpProgressBridge = this._getQueuePersistenceBridge();
2364
+ if (tmpProgressBridge && !this._isMetaCapability(tmpWorkItem.Capability))
2365
+ {
2366
+ this._persistBest(tmpProgressBridge.updateWorkItem(pWorkItemHash, {
2367
+ LastEventAt: tmpWorkItem.LastEventAt,
2368
+ StartedAt: tmpWorkItem.StartedAt
2369
+ }), 'queue progress update');
2182
2370
  }
2183
2371
 
2184
2372
  // Accumulate log entries