Delayed signals between ModelingToolkit components

What breaks with `sth.x ~ lv.x`, and what to write instead

The fix

mtkcompile alias-eliminates sth₊x into an observed equation, and MTK only lowers a delay to a history lookup when the delayed variable is an unknown of the compiled system. So sth.x ~ lv.x can't work. Don't create the alias.

Three changes. mtkcompile, SDDEProblem and solve(prob, ImplicitEM()) stay as posted.

(1) The component that owns the signal declares it callable. x(..) rather than x(t), applied as x(t) in its own equations. This is the part that keeps lv₊x an unknown.

 @component function StochasticLV(;name)
-    @variables x(t) = 0.9 y(t) = 0.9
+    @variables x(..) y(t) = 0.9
     ...
     eqs = [
-        D(x) ~ α * x - β * x * y + η
-        D(y) ~ -γ * y + δ * x * y
+        D(x(t)) ~ α * x(t) - β * x(t) * y + η
+        D(y)    ~ -γ * y + δ * x(t) * y
     ]
-    System(eqs, t; name)
+    System(eqs, t; name, initial_conditions = [x(t) => 0.9])
 end

x(..) can't carry an inline = 0.9, so the initial condition moves to initial_conditions.

(2) The consumer takes the signal as an argument, and the connection equation goes away.

-@component function SomethingElse(;name, τ)
-    @variables z(t) = 0 x(..)
+@component function SomethingElse(;name, τ, u)
+    @variables z(t) = 0
     @parameters θ = 0.1 τ = τ
     @brownians ξ
     eqs = [
-        D(z) ~ -θ * x(t - τ) + ξ
+        D(z) ~ -θ * u(t - τ) + ξ
     ]
     System(eqs, t; name)
 end

+callable(v) = operation(ModelingToolkit.unwrap(v))
+
 @named lv = StochasticLV()
-@named sth = SomethingElse(τ = 3)
+@named sth = SomethingElse(τ = 3, u = callable(lv.x))

-connection_eqs = [
-    sth.x ~ lv.x
-]
-@named connected_model = System(connection_eqs, t, [], []; systems = [lv, sth])
+@named connected_model = System(Equation[], t, [], []; systems = [lv, sth])

lv.x auto-applies at t and hands back lv₊x(t), so callable is there to recover the bare operator, which is the thing you can call at t - τ.

(3) Add using DelayDiffEq. Nothing to do with the modelling bug. using DifferentialEquations doesn't load DelayDiffEq, so as posted there is no SDDE integrator available and even the fixed model dies at the solve line with SciMLBase.NoDefaultAlgorithmError(). Don't substitute StochasticDelayDiffEq; it can no longer co-resolve with the current stack (last section).

 using ModelingToolkit
 using DifferentialEquations
 using ModelingToolkit: t_nounits as t, D_nounits as D
 using StochasticDiffEq
+using DelayDiffEq

The original

The MRE from issue #4985, filed by bebrunn on 2026-08-17 and still open:

using ModelingToolkit
using DifferentialEquations
using ModelingToolkit: t_nounits as t, D_nounits as D
using StochasticDiffEq

@component function StochasticLV(;name)
    @variables x(t) = 0.9 y(t) = 0.9
    @parameters begin
        α = 2/3
        β = 4/3
        γ = 1
        δ = 1
    end
    @brownians η
    eqs = [
        D(x) ~ α * x - β * x * y + η
        D(y) ~ -γ * y + δ * x * y
    ]
    System(eqs, t; name)
end

@component function SomethingElse(;name, τ)
    @variables z(t) = 0 x(..)
    @parameters θ = 0.1 τ = τ
    @brownians ξ
    eqs = [
        D(z) ~ -θ * x(t - τ) + ξ
    ]
    System(eqs, t; name)
end

@named lv = StochasticLV()
@named sth = SomethingElse(τ = 3)

connection_eqs = [
    sth.x ~ lv.x
]

@named connected_model = System(connection_eqs, t, [], []; systems = [lv, sth])
connected = mtkcompile(connected_model)
prob = SDDEProblem(connected, [], (0.0, 1.0))
sol = solve(prob, ImplicitEM())

It fails at the SDDEProblem line:

ArgumentError: sth₊x(-sth₊τ + t) is present in the system but
               sth₊x(-sth₊τ + t) is not an unknown.

