Skip to content

~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 /visit once 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:

  1. Every point costs someone else a gesture. Following earns you nothing; being followed earns follow_received: 10, and completing a mutual follow pays both sides mutual_follow: 5. An account cannot follow 500 builders and buy itself past the rate limit.
  2. 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.