Quellcode durchsuchen

fix: align the sd.cpp nodes to the current API, and show swallowed failures

Model loading failed with "Unknown field(s) in /models/load options:
stream_layers". Upstream removed that toggle: layer streaming and segmented
compute are how sd.cpp runs now, always on with nothing to enable, and the two
new fields only turn them OFF. The node sends disable_prefetch and
disable_segmented_compute instead, and the generation options were regenerated
from the server's own /options/generation.

max_vram defaults to 0, which is the adaptive setting - sd.cpp re-checks free
VRAM continuously and uses what is there. The old -1 is normalised to 0 before
sending, matching what the server's own parser does with a negative, so the
request that gets logged is the request that takes effect.

Two things the schema got wrong, both worth knowing:

/openapi.json is incomplete. force_sdxl_vae_conv_scale, expand_prompt,
flow_shift, image_index and job_id are all absent from it and all genuinely
accepted - they are in the server's allow-lists and read in
model_manager.cpp / request_handlers.cpp. force_sdxl_vae_conv_scale was
removed here on the strength of the schema and has been put back. A field
missing from that schema is not evidence the server rejects it, which matters
because the server's own error message says to check the spelling against it.

Verified end to end with the GPU free: the model loads (z_image_turbo_bf16,
Z-Image) and a generation produced two images stored in sdcpp_outputs.

Separately, the execution list and results panel now show when a run swallowed
failures. A run of 10 loop iterations where 3 failed reported "completed" and
nothing on screen said otherwise - toleratedErrorCount was on the record and
referenced nowhere in the UI. The list carries an amber "3 tolerated" badge and
the panel a banner explaining that Continue On Error carried the loop past bad
items. The engine rule is unchanged; only its visibility.
fszontagh vor 2 Wochen
Ursprung
Commit
03135d1952

+ 4 - 4
nodes/sdcpp/sdcpp-edit.js

