ultravisor 1.0.18 → 1.0.20

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.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Cyclic process execution with ai integration.",
5
5
  "main": "source/Ultravisor.cjs",
6
6
  "bin": {
@@ -34,7 +34,7 @@
34
34
  "pict": "^1.0.361",
35
35
  "pict-service-commandlineutility": "^1.0.19",
36
36
  "pict-serviceproviderbase": "^1.0.4",
37
- "ultravisor-beacon": "^0.0.9",
37
+ "ultravisor-beacon": "^0.0.10",
38
38
  "ws": "^8.20.0"
39
39
  },
40
40
  "devDependencies": {
@@ -116,6 +116,34 @@ if (_LogFilePath)
116
116
  console.log(`[Ultravisor] Logging to file: ${_LogFilePath}`);
117
117
  }
118
118
 
119
+ // Apply LogNoisiness from RETOLD_LOG_NOISINESS env var. Pict-style log
120
+ // noisiness is a 0-5 scale where 0 is silent (production default) and 5 shows
121
+ // everything. Diagnostic log statements throughout Ultravisor (especially the
122
+ // shared-fs reachability auto-detect path and the platform tasks) are gated
123
+ // with `if (this.fable.LogNoisiness >= N)` so they're free at level 0 and
124
+ // explosively detailed at level 4-5.
125
+ //
126
+ // Useful values:
127
+ // 1 — high-level decisions (auto-detected shared-fs peer X)
128
+ // 2 — entry points and decisions in shared-fs / dispatch paths
129
+ // 3 — per-candidate iteration in reachability
130
+ // 4 — per-mount comparison details
131
+ // 5 — everything
132
+ //
133
+ // In stack-mode the launcher inherits process.env into the child Ultravisor
134
+ // process automatically, so setting RETOLD_LOG_NOISINESS once on the host
135
+ // (or in the docker-compose `environment:` block) lights up both processes.
136
+ let _LogNoisiness = parseInt(process.env.RETOLD_LOG_NOISINESS, 10);
137
+ if (!isNaN(_LogNoisiness) && _LogNoisiness > 0)
138
+ {
139
+ _Ultravisor_Pict.LogNoisiness = _LogNoisiness;
140
+ if (_Ultravisor_Pict.fable && _Ultravisor_Pict.fable !== _Ultravisor_Pict)
141
+ {
142
+ _Ultravisor_Pict.fable.LogNoisiness = _LogNoisiness;
143
+ }
144
+ console.log(`[Ultravisor] LogNoisiness=${_LogNoisiness} (verbose diagnostics enabled).`);
145
+ }
146
+
119
147
  // If a config file override was passed via --config / -c, apply it on top of the gathered config
120
148
  if (_ConfigFileOverride)
121
149
  {
@@ -474,6 +474,135 @@ class UltravisorBeaconReachability extends libPictService
474
474
  }
475
475
  return null;
476
476
  }
