mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
`filter_applicable` dry-ran STRUCTURE only. In the live F4 Edit four intents came back "applicable, 0 rejected", the batch then died at `apply_repair`'s preflight, and the one corrective re-prompt could never fire -- it is keyed on `rejected` being non-empty. Every re-approval re-sent a byte-identical prompt and got a byte-identical batch back. `filter_applicable` now returns its dry-run graph (`DryRun` NamedTuple) after running it through the shared `heal_nodes_for_preflight` -- the same normalizers, at the same point in the sequence, the write chokepoint runs. A dry run that healed less would report a defect the write was going to fix; one that healed more would pass a batch the write refuses. It also skips the intents the port drops as already-present, via a predicate both now share, so a re-sent duplicate `create_node` no longer burns the re-prompt. `preflight.vet_intents` then puts that graph through `validate_node_config` and feeds any NEW problem into the existing re-prompt, quoting the engine's own message. `new_preflight_problems` is the one definition of that rule, shared with `apply_repair` so the dry run and the write cannot answer differently. It is keyed on the set of pydantic error LOCATIONS per node, which is the only key that gets both halves right. Node identity exempts an already-refused node from every later verdict -- and since a Fix repair targets a node that is by definition already invalid, that would leave `propose_repair` with no node-data coverage on its own culprit. Message text is too fine: pydantic quotes a truncated repr of the input, so filling one of three missing fields rewrites the other two messages and an exact-text diff refuses the repair that improved the node. The refusal text is rebuilt from a credential-redacted copy of the node. The verdict still comes from the real one; only the words the model is shown are withheld, because a model-level pydantic error echoes the node's whole `data` and that is where a live bearer token sits. The retry budget is unchanged: one corrective re-prompt, then Edit's existing fallback to the first attempt's applicable intents and Fix's `_no_fix()`. Also folds in the Task 1 review: the generator's postprocess calls the shared heal set plus `derive_if_else_var_types` instead of re-spelling three normalizers inline, and the valid-operator no-op sweep covers the list-operator container so `FilterOperator` gets its own check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>