Found while building a demo swarm, not by a field report. A HighThreshold /
LowThreshold trigger wired with the same node as both sources and inputs
never fires — no error, no warning, no visual hint on the canvas. The trigger’s
own meta.snapshot.value visibly updates on every source invocation (so it
looks alive), the downstream observer never runs, and node.error stays
empty.
Wiring the same node to both sources and inputs is also the documented
recipe for every other observer type in the system — logic gates, lambdas,
lamps — so it is the natural next step to try on a threshold, and it is
exactly wrong there.
ServerTriggerProcessor.process() resolves the threshold (“limit”) by looking
for an inputs entry whose node type carries the @DataSource annotation:
1
2
3
4
val triggerSnapshot =
inputs.firstOrNull { it.type.javaClass.isAnnotationPresent(DataSource::class.java) }
?.let { input -> (input.meta as SourceMetaData).snapshot }
?: meta.snapshot
and separately resolves the sample from by — the node that invoked the
trigger:
1
val sample = (source.meta as SourceMetaData).snapshot.value.toDoubleOrNull() ?: 0.0
If the same node is wired as both sources (so it invokes the trigger, i.e.
becomes by) and inputs (so it is read back as the limit), threshold and
sample are read from the same snapshot on the same invocation — they are
always equal. sample > threshold (and sample < threshold) can then never
be true, so the trigger can never cross. Nothing rejects or flags this
wiring; it type-checks and persists like any other configuration.
server/src/jvmMain/kotlin/krill/zone/server/krillapp/trigger/ServerTriggerProcessor.kt
now excludes any inputs entry whose identity equals the invoking source
(by) before searching for the @DataSource limit:
1
meta.inputs.filterNot { it == by }.forEach { ... }
A self-referential input can never be a meaningful limit, so this falls back
to the trigger’s own static meta.snapshot — the same behavior as leaving
inputs empty — instead of silently comparing the sample to itself.
inputs role is not uniform across node types, and nothing in
the wiring UI or the wire format says so. For a logic gate or lamp, inputs
duplicates sources (operand data); for a threshold, inputs is a
different signal (the limit) that must never coincide with the invoking
source. When a wiring slot means two different things depending on node
type, guard the type-specific meaning in code rather than trusting the
caller to know the difference.