477
+
478
+ /**
479
+ * Walk all online beacons looking for one that shares a filesystem with
480
+ * the given source beacon. Returns the first peer that:
481
+ * 1. is not the source beacon itself
482
+ * 2. has the same HostID as the source
483
+ * 3. has at least one MountID overlap with the source
484
+ *
485
+ * This is used by the resolve-address task to auto-detect when a shared-fs
486
+ * fast path is available even when the caller did not explicitly identify
487
+ * a "requesting beacon". The typical case: retold-remote dispatches a media
488
+ * operation, the file lives on retold-remote, and orator-conversion (which
489
+ * will eventually consume the file) runs in the same process / on the same
490
+ * host with the same content mount. The auto-detection finds orator-conversion
491
+ * as a peer of retold-remote and reports the shared-fs strategy so the
492
+ * file-transfer task can short-circuit.
493
+ *
494
+ * Diagnostic logging is gated by Fable.LogNoisiness (set via the
495
+ * RETOLD_LOG_NOISINESS env var, 0-5):
496
+ * >= 2: log entry, source beacon summary, final decision
497
+ * >= 3: per-candidate iteration with rejection reasons
498
+ * >= 4: per-mount comparison details
499
+ *
500
+ * @param {string} pSourceBeaconID
501
+ * @returns {object|null} { Peer, Mount } or null if no shared-fs peer exists
502
+ */
503
+ findSharedFsPeer(pSourceBeaconID)
504
+ {
505
+ let tmpNoisy = (this.fable && this.fable.LogNoisiness) || 0;
506
+
507
+ let tmpCoordinator = this._getService('UltravisorBeaconCoordinator');
508
+ if (!tmpCoordinator)
509
+ {
510
+ if (tmpNoisy >= 2)
511
+ {
512
+ this.log.info(`[Reachability] findSharedFsPeer(${pSourceBeaconID}): no UltravisorBeaconCoordinator service registered, returning null.`);
513
+ }
514
+ return null;
515
+ }
516
+
517
+ let tmpSource = tmpCoordinator.getBeacon(pSourceBeaconID);
518
+ if (!tmpSource)
519
+ {
520
+ if (tmpNoisy >= 2)
521
+ {
522
+ this.log.info(`[Reachability] findSharedFsPeer(${pSourceBeaconID}): source beacon not in coordinator registry, returning null.`);
523
+ }
524
+ return null;
525
+ }
526
+ if (!tmpSource.HostID)
527
+ {
528
+ if (tmpNoisy >= 2)
529
+ {
530
+ this.log.info(`[Reachability] findSharedFsPeer(${pSourceBeaconID}): source beacon has no HostID (legacy beacon), returning null.`);
531
+ }
532
+ return null;
533
+ }
534
+ if (!Array.isArray(tmpSource.SharedMounts) || tmpSource.SharedMounts.length === 0)
535
+ {
536
+ if (tmpNoisy >= 2)
537
+ {
538
+ this.log.info(`[Reachability] findSharedFsPeer(${pSourceBeaconID}): source beacon advertises no SharedMounts, returning null.`);
539
+ }
540
+ return null;
541
+ }
542
+
543
+ if (tmpNoisy >= 2)
544
+ {
545
+ this.log.info(`[Reachability] findSharedFsPeer(${pSourceBeaconID}): source HostID=${tmpSource.HostID}, ${tmpSource.SharedMounts.length} mount(s)=${JSON.stringify(tmpSource.SharedMounts.map((m) => m.MountID))}`);
546
+ }
547
+
548
+ let tmpAllBeacons = tmpCoordinator.listBeacons();
549
+ if (tmpNoisy >= 2)
550
+ {
551
+ this.log.info(`[Reachability] findSharedFsPeer(${pSourceBeaconID}): scanning ${tmpAllBeacons.length} registered beacon(s) for shared-fs peers...`);
552
+ }
553
+
554
+ for (let i = 0; i < tmpAllBeacons.length; i++)
555
+ {
556
+ let tmpPeer = tmpAllBeacons[i];
557
+ if (!tmpPeer || tmpPeer.BeaconID === pSourceBeaconID)
558
+ {
559
+ if (tmpNoisy >= 3)
560
+ {
561
+ this.log.info(`[Reachability] skip [${tmpPeer ? tmpPeer.BeaconID : '(null)'}]: ${tmpPeer ? 'self' : 'null entry'}`);
562
+ }
563
+ continue;
564
+ }
565
+ if (tmpPeer.Status && tmpPeer.Status !== 'Online')
566
+ {
567
+ if (tmpNoisy >= 3)
568
+ {
569
+ this.log.info(`[Reachability] skip [${tmpPeer.BeaconID}]: status=${tmpPeer.Status} (not Online)`);
570
+ }
571
+ continue;
572
+ }
573
+ if (!tmpPeer.HostID || tmpPeer.HostID !== tmpSource.HostID)
574
+ {
575
+ if (tmpNoisy >= 3)
576
+ {
577
+ this.log.info(`[Reachability] skip [${tmpPeer.BeaconID}]: HostID=${tmpPeer.HostID || '(none)'} != source ${tmpSource.HostID}`);
578
+ }
579
+ continue;
580
+ }
581
+ if (tmpNoisy >= 4)
582
+ {
583
+ this.log.info(`[Reachability] compare mounts for [${tmpPeer.BeaconID}]: source=${JSON.stringify(tmpSource.SharedMounts.map((m) => m.MountID))} vs peer=${JSON.stringify((tmpPeer.SharedMounts || []).map((m) => m.MountID))}`);
584
+ }
585
+ let tmpSharedMount = this._findSharedMount(tmpSource.SharedMounts, tmpPeer.SharedMounts);
586
+ if (tmpSharedMount)
587
+ {
588
+ if (tmpNoisy >= 2)
589
+ {
590
+ this.log.info(`[Reachability] findSharedFsPeer(${pSourceBeaconID}): MATCH peer=[${tmpPeer.BeaconID}] mount=[${tmpSharedMount.MountID}] root=${tmpSharedMount.Root}`);
591
+ }
592
+ return { Peer: tmpPeer, Mount: tmpSharedMount };
593
+ }
594
+ if (tmpNoisy >= 3)
595
+ {
596
+ this.log.info(`[Reachability] skip [${tmpPeer.BeaconID}]: same HostID but no overlapping MountID`);
597
+ }
598
+ }
599
+
600
+ if (tmpNoisy >= 2)
601
+ {
602
+ this.log.info(`[Reachability] findSharedFsPeer(${pSourceBeaconID}): no shared-fs peer found among ${tmpAllBeacons.length} beacon(s).`);
603
+ }
604
+ return null;
605
+ }
477
606
  }