@@ -193,8 +193,8 @@ const configSchema = {
         prompt: { title: "Prompt", description: "Text prompt fed to the model's text encoder (CLIP / T5 / LLM). Supports inline `<lora:name:weight>` tags - these are auto-extracted into the LoRA list before being sent to the encoder. Supports a1111-style dynamic-prompts (`{a|b|c}`) when `expand_prompt: true` is set. Recommended: Required. Leave empty for the architecture default.", type: "string", format: "textarea" },
         refImageArgs: { title: "Reference Image Args", description: "Comma-separated k=v flags controlling reference-image preprocessing (e.g. `resize_before_vae=0,ref_index_mode=increase`). Replaces the previous auto_resize_ref_image / increase_ref_index bools. See sd.cpp docs. Recommended: Leave empty unless you need to override defaults. Leave empty for the architecture default.", type: "string" },
         refImages: { title: "Reference Images (base64)", description: "Base64-encoded reference images for Flux Kontext / image-edit. Each image gets encoded into the conditioner alongside the text prompt. Recommended: Use for Flux Kontext or models that accept reference images. Leave empty for the architecture default.", type: "array", items: {"type": "string"} },
-        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "euler", "euler_a", "heun", "dpm2", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "ipndm", "ipndm_v", "lcm", "ddim_trailing", "tcd", "res_multistep", "res_2s", "er_sde", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], enumLabels: ["(architecture default)", "Euler - simple, fast, deterministic", "Euler ancestral - adds noise each step (less reproducible, more variet", "Heun - 2nd-order, slower, sometimes higher quality", "DPM2 - 2nd-order, balanced", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "IPNDM", "IPNDM-V", "LCM - for LCM-finetuned models (4-8 step generation)", "DDIM trailing - required for some fine-tunes", "TCD - for TCD-finetuned models", "RES multistep - flow-model variant", "RES 2S", "ER SDE - SDE sampler (added recently)", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], default: "" },
-        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler - some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "discrete", "karras", "exponential", "ays", "gits", "sgm_uniform", "simple", "smoothstep", "kl_optimal", "lcm", "bong_tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], enumLabels: ["(architecture default)", "Discrete - uniform across model timesteps (default)", "Karras - concentrates more steps near the end, common for SDXL", "Exponential", "Align Your Steps (AYS) - auto-tuned", "GITS", "SGM uniform - for SGM-trained models", "Simple - used by Flux / SD3 flow models", "Smoothstep - Z-Image's recommended scheduler", "KL optimal", "LCM - for LCM samplers", "Bong Tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], default: "" },
+        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "euler", "euler_a", "heun", "dpm2", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "ipndm", "ipndm_v", "lcm", "ddim_trailing", "tcd", "res_multistep", "res_2s", "er_sde", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], enumLabels: ["(architecture default)", "Euler - simple, fast, deterministic", "Euler ancestral - adds noise each step (less reproducible, more variet", "Heun - 2nd-order, slower, sometimes higher quality", "DPM2 - 2nd-order, balanced", "DPM++ 2S ancestral - popular for SDXL", "DPM++ 2M - fast, good quality", "DPM++ 2M v2 - improved schedule", "IPNDM", "IPNDM-V", "LCM - for LCM-finetuned models (4-8 step generation)", "DDIM trailing - required for some fine-tunes", "TCD - for TCD-finetuned models", "RES multistep - flow-model variant", "RES 2S", "ER SDE - SDE sampler", "Euler CFG++ - improved CFG variant of Euler", "Euler ancestral CFG++ - ancestral variant of Euler CFG++", "Euler GE - graph-effective Euler variant", "DPM++ 2M SDE (leejet PR #1742)", "DPM++ 2M SDE with Brownian-tree noise (leejet PR #1743)", "LMS - linear multi-step (leejet PR #1843)"], default: "" },
+        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler - some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "discrete", "karras", "exponential", "ays", "gits", "sgm_uniform", "simple", "smoothstep", "kl_optimal", "lcm", "bong_tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], enumLabels: ["(architecture default)", "Discrete - standard sd.cpp default", "Karras - improves sample quality, especially at low step counts", "Exponential - decays sigma exponentially", "AYS - align-your-steps", "GITS - Golden-Interval Tabular Sampling", "SGM uniform - matches SGM training-time noise schedule", "Simple - basic linear", "Smoothstep - smoothly interpolated schedule", "KL-optimal - minimizes discretization error", "LCM - for LCM-finetuned models", "Bong tangent", "LTX 2 - LTX-Video 2 schedule", "Logit-normal", "Flux - Flux-family model schedule (leejet PR #1723)", "Flux 2 - Flux 2 model schedule (leejet PR #1722)", "Beta - configurable via extra_sample_args beta_alpha=/beta_beta= (leej", "Normal - alias for discrete (leejet PR #1724)"], default: "" },
         seed: { title: "Seed", description: "RNG seed for the initial noise tensor (and stochastic samplers). -1 = pick a random one each generation. Same seed + same prompt + same model = same image. Recommended: -1 for variety. Pin a specific number for A/B comparing prompt or sampler changes. Leave empty for the architecture default.", type: "number" },
         shiftedTimestep: { title: "Shifted Timestep", description: "Start the sampling schedule from a non-final timestep (used by NitroFusion and similar fast-sampling fine-tunes). 0 = standard schedule. 250-500 = NitroFusion's range. Recommended: 0 unless you're explicitly running a NitroFusion-style fine-tune. Leave empty for the architecture default.", type: "number" },
         skipLayers: { title: "SLG Skip Layers", description: "Which transformer layers to skip during the SLG unconditional pass. SD3.5 Medium uses [7, 8, 9]. Recommended: [7,8,9] for SD3.5 Medium. Other models: leave default, ignored when slg_scale=0. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
@@ -389,8 +389,8 @@ const GENERATION_OPTIONS = [
 // file was generated. See scripts/gen-sdcpp-generation-options.py.
 const KNOWN_CHOICES = {
     "cacheMode": ["easycache", "spectrum"],
-    "sampler": ["ddim_trailing", "dpm2", "dpmpp2m", "dpmpp2mv2", "dpmpp2s_a", "er_sde", "euler", "euler_a", "heun", "ipndm", "ipndm_v", "lcm", "res_2s", "res_multistep", "tcd"],
-    "scheduler": ["ays", "bong_tangent", "discrete", "exponential", "gits", "karras", "kl_optimal", "lcm", "sgm_uniform", "simple", "smoothstep"]
+    "sampler": ["ddim_trailing", "dpm++2m", "dpm++2m_sde", "dpm++2m_sde_bt", "dpm++2mv2", "dpm++2s_a", "dpm2", "er_sde", "euler", "euler_a", "euler_a_cfg_pp", "euler_cfg_pp", "euler_ge", "heun", "ipndm", "ipndm_v", "lcm", "lms", "res_2s", "res_multistep", "tcd"],
+    "scheduler": ["ays", "beta", "bong_tangent", "discrete", "exponential", "flux", "flux2", "gits", "karras", "kl_optimal", "lcm", "logit_normal", "ltx2", "normal", "sgm_uniform", "simple", "smoothstep"]
 };
 
 async function execute(config, input, context) {

+ 4 - 4
nodes/sdcpp/sdcpp-img2img.js

@@ -189,8 +189,8 @@ const configSchema = {
         maskImageBase64: { title: "Mask Image (base64)", description: "Base64-encoded inpainting mask. White areas are repainted, black areas preserved. Recommended: Optional. Empty mask = standard img2img (no inpainting). Leave empty for the architecture default.", type: "string" },
         negativePrompt: { title: "Negative Prompt", description: "What to push the model away from. Has effect only when `cfg_scale > 1` (CFG is what compares cond vs uncond). Ignored on architectures that don't use CFG (most flow-matching models default to `cfg_scale = 1`). Recommended: Optional. Empty is a fine default for Flux / SD3 / Z-Image. Leave empty for the architecture default.", type: "string", format: "textarea" },
         prompt: { title: "Prompt", description: "Text prompt fed to the model's text encoder (CLIP / T5 / LLM). Supports inline `<lora:name:weight>` tags - these are auto-extracted into the LoRA list before being sent to the encoder. Supports a1111-style dynamic-prompts (`{a|b|c}`) when `expand_prompt: true` is set. Recommended: Required. Leave empty for the architecture default.", type: "string", format: "textarea" },
-        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "euler", "euler_a", "heun", "dpm2", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "ipndm", "ipndm_v", "lcm", "ddim_trailing", "tcd", "res_multistep", "res_2s", "er_sde", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], enumLabels: ["(architecture default)", "Euler - simple, fast, deterministic", "Euler ancestral - adds noise each step (less reproducible, more variet", "Heun - 2nd-order, slower, sometimes higher quality", "DPM2 - 2nd-order, balanced", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "IPNDM", "IPNDM-V", "LCM - for LCM-finetuned models (4-8 step generation)", "DDIM trailing - required for some fine-tunes", "TCD - for TCD-finetuned models", "RES multistep - flow-model variant", "RES 2S", "ER SDE - SDE sampler (added recently)", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], default: "" },
-        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler - some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "discrete", "karras", "exponential", "ays", "gits", "sgm_uniform", "simple", "smoothstep", "kl_optimal", "lcm", "bong_tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], enumLabels: ["(architecture default)", "Discrete - uniform across model timesteps (default)", "Karras - concentrates more steps near the end, common for SDXL", "Exponential", "Align Your Steps (AYS) - auto-tuned", "GITS", "SGM uniform - for SGM-trained models", "Simple - used by Flux / SD3 flow models", "Smoothstep - Z-Image's recommended scheduler", "KL optimal", "LCM - for LCM samplers", "Bong Tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], default: "" },
+        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "euler", "euler_a", "heun", "dpm2", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "ipndm", "ipndm_v", "lcm", "ddim_trailing", "tcd", "res_multistep", "res_2s", "er_sde", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], enumLabels: ["(architecture default)", "Euler - simple, fast, deterministic", "Euler ancestral - adds noise each step (less reproducible, more variet", "Heun - 2nd-order, slower, sometimes higher quality", "DPM2 - 2nd-order, balanced", "DPM++ 2S ancestral - popular for SDXL", "DPM++ 2M - fast, good quality", "DPM++ 2M v2 - improved schedule", "IPNDM", "IPNDM-V", "LCM - for LCM-finetuned models (4-8 step generation)", "DDIM trailing - required for some fine-tunes", "TCD - for TCD-finetuned models", "RES multistep - flow-model variant", "RES 2S", "ER SDE - SDE sampler", "Euler CFG++ - improved CFG variant of Euler", "Euler ancestral CFG++ - ancestral variant of Euler CFG++", "Euler GE - graph-effective Euler variant", "DPM++ 2M SDE (leejet PR #1742)", "DPM++ 2M SDE with Brownian-tree noise (leejet PR #1743)", "LMS - linear multi-step (leejet PR #1843)"], default: "" },
+        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler - some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "discrete", "karras", "exponential", "ays", "gits", "sgm_uniform", "simple", "smoothstep", "kl_optimal", "lcm", "bong_tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], enumLabels: ["(architecture default)", "Discrete - standard sd.cpp default", "Karras - improves sample quality, especially at low step counts", "Exponential - decays sigma exponentially", "AYS - align-your-steps", "GITS - Golden-Interval Tabular Sampling", "SGM uniform - matches SGM training-time noise schedule", "Simple - basic linear", "Smoothstep - smoothly interpolated schedule", "KL-optimal - minimizes discretization error", "LCM - for LCM-finetuned models", "Bong tangent", "LTX 2 - LTX-Video 2 schedule", "Logit-normal", "Flux - Flux-family model schedule (leejet PR #1723)", "Flux 2 - Flux 2 model schedule (leejet PR #1722)", "Beta - configurable via extra_sample_args beta_alpha=/beta_beta= (leej", "Normal - alias for discrete (leejet PR #1724)"], default: "" },
         seed: { title: "Seed", description: "RNG seed for the initial noise tensor (and stochastic samplers). -1 = pick a random one each generation. Same seed + same prompt + same model = same image. Recommended: -1 for variety. Pin a specific number for A/B comparing prompt or sampler changes. Leave empty for the architecture default.", type: "number" },
         shiftedTimestep: { title: "Shifted Timestep", description: "Start the sampling schedule from a non-final timestep (used by NitroFusion and similar fast-sampling fine-tunes). 0 = standard schedule. 250-500 = NitroFusion's range. Recommended: 0 unless you're explicitly running a NitroFusion-style fine-tune. Leave empty for the architecture default.", type: "number" },
         skipLayers: { title: "SLG Skip Layers", description: "Which transformer layers to skip during the SLG unconditional pass. SD3.5 Medium uses [7, 8, 9]. Recommended: [7,8,9] for SD3.5 Medium. Other models: leave default, ignored when slg_scale=0. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
@@ -383,8 +383,8 @@ const GENERATION_OPTIONS = [
 // file was generated. See scripts/gen-sdcpp-generation-options.py.
 const KNOWN_CHOICES = {
     "cacheMode": ["easycache", "spectrum"],
-    "sampler": ["ddim_trailing", "dpm2", "dpmpp2m", "dpmpp2mv2", "dpmpp2s_a", "er_sde", "euler", "euler_a", "heun", "ipndm", "ipndm_v", "lcm", "res_2s", "res_multistep", "tcd"],
-    "scheduler": ["ays", "bong_tangent", "discrete", "exponential", "gits", "karras", "kl_optimal", "lcm", "sgm_uniform", "simple", "smoothstep"]
+    "sampler": ["ddim_trailing", "dpm++2m", "dpm++2m_sde", "dpm++2m_sde_bt", "dpm++2mv2", "dpm++2s_a", "dpm2", "er_sde", "euler", "euler_a", "euler_a_cfg_pp", "euler_cfg_pp", "euler_ge", "heun", "ipndm", "ipndm_v", "lcm", "lms", "res_2s", "res_multistep", "tcd"],
+    "scheduler": ["ays", "beta", "bong_tangent", "discrete", "exponential", "flux", "flux2", "gits", "karras", "kl_optimal", "lcm", "logit_normal", "ltx2", "normal", "sgm_uniform", "simple", "smoothstep"]
 };
 
 async function execute(config, input, context) {

+ 36 - 6
nodes/sdcpp/sdcpp-model-load.js

@@ -30,7 +30,7 @@ const configSchema = {
         { title: 'Model', fields: ['modelName', 'modelType', 'whenDifferent', 'force'] },
         { title: 'Components', fields: ['vae', 'clipL', 'clipG', 't5xxl', 'llm', 'taesd', 'controlnet', 'ipAdapter'] },
         { title: 'Loading', fields: ['flashAttn', 'diffusionFlashAttn', 'enableMmap', 'eagerLoad',
-                                     'streamLayers', 'maxVram', 'nThreads', 'weightType'] },
+                                     'disablePrefetch', 'disableSegmentedCompute', 'maxVram', 'nThreads', 'weightType'] },
         { title: 'Advanced', fields: ['vaeFormat', 'prediction', 'rngType', 'samplerRngType',
                                       'loraApplyMode', 'vaeConvDirect', 'diffusionConvDirect',
                                       'taePreviewOnly', 'forceSdxlVaeConvScale', 'backend', 'paramsBackend', 'rpcServers',
@@ -60,7 +60,8 @@ const configSchema = {
             'loadOptions.diffusion_flash_attn': 'diffusionFlashAttn',
             'loadOptions.enable_mmap': 'enableMmap',
             'loadOptions.eager_load': 'eagerLoad',
-            'loadOptions.stream_layers': 'streamLayers',
+            'loadOptions.disable_prefetch': 'disablePrefetch',
+            'loadOptions.disable_segmented_compute': 'disableSegmentedCompute',
             'loadOptions.max_vram': 'maxVram',
             'loadOptions.n_threads': 'nThreads',
             'loadOptions.vae_format': 'vaeFormat',
@@ -219,8 +220,14 @@ const configSchema = {
         diffusionFlashAttn: { type: 'boolean', title: 'Flash Attention (diffusion)', description: 'Flash attention for the diffusion model specifically' },
         enableMmap: { type: 'boolean', title: 'Memory-map Weights', description: 'Recommended for large files' },
         eagerLoad: { type: 'boolean', title: 'Eager Load', description: 'Move every parameter to the compute backend at load time instead of on demand' },
-        streamLayers: { type: 'boolean', title: 'Stream Layers', description: 'Stream diffusion layers when the model does not fit in VRAM. Pair with a VRAM budget' },
-        maxVram: { type: 'number', title: 'VRAM Budget (GiB)', description: 'Budget for segmented parameter offload. 0 leaves it to the server' },
+        disablePrefetch: { type: 'boolean', title: 'Disable Prefetch', description: 'Turn off prefetching of the next layer\'s weights. On by default upstream - only switch this off to diagnose a problem, it costs speed' },
+        disableSegmentedCompute: { type: 'boolean', title: 'Disable Segmented Compute', description: 'Turn off running the diffusion graph in segments. On by default upstream, and what lets a model larger than VRAM run at all - switching it off will OOM on a big model' },
+        maxVram: {
+            type: 'number',
+            title: 'VRAM Budget (GiB)',
+            default: 0,
+            description: '0 (the default) lets sd.cpp re-check free VRAM continuously and use what is actually there - the safest setting, and the right one unless you have a specific reason. A positive N caps managed weights and runner buffers at N GiB regardless of what is free, which is what you want when sharing the card with something else and you need a hard ceiling. The old -1 is no longer accepted; the server coerces any negative value to 0, which matches what -1 was asking for'
+        },
         nThreads: { type: 'number', title: 'CPU Threads', description: '-1 lets the server decide' },
         weightType: {
             type: 'string', title: 'Weight Type',
@@ -267,7 +274,7 @@ const configSchema = {
         tensorTypeRules: { type: 'string', title: 'Tensor Type Rules', description: 'Per-tensor weight overrides using regex, such as ^vae\\.=f16' },
         options: {
             type: 'object', title: 'Other Load Options',
-            description: 'Extra load options passed through, such as flash_attn, enable_mmap, weight_type, stream_layers or max_vram'
+            description: 'Extra load options passed through, such as flash_attn, enable_mmap, weight_type, disable_prefetch or max_vram'
         },
         whenDifferent: {
             type: 'string', title: 'When A Different Model Is Loaded',
@@ -428,7 +435,12 @@ const LOAD_OPTIONS = [
     { setting: 'diffusionFlashAttn', server: 'diffusion_flash_attn' },
     { setting: 'enableMmap', server: 'enable_mmap' },
     { setting: 'eagerLoad', server: 'eager_load' },
-    { setting: 'streamLayers', server: 'stream_layers' },
+    // Layer streaming and segmented compute are how sd.cpp works now - always
+    // on, with nothing to enable. The old stream_layers toggle is gone
+    // entirely, and a load still carrying it is rejected outright: "Unknown
+    // field(s) in /models/load options". These two only turn the behaviour OFF.
+    { setting: 'disablePrefetch', server: 'disable_prefetch' },
+    { setting: 'disableSegmentedCompute', server: 'disable_segmented_compute' },
     { setting: 'maxVram', server: 'max_vram' },
     { setting: 'nThreads', server: 'n_threads' },
     { setting: 'weightType', server: 'weight_type' },
@@ -440,6 +452,10 @@ const LOAD_OPTIONS = [
     { setting: 'vaeConvDirect', server: 'vae_conv_direct' },
     { setting: 'diffusionConvDirect', server: 'diffusion_conv_direct' },
     { setting: 'taePreviewOnly', server: 'tae_preview_only' },
+    // Absent from /openapi.json but genuinely accepted - it is in the server's
+    // own allow-list in model_manager.cpp and read into ctx_params. The schema
+    // is incomplete here, so a field being missing from it is not evidence
+    // that the server rejects it.
     { setting: 'forceSdxlVaeConvScale', server: 'force_sdxl_vae_conv_scale' },
     { setting: 'backend', server: 'backend' },
     { setting: 'paramsBackend', server: 'params_backend' },
@@ -459,6 +475,20 @@ function wantedOptions(config) {
         var value = config[entry.setting];
         if (value === undefined || value === null || value === '') continue;
         if (typeof value === 'number' && !isFinite(value)) continue;
+
+        // max_vram used to take -1 for "auto". The server's own parser
+        // (ModelLoadParams::from_json) already coerces any negative to 0, which
+        // is the closest match to that intent - 0 means sd.cpp re-checks free
+        // VRAM continuously. Normalised here too so the value the node reports
+        // sending is the value that takes effect, rather than the caller seeing
+        // -1 in the request and 0 in the behaviour.
+        if (entry.server === 'max_vram' && value < 0) {
+            smartbotic.log.info('SD.cpp: max_vram ' + value +
+                ' is the old "auto" value; sending 0, which is what the server ' +
+                'would coerce it to and means "use whatever VRAM is free".');
+            value = 0;
+        }
+
         wanted[entry.server] = value;
     }
     if (config.options && typeof config.options === 'object') {

+ 4 - 4
nodes/sdcpp/sdcpp-txt2img.js

@@ -197,8 +197,8 @@ const configSchema = {
         prompt: { title: "Prompt", description: "Text prompt fed to the model's text encoder (CLIP / T5 / LLM). Supports inline `<lora:name:weight>` tags - these are auto-extracted into the LoRA list before being sent to the encoder. Supports a1111-style dynamic-prompts (`{a|b|c}`) when `expand_prompt: true` is set. Recommended: Required. Leave empty for the architecture default.", type: "string", format: "textarea" },
         refImageArgs: { title: "Reference Image Args", description: "Comma-separated k=v flags controlling reference-image preprocessing (e.g. `resize_before_vae=0,ref_index_mode=increase`). Replaces the previous auto_resize_ref_image / increase_ref_index bools. See sd.cpp docs. Recommended: Leave empty unless you need to override defaults. Leave empty for the architecture default.", type: "string" },
         refImages: { title: "Reference Images (base64)", description: "Base64-encoded reference images for Flux Kontext / image-edit. Each image gets encoded into the conditioner alongside the text prompt. Recommended: Use for Flux Kontext or models that accept reference images. Leave empty for the architecture default.", type: "array", items: {"type": "string"} },
-        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "euler", "euler_a", "heun", "dpm2", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "ipndm", "ipndm_v", "lcm", "ddim_trailing", "tcd", "res_multistep", "res_2s", "er_sde", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], enumLabels: ["(architecture default)", "Euler - simple, fast, deterministic", "Euler ancestral - adds noise each step (less reproducible, more variet", "Heun - 2nd-order, slower, sometimes higher quality", "DPM2 - 2nd-order, balanced", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "IPNDM", "IPNDM-V", "LCM - for LCM-finetuned models (4-8 step generation)", "DDIM trailing - required for some fine-tunes", "TCD - for TCD-finetuned models", "RES multistep - flow-model variant", "RES 2S", "ER SDE - SDE sampler (added recently)", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], default: "" },
-        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler - some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "discrete", "karras", "exponential", "ays", "gits", "sgm_uniform", "simple", "smoothstep", "kl_optimal", "lcm", "bong_tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], enumLabels: ["(architecture default)", "Discrete - uniform across model timesteps (default)", "Karras - concentrates more steps near the end, common for SDXL", "Exponential", "Align Your Steps (AYS) - auto-tuned", "GITS", "SGM uniform - for SGM-trained models", "Simple - used by Flux / SD3 flow models", "Smoothstep - Z-Image's recommended scheduler", "KL optimal", "LCM - for LCM samplers", "Bong Tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], default: "" },
+        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "euler", "euler_a", "heun", "dpm2", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "ipndm", "ipndm_v", "lcm", "ddim_trailing", "tcd", "res_multistep", "res_2s", "er_sde", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], enumLabels: ["(architecture default)", "Euler - simple, fast, deterministic", "Euler ancestral - adds noise each step (less reproducible, more variet", "Heun - 2nd-order, slower, sometimes higher quality", "DPM2 - 2nd-order, balanced", "DPM++ 2S ancestral - popular for SDXL", "DPM++ 2M - fast, good quality", "DPM++ 2M v2 - improved schedule", "IPNDM", "IPNDM-V", "LCM - for LCM-finetuned models (4-8 step generation)", "DDIM trailing - required for some fine-tunes", "TCD - for TCD-finetuned models", "RES multistep - flow-model variant", "RES 2S", "ER SDE - SDE sampler", "Euler CFG++ - improved CFG variant of Euler", "Euler ancestral CFG++ - ancestral variant of Euler CFG++", "Euler GE - graph-effective Euler variant", "DPM++ 2M SDE (leejet PR #1742)", "DPM++ 2M SDE with Brownian-tree noise (leejet PR #1743)", "LMS - linear multi-step (leejet PR #1843)"], default: "" },
+        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler - some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "discrete", "karras", "exponential", "ays", "gits", "sgm_uniform", "simple", "smoothstep", "kl_optimal", "lcm", "bong_tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], enumLabels: ["(architecture default)", "Discrete - standard sd.cpp default", "Karras - improves sample quality, especially at low step counts", "Exponential - decays sigma exponentially", "AYS - align-your-steps", "GITS - Golden-Interval Tabular Sampling", "SGM uniform - matches SGM training-time noise schedule", "Simple - basic linear", "Smoothstep - smoothly interpolated schedule", "KL-optimal - minimizes discretization error", "LCM - for LCM-finetuned models", "Bong tangent", "LTX 2 - LTX-Video 2 schedule", "Logit-normal", "Flux - Flux-family model schedule (leejet PR #1723)", "Flux 2 - Flux 2 model schedule (leejet PR #1722)", "Beta - configurable via extra_sample_args beta_alpha=/beta_beta= (leej", "Normal - alias for discrete (leejet PR #1724)"], default: "" },
         seed: { title: "Seed", description: "RNG seed for the initial noise tensor (and stochastic samplers). -1 = pick a random one each generation. Same seed + same prompt + same model = same image. Recommended: -1 for variety. Pin a specific number for A/B comparing prompt or sampler changes. Leave empty for the architecture default.", type: "number" },
         shiftedTimestep: { title: "Shifted Timestep", description: "Start the sampling schedule from a non-final timestep (used by NitroFusion and similar fast-sampling fine-tunes). 0 = standard schedule. 250-500 = NitroFusion's range. Recommended: 0 unless you're explicitly running a NitroFusion-style fine-tune. Leave empty for the architecture default.", type: "number" },
         skipLayers: { title: "SLG Skip Layers", description: "Which transformer layers to skip during the SLG unconditional pass. SD3.5 Medium uses [7, 8, 9]. Recommended: [7,8,9] for SD3.5 Medium. Other models: leave default, ignored when slg_scale=0. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
@@ -391,8 +391,8 @@ const GENERATION_OPTIONS = [
 // file was generated. See scripts/gen-sdcpp-generation-options.py.
 const KNOWN_CHOICES = {
     "cacheMode": ["easycache", "spectrum"],
-    "sampler": ["ddim_trailing", "dpm2", "dpmpp2m", "dpmpp2mv2", "dpmpp2s_a", "er_sde", "euler", "euler_a", "heun", "ipndm", "ipndm_v", "lcm", "res_2s", "res_multistep", "tcd"],
-    "scheduler": ["ays", "bong_tangent", "discrete", "exponential", "gits", "karras", "kl_optimal", "lcm", "sgm_uniform", "simple", "smoothstep"]
+    "sampler": ["ddim_trailing", "dpm++2m", "dpm++2m_sde", "dpm++2m_sde_bt", "dpm++2mv2", "dpm++2s_a", "dpm2", "er_sde", "euler", "euler_a", "euler_a_cfg_pp", "euler_cfg_pp", "euler_ge", "heun", "ipndm", "ipndm_v", "lcm", "lms", "res_2s", "res_multistep", "tcd"],
+    "scheduler": ["ays", "beta", "bong_tangent", "discrete", "exponential", "flux", "flux2", "gits", "karras", "kl_optimal", "lcm", "logit_normal", "ltx2", "normal", "sgm_uniform", "simple", "smoothstep"]
 };
 
 async function execute(config, input, context) {

+ 4 - 4
nodes/sdcpp/sdcpp-txt2vid.js

@@ -195,8 +195,8 @@ const configSchema = {
         moeBoundary: { title: "MoE Expert Boundary", description: "For Wan 2.2 MoE: fraction of total steps at which to switch from high-noise to low-noise expert. 0 = use `high_noise_steps` directly. Recommended: 0.875 (Wan 2.2 default). Leave empty for the architecture default.", type: "number" },
         negativePrompt: { title: "Negative Prompt", description: "What to push the model away from. Has effect only when `cfg_scale > 1` (CFG is what compares cond vs uncond). Ignored on architectures that don't use CFG (most flow-matching models default to `cfg_scale = 1`). Recommended: Optional. Empty is a fine default for Flux / SD3 / Z-Image. Leave empty for the architecture default.", type: "string", format: "textarea" },
         prompt: { title: "Prompt", description: "Text prompt fed to the model's text encoder (CLIP / T5 / LLM). Supports inline `<lora:name:weight>` tags - these are auto-extracted into the LoRA list before being sent to the encoder. Supports a1111-style dynamic-prompts (`{a|b|c}`) when `expand_prompt: true` is set. Recommended: Required. Leave empty for the architecture default.", type: "string", format: "textarea" },
-        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "euler", "euler_a", "heun", "dpm2", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "ipndm", "ipndm_v", "lcm", "ddim_trailing", "tcd", "res_multistep", "res_2s", "er_sde", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], enumLabels: ["(architecture default)", "Euler - simple, fast, deterministic", "Euler ancestral - adds noise each step (less reproducible, more variet", "Heun - 2nd-order, slower, sometimes higher quality", "DPM2 - 2nd-order, balanced", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "IPNDM", "IPNDM-V", "LCM - for LCM-finetuned models (4-8 step generation)", "DDIM trailing - required for some fine-tunes", "TCD - for TCD-finetuned models", "RES multistep - flow-model variant", "RES 2S", "ER SDE - SDE sampler (added recently)", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], default: "" },
-        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler - some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "discrete", "karras", "exponential", "ays", "gits", "sgm_uniform", "simple", "smoothstep", "kl_optimal", "lcm", "bong_tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], enumLabels: ["(architecture default)", "Discrete - uniform across model timesteps (default)", "Karras - concentrates more steps near the end, common for SDXL", "Exponential", "Align Your Steps (AYS) - auto-tuned", "GITS", "SGM uniform - for SGM-trained models", "Simple - used by Flux / SD3 flow models", "Smoothstep - Z-Image's recommended scheduler", "KL optimal", "LCM - for LCM samplers", "Bong Tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], default: "" },
+        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "euler", "euler_a", "heun", "dpm2", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "ipndm", "ipndm_v", "lcm", "ddim_trailing", "tcd", "res_multistep", "res_2s", "er_sde", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], enumLabels: ["(architecture default)", "Euler - simple, fast, deterministic", "Euler ancestral - adds noise each step (less reproducible, more variet", "Heun - 2nd-order, slower, sometimes higher quality", "DPM2 - 2nd-order, balanced", "DPM++ 2S ancestral - popular for SDXL", "DPM++ 2M - fast, good quality", "DPM++ 2M v2 - improved schedule", "IPNDM", "IPNDM-V", "LCM - for LCM-finetuned models (4-8 step generation)", "DDIM trailing - required for some fine-tunes", "TCD - for TCD-finetuned models", "RES multistep - flow-model variant", "RES 2S", "ER SDE - SDE sampler", "Euler CFG++ - improved CFG variant of Euler", "Euler ancestral CFG++ - ancestral variant of Euler CFG++", "Euler GE - graph-effective Euler variant", "DPM++ 2M SDE (leejet PR #1742)", "DPM++ 2M SDE with Brownian-tree noise (leejet PR #1743)", "LMS - linear multi-step (leejet PR #1843)"], default: "" },
+        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler - some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "discrete", "karras", "exponential", "ays", "gits", "sgm_uniform", "simple", "smoothstep", "kl_optimal", "lcm", "bong_tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], enumLabels: ["(architecture default)", "Discrete - standard sd.cpp default", "Karras - improves sample quality, especially at low step counts", "Exponential - decays sigma exponentially", "AYS - align-your-steps", "GITS - Golden-Interval Tabular Sampling", "SGM uniform - matches SGM training-time noise schedule", "Simple - basic linear", "Smoothstep - smoothly interpolated schedule", "KL-optimal - minimizes discretization error", "LCM - for LCM-finetuned models", "Bong tangent", "LTX 2 - LTX-Video 2 schedule", "Logit-normal", "Flux - Flux-family model schedule (leejet PR #1723)", "Flux 2 - Flux 2 model schedule (leejet PR #1722)", "Beta - configurable via extra_sample_args beta_alpha=/beta_beta= (leej", "Normal - alias for discrete (leejet PR #1724)"], default: "" },
         seed: { title: "Seed", description: "RNG seed for the initial noise tensor (and stochastic samplers). -1 = pick a random one each generation. Same seed + same prompt + same model = same image. Recommended: -1 for variety. Pin a specific number for A/B comparing prompt or sampler changes. Leave empty for the architecture default.", type: "number" },
         shiftedTimestep: { title: "Shifted Timestep", description: "Start the sampling schedule from a non-final timestep (used by NitroFusion and similar fast-sampling fine-tunes). 0 = standard schedule. 250-500 = NitroFusion's range. Recommended: 0 unless you're explicitly running a NitroFusion-style fine-tune. Leave empty for the architecture default.", type: "number" },
         skipLayers: { title: "SLG Skip Layers", description: "Which transformer layers to skip during the SLG unconditional pass. SD3.5 Medium uses [7, 8, 9]. Recommended: [7,8,9] for SD3.5 Medium. Other models: leave default, ignored when slg_scale=0. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
@@ -395,8 +395,8 @@ const GENERATION_OPTIONS = [
 // file was generated. See scripts/gen-sdcpp-generation-options.py.
 const KNOWN_CHOICES = {
     "cacheMode": ["easycache", "spectrum"],
-    "sampler": ["ddim_trailing", "dpm2", "dpmpp2m", "dpmpp2mv2", "dpmpp2s_a", "er_sde", "euler", "euler_a", "heun", "ipndm", "ipndm_v", "lcm", "res_2s", "res_multistep", "tcd"],
-    "scheduler": ["ays", "bong_tangent", "discrete", "exponential", "gits", "karras", "kl_optimal", "lcm", "sgm_uniform", "simple", "smoothstep"]
+    "sampler": ["ddim_trailing", "dpm++2m", "dpm++2m_sde", "dpm++2m_sde_bt", "dpm++2mv2", "dpm++2s_a", "dpm2", "er_sde", "euler", "euler_a", "euler_a_cfg_pp", "euler_cfg_pp", "euler_ge", "heun", "ipndm", "ipndm_v", "lcm", "lms", "res_2s", "res_multistep", "tcd"],
+    "scheduler": ["ays", "beta", "bong_tangent", "discrete", "exponential", "flux", "flux2", "gits", "karras", "kl_optimal", "lcm", "logit_normal", "ltx2", "normal", "sgm_uniform", "simple", "smoothstep"]
 };
 
 async function execute(config, input, context) {

+ 10 - 0
webui/src/api/workflows.ts

@@ -523,6 +523,8 @@ export interface ExecutionDetail {
   // engine that doesn't send the field yet - both read the same way, so
   // this is never treated as an error condition.
   singleNodeIteration?: number
+  /** See ExecutionListItem.toleratedErrorCount. */
+  toleratedErrorCount?: number
 }
 
 export interface ExecutionListItem {
@@ -543,6 +545,14 @@ export interface ExecutionListItem {
    * listing treats "absent" as top level rather than filtering on it.
    */
   parentExecutionId?: string
+  /**
+   * How many failures the run swallowed and still finished. A loop with
+   * Continue On Error turned on tolerates a bad item, which is usually what
+   * you want - but the run then reports "completed" and there is otherwise
+   * nothing on screen to say that 3 of 10 items failed. Absent or 0 means a
+   * genuinely clean run.
+   */
+  toleratedErrorCount?: number
 }
 
 // Transform backend execution list item to frontend format. Spread first, as

+ 22 - 15
webui/src/components/workflow/ExecutionListPanel.tsx

@@ -1,19 +1,6 @@
 import { useState, useMemo } from 'react'
 import { useQuery } from '@tanstack/react-query'
-import {
-  X,
-  Clock,
-  CheckCircle,
-  XCircle,
-  AlertCircle,
-  Play,
-  Eye,
-  Pin,
-  Loader2,
-  ChevronLeft,
-  ChevronRight,
-  Filter,
-} from 'lucide-react'
+import { X, Clock, CheckCircle, XCircle, AlertCircle, Play, Eye, Pin, Loader2, ChevronLeft, ChevronRight, Filter, AlertTriangle } from 'lucide-react'
 import { executionsApi, type ExecutionListItem, type ExecutionDetail } from '../../api/workflows'
 import { useExecutionListUpdates } from '../../hooks/useExecutionListUpdates'
 
@@ -79,6 +66,23 @@ function parseDurationToMs(value: string): number | null {
   return num
 }
 
+// A run that finished while swallowing failures. "Completed" on its own reads
+// as "nothing went wrong", and a loop with Continue On Error turned on will
+// happily finish after 3 of 10 items failed - which is the behaviour asked for,
+// but not something the list should keep to itself.
+function ToleratedBadge({ count }: { count?: number }) {
+  if (!count) return null
+  return (
+    <span
+      title={`${count} node ${count === 1 ? 'failure was' : 'failures were'} tolerated - the run finished anyway. Open it to see which.`}
+      className="shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300"
+    >
+      <AlertTriangle className="w-2.5 h-2.5" />
+      {count} tolerated
+    </span>
+  )
+}
+
 function StatusBadge({ status }: { status: string }) {
   const config: Record<string, { icon: typeof CheckCircle; color: string; bg: string }> = {
     completed: { icon: CheckCircle, color: 'text-green-600 dark:text-green-400', bg: 'bg-green-100 dark:bg-green-900/30' },
@@ -339,7 +343,10 @@ export function ExecutionListPanel({
                 className="p-4 hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors"
               >
                 <div className="flex items-start justify-between mb-1">
-                  <StatusBadge status={exec.status} />
+                  <div className="flex items-center gap-1.5 min-w-0">
+                    <StatusBadge status={exec.status} />
+                    <ToleratedBadge count={exec.toleratedErrorCount} />
+                  </div>
                   <div className="text-right">
                     <div className="text-xs text-gray-500 dark:text-gray-400">
                       {formatRelativeTimestamp(exec.startedAt)}

+ 22 - 1
webui/src/components/workflow/ExecutionResultsPanel.tsx

@@ -1,6 +1,6 @@
 import { useEffect, useMemo, useState } from 'react'
 import { Node, Edge } from 'reactflow'
-import { X, ChevronRight, ChevronDown, Loader2, CheckCircle, XCircle, SkipForward, Ban, Repeat, FlaskConical, RotateCw } from 'lucide-react'
+import { X, ChevronRight, ChevronDown, Loader2, CheckCircle, XCircle, SkipForward, Ban, Repeat, FlaskConical, RotateCw, AlertTriangle } from 'lucide-react'
 import { NodeExecutionState } from './WorkflowNode'
 import { NodeOutputView } from './NodeOutputView'
 import { computeNodeToLoopMap } from '../../utils/loopMembership'
@@ -19,6 +19,9 @@ interface ExecutionState {
   // record) means "fall back to the flat per-node list", which is exactly
   // how this panel behaved before grouping existed.
   rawNodeExecutions?: NodeExecution[]
+  // Failures the run swallowed and finished anyway. Present only for a pinned
+  // execution; a live run has no total until it ends.
+  toleratedErrorCount?: number
   // The targeted node's id when this run was a "Test this node" run - see
   // WorkflowEditorPage. Drives the banner below; absent means a normal run.
   singleNodeTarget?: string
@@ -743,6 +746,24 @@ export function ExecutionResultsPanel({
           folded into that list and never left ambiguous about which one is
           which. Gone as soon as it's dismissed or another re-run replaces it;
           nothing here is ever written back into the execution above. */}
+      {!!executionState.toleratedErrorCount && (
+        <div className="flex-shrink-0 px-4 py-2 border-b border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20">
+          <div className="flex items-start gap-2 text-xs text-amber-900 dark:text-amber-200">
+            <AlertTriangle className="w-4 h-4 shrink-0 mt-0.5 text-amber-600 dark:text-amber-400" />
+            <div>
+              <span className="font-medium">
+                This run finished, but {executionState.toleratedErrorCount}{' '}
+                {executionState.toleratedErrorCount === 1 ? 'failure was' : 'failures were'} tolerated.
+              </span>
+              <div className="mt-0.5 text-amber-700 dark:text-amber-300">
+                A loop with Continue On Error carries on past a bad item, so the run reads as
+                completed. The failed nodes are marked below.
+              </div>
+            </div>
+          </div>
+        </div>
+      )}
+
       {rerunResult && (() => {
         const node = nodes.find(n => n.id === rerunResult.nodeId)
         const label = node?.data.config?._customLabel || node?.data.label || rerunResult.nodeId

+ 5 - 0
webui/src/pages/WorkflowEditorPage.tsx

@@ -593,6 +593,11 @@ function WorkflowEditorInner() {
         rawNodeExecutions: sortedExecutions,
         singleNodeTarget: pinnedExecution.singleNodeTarget,
         singleNodeIteration: pinnedExecution.singleNodeIteration,
+        // Carried through so the panel can say the run swallowed failures.
+        // Computed by the engine, not re-derived here - "tolerated" has a
+        // precise meaning there (a node that failed while the run finished, or
+        // one that swallowed its own failure) and two definitions would drift.
+        toleratedErrorCount: pinnedExecution.toleratedErrorCount,
       }
     }
     return executionState