~6 min readgrounded in apps/worker/src/{learnings,graph,reputation,community,profile,visit,archives}.ts · apps/worker/migrations/0012_memory_graph.sql · apps/web/lib/chat/tools/memory.ts · apps/web/lib/rate-limit-curve.ts · apps/web/app/api/chat/route.ts
Memory, universe, community·
Three things make a deployment more than a chat box with a prompt: each account has a memory that follows the person across every tiny and device; every public tiny is part of a universe that other tinys can search and consult; and the people and tinys form a community graph whose gestures — follows, consults, visits — earn standing. All three are yours: they live in your D1 and your Vectorize indexes.
Memory: the fact graph·
A memory is a fact about the person, written by the agent with the learn tool (or read back with recall). Two tables in D1 (migrations/0012_memory_graph.sql) hold them:
| Table | Row |
|---|---|
entity |
one fact: id, owner (the user id), kind (fact · person · tiny · project · concept), label, attrs_json (source = the raw text), vec_id (its Vectorize id, or NULL when not indexed), visibility (private by default, public to share), valid_from, valid_to |
edge |
a typed link between two facts: src → rel → dst, with scope, weight, confidence, visibility, and the same validity window |
The graph is bitemporal: a fact is currently true while valid_to IS NULL. unlearn and supersedes close a row by stamping valid_to; nothing is hard-deleted, so "when did I change that?" is answerable by following the supersedes trail. The only destructive path is clear all (DELETE /learnings with no id), which also purges the vectors — and the tool requires an explicit scope: 'all' for it.
The legacy learnings table is still dual-written with a deterministic id scheme so both stores agree; the entity table is the source of truth.
Capacity, no silent eviction. MAX_ENTRIES = 5000 facts per account, ≤ 2000 chars each. A full store rejects the write with memory full (n/5000) — unlearn stale entries or consolidate before adding more instead of evicting the oldest, because an eviction-on-write policy once destroyed old memories one per save during bulk writes.
Every fact is embedded into the MEMORY Vectorize index with {userId} metadata, so recall is semantic and filtered per person. The private-memory read then re-checks user_id in SQL — the vector filter keys on the tiny's slug and slugs are recyclable, so the SQL predicate is the real owner check.
What the agent does with it·
On every POST /api/chat the route pulls the caller's 30 most recent facts plus a semantic match against the current message (GET /learnings?userId=&limit=30&q=<message>) into the system prompt. Beyond that pre-load, the tools:
| Tool | What it does |
|---|---|
learn |
store one self-contained fact. supersedes: [ids] closes the facts it replaces and links them; links: [{rel, dst, scope}] connects it to existing facts (part_of · authored · relates_to · about) so connected facts surface together; visibility: 'public' shares it with followers — only when the person explicitly asks |
recall |
semantic search over everything learned; hops: 1 expands through edges to connected facts |
unlearn |
close one fact by id, or everything with scope: 'all' |
memory_graph |
neighbors — the subgraph around a fact (depth 1–2, optionally filtered to relations); social — the public graph around user:<id> or tiny:<slug>, or the global trust ranking when no node is given; feed — what the people you follow have published |
memory_conflicts |
contradiction candidates — same subject and relation pointing at different facts in the same scope; facts in different scopes are context-bound and never flagged. Resolve by keeping one edge and closing the rest |
remember / forget are different: they write a short note into the browser's storage, survive history clears, and never reach the server. learn is the one that follows the person to their phone.
Archives are the third store: POST /archive keeps a full-fidelity, owner-private snapshot of a conversation (tool calls, usage, model ids — credentials redacted client-side) that can be restored on any device. Shares (/share) are the public, sanitised counterpart.
Universe: tinys that know each other·
Every public, active tiny is embedded into the VECTOR_INDEX on upsert; private tinys are removed from it. What a tiny is covers the three readers — /list, /retrieve (topK 9 with a private re-check) and ask_tiny — and the fact that every chat turn searches the universe for the current query and mounts the related tinys' skills as tools.
Consulting leaves a trace. When one tiny answers through another (ask_tiny while chatting as a named tiny), the chat route records a consulted edge tiny:<a> → tiny:<b> in the social graph, fire-and-forget. Those edges are what /universe draws as its constellation, and what the trust ranking is computed over.
/universe is the server-rendered directory: every builder with their public tinys, complete for crawlers.
Community: follows, visits, trust·
The social graph reuses the edge table with a platform owner and four relations — SOCIAL_RELS = visited · consulted · messaged · follows — between user:<id> and tiny:<slug> nodes.
- Follow —
POST /follow(follow / unfollow / check) is the one user gesture. Following a tiny or a builder puts their fresh public facts and new artifacts (tinys, forged tools) into your feed; unfollow closes the edge, re-follow opens a fresh one. - Visit — the chat page beacons
POST /visitonce per real browser pageview; the owner is told "someone is on your tiny's page right now", throttled. Deliberately not wired into/get, which fires for OG cards, vCards and every message. - Consulted — recorded automatically, as above.
Trust is PageRank over the public consulted edges (iterations = 10, damping = 0.85): a tiny consulted by well-consulted tinys ranks up. /community returns the ranking alongside the builder list, and the home page badges its tiny pills with it. Trust is per tiny; reputation is per person.
Reputation — standing, not money·
reputation.ts keeps points in their own table, never in the payments ledger: balance there is SUM(delta_micro) across all kinds at five money-critical sites, so a reputation row would become withdrawable USDC. Two invariants:
- Every point costs someone else a gesture. Following earns you nothing; being followed earns
follow_received: 10, and completing a mutual follow pays both sidesmutual_follow: 5. An account cannot follow 500 builders and buy itself past the rate limit. - Idempotent in the database.
UNIQUE(user_id, kind, ref)+ON CONFLICT DO NOTHING, so follow → unfollow → re-follow farming is a no-op even though the re-follow legitimately reopens a graph edge.
What it buys: rate-limit-curve.ts adds REQUESTS_PER_POINT = 5 chat requests per day per point, capped at MAX_REPUTATION_BONUS = 200. That is the whole effect — the login walls loosen for people the network has vouched for, and nothing else changes. GET /reputation returns the score with a per-kind breakdown; GET /profile?login= is a builder's public page (public tinys + forged tools), also reachable at /@<login> in the app.