Context and Memory Workspace - Architecture
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.
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.
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.
| Cron | Jobs 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.
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
| Binding | Holds | Why not somewhere else |
|---|---|---|
| D1 | Conversations, messages, wiki pages and versions, tags, branches, themes, convergences, gossip | Relational, queryable, and the graph needs joins |
| KV | Attractor state and history, model status, rate limit counters | Small, hot, read on nearly every request; eventually consistent is acceptable here |
| R2 | Uploaded files and images | Blobs do not belong in a SQL row; object access needs its own boundary |
| Vectorize | Embeddings of conversations, wiki pages, branches, themes | Similarity search is not a SQL query |
| Workers AI | @cf/baai/bge-base-en-v1.5 embeddings | Embedding at the edge, no second provider, no egress |
| Durable Object | Shared room state and WebSocket delivery | Coordinates the real-time part of the experiment |
| Queue | Every job above | Keeps 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:
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 acrossclaude-api.tsand 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.
Read next
- source — the router, the queue consumer, the attractor state model, and the embedding pipeline, sanitized
- Project overview