ClientScreen node graph, pi-krill: CronTimer (5 s) → Calculation
(“Counter + 1”) → DataPoint “Counter”. Pulses fired in the correct order
and the static source/target relationship arcs were perfect, but the
temporary “flowing activity” interaction arc (DrawInteractionArcs)
rendered Calculation→Counter while CronTimer→Calculation never drew —
every cycle, deterministically.
Interaction arcs are built in ClientScreen.DisplayNodes from
ClientNodeManager.interactions, fed by addInteraction(node).
addInteraction is only called by UniversalAppNodeProcessor.executeVisuals,
and post() routed only NodeState.EXECUTED there. EXECUTED is the
legacy structural-push signal. Under the pull model a source-invoked node
is woken via onSourceTrigger and only ever reaches the universal
PROCESSING pulse (ServerNodeManager.broadcastActivityPulse), never
EXECUTED — so the Calculation produced no addInteraction and no incoming
arc. The Counter masked the gap: a DataPoint still hits
STATE_CHANGE(EXECUTED) on ingest (DataProcessor.kt:311), so
addInteraction(Counter) fired and its sources=[Calc] drew the
Calc→Counter arc — incidental, not by design. The arc subsystem was never
migrated from push (EXECUTED) to pull (PROCESSING pulse).
post() now routes both EXECUTED and PROCESSING to executeVisuals
(addInteraction + showActivity), so the universal source-invocation
pulse drives the arc exactly as it drives the visual pulse. The existing
DisplayNodes sources→node branch then yields CronTimer→Calculation
(Calc.sources=[Cron]). Decision extracted as the pure, unit-tested helper
recordsInteractionArc(state) (mirrors ServerNodeManager.shouldPulse),
guarded by InteractionArcStateTest (red→green: PROCESSING was excluded
under the legacy predicate).
Recurring class (third instance): a pull-model migration must sweep every
consumer keyed on the old push/EXECUTED signal, not just the producer —
arcs, pulses, and the source-trigger dispatch were each missed in turn
because a different node type incidentally still emitted the legacy signal
and masked the regression. Rule: when a signal’s delivery mechanism changes
(push→pull, _nodeUpdates→/events, EXECUTED→PROCESSING), grep all
readers of the old signal and verify each on the node that only has the
new path (here: a Calculation, which never reaches EXECUTED). The
unit-testable seam is a pure NodeState → Boolean classifier, not the Koin
processor hub. Related: 2026-05-18-activity-pulse-wrong-stream.md,
2026-05-17-uniform-source-invocation.md.