From f780e6507bdd96835d68dc01289566953fad1cb3 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Sun, 28 Jun 2026 04:04:45 +0200 Subject: [PATCH] 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. --- web/resolver/actions/queue_methods.js | 9 ++++ .../actions/resolve_download_methods.js | 15 ++++++ web/resolver/search/search_panel.js | 49 +++++++++++++++++-- web/resolver/shell/lifecycle_graph_methods.js | 1 + web/resolver/shell/workflow_state_methods.js | 46 +++++++++++++++++ 5 files changed, 117 insertions(+), 3 deletions(-) diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index 022b04e..6366d30 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -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, diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index bbd0432..36ec7b7 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -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, diff --git a/web/resolver/search/search_panel.js b/web/resolver/search/search_panel.js index c7d4a84..4116748 100644 --- a/web/resolver/search/search_panel.js +++ b/web/resolver/search/search_panel.js @@ -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 }); diff --git a/web/resolver/shell/lifecycle_graph_methods.js b/web/resolver/shell/lifecycle_graph_methods.js index 9685e76..4c73cd8 100644 --- a/web/resolver/shell/lifecycle_graph_methods.js +++ b/web/resolver/shell/lifecycle_graph_methods.js @@ -210,6 +210,7 @@ export const lifecycleGraphMethods = { } await progressPromise; } + this.applyResolvedSelectionAliasesToAnalysisData?.(data); if (this._workflowDataLoadToken === loadToken) { this.cachedWorkflowSignature = workflowSignature; this.cachedAnalysisData = data; diff --git a/web/resolver/shell/workflow_state_methods.js b/web/resolver/shell/workflow_state_methods.js index 4f55296..d9c11cf 100644 --- a/web/resolver/shell/workflow_state_methods.js +++ b/web/resolver/shell/workflow_state_methods.js @@ -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() {