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-architecture

Attractor — Adaptive Context and Personalization - Architecture

attractorcontext-and-memory

README · ARCHITECTURE · TECHNICAL · source


The shape of it

Two engines, two stores, and one shared core for applying state changes.

engines and coreengines and core

applyUpdate keeps the state-transition rules in one place, without storage or model I/O. The CLI, Worker, and comparison flow all use it, so their normal write paths share the same bounds and decay behavior.


Why two engines

The Engine interface has one primitive, call(system, user, model, maxTokens), and both implementations must place system and user in the same roles.

The reason is stated in the source: any comparison between them measures the asymmetry rather than the transport. Both send the identical prompt from buildUpdatePrompt and run the reply through the identical parseUpdate, so neither path can quietly drift from the other.

The practical motivation is access. The HTTP engine needs a pay-as-you-go API key. The claude -p engine uses whatever Claude Code is signed in as, so a subscription works. That makes the tool runnable by someone who has Claude but no API billing — and it makes the two paths comparable, which is what the compare command exists to exploit.


The update cycle

update cycleupdate cycle

The prompt asks for changes of about ±0.1. applyUpdate clamps to ±0.3 regardless of what comes back. That gap is deliberate: the prompt sets the intent and the code sets the limit, so a model that ignores the instruction cannot swing the system.


The safeguard layer

parseUpdate does not trust the model to return well-formed shapes or honest numbers. It clamps model-proposed deltas, and applyUpdate bounds the resulting weights. Direct callers still need to validate finite numeric deltas before they apply an update.

RuleValueWhy
Delta clamp±0.3The prompt asks for ±0.1; this is the hard limit if it does not comply
Weight floor0.05A basin goes dormant, never away, and a dormant basin can reactivate
Weight ceiling1.0
Decay of untouched basins5% of the gap to 0.3 per updateHalf-life ≈ 13.5 updates: drifts, does not swing
Seeded basin start0.5
Emergent basin start0.4A new basin has to earn its place
Keywords per basin10, deduplicated case-insensitively

The full derivation is in the state rules.


Keyword consolidation is a separate call

When a basin fills its keyword slots for the second time, its keywords are rewritten as five more general ones rather than evicted by recency.

The source gives the reason: eviction by recency means a basin's keyword list describes its last few conversations instead of 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.

It is a separate model call on purpose. The per-conversation update answers a local question ("what did this conversation do?") and is purely additive in practice: across 40 logged updates it proposed 115 keyword additions and zero removals. Abstraction is a global question about the whole basin, and one call asked to do both does neither well.


Storage

storagestorage

History is capped at 10 snapshots, each holding only the timestamp, per-basin weights, and which engine and model produced it. The source is explicit about what that is: a trend line, not an archive. It is enough to compute a trajectory and not enough to study one — which is a real limit on the study plan, and why that study freezes state and records versions explicitly rather than reading live.


Surfaces

SurfaceCommands or routes
CLIseed, ingest, sessions, compare, runs, history, context, show
WorkerGET /api/attractor, POST /seed, POST /basins, GET /history, POST /trigger, POST /ingest-transcript, POST /ingest
WebAttractorView.jsx — a force-directed view of basins and connections

attractor context prints the text block a calling application includes in its next model request. Attractor does not inject anything itself; the application using it is responsible for that, which keeps the tool honest about where its output goes.


Failing closed

Every /api/attractor/* route requires a bearer token matching the ATTRACTOR_TOKEN secret. Equal-length token contents are compared without an early exit; unequal lengths are rejected first.

If the secret is unset the Worker refuses all requests rather than serving them openly. The reason is written into the source: seed can wipe state and ingest spends your Anthropic key, so failing closed is the only safe default.


Snapshot

This architecture describes source snapshot 1ad1783. Recorded development examples help explain state changes; they are separate from a controlled evaluation.


Project overview · All projects