Skip to main content
Version: beta_0.0.0

Overview

The activation model defines how nodes decide whether to process incoming signals. It replaces traditional rate limiting with a biologically-inspired threshold system.

Activation State

Each node MUST maintain an activation state:

Activation Sequence

  1. Signal arrives at node via synapse.
  2. Signal contribution is calculated: contribution = signal.weight * synapse.weight
  3. Contribution is added to the node’s potential: potential += min(potential + contribution, max_potential)
  4. The signal is appended to the pending queue (see Queue Management).
  5. If potential >= threshold AND the node is not in its refractory period: a. The node fires, draining a batch from the queue (What Firing Processes). b. Potential resets to 0.0. c. The node enters its refractory period.
  6. Otherwise no processing occurs; the signal waits in the queue.

What Firing Processes

Earlier drafts said potential accumulates across all signals and then “the node fires (processes the signal)”, without saying which signal. When potential is the sum of many contributions, “the signal” is ambiguous, and implementations diverged: some processed only the signal that crossed the threshold, leaking the rest. This version resolves it. On firing, a node MUST drain a batch from the pending queue:
  1. Order the pending queue by signal weight descending. Where weights tie, order by signal ID ascending, so the order is total and reproducible.
  2. Process up to fire_batch_size signals from the front of that order.
  3. Reset potential to 0.0 after the batch, not per signal.
  4. Enter the refractory period.
A node MUST retain whatever a released signal needs to be processed — for the reference implementation, the signal body, since the queue itself holds only scheduling state. That retention MUST be tied to queue membership and MUST NOT have an eviction policy of its own: anything evicted on a criterion the queue does not share belongs to a signal that is still queued, which then releases with nothing to process. A deduplicated arrival in particular is not queued, and retaining it leaks one entry per duplicate. Signals left in the queue remain queued and are eligible for the next fire. They MUST NOT be discarded merely because they were not in this batch. Every signal in the batch MUST be processed in full — handled locally, routed onward, and receipted — not only the arrival that crossed the threshold. This is the same divergence as above, one layer out: an implementation whose transport handles “the signal it just read” and ignores the rest of the batch loses up to fire_batch_size − 1 signals per fire, silently, with no receipt for their senders. The batch is the unit of work, so a node MUST route each member of it and MUST NOT assume a member’s route was planned when it was queued: the route is chosen at release, because the topology may have changed while it waited. The same applies to a signal released by the latency guard below: releasing it means processing it, which includes forwarding it. A node that emits a positive receipt for a released signal it did not then route has reported a delivery that did not happen. fire_batch_size defaults to 16. A node MUST NOT set it to zero. Setting it to 1 yields the strictest interpretation of the earlier text — one signal per fire — at the cost of throughput, and is permitted.
Potential resets once per fire, not once per signal. A batch of sixteen signals costs the same one refractory period as a batch of one, which is what makes batching worth doing under load.

Dynamic Threshold

The threshold SHOULD adjust dynamically based on node load:
Where load_factor = current_queue_depth / max_queue_depth. This ensures that overloaded nodes become more selective, providing natural backpressure.

Refractory Period

After firing, a node MUST enter a refractory period during which it cannot fire again. During it:
  • Incoming signals still accumulate potential
  • No processing occurs
  • Signals queue, subject to the bounded-queue policy below

Refractory Period Is a Node-Class Parameter

A single global default cannot serve both a solar-powered sensor and a datacentre node. 10 ms on server-class hardware caps the node at 100 fires per second, which is a throughput ceiling far below what the hardware can sustain — and a self-inflicted one. The refractory period MUST therefore be configured per node class: A value of 0 disables the refractory period. This is legitimate on server-class nodes: the dynamic threshold and the bounded queue already provide backpressure, and the refractory period is the crudest of the three. A node MUST report its class and effective refractory period, so that an operator diagnosing a throughput ceiling can see it.

Queue Management

load_factor = current_queue_depth / max_queue_depth implies a bounded queue. Earlier drafts also stated that during the refractory period “no signals are dropped (they queue)”. Both cannot hold: a bounded queue necessarily rejects something once full. This version specifies what.

Bounded Queue

A node MUST maintain a bounded pending queue of max_queue_depth signals (defaults in Configuration). load_factor is defined against this bound, and is therefore always in [0, 1].

Overflow Policy

When a signal arrives at a full queue, the node MUST apply its configured overflow_policy: drop_lowest_weight is the default because it preserves the protocol’s own priority semantics: weight already means importance everywhere else, and under load is exactly when honouring it matters.
drop_oldest MUST select by enqueue time, not by queue position. Firing reorders the queue by weight descending, so after any partial drain the front of the queue is the heaviest remaining signal. An implementation that pops the front believes it is shedding stale traffic while it is in fact shedding the most important traffic it holds — the exact inverse of the policy, and worst under precisely the load that triggers it. Each queued signal therefore needs its enqueue timestamp retained.

Overflow and Delivery Class

A dropped signal’s delivery class governs whether the drop is reported:
  • best-effort — dropped silently. No receipt.
  • acknowledged — the node MUST emit a negative receipt with reason queue_full toward the signal’s origin. Dropping it silently is prohibited; see delivery-semantics §2.2.
A node MUST NOT drop an acknowledged signal without emitting a receipt, even under overflow. Overload is not an exemption from the delivery guarantee — it is the case the guarantee exists for. A refused arrival does not refuse the batch. Admission enqueues before it evaluates, so an arrival’s own contribution can fire the gate on its way in and the overflow policy can then choose that same arrival as the drop. It can also fire a batch it is not itself part of, since the drain takes the top fire_batch_size by weight. Either way the batch is other senders’ traffic and has nothing to do with why this signal was turned away: a node MUST process it and MUST NOT report the refused signal as also handled — those are contradictory outcomes for one signal. This holds for the displaced signal as much as the refused arrival. Under drop_lowest_weight and drop_oldest the signal dropped is usually not the one that just arrived, and it came from a different peer — so the receipt goes to a different origin, over a different synapse, and cannot be inferred from the arrival. An implementation MUST therefore report which signal was displaced, not merely that a drop occurred. Reporting only the arrival’s fate is the shape this omission takes in practice, and it silently breaks the guarantee for exactly the signals the policy chose to sacrifice.

Interaction With the Dynamic Threshold

Overflow should be rare, because the rising threshold throttles admission before the queue fills. A node whose queue overflows regularly is misconfigured: either max_queue_depth is too small for its traffic, or dynamic_threshold is disabled. Implementations SHOULD expose an overflow counter so this is visible rather than inferred.

Activation Functions

Implementations MUST support the step activation function. Implementations SHOULD support sigmoid and leaky functions.

Configuration

Defaults by node class:

Persistence

Activation state MAY be memory-only on edge nodes, and SHOULD be persisted on other classes. A node that discards its activation state on restart offers an attacker a free reset of backpressure: flood it, let it restart, and it comes back ready to be flooded again. See storage-interface §4.
Last modified on September 11, 2026