The intent reads fine. SomethingElse declares a callable input x(..), and the connection equation binds it to the prey population. It just doesn't survive mtkcompile.

The issue also notes, correctly, that "calling mtkcompile() on the connected_model removes the sth₊x(t) unknown". That is the whole bug, and the cells below confirm it.

Everything here was re-run against the versions in the issue's manifest: ModelingToolkit 11.39.0, ModelingToolkitBase 1.65.0, ModelingToolkitTearing 1.20.5, StochasticDiffEq 7.1.4, StochasticDiffEqCore 2.0.6, DelayDiffEq 6.1.2, DifferentialEquations 8.0.3. The reporter is on MTK 11.38.0 / MTKBase 1.60.0 / SDECore 2.0.5, and behaviour is identical.

using ModelingToolkit
using DifferentialEquations
using ModelingToolkit: t_nounits as t, D_nounits as D
using StochasticDiffEq
using DelayDiffEq          # (3) required: `using DifferentialEquations` does not load it,
                           #     and SDDE integration lives here, not StochasticDelayDiffEq
import Pkg

VERSIONS = let names = ("ModelingToolkit", "ModelingToolkitBase", "ModelingToolkitTearing",
                        "StochasticDiffEq", "StochasticDiffEqCore", "DelayDiffEq",
                        "StochasticDelayDiffEq")
    deps = filter(p -> p.second.name in names, Pkg.dependencies())
    sort([p.second.name => string(p.second.version) for p in deps])
end
6-element Vector{Pair{String, String}}:
            "DelayDiffEq" => "6.1.2"
        "ModelingToolkit" => "11.39.0"
    "ModelingToolkitBase" => "1.65.0"
 "ModelingToolkitTearing" => "1.20.5"
       "StochasticDiffEq" => "7.1.4"
   "StochasticDiffEqCore" => "2.0.6"

The fixed version, running

Same physics, same parameters, same names. Only the three changes above. Changed lines are marked # (1), # (2), # (3).

# (2) `lv.x` auto-applies at `t` and gives back `lv₊x(t)`. This recovers the bare operator
#     so it can be called at `t - τ`.
callable(v) = operation(ModelingToolkit.unwrap(v))

@component function StochasticLV(; name)
    @variables x(..) y(t) = 0.9                                  # (1) x(..) not x(t) = 0.9
    @parameters begin
        α = 2 / 3
        β = 4 / 3
        γ = 1
        δ = 1
    end
    @brownians η
    eqs = [
        D(x(t)) ~ α * x(t) - β * x(t) * y + η                    # (1) x -> x(t)
        D(y) ~ -γ * y + δ * x(t) * y                             # (1) x -> x(t)
    ]
    System(eqs, t; name, initial_conditions = [x(t) => 0.9])     # (1) default moves here
end

@component function SomethingElse(; name, τ, u)                  # (2) takes `u`
    @variables z(t) = 0                                          # (2) local x(..) deleted
    @parameters θ = 0.1 τ = τ
    @brownians ξ
    eqs = [
        D(z) ~ -θ * u(t - τ) + ξ                                 # (2) delay `u`, not `x`
    ]
    System(eqs, t; name)
end

@named lv = StochasticLV()
@named sth = SomethingElse= 3, u = callable(lv.x))             # (2) signal injected

# (2) `connection_eqs = [sth.x ~ lv.x]` is gone. That alias was the whole problem.
@named connected_model = System(Equation[], t, [], []; systems = [lv, sth])
connected = mtkcompile(connected_model)

(equations = equations(connected),
 unknowns = unknowns(connected),
 observed = observed(connected))
(equations = Symbolics.Equation[Differential(t, 1)(sth₊z(t)) ~ -lv₊x(-sth₊τ + t)*sth₊θ, Differential(t, 1)(lv₊y(t)) ~ -lv₊y(t)*lv₊γ + lv₊x(t)*lv₊y(t)*lv₊δ, Differential(t, 1)(lv₊x(t)) ~ lv₊x(t)*lv₊α - lv₊x(t)*lv₊y(t)*lv₊β], unknowns = SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymbolicUtils.SymReal}[sth₊z(t), lv₊y(t), lv₊x(t)], observed = Symbolics.Equation[])
# Exactly as posted. No constant_lags, no MethodOfSteps wrapper.
prob = SDDEProblem(connected, [], (0.0, 1.0))
sol = solve(prob, ImplicitEM())

