feat: preserve search state and resolution aliases across workflow transitions

- Propagate missing_search_key through resolution queue and download
  resolution paths so cache lookups remain consistent after workflow
  re-analysis or active searches.
- Set preserveSearchCacheAcrossNextWorkflowSync before reloading
  workflow data following batch queue/resolve operations.
- Remember applied resolu tions (rememberAppliedResolvedSelections)
  after both queue and direct downloads so alias tracking is not lost.
- getMissingSearchKey now short-circuits on missing_search_key and
  search_key fields instead of always deriving from node identity,
  fixing spurious cache misses.
- Guard settleInactiveSearchProgress so non-active workflow states
  only update the cache when the active workflow hosts a matching
  background search job.
- After workflow analysis, call applyResolvedSelectionAliases
  so analysis data reflects user selections.
- When switching workflows, clone and merge the previous workflow
  search-result cache and migrate background search jobs that are
  still running, so active searches are not interrupted or dropped.
This commit is contained in:
Dariusz L
2026-06-28 04:04:45 +02:00
parent 59e874c17f
commit f780e6507b
5 changed files with 117 additions and 3 deletions
+9
View File
@@ -1762,6 +1762,7 @@ export const queueMethods = {
const missingKey = this.getMissingModelKey(missing);
const resolution = {
missing_key: missingKey,
missing_search_key: this.getMissingSearchKey?.(missing),
node_id: missing.node_id,
widget_index: missing.widget_index,
resolved_path: resolvedModel.path,
@@ -1869,6 +1870,7 @@ export const queueMethods = {
this.pendingResolutions = remainingSelections;
this.rebuildPendingIndex();
this.savePendingQueueForActiveWorkflow();
this.preserveSearchCacheAcrossNextWorkflowSync = true;
this.syncWorkflowScopedQueue?.(data.workflow);
this.pendingResolutions = remainingSelections;
@@ -2056,6 +2058,7 @@ export const queueMethods = {
this.appliedResolvedSelectionAliases.set(key, {
key,
category: selection.category || '',
searchKey: selection.missing_search_key || selection.search_key || '',
refs: this.getResolvedSelectionRefs(selection),
tokens: this.getResolvedSelectionTokenSet(this.getResolvedSelectionPathCandidates(selection))
});
@@ -2110,6 +2113,9 @@ export const queueMethods = {
if (matchedKeys.has(key)) continue;
if (!this.doesResolvedModelMatchAlias(model, alias)) continue;
model.missing_key = key;
if (alias.searchKey) {
model.missing_search_key = alias.searchKey;
}
matchedKeys.add(key);
break;
}
@@ -2133,6 +2139,9 @@ export const queueMethods = {
return {
...(sourceMissing || {}),
missing_key: this.getResolutionQueueKey(selection),
missing_search_key: selection.missing_search_key
|| sourceMissing?.missing_search_key
|| (sourceMissing ? this.getMissingSearchKey?.(sourceMissing) : ''),
__isOptimisticResolved: true,
node_id: selection.node_id ?? sourceMissing?.node_id,
widget_index: selection.widget_index ?? sourceMissing?.widget_index,
@@ -29,7 +29,11 @@ export const resolveDownloadMethods = {
const nodeRefs = missing.all_node_refs || [missing];
log.debug('nodeRefs count:', nodeRefs?.length, 'is_lora_v2:', nodeRefs?.[0]?.is_lora_v2);
const missingKey = this.getMissingModelKey?.(missing);
const missingSearchKey = this.getMissingSearchKey?.(missing);
const resolutions = nodeRefs.map(ref => ({
missing_key: missingKey,
missing_search_key: missingSearchKey,
node_id: ref.node_id,
widget_index: ref.widget_index,
resolved_path: resolvedModel.path,
@@ -61,9 +65,11 @@ export const resolveDownloadMethods = {
const count = resolutions.length;
const refText = count > 1 ? ` (${count} references)` : '';
this.showNotification(`✓ Model linked successfully: ${modelName}${refText}`, 'success');
this.rememberAppliedResolvedSelections?.(resolutions);
// Reload dialog using the updated workflow from API response
// This ensures we're analyzing the correct updated workflow
this.preserveSearchCacheAcrossNextWorkflowSync = true;
await this.loadWorkflowData(data.workflow, { force: true });
} else {
this.showNotification('Failed to resolve model: ' + (data.error || 'Unknown error'), 'error');
@@ -102,6 +108,8 @@ export const resolveDownloadMethods = {
if (perfectMatch && perfectMatch.model) {
resolutions.push({
missing_key: this.getMissingModelKey?.(missing),
missing_search_key: this.getMissingSearchKey?.(missing),
node_id: missing.node_id,
widget_index: missing.widget_index,
resolved_path: perfectMatch.model.path,
@@ -140,9 +148,11 @@ export const resolveDownloadMethods = {
`✓ Successfully linked ${resolutions.length} model${resolutions.length > 1 ? 's' : ''}!`,
'success'
);
this.rememberAppliedResolvedSelections?.(resolutions);
// Reload dialog using the updated workflow from API response (if dialog is visible)
if (this.contentElement) {
this.preserveSearchCacheAcrossNextWorkflowSync = true;
await this.loadWorkflowData(resolveData.workflow, { force: true });
}
@@ -2160,6 +2170,11 @@ export const resolveDownloadMethods = {
if (currentJob?.runId === searchRunId) {
this.backgroundSearchJobs.delete(backgroundJobKey);
}
for (const [jobKey, job] of Array.from(this.backgroundSearchJobs?.entries?.() || [])) {
if (job?.runId === searchRunId && job?.missingSearchKey === missingSearchKey) {
this.backgroundSearchJobs.delete(jobKey);
}
}
if (!completedSearchRun) {
this.settleInactiveSearchProgress?.(missing, state, {
workflowKey,
+46 -3
View File
@@ -7,6 +7,13 @@ export const searchPanelMethods = {
* Build stable cache key for a missing model entry
*/
getMissingSearchKey(missing) {
if (missing?.missing_search_key) {
return String(missing.missing_search_key);
}
if (missing?.search_key) {
return String(missing.search_key);
}
const nodeId = missing?.node_id ?? '';
const widgetIndex = missing?.widget_index ?? '';
const subgraphId = missing?.subgraph_id ?? '';
@@ -526,14 +533,45 @@ export const searchPanelMethods = {
if (workflowKey === this.getWorkflowScopedQueueKey()) {
this.searchResultCache.set(missingSearchKey, state);
return;
}
const activeWorkflowKey = this.getWorkflowScopedQueueKey();
const activeState = this.searchResultCache?.get(missingSearchKey);
const runId = state.activeSearchRunId || activeState?.activeSearchRunId;
const mirrorsActiveSearch = Boolean(
activeState === state
|| (
runId
&& activeState?.activeSearchRunId === runId
&& this.hasBackgroundSearchJob(activeWorkflowKey, missingSearchKey, runId)
)
);
if (mirrorsActiveSearch) {
this.searchResultCache.set(missingSearchKey, state);
this.saveSearchCacheForActiveWorkflow?.();
}
},
refreshSearchUiForMissing(missing, state = null, { workflowKey = this.getWorkflowScopedQueueKey() } = {}) {
if (!missing) return;
if (workflowKey && workflowKey !== this.getWorkflowScopedQueueKey()) return;
const currentState = state || this.searchResultCache.get(this.getMissingSearchKey(missing));
const activeWorkflowKey = this.getWorkflowScopedQueueKey();
const missingSearchKey = this.getMissingSearchKey(missing);
let currentState = state || this.searchResultCache.get(missingSearchKey);
if (workflowKey && workflowKey !== activeWorkflowKey) {
const activeState = this.searchResultCache.get(missingSearchKey);
const runId = currentState?.activeSearchRunId || activeState?.activeSearchRunId;
const mirrorsActiveSearch = Boolean(
activeState === currentState
|| (
runId
&& activeState?.activeSearchRunId === runId
&& this.hasBackgroundSearchJob(activeWorkflowKey, missingSearchKey, runId)
)
);
if (!mirrorsActiveSearch) return;
currentState = activeState || currentState;
}
this.refreshMissingSourcesSummary(missing);
this.updateBatchFooterButtons?.();
@@ -1228,8 +1266,13 @@ export const searchPanelMethods = {
const runId = state.activeSearchRunId;
if (!runId || !this.hasBackgroundSearchJob(workflowKey, missingSearchKey, runId)) continue;
const job = this.getBackgroundSearchJob(workflowKey, missingSearchKey);
for (const [source, progress] of Object.entries(state.sourceProgress || {})) {
if (progress?.status !== 'running') continue;
const progressId = job?.sourceProgressIds?.get?.(source);
if (progressId) {
this.startBackendSearchProgressPolling?.(state, missing, source, runId, progressId, { workflowKey });
}
this.startEstimatedSearchProgress(state, missing, null, source, runId, { workflowKey });
}
this.refreshSearchUiForMissing(missing, state, { workflowKey });
@@ -210,6 +210,7 @@ export const lifecycleGraphMethods = {
}
await progressPromise;
}
this.applyResolvedSelectionAliasesToAnalysisData?.(data);
if (this._workflowDataLoadToken === loadToken) {
this.cachedWorkflowSignature = workflowSignature;
this.cachedAnalysisData = data;
@@ -560,10 +560,31 @@ export const workflowStateMethods = {
const nextSignature = this.getWorkflowSignature(currentWorkflow);
const previousKey = this.getWorkflowScopedQueueKey();
const nextKey = this.getWorkflowScopedQueueKey(nextRoute, nextSignature);
const preserveSearchCache = Boolean(this.preserveSearchCacheAcrossNextWorkflowSync);
const previousSearchCache = preserveSearchCache
? this.cloneSearchResultCache(this.searchResultCache, {
preserveActive: true,
workflowKey: previousKey
})
: null;
const preservedActiveSearchStates = new Map();
const preservedActiveSearchMissingModels = [];
if (preserveSearchCache && previousKey && nextKey && nextKey !== previousKey) {
for (const [missingSearchKey, state] of this.searchResultCache.entries()) {
const runId = state?.activeSearchRunId;
if (!runId || !this.hasBackgroundSearchJob?.(previousKey, missingSearchKey, runId)) continue;
const job = this.getBackgroundSearchJob?.(previousKey, missingSearchKey);
preservedActiveSearchStates.set(missingSearchKey, { state, job });
if (job?.missing) {
preservedActiveSearchMissingModels.push(job.missing);
}
}
}
if (!nextKey || nextKey === previousKey) {
this.activeWorkflowRouteKey = nextRoute;
this.activeWorkflowSignature = nextSignature;
this.preserveSearchCacheAcrossNextWorkflowSync = false;
return;
}
@@ -579,7 +600,32 @@ export const workflowStateMethods = {
this.restoreAnalysisCacheForActiveWorkflow();
this.restoreLoadedModelsCacheForActiveWorkflow();
this.restoreSearchCacheForActiveWorkflow();
if (preserveSearchCache && previousSearchCache?.size) {
const mergedSearchCache = this.searchResultCache instanceof Map
? this.searchResultCache
: new Map();
for (const [missingSearchKey, state] of previousSearchCache.entries()) {
if (!mergedSearchCache.has(missingSearchKey)) {
mergedSearchCache.set(missingSearchKey, state);
}
}
for (const [missingSearchKey, preserved] of preservedActiveSearchStates.entries()) {
mergedSearchCache.set(missingSearchKey, preserved.state);
if (preserved.job) {
this.backgroundSearchJobs?.set(
this.getBackgroundSearchJobKey(nextKey, missingSearchKey),
preserved.job
);
}
}
this.searchResultCache = mergedSearchCache;
this.saveSearchCacheForActiveWorkflow();
}
this.preserveSearchCacheAcrossNextWorkflowSync = false;
this.restoreDownloadTargetSelectionsForActiveWorkflow();
if (preservedActiveSearchMissingModels.length) {
this.syncSearchProgressAfterResume?.(preservedActiveSearchMissingModels, { workflowKey: nextKey });
}
},
clearWorkflowScopedState() {