ultravisor 1.0.21 → 1.0.22

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.21",
3
+ "version": "1.0.22",
4
4
  "description": "Cyclic process execution with ai integration.",
5
5
  "main": "source/Ultravisor.cjs",
6
6
  "bin": {
@@ -31,15 +31,16 @@
31
31
  "orator": "^6.0.4",
32
32
  "orator-authentication": "^1.0.0",
33
33
  "orator-serviceserver-restify": "^2.0.10",
34
- "pict": "^1.0.362",
34
+ "pict": "^1.0.363",
35
35
  "pict-service-commandlineutility": "^1.0.19",
36
36
  "pict-serviceproviderbase": "^1.0.4",
37
37
  "ultravisor-beacon": "^0.0.11",
38
38
  "ws": "^8.20.0"
39
39
  },
40
40
  "devDependencies": {
41
+ "pict-docuserve": "^0.1.5",
41
42
  "puppeteer": "^24.40.0",
42
- "quackage": "^1.0.65"
43
+ "quackage": "^1.1.0"
43
44
  },
44
45
  "mocha": {
45
46
  "diff": true,
@@ -1923,6 +1923,13 @@ class UltravisorBeaconCoordinator extends libPictService
1923
1923
 
1924
1924
  /**
1925
1925
  * Remove a work item hash from a Beacon's CurrentWorkItems array.
1926
+ *
1927
+ * After freeing the slot, any still-Pending work items that match
1928
+ * the Beacon's capabilities get a fresh push attempt. This is
1929
+ * important for WebSocket beacons: they only receive work via
1930
+ * `_tryPushToWebSocketBeacon`, and without this re-dispatch a
1931
+ * parallel work item enqueued while the beacon was full would sit
1932
+ * Pending forever (nobody polls for WebSocket beacons).
1926
1933
  */
1927
1934
  _removeWorkItemFromBeacon(pBeaconID, pWorkItemHash)
1928
1935
  {
@@ -1949,6 +1956,38 @@ class UltravisorBeaconCoordinator extends libPictService
1949
1956
  {
1950
1957
  tmpBeacon.Status = 'Online';
1951
1958
  }
1959
+
1960
+ // Re-attempt dispatch of any pending work items. The beacon may
1961
+ // now have capacity for items that were enqueued while it was
1962
+ // busy. _tryPushToWebSocketBeacon iterates all registered
1963
+ // beacons so this also covers the case where a different beacon
1964
+ // came online mid-run.
1965
+ this._dispatchPendingWorkItems();
1966
+ }
1967
+
1968
+ /**
1969
+ * Walk the work queue and attempt to push any Pending,
1970
+ * unassigned work items via the WebSocket dispatch path.
1971
+ *
1972
+ * Safe to call any time — the push helper re-checks beacon
1973
+ * capacity, capability match, and state. This is the single
1974
+ * entry point for "something changed that might let a parked
1975
+ * work item move" (slot freed, new beacon registered, etc.).
1976
+ */
1977
+ _dispatchPendingWorkItems()
1978
+ {
1979
+ // No WebSocket dispatch handler means nothing to push to.
1980
+ if (!this._WorkItemPushHandler) return;
1981
+
1982
+ let tmpHashes = Object.keys(this._WorkQueue);
1983
+ for (let i = 0; i < tmpHashes.length; i++)
1984
+ {
1985
+ let tmpWI = this._WorkQueue[tmpHashes[i]];
1986
+ if (!tmpWI) continue;
1987
+ if (tmpWI.Status !== 'Pending') continue;
1988
+ if (tmpWI.AssignedBeaconID) continue; // affinity-assigned, leave it
1989
+ this._tryPushToWebSocketBeacon(tmpWI);
1990
+ }
1952
1991
  }
1953
1992
 
1954
1993
  /**
@@ -0,0 +1,471 @@
1
+ const libPictService = require('pict-serviceproviderbase');
2
+
3
+ /**
4
+ * Ultravisor Operation Auditor
5
+ *
6
+ * Cross-references beacon-dispatch nodes in registered operations against
7
+ * the beacon action catalog (SettingsSchema) to find port-mapping bugs.
8
+ *
9
+ * Why this exists: Ultravisor resolves state-connection values into a
10
+ * worker's Settings dict by extracting the port name from the substring
11
+ * AFTER the last -ei-/-eo-/-si-/-so- in the port hash. That extracted
12
+ * name becomes the settings key the worker receives. If the port hash
13
+ * doesn't end with a key the worker actually reads, the value is silently
14
+ * dropped and the worker falls back to its default — producing
15
+ * confusing-but-non-fatal bugs that only surface as "wrong output".
16
+ *
17
+ * This auditor walks every registered operation and flags:
18
+ *
19
+ * - TGT_PORT_NOT_IN_SCHEMA — a state connection's target port hash
20
+ * extracts to a name that is not in the target action's
21
+ * SettingsSchema. The value will be sent to the worker under a key
22
+ * the worker ignores.
23
+ *
24
+ * - UNKNOWN_DATA_KEY — a beacon-dispatch node's static Data object
25
+ * contains a key that is not in the target action's SettingsSchema.
26
+ * Same problem: the worker never reads it.
27
+ *
28
+ * - VALUE_INPUT_SRC_MISMATCH — a state connection whose source is a
29
+ * value-input node has a source port hash that does not extract to
30
+ * "InputValue" (which is the fixed output key for value-input).
31
+ *
32
+ * - SRC_PORT_NOT_IN_SCHEMA (soft) — only reported when the source
33
+ * action publishes an OutputsSchema. Since the standard beacon
34
+ * registration only exposes input schemas, this is skipped unless
35
+ * an OutputsSchema has been added.
36
+ *
37
+ * Source-side mismatches (e.g. reading `output_file` when the worker
38
+ * returns `video_file`) cannot be detected from the action catalog alone
39
+ * because beacons do not publish output schemas. Runtime telemetry or a
40
+ * caller-supplied map is required for full source-side coverage.
41
+ *
42
+ * @author Steven Velozo <steven@velozo.com>
43
+ */
44
+ class UltravisorOperationAuditor extends libPictService
45
+ {
46
+ constructor(pPict, pOptions, pServiceHash)
47
+ {
48
+ super(pPict, pOptions, pServiceHash);
49
+
50
+ this.serviceType = 'UltravisorOperationAuditor';
51
+ }
52
+
53
+ /**
54
+ * Get a named Fable service or null if not registered.
55
+ */
56
+ _getService(pTypeName)
57
+ {
58
+ return this.fable.servicesMap[pTypeName]
59
+ ? Object.values(this.fable.servicesMap[pTypeName])[0]
60
+ : null;
61
+ }
62
+
63
+ /**
64
+ * Extract the logical port name from a port hash, mirroring
65
+ * Ultravisor-ExecutionEngine._extractPortName so the auditor reports
66
+ * what the real execution engine would actually resolve.
67
+ *
68
+ * @param {string} pPortHash - The port hash string.
69
+ * @param {object} [pPortLabelMap] - Optional hash -> label fallback map.
70
+ * @returns {string} The extracted port name, or '' if input is invalid.
71
+ */
72
+ extractPortName(pPortHash, pPortLabelMap)
73
+ {
74
+ if (!pPortHash || typeof(pPortHash) !== 'string')
75
+ {
76
+ return '';
77
+ }
78
+
79
+ let tmpPrefixes = ['-ei-', '-eo-', '-si-', '-so-'];
80
+
81
+ for (let i = 0; i < tmpPrefixes.length; i++)
82
+ {
83
+ let tmpIndex = pPortHash.lastIndexOf(tmpPrefixes[i]);
84
+ if (tmpIndex > -1)
85
+ {
86
+ return pPortHash.substring(tmpIndex + tmpPrefixes[i].length);
87
+ }
88
+ }
89
+
90
+ if (pPortLabelMap && pPortLabelMap[pPortHash])
91
+ {
92
+ return pPortLabelMap[pPortHash];
93
+ }
94
+
95
+ return pPortHash;
96
+ }
97
+
98
+ /**
99
+ * Build a fallback hash -> label lookup from a graph.
100
+ */
101
+ _buildPortLabelMap(pGraph)
102
+ {
103
+ let tmpMap = {};
104
+ let tmpNodes = (pGraph && pGraph.Nodes) || [];
105
+
106
+ for (let i = 0; i < tmpNodes.length; i++)
107
+ {
108
+ let tmpPorts = tmpNodes[i].Ports || [];
109
+ for (let j = 0; j < tmpPorts.length; j++)
110
+ {
111
+ tmpMap[tmpPorts[j].Hash] = tmpPorts[j].Label || tmpPorts[j].Hash;
112
+ }
113
+ }
114
+
115
+ return tmpMap;
116
+ }
117
+
118
+ /**
119
+ * Index the beacon action catalog by "Capability/Action" for lookup.
120
+ * Returns a map where each value is the set of SettingsSchema field
121
+ * names the worker accepts.
122
+ *
123
+ * @returns {object} { "Capability/Action": Set<string> }
124
+ */
125
+ _buildActionSchemaIndex()
126
+ {
127
+ let tmpIndex = {};
128
+ let tmpCoordinator = this._getService('UltravisorBeaconCoordinator');
129
+
130
+ if (!tmpCoordinator || typeof(tmpCoordinator.getActionCatalog) !== 'function')
131
+ {
132
+ return tmpIndex;
133
+ }
134
+
135
+ let tmpCatalog = tmpCoordinator.getActionCatalog();
136
+
137
+ for (let i = 0; i < tmpCatalog.length; i++)
138
+ {
139
+ let tmpEntry = tmpCatalog[i];
140
+ let tmpKey = `${tmpEntry.Capability}/${tmpEntry.Action}`;
141
+ let tmpReads = new Set();
142
+
143
+ let tmpSchema = tmpEntry.SettingsSchema || [];
144
+ for (let j = 0; j < tmpSchema.length; j++)
145
+ {
146
+ let tmpField = tmpSchema[j];
147
+ if (tmpField && tmpField.Name)
148
+ {
149
+ tmpReads.add(tmpField.Name);
150
+ }
151
+ }
152
+
153
+ tmpIndex[tmpKey] = tmpReads;
154
+ }
155
+
156
+ return tmpIndex;
157
+ }
158
+
159
+ /**
160
+ * Meta keys that live inside a beacon-dispatch node's Data object but
161
+ * are NOT forwarded to the worker under their own name. Two categories:
162
+ *
163
+ * 1. Ultravisor-level meta consumed by Extension task config itself.
164
+ * See Ultravisor-TaskConfigs-Extension.cjs. These identify
165
+ * capability/action/timeout/affinity for the beacon dispatch
166
+ * machinery before anything hits a worker.
167
+ *
168
+ * 2. Beacon-host meta consumed by retold-labs (or a similar beacon
169
+ * provider) before the work item is sent to the Python worker.
170
+ * The current example is `venv_path`, which retold-labs'
171
+ * LibraryScanner injects into library-generated operations so
172
+ * BeaconSetup._executeWorker can resolve a library-declared env
173
+ * to a Python interpreter. BeaconSetup strips the key before
174
+ * sending settings to the worker, so the worker never sees it
175
+ * and its SettingsSchema never declares it.
176
+ *
177
+ * Auditor treats both categories as meta so they don't flag as
178
+ * UNKNOWN_DATA_KEY when present on a beacon-dispatch node.
179
+ */
180
+ _getDispatchMetaKeys()
181
+ {
182
+ return new Set([
183
+ // Ultravisor-level meta (Extension task config)
184
+ 'RemoteCapability',
185
+ 'RemoteAction',
186
+ 'AffinityKey',
187
+ 'TimeoutMs',
188
+ 'PromptMessage',
189
+ 'OutputAddress',
190
+ 'InputSchema',
191
+
192
+ // Beacon-host meta (consumed by retold-labs before worker dispatch)
193
+ 'venv_path'
194
+ ]);
195
+ }
196
+
197
+ /**
198
+ * Audit a single operation definition.
199
+ *
200
+ * @param {object} pOperation - An operation definition (with .Graph).
201
+ * @param {object} pActionIndex - From _buildActionSchemaIndex().
202
+ * @returns {Array<object>} List of issue objects.
203
+ */
204
+ auditOperation(pOperation, pActionIndex)
205
+ {
206
+ let tmpIssues = [];
207
+
208
+ if (!pOperation || !pOperation.Graph)
209
+ {
210
+ return tmpIssues;
211
+ }
212
+
213
+ let tmpGraph = pOperation.Graph;
214
+ let tmpNodes = tmpGraph.Nodes || [];
215
+ let tmpConnections = tmpGraph.Connections || [];
216
+ let tmpPortLabelMap = this._buildPortLabelMap(tmpGraph);
217
+ let tmpMetaKeys = this._getDispatchMetaKeys();
218
+
219
+ // Index nodes and build a dispatch-node map with action + reads
220
+ let tmpNodesByHash = {};
221
+ let tmpDispatchNodes = {};
222
+
223
+ for (let i = 0; i < tmpNodes.length; i++)
224
+ {
225
+ let tmpNode = tmpNodes[i];
226
+ tmpNodesByHash[tmpNode.Hash] = tmpNode;
227
+
228
+ if (tmpNode.Type === 'beacon-dispatch')
229
+ {
230
+ let tmpData = tmpNode.Data || {};
231
+ let tmpCap = tmpData.RemoteCapability || '';
232
+ let tmpAction = tmpData.RemoteAction || '';
233
+ let tmpKey = `${tmpCap}/${tmpAction}`;
234
+ let tmpReads = pActionIndex[tmpKey] || null;
235
+
236
+ tmpDispatchNodes[tmpNode.Hash] = {
237
+ Node: tmpNode,
238
+ Capability: tmpCap,
239
+ Action: tmpAction,
240
+ CatalogKey: tmpKey,
241
+ Reads: tmpReads,
242
+ HasSchema: !!tmpReads
243
+ };
244
+
245
+ // If the action isn't in the catalog, the auditor has no ground
246
+ // truth for this node — emit a skip notice so callers understand
247
+ // why nothing else was flagged here.
248
+ if (!tmpReads)
249
+ {
250
+ tmpIssues.push(
251
+ {
252
+ Kind: 'ACTION_NOT_IN_CATALOG',
253
+ Severity: 'info',
254
+ Node: tmpNode.Hash,
255
+ NodeTitle: tmpNode.Title || '',
256
+ CatalogKey: tmpKey,
257
+ Detail: `Capability/Action [${tmpKey}] is not in the beacon action catalog; no schema is available to audit this node against. Connect the providing beacon to populate the catalog, then re-run.`
258
+ });
259
+ }
260
+ else
261
+ {
262
+ // Check static Data keys against the worker's reads
263
+ let tmpDataKeys = Object.keys(tmpData);
264
+ for (let k = 0; k < tmpDataKeys.length; k++)
265
+ {
266
+ let tmpKeyName = tmpDataKeys[k];
267
+ if (tmpMetaKeys.has(tmpKeyName))
268
+ {
269
+ continue;
270
+ }
271
+ if (!tmpReads.has(tmpKeyName))
272
+ {
273
+ tmpIssues.push(
274
+ {
275
+ Kind: 'UNKNOWN_DATA_KEY',
276
+ Severity: 'warning',
277
+ Node: tmpNode.Hash,
278
+ NodeTitle: tmpNode.Title || '',
279
+ CatalogKey: tmpKey,
280
+ Key: tmpKeyName,
281
+ Detail: `Node Data contains '${tmpKeyName}' which ${tmpKey} does not declare in its SettingsSchema; the worker will ignore it.`
282
+ });
283
+ }
284
+ }
285
+ }
286
+ }
287
+ }
288
+
289
+ // Check every state connection
290
+ for (let i = 0; i < tmpConnections.length; i++)
291
+ {
292
+ let tmpConn = tmpConnections[i];
293
+ if (tmpConn.ConnectionType !== 'state')
294
+ {
295
+ continue;
296
+ }
297
+
298
+ let tmpSrcNodeHash = tmpConn.SourceNodeHash || '';
299
+ let tmpTgtNodeHash = tmpConn.TargetNodeHash || '';
300
+ let tmpSrcPortHash = tmpConn.SourcePortHash || '';
301
+ let tmpTgtPortHash = tmpConn.TargetPortHash || '';
302
+
303
+ let tmpSrcName = this.extractPortName(tmpSrcPortHash, tmpPortLabelMap);
304
+ let tmpTgtName = this.extractPortName(tmpTgtPortHash, tmpPortLabelMap);
305
+ let tmpConnHash = tmpConn.Hash || '(unnamed)';
306
+
307
+ // Target side: resolved name must be in the target action's SettingsSchema
308
+ let tmpTgtDispatch = tmpDispatchNodes[tmpTgtNodeHash];
309
+ if (tmpTgtDispatch && tmpTgtDispatch.HasSchema)
310
+ {
311
+ if (!tmpTgtDispatch.Reads.has(tmpTgtName))
312
+ {
313
+ tmpIssues.push(
314
+ {
315
+ Kind: 'TGT_PORT_NOT_IN_SCHEMA',
316
+ Severity: 'error',
317
+ Connection: tmpConnHash,
318
+ TargetNode: tmpTgtNodeHash,
319
+ CatalogKey: tmpTgtDispatch.CatalogKey,
320
+ TargetPort: tmpTgtPortHash,
321
+ ExtractedName: tmpTgtName,
322
+ Detail: `Target port hash extracts to '${tmpTgtName}' but ${tmpTgtDispatch.CatalogKey} does not declare that field in its SettingsSchema; the state value will be delivered under a key the worker ignores.`
323
+ });
324
+ }
325
+ }
326
+
327
+ // Source side: value-input nodes always emit under "InputValue"
328
+ let tmpSrcNode = tmpNodesByHash[tmpSrcNodeHash];
329
+ if (tmpSrcNode && tmpSrcNode.Type === 'value-input')
330
+ {
331
+ if (tmpSrcName !== 'InputValue')
332
+ {
333
+ tmpIssues.push(
334
+ {
335
+ Kind: 'VALUE_INPUT_SRC_MISMATCH',
336
+ Severity: 'error',
337
+ Connection: tmpConnHash,
338
+ SourceNode: tmpSrcNodeHash,
339
+ SourcePort: tmpSrcPortHash,
340
+ ExtractedName: tmpSrcName,
341
+ Detail: `Source is a value-input node; source port should extract to 'InputValue' but extracts to '${tmpSrcName}'. The connection will read undefined.`
342
+ });
343
+ }
344
+ }
345
+ }
346
+
347
+ return tmpIssues;
348
+ }
349
+
350
+ /**
351
+ * Audit every operation currently registered in HypervisorState.
352
+ *
353
+ * @param {function} fCallback - (pError, pReport)
354
+ * where pReport = {
355
+ * AuditedAt: ISO timestamp,
356
+ * OperationCount: number,
357
+ * IssueCount: number,
358
+ * ActionCatalogSize: number,
359
+ * Operations: [ { Hash, Name, IssueCount, Issues } ],
360
+ * WorstByKind: { KIND: count }
361
+ * }
362
+ */
363
+ auditAll(fCallback)
364
+ {
365
+ let tmpState = this._getService('UltravisorHypervisorState');
366
+ if (!tmpState)
367
+ {
368
+ return fCallback(new Error('UltravisorHypervisorState service not available.'));
369
+ }
370
+
371
+ let tmpActionIndex = this._buildActionSchemaIndex();
372
+ let tmpActionCatalogSize = Object.keys(tmpActionIndex).length;
373
+
374
+ tmpState.getOperationList(
375
+ (pError, pOperations) =>
376
+ {
377
+ if (pError)
378
+ {
379
+ return fCallback(pError);
380
+ }
381
+
382
+ let tmpOperations = [];
383
+ let tmpTotalIssues = 0;
384
+ let tmpWorstByKind = {};
385
+
386
+ for (let i = 0; i < pOperations.length; i++)
387
+ {
388
+ let tmpOp = pOperations[i];
389
+ let tmpIssues = this.auditOperation(tmpOp, tmpActionIndex);
390
+ tmpTotalIssues += tmpIssues.length;
391
+
392
+ for (let j = 0; j < tmpIssues.length; j++)
393
+ {
394
+ let tmpKind = tmpIssues[j].Kind;
395
+ tmpWorstByKind[tmpKind] = (tmpWorstByKind[tmpKind] || 0) + 1;
396
+ }
397
+
398
+ tmpOperations.push(
399
+ {
400
+ Hash: tmpOp.Hash,
401
+ Name: tmpOp.Name || '',
402
+ IssueCount: tmpIssues.length,
403
+ Issues: tmpIssues
404
+ });
405
+ }
406
+
407
+ // Sort: worst first, then by name
408
+ tmpOperations.sort(
409
+ (a, b) =>
410
+ {
411
+ if (b.IssueCount !== a.IssueCount) return b.IssueCount - a.IssueCount;
412
+ return (a.Name || a.Hash).localeCompare(b.Name || b.Hash);
413
+ });
414
+
415
+ let tmpReport =
416
+ {
417
+ AuditedAt: new Date().toISOString(),
418
+ OperationCount: pOperations.length,
419
+ IssueCount: tmpTotalIssues,
420
+ ActionCatalogSize: tmpActionCatalogSize,
421
+ WorstByKind: tmpWorstByKind,
422
+ Operations: tmpOperations
423
+ };
424
+
425
+ this.log.info(`OperationAuditor: scanned ${pOperations.length} operations, found ${tmpTotalIssues} issue(s).`);
426
+
427
+ return fCallback(null, tmpReport);
428
+ });
429
+ }
430
+
431
+ /**
432
+ * Audit a single operation by hash.
433
+ *
434
+ * @param {string} pHash - Operation hash.
435
+ * @param {function} fCallback - (pError, pResult)
436
+ */
437
+ auditByHash(pHash, fCallback)
438
+ {
439
+ let tmpState = this._getService('UltravisorHypervisorState');
440
+ if (!tmpState)
441
+ {
442
+ return fCallback(new Error('UltravisorHypervisorState service not available.'));
443
+ }
444
+
445
+ tmpState.getOperation(pHash,
446
+ (pError, pOperation) =>
447
+ {
448
+ if (pError)
449
+ {
450
+ return fCallback(pError);
451
+ }
452
+
453
+ let tmpActionIndex = this._buildActionSchemaIndex();
454
+ let tmpIssues = this.auditOperation(pOperation, tmpActionIndex);
455
+
456
+ return fCallback(null,
457
+ {
458
+ AuditedAt: new Date().toISOString(),
459
+ Hash: pOperation.Hash,
460
+ Name: pOperation.Name || '',
461
+ IssueCount: tmpIssues.length,
462
+ ActionCatalogSize: Object.keys(tmpActionIndex).length,
463
+ Issues: tmpIssues
464
+ });
465
+ });
466
+ }
467
+ }
468
+
469
+ module.exports = UltravisorOperationAuditor;
470
+ module.exports.serviceType = 'UltravisorOperationAuditor';
471
+ module.exports.default_configuration = {};
@@ -253,6 +253,60 @@ class UltravisorAPIServer extends libPictService
253
253
  }.bind(this)