(retcode = sol.retcode, steps = length(sol.t), final = sol.u[end])
(retcode = SciMLBase.ReturnCode.Success, steps = 175, final = [1.755910429359539, 0.7206220094566499, 1.0273877364780768])

Checks

The cell below feeds a history function reporting lv₊x == 7.0 in the past. If the delay really lowered to a history lookup, dz comes out at -θ·7 = -0.7. Default history is constant extrapolation of u0, so lv₊x is 0.9 for t < 0.

du = zeros(3)
prob.f(du, prob.u0, (p, tt) -> [0.0, 0.9, 7.0], prob.p, 0.0)

(unknown_order = unknowns(connected),
 u0 = prob.u0,
 du = du,
 dz_expected = -0.1 * 7.0,
 dz_matches = du[1]  -0.1 * 7.0,
 noise = ModelingToolkit.get_noise_eqs(connected))
(unknown_order = SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymbolicUtils.SymReal}[sth₊z(t), lv₊y(t), lv₊x(t)], u0 = [0.0, 0.9, 0.9], du = [-0.7000000000000001, -0.08999999999999997, -0.4800000000000001], dz_expected = -0.7000000000000001, dz_matches = true, noise = SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymbolicUtils.SymReal}[1, 0, 1])

Past the delay

On (0, 1) the delay never reaches into the solved interval, so z does nothing. Running past τ = 3 shows the coupling. The posted model puts additive noise of coefficient 1 on x, which is large next to x ≈ 0.9, so the trajectory leaves the deterministic LV cycle quickly. That's the model as written, not a solver artifact.

long_prob = SDDEProblem(connected, [], (0.0, 12.0))
long_sol = solve(long_prob, ImplicitEM(); seed = 20250817, saveat = 0.02)

echart(
    series(:line, long_sol.t, long_sol[connected.lv.x]; name = "lv₊x (prey)"),
    series(:line, long_sol.t, long_sol[connected.lv.y]; name = "lv₊y (predator)"),
    series(:line, long_sol.t, long_sol[connected.sth.z]; name = "sth₊z (delay-driven)",
        yAxisIndex = 1);
    yAxis = [(name = "LV",), (name = "z",)],
    xAxis = (name = "t",),
    legend = true,
    title = "Delayed coupling across components (τ = 3)",
    height = 420,
    zoom = true)

Where this pattern stops working

Only siblings under a common parent. Two things to check before relying on it.

Nesting gives you a wrong model, quietly

The injected reference survives one level of namespacing and no more. Put the consumer a level deeper and namespacing rewrites the operator to mid₊lv₊x, which is neither an unknown nor observed. mtkcompile still succeeds. No scope annotation changes this.

@component function Sink_nested(; name, τ, u)
    @variables z(t) = 0
    @parameters θ = 0.1 τ = τ
    System([D(z) ~ -θ * u(t - τ)], t; name)
end

# Build `lvn` plus a `midn` wrapper holding `sthn`, so the consumer sits two levels deep.
function build_nested(scope)
    @named lvn = StochasticLV()
    @named sthn = Sink_nested= 3, u = callable(scope(lvn.x)))
    @named midn = System(Equation[], t, [], []; systems = [sthn])
    @named nested = System(Equation[], t, [], []; systems = [lvn, midn])
    mtkcompile(nested)
end

nesting_results = map((:none => identity,
                       :ParentScope => ModelingToolkit.ParentScope,
                       :GlobalScope => ModelingToolkit.GlobalScope)) do (label, scope)
    label => try
        string(equations(build_nested(scope))[1])
    catch e
        first(split(sprint(showerror, e), "\n"))
    end
end
(:none => "Differential(t, 1)(midn₊sthn₊z(t)) ~ -midn₊lvn₊x(-midn₊sthn₊τ + t)*midn₊sthn₊θ", :ParentScope => "Differential(t, 1)(midn₊sthn₊z(t)) ~ -midn₊lvn₊x(-midn₊sthn₊τ + t)*midn₊sthn₊θ", :GlobalScope => "Differential(t, 1)(midn₊sthn₊z(t)) ~ -midn₊lvn₊x(-midn₊sthn₊τ + t)*midn₊sthn₊θ")
# The delayed name is in the equations but nowhere in the system, and the failure is the
# same silent-codegen bug as the original, reached by a different route.
nested_sys = build_nested(identity)

