The RESET verb (NodeAction.RESET) was only interpreted by three processors
(Timer stop, TaskList complete-tasks, LLM clear-reply); every other
snapshot-bearing node type either ignored RESET or had a no-op guard. There was
no general “a source firing RESET clears the observing node’s stored value”
behavior — so the canonical loop (a Calculation counting a DataPoint up to a
threshold, an Alert firing RESET at the threshold, the DataPoint observing the
Alert and clearing back to 0 to restart the count) could not be built.
Every node’s metadata carries a Snapshot (via SourceMetaData.snapshot), but
the SDK exposes only val snapshot (read-only) plus withError(...) — there is
no withSnapshot(...), and each concrete *MetaData is its own data class
with its own copy(). So there was no generic way to clear a node’s snapshot,
and RESET semantics had to be hand-written per processor.
Two further traps, both discovered by reading the code before implementing:
meta.snapshot. ServerNodeManager.readNode
overlays dataStore.last(node) over the persisted meta, and dataStore.last
returns the max-timestamp sample bounded by retention. A meta-only reset
(or a timestamp = 0 write) is invisible to the next reader — the counter
would never actually restart.Compute, Calculation, SMTP) carried
if (verb == RESET) return no-op guards that read as “RESET handled” but
really meant “ignore RESET”.A single interception seam, not per-processor edits:
ServerNodeManager.invoke now checks isGenericReset(processor, verb): a
RESET aimed at a processor that is not a CustomResetHandler is routed to
genericReset(target) and process() is never dispatched.CustomResetHandler (server/.../CustomResetHandler.kt) is a marker for the
four processors that own their RESET: Timer, TaskList, LLM, and DataPoint.genericReset clears the snapshot to Snapshot(0L, "") via clearSnapshot
(server/.../SnapshotReset.kt), persists non-propagating (terminal under the
ambient RESET PropagationContext), and surfaces a SNAPSHOT_UPDATE.clearSnapshot is a when(meta) with one arm per concrete SourceMetaData
(the SDK gap forces this); SnapshotResetTest builds all 29 non-DataPoint
types and fails if any is missing an arm.CustomResetHandler: ServerDataPointProcessor resets to
the DataType baseline ("" / "{}" / "0.0" / "0"=black) through the
time-series ingest path at the current time, so dataStore.last (the
DataPoint’s source of truth on read) actually reflects the cleared value.DataProcessor — was extracted to DataType.baselineValue() so the ingest
path and the reset path share one source of truth.invoke already
owned RESET’s terminal semantics (suppressPublish); adding the generic reset
there kept every processor untouched except an opt-out marker.when blocks that could drift; the reset
feature would have been a fourth. Extract before you add.dataStore.last, selected by max timestamp
and bounded by retention), not meta.snapshot; a timestamp = 0 write sorts
as the oldest sample and silently does nothing. Verify the read path, not just
the write path.when over a non-sealed cross-repo interface needs a completeness guard.
SourceMetaData lives in the SDK and isn’t sealed, so the compiler can’t force
exhaustiveness. SnapshotResetTest enumerates every concrete type and the
else logs loudly — a new SDK node type surfaces at test time, not in prod.if (verb == RESET) return is not “handling RESET”. It reads like
intent but is indistinguishable from ignoring it; make the ownership explicit
(here: implement CustomResetHandler or don’t).