CyclicDecayField¶
A DecayingSortedField subclass that adds cyclical resonance and homeostatic pressure to time-weighted scoring.
Overview¶
CyclicDecayField extends DecayingSortedField with two additional temporal forces computed atomically in a single Lua script:
- Cyclical resonance: Periodic boosts following cosine curves. A record about Q1 renewals can resurface every January.
- Homeostatic pressure: Urgency that builds linearly the longer an item goes unresolved. Discharged by calling
resolve_pressure().
The effective score is: decay + cyclic_resonance + pressure
When cycles=[] and pressure_rate=0.0, behavior is identical to DecayingSortedField.
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
decay_rate |
float | 0.1 | Power-law decay exponent (inherited). Empirically tuned in sweep 2026-04-17; prior default was 0.5. |
base_score_field |
str | None | Companion field whose value multiplies the decay curve (inherited) |
cycles |
list | [] |
List of (period, amplitude, phase) tuples |
pressure_rate |
float | 0.0 | Rate of urgency buildup per unresolved day |
partition_by |
str/tuple | () |
Partition sorted set by key fields (inherited) |
Cycle Tuples¶
Each cycle is a (period, amplitude, phase) tuple:
- period: Duration in seconds. Use
TemporalPeriodconstants. - amplitude: Peak boost value (non-negative).
- phase: Time offset in seconds (shifts the cosine curve).
The resonance formula: amplitude * cos(2 * pi * (now - phase) / period)
TemporalPeriod Constants¶
Import from popoto.fields.constants:
| Constant | Value (seconds) |
|---|---|
DAILY |
86,400 |
WEEKLY |
604,800 |
MONTHLY |
2,592,000 |
QUARTERLY |
7,776,000 |
YEARLY |
31,536,000 |
Usage¶
Basic Model Definition¶
from popoto import Model, KeyField, Field, CyclicDecayField
from popoto.fields.constants import TemporalPeriod
class Directive(Model):
agent_id = KeyField()
content = Field(type=str)
relevance = CyclicDecayField(
decay_rate=0.5, # override default (0.1) for faster forgetting
cycles=[(TemporalPeriod.QUARTERLY, 5.0, 0)],
pressure_rate=0.1,
)
Querying Top Results¶
# Top 10 directives by combined decay + cyclic + pressure score
top = Directive.query.filter(agent_id="agent-1").top_by_decay(n=10)
Resolving Pressure¶
Adjusting Cycle Amplitudes¶
Use strengthen_cycle() and weaken_cycle() to dynamically adjust how strongly cycles influence a record's score. Both methods multiply all cycle amplitudes by a factor, with clamping to [0.0, 100.0]. Amplitudes below 0.01 snap to zero (effectively killing the cycle).
# Strengthen: multiply all cycle amplitudes by 1.5x
directive.strengthen_cycle("relevance", factor=1.5)
# Weaken: multiply all cycle amplitudes by 0.6x
directive.weaken_cycle("relevance", factor=0.6)
These methods are used internally by ObservationProtocol to adjust cycles based on agent behavior outcomes:
- acted outcome calls
strengthen_cycle(factor=1.2)— reinforcing cycles that led to useful memories - dismissed outcome calls
weaken_cycle(factor=0.8)— dampening cycles for rejected memories - contradicted outcome calls
weaken_cycle(factor=0.5)— aggressively dampening contradicted memories
You can also call them directly for custom cycle management outside the ObservationProtocol.
Refreshing the Decay Clock¶
Redis Data Model¶
CyclicDecayField stores data in three Redis structures:
- Sorted set (inherited):
$CyclicDecayF:{Model}:{field}:{partitions}— member timestamps - Cycles hash:
$CyclicDecayF:{Model}:{field}:{partitions}:cycles— per-member cycle tuples (msgpack) - Pressure hash:
$CyclicDecayF:{Model}:{field}:{partitions}:pressure— per-member{rate, last_resolved}(msgpack)
All three structures are maintained automatically by on_save() and on_delete().
Scoring Formula¶
The extended Lua script computes per member:
elapsed_days = max((now - last_updated) / 86400, 0.01)
decay = base_score * elapsed_days ^ (-decay_rate)
cyclic = sum(amplitude * cos(2 * pi * (now - phase) / period) for each cycle)
pressure = pressure_rate * max((now - last_resolved) / 86400, 0)
effective_score = decay + cyclic + pressure
When companion hashes return nil (no cycle/pressure data), the overhead is two nil HGET lookups per member.
Error Handling¶
CyclicDecayField(cycles=[(0, 1.0, 0)])raisesModelException(zero period)CyclicDecayField(pressure_rate=-1)raisesModelException(negative rate)resolve_pressure()on unsaved model raisesTypeErrorresolve_pressure()on non-CyclicDecayField raisesTypeErrorresolve_pressure()withpressure_rate=0raisesTypeErrorstrengthen_cycle()/weaken_cycle()on non-CyclicDecayField raisesTypeErrorstrengthen_cycle()/weaken_cycle()on unsaved model raisesTypeError
Integration with ObservationProtocol¶
When used with ObservationProtocol, cycle amplitudes are adjusted automatically based on how the agent responds to surfaced memories. See Agent Memory — Four outcomes for the full effects table.