Symptom

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.

Root cause

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).

Fix

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).

Prevention

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.