ultravisor 1.0.24 → 1.0.25

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.24",
3
+ "version": "1.0.25",
4
4
  "description": "Cyclic process execution with ai integration.",
5
5
  "main": "source/Ultravisor.cjs",
6
6
  "bin": {
@@ -18,6 +18,9 @@ const libServiceExecutionManifest = require('../services/Ultravisor-ExecutionMan
18
18
  const libServiceBeaconCoordinator = require('../services/Ultravisor-Beacon-Coordinator.cjs');
19
19
  const libServiceBeaconReachability = require('../services/Ultravisor-Beacon-Reachability.cjs');
20
20
  const libServiceBeaconQueueJournal = require('../services/persistence/Ultravisor-Beacon-QueueJournal.cjs');
21
+ const libServiceBeaconFleetStore = require('../services/persistence/Ultravisor-Beacon-FleetStore.cjs');
22
+ const libServiceDirectoryDistributor = require('../services/Ultravisor-DirectoryDistributor.cjs');
23
+ const libServiceFleetManager = require('../services/Ultravisor-FleetManager.cjs');
21
24
 
22
25
  // TODO: Remove this when Restify is fixed.
23
26
  process.removeAllListeners('warning')
@@ -215,6 +218,21 @@ if (tmpCoordinator)
215
218
  tmpCoordinator.loadActionCatalog();
216
219
  }
217
220
 
221
+ // --- Fleet management (per-(beacon, model) install/enable state) ---
222
+ _Ultravisor_Pict.fable.addAndInstantiateServiceTypeIfNotExists(
223
+ 'UltravisorBeaconFleetStore', libServiceBeaconFleetStore);
224
+ let tmpFleetStore = Object.values(_Ultravisor_Pict.fable.servicesMap['UltravisorBeaconFleetStore'])[0];
225
+ if (tmpFleetStore)
226
+ {
227
+ tmpFleetStore.initialize(_Ultravisor_Pict.fable.settings.UltravisorFileStorePath);
228
+ }
229
+
230
+ _Ultravisor_Pict.fable.addAndInstantiateServiceTypeIfNotExists(
231
+ 'UltravisorDirectoryDistributor', libServiceDirectoryDistributor);
232
+
233
+ _Ultravisor_Pict.fable.addAndInstantiateServiceTypeIfNotExists(
234
+ 'UltravisorFleetManager', libServiceFleetManager);
235
+
218
236
  _Ultravisor_Pict.fable.addAndInstantiateServiceTypeIfNotExists('UltravisorAPIServer', libWebServerAPIServer);
219
237
 
220
238
  // ── Service name aliases ────────────────────────────────
@@ -0,0 +1,66 @@
1
+ {
2
+ "Tables":
3
+ {
4
+ "BeaconModelInstallation":
5
+ {
6
+ "TableName": "BeaconModelInstallation",
7
+ "Domain": "Default",
8
+ "Description": "Per-(beacon, model) installation state. Authoritative source of truth for which beacons may be dispatched a given model. Status tracks install lifecycle; EnabledForDispatch is a separate operator toggle.",
9
+ "Columns":
10
+ [
11
+ { "Column": "IDBeaconModelInstallation", "DataType": "ID" },
12
+ { "Column": "GUIDBeaconModelInstallation", "DataType": "GUID", "Size": "36" },
13
+ { "Column": "CreateDate", "DataType": "DateTime" },
14
+ { "Column": "CreatingIDUser", "DataType": "Numeric", "Size": "int" },
15
+ { "Column": "UpdateDate", "DataType": "DateTime" },
16
+ { "Column": "UpdatingIDUser", "DataType": "Numeric", "Size": "int" },
17
+ { "Column": "Deleted", "DataType": "Boolean" },
18
+ { "Column": "DeleteDate", "DataType": "DateTime" },
19
+ { "Column": "DeletingIDUser", "DataType": "Numeric", "Size": "int" },
20
+ { "Column": "BeaconID", "DataType": "String", "Size": "128" },
21
+ { "Column": "BeaconName", "DataType": "String", "Size": "256" },
22
+ { "Column": "ModelKey", "DataType": "String", "Size": "256" },
23
+ { "Column": "ModelName", "DataType": "String", "Size": "256" },
24
+ { "Column": "ModelSourceDir", "DataType": "String", "Size": "1024" },
25
+ { "Column": "ExpectedTreeHash", "DataType": "String", "Size": "128" },
26
+ { "Column": "InstalledTreeHash", "DataType": "String", "Size": "128" },
27
+ { "Column": "InstalledBytes", "DataType": "Numeric", "Size": "bigint" },
28
+ { "Column": "Status", "DataType": "String", "Size": "32" },
29
+ { "Column": "EnabledForDispatch", "DataType": "Boolean" },
30
+ { "Column": "PushProgressBytes", "DataType": "Numeric", "Size": "bigint" },
31
+ { "Column": "PushTotalBytes", "DataType": "Numeric", "Size": "bigint" },
32
+ { "Column": "LastError", "DataType": "String", "Size": "4096" },
33
+ { "Column": "InstalledAt", "DataType": "DateTime" },
34
+ { "Column": "LastUpdatedAt", "DataType": "DateTime" },
35
+ { "Column": "Source", "DataType": "String", "Size": "32" }
36
+ ]
37
+ },
38
+ "BeaconRuntimeInstallation":
39
+ {
40
+ "TableName": "BeaconRuntimeInstallation",
41
+ "Domain": "Default",
42
+ "Description": "Per-(beacon, runtime) state. A runtime is a named directory of pipeline-worker code (e.g. retold-labs' examples/pipeline-workers). Auto-pushed on beacon connect. Tracks last-known runtime hash so the hub can short-circuit pushes when the worker is already up to date.",
43
+ "Columns":
44
+ [
45
+ { "Column": "IDBeaconRuntimeInstallation", "DataType": "ID" },
46
+ { "Column": "GUIDBeaconRuntimeInstallation", "DataType": "GUID", "Size": "36" },
47
+ { "Column": "CreateDate", "DataType": "DateTime" },
48
+ { "Column": "CreatingIDUser", "DataType": "Numeric", "Size": "int" },
49
+ { "Column": "UpdateDate", "DataType": "DateTime" },
50
+ { "Column": "UpdatingIDUser", "DataType": "Numeric", "Size": "int" },
51
+ { "Column": "Deleted", "DataType": "Boolean" },
52
+ { "Column": "DeleteDate", "DataType": "DateTime" },
53
+ { "Column": "DeletingIDUser", "DataType": "Numeric", "Size": "int" },
54
+ { "Column": "BeaconID", "DataType": "String", "Size": "128" },
55
+ { "Column": "BeaconName", "DataType": "String", "Size": "256" },
56
+ { "Column": "RuntimeName", "DataType": "String", "Size": "128" },
57
+ { "Column": "ExpectedRuntimeHash", "DataType": "String", "Size": "128" },
58
+ { "Column": "InstalledRuntimeHash", "DataType": "String", "Size": "128" },
59
+ { "Column": "Status", "DataType": "String", "Size": "32" },
60
+ { "Column": "LastError", "DataType": "String", "Size": "4096" },
61
+ { "Column": "InstalledAt", "DataType": "DateTime" },
62
+ { "Column": "LastUpdatedAt", "DataType": "DateTime" }
63
+ ]
64
+ }
65
+ }
66
+ }
@@ -242,9 +242,19 @@ class UltravisorBeaconCoordinator extends libPictService
242
242
  {
243
243
  let tmpName = pBeaconInfo.Name || 'unnamed';
244
244
 
245
- // Check for an existing offline beacon with the same name to reclaim
245
+ // Check for an existing beacon with the same name to reclaim.
246
+ // Status='Offline' is the original reconnect-after-disconnect case.
247
+ // Status='Online' is the post-enable refreshRegistration case
248
+ // (SDK pushed updated capabilities without disconnecting first).
249
+ // Either way, same name → same beacon — update in place rather
250
+ // than creating a duplicate record (which would orphan the
251
+ // FleetStore installation rows keyed on the old BeaconID and
252
+ // confuse the dispatch filter).
246
253
  let tmpExistingBeacon = this.findBeaconByName(tmpName);
247
- if (tmpExistingBeacon && tmpExistingBeacon.Status === 'Offline')
254
+ if (tmpExistingBeacon
255
+ && (tmpExistingBeacon.Status === 'Offline'
256
+ || tmpExistingBeacon.Status === 'Online'
257
+ || tmpExistingBeacon.Status === 'Busy'))
248
258
  {
249
259
  tmpExistingBeacon.SessionID = pSessionID || null;
250
260
  tmpExistingBeacon.LastHeartbeat = new Date().toISOString();
@@ -352,6 +362,37 @@ class UltravisorBeaconCoordinator extends libPictService
352
362
  tmpReachability.onBeaconRegistered(tmpBeaconID);
353
363
  }
354
364
 
365
+ // Notify the fleet manager so it can:
366
+ // - auto-push any registered runtimes whose AutoPushOnConnect
367
+ // is true and whose hash on the worker is stale
368
+ // - auto-discover models the worker reports via LWM_Inventory
369
+ // and import them into the fleet table at Source='discovered',
370
+ // EnabledForDispatch=true (so existing dispatches keep working
371
+ // without operator intervention)
372
+ //
373
+ // Critical: defer to the next tick. The WebSocket upgrade handler
374
+ // (which sets `_WorkItemPushHandler`) runs in the SAME tick as
375
+ // `/Beacon/Register`. If we kick off LWM_Inventory or runtime
376
+ // pushes synchronously here, those work items get pre-assigned
377
+ // (via the fleet-push affinity) BUT the WS handler isn't wired
378
+ // yet, so they never deliver — they just fill the beacon's
379
+ // CurrentWorkItems slot and starve every subsequent dispatch.
380
+ // setImmediate buys us "after the WS upgrade settles" without
381
+ // adding a real timer.
382
+ let tmpFleet = this._getService('UltravisorFleetManager');
383
+ if (tmpFleet && typeof tmpFleet.onBeaconConnected === 'function')
384
+ {
385
+ setImmediate(() =>
386
+ {
387
+ try { tmpFleet.onBeaconConnected(tmpBeaconID); }
388
+ catch (pErr)
389
+ {
390
+ this.log.warn(
391
+ `BeaconCoordinator: FleetManager.onBeaconConnected threw: ${pErr.message}`);
392
+ }
393
+ });
394
+ }
395
+
355
396
  return tmpBeacon;
356
397
  }
357
398
 
@@ -1171,6 +1212,31 @@ class UltravisorBeaconCoordinator extends libPictService
1171
1212
  continue;
1172
1213
  }
