Symptom

pi-krill, Cron→Calc→Counter. The Counter (DataPoint) showed the correct new value during the activity pulse, then reverted to its app-launch value the moment the pulse ended. The value was received and briefly displayed, so it reached ClientNodeManager, but did not stick.

Root cause

UniversalAppNodeProcessor.showActivity captures the node at pulse time, calls startPairing(node), waits 1500 ms, then reset(node) — using the stale captured node. ClientNodeManager.reset/startPairing did update(node.copy(state = …)), writing that stale node’s old meta back with only state changed. The updateSnapshot ingested during the 1500 ms window was clobbered when reset fired at the end. showActivity even read the current state to decide whether to reset, but then reset with the stale node.

Fix

reset/startPairing now resolve the node from the store (readNodeStateOrNull(node.id).value ?: node) and transition state on that, preserving the latest meta/snapshot. A pulse/lifecycle state transition is not a data revert. Regression: ClientNodeManagerPulseSnapshotTest (red→green) — a snapshot ingested between capture and reset must survive.

Prevention

Any “set this node’s state” helper that takes a Node and is invoked after a delay (or from a captured closure) must re-resolve the node from the store, never trust the captured copy’s meta. Captured-node-plus-copy across an await is a clobber waiting to happen whenever another path can mutate the same node concurrently. Rule: state-only mutators read current, copy state, write back — they never carry stale meta forward.