254
254
  );
255
255
 
256
+ // --- Operation Audit ---
257
+ // Static port-mapping audit across all registered operations.
258
+ // Cross-references beacon-dispatch nodes' state connections and
259
+ // Data keys against the beacon action catalog's SettingsSchema.
260
+ this._OratorServer.get
261
+ (
262
+ '/OperationAudit',
263
+ function (pRequest, pResponse, fNext)
264
+ {
265
+ let tmpAuditor = this._getService('UltravisorOperationAuditor');
266
+ if (!tmpAuditor)
267
+ {
268
+ pResponse.send(503, { Error: 'UltravisorOperationAuditor service not available.' });
269
+ return fNext();
270
+ }
271
+ tmpAuditor.auditAll(
272
+ function (pError, pReport)
273
+ {
274
+ if (pError)
275
+ {
276
+ pResponse.send(500, { Error: pError.message });
277
+ return fNext();
278
+ }
279
+ pResponse.send(pReport);
280
+ return fNext();
281
+ });
282
+ }.bind(this)
283
+ );
284
+
285
+ this._OratorServer.get
286
+ (
287
+ '/OperationAudit/:Hash',
288
+ function (pRequest, pResponse, fNext)
289
+ {
290
+ let tmpAuditor = this._getService('UltravisorOperationAuditor');
291
+ if (!tmpAuditor)
292
+ {
293
+ pResponse.send(503, { Error: 'UltravisorOperationAuditor service not available.' });
294
+ return fNext();
295
+ }
296
+ tmpAuditor.auditByHash(pRequest.params.Hash,
297
+ function (pError, pResult)
298
+ {
299
+ if (pError)
300
+ {
301
+ pResponse.send(404, { Error: pError.message });
302
+ return fNext();
303
+ }
304
+ pResponse.send(pResult);
305
+ return fNext();
306
+ });
307
+ }.bind(this)
308
+ );
309
+
256
310
  this._OratorServer.post
257
311
  (
258
312
  '/Operation',