478
607
 
479
608
  module.exports = UltravisorBeaconReachability;
@@ -25,6 +25,18 @@ function _getService(pTask, pTypeName)
25
25
  : null;
26
26
  }
27
27
 
28
+ /**
29
+ * Get the LogNoisiness level (0-5) from the Fable instance attached to a task.
30
+ * Used to gate verbose diagnostic logging in the platform tasks. The user
31
+ * controls this via the RETOLD_LOG_NOISINESS environment variable, which the
32
+ * stack launcher applies to both the retold-remote Fable and the Ultravisor
33
+ * Pict instance at startup.
34
+ */
35
+ function _getNoisiness(pTask)
36
+ {
37
+ return (pTask && pTask.fable && pTask.fable.LogNoisiness) || 0;
38
+ }
39
+
28
40
 
29
41
  module.exports =
30
42
  [
@@ -106,55 +118,115 @@ module.exports =
106
118
  return pDirectBaseURL.replace(/\/$/, '') + tmpContextPath + tmpEncodedPath;
107
119
  };
108
120
 
121
+ // Helper: compute the absolute path on the source beacon's filesystem
122
+ // for the resolved resource. Returns null if the source beacon does
123
+ // not have a BasePath registered for this context.
124
+ let _computeSharedFsLocalPath = function ()
125
+ {
126
+ let tmpSourceBeacon = tmpCoordinator.getBeacon(tmpResolved.BeaconID);
127
+ let tmpCtx = tmpSourceBeacon && tmpSourceBeacon.Contexts
128
+ ? tmpSourceBeacon.Contexts[tmpResolved.Context]
129
+ : null;
130
+ if (tmpCtx && tmpCtx.BasePath)
131
+ {
132
+ return libPath.join(tmpCtx.BasePath, tmpResolved.Path);
133
+ }
134
+ return null;
135
+ };
136
+
137
+ let tmpReachability = _getService(pTask, 'UltravisorBeaconReachability');
138
+ let tmpNoisy = _getNoisiness(pTask);
139
+
140
+ if (tmpNoisy >= 2)
141
+ {
142
+ pTask.log.info(`[ResolveAddress] entry: address=${tmpAddress} sourceBeacon=${tmpResolved.BeaconID} requestingBeacon=${pResolvedSettings.RequestingBeaconID || '(none)'} reachability=${tmpReachability ? 'present' : 'missing'}`);
143
+ }
144
+
109
145
  // Resolve transfer strategy when a requesting beacon is specified
110
146
  let tmpRequestingBeaconID = pResolvedSettings.RequestingBeaconID;