1173
1214
 
1215
+ // Fleet-manager dispatch filter (same logic as pollForWork's
1216
+ // pending-pass). System actions / unknown-model dispatches
1217
+ // fall through; per-(beacon, model) gating only applies when
1218
+ // the work item targets a known-installed model.
1219
+ let tmpFleetWS = this._getService('UltravisorFleetManager');
1220
+ if (tmpFleetWS && typeof tmpFleetWS.checkDispatchAllowed === 'function')
1221
+ {
1222
+ let tmpAllowWS;
1223
+ try { tmpAllowWS = tmpFleetWS.checkDispatchAllowed(tmpBeacon.BeaconID, pWorkItem); }
1224
+ catch (pErr)
1225
+ {
1226
+ this.log.warn(
1227
+ `BeaconCoordinator: WS-push fleet filter threw: ${pErr.message} — allowing`);
1228
+ tmpAllowWS = { Allowed: true };
1229
+ }
1230
+ if (tmpAllowWS && tmpAllowWS.Allowed === false)
1231
+ {
1232
+ this.log.info(
1233
+ `BeaconCoordinator: WS-push fleet filter denied beacon [${tmpBeacon.BeaconID}] `
1234
+ + `for [${pWorkItem.Capability}/${pWorkItem.Action}] `
1235
+ + `(model=${tmpAllowWS.MatchedModelKey}, reason=${tmpAllowWS.Reason}).`);
1236
+ continue;
1237
+ }
1238
+ }
1239
+
1174
1240
  // Assign the work item to this beacon
