Follow-up to the reply-language feature. The update_model action commits its
"Model changed to <name>" notice directly via compare_and_advance, bypassing
Runner._commit and thus the Env.localize_items hook, so that one notice stayed
English. Localize it in the service (when the session has a detected
reply_language) with a "Model changed to {name}" catalog template that
translates the frame and re-inserts the model name verbatim.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the reply-language feature. `_model_or_none` was private to
LlmBuilderAgent, so the advance-task wiring reached it via a fragile
`getattr(agent, "_model_or_none", lambda: None)`. Make it part of the
DifyBuilderAgent protocol (typed `object | None` to keep the pure port free of
any model-runtime import) and implement it on every conforming agent
(LlmBuilderAgent renames the private method public; PlaceholderAgent and the
test doubles return None). The Localizer wiring now uses `agent.model_or_none`
directly. Also drops the useless-return ruff nit in the wiring test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The build/edit test handlers wrapped run_draft in a bare, non-logging
`except Exception` that turned any launch-time failure into a blank failed
run (dify_run_id="", per_node=[], no error). Diagnose then had nothing to
act on and the repair loop ran blind — the prod symptom where a stale test
input (ValueError "<var> is required in input form") produced an
undiagnosable await_repair loop.
Now the launch error is logged, captured, and persisted so diagnose/routing
can act on it:
- Run.error field + dify_builder_runs.error column (migration a9cc8d552db4)
- handlers_build/edit: logger.exception + launch_error_text(exc) -> Run.error
- is_input_failure() also inspects Run.error (+ "in input form" signal), so a
stale-input launch failure routes to the testdata gate, not blind repair
- diagnose/_degraded_diagnosis surface the launch error when no node executed
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render change sets directly in plan-style cards with affected node names,
IDs, and change details. Capture node metadata for proposed and applied
changes and persist it with conversation history.
Remove the view_changes action and expand/collapse state. Update generated
contracts, translations, and regression coverage.
Validation: 112 frontend tests and 742 backend unit tests passed.
Integrates the FE team's live-progress + conversation-streaming rework
(durable progress events, repository layer, snapshot-consistent reads,
idempotent message retries, EDIT_PUBLISH->EDIT_COMPLETE) and reconciles its
interrupted-step recovery model with the reconciler already on
feat/dify-builder.
Combined recovery model:
- interrupted (crash mid working-step, lock released, nothing durably
recorded) -> Retry (recovery_continue, re-runs the handler) + Start over
- FAILED (handler raised, recorded by runner.fail) -> Restart only
- a Retry that itself raises lands in FAILED -> Restart (no wedge)
Kept from the PR: EDIT_PUBLISH working / EDIT_COMPLETE terminal, snapshot
refresh in apply_recovery_action, paused reset. Kept from the reconciler:
Retry on interrupted, state-aware access tier (EDIT_PUBLISH added to the
publish tier), apply_repair idempotency filter.
680 dify_builder unit tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a session crashes mid working-step (e.g. build.publish; the async advance
raised and released the lock), it was a locked dead-end: working states surface
no UI actions and the composer locked with no way forward.
- Surface a RecoveryRef offer (can_continue=Retry, can_restart=Start over,
recovery_class="") on an interrupted working-state view -- built directly so a
real drift class still wins.
- Allow recovery_continue / recovery_restart at an interrupted working state in
_internal_action_allowed. recovery_continue returns the working state
(non-settled) so the runner re-runs the stalled handler; recovery_restart
resets working fields and re-enters the entry state -- both reuse existing
recovery machinery, no new action kinds / no runner change.
Adds unit tests (offer surfaced, allowance) + regression tests (recovery_continue
re-runs handle_publish and advances; recovery_restart resets to entry state;
repeat-failure re-surfaces the offer with no wedge).
This reconciler was reviewed as a standalone plan on the pre-merge base; here it
is re-applied onto feat/dify-builder after PR #41677 merged in the reworked
service.py + _internal_action_allowed gate (which makes the allowance necessary).
The apply_repair idempotency + delete-guard commits (safe fix.apply Retry) land
alongside it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>