phantom_rhs_error = let np = SDDEProblem(nested_sys, [], (0.0, 1.0); build_initializeprob = false)
    du = zeros(3)
    try
        np.f(du, np.u0, (p, tt) -> np.u0, np.p, 0.0)
        "no error (unexpected)"
    catch e
        first(split(sprint(showerror, e), "\n"))
    end
end

(delayed_name_in_equation = "midn₊lvn₊x",
 unknowns = unknowns(nested_sys),
 observed = observed(nested_sys),
 mtkcompile_complained = false,
 rhs_error = phantom_rhs_error)
(delayed_name_in_equation = "midn₊lvn₊x", unknowns = SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymbolicUtils.SymReal}[midn₊sthn₊z(t), lvn₊y(t), lvn₊x(t)], observed = Symbolics.Equation[], mtkcompile_complained = false, rhs_error = "UndefVarError: `midn₊lvn₊x` not defined in `ModelingToolkitBase`")

irreducible doesn't save the connection

Keeping sth.x ~ lv.x and marking the input [irreducible = true] so it survives alias elimination looks promising. MTK then wants a derivative for it.

# Keep the alias, mark it irreducible so it stays an unknown. MTK asks for D(sth₊x) instead.
irreducible_result = try
    @component function LV_plain2(; name)
        @variables x(t) = 0.9 y(t) = 0.9
        @parameters α = 2 / 3 β = 4 / 3 γ = 1 δ = 1
        @brownians η
        System([D(x) ~ α * x - β * x * y + η
                D(y) ~ -γ * y + δ * x * y], t; name)
    end
    @component function Sink_irred(; name, τ)
        @variables z(t) = 0 x(..) [irreducible = true]
        @parameters θ = 0.1 τ = τ
        @brownians ξ
        System([D(z) ~ -θ * x(t - τ) + ξ], t; name)
    end
    @named lvi = LV_plain2()
    @named sthi = Sink_irred= 3)
    @named irred = System([sthi.x ~ lvi.x], t, [], []; systems = [lvi, sthi])
    string(equations(mtkcompile(irred)))
catch e
    first(split(sprint(showerror, e), "\n"))
end
"Differential(t, 1)(sthi₊x(t)) isn't handled."

Why the original fails

Diagnosis from here down. The definitions below are the posted ones with an _orig suffix so they can share a namespace with the working versions above.

@component function StochasticLV_orig(; name)
    @variables x(t) = 0.9 y(t) = 0.9
    @parameters begin
        α = 2 / 3
        β = 4 / 3
        γ = 1
        δ = 1
    end
    @brownians η
    eqs = [
        D(x) ~ α * x - β * x * y + η
        D(y) ~ -γ * y + δ * x * y
    ]
    System(eqs, t; name)
end

@component function SomethingElse_orig(; name, τ)
    @variables z(t) = 0 x(..)
    @parameters θ = 0.1 τ = τ
    @brownians ξ
    eqs = [
        D(z) ~ -θ * x(t - τ) + ξ
    ]
    System(eqs, t; name)
end

@named lv_orig = StochasticLV_orig()
@named sth_orig = SomethingElse_orig= 3)

# `sth.x` auto-applies at `t`, so this connection equation is well formed
connection_eqs = [sth_orig.x ~ lv_orig.x]
@named broken_model = System(connection_eqs, t, [], []; systems = [lv_orig, sth_orig])

equations(broken_model)
4-element Vector{Symbolics.Equation}:
 sth_orig₊x(t) ~ lv_orig₊x(t)
 Differential(t, 1)(lv_orig₊x(t)) ~ lv_orig₊η + lv_orig₊x(t)*lv_orig₊α - lv_orig₊x(t)*lv_orig₊y(t)*lv_orig₊β
 Differential(t, 1)(lv_orig₊y(t)) ~ -lv_orig₊y(t)*lv_orig₊γ + lv_orig₊x(t)*lv_orig₊y(t)*lv_orig₊δ
 Differential(t, 1)(sth_orig₊z(t)) ~ sth_orig₊ξ - sth_orig₊x(-sth_orig₊τ + t)*sth_orig₊θ