111
- if (tmpRequestingBeaconID)
147
+ if (tmpRequestingBeaconID && tmpReachability)
112
148
  {
113
- let tmpReachability = _getService(pTask, 'UltravisorBeaconReachability');
114
- if (tmpReachability)
149
+ if (tmpNoisy >= 2)
115
150
  {
116
- let tmpStrategyResult = tmpReachability.resolveStrategy(tmpResolved.BeaconID, tmpRequestingBeaconID);
117
- tmpOutputs.Strategy = tmpStrategyResult.Strategy;
151
+ pTask.log.info(`[ResolveAddress] explicit RequestingBeaconID=${tmpRequestingBeaconID} provided — calling resolveStrategy directly.`);
152
+ }
153
+ let tmpStrategyResult = tmpReachability.resolveStrategy(tmpResolved.BeaconID, tmpRequestingBeaconID);
154
+ tmpOutputs.Strategy = tmpStrategyResult.Strategy;
118
155
 
119
- if (tmpStrategyResult.Strategy === 'shared-fs')
156
+ if (tmpStrategyResult.Strategy === 'shared-fs')
157
+ {
158
+ // Both beacons see the same filesystem mount. Look up the
159
+ // source beacon's context BasePath and join it with the inner
160
+ // resource path to get an absolute path that's also valid on
161
+ // the requesting beacon (because they share the mount).
162
+ let tmpAbsPath = _computeSharedFsLocalPath();
163
+ if (tmpAbsPath)
120
164
  {
121
- // Both beacons see the same filesystem mount. Look up the
122
- // source beacon's context BasePath and join it with the inner
123
- // resource path to get an absolute path that's also valid on
124
- // the requesting beacon (because they share the mount).
125
- let tmpSourceBeacon = tmpCoordinator.getBeacon(tmpResolved.BeaconID);
126
- let tmpCtx = tmpSourceBeacon && tmpSourceBeacon.Contexts
127
- ? tmpSourceBeacon.Contexts[tmpResolved.Context]
128
- : null;
129
- if (tmpCtx && tmpCtx.BasePath)
130
- {
131
- let tmpAbsPath = libPath.join(tmpCtx.BasePath, tmpResolved.Path);
132
- tmpOutputs.LocalPath = tmpAbsPath;
133
- // URL is intentionally left as the original (relative) URL
134
- // — file-transfer will see LocalPath and short-circuit, so
135
- // the URL is never actually fetched.
136
- }
137
- else
138
- {
139
- // No BasePath available — fall back to direct so the
140
- // transfer still works via HTTP.
141
- pTask.log.warn(`Resolve Address: shared-fs strategy chosen but no BasePath available for context [${tmpResolved.Context}] on beacon [${tmpResolved.BeaconID}], falling back to direct.`);
142
- tmpOutputs.Strategy = 'direct';
143
- }
165
+ tmpOutputs.LocalPath = tmpAbsPath;
166
+ // URL is intentionally left as the original (relative) URL
167
+ // file-transfer will see LocalPath and short-circuit, so
168
+ // the URL is never actually fetched.
144
169
  }
145
- else if (tmpStrategyResult.Strategy === 'direct' && tmpStrategyResult.DirectURL)
170
+ else
171
+ {
172
+ // No BasePath available — fall back to direct so the
173
+ // transfer still works via HTTP.
174
+ pTask.log.warn(`Resolve Address: shared-fs strategy chosen but no BasePath available for context [${tmpResolved.Context}] on beacon [${tmpResolved.BeaconID}], falling back to direct.`);
175
+ tmpOutputs.Strategy = 'direct';
176
+ }
177
+ }
178
+ else if (tmpStrategyResult.Strategy === 'direct' && tmpStrategyResult.DirectURL)
179
+ {
180
+ tmpOutputs.DirectURL = _buildDirectURL(tmpStrategyResult.DirectURL);
181
+ tmpOutputs.URL = tmpOutputs.DirectURL;
182
+ }
183
+ else if (tmpStrategyResult.Strategy === 'proxy')
184
+ {
185
+ // Proxy URL: Ultravisor's own endpoint serves the file
186
+ tmpOutputs.ProxyURL = tmpResolved.URL;
187
+ tmpOutputs.URL = tmpResolved.URL;
188
+ }
189
+ // 'local' strategy: URL stays as the context BaseURL (same host)
190
+ }
191
+ else if (tmpReachability)
192
+ {
193
+ // Auto-detect a shared-fs peer when no RequestingBeaconID was passed.
194
+ // This is the common case for retold-remote: it dispatches a media
195
+ // operation, the file lives on the retold-remote beacon, and an
196
+ // orator-conversion beacon on the same host shares the mount. The
197
+ // auto-detection finds the orator-conversion peer and lets us
198
+ // short-circuit the file-transfer entirely.
199
+ if (tmpNoisy >= 2)
200
+ {
201
+ pTask.log.info(`[ResolveAddress] no RequestingBeaconID — entering auto-detect path for source ${tmpResolved.BeaconID}`);
202
+ }
203
+ let tmpPeerInfo = tmpReachability.findSharedFsPeer(tmpResolved.BeaconID);
204
+ if (tmpPeerInfo)
205
+ {
206
+ let tmpAbsPath = _computeSharedFsLocalPath();
207
+ if (tmpAbsPath)
146
208
  {
147
- tmpOutputs.DirectURL = _buildDirectURL(tmpStrategyResult.DirectURL);
148
- tmpOutputs.URL = tmpOutputs.DirectURL;
209
+ tmpOutputs.Strategy = 'shared-fs';
210
+ tmpOutputs.LocalPath = tmpAbsPath;
211
+ pTask.log.info(`Resolve Address: auto-detected shared-fs peer [${tmpPeerInfo.Peer.BeaconID}] for source [${tmpResolved.BeaconID}] via mount [${tmpPeerInfo.Mount.MountID}].`);
212
+ if (tmpNoisy >= 2)
213
+ {
214
+ pTask.log.info(`[ResolveAddress] auto-detect SUCCESS: LocalPath=${tmpAbsPath} (peer=${tmpPeerInfo.Peer.BeaconID}, mount=${tmpPeerInfo.Mount.MountID})`);
215
+ }
149
216
  }
150
- else if (tmpStrategyResult.Strategy === 'proxy')
217
+ else if (tmpNoisy >= 2)
151
218
  {
152
- // Proxy URL: Ultravisor's own endpoint serves the file
153
- tmpOutputs.ProxyURL = tmpResolved.URL;
154
- tmpOutputs.URL = tmpResolved.URL;
219
+ pTask.log.info(`[ResolveAddress] auto-detect found peer [${tmpPeerInfo.Peer.BeaconID}] but source beacon has no BasePath for context [${tmpResolved.Context}] — cannot use shared-fs.`);
155
220
  }
156
- // 'local' strategy: URL stays as the context BaseURL (same host)
157
221
  }
222
+ else if (tmpNoisy >= 2)
223
+ {
224
+ pTask.log.info(`[ResolveAddress] auto-detect found NO shared-fs peer for source ${tmpResolved.BeaconID} — falling through to default direct/proxy strategy.`);
225
+ }
226
+ }
227
+ else if (tmpNoisy >= 2)
228
+ {
229
+ pTask.log.info(`[ResolveAddress] reachability service not available — skipping auto-detect, default strategy will be used.`);
158
230
  }
