Jennifer Nguyen

Bootwitch

Scientist building AI and research tools
10+ years in immunology research · Building with AI since 2024
Bootwitch / Interactive demo
Simulated session
Files
Terminal
Welcome to Bootwitch.
Projects / attractor-reference-state-rules

How the state changes

attractorcontext-and-memory

Implementation reference · updated September 11, 2026

Every rule the attractor follows, in one place. The state rules live in src/model.ts and snapshot retention in src/store.ts. The reference below describes the current implementation, followed by the direction I want to test next.

Current implementation

ConstantValueMeaning
SEED_WEIGHT0.5Starting weight for seeded basins
NEW_BASIN_WEIGHT0.4Starting weight for basins that emerge later
MIN_WEIGHT / MAX_WEIGHT0.05 / 1.0Weight bounds
DECAY_TARGET0.3Value untouched basins drift toward
DECAY_RATE0.05Fraction of the gap closed per update
MAX_DELTA0.3Hard clamp on model-proposed deltas
MAX_KEYWORDS10Keyword slots per basin
MAX_TRAJECTORY20Weight history kept per basin
ACTIVE_THRESHOLD0.4Above this, a basin is listed as active in the injected context

Weights

A basin's weight is its activity. Basins the model mentions move by the delta it proposes; basins it doesn't mention decay on their own.

touched:    w ← clamp(w + Δ,  0.05,  1.0)        Δ ∈ [−0.3, +0.3]
untouched:  w ← w + (0.3 − w) × 0.05

The safeguards happen in two places. parseUpdate clamps each proposed delta to ±0.3 as the model's reply is read. applyUpdate then bounds the resulting weight to [0.05, 1.0]. The update prompt asks for deltas between −0.2 and +0.2 and reserves large ones for conversations deeply about a topic, so the clamp limits numeric proposals outside the permitted range. These are separate boundaries: direct calls to applyUpdate do not clamp the delta, and malformed nonnumeric deltas can still produce NaN. Callers should validate finite numeric deltas before applying an update.

"Touched" means "listed in the model's basin_updates", not "increased". A basin listed with a delta of 0 or a negative delta does not also decay on that update. Only a positive delta counts as the basin being used: it updates lastActive and increments conversationCount.

Seeded basins start at 0.5, so nothing is favoured at seed time. Basins that emerge later start at 0.4, so they have to earn their place. The floor is 0.05, not 0: automatic updates retain dormant basins, and they can reactivate. The REST API separately allows explicit deletion.

Decay half-life

Each untouched update closes 5% of the gap to 0.3, so the remaining gap after n updates is 0.95ⁿ of the original. Solving 0.95ⁿ = 0.5 gives n ≈ 13.5 updates. This is the "drifts, doesn't swing" property, and it's the one number to change if the attractor feels too sticky or too twitchy.

Delta size depends on the model

In the small sample recorded in the development notes, Haiku proposed a mean delta of +0.126 across five basin-update entries and Opus +0.083 across four. These counts refer to basin-update entries. Each comparison leg generated its own summary, making this a small example of variation across the full pipeline. See comparing models.

What I want to test next

The current rules treat an omitted basin differently from one given a zero delta, and direct callers can bypass the parser's delta clamp. The next experiment is a single update rule: relax every basin a little toward baseline, apply a bounded conversation signal, then enforce the weight limits.

That is a proposed comparison, not the running implementation. I want to test it against repeated positive signals, alternating signals, quiet stretches, and omitted-versus-zero updates before choosing the signal strength or changing stored state.

Entropy

Normalized Shannon entropy over the weights, treated as a distribution:

pᵢ = wᵢ / Σw
H  = −Σ pᵢ log₂ pᵢ
H_norm = H / log₂(n)          n = basin count

0 means one basin holds everything; 1 means weight is spread evenly. Edge cases: Σw = 0 returns 1, and a single basin returns 0.

Entropy describes the spread of the stored weights. They are normalized by their sum, so the metric reflects their proportions. If every basin rises together the proportions barely move. In the reproducible manual walkthrough, entropy goes from 1.000 to 0.951 while the Systems basin rises from 0.5 to 1.0.

Untouched basins approach 0.3 rather than disappearing. Whether a decay step raises or lowers entropy depends on the whole weight distribution.

Trajectory

Computed from the most recent step of each basin, not a longer trend:

cᵢ = |trajectoryᵢ[-1] − trajectoryᵢ[-2]|
c̄  = mean(cᵢ)

converging     top basin grew  and  c̄ < 0.10
restructuring  c̄ > 0.15
diverging      c̄ > 0.05
stable         otherwise

The rules are checked in that order, so "converging" wins whenever the top basin grew and average movement was small.

Connections

Connections are symmetric. When the model proposes a connection from A to B, both basins record the other. Connections are only added, never removed by an update.

New basins and phases

If the model proposes a new_basin whose slugified label doesn't already exist, it is added at weight 0.4 with a conversation count of 1. The prompt asks for this to be rare. Up to 5 emerging_patterns are carried forward each update; these are how the model flags concepts that fit no basin, but the code does not require a pattern to recur before accepting new_basin. The two fields are separate.

phase_shift: true increments the phase counter. The prompt reserves it for a fundamental change of direction.

Keywords

Capped at 10 per basin and deduplicated case-insensitively. When additions push a basin past 10, the oldest keywords are dropped and the basin's capHits counter increments.

After the 2nd cap hit, the basin is eligible for consolidation. Local CLI ingestion invokes a separate model call: its keywords are rewritten as ≤5 more general ones, consolidationCount increments, and capHits resets. If the consolidation reply can't be parsed, the existing keywords are kept.

The update function supports keyword removal, but the development notes report 115 additions and no removals across 40 logged updates. That development observation motivated a separate consolidation prompt. Local ingestion runs the consolidation flow; the hosted routes apply the standard update.

Other bounds

Trajectory history20 points per basin
Snapshot history10 states
Active thresholdweight > 0.4 appears in the injected context
Emerging patterns5 carried forward

The injected context

buildAttractorContext renders the state for the next conversation's system prompt. Active basins (weight > 0.4) are listed with their weight, a trend arrow from their last step, their description and their connections. Dormant basins are named only. The block asks the model to use it for continuity but integrate it naturally rather than announce it. See attractor context for the exact text.

The renderer includes the current state, with text length growing as basins and descriptions accumulate. A fixed token budget is planned for the personalization study. The calling application supplies the rendered block to its next model request.


Project overview · All projects