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.
@@ -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
- this._log(pContext, 'Operation paused: waiting for user input.');
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 (tmpTargetPortName && tmpSourceValue !== undefined)
1208
+ if (tmpSettingsKey && tmpSourceValue !== undefined)
996
1209
  {
997
- tmpSettings[tmpTargetPortName] = tmpSourceValue;
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