Symptom

A nightly architectural scan filed a blocker against NodeItem.kt: the while (true) loop inside Modifier.pointerInput { awaitPointerEventScope { … } } was claimed to block threads, accumulate uncancelable coroutines on every node render, starve Dispatchers.Default, and leak memory and battery — “hundreds of blocked coroutines” with 100+ visible nodes.

This was the second filing of substantially the same claim against the same lines; the first was closed as a false positive with no code change.

Root cause

The severity claims are refuted. awaitPointerEvent() is a cancellable suspension point, not a blocking call — it parks the coroutine and consumes no thread while waiting. The block passed to Modifier.pointerInput(keys) runs in a coroutine owned by the pointer-input modifier node, which Compose cancels and relaunches when the keys change or the node detaches. Cancellation is therefore delivered at awaitPointerEvent(), the loop unwinds, and nothing leaks. while (true) { awaitPointerEvent() } is the canonical raw-gesture idiom — detectTapGestures and detectDragGestures use exactly this shape internally.

The strongest evidence is upstream’s own API design: AwaitPointerEventScope is annotated @RestrictsSuspension, so you cannot call currentCoroutineContext() inside it at all. A scope that forbids arbitrary suspension is not a scope that hosts free-running work.

Two things were nevertheless real:

  1. Both awaitPointerEventScope loops (NodeItem.kt, ClientScreen.kt) were the only remaining while (true) coroutine loops in composeApp, contradicting this repo’s own archived compose-lifecycle-management requirement that such loops gate on isActive. Every scan re-derives the same lead from that inconsistency.
  2. NodeItem.kt’s right-click branch wrapped the entire handler in catch (_: Throwable) { } whose comment claimed it existed only to probe event.buttons. Any ScreenCore failure — selectNode, editSelect, executeCommand — was swallowed silently, with no log line.

Fix

Prevention