159
231
 
160
232
  // If the URL is still relative (no protocol), use the beacon's first
@@ -218,6 +290,12 @@ module.exports =
218
290
  let tmpSourceURL = pResolvedSettings.SourceURL;
219
291
  let tmpSourceLocalPath = pResolvedSettings.SourceLocalPath;
220
292
  let tmpFilename = pResolvedSettings.Filename;
293
+ let tmpNoisy = _getNoisiness(pTask);
294
+
295
+ if (tmpNoisy >= 2)
296
+ {
297
+ pTask.log.info(`[FileTransfer] entry: SourceURL=${tmpSourceURL ? tmpSourceURL.substring(0, 80) : '(none)'} SourceLocalPath=${tmpSourceLocalPath || '(none)'} Filename=${tmpFilename || '(none)'}`);
298
+ }
221
299
 
222
300
  // Shared-filesystem fast path: if the source beacon and the requesting
223
301
  // beacon both report the same MountID, resolve-address will populate
@@ -259,6 +337,14 @@ module.exports =
259
337
  // Path was provided but doesn't exist on this beacon — log and fall
260
338
  // through to the HTTP path so we still satisfy the request.
261
339
  pTask.log.warn(`File Transfer: SourceLocalPath [${tmpSourceLocalPath}] does not exist on this beacon, falling back to HTTP transfer.`);
340
+ if (tmpNoisy >= 2)
341
+ {
342
+ pTask.log.info(`[FileTransfer] SourceLocalPath was set but file is missing on this beacon — the requesting beacon doesn't actually share this filesystem at the expected path.`);
343
+ }
344
+ }
345
+ else if (tmpNoisy >= 2)
346
+ {
347
+ pTask.log.info(`[FileTransfer] no SourceLocalPath in settings — running standard HTTP download path. (Either resolve-address chose 'direct'/'proxy', or the operation graph isn't wiring resolve.LocalPath → transfer.SourceLocalPath.)`);
262
348
  }
263
349
 
264
350
  if (!tmpSourceURL)
@@ -2055,6 +2055,11 @@ class UltravisorAPIServer extends libPictService
2055
2055
  return;
2056
2056
  }
2057
2057
 
2058
+ // IMPORTANT: this enumeration must include every field the coordinator
2059
+ // cares about, including HostID and SharedMounts (used by the shared-fs
2060
+ // reachability auto-detect). Forgetting to forward a field here means
2061
+ // the WebSocket-registered beacon record will have that field set to
2062
+ // null/empty in the coordinator, even though the client sent the value.
2058
2063
  let tmpBeacon = tmpCoordinator.registerBeacon({
2059
2064
  Name: pData.Name,
2060
2065
  Capabilities: pData.Capabilities,
@@ -2063,7 +2068,9 @@ class UltravisorAPIServer extends libPictService
2063
2068
  MaxConcurrent: pData.MaxConcurrent,
2064
2069
  Tags: pData.Tags,
2065
2070
  Contexts: pData.Contexts,
2066
- BindAddresses: pData.BindAddresses
2071
+ BindAddresses: pData.BindAddresses,
2072
+ HostID: pData.HostID,
2073
+ SharedMounts: pData.SharedMounts
2067
2074
  });
2068
2075
 
2069
2076
  pWebSocket._BeaconID = tmpBeacon.BeaconID;