# mtkcompile succeeds. That's the first surprise.
broken = mtkcompile(broken_model)

(equations = equations(broken),
 unknowns = unknowns(broken),
 observed = observed(broken))
(equations = Symbolics.Equation[Differential(t, 1)(sth_orig₊z(t)) ~ -sth_orig₊x(-sth_orig₊τ + t)*sth_orig₊θ, Differential(t, 1)(lv_orig₊y(t)) ~ -lv_orig₊y(t)*lv_orig₊γ + lv_orig₊x(t)*lv_orig₊y(t)*lv_orig₊δ, Differential(t, 1)(lv_orig₊x(t)) ~ lv_orig₊x(t)*lv_orig₊α - lv_orig₊x(t)*lv_orig₊y(t)*lv_orig₊β], unknowns = SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymbolicUtils.SymReal}[sth_orig₊z(t), lv_orig₊y(t), lv_orig₊x(t)], observed = Symbolics.Equation[sth_orig₊x(t) ~ lv_orig₊x(t)])

Root cause

Three things in the cell above:

equationD(sth₊z) ~ -sth₊x(-sth₊τ + t) * sth₊θ, still naming sth₊x
unknownssth₊x isn't there
observedsth₊x(t) ~ lv₊x(t), alias-eliminated

Alias elimination rewrote sth₊x(t) and left sth₊x(t - τ) alone.

That's fatal because of how a delay gets lowered. delay_to_function (ModelingToolkitBase/src/systems/codegen_utils.jl) turns v(t - τ) into a history lookup h(p, t - τ)[i], but only for v ∈ unknowns(sys):

function delay_to_function(expr, iv, sts, ps, h; param_arg = MTKPARAMETERS_ARG)
    if isdelay(expr, iv)
        v = operation(expr)
        time = arguments(expr)[1]
        idx = sts[v]            # sts = Dict(operation(s) => i for s in unknowns(sys))
        return term(getindex, h(param_arg, time), idx, type = Real)
    ...

sth₊x is an observed alias rather than an unknown, so the delay term is orphaned. Everything below is that one fact showing up in two places.

Symptom 1: the error you see

SDDEProblem builds an initialization system for consistent initial conditions. That system is time-independent (get_iv(isys) === nothing) and still carries the delay term.

isys = ModelingToolkit.generate_initializesystem(broken)

init_iv = ModelingToolkit.get_iv(isys)          # nothing, so time-independent
delay_terms = filter(eq -> occursin("sth_orig₊x(-", string(eq)), equations(isys))

(init_iv = init_iv, delay_still_present = delay_terms)
(init_iv = nothing, delay_still_present = Symbolics.Equation[sth_orig₊zˍt(t) ~ -sth_orig₊x(-sth_orig₊τ + t)*sth_orig₊θ])
# SDDEProblem -> InitializationProblem -> mtkcompile -> TearingState
try
    SDDEProblem(broken, [], (0.0, 1.0))
    println("no error (unexpected)")
catch e
    show_trimmed(e, catch_backtrace())
