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 / cloudflare-worker-project-architecture

Context and Memory Workspace - Architecture

context-notebookcontext-and-memory

README · ARCHITECTURE · TECHNICAL · source


The shape of it

One Worker serves both the static frontend and the API. Everything under /api/* is handled by the worker; everything else is served from the assets binding as a single-page application.

worker overviewworker overview

Route handlers are composed with ?? in a fixed order, each returning a Response if it matched and null if it did not. Specific routes come before parameterized ones. It is a hand-rolled router, and the ordering is load-bearing — that is a tradeoff I took knowingly in exchange for no framework and a very small bundle.


Two execution paths

The defining rule of this source snapshot: a user waiting on a response is not also waiting on derived memory work.

execution pathsexecution paths

The user-facing path does four things: authenticate, persist, stream, enqueue. Every derived artifact — summary, embedding, knowledge graph edge, attractor update, wiki snapshot — happens later, in a queue consumer.


Scheduled work

Two cron triggers drive the system when nobody is using it.

CronJobs enqueued
0 */6 * * *summarize_batch, water_cooler
0 3 * * *database_backup, archive_batch, r2_cleanup

summarize_batch is a selector, not a worker: it finds up to 20 conversations that have messages newer than their last summary and at least four messages total, then enqueues one summarize_conversation job each. Splitting "decide what to do" from "do it" keeps any single job small enough to finish inside a worker invocation, and makes a failure retry one conversation instead of twenty.

The four-message floor exists because a two-message conversation has nothing to summarize, and summarizing it anyway costs a model call and writes a useless row into memory.


The memory cascade

A single summarization is the root of a small tree of derived memory.

memory cascadememory cascade

The dotted edges are the point of the whole project. Memory is not just stored, it is fed back in — which means the system's output changes its own next input. That feedback loop is exactly what the Attractor study was designed to measure, because a loop like this can converge on something useful or slowly wall a person into their own past.


Storage, and why each one

BindingHoldsWhy not somewhere else
D1Conversations, messages, wiki pages and versions, tags, branches, themes, convergences, gossipRelational, queryable, and the graph needs joins
KVAttractor state and history, model status, rate limit countersSmall, hot, read on nearly every request; eventually consistent is acceptable here
R2Uploaded files and imagesBlobs do not belong in a SQL row; object access needs its own boundary
VectorizeEmbeddings of conversations, wiki pages, branches, themesSimilarity search is not a SQL query
Workers AI@cf/baai/bge-base-en-v1.5 embeddingsEmbedding at the edge, no second provider, no egress
Durable ObjectShared room state and WebSocket deliveryCoordinates the real-time part of the experiment
QueueEvery job aboveKeeps expensive work off the request path

The attractor lives in KV rather than D1 on purpose: it is a single document read on nearly every conversation start, and it is rewritten wholesale rather than updated in parts. Its eventual consistency is a real property, not an oversight — it is why any evaluation of the attractor uses a frozen state and an explicit version record rather than reading live.


The data model

The parts of the schema that carry memory rather than mechanics:

data modeldata model

Twenty-plus tables, built across six numbered migrations rather than designed up front — 001_phase1_queues_r2 through 006_knowledge_graph. Each migration is one capability, which means the schema history is also a record of the order the product was figured out in.


Knowledge graph growth

Branches are not created by hand. Summarization proposes branch names with a relevance score; the engine slugifies each name, finds or creates the branch, and links the conversation to it.

A new branch starts in state seedling. It has to recur across conversations to become anything more. This was a deliberate inversion of how tagging usually works: instead of me deciding in advance what my projects are, the projects are whatever keeps coming back.


What I would change

  • The router. Ordering-dependent ?? chaining is clear at fifteen handlers and will not be at thirty.
  • Attractor in KV. Correct for reads, awkward for anything that wants history; the history key is capped at ten snapshots and that is already limiting.
  • Model routing. Profiles (chatter, and others) select models per task, but the selection logic is spread across claude-api.ts and callers rather than being one table.
  • The water cooler's cost. It is charming, but any scheduled model experiment has to keep earning the calls it makes.

Snapshot

This architecture describes source revision 44dc238: request routing, queue jobs, storage bindings, and the memory update path. It is a structure walkthrough, not a performance measurement.

  • source — the router, the queue consumer, the attractor state model, and the embedding pipeline, sanitized
  • Project overview

Project overview · All projects