Symptom

On a Cron→Calc→Counter swarm, editing a node’s settings (formula, timer interval, wiring) while the nodes were firing drew an interaction arc from the node’s sources to itself — as if an upstream node had just invoked it. No invocation happened; it was a pure user edit.

Root cause

Interaction arcs are lit by UniversalAppNodeProcessor.process() whenever the per-node observer (DefaultNodeObserver) re-dispatches a node whose state satisfies recordsInteractionArc (PROCESSING/EXECUTED). state is a sticky field that rides along on every re-emission, not a per-invocation event. A settings edit round-trips as STATE_CHANGE(USER_EDIT); EventClient then calls ClientNodeManager.refreshNodeMeta() with the node it captured before the edit. If that node was mid-pulse, updateMetaData() did node.copy(meta = …) preserving the stale firing state, so the observer re-fired and drew an arc. A metadata update is not an invocation, so it must never present a firing state.

Fix

ClientNodeManager.updateMetaData now neutralises an arc-recording state to NONE before writing (val safeState = if (recordsInteractionArc(node.state)) NodeState.NONE else node.state). Health states (WARN/ERROR/…) are deliberately preserved; a genuine invocation re-asserts PROCESSING/EXECUTED via its own STATE_CHANGE. Regression: ClientNodeManagerMetaEditArcTest (red→green) — updateMetaData/refreshNodeMeta on a pulsing node leave it in a non-arc state, and a WARN survives.

Prevention

A visual that means “a node was invoked” must be driven by an invocation event/transition, never by the presence of a mutable lifecycle field on a re-emitted node. Any mutator that copies a node forward for a reason other than an invocation (meta/wiring edits, optimistic writes) must not carry a firing (recordsInteractionArc) state with it. Rule mirrors the pulse-reset-clobbers-snapshot lesson: a copy-and-write across a non-invocation path must scrub fields whose semantics are “this just fired.”