end
ArgumentError: sth_orig₊x(-sth_orig₊τ + t) is present in the system but sth_orig₊x(-sth_orig₊τ + t) is not an unknown.
Stacktrace (29 of 49 frames):
  [$i] #TearingState#2
        @ /Users/kburke/.julia/packages/ModelingToolkitTearing/ZusxN/src/tearingstate.jl:298
  [$i] TearingState
        @ /Users/kburke/.julia/packages/ModelingToolkitTearing/ZusxN/src/tearingstate.jl:149
  [$i] #__mtkcompile#153
        @ /Users/kburke/.julia/packages/ModelingToolkit/YCGiA/src/systems/systems.jl:34
  [$i] __mtkcompile
        @ /Users/kburke/.julia/packages/ModelingToolkit/YCGiA/src/systems/systems.jl:23
  [$i] #_mtkcompile#1246
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/systems.jl:155
  [$i] _mtkcompile
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/systems.jl:128
  [$i] #mtkcompile#1245
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/systems.jl:100
  [$i] mtkcompile
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/systems.jl:84
  [$i] #mtkcompile_initialization_system#943
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/initializationproblem.jl:197
  [$i] mtkcompile_initialization_system
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/initializationproblem.jl:193
  [$i] #_#942
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/initializationproblem.jl:110
  [$i] InitializationProblem
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/initializationproblem.jl:46
  [$i] #_#941
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/initializationproblem.jl:43
  [$i] InitializationProblem
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/initializationproblem.jl:40
  [$i] #maybe_build_initialization_problem#792
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:1873
  [$i] maybe_build_initialization_problem
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:1850
  [$i] #__process_SciMLProblem#801
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:2167
  [$i] __process_SciMLProblem
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:2123
  [$i] #process_SciMLProblem#800
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:2120
  [$i] process_SciMLProblem
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:2108
  [$i] #process_SciMLProblem#799
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:2090
  [$i] process_SciMLProblem
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:2058
  [$i] #_#865
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/sddeproblem.jl:76
  [$i] SDDEProblem
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/sddeproblem.jl:65
  [$i] #_#864
        @ ./none:-1
  [$i] SDDEProblem
        @ ./none:-1
  [$i] #SDDEProblem#863
        @ ./none:-1
  [$i] SDDEProblem
        @ ./none:-1
  [$i] top-level scope
        @ cell:symptom1_traceback:3
        … 20 harness frames elided

Frame [1] is this guard in ModelingToolkitTearing/src/tearingstate.jl:

iv isa SymbolicT && isequal(v, iv) && continue
iv isa SymbolicT && MTKBase.isdelay(v, iv) && continue   # exemption for delay terms,
                                                          # dead when iv === nothing
if !in(v, dvs)
    ...
    if !isvalid
        throw(ArgumentError("\$v is present in the system but \$v′ is not an unknown."))
    end

The delay exemption is gated on iv isa SymbolicT, so it's dead when iv === nothing. The delayed term then gets treated as an ordinary unknown variable, which it isn't. That's why the error names the delay term when the actual problem is the missing alias substitution.

Symptom 2: no error at all

Skip initialization and the problem constructs fine. The generated right-hand side then contains a literal call to sth₊x, which isn't a Julia function.

bad_prob = SDDEProblem(broken, [], (0.0, 1.0); build_initializeprob = false)

(constructed_fine = summary(bad_prob),)
(constructed_fine = "SDDEProblem with uType Vector{Float64} and tType Float64. In-place: true\nNon-trivial mass matrix: false",)

The lowering is visible in the emitted code. Every unknown gets bound to a closure over the history function, getindex(i) ∘ Fix1(h, p), i.e. h(p, ·)[i]:

lv₊x = getindex(3) ∘ Fix1(h, p)      # h(p, τ)[3]

sth₊x gets no binding, because it isn't an unknown. The name is emitted bare, then called:

local var"##cse#3" = sth₊x                        # unbound, no history closure
local var"##cse#8" = var"##cse#3"(var"##cse#7")   # called at (t - τ) regardless

It resolves in ModelingToolkitBase, where no such name exists, so you get an UndefVarError on the first RHS evaluation instead of at build time.

rhs_expr = let e = ModelingToolkit.generate_rhs(broken; expression = Val{true})
    e isa Tuple ? e[1] : e
end
rhs_lines = split(string(rhs_expr), '\n')

for (i, ln) in enumerate(rhs_lines)
    if occursin("ComposedFunction", ln) || occursin("sth_orig₊x", ln) ||
       occursin(r"^\s+(sth_orig₊z|lv_orig₊y|lv_orig₊x) = ", ln)
        marker = occursin("sth_orig₊x", ln) ? "  <== ORPHANED" : ""
        println(lpad(i, 4), "", strip(ln), marker)
    end
end
  14 │ sth_orig₊z = begin
  19 │ local var"##cse#4" = (ComposedFunction)(var"##cse#0", var"##cse#3")
  21 │ lv_orig₊y = begin
  26 │ local var"##cse#4" = (ComposedFunction)(var"##cse#0", var"##cse#3")
  28 │ lv_orig₊x = begin
  33 │ local var"##cse#4" = (ComposedFunction)(var"##cse#0", var"##cse#3")
  39 │ local var"##cse#3" = sth_orig₊x  <== ORPHANED