1175
1241
  pWorkItem.Status = 'Running';
1176
1242
  pWorkItem.AssignedBeaconID = tmpBeacon.BeaconID;
@@ -1341,6 +1407,35 @@ class UltravisorBeaconCoordinator extends libPictService
1341
1407
  continue;
1342
1408
  }
1343
1409
 
1410
+ // Fleet-manager dispatch filter: gate on per-(beacon, model)
1411
+ // EnabledForDispatch state when the work item targets a
1412
+ // known-installed model. System actions (LabsWorkerManagement,
1413
+ // VideoPipeline/VP_EncodeVideo, etc.) and dispatches with no
1414
+ // extractable model key fall through unchanged. See
1415
+ // FleetManager.checkDispatchAllowed() for policy.
1416
+ let tmpFleetForFilter = this._getService('UltravisorFleetManager');
1417
+ if (tmpFleetForFilter && typeof tmpFleetForFilter.checkDispatchAllowed === 'function')
1418
+ {
1419
+ let tmpAllow;
1420
+ try { tmpAllow = tmpFleetForFilter.checkDispatchAllowed(pBeaconID, tmpWorkItem); }
1421
+ catch (pErr)
1422
+ {
1423
+ this.log.warn(
1424
+ `BeaconCoordinator: fleet filter threw on `
1425
+ + `[${tmpWorkItem.WorkItemHash}]: ${pErr.message} — allowing`);
1426
+ tmpAllow = { Allowed: true };
1427
+ }
1428
+ if (tmpAllow && tmpAllow.Allowed === false)
1429
+ {
1430
+ this.log.info(
1431
+ `BeaconCoordinator: fleet filter denied beacon [${pBeaconID}] `
1432
+ + `for [${tmpWorkItem.Capability}/${tmpWorkItem.Action}] `
1433
+ + `(model=${tmpAllow.MatchedModelKey}, reason=${tmpAllow.Reason}) — `
1434
+ + `keeping work item pending for an enabled beacon.`);
1435
+ continue;
1436
+ }
1437
+ }
1438
+
1344
1439
  // Claim this work item
