Symptom

A nightly architectural scan (Kraken, finding 7ffc07667004) flagged ClientNodeManager, NodeObserver, and SharedNodeFunctions.snapshot() as having uncoordinated state reads that could let UI screens observe stale node state. Most of the specific mechanisms cited were already fixed by earlier work (#428, #454, #656) or were mischaracterized (the snapshot() casts are paired 1:1 with KrillApp type/meta invariants that are never violated). Investigation found one real, narrower instance still present.

Root cause

ClientNodeManager.execute(node: Node) — introduced in c58f90e16 (the Phase 4 /invoke migration), after #656’s mutateStored sweep had already landed, so it was never brought into that fix’s scope — still did the pre-#656 “capture-then-whole-node-write” pattern for its state-only pulse:

  1. It gated only on DELETING/ERROR, never PAUSED. A caller holding a pre-pause Node reference (e.g. a Compose pointerInput(node.id, …) closure in NodeItem.kt, which is not reinstalled on every recomposition and can hold a several-states-stale node) could still have its local pulse write state = PROCESSING over a node the user had live-paused.
  2. It forwarded the caller’s raw, possibly-stale node straight to NodeHttp.invokeNode, whose isInvokable() guard (added for krill-oss#158 — itself a prior Kraken finding) evaluates target.state. A stale node.state defeats that guard in either direction: a live-paused node can slip through on a stale non-paused snapshot, or a live-invokable node can be silently dropped on a stale paused/deleting snapshot.

Fix

Prevention