let du = zeros(3)
    try
        bad_prob.f(du, bad_prob.u0, (p, tt) -> bad_prob.u0, bad_prob.p, 0.0)
        println("no error (unexpected)")
    catch e
        show_trimmed(e, catch_backtrace())
    end
end
UndefVarError: `sth_orig₊x` not defined in `ModelingToolkitBase`
Suggestion: check for spelling errors or missing imports.
Stacktrace (10 of 30 frames):
  [$i] macro expansion
        @ /Users/kburke/.julia/packages/RuntimeGeneratedFunctions/odmYb/src/RuntimeGeneratedFunctions.jl:247
  [$i] macro expansion
        @ ./none:0
  [$i] generated_callfunc
        @ ./none:0
  [$i] RuntimeGeneratedFunction
        @ /Users/kburke/.julia/packages/RuntimeGeneratedFunctions/odmYb/src/RuntimeGeneratedFunctions.jl:234
  [$i] GeneratedFunctionWrapper
        @ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/codegen_utils.jl:1050
  [$i] macro expansion
        @ ./none:-1
  [$i] #invoke_with_despecialized_parameters#38
        @ ./none:0
  [$i] invoke_with_despecialized_parameters
        @ ./none:-1
  [$i] SDDEFunction
        @ /Users/kburke/.julia/packages/SciMLBase/W1j29/src/scimlfunctions.jl:3019
  [$i] top-level scope
        @ cell:codegen_traceback:3
        … 20 harness frames elided

The solver stack, change (3)

Once the model is fixed, the posted script still fails, for an unrelated reason. It loads ModelingToolkit, DifferentialEquations and StochasticDiffEq, and none of those loads DelayDiffEq, so no SDDE integrator is available:

SciMLBase.NoDefaultAlgorithmError()

DelayDiffEq may well be sitting in your manifest already as a transitive dependency, as it is in the issue's manifest at 6.1.2. That isn't enough. It has to be loaded, because the machinery arrives through a package extension.

using DelayDiffEq fixes it, because SDDE support lives there now. _sde_alg_cache and _create_sdde_noise are defined in DelayDiffEqStochasticDiffEqCoreExt, triggered by StochasticDiffEqCore + DiffEqNoiseProcess + Random. DelayDiffEq.__solve takes AbstractSDDEProblem directly and check_prob_alg_pairing accepts an SDE algorithm, so plain solve(prob, ImplicitEM()) works with no MethodOfSteps wrapper and no constant_lags.

Skip StochasticDelayDiffEq

It's the obvious package to reach for. On the current stack it can't be installed at all: its compat bounds conflict with DelayDiffEq 6.x, so Pkg.add reports unsatisfiable requirements. StochasticDelayDiffEq tops out at 1.13.0 and was superseded by the move into DelayDiffEq without being retired.

For the record, on an older stack where it does resolve it is still broken: against StochasticDiffEq 6.102 it fails to precompile with UndefVarError: handle_callback_modifiers!, and against 6.98 or 6.99 it loads and then dies in build_history_function with MethodError: no method matching _sde_alg_cache(...). That reproduces with a hand-written SDDEProblem and no MTK involved.

Which parts are bugs

Splitting these out because only some are fixable in user code.

MTK: delay terms aren't rewritten, and the bad code gets emitted anyway. mtkcompile will produce a system whose equations contain v(t - τ) where v is neither an unknown nor observed, then generate a function that calls that name as if it were a Julia function. Two routes get there:

Substituting through delay arguments is the right fix, and it would make the posted code work as written. Short of that, mtkcompile should refuse. Emitting a function that throws UndefVarError on first evaluation is the worst option, because a model can look compiled and be silently wrong.

MTK: the error points somewhere else. The ArgumentError people actually hit names the delay term inside a time-independent initialization system. The delay exemption in TearingState is gated on iv isa SymbolicT and dead when iv === nothing. Nothing in the message suggests the delayed variable was alias-eliminated.

MTK, missing feature. ParentScope and GlobalScope are ignored for the callable operator in a delay term, so there's no way to say "delay this variable from an enclosing scope". For now that means hoisting the delay equation up to a common parent.

Not MTK. using DifferentialEquations not being enough to solve an SDDEProblem, and NoDefaultAlgorithmError saying nothing about which package is missing. And StochasticDelayDiffEq still being registered while it can't co-resolve with the stack it belongs to.