CLAUDE.md · git:20261105.c65b6d5 · 2026-11-05 · sha256 216d711ae9816d1c
CLAUDE.md git:20261105.c65b6d5A
Immutable. This exact content never changes and is served at /api/v1/blob/216d711ae9816d1c.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Mnemion
Persistent, evolving shared memory between a human and their AI agents. MCP server on Cloudflare Workers.
Source is organized into **component directories**, each with a co-located
`*.spec.md` containing its intent, entrances, and named requirements. The
installed [PostHog Coherence](https://github.com/PostHog/coherence) package
checks those specs and delivers vocabulary and findings through Claude and Codex
hooks. `entities/` holds the Durable Objects and domain logic; `shared/` holds
cross-cutting primitives; `src/` holds the worker entry, render fragment,
system docs, and tests. The React web app lives under `web/`.
Per-doc roots: `project-docs/archived/` (design docs), `mnemion-js/` (the
worker), `Mnemion.spec.md` (root spec), `coherence.config.json` (harness
config), `scripts/setup.sh` (first-run setup).
Peer: `mnemion-swift/` — the native SwiftUI client (iOS/iPadOS now; macOS and
visionOS build from the same target; watchOS declared in the `MnemionKit`
package). A pure client of `/api/*` + `/ws` via OAuth bearer, no replica. See
`mnemion-swift/README.md`; the Xcode project is generated by XcodeGen.
The current component and invariant reading is available with
`cd mnemion-js && npx --no -- coherence scope`. The old generated graph is
historical material under `mnemion-js/docs/coherence/`; it is not refreshed by
the new package.
## This repository is public
Everything pushed here is readable by anyone: branches, commits, PR and issue
text, comments and images. A hive is private. Keep the two apart.
- **No hive content in the repo or on GitHub.** That covers entry text, task,
project and goal names, member names and hive hostnames, in code, tests,
fixtures, docs, commit messages, PR text and comments. Use invented examples.
- **No screenshots of a signed-in app on GitHub.** A build signed in to a real
hive shows that hive's data. Don't attach them to a PR or issue, and don't push
them to any branch, including a throwaway one. Don't add screenshots to a PR
unless the owner asks for them and they show only invented data. To show the
owner a real screen, send it to them directly, off GitHub.
- **Deleting does not unpublish.** A deleted branch's commits stay fetchable by
SHA, and PR edit history stays visible. Purging takes a GitHub Support request,
so check what a push contains before making it.
## Current state
Deploys as a single Cloudflare Worker that exposes MCP at `/mcp`. Cross-surface data sharing proven (Claude Code + Claude.ai reading/writing the same store).
### Architecture
Two Durable Objects:
- **SessionDO** (`McpAgent`) — one per MCP session, handles protocol, proxies to hive via RPC
- **HiveDO** — one per user, holds all SQLite data. Keyed by `user:{userId}` (currently always `"owner"`)
HiveDO is a thin wiring shell. Domain logic lives in pure-function modules with `db`/context injected:
- **`data.ts`** — query, mutate, search (DataContext)
- **`evolution.ts`** — schema evolution via `CHANGE_TYPES` declaration table (EvolutionContext)
- **`credentials.ts`** — passkey + token operations (db)
- **`kernel.ts`** — pre-mutation hooks, immutable fields, scope matching
- **`schema.ts`** — DDL, migrations, kernel table declarations, audit triggers
### Auth
Layered auth behind OAuth 2.1. The `workers-oauth-provider` package wraps the worker and handles the OAuth flow (DCR, tokens).
- **Master secret** (`MNEMION_SECRET`): High-entropy random hex, set via `npm run setup`. Root of trust — used to bootstrap the owner passkey and as fallback for headless agents. The user's Cloudflare login is the true credential; the secret is ephemeral and replaceable. Master-secret logins resolve to the `owner` actor.
- **Passkey** (WebAuthn): Optional convenience layer for browser-based OAuth. Registered via one-time setup URL. Stored in HiveDO `_passkeys` table — **one credential per member** (re-registering a member replaces only that member's row). If any passkey is registered, `/authorize` shows passkey-first UI with secret fallback. Authentication offers all members' credentials and resolves the actor from the one used.
- **Client ID Metadata Documents** (CIMD): an MCP client can use an https URL as its OAuth `client_id` instead of registering through DCR (which stays on). `workers-oauth-provider` fetches the JSON document at that URL, requires its `client_id` to match the URL, and takes the redirect URIs from it (`clientIdMetadataDocumentEnabled` in `src/index.ts`; needs the `global_fetch_strictly_public` compatibility flag, which also keeps every other outbound fetch off same-zone routing). The `/authorize` page names the client: a CIMD client by its verified host, a DCR client as self-registered and unverified (`clientLine` in `routes/auth.ts`). Invariant: `client metadata document clients` (Routing.spec.md).
- **Access tokens** (`_access_tokens`): Scoped bearer tokens for remote agents, uploads, marketplace, and federated access. Created via `mutate`. Scope matching is hierarchical prefix-based. Optional `member` column attributes a token to a member; the OAuth external-token path resolves the session's actor from it (member-less → `owner` sentinel; suspended/archived member → refused).
- **OAuth bearer on session routes** (native clients): every `Auth.SESSION` route (`/api/*`, `/ws`, `/export`) also accepts `Authorization: Bearer <oauth access token>` — the same tokens `/mcp` takes, minted by the passkey `/authorize` flow (`validateBearer` in `shared/Routing/router.ts`). A presented bearer is decisive (invalid → 401, never a cookie/dev fallback); the grant must name `HIVE_ID`, any token audience must cover the request (an MCP-bound token is refused at `/api`), and the member is re-checked active per request. Raw `_access_tokens` values are NOT accepted here (only on `/mcp`). Invariant: `session bearer gate` (Routing.spec.md).
- **No secret configured** = **fail CLOSED** (owner-only `Auth.SESSION` APIs return 503), UNLESS `DEV=true` is explicitly set — then it's dev mode (auto-approves as owner). Only local dev commands set `DEV`; neither production nor the deployed test environment does. "Unconfigured" means no access, not all access (`isDevAutoApprove` in `shared/Routing/router.ts`).
### Shared hive (multi-member)
One hive, several people. Hive identity (which Durable Object) is decoupled from actor identity (which person): `HIVE_ID` (`shared/core/constants.ts`, literal `"user:owner"` — a rename for clarity, not a re-key) names the single store every member authenticates into; the authenticated member's label rides in the OAuth session props as `actor`, separate from `hiveId`. Design doc: `project-docs/archived/shared-hive.md`.
- **Members** (`_members` kernel pattern): the roster. `label` (immutable handle, the join key for passkeys/tokens), `display_name`, `role` (`owner`|`member`), `status` (`active`|`suspended`). The `owner` member is seeded on boot and reserved. Creating a member is consent-gated at the MCP layer (it grants standing access to the whole hive).
- **Invite flow**: (1) create the `_members` row with `label` + `display_name` (the inviting agent names them); (2) mint a `register`-scoped access token with `member: "<label>"` (forced single-use; the hook refuses `owner` and any non-active-roster member); (3) the token is minted **inert** — an existing member must approve it in person at `/invite/{token}` via passkey (master-secret fallback) before it works; (4) once approved, give the invitee its `/setup?token=...` URL, where they register their own passkey, bound to that member. The setup endpoints accept either the master secret (owner bootstrap) or an **approved** `register` token (invitee). The passkey approval — not the agent-satisfiable mutate round-trip — is the real human-consent gate: `approved_at` is IMMUTABLE on the mutate path and set only by the approval endpoint, so an agent acting on injected content can mint an invite but never activate it.
- **Revocation**: suspend (`status: suspended`) or archive a member → their passkey logins and tokens stop resolving; also archive their `_access_tokens` rows. The global session epoch (`/sessions/revoke`) remains the all-at-once panic button.
- **Attribution** (per-entry `created_by`/`updated_by`): every write stamps the session actor onto the entry. `created_by`/`updated_by` are kernel columns (auto-provided on every pattern, like `created_at`; not facets), set by the mutate engine from the actor — MCP writes from the session props (`session.ts`), browser writes from the session cookie (which now carries the member, backward-compatibly), internal/ingress writes default to the `owner` sentinel. Caller-supplied `created_by`/`updated_by` are stripped (`executeMutate`) so attribution can't be forged. Surfaced automatically in `query`/`resolve`/prime row reads.
### Vocabulary
Mnemion uses biological vocabulary at every layer — API parameters, URIs, JSON response keys, and docs: **hive** (the whole store), **pattern** (organizing structure), **entry** (instance within a pattern), **facet** (dimension of an entry), **link** (connection between entries). Internal SQL tables (`_objects`, `_fields`) keep implementation names but are never exposed to agents.
### Resources (stable, cacheable, subscribable)
- `mnemion://index` — master index
- `mnemion://schema/{pattern_name}` — facet definitions for a pattern
- `mnemion://history` — schema evolution history (supports `?limit=N`)
- `mnemion://entry/{pattern}/{id}` — individual entry by URI
- `mnemion://stale` — entries past their staleness horizon (supports `?days=N`); review surface for maintenance passes
- `mnemion://review` — what needs attention now (`entities/Hive/review.ts`; also `GET /api/review`): due review hooks (day/week/month start and end, derived from the latest entry of each kind in the hive's reviews pattern against the charter `timezone` and `review_day_end_hour`), overdue/due/stalled entries of every pattern with `deadline` + `status` facets (timestamps read on their day in the charter timezone), and alignment gaps along chained relations (`entriesLacking` in relations.ts; closed entries and the charter `review_exempt` refs are skipped). Its one-line `message` rides the MCP init briefing and the prime response. Protocol: `src/system-docs/reviews.md`.
- `mnemion://relations` — the relation vocabulary (every label a link may carry); also the index's `relations` key
### Tools (8 total)
Tool metadata is centralized in `entities/Session/tools.ts` (SSOT for `session.ts` MCP registration + `/api/tools` frontend; the `tools SSOT totality` test reconciles the `TOOLS` rows against the actual `session.ts` registrations in both directions, so a tool can't be registered without a row or left as a stale row):
- `prime` — auto-associative recall: pass conversational context, get semantically-nearest entries + one-hop links. Workers AI embeds entries on write, Vectorize indexes them, prime queries KNN. Relevance is weighted at read time: superseded entries demoted ×0.3 + annotated `superseded_by`; patterns with a memory-policy half-life decay by `0.5^(age/half_life)` where age runs from the later of `updated_at` and the last prime hit (`_entry_access_log` — recall is rehearsal). `raw_similarity` is kept alongside weighted `relevance`. A `maintenance` field appears when a cleanup pass is overdue.
- `resolve` — read anything by `mnemion://` URI, including federated cross-hive URIs and `mnemion://web/<https-url>` for adapter-cached web fetches (escape hatch for platforms without resource support). Entry reads list `linked` items one per link, each with `direction` (`out`/`in`) and `relation` (the name read from the reader's side; `label` keeps the canonical name)
- `query` — filtered, sorted, paginated reads (supports `count_only` mode). Filter operators: `= != > < >= <= ~` (LIKE) and `|=` (IN, comma-separated values — e.g. `id|=1,3,7` for batched multi-id lookups)
- `search` — cross-pattern full-text search across text facets
- `mutate` — create, update, or archive entries. Single creates in user patterns run a policy-gated write-time conflict check (synchronous embed + KNN ≥0.80 same-pattern; the vector is reused for the post-write upsert) and return `possible_overlap` advisories; exclusive-facet duplicates advise via cheap SQL (batch included). Advisory only — never blocks.
- `propose_change` — propose schema evolution (preview, no commit), including the relation vocabulary (`declare_relation` / `amend_relation` / `retire_relation`)
- `apply_change` — commit proposed change (fires resource update notifications)
- `render` — visual twin of the read tools: returns a rich UI table (`view=patterns` or `view=entries`) via the `ui://mnemion/render` fragment in MCP-Apps-capable hosts, with a text fallback everywhere else
### HTTP I/O & Federation
Agent-defined HTTP endpoints, configured as entries:
- **Shared entries** (`_shared`): `GET /o/entry/{pattern}/{id}` — serve entries marked public or unlisted
- **Egress** (`_outputs`): `GET /o/{path}` — serve agent-constructed content at arbitrary paths
- **Documents** (`_documents`): R2-backed file store. `_documents` entry holds agent metadata (title/description/tags/visibility) + system-managed blob bookkeeping (`r2_key`/`size`/`content_type`/`stored_at`, IMMUTABLE); bytes live in R2 (`DOCUMENTS` binding), never the hive. Two-step: `mutate _documents create` auto-mints a single-use `document`-scoped token and returns `upload_url`; `POST /f/{token}` streams to R2 (≤25 MB) and records the key. Served at `GET /f/{id}`, visibility-gated like `_shared` (private 404 / unlisted token `read:document:{id}` / public edge-cached). Archiving the entry deletes the R2 object (waitUntil). Non-private visibility is consent-gated (visibility-aware, like set_sharing). R2 key is fully random (non-enumerable). On upload, text is extracted into `extracted_text` (text-family inline via `shared/IO/extract.ts`; PDF via `unpdf` in `ctx.waitUntil`) and the entry re-embedded, so document contents are searchable (`search`) and recallable (`prime` — `_documents` is in prime's KERNEL_INCLUDE); `extraction_status` reports done/pending/failed/unsupported. Chunked embeddings for long docs are a future seam.
- **Publications** (`_publications`): `GET /p/{path}` — live pattern projections rendered at request time (never stored). Formats: html (default styles + owner `css` override), rss, json, markdown (YAML frontmatter). Source query reuses the data.ts query engine (filters/facets/sort/limit pass through); superseded entries excluded by default. Per-entry `template` seam: `{{facet}}` + `{{_label}}`/`{{_uri}}`/`{{_id}}`/`{{_updated_at}}` — template text raw, substituted values HTML-escaped in html/rss. Source must be a user pattern (kernel patterns never publishable). Creation is consent-gated at the MCP layer like `_shared`. ETag = max(publication, served entries) `updated_at`; public responses edge-cached 60s.
- **Ingress** (`_inputs`): `POST /i/{path}` — accept inbound data, create entries in target patterns with optional transform DSL
**Federation**: `resolve` recognizes foreign URIs by a dot in the first path segment (hostname). `mnemion://other.hive.dev/entry/axioms/7` → `GET https://other.hive.dev/o/entry/axioms/7`. Private access via `?token=<auth_code>` → Bearer header. Public responses edge-cached. No federation protocol — sovereign hives, voluntary connections.
Federation is gated by a consent allow-list (`_federation_hosts` kernel pattern): `resolve` refuses any cross-hive URI — and never sends a token — unless the target host has an active entry there. Loopback/private/link-local/internal hosts (incl. cloud metadata) are blocked outright and can't be allow-listed (`isBlockedFederationHost` in kernel.ts). Adding a host is consent-gated at the MCP `mutate` layer (confirmation round-trip, also blocked inside batches), so an agent acting on untrusted content can't silently leak the owner's token to an attacker host.
### Memory maintenance (contradiction management + decay)
All read-time-derived, owner-ratified, never auto-deleting:
- **Supersession**: a link whose relation declares the `supersession` behavior — the kernel `supersedes` (source supersedes target), or any hive relation declaring it. Prime demotes + annotates; entry resolution and the stale view annotate `superseded_by`; publications exclude it unless opted in. Entries are never hidden. Every reader asks `relations.ts` (`supersededBy` / `supersededAmong`) — no module filters `_links` on a label literal.
- **Memory policy**: `memory_policy` JSON column on `_objects` (beside `doctrine`), set via the `set_memory_policy` change type. Shape: `{half_life_days?: number|null, conflict_check?: "annotate"|"off", exclusive_facets?: string[]}`. Defaults when unset: conflict check on, decay off. Surfaced per-pattern in the index. Kernel patterns can't carry policy.
- **Decay**: `decayMultiplier` in prime.ts — `max(0.05, 0.5^(age/half_life))`, ranking only, never data. `_entry_access_log` records prime hits on user-pattern entries.
- **Stale view**: `mnemion://stale` — per-pattern horizon of 3× half-life (90 days without a policy). Its `held_patterns` lists archived patterns past the 30-day drop that inbound links from outside keep alive (`{pattern, archived_at, days_archived, held_by: [{source, target, label}]}`), derived from the drop's own predicate (`PAST_DROP_HORIZON_SQL` + `notLinkTargetPatternSql` in relations.ts) — never auto-dropped; a maintenance pass reviews them with the owner (archive/re-point the links, or unarchive the pattern).
- **Maintenance passes**: `_maintenance_passes` kernel pattern. Days-since derived from the latest row; interval from charter key `maintenance_interval_days` (default 14). Overdue status injected into MCP init instructions (session.ts) AND the prime response (web clients never see init instructions).
- Agent protocol lives in `src/system-docs/memory-maintenance.md`.
### Relations
Link labels are declared relations — schema, like patterns and facets. Design and the Phase 1/Phase 2 interfaces: `project-docs/relations.md`. One home: `entities/Hive/relations.ts` (the only worker file that compares a label to a relation name or filters `_links` on a label).
- **Registry**: `_relations` kernel pattern (write class System — changed only via `declare_relation` / `amend_relation` / `retire_relation` in `CHANGE_TYPES`, logged to `_schema_history`). Row: `name` (immutable, = `_links.label`), `inverse`, a ≥120-char `description`, `source`/`target` (JSON arrays of patterns or `["*"]`), `cardinality` (`one`|`many`), `behaviors`, `required`, `chain` (a derived relation's steps; NULL when stored), `kernel`. Legacy status is derived, never stored (`legacy_debt`: placeholder description and/or generated `<name>-inverse`, read from current values; the stored `legacy` column is dropped at boot, v18; the generated inverse form is reserved for declare/amend). Kernel defaults `supersedes`, `references`, `depends-on`, `derived-from`, `part-of` are seeded and refreshed from code at boot (`KERNEL_RELATIONS`).
- **Behavior palette** (`BEHAVIOR_PALETTE`): `supersession`, `acyclic`, `containment` (`part-of` carries it). A behavior joins the palette only with its enforcement and an enforcement test (palette totality).
- **End state** (`endStateViolations`): every mutate and every batch runs in one transaction (`DataContext.atomic`, `executeMutate` / `executeBatch` in data.ts); the engine records what each write touched and judges two rules once, at the end, before commit — a *required* relation (every active entry of its explicitly listed source patterns holds a link; archived entries exempt) and *containment* (an archived entry contains no active member). So a move (archive old link + link new) or a container archived with its members passes in any order; anything else rolls back. Boot reports pre-existing required gaps (`findRequiredGaps`), never repairs. `archive_pattern` on a container pattern is blocked while members in other patterns link in (`containmentHolders`). Prime's recall follows containment upward only (`followLinks` `recall` vs `resolve` mode).
- **Inline `_relate`** on create (`takeRelate` in data.ts): the entry's links cross the `_links` chokepoint in the entry's own transaction; any refusal rolls the entry back; `links: [{id, source, target, label}]` in the result. Create only. Works for RPC, batch, `/api/mutate` and MCP. Batches also expand `link`/`unlink` shortcut items (operation optional).
- **Derived relations** (`chain`): ≥2 composing stored steps, no ends/cardinality/behaviors/required of their own (ends read off the steps); never written (the chokepoint refuses the name, naming the steps); `resolve` adds `derived: true` items (`derivedLinksOf`, active links between active entries, deduplicated). A step can't be retired while a chain uses it.
- **Reference facets retired (Phase 3)**: `create_pattern`/`add_facet` refuse `links` (pointing to `declare_relation`); no reader treats `_fields.references_object` as a connection (schema, index, export, `followLinks`, `archive_pattern` impact); the `reference` format is gone from `format-palette.ts`. Connections are relations + links only. `findStoredDerivedAggregates` reads containment (`containmentChildren`).
- **v19 fork migration** (`migrateReferenceFacets`, relations.ts, isolated like v18). Governing rule: v19 never drops a column whose values did not ALL become links — `planReferenceFacets` (also v19's restore-point `firstRun`: migratable work only) resolves every non-null value through the FK's target column (`PRAGMA foreign_key_list` `to`) to exactly one row id of an admissible target, else the facet is KEPT (column, data and `_fields` row untouched, nothing declared, a `<pattern>.<facet> kept` warning every boot). `_fields` names match columns case-insensitively, as SQLite does. Interrupted swaps are recovered first (`recoverInterruptedRebuilds`) — only an `x__rebuild` table NO `_objects` row names is a leftover, and `create_pattern` refuses names ending in `__rebuild` as reserved. The plan orders the rebuild (`rebuildPlan`): tables nothing still references go first; a reference cycle is broken, after its links are written, by clearing ONE nullable FK column on it (`nullColumns`), so a NOT NULL side's table is rebuilt first — one boot; a cycle with no nullable column (or a table a kept facet references) can't be rebuilt, so its facets are KEPT and reported. Each migratable facet `P.f → T` becomes a legacy relation (kebab name of `f`, else `<P>-<f>`; source P, target T, cardinality one, optional; placeholder description + generated inverse), one link per non-null value (archived rows → dormant links), and its table is rebuilt without the column through `rebuildTableWithout` (audit-triggers.ts — the trigger-safe sibling of `dropColumn`; referencing tables rebuilt before the tables they reference). A NOT NULL facet records `_relations.required_review = 1` → the third legacy debt `was mandatory` (boot warns `<relation>: was mandatory, consider required`), cleared only by an amend that carries `required` explicitly. Stored `reference` formats clear. The "trigger-safe drops" structural test keeps every other DROP COLUMN/TABLE literal out of worker code.
- **Web relations (P3-C/E)**: `shared/core/relation-view.ts` (pure, tested in the worker suite) — `createRelationFields` (one picker per required stored relation the pattern is a source of), `buildRelatePayload` (the `_relate` value), `entryRelationsView` (resolve's `linked`, grouped). `web/src/relations.tsx` renders them: the "+ new" create form (one picker per field, posts `_relate`) and the relations section in the detail and peek dialogs (chips open a peek). The dev seed declares `advances` (tasks → goals, required) instead of a `goal_id` reference facet.
- **`drop_facet`** (`CHANGE_TYPES`): preview `non_null_values`; refused for kernel patterns/columns, a pattern's only facet and reference facets (Phase 3), and — via `preview.facet_references`, re-checked at apply — while a memory policy, view, publication, clipboard, ingress endpoint or live upload token names the facet (`entities/Hive/facet-references.ts`, one finder per kind). Change types declare such blockers in one `blockers` field (`violations` / `facet_references`). `applyChange` captures every change's PITR bookmark BEFORE `handler.apply`, so reverting a history row restores the state before that change.
- **Canonical datetime**: `datetime` facets in every user pattern (knowledge too) store ISO 8601 — `YYYY-MM-DD` date-only, else a full UTC timestamp; periods (year, year-month), impossible days (no silent rollover) and relative phrases refused; existing values untouched until written. The upload path (`consumeUpload`) runs the same coercion (`data.coerceFacetValue`).
- **Unarchive**: the end state also refuses an unarchived member under an inactive container; `unarchive_pattern` blocks on members under archived containers and on a required relation's lacking entries; `required: true` is refused while a source pattern is archived.
- **Shortcuts** live in the `shortcuts.ts` leaf (re-exported by kernel.ts); the MCP consent gate (`mutate-gate.ts`, single and batch) judges expanded items. `_relate` links put `_links` in the `/ws` `changed.patterns`.
- **Link chokepoint**: the `_links` ON_CREATE hook calls `checkLinkWrite` — kernel-endpoint check (before label resolution), declared label (an inverse name swaps to canonical direction), canonical source never kernel, active endpoints, relation's patterns, (source, target, label) uniqueness, cardinality, acyclic (recursive CTE). Refusals are engine-authored and list `available_relations`. Endpoints and label are `IMMUTABLE_AFTER_CREATE`; ON_WRITE re-runs every check on unarchive. Unlink resolves to exactly one link (`resolveUnlink`), refusing when ambiguous.
- **Targets**: no kernel pattern is a link source; `_documents` and `_web_cache` may be targets.
- **Boot** (`migrateRelations`, schema v17/v18), in order: drop the pair-only `_links_pair_active` index; drop the stored `legacy` column (v18); (1) seed/refresh the kernel defaults; (2) rewrite legacy labels — unlabeled → `references`, inverse wording → canonical name with ends swapped; (3) sweep active links whose endpoint is gone (pattern dropped or entry row deleted) or is an inadmissible kernel pattern — after the rewrite, so a link whose canonical form lands on a kernel source is swept the same boot; a link to an archived entry/pattern is never swept but stays active as a *dormant* link (hidden by reads, still counted for uniqueness/cardinality, back on unarchive), so every cold-start boot gives the same result; (4) declare remaining labels verbatim as `legacy` relations (ends inferred from usage; generated inverses avoid every declared handle and every in-use label). A second boot changes nothing. Boot then warns for every legacy relation, naming its remaining debt (e.g. `refines: generated inverse`), for every active entry lacking a required link, and, via the pure `findCycles`, one line per pre-existing cycle across an acyclic relation — reported, never auto-repaired.
- **Restore point / rollback**: on the first v17 boot (no `_relations` table yet) `bootSchema` (schema.ts) captures a PITR bookmark via `ctx.storage.getCurrentBookmark()` before any v17 step and records one `_schema_history` row, `change_type: "restore_point"` (bookmark NULL if unavailable — boot never fails). To roll Phase 1 back: find that row in `mnemion://history`, call MCP `apply_change` with `revert_history_id` (a consent round-trip — call, then confirm; it restores the whole DO to before the migration), then `wrangler rollback` the worker so old code doesn't re-migrate. See `project-docs/relations.md` "Rolling back Phase 1" for the pair-index caveat. Phase 2 adds a second declared restore point (`MIGRATION_RESTORE_POINTS`, v18: first boot that still has the stored `_relations.legacy` column, which Phase 1 code reads and writes); roll Phase 2 back the same way (revert that row, then `wrangler rollback`). Phase 3 adds v19 (first boot that finds a `_fields` row with `references_object`), reverted the same way.
- **GC**: a link protects its target everywhere — the `_web_cache` GC skips active link targets (`NOT_LINK_TARGET_SQL`), and the 30-day archived-pattern drop skips a pattern targeted by an active link from outside it (`notLinkTargetPatternSql`). Both derived in the query, no flag.
- **Read surfaces**: index `relations`, `mnemion://relations`, `mnemion://schema/<pattern>` → `relations: {as_source, as_target}`, the prime response's `relations: [{name, inverse}]`, and the MCP init instructions.
### Entry sharing
Entry-level visibility via the `_shared` kernel pattern. Controlled through `propose_change` with `set_sharing` type:
- **public** — openly readable at `/o/entry/{pattern}/{id}`, edge-cached
- **unlisted** — readable with valid access token (anyone-with-the-link)
- **private** — not served (default; removes sharing)
### Access tokens
Unified `_access_tokens` kernel pattern (replaced `_auth_codes`, `_upload_tokens`, `_marketplace_tokens`). Hierarchical scope matching via `scopeMatches()` (kernel.ts, re-exported by credentials.ts):
- `*` — full access (OAuth, session login, all reads/writes)
- `read` — read any shared entry or output (matches `read:entry:axioms:7`)
- `upload` — write via `/upload/{token}` (constraints JSON: `{target_pattern, target_id, target_facet, mode}`)
- `marketplace` — private marketplace git access (constraints JSON: `{plugins: [...]}`)
- `register` — one-time passkey-registration link for inviting a member (constraints JSON: `{member}`; forced single-use). Minted **inert**; requires a member's passkey approval at `/invite/{token}` (sets `approved_at`) before `/setup` accepts it. Not usable as an API token (doesn't match `*`); rejects `owner` and non-roster members.
### Internal tables
- `_schema_history` — log of all schema changes
- `_pending_changes` — proposed but uncommitted changes
- `_access_tokens` — unified scoped access tokens (optional `member` attribution)
- `_members` — roster of people sharing the hive (label/display_name/role/status); `owner` seeded + reserved. See "Shared hive" under Auth.
- `_passkeys` — WebAuthn credentials, one row per member (`member` column; NULL = owner bootstrap). Not agent-facing (created via the setup flow, not `mutate`).
- `_shared` — entry-level sharing for HTTP access
- `_outputs` / `_inputs` — HTTP I/O endpoint definitions
- `_publications` — declarative outbound projections served at `/p/{path}`; rendering is always derived, never stored
- `_documents` — file-store metadata (title/tags/visibility + system-managed r2_key/size/content_type/stored_at); bytes live in R2, served at `/f/{id}`
- `_system_docs` — agent orientation docs (seeded from `src/system-docs/*.md`)
- `_web_cache` — adapter-fetched web content (Bluesky threads 24h, browser-rendered markdown 30d). TTL = re-fetch horizon, not eviction: active content is retained indefinitely as memory; only superseded duplicates are GC'd (7-day grace, pinned excluded). A re-fetch that returns empty keeps the existing snapshot (snapshot protection in web.ts). `pinned` column + `resolve(retain: true)` freeze a snapshot forever (always served, never re-fetched/GC'd) until `retain: false`; pinning stays system-managed (`_web_cache` is write-class System in `entities/Hive/policy.ts` — driven only through resolve, never agent mutate)
- `_fragment_access_log` — append-only log of prime hits per short-term fragment. Promotion to `_long_term_fragments` is COUNT(*)-derived from this log, not a stored counter. GC'd alongside fragments. Audit-exempt (high-frequency append-only).
- `_entry_access_log` — append-only log of prime hits per user-pattern entry. Feeds decay (`last_touch`) and the stale view. Audit-exempt, write-protected, GC'd at 90 days.
- `_maintenance_passes` — record of completed memory-maintenance passes; days-since-last-pass is derived from the latest row, never stored.
- `_relations` — the relation vocabulary for `_links.label` (see "Relations"); System write class, evolved only through the relation change types.
## Tech stack
- **Runtime**: Cloudflare Workers + Durable Objects (SQLite storage)
- **MCP**: `agents` npm package (`McpAgent` class), `@modelcontextprotocol/sdk`
- **Auth**: `@cloudflare/workers-oauth-provider` (OAuth 2.1 + PKCE + DCR), `@simplewebauthn/server` for passkeys
- **AI**: Workers AI (embeddings) + Vectorize (KNN index, `mnemion-vectors` / per-env `*-vectors`)
- **Validation**: Zod for tool parameter schemas
- **Storage**: KV for OAuth tokens, DO SQLite for all user data
- **Frontend**: React 19 + Vite (the web app, `web/`, served as static assets). The MCP `render` fragment is plain TS built by `vite.fragment.ts`. No Svelte.
### Wrangler bindings
- `MCP_OBJECT` — SessionDO binding (required name; `McpAgent.serve()` expects it)
- `MNEMION_HIVE` — HiveDO binding
- `OAUTH_KV` — OAuth token storage
- `AI` — Workers AI for embeddings
- `VECTORIZE` — Vectorize index for prime KNN
- `DOCUMENTS` — R2 bucket for document blobs (`mnemion-documents`; bytes for the `_documents` store). **Optional**: ships commented out in `wrangler.toml` (a binding to a non-existent bucket fails deploy), and `Env.DOCUMENTS` is optional. Mnemion runs fully without R2 — only `/f` upload/serve degrade (create returns a `documents_note`; `POST /f` → 503). Enable via dashboard → Storage & databases → R2, then `npm run enable-documents` (creates the bucket + uncomments the binding) and `npm run deploy`. When R2 is off, the index annotates `_documents` with an `unavailable` note and the MCP init message flags it so agents can tell the user. Document *contents* are not yet indexed for search/prime (metadata is); text extraction is the future seam.
### Environments
- Default — production deploy (host comes from your CF account's `workers.dev` subdomain, or a custom route).
- `[env.test]` — deployed federation peer / test target. It fails closed without its own `MNEMION_SECRET`; provision that secret before using authenticated test routes.
## Key conventions
- The `agents` package bundles its own `@modelcontextprotocol/sdk`. Pin the top-level dep to match (currently 1.26.0) to avoid type conflicts.
- McpAgent's base class has a `sql` tagged template property. Use `db` as the name for the raw `ctx.storage.sql` accessor in HiveDO.
- The DO binding must be named `MCP_OBJECT` — the `McpAgent.serve()` method expects this.
- `wrangler.toml` requires `compatibility_flags = ["nodejs_compat"]` for the agents package.
- Kernel columns (`id`, `version`, `created_at`, `updated_at`, `archived_at`, `created_by`, `updated_by`) are auto-provided on every pattern. They cannot be defined via `propose_change`. `created_by`/`updated_by` are set by the mutate engine from the session actor (not facets, not caller-settable).
- Structure is resources, operations are tools. If it describes what the organism is, it's a resource. If it changes what the organism is or retrieves dynamic content, it's a tool.
- Product name and URI scheme are defined in `shared/core/constants.ts`. Import `PRODUCT_NAME`, `URI_SCHEME`, `uri()` from there — never hardcode `"mnemion://"` in source.
- `@simplewebauthn/server` is lazy-imported in `routes/auth.ts` to avoid `tslib` resolution issues in the vitest/workerd test environment.
- System docs live in `src/system-docs/*.md` with YAML frontmatter (`slug`, `title`). Placeholders (`{{PRODUCT_NAME}}`, `{{uri:path}}`) are resolved at runtime by `resolveDocPlaceholders()` in schema.ts.
- `wrangler.toml` has a `[[rules]]` entry to import `.md` files as text modules.
## Design principle: data is destiny
`project-docs/data-is-destiny.md` is doctrine. Operational rules derived from it:
- **Store truth once, derive its consequences.** Counts, summaries, labels, and previews are computed at read time, not stored as columns. The `entry_count`/`latest_activity` on `/api/index` are SQL-computed every call. Entry labels are derived via `entities/Hive/labels.ts` (`deriveLabel`) wherever they're needed; never persisted.
- **Derived state references the source, never denormalizes it.** `_fragment_access_log` is the source of promotion eligibility (COUNT-derived), not a stored counter; the index's `entry_count`/`latest_activity` are SQL-computed per call, not columns.
- **Audit logs are exempt** — `_mutation_log`, `_schema_history`, `_pending_changes` are point-in-time records on purpose.
- **Caches are bounded with explicit invalidation policy** — `_web_cache` has per-adapter TTLs (re-fetch horizon) with superseded-duplicate GC; resolved content is retained as durable memory, not evicted on TTL. Vectorize embeddings are re-upserted on every mutate via `embedAfterMutate`.
- **Schema integrity is checked at boot.** `verifyFieldsIntegrity` in `schema.ts` walks every pattern and warns on drift between actual table DDL and the `_fields` metadata table — guards against migration mistakes producing lying agent-facing schemas. `_fields` is the long-standing partial duplication that justifies the check; full deduplication (deriving facet metadata from PRAGMA) is a future cleanup.
- **Instance identity is configuration, not request data.** `env.WORKER_HOST` is the authoritative host for every generated URL (`upload_url`, `page_url`, `og_image`, the `_system/instance` doc); `currentHost()` returns it and **ignores** the inbound `Host` header when it's a real value. The inbound `Host` (recorded in `fetch()`) is only the fallback when `WORKER_HOST` isn't configured — i.e. local dev. This is a security boundary: an attacker who sends a spoofed `Host` on an unauthenticated request (e.g. a `/ws` upgrade) must not be able to poison a capability URL handed to the owner. `scripts/setup.sh` pins `WORKER_HOST` to the deployed host at first deploy; production should always have it set. (This inverts the earlier "derive from inbound Host" convenience, which made request data authoritative for a security-relevant value.)
## Design principle: code as schematic
Structure code as declarative, scannable tables — not procedural chains. A reader should grasp the system's shape from the declarations alone, without tracing control flow. Enums for categories, typed records for configuration, implementation in focused single-purpose files.
The route table in `index.ts` is the reference example: method, pattern, auth gate, and handler on one line per route. The full routing surface is visible in 15 lines. The `CHANGE_TYPES` table in `evolution.ts` follows the same pattern: validate, preview, apply per change type — the full schema evolution surface scannable in one table.
Domain logic lives in pure-function modules with context injected. HiveDO builds context objects (`dataCtx()`, `evoCtx()`) and delegates. No God objects — each module owns one concern.
## Design principle: self-enforcing declarations
The operational synthesis of the two principles above, for a codebase agents edit. Every invariant gets **one declarative home** (a table keyed by what it governs) that is simultaneously the spec an agent reads, the enforcement every gate derives from, and the oracle a totality check asserts completeness against — so spec, enforcement, and test can't drift. Five properties make a declaration self-enforcing: (1) one table, and it's data; (2) derive, never duplicate; (3) enforce at the chokepoint the invariant is about, not the convenient layer; (4) fail closed (absence → the safe state); (5) a totality check that fails loudly. Reference: `entities/Hive/policy.ts` (write-class registry) + `verifyWritePolicyTotality` + `src/__tests__/policy.test.ts`. Full doctrine and checklists: `project-docs/archived/self-enforcing-declarations.md`. Quality metric: minimize the number of files an agent must touch in lockstep to add a pattern/tool/route/invariant — drive it toward one. Not for effects (keep imperative), one-offs, or history (migrations/audit logs).
## Design principle: security boundaries are self-enforcing declarations
The security model is a set of **boundaries**, each enforced at ONE chokepoint a
new code path physically cannot avoid — never a guard remembered at each call
site (a block-list fails open the moment someone forgets a sink). Each boundary
has the same anatomy: a stated invariant, its `because:` rationale, and a
totality or structural check declared in the co-located spec, anchoring the
chokepoint. The boundaries:
- **Untrusted reads & writes** → one required `trusted` flag on `DataContext`,
symmetric: an untrusted context (`!ctx.trusted`) may neither WRITE a kernel
pattern (the mutate engine refuses it via `!ctx.trusted &&
!isValidWriteTarget(patternName)`) nor READ one (`query`/`search`/`aggregate`).
Writes enter untrusted via ingress/upload; reads via `servedDataCtx`/
`servedQuery` (public page, OG, publication, `/o/entry`). The flag is required
(no default) so a new serve/ingress path can't silently inherit kernel access —
it fails CLOSED. The trust decision is a CAPABILITY, not a per-call-site
convention: `hive.ts` exposes two named constructors over a trust-agnostic
`ctxFields` — `ownerDataCtx` (the ONLY `trusted:true`) and `servedDataCtx`
(`trusted:false`, used by served reads AND ingress/upload writes) — and there is
no trust parameter to dial at a call site, so orchestration handed only the served
constructor (e.g. `served.ts`) physically cannot reach kernel data. Oracle:
`verifyWritePolicyTotality` + `policy.test.ts` (writes), the served-entry-point
totality in `security.test.ts` (reads), and `context-capability.test.ts` (locks
the split — no trust-defaulting factory can return).
- **Egress sensitivity** → the read/serialization dual of the write registry.
`SENSITIVE_COLUMNS` (`policy.ts`) declares which columns must never leave the DO
in the clear. Two complementary mechanisms, by reader:
- **OUTWARD egresses** — the `/ws` broadcast (reaches every session), `export`,
and served public reads (`getSharedEntry`, publication projections) — route
rows through `seal`/`sealAll` (`policy.ts`), which strips `secret` + `redact`
columns. The audit trigger does the same (records sensitive columns as NULL).
Convention: **any new served path that emits pattern rows calls `seal`/`sealAll`.**
Oracle: `exportPattern` is the one served path that emits *every* pattern
(incl. kernel), so the `security.test` that asserts export strips every
`SENSITIVE_COLUMNS` column for every exportable pattern is the registry-driven
totality check for outward emission.
- **The trusted owner read plane** (`query`/`getEntry`/`search`) returns rows
as-is, but `secret` columns are **born hashed**: generated in app code and
stored as a digest *before* INSERT (`hive.ts mintSecrets`; the `randomblob`
SQL default is gone), so a read — owner or otherwise — yields a digest, never
a usable bearer. The preimage never lands in a column, the audit log, or a
delta; the raw is returned ONCE at mint.
Oracle: `findUnclassifiedSensitiveColumns` — a boot `warn` plus a `security.test`
assertion over the real kernel schema that **fails the suite** if a secret-shaped
column is unclassified. A new outward egress inherits redaction by routing
through `seal`; a new secret column fails the test until classified + born-hashed.
- **Instance IDENTITY** → the host is configuration (`WORKER_HOST`), never request
data; `currentHost()` ignores the inbound `Host`.
- **Served CONTENT** → agent-authored egress is inert (`Content-Security-Policy:
sandbox`), never active script on the first-party origin.
When you add a serve path, a stored secret, a generated URL, or a content egress:
**route it through the existing boundary — do not re-implement the guard.** Adding
a *new* boundary means adding all four parts (chokepoint, why, totality test,
claim) so the next agent inherits it by construction.
These registries now **compose CORE + per-feature**: a feature owns its write-class +
egress-sensitivity in its pure-data `entities/features/<name>/security.ts` and its
pre-mutation hooks in `<name>/hooks.ts`, folded into `KERNEL_WRITE_POLICY` /
`SENSITIVE_COLUMNS` / `ON_CREATE`/`ON_WRITE`/`IMMUTABLE` at the same chokepoints
(`mergeDisjoint` throws if a feature shadows a CORE pattern). Enforcement stays
central; only the *declaration* moved into the feature dir. **To add a feature:
`project-docs/archived/authoring-a-feature.md`** (the fork contract — it also lists the
sharp edges: keep `security.ts`/`hooks.ts` leaf-clean `import type` only, your
write-class is a typo-prone string cast guarded by the totality test, declare every
sensitive column).
## Extending the system (forks)
A feature is one directory under `entities/features/<name>/`; its manifest FEEDS the
effect/route/tool/pattern/hook/write-policy registries above, composed at boot, each
enforced at its chokepoint. Adding one = a directory + one `FEATURES` barrel line.
Follow `entities/features/Features.practice.md` when adding a feature. Verify
with `npm test`, `npm run coherence:spec`, `npm run cruft`. Historical design
details are in `project-docs/archived/{authoring-a-feature,feature-manifests}.md`.
## Coherence (spec ↔ code)
`@posthog/coherence` is a dev dependency. Its Claude and Codex hooks read the
project under `mnemion-js/`. Run `npm run coherence:spec` for grammar and
handler checks, `npm run coherence:run` for test-backed requirement verdicts,
`npm run coherence:hooks-check` for hook wiring, and `npm run coherence:scope`
for the live reading. Use Node 22.18 or newer.
Each component spec holds a one-line intent, optional `## entrances`, and
`## invariants` bullets. Each bullet states a rule, `because:` rationale,
`over:` domain and `via:` test or a protected symbol and chokepoint. A
requirement becomes a verified invariant only after its detector has passed
and a staged break has made it fail. The prior spec rationale is archived in
`project-docs/archived/coherence-v0-rationale.md`.
The old generated pages in `mnemion-js/docs/coherence/` are snapshots from the
previous harness. Do not use their verdicts as current evidence.
## Router architecture
Declarative dispatch table in `shared/Routing/router.ts`. Routes are matched in declaration order.
- **`Method`** enum: `GET`, `POST`, `ANY`
- **`Auth`** enum: `NONE` (default), `DEV` (no secret configured), `CONFIGURED` (secret required to exist), `SECRET` (Basic auth against master secret)
- **`where`** constraints: regex validation on extracted params (e.g. `{ token: /^[a-fA-F0-9]+$/ }`)
- **`RouteContext`**: `request`, `url`, `env`, `params`, `store` — passed to every handler
Route handlers are grouped by domain in `shared/Routing/routes/`. OAuthProvider intercepts `/mcp`, `/token`, `/register` before the dispatch table runs.
## The MCP render fragment
The web app proper is **React** (see Development below). The one non-React web
surface is the **MCP render fragment** — the `ui://mnemion/render` UI the `render`
tool returns (`src/pages/render-*.ts`, plain TS, no framework). It's built by
`vite.fragment.ts` into a self-contained `.client.txt` text module that
`session.ts` inlines into the resource HTML (consumed via wrangler `[[rules]]`) and
embeds in MCP-Apps-capable hosts.
- **No Svelte.** The Svelte era is over: the Canvas spatial-thinking surface and the
legacy SSR pages (SchemaViewer / HiveMap / LinkMap, EntryDetail) are all retired;
their function lives in the React app under `web/`. The `_canvases` kernel pattern
is dropped (v16 migration).
- WebSocket live updates via the Hibernatable API on HiveDO.
- Deployed test environment configured as `[env.test]` in wrangler.toml; it requires its own secret for authenticated routes.
## Development
The web app is **React + Vite** (`web/`), served by the worker as static assets
(the `ASSETS` binding, `dist/web`). The worker owns `/mcp`, `/api/*`, `/o /p /f
/i`, OAuth; everything else falls through to the SPA shell (`src/index.ts`
`BACKEND_PREFIXES`). The MCP render fragment (`vite.fragment.ts`) is the only
non-React web build. The legacy Svelte surfaces (Canvas + the SSR notebook) are retired.
```bash
cd mnemion-js
npm install
npm run dev # concurrently: wrangler dev (DOs + DEV_SEED data, :8787) +
# Vite HMR (:5173, proxies /api + /ws to the worker). Open :5173.
npm run build:web # React SPA → dist/web
npm run build:pages # MCP render fragment → dist/fragment
npm run types # regenerate worker-configuration.d.ts from wrangler.toml
npm run coherence:spec # check spec grammar
npm run coherence:run # run declared test oracles
# Seed a local dev hive with your REAL data (when you want realistic content):
npm run pull-hive https://<your-host> <*-token> # once → dev-data/hive.json
npm run dev # then, in another terminal:
npm run import-hive # loads it into the local hive
```
**Node ≥22.18** is required for `coherence`; Wrangler also needs Node 22. If your default is older, use a version manager (`nvm use 22`) for `deploy`/`types`/`dev:worker`/`coherence:*`.
### Agent-authored views (hyperdesk)
The web app renders each pattern per an optional agent-authored **view spec** in
the `_views` kernel pattern: a `view_type` (`board`/`table`/`list`/`cards`) + a
declarative `config` mapping facets to UI roles (e.g. board `{group_by, title}`).
The agent writes these via `mutate` — declarative data interpreted against a
fixed React component palette (`web/src/views.tsx`), never code. The SPA holds
entries in a normalized store (`web/src/store.ts`, `useSyncExternalStore`); a
mutate broadcasts a granular delta `{pattern, op, id, entry}` over `/ws`, which
patches one entry so only its `React.memo`'d card re-renders (the faint `r{n}`
badge is the visible proof).
## Testing
```bash
cd mnemion-js
npm test # vitest run — full suite
npm run test:watch # vitest in watch mode
npx vitest run path/to/file.test.ts # single test file
npx vitest run -t "name of test" # tests matching a name
```
Tests live in `mnemion-js/src/__tests__/` and run inside `@cloudflare/vitest-pool-workers` (workerd runtime). `isolatedStorage: true` rolls back DO SQLite between tests. `tsconfig` excludes `src/__tests__` because tests import the `cloudflare:test` virtual module. Use `fetchMock` from `cloudflare:test` to mock outbound HTTP in DO tests (`activate()`/`deactivate()` per test).
## First-time setup
```bash
cd mnemion-js
npm run setup # generates secret, deploys, prints passkey registration URL
```
This generates a 256-bit random master secret, sets it via wrangler, deploys, and opens the passkey registration page. Run again anytime to rotate the secret and re-register.
## Deploy
```bash
cd mnemion-js
npm run deploy # deploy code changes (does not rotate secret)
```