ultravisor 1.0.22 → 1.0.24
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/docs/queue-followups/03-config-migration.md +123 -0
- package/docs/queue-followups/04-direct-dispatch-phase-emission.md +128 -0
- package/package.json +3 -1
- package/source/Ultravisor.cjs +4 -0
- package/source/datamodel/Ultravisor-BeaconQueue.json +165 -0
- package/source/services/Ultravisor-Beacon-ActionDefaults.cjs +174 -0
- package/source/services/Ultravisor-Beacon-Coordinator.cjs +382 -6
- package/source/services/Ultravisor-Beacon-RunManager.cjs +169 -0
- package/source/services/Ultravisor-Beacon-Scheduler.cjs +789 -0
- package/source/services/Ultravisor-ExecutionEngine.cjs +242 -6
- package/source/services/Ultravisor-ExecutionManifest.cjs +1 -0
- package/source/services/persistence/Ultravisor-Beacon-QueueStore.cjs +886 -0
- package/source/services/tasks/file-system/Ultravisor-TaskConfigs-FileSystem.cjs +81 -0
- package/source/services/tasks/file-system/definitions/chunked-write.json +38 -0
- package/source/web_server/Ultravisor-API-Server.cjs +354 -0
- package/test/Ultravisor_BeaconQueue_tests.js +502 -0
- package/test/Ultravisor_tests.js +132 -0
|
@@ -590,6 +590,191 @@ class UltravisorExecutionEngine extends libPictService
|
|
|
590
590
|
});
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
+
/**
|
|
594
|
+
* Retry a completed/errored operation from its last failed node.
|
|
595
|
+
*
|
|
596
|
+
* Reads the persisted manifest, finds the node that errored, resets
|
|
597
|
+
* it to WaitingForInput, rebuilds the graph, and re-dispatches the
|
|
598
|
+
* work item through the coordinator. All prior node outputs
|
|
599
|
+
* (materialized files, state writes) are preserved — only the
|
|
600
|
+
* failed node re-runs.
|
|
601
|
+
*
|
|
602
|
+
* @param {string} pRunHash - The execution run hash to retry
|
|
603
|
+
* @param {object} [pOptions] - Optional overrides
|
|
604
|
+
* - NodeHash: retry a specific node (default: auto-detect failed node)
|
|
605
|
+
* - SettingsOverrides: merge into the re-dispatched Settings
|
|
606
|
+
* @param {function} fCallback - function(pError, pExecutionContext)
|
|
607
|
+
*/
|
|
608
|
+
retryFromCheckpoint(pRunHash, pOptions, fCallback)
|
|
609
|
+
{
|
|
610
|
+
if (typeof pOptions === 'function')
|
|
611
|
+
{
|
|
612
|
+
fCallback = pOptions;
|
|
613
|
+
pOptions = {};
|
|
614
|
+
}
|
|
615
|
+
pOptions = pOptions || {};
|
|
616
|
+
|
|
617
|
+
let tmpManifestService = this._getManifestService();
|
|
618
|
+
if (!tmpManifestService)
|
|
619
|
+
{
|
|
620
|
+
return fCallback(new Error('ExecutionEngine: UltravisorExecutionManifest service not found.'));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
let tmpContext = tmpManifestService.getRun(pRunHash);
|
|
624
|
+
if (!tmpContext)
|
|
625
|
+
{
|
|
626
|
+
return fCallback(new Error(`ExecutionEngine: run [${pRunHash}] not found.`));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (tmpContext.Status === 'Running' || tmpContext.Status === 'WaitingForInput')
|
|
630
|
+
{
|
|
631
|
+
return fCallback(new Error(`ExecutionEngine: run [${pRunHash}] is still active (${tmpContext.Status}). Cannot retry.`));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Find the failed node — either specified or auto-detected from TaskManifests
|
|
635
|
+
let tmpTargetNode = pOptions.NodeHash || null;
|
|
636
|
+
if (!tmpTargetNode)
|
|
637
|
+
{
|
|
638
|
+
for (let tmpNodeHash of Object.keys(tmpContext.TaskManifests || {}))
|
|
639
|
+
{
|
|
640
|
+
let tmpManifest = tmpContext.TaskManifests[tmpNodeHash];
|
|
641
|
+
if (tmpManifest.Executions && tmpManifest.Executions.length > 0)
|
|
642
|
+
{
|
|
643
|
+
let tmpLastExec = tmpManifest.Executions[tmpManifest.Executions.length - 1];
|
|
644
|
+
if (tmpLastExec.Status === 'Error')
|
|
645
|
+
{
|
|
646
|
+
tmpTargetNode = tmpNodeHash;
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Also check TaskOutputs for _BeaconError flag (beacon-dispatch failures)
|
|
654
|
+
if (!tmpTargetNode)
|
|
655
|
+
{
|
|
656
|
+
for (let tmpNodeHash of Object.keys(tmpContext.TaskOutputs || {}))
|
|
657
|
+
{
|
|
658
|
+
if (tmpContext.TaskOutputs[tmpNodeHash]._BeaconError)
|
|
659
|
+
{
|
|
660
|
+
tmpTargetNode = tmpNodeHash;
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (!tmpTargetNode)
|
|
667
|
+
{
|
|
668
|
+
return fCallback(new Error(`ExecutionEngine: no failed node found in run [${pRunHash}].`));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
this.log.info(`[Engine] retryFromCheckpoint: run=${pRunHash} node=${tmpTargetNode}`);
|
|
672
|
+
this._log(tmpContext, `Retrying from checkpoint: re-dispatching node [${tmpTargetNode}]`);
|
|
673
|
+
|
|
674
|
+
// Look up the operation definition to rebuild the graph
|
|
675
|
+
let tmpStateService = this.fable.servicesMap['UltravisorHypervisorState']
|
|
676
|
+
? Object.values(this.fable.servicesMap['UltravisorHypervisorState'])[0]
|
|
677
|
+
: null;
|
|
678
|
+
if (!tmpStateService)
|
|
679
|
+
{
|
|
680
|
+
return fCallback(new Error('ExecutionEngine: UltravisorHypervisorState service not found.'));
|
|
681
|
+
}
|
|
682
|
+
let tmpOperation = tmpStateService.getOperationSync(tmpContext.OperationHash);
|
|
683
|
+
if (!tmpOperation || !tmpOperation.Graph)
|
|
684
|
+
{
|
|
685
|
+
return fCallback(new Error(`ExecutionEngine: operation [${tmpContext.OperationHash}] not found.`));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Rebuild the graph context so traversal works
|
|
689
|
+
this._rebuildGraphContext(tmpContext, tmpOperation);
|
|
690
|
+
|
|
691
|
+
// Clear the failed node's error state
|
|
692
|
+
delete tmpContext.TaskOutputs[tmpTargetNode];
|
|
693
|
+
if (tmpContext.TaskManifests[tmpTargetNode])
|
|
694
|
+
{
|
|
695
|
+
// Keep the execution history but mark the new attempt
|
|
696
|
+
tmpContext.TaskManifests[tmpTargetNode].Executions.push({
|
|
697
|
+
Status: 'Retrying',
|
|
698
|
+
AttemptNumber: tmpContext.TaskManifests[tmpTargetNode].Executions.length,
|
|
699
|
+
StartTime: new Date().toISOString(),
|
|
700
|
+
StartTimeMs: Date.now()
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Re-add the node as WaitingForInput so the coordinator
|
|
705
|
+
// picks it up. Use the same ResumeEventName the original
|
|
706
|
+
// beacon-dispatch used ('Complete' for successful retry).
|
|
707
|
+
tmpContext.WaitingTasks[tmpTargetNode] = {
|
|
708
|
+
PromptMessage: '',
|
|
709
|
+
OutputAddress: '',
|
|
710
|
+
ResumeEventName: 'Complete',
|
|
711
|
+
Timestamp: new Date().toISOString()
|
|
712
|
+
};
|
|
713
|
+
tmpContext.Status = 'WaitingForInput';
|
|
714
|
+
tmpContext.StopTime = null;
|
|
715
|
+
|
|
716
|
+
// Clear operation-level errors from the previous run
|
|
717
|
+
tmpContext.Errors = tmpContext.Errors.filter(
|
|
718
|
+
(e) => e.NodeHash !== tmpTargetNode);
|
|
719
|
+
|
|
720
|
+
// Persist the updated manifest
|
|
721
|
+
if (tmpContext.StagingPath)
|
|
722
|
+
{
|
|
723
|
+
tmpManifestService._writeManifest(tmpContext, tmpContext.StagingPath);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
this.log.info(`ExecutionEngine: run [${pRunHash}] reset to WaitingForInput at node [${tmpTargetNode}]. Re-dispatch the work item to continue.`);
|
|
727
|
+
|
|
728
|
+
// Now re-dispatch the beacon work item through the coordinator.
|
|
729
|
+
// The node's original Settings are in the operation graph.
|
|
730
|
+
let tmpCoordinator = this.fable.servicesMap['UltravisorBeaconCoordinator']
|
|
731
|
+
? Object.values(this.fable.servicesMap['UltravisorBeaconCoordinator'])[0]
|
|
732
|
+
: null;
|
|
733
|
+
|
|
734
|
+
if (tmpCoordinator && tmpContext._NodeMap && tmpContext._NodeMap[tmpTargetNode])
|
|
735
|
+
{
|
|
736
|
+
let tmpNodeDef = tmpContext._NodeMap[tmpTargetNode];
|
|
737
|
+
let tmpSettings = Object.assign({}, tmpNodeDef.Settings || {}, pOptions.SettingsOverrides || {});
|
|
738
|
+
|
|
739
|
+
// Resolve state addresses in Settings (same as original dispatch)
|
|
740
|
+
let tmpStateManager = this._getStateManager();
|
|
741
|
+
if (tmpStateManager)
|
|
742
|
+
{
|
|
743
|
+
for (let tmpKey of Object.keys(tmpSettings))
|
|
744
|
+
{
|
|
745
|
+
let tmpVal = tmpSettings[tmpKey];
|
|
746
|
+
if (typeof tmpVal === 'string' && tmpVal.indexOf('.') !== -1)
|
|
747
|
+
{
|
|
748
|
+
let tmpResolved = tmpStateManager.getAddress(tmpVal, tmpContext, tmpTargetNode);
|
|
749
|
+
if (tmpResolved !== undefined && tmpResolved !== tmpVal)
|
|
750
|
+
{
|
|
751
|
+
tmpSettings[tmpKey] = tmpResolved;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
let tmpWorkItemInfo = {
|
|
758
|
+
RunHash: pRunHash,
|
|
759
|
+
NodeHash: tmpTargetNode,
|
|
760
|
+
OperationHash: tmpContext.OperationHash,
|
|
761
|
+
Capability: tmpNodeDef.Capability || tmpSettings.Capability || '',
|
|
762
|
+
Action: tmpNodeDef.Action || tmpSettings.Action || '',
|
|
763
|
+
Settings: tmpSettings,
|
|
764
|
+
TimeoutMs: tmpNodeDef.TimeoutMs || 600000
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
tmpCoordinator.enqueueWorkItem(tmpWorkItemInfo);
|
|
768
|
+
this.log.info(`ExecutionEngine: re-dispatched work item for node [${tmpTargetNode}]`);
|
|
769
|
+
}
|
|
770
|
+
else
|
|
771
|
+
{
|
|
772
|
+
this.log.warn(`ExecutionEngine: could not re-dispatch — coordinator or node definition not available. Run is waiting; resume manually.`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return fCallback(null, tmpContext);
|
|
776
|
+
}
|
|
777
|
+
|
|
593
778
|
// ====================================================================
|
|
594
779
|
// Internal: Event Queue Processing
|
|
595
780
|
// ====================================================================
|
|
@@ -608,7 +793,22 @@ class UltravisorExecutionEngine extends libPictService
|
|
|
608
793
|
if (Object.keys(pContext.WaitingTasks).length > 0)
|
|
609
794
|
{
|
|
610
795
|
pContext.Status = 'WaitingForInput';
|
|
611
|
-
|
|
796
|
+
// Use each waiting task's PromptMessage — set by the task
|
|
797
|
+
// when it returned {WaitingForInput:true, ...}. That's
|
|
798
|
+
// where tasks already describe what they're waiting for
|
|
799
|
+
// ("Waiting for Beacon (...)", "Waiting for LLM response",
|
|
800
|
+
// "Please provide a value"). A hardcoded "user input"
|
|
801
|
+
// log misled operators for beacon/LLM waits that never
|
|
802
|
+
// involve the user.
|
|
803
|
+
let tmpWaitHashes = Object.keys(pContext.WaitingTasks);
|
|
804
|
+
let tmpReasons = [];
|
|
805
|
+
for (let i = 0; i < tmpWaitHashes.length; i++)
|
|
806
|
+
{
|
|
807
|
+
let tmpWait = pContext.WaitingTasks[tmpWaitHashes[i]];
|
|
808
|
+
if (tmpWait && tmpWait.PromptMessage) tmpReasons.push(tmpWait.PromptMessage);
|
|
809
|
+
}
|
|
810
|
+
let tmpReason = tmpReasons.length ? tmpReasons.join('; ') : 'waiting for input';
|
|
811
|
+
this._log(pContext, 'Operation paused: ' + tmpReason + '.');
|
|
612
812
|
}
|
|
613
813
|
return fCallback(null);
|
|
614
814
|
}
|
|
@@ -687,8 +887,8 @@ class UltravisorExecutionEngine extends libPictService
|
|
|
687
887
|
return fCallback(null);
|
|
688
888
|
}
|
|
689
889
|
|
|
690
|
-
// Resolve incoming state connections
|
|
691
|
-
let tmpResolvedSettings = this._resolveStateConnections(pNodeHash, tmpNode, pContext);
|
|
890
|
+
// Resolve incoming state connections (pass definition for type-aware resolution)
|
|
891
|
+
let tmpResolvedSettings = this._resolveStateConnections(pNodeHash, tmpNode, pContext, tmpDefinition);
|
|
692
892
|
|
|
693
893
|
// Get the manifest service for recording
|
|
694
894
|
let tmpManifestService = this._getManifestService();
|
|
@@ -952,7 +1152,7 @@ class UltravisorExecutionEngine extends libPictService
|
|
|
952
1152
|
* @param {object} pContext - The execution context.
|
|
953
1153
|
* @returns {object} The resolved settings object.
|
|
954
1154
|
*/
|
|
955
|
-
_resolveStateConnections(pNodeHash, pNode, pContext)
|
|
1155
|
+
_resolveStateConnections(pNodeHash, pNode, pContext, pDefinition)
|
|
956
1156
|
{
|
|
957
1157
|
// Start with a copy of the node's static settings
|
|
958
1158
|
// Nodes may store config in Settings or Data (flow editor uses Data)
|
|
@@ -991,10 +1191,23 @@ class UltravisorExecutionEngine extends libPictService
|
|
|
991
1191
|
tmpSourceValue = this._resolveTemplate(tmpConn.Data.Template, tmpTemplateContext);
|
|
992
1192
|
}
|
|
993
1193
|
|
|
1194
|
+
// Determine which settings key to write under. When a
|
|
1195
|
+
// state connection explicitly declares `Data.StateKey`,
|
|
1196
|
+
// honor it — this lets operations fan state into a
|
|
1197
|
+
// setting whose name doesn't match any physical port
|
|
1198
|
+
// (e.g. the storyboard's parameter-sweep connection
|
|
1199
|
+
// routes a value-input's InputValue into the sweep
|
|
1200
|
+
// task's `ParameterSets` setting even though the sweep
|
|
1201
|
+
// only exposes event trigger ports). Falls through to
|
|
1202
|
+
// the target port name for backward compatibility.
|
|
1203
|
+
let tmpSettingsKey = (tmpConn.Data && typeof(tmpConn.Data.StateKey) === 'string' && tmpConn.Data.StateKey)
|
|
1204
|
+
? tmpConn.Data.StateKey
|
|
1205
|
+
: tmpTargetPortName;
|
|
1206
|
+
|
|
994
1207
|
// Write the resolved value into settings
|
|
995
|
-
if (
|
|
1208
|
+
if (tmpSettingsKey && tmpSourceValue !== undefined)
|
|
996
1209
|
{
|
|
997
|
-
tmpSettings[
|
|
1210
|
+
tmpSettings[tmpSettingsKey] = tmpSourceValue;
|
|
998
1211
|
}
|
|
999
1212
|
}
|
|
1000
1213
|
|
|
@@ -1002,12 +1215,35 @@ class UltravisorExecutionEngine extends libPictService
|
|
|
1002
1215
|
// (e.g. "{~D:Record.Operation.InputFilePath~}" -> actual value from state)
|
|
1003
1216
|
let tmpTemplateContext = tmpStateManager.buildTemplateContext(pContext);
|
|
1004
1217
|
|
|
1218
|
+
// Build a type lookup from the task definition's SettingsInputs.
|
|
1219
|
+
// Object and Array typed settings should NOT have their contents
|
|
1220
|
+
// template-resolved — they carry configuration (like MappingConfiguration)
|
|
1221
|
+
// whose {~D:Record.Field~} expressions are meant for downstream consumers
|
|
1222
|
+
// (e.g. TabularTransform), not for the execution engine.
|
|
1223
|
+
let tmpSettingsTypeMap = {};
|
|
1224
|
+
if (pDefinition && Array.isArray(pDefinition.SettingsInputs))
|
|
1225
|
+
{
|
|
1226
|
+
for (let s = 0; s < pDefinition.SettingsInputs.length; s++)
|
|
1227
|
+
{
|
|
1228
|
+
tmpSettingsTypeMap[pDefinition.SettingsInputs[s].Name] = pDefinition.SettingsInputs[s].DataType || 'String';
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1005
1232
|
let tmpSettingsKeys = Object.keys(tmpSettings);
|
|
1006
1233
|
for (let i = 0; i < tmpSettingsKeys.length; i++)
|
|
1007
1234
|
{
|
|
1008
1235
|
let tmpKey = tmpSettingsKeys[i];
|
|
1009
1236
|
let tmpVal = tmpSettings[tmpKey];
|
|
1010
1237
|
|
|
1238
|
+
// Skip template resolution for Object and Array typed settings.
|
|
1239
|
+
// These carry opaque configuration (e.g. mapping rules) whose
|
|
1240
|
+
// template expressions belong to the consuming service, not the engine.
|
|
1241
|
+
let tmpDeclaredType = tmpSettingsTypeMap[tmpKey];
|
|
1242
|
+
if (tmpDeclaredType === 'Object' || tmpDeclaredType === 'Array')
|
|
1243
|
+
{
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1011
1247
|
if (typeof(tmpVal) === 'string' && tmpVal.indexOf('{~') >= 0)
|
|
1012
1248
|
{
|
|
1013
1249
|
// When the entire value is a single {~D:Record.X~} expression,
|
|
@@ -541,6 +541,7 @@ class UltravisorExecutionManifest extends libPictService
|
|
|
541
541
|
|
|
542
542
|
this._emitExecutionEvent('ExecutionComplete', pExecutionContext.Hash,
|
|
543
543
|
{
|
|
544
|
+
OperationHash: pExecutionContext.OperationHash,
|
|
544
545
|
Status: pExecutionContext.Status,
|
|
545
546
|
ElapsedMs: pExecutionContext.ElapsedMs,
|
|
546
547
|
ErrorCount: pExecutionContext.Errors.length
|