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-technical-notes

Attractor — Adaptive Context and Personalization - Technical

attractorcontext-and-memory

README · ARCHITECTURE · TECHNICAL · source


1. The safeguard layer centralizes state changes

A model asked to update a memory structure will occasionally propose a large swing, a malformed shape, or a number outside the range it was asked for. If any of that can reach stored state, the memory is only as stable as the last generation.

applyUpdate(state, update) → newState has no storage or model I/O. It applies a timestamp, bounds weights to [0.05, 1.0], decays untouched basins, and deduplicates keyword lists case-insensitively. The CLI, Worker, and experiment entry points use it; model-proposed deltas are clamped earlier by parseUpdate.

Centralizing the transition is what lets the same rules travel across two engines and two stores without being restated in either. It also makes the update behavior testable without a network.

Anything needing a model call cannot live here. When a basin fills its keyword slots, applyUpdate increments capHits and returns; the caller decides whether to consolidate. That is an extra round trip and an extra piece of state.

The clamp is symmetric and fixed. A genuinely large, correct change — a real shift in how someone works — is clamped exactly as hard as a model error, and the system takes several updates to catch up. That is the intended behaviour ("drifts, does not swing") and it is still a limitation.


2. Two engines behind a one-method interface

The HTTP API needs pay-as-you-go billing. Many people who have Claude do not have that. Running Attractor should not require a second kind of account.

The Engine interface has a single primitive: call(system, user, model, maxTokens), implemented over the Anthropic HTTP API and over the local claude -p binary. Both send the identical prompt from buildUpdatePrompt and parse with the identical parseUpdate.

Stated in the source: both engines must place system and user in the same roles, or any comparison between them measures the asymmetry rather than the transport. Since the compare command exists to compare engines and models, an interface that permitted drift would manufacture findings.

One method is a narrow interface. Anything a transport could do better — streaming, tool use, batching — is unavailable to both because it is unavailable to the comparison.

The CLI engine depends on whatever Claude Code is signed in as, which is not recorded in the same way an API model id is. Provenance captures engine and model where known; "where known" is doing real work in that sentence.


3. Keyword abstraction instead of eviction

A basin's keyword list is capped at ten. The obvious eviction policy is recency, and it is wrong.

The second time a basin fills its slots, a separate model call rewrites its keywords as five more general ones. Then consolidationCount increments and the counter resets.

Eviction by recency means the list describes a basin's last few conversations rather than its identity — the concepts that founded it get pushed out by whatever arrived most recently. Abstraction keeps the shape and drops the specifics, which is what a mode of engagement is as opposed to a topic.

Separating it from the per-conversation update is supported by the logs: across 40 logged updates the update call proposed 115 keyword additions and zero removals. It will not prune, so pruning must be a different call with a different question.

An extra model call, and a lossy one. Consolidation genuinely discards specificity; a keyword that mattered can vanish into a general term.

It triggers on the second cap hit, which is a guess. A basin that fills slowly and one that fills in a week get the same treatment.


4. Entropy measures spread, and the source says so

Normalized Shannon entropy over basin weights looks like a focus metric and is not one.

Weights are normalised by their sum before entropy is taken, and the docstring says plainly that the result measures how evenly attention is spread, not how uncertain it is. computeTrajectory carries a matching warning that it reads the last step only.

Both numbers are printed in CLI output and stored in every snapshot, so they will be read by someone — including me, months later — who did not write them.

For now, I keep the metric and its warning together instead of pretending it answers a question it does not.

Limitation, measured. Because proportions barely move when every basin rises together, entropy went 1.000 → 0.986 across seven updates while the dominant basin went from 50% to 100% of the weight. A metric that moves 1.4% while the thing you care about doubles is close to useless for that question, and the honest fix is an additional concentration measure rather than a better warning.


A number worth carrying forward

Measured across 40 logged updates, Haiku proposed a mean weight delta of +0.126 and Opus +0.083. Both are inside the ±0.3 clamp; Haiku's mean is outside the prompt's requested ±0.1 range, while Opus's is inside it. The sample is small and includes variation from the full summarization-and-update pipeline, but the difference was interesting enough to motivate a more careful model comparison.



Project overview · All projects