Problem
Module-INTERNAL PREVIOUS() state is uncounted on every LTM surface. A loop
driver -> sub(out = PREVIOUS(in)) -> reader -> driver
is genuine feedback -- the sub-model implements a one-DT lag, the parent cycle compiles and simulates as a first-order discrete lag -- but it is entirely unscored and unpinnable:
model_is_stateless's lagged leg (model_has_lagged_dt_deps, src/simlin-engine/src/db/ltm/mod.rs) is deliberately parent-level only: it checks previous_only dt deps on the model's own variables, skipping Module kinds.
- The module leg (
modules_carry_state, same file, ~line 288) walks sub-models transitively but tests only sub_edges.stocks.is_empty() -- a sub-model whose only state is a PREVIOUS lag reads as stateless.
- Pin validation (
src/simlin-engine/src/db/ltm/pinned.rs) mirrors the same blindness: it cannot see lagged edges inside a module's pathway, so a pin over the parent loop is rejected.
So on a parent whose only state is a lag-only sub-model, the stateless early return fires and LTM emits nothing (no scores, and pins are dropped without even an invalid-pin warning), while model_detected_loops -- which has no stateless gate -- still enumerates the loop. The detected-vs-scored gap is the same shape #748 describes for module-internal stocks, on the lagged-state leg.
This is a known, documented residual of the #749 fix (branch ltm-fix-batch-2, commit 69e84d8e): that fix made PARENT-level previous_only dt deps count as state in both model_is_stateless and pin validation, and deliberately left module-internal lagged state uncounted on BOTH surfaces so the two surfaces at least agree (both treat the shape as stateless). The rustdoc on model_has_lagged_dt_deps records the scope cut.
Why it matters
Genuine feedback is invisible to LTM for lag-only sub-models -- e.g. a user sub-model implementing a custom delay or smoothing structure via PREVIOUS instead of an INTEG. #749 established (per the LTM reference, sec. 7) that PREVIOUS is state the enumerator legitimately scores; the module boundary should not change that verdict. Severity: low-medium (uncommon model shape today; silent no-output rather than wrong numbers).
Components affected
src/simlin-engine/src/db/ltm/mod.rs -- modules_carry_state (~line 288) and/or model_has_lagged_dt_deps (the stateless gate's two legs)
src/simlin-engine/src/db/ltm/pinned.rs -- pin validation's view of module-internal lagged edges
Constraint / fix direction
Both surfaces must move together (the #749 invariant): give modules_carry_state a lagged leg (a sub-model variable with a previous_only dt dep counts as state, transitively, mirroring its stock walk) AND give pin validation module-pathway lagged-edge visibility, so a scored loop is also pinnable. Fixing only the gate would recreate the exact enumerable-but-unpinnable disagreement #749 closed.
Cross-references
Discovery context
Identified during the #749 fix on branch ltm-fix-batch-2 (commit 69e84d8e), 2026-06-09: the commit message and model_has_lagged_dt_deps rustdoc record the deliberate scope cut; this issue makes the residual independently trackable.
Problem
Module-INTERNAL
PREVIOUS()state is uncounted on every LTM surface. A loopis genuine feedback -- the sub-model implements a one-DT lag, the parent cycle compiles and simulates as a first-order discrete lag -- but it is entirely unscored and unpinnable:
model_is_stateless's lagged leg (model_has_lagged_dt_deps,src/simlin-engine/src/db/ltm/mod.rs) is deliberately parent-level only: it checksprevious_onlydt deps on the model's own variables, skippingModulekinds.modules_carry_state, same file, ~line 288) walks sub-models transitively but tests onlysub_edges.stocks.is_empty()-- a sub-model whose only state is aPREVIOUSlag reads as stateless.src/simlin-engine/src/db/ltm/pinned.rs) mirrors the same blindness: it cannot see lagged edges inside a module's pathway, so a pin over the parent loop is rejected.So on a parent whose only state is a lag-only sub-model, the stateless early return fires and LTM emits nothing (no scores, and pins are dropped without even an invalid-pin warning), while
model_detected_loops-- which has no stateless gate -- still enumerates the loop. The detected-vs-scored gap is the same shape #748 describes for module-internal stocks, on the lagged-state leg.This is a known, documented residual of the #749 fix (branch
ltm-fix-batch-2, commit69e84d8e): that fix made PARENT-levelprevious_onlydt deps count as state in bothmodel_is_statelessand pin validation, and deliberately left module-internal lagged state uncounted on BOTH surfaces so the two surfaces at least agree (both treat the shape as stateless). The rustdoc onmodel_has_lagged_dt_depsrecords the scope cut.Why it matters
Genuine feedback is invisible to LTM for lag-only sub-models -- e.g. a user sub-model implementing a custom delay or smoothing structure via
PREVIOUSinstead of an INTEG. #749 established (per the LTM reference, sec. 7) that PREVIOUS is state the enumerator legitimately scores; the module boundary should not change that verdict. Severity: low-medium (uncommon model shape today; silent no-output rather than wrong numbers).Components affected
src/simlin-engine/src/db/ltm/mod.rs--modules_carry_state(~line 288) and/ormodel_has_lagged_dt_deps(the stateless gate's two legs)src/simlin-engine/src/db/ltm/pinned.rs-- pin validation's view of module-internal lagged edgesConstraint / fix direction
Both surfaces must move together (the #749 invariant): give
modules_carry_statea lagged leg (a sub-model variable with aprevious_onlydt dep counts as state, transitively, mirroring its stock walk) AND give pin validation module-pathway lagged-edge visibility, so a scored loop is also pinnable. Fixing only the gate would recreate the exact enumerable-but-unpinnable disagreement #749 closed.Cross-references
69e84d8eonltm-fix-batch-2)Discovery context
Identified during the #749 fix on branch
ltm-fix-batch-2(commit69e84d8e), 2026-06-09: the commit message andmodel_has_lagged_dt_depsrustdoc record the deliberate scope cut; this issue makes the residual independently trackable.