Problem
ltm_discovery_mode is a bool field on the SourceProject salsa input (src/simlin-engine/src/db/input.rs, setter db::set_project_ltm_discovery_mode in src/simlin-engine/src/db.rs). Two tracked queries read it directly:
model_ltm_mode (src/simlin-engine/src/db/ltm/mod.rs) returns LtmMode::Discovery when the flag is set, before any SCC gate is consulted.
model_ltm_variables (same file) reads it as is_discovery_user to decide whether every edge gets a link score and whether the auto-flip warning is emitted.
Everything under model_ltm_variables (the shaped link scores, the loop/relative-loop scores, the LTM fragments, the LTM-overlay layout and assembly) therefore depends on the flag. Because it is an input, every flip is a new salsa revision:
- the other mode's
model_ltm_mode / model_ltm_variables memos are replaced on the next demand (the values differ, so nothing backdates and the whole LTM tier re-derives), and
- every memo in the database is deep-verified on its next read.
This is exactly the shape the LTM overlay had before it became an explicit query argument (db::LtmOverlay { Off, On }, keyed on the compile rather than stored on the project). That change removed a revision per overlay flip; on C-LEARN the flip had been re-executing ~2.9 G instructions for a warm LTM sim after a plain diagnostics pass and ~2.5 G for the reverse. docs/design/engine-performance.md section C7 ("The LTM overlay is an argument, not an input") records that change and its measurements; discovery mode is the remaining input of the same shape.
Who flips it
Production (non-test) callers:
simlin_engine::analysis::analyze_model (src/simlin-engine/src/analysis.rs) sets the flag to true for its pipeline and restores the caller's prior value before returning. The set is a no-op write when the caller is already in discovery mode, but for a caller in exhaustive mode every analyze_model call is two revisions (set, restore), and the exhaustive-mode LTM memos are gone afterwards.
wasmgen::compile_datamodel_to_artifact (src/simlin-engine/src/wasmgen/module.rs) sets it on a fresh SimlinDb per call, so it does not thrash a shared database, but it is the same flag threaded as a bool.
- libsimlin's
simlin_analyze_discover_loops (src/libsimlin/src/analysis.rs) and the MCP read_model / edit_model tools (src/simlin-mcp-core/src/tools/) call analyze_model on the project's shared SimlinDb.
simlin_sim_new(.., enable_ltm) does not set the flag itself. An LTM simulation created through it runs in whatever mode the project is in (exhaustive by default, subject to the auto-flip gate). So the concrete thrash pattern is a caller alternating an exhaustive-mode LTM sim (simlin_sim_new with LTM enabled, which is the editor's sparkline / link-score path) with simlin_analyze_discover_loops on one project: each round trip re-derives the LTM tier for both modes and re-verifies every memo.
Why it matters
- Performance of the shared-database callers. libsimlin and simlin-serve keep one
SimlinDb per project precisely so warm queries are cheap. This input defeats that for any caller that uses both modes.
- Consistency of the salsa design. The overlay is a query argument; the mode is an input. Two knobs of the same shape with two different mechanisms is a trap for the next reader (and the
analyze_model set/restore dance, with its own regression tests ltm_discovery_mode_reset_after_analyze / ..._after_failed_analysis, exists only to keep the input clean).
- Deferred-delete residency. Each replaced memo sits in the ingredient's deferred-delete list until the next revision; a flip-heavy caller carries both modes' LTM values plus the stale generation between them.
Components affected
src/simlin-engine (db/input.rs, db.rs, db/ltm/mod.rs, analysis.rs, wasmgen/module.rs)
src/libsimlin (analysis.rs comment and the shared-db call sites)
src/simlin-mcp-core (tool call sites, transitively)
Proposed approach
Make the requested mode a key of the LTM derivation the way the overlay is a key of the compile:
- Introduce a requested-mode argument (e.g.
LtmModeRequest { Exhaustive, Discovery }, distinct from the existing db::LtmMode which is the effective mode after the auto-flip gate) on model_ltm_mode and model_ltm_variables, and thread it through their dependents; layout/assembly take it alongside LtmOverlay.
- Delete the
ltm_discovery_mode input field, set_project_ltm_discovery_mode, and the set/restore in analyze_model (which then just passes Discovery). compile_datamodel_to_artifact and libsimlin pass the argument instead of setting the flag.
- Pin both modes staying memoized side by side, mirroring
src/simlin-engine/src/db/ltm_overlay_tests.rs.
Cost: a second set of LTM memos when both modes have been used on one project (the overlay change paid about 5 MiB on C-LEARN for the analogous fragment duplication; the LTM tier is larger, so expect more).
Measure first
Before doing this, measure how often real callers actually alternate modes on one project: the editor's sparkline path through libsimlin, and the MCP tools (which today only ever request discovery via analyze_model). If nothing alternates in practice, the cleanup is worth doing for consistency but the memory cost should be weighed against the instruction savings.
Context
Identified on branch salsa-roofline-engine (PR pending) while converting the LTM overlay from a project input to a query argument; the discovery-mode flag was left as is to keep that change reviewable.
Problem
ltm_discovery_modeis aboolfield on theSourceProjectsalsa input (src/simlin-engine/src/db/input.rs, setterdb::set_project_ltm_discovery_modeinsrc/simlin-engine/src/db.rs). Two tracked queries read it directly:model_ltm_mode(src/simlin-engine/src/db/ltm/mod.rs) returnsLtmMode::Discoverywhen the flag is set, before any SCC gate is consulted.model_ltm_variables(same file) reads it asis_discovery_userto decide whether every edge gets a link score and whether the auto-flip warning is emitted.Everything under
model_ltm_variables(the shaped link scores, the loop/relative-loop scores, the LTM fragments, the LTM-overlay layout and assembly) therefore depends on the flag. Because it is an input, every flip is a new salsa revision:model_ltm_mode/model_ltm_variablesmemos are replaced on the next demand (the values differ, so nothing backdates and the whole LTM tier re-derives), andThis is exactly the shape the LTM overlay had before it became an explicit query argument (
db::LtmOverlay { Off, On }, keyed on the compile rather than stored on the project). That change removed a revision per overlay flip; on C-LEARN the flip had been re-executing ~2.9 G instructions for a warm LTM sim after a plain diagnostics pass and ~2.5 G for the reverse.docs/design/engine-performance.mdsection C7 ("The LTM overlay is an argument, not an input") records that change and its measurements; discovery mode is the remaining input of the same shape.Who flips it
Production (non-test) callers:
simlin_engine::analysis::analyze_model(src/simlin-engine/src/analysis.rs) sets the flag totruefor its pipeline and restores the caller's prior value before returning. The set is a no-op write when the caller is already in discovery mode, but for a caller in exhaustive mode everyanalyze_modelcall is two revisions (set, restore), and the exhaustive-mode LTM memos are gone afterwards.wasmgen::compile_datamodel_to_artifact(src/simlin-engine/src/wasmgen/module.rs) sets it on a freshSimlinDbper call, so it does not thrash a shared database, but it is the same flag threaded as a bool.simlin_analyze_discover_loops(src/libsimlin/src/analysis.rs) and the MCPread_model/edit_modeltools (src/simlin-mcp-core/src/tools/) callanalyze_modelon the project's sharedSimlinDb.simlin_sim_new(.., enable_ltm)does not set the flag itself. An LTM simulation created through it runs in whatever mode the project is in (exhaustive by default, subject to the auto-flip gate). So the concrete thrash pattern is a caller alternating an exhaustive-mode LTM sim (simlin_sim_newwith LTM enabled, which is the editor's sparkline / link-score path) withsimlin_analyze_discover_loopson one project: each round trip re-derives the LTM tier for both modes and re-verifies every memo.Why it matters
SimlinDbper project precisely so warm queries are cheap. This input defeats that for any caller that uses both modes.analyze_modelset/restore dance, with its own regression testsltm_discovery_mode_reset_after_analyze/..._after_failed_analysis, exists only to keep the input clean).Components affected
src/simlin-engine(db/input.rs,db.rs,db/ltm/mod.rs,analysis.rs,wasmgen/module.rs)src/libsimlin(analysis.rscomment and the shared-db call sites)src/simlin-mcp-core(tool call sites, transitively)Proposed approach
Make the requested mode a key of the LTM derivation the way the overlay is a key of the compile:
LtmModeRequest { Exhaustive, Discovery }, distinct from the existingdb::LtmModewhich is the effective mode after the auto-flip gate) onmodel_ltm_modeandmodel_ltm_variables, and thread it through their dependents; layout/assembly take it alongsideLtmOverlay.ltm_discovery_modeinput field,set_project_ltm_discovery_mode, and the set/restore inanalyze_model(which then just passesDiscovery).compile_datamodel_to_artifactand libsimlin pass the argument instead of setting the flag.src/simlin-engine/src/db/ltm_overlay_tests.rs.Cost: a second set of LTM memos when both modes have been used on one project (the overlay change paid about 5 MiB on C-LEARN for the analogous fragment duplication; the LTM tier is larger, so expect more).
Measure first
Before doing this, measure how often real callers actually alternate modes on one project: the editor's sparkline path through libsimlin, and the MCP tools (which today only ever request discovery via
analyze_model). If nothing alternates in practice, the cleanup is worth doing for consistency but the memory cost should be weighed against the instruction savings.Context
Identified on branch
salsa-roofline-engine(PR pending) while converting the LTM overlay from a project input to a query argument; the discovery-mode flag was left as is to keep that change reviewable.