1345
1440
  let tmpPollClaimIso = new Date().toISOString();
1346
1441
  let tmpPollFromStatus = tmpWorkItem.Status;
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Ultravisor — DirectoryDistributor
3
+ *
4
+ * Generic mesh primitive for "push the contents of a directory to a
5
+ * remote beacon, validate end-to-end with sha256 tree-hashing, and
6
+ * finalize." Originally cribbed from retold-labs/source/RetoldLabs-PythonRuntime.cjs
7
+ * (which was app-specific to pushing pipeline-workers/) and
8
+ * generalized so both runtime push (Layer 1b) and model push (Layer 2)
9
+ * — and any future "ship a directory tree to a worker" use case — can
10
+ * use the same code path.
11
+ *
12
+ * What it does:
13
+ * 1. Scans a source directory (skipping ignored basenames), computes
14
+ * a sha256 tree-hash + per-file chunk plan.
15
+ * 2. Streams each file as a sequence of chunks via a caller-supplied
16
+ * `pDispatch(actionName, settingsObject)` async function. The
17
+ * dispatcher is the only thing this service knows about beacons
18
+ * (we don't reach into the BeaconCoordinator directly — that's
19
+ * the FleetManager's job).
20
+ * 3. Calls a finalizer LWM action with the expected tree-hash.
21
+ *
22
+ * What it doesn't do:
23
+ * - Pick which beacon to push to (FleetManager).
24
+ * - Persist installation state (FleetManager + DB).
25
+ * - Run the worker's post-finalize re-scan (worker handles that
26
+ * locally inside its finalize action handler).
27
+ *
28
+ * Caller contract for `pDispatch`:
29
+ * pDispatch(actionName: string, settings: object) -> Promise<response>
30
+ * where `response` is the beacon's `{ Outputs: {...}, Log: [...] }`
31
+ * envelope (see Ultravisor-Beacon-Client.cjs:588). Outputs.Status
32
+ * and Outputs.ExitCode drive success/failure detection.
33
+ *
34
+ * @author Steven Velozo <steven@velozo.com>
35
+ */
36
+
37
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
38
+ const libPath = require('path');
39
+ const libFileStream = require('ultravisor-file-stream');
40
+
41
+ // Always-skip names. Callers may add to this set per push (e.g.
42
+ // pushing a model dir excludes 'venvs' too); we keep the universal
43
+ // suspects baked in.
44
+ const DEFAULT_IGNORE_BASENAMES = new Set([
45
+ '__pycache__',
46
+ 'node_modules',
47
+ '.DS_Store',
48
+ '.git'
49
+ ]);
50
+
51
+ const DEFAULT_CHUNK_BYTES = libFileStream.DEFAULT_CHUNK_BYTES;
52
+
53
+ class UltravisorDirectoryDistributor extends libFableServiceProviderBase
54
+ {
55
+ constructor(pFable, pOptions, pServiceHash)
56
+ {
57
+ super(pFable, pOptions, pServiceHash);
58
+ this.serviceType = 'UltravisorDirectoryDistributor';
59
+
60
+ this._chunkBytes = this.options.ChunkBytes || DEFAULT_CHUNK_BYTES;
61
+ }
62
+
63
+ /**
64
+ * Hash + manifest a source directory.
65
+ *
66
+ * @param {string} pSourceDir
67
+ * @param {object} [pOptions]
68
+ * - IgnoreBasenames Set<string> extra skip names (merged with default)
69
+ * @returns {{Hash, FileCount, TotalBytes, Files}}
70
+ */
71
+ scan(pSourceDir, pOptions)
72
+ {
73
+ let tmpIgnore = this._mergedIgnore(pOptions && pOptions.IgnoreBasenames);
74
+ let tmpScan = libFileStream.hashDirectoryTree(pSourceDir, tmpIgnore);
75
+ if (this.log)
76
+ {
77
+ this.log.info(
78
+ `UltravisorDirectoryDistributor: scanned ${pSourceDir} — `
79
+ + `hash=${tmpScan.Hash.slice(0, 16)}... `
80
+ + `files=${tmpScan.FileCount} bytes=${tmpScan.TotalBytes}`);
81
+ }
82
+ return tmpScan;
83
+ }
84
+
85
+ /**
86
+ * Build the chunk payloads for a single file inside a source dir.
87
+ *
88
+ * The wire RelativePath is what the worker stores at — it's the
89
+ * caller's responsibility to compose that. For a runtime push the
90
+ * orchestrator passes the in-source relative path verbatim
91
+ * ('worker_protocol.py'); for a model push the orchestrator
92
+ * prefixes the model basename ('sd15/weights/model.safetensors')
93
+ * so the worker's LWM_PushModel handler writes it under
94
+ * `<library>/sd15/weights/model.safetensors`.
95
+ */
96
+ buildChunksForFile(pSourceDir, pInSourceRelativePath, pWireRelativePath)
97
+ {
98
+ let tmpLocalRel = pInSourceRelativePath.split('/').join(libPath.sep);
99
+ let tmpFullPath = libPath.join(pSourceDir, tmpLocalRel);
100
+ return libFileStream.buildChunksForFile(tmpFullPath,
101
+ { ChunkBytes: this._chunkBytes, RelativePath: pWireRelativePath });
102
+ }
103
+
104
+ /**
105
+ * Orchestrate the full push to one beacon target.
106
+ *
107
+ * @param {object} pConfig
108
+ * - SourceDir (required) absolute path on hub disk
109
+ * - PushAction (required) LWM action name for chunks
110
+ * (e.g. 'LWM_PushPythonRuntime',
111
+ * 'LWM_PushModel')
112
+ * - FinalizeAction (required) LWM action name for finalize
113
+ * - DestPathPrefix (optional) prepended to each file's
114
+ * in-source RelativePath when
115
+ * building the wire RelativePath.
116
+ * Empty for runtime pushes;
117
+ * '<model-name>' for model pushes.
118
+ * - ExpectedHashKey (optional) Settings key the finalize action
119
+ * reads to validate the tree-hash.
120
+ * 'ExpectedRuntimeHash' (default),
121
+ * 'ExpectedModelHash', etc.
122
+ * - FinalizeExtras (optional) merged into the finalize-action
123
+ * Settings (worker-side handlers
124
+ * may need extra context — e.g.
125
+ * the model name for inventory
126
+ * re-scan).
127
+ * - IgnoreBasenames (optional) Set of extra skip names.
128
+ * @param {function} pDispatch async (actionName, settings) -> response
129
+ * @param {function} [pProgress] optional ({BytesPushed, TotalBytes,
130
+ * FilesPushed, FileCount, CurrentFile})
131
+ */
132
+ async pushDirectoryToTarget(pConfig, pDispatch, pProgress)
133
+ {
134
+ let tmpSourceDir = pConfig.SourceDir;
135
+ let tmpPushAction = pConfig.PushAction;
136
+ let tmpFinalizeAction = pConfig.FinalizeAction;
137
+ let tmpDestPrefix = pConfig.DestPathPrefix || '';
138
+ let tmpExpectedHashKey = pConfig.ExpectedHashKey || 'ExpectedRuntimeHash';
139
+ let tmpFinalizeExtras = pConfig.FinalizeExtras || {};
140
+
141
+ if (!tmpSourceDir || !tmpPushAction || !tmpFinalizeAction)
142
+ {
143
+ return {
144
+ Status: 'Error',
145
+ Error: 'pushDirectoryToTarget: SourceDir, PushAction, FinalizeAction all required.',
146
+ FilesPushed: 0,
147
+ BytesPushed: 0
148
+ };
149
+ }
150
+
151
+ let tmpStart = Date.now();
152
+ let tmpScan = this.scan(tmpSourceDir, { IgnoreBasenames: pConfig.IgnoreBasenames });
153
+
154
+ if (tmpScan.FileCount === 0)
155
+ {
156
+ return {
157
+ Status: 'Error',
158
+ Error: `pushDirectoryToTarget: source directory ${tmpSourceDir} is empty.`,
159
+ FilesPushed: 0,
160
+ BytesPushed: 0
161
+ };
162
+ }
163
+
164
+ let tmpBytesPushed = 0;
165
+ let tmpFilesPushed = 0;
166
+
167
+ for (let tmpFile of tmpScan.Files)
168
+ {
169
+ // Compose wire RelativePath: optionally prefix with the
170
+ // destination subdir so the worker writes the file at
171
+ // '<base>/<DestPathPrefix>/<file-rel>'.
172
+ let tmpWireRel = tmpDestPrefix
173
+ ? this._joinForwardSlash(tmpDestPrefix, tmpFile.RelativePath)
174
+ : tmpFile.RelativePath;
175
+
176
+ let tmpChunks = this.buildChunksForFile(tmpSourceDir, tmpFile.RelativePath, tmpWireRel);
177
+ for (let tmpChunk of tmpChunks)
178
+ {
179
+ let tmpResp = await pDispatch(tmpPushAction, tmpChunk);
180
+ if (!this._isOkResponse(tmpResp))
181
+ {
182
+ return {
183
+ Status: 'Error',
184
+ Error: `push chunk ${tmpChunk.ChunkIndex}/${tmpChunk.TotalChunks} `
185
+ + `for ${tmpWireRel} failed: `
186
+ + `${this._extractError(tmpResp)}`,
187
+ FilesPushed: tmpFilesPushed,
188
+ BytesPushed: tmpBytesPushed,
189
+ FailedResponse: tmpResp
190
+ };
191
+ }
192
+ tmpBytesPushed += Buffer.byteLength(tmpChunk.Content || '', 'base64');
193
+ if (typeof pProgress === 'function')
194
+ {
195
+ try
196
+ {
197
+ pProgress({
198
+ BytesPushed: tmpBytesPushed,
199
+ TotalBytes: tmpScan.TotalBytes,
200
+ FilesPushed: tmpFilesPushed,
201
+ FileCount: tmpScan.FileCount,
202
+ CurrentFile: tmpWireRel
203
+ });
204
+ }
205
+ catch (e) { /* progress callbacks shouldn't fail the push */ }
206
+ }
207
+ }
208
+ tmpFilesPushed++;
209
+ }
210
+
211
+ // Finalize.
212
+ let tmpFinalizeSettings = Object.assign({}, tmpFinalizeExtras);
213
+ tmpFinalizeSettings[tmpExpectedHashKey] = tmpScan.Hash;
214
+
215
+ let tmpFinalResp = await pDispatch(tmpFinalizeAction, tmpFinalizeSettings);
216
+ let tmpFinalOk = this._isOkResponse(tmpFinalResp);
217
+
218
+ return {
219
+ Status: tmpFinalOk ? 'Success' : 'Error',
220
+ Error: tmpFinalOk ? null : this._extractError(tmpFinalResp),
221
+ SourceDir: tmpSourceDir,
222
+ TreeHash: tmpScan.Hash,
223
+ FilesPushed: tmpFilesPushed,
224
+ FileCount: tmpScan.FileCount,
225
+ BytesPushed: tmpBytesPushed,
226
+ TotalBytes: tmpScan.TotalBytes,
227
+ FinalizeResponse: tmpFinalResp,
228
+ DurationMs: Date.now() - tmpStart
229
+ };
230
+ }
231
+
232
+ // ── Internals ──────────────────────────────────────────────
233
+
234
+ _mergedIgnore(pExtra)
235
+ {
236
+ if (!pExtra || (pExtra.size === 0))
237
+ {
238
+ return DEFAULT_IGNORE_BASENAMES;
239
+ }
240
+ let tmpMerged = new Set(DEFAULT_IGNORE_BASENAMES);
241
+ for (let tmpName of pExtra) tmpMerged.add(tmpName);
242
+ return tmpMerged;
243
+ }
244
+
245
+ _joinForwardSlash(pA, pB)
246
+ {
247
+ // Always emit forward-slash-delimited wire paths regardless of
248
+ // host OS. Worker's chunked-write normalizes to local sep.
249
+ let tmpA = String(pA || '').replace(/^\/+|\/+$/g, '');
250
+ let tmpB = String(pB || '').replace(/^\/+/, '');
251
+ if (!tmpA) return tmpB;
252
+ if (!tmpB) return tmpA;
253
+ return tmpA + '/' + tmpB;
254
+ }
255
+
256
+ _isOkResponse(pResp)
257
+ {
258
+ if (!pResp) return false;
259
+ if (pResp.Success === false) return false;
260
+ let tmpOutputs = pResp.Outputs || {};
261
+ // Worker contract: Outputs.Status === 'Success' OR ExitCode falsy.
262
+ if (tmpOutputs.Status && tmpOutputs.Status !== 'Success') return false;
263
+ if (tmpOutputs.ExitCode && tmpOutputs.ExitCode !== 0) return false;
264
+ return true;
265
+ }
266
+
267
+ _extractError(pResp)
268
+ {
269
+ if (!pResp) return 'no response';
270
+ let tmpOutputs = pResp.Outputs || {};
271
+ return tmpOutputs.Error
272
+ || tmpOutputs.Status
273
+ || pResp.Error
274
+ || 'unknown';
275
+ }
276
+ }
277
+
278
+ module.exports = UltravisorDirectoryDistributor;
279
+ module.exports.DEFAULT_IGNORE_BASENAMES = DEFAULT_IGNORE_BASENAMES;
280
+ module.exports.DEFAULT_CHUNK_BYTES = DEFAULT_CHUNK_BYTES;