Symptom

A Button configured with nodeAction = RESET whose child is a Project.TaskList: clicking it ran ServerTaskListProcessor.processReset (tasks were marked complete in the DB), but the UI kept showing the old incomplete tasks and the past-due “confounded face” / escalated chip until the app was fully restarted. Intermediate states of the fix also exposed an infinite reset loop.

Root cause

Three stacked issues, peeled one at a time:

  1. The meta-bearing update() settled with state = NONE. SseStateForwarding.filtered deliberately drops NONE from /sse, so the reset meta was persisted but never broadcast.
  2. Emitting the meta change under state = RESET instead is forwarded — but ServerTaskListProcessor.post() re-dispatches RESET into processReset, so re-emitting under RESET is an infinite loop.
  3. The real gap: EventClient (the client’s only SSE consumer) reads /events, not /sse. Its STATE_CHANGE handler copies only the new state onto the client’s stale local node, discarding server-side meta. No /events type carried a generic node-meta mutation, so a TaskList reset was invisible until a full REST reload. This was pre-existing and general (server-side expiredExecuted flips had the same blind spot); RESET merely surfaced it.

Fix

processReset now writes the reset meta under NodeState.PROCESSING (SSE-forwarded and falls through post()’s else, so no loop), then setStateToNone to settle. To actually reach live clients it additionally posts an EventType.CREATED event carrying the full reset node — CREATED is the one /events type whose EventClient handler replaces the whole node (nodeManager.update(payload.node)). IconManager then re-derives computeTaskListState over the completed/future-dated tasks → NONE, so visuals clear without a restart. (CREATED-for-an-existing-node is a deliberate stopgap; the clean form is a dedicated NODE_UPDATED event.)

Prevention

Regression test asserts the propagation, not just the DB row: ServerTaskListProcessorResetTest verifies a CREATED event is emitted whose payload node carries the reset meta and state == NONE, and that executeChildren is never called. TaskListResetTest (pure resetAllTasks) locks the no-immediate-refire invariant and that computeTaskListState derives to NONE post-reset. General rule: when a server-side change must reach clients, the test must observe it via the event/SSE path a client actually consumes — asserting persistence alone hides this entire class of bug.