Skip to content

Latest commit

 

History

History
482 lines (355 loc) · 98.8 KB

architecture.md

File metadata and controls

482 lines (355 loc) · 98.8 KB

Architecture

This document describes Origin's current design: the problem it targets, the data model, the invariants every change must preserve, and how ingestion, entries, search, the MCP server, and the personal hub work. It is the reference for contributors; if a change alters the data model, an invariant, or a public interface, update this document in the same commit.

Contents

Problem

Developers increasingly work across several AI agents at once — Claude Code, Codex, Cursor, ZCode, Grok Build. Each agent keeps its own memory, scoped to a repository, a machine, or a cloud account. Switching tools or projects means starting from zero. That memory is hard to inspect, hard to correct, invisible to other agents, and often silently purged (Claude Code, for example, deletes local session logs after 30 days by default).

At the same time, every one of these agents writes plain, parseable session logs to the local disk, and every one of them can talk to a local MCP server. Origin builds on both facts: it archives all local agent sessions into a single local store and makes that store available to every agent through one MCP server.

The long-term direction is an AI that accompanies one person over time, where memory, knowledge, and an understanding of the person persist across tools and model generations. Origin starts with that core.

Scope

Current goal — a cross-agent personal memory over MCP:

  • Archive local sessions from Claude Code, Codex, Cursor, ZCode, and Grok Build into one append-only store.
  • Import the memories agents keep for themselves (the memory files of Claude Code, ZCode, and Grok Build) as entries, so that what one agent remembers reaches the others.
  • Let any agent search the archive, walk the timeline, expand any answer to its original source, and record durable entries (memory / knowledge / cognition) via MCP.
  • Let the person inspect, correct, and forget entries via the origin CLI or the admin UI, on their hub or, with origin ui, on their own machine; forgetting must stick until the person restores what they forgot.
  • One person often runs agents on several machines (laptops, servers, cloud agents). Every device keeps a complete memory of its own; an optional, self-hosted personal hub gathers what every device writes into one store, so that agents anywhere can find it. A machine with little disk can instead be a relay of the hub, which keeps no memory of its own.

The goal is reached when, from any connected agent on any of your machines, you can ask "what did I decide in project X last week, and why?" and get an answer with sources, and a forgotten entry never comes back unless you restore it.

Explicit non-goals for now: multi-tenant hosting (Origin is for one person; everyone runs their own), a hosted service run by the project, end-to-end encryption, devices pulling each other's data, peer-to-peer sync without a hub, real-time collaboration, any web interface beyond the owner's admin UI on their own hub or machine (no public pages, no sharing, no multiple accounts), screen/email/browser capture, knowledge graphs, vector search, proactive reminders, and any call to a model API.

Memory, knowledge, and cognition

Origin distinguishes three kinds of durable entries. Every entry carries its sources and can be corrected or forgotten.

Kind Meaning Example
memory What happened (episodic) "On Oct 20 we dropped schema versioning until the first release, to keep the code simple."
knowledge Stable facts and relations "Project A depends on library B."
cognition A model of the person: preferences, methods, goals, boundaries "Prefers commit messages without attribution trailers."

Core objects

Object Meaning Key fields
Source An adapter for one agent: claude-code, codex, cursor, zcode, grok id, agent version, discovery root
Raw Append-only mirror of an original log file, or of the records an adapter writes out of a database, stored in zstd frames source, original path, generation, byte/line cursor, tail hash (for a generated source, its token), sealed frames
Event A normalized conversation event: user message, assistant message, tool call, tool result; or a version of a memory file an agent keeps (see Native memories) id, source, session, timestamp, project, cwd, git branch, role, kind, text, raw locator
Entry A durable memory / knowledge / cognition statement id (ent_…), kind, text, confidence, author, project, sources (event ids + quotes), supersedes, root (its first version), valid_from / valid_to, created_at, origin (the store that wrote it); status (active / superseded / forgotten) is derived, never stored, and so is imported, whether the version was derived from a native memory
Tombstone Record of a forget or a restore id (tmb_…), target (an entry or event id for a forget; a forget's id for a restore), reason, author (when known), time, origin (the store that created it); for a forget, what it covers on each store

Data flow

agent session logs on disk ──scan──▶ Raw (append-only mirror)
                                        │ deterministic adapter parsing (no LLM)
                                        ▼
                                  Event store (SQLite + FTS5) ◀── Entries · Tombstones
                                        │
 agents ◀── MCP (stdio): search · timeline · get · sources · remember · correct · forget
 people ◀── CLI `origin`: sync · status · timeline · search · show · entries · correct · forget · forgotten · restore
 people ◀── admin UI (hub, /admin): overview · timeline · search · entries · remember · correct · forget · restore · tokens
 people ◀── origin ui (device, 127.0.0.1): overview · timeline · search · entries · remember · correct · forget · restore

With a personal hub, every device keeps a complete store of its own and stays usable offline. It sends the hub its raw mirrors, which the hub parses with the same adapters, and the entries and tombstones (forgets and restores) it writes; it receives only tombstones. Agents on a device use its local origin mcp, which answers from the device's store and merges in the hub's answers; cloud agents use the hub's HTTP endpoint:

agents on A ──▶ origin mcp ──▶ store A ──raw mirrors · entries · tombstones──▶ ┐
                    │             ▲                                             │
                    │             └──────────────── tombstones ─────────────────┤
                    └──────── reads, merged (MCP over HTTP) ───────────────────▶ hub store ◀── MCP over HTTP ── cloud agents
agents on B ──▶ origin mcp ──▶ store B ── (as for device A) ───────────────────▶ ┘

A relay (see Relays) has no store to answer from. It sends the new lines of its agents' logs to the hub, straight from the logs, and its origin mcp hands every tool call to the hub:

agent logs on C ──new complete lines, read after C's cursors──────────────────▶ hub store
agents on C ──▶ origin mcp ──── every tool call (MCP over HTTP) ──────────────▶ hub store

Invariants

These are product guarantees. Each must be covered by tests, and code comments refer to them by number.

  1. Provenance. Every Entry cites at least one Event; every Event points to a Raw file and line. Any answer can be expanded down to the original text. A relay holds no events, so what is written through it is checked, and its provenance resolved, on its hub.
  2. Recorded forgetting. After forget, its target and their cascade leave every query on that store immediately, and on every other store as the forget reaches it, and they stay out until the owner restores the forget: re-syncing, versions and copies that arrive later, and the order in which tombstones arrive never bring them back. Every forget and every restore is kept as a record with its reason; tombstones reach every store and only accumulate.
  3. Append-only. Raw mirrors and Events are never rewritten: the content of a mirror, its uncompressed bytes, only grows, and a line once ingested never changes, however it is stored (see Raw mirrors). Entries change by supersession (supersedes), never in place.
  4. Your data, your machines. All data lives on machines you control: your devices under ~/.origin/ and, optionally, your own hub, with the bucket you give it for raw mirrors, if any. Origin never calls a third-party service; the only network traffic is between your devices and your hub, and between your hub and its bucket.
  5. Single entry point. Every read and write goes through the same core operations, whichever interface it comes from: agents use MCP (their device's origin mcp, or the hub's HTTP endpoint); people use the CLI or the admin UI (on their hub, or origin ui on a device). A write lands in the store where it is made, on a device its own store first (a relay has none, so its writes are made on its hub), and replication applies it to other stores through the same operations. The hub's replication API carries raw mirrors, entries, and tombstones between your own stores and is not an entry point for agents or people.

Design principles

  • No LLM in the core. Origin never calls a model API. It archives, normalizes, indexes, and stores. Analysis happens in the agent that is asking — it is already a capable model. Agents persist what they learn through MCP remember; people correct and forget through the CLI or the admin UI. If periodic consolidation is ever needed, it will run as a headless coding-agent task (for example claude -p), not as a direct model API integration. This is the current position and can be revisited if evaluation shows it falls short, especially for the cognition layer.
  • Deterministic, tolerant adapters. Each source has a pure, deterministic parser. Agent log formats change frequently, so adapters record the agent version, skip lines they cannot parse, count the failures, and never abort ingestion.
  • MCP as the interoperability point. MCP is the only interface all target agents share. Several of them also read Claude Code's MCP configuration, so one registration can reach more than one agent.
  • A small, conservative admin UI. The admin UI, on a hub or as origin ui on a device, is for its owner only. Pages are rendered on the server with a template that escapes every value, plus a little optional script. Its stylesheet, fonts, and icons come from the OpenLinkOS design system (@openlinkos/ui) and are copied into the package when it is built; nothing is loaded from elsewhere, and there is no bundler. It can do what the CLI can, and nothing that would break an invariant: no editing or deleting in place, and hidden text appears only on a forget's own page, for its owner, who can restore it there.
  • Small, runnable increments. Each source adapter ships and is used on its own; the design only goes as deep as the current increment needs.

Packages

pnpm workspace, TypeScript (strict, ESM, NodeNext), Node.js ≥ 22.16 (for zstd in node:zlib and the online backup of node:sqlite). core, adapters, and admin use only Node built-ins; mcp depends on the official MCP TypeScript SDK and zod. The hub and its admin UI use Node's built-in node:http.

Package Path Responsibility Depends on
@openlinkos/origin-core packages/core Storage, schema and migrations, ingestion (local and remote), relaying, entries, forgetting, tokens, the admin password and sessions, the audit log, queries —
@openlinkos/origin-adapters packages/adapters One deterministic parser per source core
@openlinkos/origin-mcp packages/mcp MCP tools; servers over stdio and stateless HTTP, the server of a device connected to a hub, which merges the hub's answers into its own, and the server of a relay, which hands every tool call to the hub core
@openlinkos/origin-admin packages/admin The admin UI, of a hub and of origin ui: server-rendered pages, a JSON API, and its static assets core
@openlinkos/origin packages/cli The origin command, including origin mcp, origin serve (the hub), origin ui, a device's or a relay's sync with its hub, and the hub's S3 client core, adapters, mcp, admin

All five packages are published to npm together under one version; people install @openlinkos/origin, which brings the others along. Before 1.0 the other four are internal: their APIs may change in any release.

core must not depend on any other package, and neither mcp nor admin may depend on adapters: the CLI injects ingestion into the servers as callbacks, and the HTTP servers of the hub and of origin ui live in the CLI, which mounts the admin UI's request handler. Adapters implement the SourceAdapter interface from core:

interface SourceAdapter {
  id: string; // 'claude-code' | 'codex' | 'cursor' | 'zcode' | 'grok'
  displayName: string;
  version(): string | null; // agent version, if known
  discover(): Promise<SourceFile[]>; // { absPath, relPath, compression?: 'zstd', generated? }
  parseLine(line: string, ctx: ParseContext): ParsedLine | null; // deterministic; throws on malformed input
}
// ctx: { absPath, relPath, lineNo, fileMtimeMs, readLine(n) }; readLine returns an earlier line of
// the same file as ingested, for an adapter that needs, say, a file's first line on a hub.
// generated: { token, mtimeMs, next(last, maxBytes) } for a source that is not a log file, such as
// a database: the adapter writes the lines (see Generated sources).

Storage

~/.origin/                 # or $ORIGIN_HOME
├── origin.db              # SQLite (WAL)
└── raw/<source>/...       # append-only mirrors of the original log files:
                           #   <mirror>.zst (sealed zstd frames) and <mirror>.tail (open tail)

s3://<bucket>/<prefix>/    # optionally, on a hub: its sealed frames, one object per frame
└── <mirror>.zst/<offset>
Table Contents
raw_file One row per mirrored file generation, with its ingestion cursor
raw_chunk The sealed frames of each mirror: first line, line count, offset and length in the content and in the .zst file. Triggers reject updates and deletes
event Normalized events, keyed by their order of arrival in this store (seq)
event_block For each block of 4,096 consecutive arrivals, the latest timestamp in it and up to it (derived, kept by a trigger)
message_fts, event_fts, event_cjk The search indexes over events: a full trigram index of message events, a trigram index of every event that records only which events contain each trigram, and an index of CJK bigrams (see Search and timeline)
session Per-session aggregates over visible events (derived, rebuildable), and the session's title from the lines that carry one, with whether the user gave it
sync_log One row per sync run
entry, entry_source, entry_fts Entries, the events they cite (with quotes), and their FTS5 index. Each version records its root (the first version, fixed when it is written) and its origin
imported_entry The entry versions this store derived from native memories (see Native memories), which it never sends to another store
tombstone, forgotten_entry, excluded_event Forgets and restores, each with a global id (tmb_ and random hex, like ent_ ids) and its origin; and what is hidden on this store, each hidden entry and event with one forget in effect that covers it
tombstone_effect What each forget covers on this store, several forgets per item: by the rules from its target, as its hub listed it, as a device reported it, or by rule 3 (see Forgetting)
tombstone_meta The author of each tombstone, when known
change_log Append-only sequence of the entries and tombstones this store has written or received (sequence number, kind, id, origin, time), filled by triggers; triggers reject updates and deletes. Replication cursors point into it
api_token Hub access tokens: name (for a device, the device's name) and SHA-256 hash, never the token itself
setting Small key-value state, local to each store and never replicated: the store's name and mode, a device's replication cursors and when it last synced with its hub, on a relay one cursor per source file, and on a hub the scrypt hash of the admin password and the bucket that holds its sealed frames, if any
admin_session Sessions of the hub's admin UI: a hash of the session id, its CSRF token, and its timestamps (origin ui keeps its sessions in memory)
audit_log Append-only record of administrative actions, with ids and counts but never content; triggers reject updates and deletes
  • Storage uses Node's built-in node:sqlite, so there are no native modules to compile. Several processes may share the database (the CLI and one MCP server per agent session); WAL mode and a busy timeout handle this.
  • session is derived and can be rebuilt from event; per-source totals (origin status, the admin overview) are read from it. The search indexes are derived too and kept in sync by triggers: message_fts and event_fts are external-content tables (the content of message_fts is the event_message view of message events), and event_cjk is contentless, filled through a function that the core registers on every connection. rebuildSearchIndexes() rebuilds them all, and event_block, from the events and entries.
  • Arrival order. Events, and the search indexes with them, are keyed by seq, their order of arrival in this store: FTS5 stores the differences between consecutive keys, so dense keys keep the indexes small. A sync ingests the files it finds oldest first, by modification time, and a device pushes its files to its hub in the order it last updated them, so arrival order follows time as far as possible; but it is not time order, since a hub, for example, may receive a device's old sessions long after newer ones. So a trigger records, for each block of 4,096 consecutive arrivals, the latest timestamp in it and up to it (event_block). A newest-first query reads its index latest arrival first, in a few chains of consecutive blocks (a block more than a day newer than the chain before it starts a new one), always from the chain whose unread part may hold the newest event, and stops as soon as no chain may hold one newer than those it needs. A narrow (seq, ts) index gives an event's timestamp without reading the event.
  • Size. On a store of two million events averaging about 800 characters of text, the database takes about 2,550 bytes per event: 1,360 for the event itself, 205 for its indexes, 500 for event_fts, 460 for message_fts, and 20 for event_cjk. A full trigram index of every event would take about 2,350 bytes per event on its own.
  • Backups. origin backup <dir> copies the database with SQLite's online backup in one step, a consistent snapshot even while other processes write, and then the raw mirrors as that snapshot records them: the sealed frames the directory does not have yet, and each open tail, extended, or written again if frames were sealed from it since. A tail is read from the live store, where it may have been sealed meanwhile; a mirror's content only grows, so it is the same (see Backing up). From a hub that keeps its sealed frames in a bucket, they are copied into the backup, laid out as on disk, so that a backup never depends on the bucket; the backup's database then no longer records the bucket.
  • A relay's store holds no raw/ directory, and its database no events, mirrored files, entries, or tombstones: only setting (its name, its mode, its cursors) and its sync log (see Relays).
  • Store names. Every store has a name, kept in setting: on a device it is the device's name, and a hub calls its store hub. Entries and tombstones record the name of the store that wrote them as their origin, and a device is known to its hub by its store's name. A store is named the first time it needs a name: origin serve names it hub; other commands use ORIGIN_DEVICE or the host name. Once a store has written an entry or a tombstone, it keeps its name; entries it derived from native memories do not count, since it never sends them.
  • Format versions. The database records the version of the store's format, and opening a store brings an older one up to date in place; a store of a newer Origin is refused (see Migrations). Tables derived from existing ones are filled from them when the database is opened, whenever they lack rows: tombstone_effect and tombstone_meta for tombstones written without them, by Origin 0.1 or by a process of an older version still running, from forgotten_entry, excluded_event, and the rules. The database, the mirrors under raw/, and the sealed frames in a hub's bucket belong together: never delete or replace one without the others.

Raw mirrors

A mirror holds the ingested bytes of one generation of a file (for a generated source, the lines its adapter wrote out): its content, complete lines only. It is stored compressed, in two files named after it:

  • <mirror>.zst: the sealed frames, independent zstd frames (level 3, with checksums), each holding complete lines, appended one after another. raw_chunk records each frame: its first line, its line count, its offset and length in the content, and its offset and length in the file. Reading a line decompresses only the frame that holds it.
  • <mirror>.tail: the open tail, the newest lines, uncompressed.

The content is the frames, decompressed, followed by the tail, each up to what the database records (raw_chunk for the frames, raw_file.bytes_ingested for the end); every read and write of raw/ goes through one module (mirror.ts in core) and its frame store, which keeps the sealed frames in <mirror>.zst or in a hub's bucket (see Frame stores). Line numbers, cursors, rewrite generations, tail hashes, and event ids are all in terms of the content, as for a plain copy of the file.

  • Sealing. New lines are appended to the tail while it stays under 1 MiB. The append that would bring it to 1 MiB or more seals the tail and the new lines instead: they are cut into frames, each ending at the first line break once it holds 1 MiB (a longer line fills a frame of its own, and a last piece under half a MiB joins the frame before it), the frames are appended to <mirror>.zst and flushed to disk, and raw_chunk records them in the transaction that advances the cursor. Once that transaction has committed and the write-ahead log has been flushed, a short transaction empties the tail. A file that has not grown for an hour is likely finished, so its tail is sealed too, whatever its size, by the next sync of its source on a device and by a check every 10 minutes on a hub; lines appended later start a new tail.
  • What append-only means here. The content only grows, and the bytes of a line never change (invariant 3). On disk, <mirror>.zst only grows and its frames are never rewritten (raw_chunk rejects updates and deletes); <mirror>.tail is only appended to, except that it is emptied once its lines are in frames. Sealing moves lines from the tail into a frame without changing the content.
  • Crash safety. Bytes in either file past what the database records are not part of the mirror: readers never read them, and the next write to the mirror drops them before appending. A crash after a frame is written but before its transaction commits leaves a frame that nothing records; it is dropped, and the lines, still in the tail, are sealed again. A crash after the commit but before the tail is emptied leaves lines in the tail that are also in a frame; the database says the tail is empty, so they are ignored and then dropped. Neither loses or doubles a line. A file shorter than the database records has lost data, and Origin refuses to append to that mirror rather than break provenance (invariant 1). A reader that finds the tail emptied by a concurrent seal in another process reads again.
  • Size. On 171 MiB of synthetic Claude Code logs shaped like real ones (large tool results, base64 screenshots for about a quarter of the bytes, Chinese and English text), the mirrors take 35 MiB once their files have stopped growing (4.9×; 52 MiB right after an import, while every file still has an open tail). Ingesting takes about 15% longer, and reading a random raw line takes 1.5 ms on average, against 6.8 ms to scan a whole plain mirror.

Frame stores

Sealed frames are written once and never change, which suits object storage, and on a hub they take most of the space. Where they are kept is up to a frame store: mirror.ts decides what the frames are and records them in raw_chunk, and the frame store keeps their bytes at the offsets raw_chunk records, as if they were appended to one file per mirror. A store uses one of two (frames.ts in core):

  • On disk, the default, and the only one a device uses: a mirror's frames are <mirror>.zst, as described above, byte for byte.
  • In a bucket, on a hub given one (see Personal hub): one object per frame, through an object store with two synchronous operations, put(key, bytes) and get(key). core makes no network calls, so the CLI provides the object store: an S3 client for MinIO or any other store that speaks the S3 API, with AWS Signature Version 4 computed with node:crypto, the built-in fetch, path-style or virtual-hosted URLs, and only PUT and GET of single objects.

Open tails stay on disk either way: each is under 1 MiB, still growing, and emptied once it is sealed.

  • Keys. The frame at offset <offset> of a mirror's frames is the object <prefix>/<mirror>.zst/<offset>, with / between path segments and the offset written with at least 12 decimal places, such as hub/devices/laptop/claude-code/proj/a.jsonl.zst/000000412345. A key is derived from what raw_chunk records, and the offsets are those of <mirror>.zst, so the database is the same whichever store keeps the frames: there is no schema change, and a mirror can have its older frames on disk and its newer ones in the bucket.
  • Crash safety. A frame is uploaded before the transaction that records it commits, and the tail is emptied only after that, as on disk. A crash after the upload but before the commit leaves an object that nothing records: no reader asks for it, its lines are still in the tail, and the next seal writes their frame at the same offset, under the same key, replacing it. Objects are never deleted (invariant 3), and a key is written again only to replace such an object. A frame that cannot be uploaded fails its transaction, so its lines stay in the tail and a device's push is refused until the bucket is back. Bytes of <mirror>.zst past what the database records, left on disk by a write that never committed before the hub used the bucket, are cut before the next frame goes to the bucket, so that the file only ever holds frames the database records.
  • Reading. A frame is read from memory, else from <mirror>.zst if the file holds it (a frame sealed before the hub used the bucket), else from the bucket. The hub keeps the frames it wrote or read last in memory, up to 32 MiB: at least 32 frames, and usually over a hundred, since a frame of 1 MiB of lines takes 200 to 300 KiB compressed. That covers the frames read again and again, such as the newest frame of a mirror a device is pushing, which the next push reads for its tail hash. A frame the bucket does not have is missing, and so are its lines (invariant 1); a bucket that cannot be reached is an error, never a missing line, so that an adapter never parses a pushed line without the earlier line it asked for.
  • Synchronous transfers. node:sqlite is synchronous, and frames are written and read inside write transactions (sealing, and an adapter's readLine while it parses a device's push), so the object store is synchronous too. The S3 client runs fetch in a worker thread: the calling thread posts it the request, blocks on Atomics.wait until the worker has posted the response, and takes it with receiveMessageOnPort. Each request has a timeout (30 seconds by default), and one that fails with a network error, or that the store answers as busy or with a server error, is tried up to three times. The cost: the hub's event loop stands still during each transfer, and while it seals, the hub holds the write lock for it. On a LAN a frame takes milliseconds (about 2 ms to upload 256 KiB to a test server on the same machine, of which the worker thread adds about 0.15 ms), but a slow or unreachable bucket stalls the hub for up to the timeout per request. A hub uploads a frame or two for a pushed chunk that seals its tail, and one per idle mirror every 10 minutes.
  • The bucket a store uses. A store records its bucket and prefix in setting (raw.bucket) when it first uses them. From then on it refuses any other, origin serve refuses to start without it, and other commands take it from the environment, failing with a message that says what to set when they need a frame from the bucket and it is not configured. The endpoint is not recorded, so it may change. Give each store a prefix of its own: two stores writing the same keys would replace each other's frames.
  • Moving frames to the bucket. Frames sealed before a hub used its bucket stay in <mirror>.zst and are read from there. origin raw upload uploads them under the keys above, so that running it again, or after an interruption, uploads nothing twice: each frame is checked before it is uploaded (it must decompress, with its checksum, to the size raw_chunk records) and read back from the bucket afterwards, and a frame already there with the same bytes is left as it is. It runs outside transactions, so it does not block on each transfer as the hub does: it sends its requests from the calling thread, 8 frames at a time unless --concurrency says otherwise. A frame that fails a check is reported and the file that holds it is kept, while the other mirrors go on; after an error that would fail every frame, such as a bucket that cannot be reached, no further frame is started. It lists the <mirror>.zst files whose every frame is verified in the bucket, and --remove removes each one as soon as all its frames are. This moves frames without changing them, so the content of every mirror stays the same (invariant 3). It may run while the hub serves.

Migrations

A store records the version of its format in the database header, PRAGMA user_version: SQLite keeps that integer for applications, it can be read before any table exists, it is written in the same transaction as the changes it describes, and a backup copies it with the database. Origin 0.1.0 recorded none, so a store without a version is version 1, the schema of 0.1.0, if it has the tables of 0.1.0, and a new store otherwise. Opening a store (migrations.ts in core) brings it to the latest version:

  • A new store gets the current schema and the latest version in one transaction.
  • An older store runs each migration it lacks, in order, each in a write transaction (BEGIN IMMEDIATE) of its own that also records its version. The version is read again once the lock is held, so when several processes open the store at once (the CLI, one MCP server per agent), one of them runs each migration while the others wait, up to 5 minutes, and then find it done. A migration that fails is rolled back with its version, and the error names it; what the migrations before it did stays, and the next open runs it again.
  • A store of a newer Origin is refused before anything in it changes, with a message that says to upgrade.

After the migrations, the schema's idempotent statements run, on every open, so that a new table, index, view, or trigger that existing data needs nothing from appears without a migration.

Version Change
1 The schema of 0.1.0, the first release, which records no version
2 session.title_manual: whether the user gave the session its title
3 Data: the titles of Claude Code sessions, from the ai-title, custom-title, and summary lines already mirrored, which 0.1.0 skipped. Only lines that contain one of these names ("ai-title", "custom-title", "summary") are parsed again, on a device and on a hub alike (raw/devices/<device>/claude-code/…)
4 Data: the events and entries of the memory streams a store mirrored before it could read them, as a hub of 0.2 does with the streams a device of a later version sends. Every line of the mirrors under memory/ of Claude Code, ZCode, and Grok Build is parsed, its events stored, and its entry derived (see Native memories)

When a change needs one. Any change to the store's format other than a new table, index, view, or trigger that existing data needs nothing from: a new, renamed, or dropped column, a changed constraint, a changed trigger or view (IF NOT EXISTS keeps the old one), derived rows to fill in for existing data, or mirrored lines to parse again because an adapter now reads more from them. A schema migration changes tables in place; a data migration changes or fills rows from what the store holds, its raw mirrors included. Either keeps the invariants: raw mirrors and events are never rewritten (invariant 3), and what was forgotten stays forgotten (invariant 2). core has no adapters, so the process that opens the store passes them in for data migrations that parse mirrored lines; the CLI does, and before the first migration it runs, it also sets up the store's frame store, so that a hub reads the frames in its bucket.

Writing one. Add a Migration with the next version to MIGRATIONS in migrations.ts, change schema.ts to the new shape, so that a new store and an upgraded one end up the same, and add its row to the table above. A released migration is never changed; a fix is a new migration. Test it on a store of the version before it: the tests build a store of 0.1.0 from its schema (packages/core/test/fixtures/schema-0.1.0.sql), upgrade it, and compare its schema with that of a new store.

Ingestion

origin sync (and the MCP server, see below) runs every adapter's discover(). A file whose size and tail have not changed since its last ingestion is skipped without taking the write lock. The new lines of a changed file are ingested in BEGIN IMMEDIATE transactions of at most 1 MiB of lines each (a longer line takes a transaction of its own), so concurrent syncs from different processes are serialized and never ingest the same bytes twice, and no process holds the write lock for long, even while it imports a large backlog:

  1. Cursor. Each raw_file row tracks bytes_ingested, lines_ingested, and tail_hash (SHA-256 of the last 4 KB ingested). The cursor is read under the write lock, and only bytes after it are read.
  2. Rewrite detection. If the file shrank or its tail no longer matches, it was rewritten rather than appended. Origin opens a new generation with a fresh mirror and keeps the old mirror intact (invariant 3).
  3. Complete lines only. Bytes up to the last newline are appended to the mirror (see Raw mirrors); a trailing partial line waits for the next sync. A mirror's content always equals the ingested bytes of its generation, so bytes left past the cursor by an interrupted sync are dropped before appending (invariant 1).
  4. Parse and insert. Each line goes through parseLine; failures are counted, not fatal. Events are inserted with INSERT OR IGNORE under the id <source>:<native-id>[#<block-index>], which makes sync idempotent (invariant 3). Each event stores its mirror file and line number (invariant 1). A line of a memory stream also yields a version of an entry (see Native memories).
  5. Bookkeeping. Affected sessions are re-aggregated, the cursor advances in the same transaction, and a sync_log row records the sync. A line may also carry its session's title (sessionTitle): the latest replaces the one before, except that a title the agent generated never replaces one the user gave (manualTitle).

Compressed sources. An adapter can mark a file as compression: 'zstd'. Origin then ingests and mirrors the decompressed content, using Node's built-in zstd, so cursors, line numbers, and raw lines work as for plain files. Compressed files are written once, so they are skipped when unchanged since their last ingestion, and each is ingested in a single transaction.

Generated sources. Some agents keep their sessions in a database and change its rows in place, while a reply streams and again long after, or keep a session's title in a file they rewrite every turn (Grok Build's summary.json). For such a source the adapter marks each of its streams (say, one session) as generated: rather than reading a file, Origin asks the adapter for the lines that follow the last line it mirrored, at most about one transaction's worth at a time and again until there are none, and mirrors and ingests them like the lines of a file. The adapter writes each record out once, when it will no longer change (a message once it has completed), so the content only grows (invariant 3) and is never checked for a rewrite; the last mirrored line tells it where to go on, so it keeps no state of its own. A record that changes after it was written out is not written again: its mirrored line is the version its events come from (invariant 1). Each stream also has a token that changes whenever there may be new lines; it is kept with the cursor, in place of a file's tail hash, and a stream whose token has not changed is skipped without asking the adapter. A hub receives these mirrors like any other and parses them with the same adapter.

Several processes. The CLI and one MCP server per agent session may use the same store at once. Readers never wait for writers (WAL). A writer waits up to 10 seconds for the write lock (the busy timeout); since syncs take the lock only for changed files and only for one bounded transaction at a time, a sync or a remember waits at most about one such transaction, even while another process imports a large backlog. After each checkpoint, the write-ahead log is truncated to 64 MiB (journal_size_limit), so a large import does not leave a log of gigabytes behind.

On a relay, nothing is ingested locally: the new complete lines of each file, read the same way (cursors, rewrite detection, decompressed content, generated lines), are sent to the hub, which ingests them as described here (see Relays).

Claude Code mapping

  • Files: <projects>/<cwd-slug>/<session-id>.jsonl, plus sub-agent logs under <session-id>/subagents/. The tool-results/ directory is skipped, and each project's memory/ directory is read as a memory stream (see Native memories).
  • user / assistant lines become events. Text blocks are joined into one message event; each tool_use and tool_result block becomes its own event (truncated to 2,000 / 4,000 characters). isMeta lines become meta events. thinking blocks are not indexed but remain in the raw mirror.
  • Title lines in a session's own log (not its sub-agents') set its title. Each is written many times, and the latest is current: ai-title lines, which Claude Code generates as the conversation goes on, and custom-title lines, the name the user gave (/rename, --name), which a later ai-title line does not replace; older logs have summary lines instead. agent-name lines repeat one of these titles and are not read.
  • The project name is the last segment of cwd.

The other adapters' mappings are documented next to their formats in sources.md (Codex: Codex CLI).

Native memories

Claude Code, ZCode, and Grok Build keep memories of their own: Markdown files the agent writes, rewrites, and deletes in place, one note per file, in a directory per project or workspace (each layout is in sources.md). Origin imports them as entries, one way: it reads the files and never writes them, so that what one agent remembered reaches every agent, with its provenance.

  • Memory streams. Each memory directory is a generated source, a memory stream, mirrored under memory/ of its source (such as memory/<cwd-slug>.jsonl for a Claude Code project). Its adapter writes a line for every version of a memory file, holding the whole file, and a line when a file is deleted: first one for every file that is new or whose content has changed since the stream's last line, oldest first, then one for every file that is gone. A line holds the file's name, write or delete, its time (the file's modification time, or when the deletion was noticed), the project and working directory of the memories if known, and the directory's state after it: for each file, the id of its latest line and a hash of its content, none once deleted. The adapter writes the next lines from the state in the last one, so it keeps no state of its own, and the content only grows (invariant 3). The stream's token is a digest of its files' names, sizes, and modification times, so an unchanged directory is skipped without reading a file. A file that cannot be read is left as it was, and files over 256 KiB are left out.
  • Events. Each line is one event, of kind memory: its text is the file as written (empty for a deletion), and it records the file's name and the operation. Its id, <source>:memory:<hash>, is a SHA-256 chained from the file's previous line: of that line's id (for a file's first line, the stream's path and the file's name), the operation, and the content's hash. So the same history gets the same ids wherever it is parsed, and a file that comes back after a deletion gets new ones. A memory event belongs to no conversation: it counts in no session, and memory streams never show in the timeline.
  • One entry per memory file. The adapter reads the memory in each version of a file: its kind, its text, and a confidence (see sources.md), or nothing if the file holds none. Origin derives a version of an entry from each line (memories.ts in core). It cites the line's event (invariant 1); its id is derived from the event's (ent_ and 24 hex characters of a SHA-256); its author is <source>@<device>, and its origin the device the memory is on: this store's name for its own files, and on a hub, the device or relay that sent the stream.
    • A file's first version starts an entry. Each later version supersedes the version derived from the file's previous line, and is dated by the file's modification time, after the version it supersedes (invariant 3), so the entry's current version follows the file.
    • A deleted file, or one left without content, ends the entry: its last version is written again, with valid_to set to the time of the deletion. A file that comes back supersedes that version, and the entry is current again. A deletion with nothing to end derives nothing, and the version after a line that derived nothing starts a new entry.
    • An imported entry can be corrected and forgotten like any other. A correction made in Origin supersedes the current version, and the file's next version supersedes the version derived before it; by the rule for concurrent corrections, the later of the two is current, so the file wins once it changes after the correction. A forgotten entry stays forgotten as its file changes: each new version, and the event it cites, is hidden on arrival (invariant 2), and a restore brings the entry back at its latest version. Forgetting hides the entry in Origin only; the agent's file is left as it is.
  • Derived on every store. Like events, these versions are derived by every store that parses the stream, with the same ids and contents: by a device from its own files, and by its hub from the device's mirror or from a relay's lines. So they are never sent to another store: imported_entry lists them, a device leaves them out of what it pushes, and they do not fix a store's name (see Storage). Corrections and forgets of them are sent as usual; the hub has the versions they refer to once it has parsed the device's mirror, which the device pushes first. A hub of an earlier version mirrors the memory streams a device sends without reading them, and reads them once it is upgraded, when it opens its store (migration 4, see Migrations); until then, a device's corrections of imported entries wait for it. A forget of an imported entry, or of its memory event, made on a device before its hub derived the entry covers the entry once it is derived.
  • In results. Entries in MCP results carry imported; the admin UI and the CLI mark imported entries, show when an entry's validity ended, and show a memory event as a version of its file. The MCP instructions tell agents that an imported entry mirrors an agent's own memory file, so a later change of the file supersedes a correction made in Origin.

Entries

  • Remember. An entry is one self-contained statement with a kind, a confidence between 0 and 1, an author (<agent>@<device>: the MCP client's name and the name of the store it writes to, such as claude-code@laptop, or user@<device> for the CLI and the admin UI; see Access for agents that use a hub directly), an optional project and validity range, and at least one source (invariant 1). A source is an event id, an exact quote, or both:
    • With an id, the event must exist and be visible; a quote, if given, must occur in it (whitespace and case are ignored).
    • With only a quote, Origin cites the most recent visible message event containing it, falling back to tool_result events. Tool calls are never matched, because the agent's own remember call, which contains the quote, is itself logged as a tool call. Most recent means by timestamp. Origin looks up the quote's words the way a search does (see Search and timeline) and checks at most the newest 25,000 events of each kind that contain all of them, so a quote made only of very common words may not be found in older events; cite the event id instead.
  • Correct. Correcting the current version of an entry inserts a new version that supersedes it (invariant 3). Sources carry over and new ones can be added. A correction is always dated after the version it corrects, even when the local clock is behind. All versions of an entry share its root, the first version; together they form its chain.
  • Concurrent corrections. Two stores may correct the same version before they hear of each other, so a version can be superseded by more than one. The chain then forks, and every store resolves it the same way: the version with the greatest (created_at, id) in the chain is the current version; every other version is superseded. A superseded version that nothing corrects lost to a concurrent correction and is marked as such (concurrent). Only the current version can be corrected.
  • Status is derived: forgotten if a forget in effect covers the entry (see Forgetting), active if it is the current version of its chain, otherwise superseded (with the current version as superseded_by). Queries return active entries unless history is requested; forgotten entries are never returned. History lists every version of the chain, forks included, oldest first.
  • Validity. An entry may say when it holds (valid_from, valid_to). Once its valid_to has passed, it has ended, like an imported memory whose file was deleted: it is history, no longer current knowledge. Search and listings leave ended entries out unless history is requested (MCP search, origin search, origin entries, and the admin UI's search and entries; origin entries --history and the admin UI's history include them), and get and origin show still show them, with their validity.
  • Imported entries are derived from the memories agents keep for themselves, and marked imported (see Native memories).

Forgetting

forget takes an entry id or an event id and a reason, and records a tombstone, a forget (invariant 2). It hides its target and their cascade by three rules:

  1. Forgetting an entry hides every version of it (its whole chain, forks included) and every event those versions cite.
  2. Forgetting an event hides it and every entry that cites it, as in rule 1.
  3. An entry whose current version is left with no visible source has lost its provenance and is hidden as well, every version of it. This hides no further events, so the cascade stops here.

A chain is hidden as a whole or not at all. Hidden entries and events disappear from search, timelines, session counts, get, and sources, and hidden events cannot be cited. Re-syncing, even a rewritten source file, does not bring them back, because event ids are stable and hiding is keyed by id. A forget that would hide nothing new is not recorded.

A forget is a record. Every tombstone keeps its target, its reason, its time, the store that made it (origin), and, when known, who made it (author: <agent>@<device> for an agent's forget, user@<device> for the CLI and the admin UI). A reason is required wherever a forget is made: the MCP tool, the CLI, and the admin UI refuse an empty one (tombstones written by Origin 0.1 may have none). Nothing is ever deleted from tombstone, so the history of what was forgotten and restored stays complete.

Restoring. The owner can restore a forget at any time, with a reason, from the CLI (origin restore) or the admin UI; agents cannot. A restore is a tombstone too, with its own id, reason, time, origin, and author, whose target is the forget's id: it forgets the forget. A forget is in effect while no restore targets it. Only a forget can be restored, and only once; a restore cannot be restored. To hide the target again, forget it again: that makes a new forget, with a record of its own.

What a forget covers. An entry or event is hidden while a forget in effect covers it. What each forget covers on a store is recorded in tombstone_effect, several forgets per item, in four ways:

  • direct: what the rules give from its target on this store's data, including versions and citing entries that arrive later.
  • listed, on a device: what its hub listed for the forget. Each new list from the hub replaces the last one, since the hub decides rule 3 for the entries it holds and may withdraw a decision after a restore.
  • reported, on a hub: what the device that sent the forget covered there, added to what it reported before.
  • cascade: rule 3 on this store. When a forget is applied, it covers every chain that cites an event it covers and whose current version has no visible source left.

forgotten_entry and excluded_event hold what is hidden, each item with one forget in effect that covers it, and every query filters on them, so hiding costs a query nothing more than before.

What a restore brings back. When a forget leaves effect, each event it covers comes back unless another forget in effect covers it. Each chain it covers, and each chain that cites an event that comes back, is decided again: it stays hidden if another forget in effect covers it by its rules or its hub's list, or if its current version still has no visible source (rule 3), in which case it is covered by every forget in effect that covers one of those sources. On a hub, what devices reported about such a chain is decided again the same way. Everything else comes back, with its session counts, search results, and citations, so a restore leaves the store as if the forget had never been made. The entries a store wrote itself that come back are logged again in change_log, so that a device sends its hub those it held back while they were hidden. The CLI and the admin UI show what a restore will bring back before it is made.

Across stores. Tombstone ids are global, so a forget or a restore created on one store keeps its id, origin, reason, and author on every store it reaches. A store applies a forget by the rules to the data it holds, and covers what the sender listed as far as it knows those ids; it hides all that unless it knows a restore of the forget. A restore lifts its forget, or, if the forget has not arrived, waits for it: a hub keeps such a restore, and a device, which keeps only restores of forgets it holds, receives it again after the forget (see Replication). Applying a tombstone twice changes nothing. Whether an item is hidden depends only on the forgets in effect and on what they cover, and tombstones only accumulate, so every store converges on the same state whatever order forgets and restores arrive in: a restore before its forget, a forget after it was restored elsewhere, either one twice. A version that arrives later in a hidden chain is hidden on arrival, and so is an entry whose sources are hidden here (rules 2 and 3), and an entry that a forget targets, or that cites an event a forget targets, when neither was here as the forget was applied: an entry derived from a native memory has the same id on every store, so it can be forgotten on one store before another derives it.

Forgetting hides; it does not erase. The raw mirror files stay on disk (invariant 3) but are never shown, and a restore relies on them. Physically erasing forgotten content would be a separate, explicit action; it is not implemented. The CLI shows what a forget will hide, and what a restore will bring back, and acts only with --yes.

Search and timeline

Event search covers message events unless tool events are requested (--all / include_tools), and never returns forgotten events. Entry search covers active entries whose validity has not ended (see Entries).

Terms. A query is split on whitespace, and every term must occur in the text, ignoring case, as a substring (log matches catalog). How a term is found depends on its length in characters:

  • Three or more characters: through a trigram index, for CJK as well as Latin text.
  • One or two CJK letters (Han, kana, or Hangul), such as 部署 or 库: through the CJK index, which holds every pair of adjacent CJK letters and the last letter of each stretch of them.
  • One or two other characters, such as AI, C#, or 5号: no index can find them, so Origin checks the text of candidate events, newest first. The candidates are the events the search covers (its kinds, and the requested project, source, and time range) that match the query's other terms. At most the newest 25,000 candidates are checked (SCAN_LIMIT), so such a term can miss older events; a time range, a project, or a longer term reaches them.

Indexes. Three FTS5 indexes serve event search, all keyed by seq. One of them drives a query, newest first, and the other terms filter its matches:

Index Holds Used for
message_fts message events, with the position of every trigram (detail=full) Message searches with a term of three or more characters: each term is matched exactly as a phrase and scored by BM25
event_fts Every event, recording only which events contain each trigram (detail=none), a fraction of the size of a full index Searches that include tool events: an event must contain every trigram of every term, and is then checked against the terms themselves
event_cjk The CJK bigrams of every event with CJK text (detail=none, contentless) CJK terms of one or two letters, and queries whose only other terms are that short

Order. By default, results are ranked by BM25 relevance, best first. Ranking considers the newest 2,000 matches (RANK_WINDOW), or all of them if there are fewer: when a query matches more events, the older ones are neither ranked nor returned, so very common terms favour recent events, and a time range moves the window. For messages, BM25 scores the terms as phrases; with tool events, it scores their trigrams. Queries without a term of three or more characters, and --recent, are ordered by time instead. Ordered by time means newest first by timestamp, and among events with the same timestamp, the later arrival first; ties in relevance are broken the same way. Results ordered by time, and the window of the newest 2,000 matches, are exact whatever order events arrived in: a query reads its index by blocks of arrivals (see Storage) and stops as soon as no unread block can hold a newer match than those it needs. A query with only a few matches (up to 2,000, or five times as many as it needs) reads them all instead.

Snippets show up to 200 characters of text around the first match, with every match in brackets.

Entries are searched by the same terms: terms of three or more characters through entry_fts, ranked by BM25, and shorter ones by checking the text (entries are few). Without a term of three or more characters, entries are newest first.

The timeline lists sessions by time, or the events of one session in order (tool events are hidden unless requested).

Session names. A session's title is the one its agent gave it, or the user through the agent (see each source's mapping); Codex gives none, and the others not always. A session without a title has a prompt instead: its first prompt, as a provisional name that UIs can show as such. It is the text of the earliest visible user message event of the session that is a prompt the user typed: the XML-style elements a message starts with (<tag …>…</tag>), which agents add as context or command echoes, such as Claude Code's <command-name>, <local-command-stdout>, and <local-command-caveat>, or Codex's <environment_context>, are skipped and what follows them counts, and a message left empty, or holding only a bracketed notice such as [Request interrupted by user], is passed over. Events adapters mark as meta (injected context, synthetic prompts) are not messages. The prompt has its whitespace collapsed and is cut to 80 characters. It is computed from the events whenever sessions are read (listSessions, getSession), through event_user_message, an index of the user messages of each session alone, so that it follows forgetting (invariant 2); nothing is stored. The MCP timeline, origin timeline (in quotes), and the admin service's sessions carry it next to the title.

MCP server

origin mcp serves these tools over stdio. Results are JSON text with local ISO 8601 timestamps, to the millisecond.

Tool Purpose
search Full-text search over entries and events; without a query, lists recent entries. Filters: scope, kind, time range, source, project, tool events
timeline Sessions in a time range (default 7 days), each with its title or, without one, its first prompt (see Session names); or the events of one session
get An entry with its sources and version history, or an event with its full text
sources Original log lines for an entry or event: file, mirror, line number, raw text
remember Write an entry with kind, text, confidence, and sources
correct Supersede an entry with a corrected version
forget Hide an entry or event, per the rules above, with the user's reason; only on the user's explicit request. There is no tool to restore: only the owner restores, from the CLI or the admin UI
  • Freshness. The server ingests all sources when it starts, before search and timeline (at most every 10 seconds; on a device connected to a hub, before get and sources too), and always before remember and correct, so an agent can cite lines from the conversation it is in.
  • While the agent runs. The server also looks for new log lines every 60 seconds (ORIGIN_WATCH_INTERVAL_MS; 0 turns this off), so that what is written after the agent's last tool call reaches the store, and the hub, without waiting for the next agent. A look compares each log file's size and modification time with what they were at the last ingestion, without reading the files, and ingests only if something changed. It runs in the background through the same gates as the other refreshes, so it never overlaps another sync, one look at a time; a tool call never waits for it unless it needs a refresh anyway, and a long ingestion lets tool calls through between its transactions.
  • When the agent leaves. When standard input ends, or the server gets SIGTERM, SIGINT, or SIGHUP, it ingests once more, and syncs with the hub when connected, waiting at most 5 seconds, then exits. An agent that kills the server outright skips this; the next start catches up.
  • On a device connected to a hub. The server still answers from the device's own store, and read tools (search, timeline, get, sources) ask the hub's /mcp at the same time. Results from both are merged, de-duplicated by id, and labelled origin: "local" (this device has it) or "hub". What the device has forgotten is never shown, whatever the hub returns, and of the versions of one entry, only the current one by the rule in Entries is listed; get merges an entry's history from both stores. get and sources for an id the device does not have are answered by the hub. If the hub does not answer within 2 seconds (ORIGIN_HUB_TIMEOUT_MS) or cannot be reached, the tool answers from the device alone and adds a second text item saying that the hub was not consulted. The server opens its hub connection as soon as the agent has initialized, so the first read does not spend that time on the TLS handshake and MCP initialization.
  • Writes on a connected device land in the device's store first and are then synced up. The server syncs with the hub before read tools at most every 10 seconds, right after every write, after each background look that found new lines, and when the agent leaves, and never holds an answer for the hub longer than the timeout. A sync that failed is retried at the next background look. correct and forget of an entry or event the device does not have are carried out on the hub, and fail with a clear error when it cannot be reached.
  • On a relay. Every tool is handed to the hub's /mcp, and the hub's answer comes back as it is, without origin labels. First, the server sends the hub the agents' new log lines: before read tools at most every 10 seconds, waiting at most the hub timeout, and before every write, waiting at most 15 seconds, so that an entry can cite what was said a moment ago. It waits up to 15 seconds for the hub's answer, since it has none of its own; when the hub cannot be reached, the tool fails with an error saying that the relay cannot answer offline. It also sends new lines every minute while log files change, and once more when the agent leaves (see Relays).
  • Guidance. The server's MCP instructions tell agents when to search, when to remember (decisions with reasons, stable facts, preferences), that forget needs an explicit request and the user's reason, that only the user can restore what was forgotten, and that an imported entry mirrors an agent's own memory file, so a later change of the file supersedes a correction made in Origin. Entries in results say whether they are imported.
  • Errors. Invalid input, unknown ids, and forgotten ids come back as tool errors with a readable message.

Personal hub

A hub is an ordinary Origin store that also runs origin serve. It is self-hosted and single-tenant: one person, their devices, their agents. Nothing in the hub is specific to any hosting provider; the published container image ghcr.io/openlinkos/origin runs it anywhere.

Roles

Every store is a replica; the hub is the one that is always online.

  • Device: any machine with agent session logs. Its store is complete on its own: it ingests locally (adapters, raw mirrors, cursors, rewrite generations, compression are unchanged), its agents read and write it, and it keeps working offline. Connected to a hub, it sends the hub what it writes (origin sync, and origin mcp as it goes) and reads what other stores have through the hub.
  • Relay: a device that keeps no memory of its own, for a machine with little disk. It sends its agents' logs to the hub as they are written and answers everything through the hub (see Relays).
  • Hub: origin serve exposes MCP over HTTP for agents, a replication API for devices, and, when enabled, an admin UI for its owner. It holds what every device has sent, plus what cloud agents write to it directly, so everything can be found there. It can keep the sealed frames of its raw mirrors in an S3-compatible bucket rather than on its disk (see Frame stores).

Replication: raw mirrors, entries, and tombstones up; tombstones down

A device sends its hub what it has written itself; the hub sends devices only what was forgotten. A sync (origin sync on a connected device) takes three steps, in this order:

  1. Raw mirrors. A device pushes raw mirror bytes, not parsed events. The hub stores them as its own append-only mirrors and parses them with the same adapters, so provenance (invariant 1) holds on the hub and parser fixes apply everywhere. Event ids are identical on every store.
    • Each remote file is keyed by (source, "@<device>:<original path>", generation); local paths never start with @. Hub mirrors live under raw/devices/<device>/<source>/<relPath>[.g<n>], stored like any other (see Raw mirrors).
    • Pushing is resumable and idempotent: the device asks for the hub's cursors, then sends [cursor, local size) of each mirror in chunks of complete lines (at most 1 MiB, or one longer line). A chunk whose offset does not match the hub's cursor is rejected with the current cursor, and the device resumes from there. Offsets and cursors count bytes of a mirror's content, uncompressed: the device reads its mirror decompressed, the request is gzip-encoded on the way, and the hub compresses what it stores.
  2. Entries and tombstones up. The device sends the entries and tombstones (forgets and restores) it wrote itself (those whose origin is its name), in the order of its change_log, after its push cursor. Entries it derived from native memories are not sent: the hub derives the same ones from the device's mirror (see Native memories). Hidden entries are not sent; one that a restore brings back is logged again, and sent then. The hub stores them under the same ids, with their author and origin, and applies the tombstones by the rules in Forgetting, so a version or entry that arrives into something already forgotten is hidden on arrival. Because the device pushes its mirrors first, the events its entries cite are normally on the hub already; if an entry's cited events or the version it supersedes are not there yet, the hub says which ids are missing, the push cursor stops before that entry, and later entries wait for the next sync. Tombstones are sent all the same: forgetting never waits.
  3. Tombstones down. The device asks for the tombstones after its pull cursor in the hub's change_log; the hub leaves out the ones that device created and sends each forget with what it covers on the hub now. The device applies them to its store, so forgetting and restoring reach every replica (invariant 2). When what a forget covers on the hub changes later, because the hub applied it to data the device did not have, because a correction or entry arrived that it covers, or because a restore made the hub decide rule 3 again, the hub logs it again, and every device, its creator included, receives the new coverage, which replaces the hub's last list. A restore that reached the hub before its forget is logged again once the forget arrives, and a tombstone logged more than once in one page of the feed is sent once, where it was logged last.
  • Only tombstones come down. The hub never sends entries or events to a device: a device's store holds its own sessions, its own entries, and the tombstones that concern them.
  • One judge per entry. When a device applies a tombstone from the hub, it decides rule 3 (no visible source left) only for entries the hub has not received yet. For the others, the hub holds every version, and its verdict arrives as part of the forget's coverage, so a device and its hub agree about an entry they both have. A forget or a restore made on the device is decided there at once with the versions it holds; for entries the hub has, the hub's decision follows. If the hub holds a later version from another store, a restore may leave the device hiding an entry the hub shows (see Open questions).
  • Cursors. A device keeps two cursors per hub URL in setting: how far it has pushed its own changes, and how far it has pulled the hub's. Each is saved after every successful request, so an interrupted sync resumes where it stopped, and running it again sends nothing.
  • Events forgotten on the hub stay excluded when re-pushed, because exclusion is keyed by event id.

Access

  • Every request to /mcp and /v1/* needs Authorization: Bearer <token>; the admin UI under /admin has its own sign-in (see Admin UI). Tokens are created on the hub (origin token create <name>), shown once, stored only as SHA-256 hashes, and revocable. Each device or cloud agent gets its own token.
  • A token is bound to one device, the one it is named after. On the replication API, a token sends and receives data only for that device: what it pushes is stored under the token's name, a request that names another device (device in its body or query) is refused with 403, and so is a record whose origin is not that device. A device's store must therefore have its token's name, which origin hub connect checks (see Using a hub from a device). A token named like the hub itself serves no device; a token used only for /mcp, such as a cloud agent's, needs no device. A revoked token can be replaced by a new one under the same name, so a device that lost its token keeps its name; an active token must be revoked first.
  • The hub speaks plain HTTP. Put it behind TLS (a reverse proxy or CDN) before exposing it beyond localhost.
  • Entries keep the author they were written with when a device pushes them. Entries written through the hub's /mcp record <agent>@<token name> when the caller names its agent in X-Origin-Client, as a device's origin mcp does (claude-code@laptop), and the token name alone otherwise (ci for a cloud agent's token), since a stateless request does not carry the MCP client's name. The device part is always the token's name: a device's origin mcp also sends its name in X-Origin-Device, and the hub refuses (403) a request whose header names another device, so no token writes as another device, and a device whose store and token disagree fails visibly. A relay's origin mcp sends the same headers, so what its agents write is recorded as <agent>@<relay>, with the hub as its origin, and what its CLI writes as user@<relay>.

HTTP interface

Method and path Purpose
GET /healthz { "ok": true, "version": … }, no auth
POST /mcp MCP Streamable HTTP, stateless, JSON responses; the same seven tools as the stdio server
GET /v1/device { device, hub }: the device the token belongs to (its name), and the hub's name
GET /v1/cursors?device=<name> The hub's byte cursor for every file of the token's device
POST /v1/ingest Append one chunk: JSON { device, source, path, relPath, generation, offset, mtimeMs, agentVersion, data } with data in base64 and offset in the mirror's uncompressed content (a relay sends the file's content, which is the same); the body may be gzip-encoded. Returns 200 { bytes, eventsAdded, … } or 409 { bytes } on an offset mismatch
POST /v1/entries { device, entries } with at most 1,000 entries, each { id, kind, text, confidence, author, project, supersedes, validFrom, validTo, createdAt, origin, sources: [{ eventId, quote }] }, whose origin must be the token's device. Stored in order; returns 200 { applied, pending }, where pending is null, or { id, missing } for the first entry whose cited events or superseded version are not on the hub yet (nothing after it is stored). 409 for a different body under an existing id
POST /v1/tombstones { device, tombstones } with at most 1,000 tombstones, each { id, target, reason, createdAt, origin, author, entries, events } (author optional) with what it covers on the device, whose origin must be the token's device. A restore's target is a forget's id, and it lists nothing; one whose forget the hub has not received yet is kept and takes effect when the forget arrives. Applied by the rules in Forgetting; returns 200 { applied }, or 409 for a different body under an existing id
GET /v1/tombstones?after=<seq>&device=<name> { tombstones, next, more }: the forgets and restores in the hub's change log after sequence number after, except those the token's device created, each with its id, target, reason, createdAt, origin, author if known, and, for a forget, the entries and events it covers on the hub. Continue with after=<next> while more is true
GET, POST /admin/… The admin UI when enabled: server-rendered pages behind an admin sign-in. 404 when disabled
GET, POST /admin/api/… The admin UI's JSON API: the same operations, session, and checks as its pages

The hub validates every field. A replication request acts for its token's device: device is optional, and a request that names another device is refused with 403 and the token's device in the body ({ error, device }). Device names match [a-z0-9][a-z0-9._-]{0,62} and may not be the hub's own name, the source must be a known adapter, and relPath must be relative with no .. segment, so a device can never write outside its own mirror directory.

Using a hub from a device

A device uses its hub in one of two modes, chosen when it connects. By default it is a complete store: it keeps its own memory, as without a hub, and replicates with the hub as described above. With --relay it is a relay: it keeps nothing but cursors, sends its agents' logs straight to the hub, and answers everything through the hub (see Relays). The rest of this section is about complete stores, except where it says otherwise.

  • origin hub connect <url> --token <token> [--device <name>] [--relay] stores the connection in <ORIGIN_HOME>/hub.json. The device's name is its store's name (see Storage), and it must be the name of the device's token: the command asks the hub which device the token belongs to (GET /v1/device) and, if that is not this store, refuses and says how to fix it. --device renames a store that has not written any entry or tombstone yet. origin hub status makes the same check. Every sync with the hub, from any command or agent, records in setting when it finished or, if it failed, when and why; origin status and origin hub status show when the last one finished, and the error of a later one that failed. ORIGIN_HUB_URL and ORIGIN_HUB_TOKEN override the stored connection.
  • With a hub configured, origin mcp keeps serving from the device's own store and merges in the hub's answers (see MCP server). It syncs with the hub as it goes: before read tools at most every 10 seconds, right after every write, every minute while log files change, and once more when the agent leaves; it waits for the hub no longer than the timeout. Agent registrations do not change, and an agent can still cite what was said a moment ago.
  • Cloud agents with no local logs register the hub's /mcp URL directly with a bearer token.
  • CLI curation commands (entries, show, correct, forget, …) act on the device's own store. On a connected device, correct and forget then sync with the hub, so the change reaches the hub and, for forgetting, every other device. origin ui also acts on the device's own store, and syncs with the hub in the background after each write. The hub's admin UI shows the entries of every device.

Relays

A relay is a device that keeps no memory of its own, for a machine with little disk. It runs the same adapters and the same origin mcp as any device, but stores no raw mirrors and no events: until the hub has them, its agents' own logs are the only copy of its sessions. So a relay is not a complete memory: it answers nothing without its hub, and the provenance of everything it sends is resolved on the hub, whose mirrors hold the raw lines.

  • Choosing it. origin hub connect <url> --token <token> --relay makes the store a relay. Only a store that holds nothing yet can become one: a store with events, mirrored files, entries, or tombstones is refused, with a message saying how to start an empty one, and nothing is deleted. The mode is kept in the store (setting), not in the connection, so ORIGIN_HUB_URL and a later origin hub connect do not change it, and a relay stays one: to keep a complete store on that machine, use a new ORIGIN_HOME. A relay cannot run origin serve, and while it is not connected to a hub, it neither sends nor answers anything.
  • What it keeps. One cursor per source file, in setting: the generation the hub is receiving, the bytes of it the hub has acknowledged, the SHA-256 of their last 4 KB, when it last changed, and for a compressed file whether all of it has been sent; for a generated source, its token in place of the hash, and the last line the hub has acknowledged, from which its adapter goes on. Cursors are kept in setting rather than raw_file, because a raw_file row stands for a mirror under raw/, which backups, sealing, storage totals, and pushes all read; and in setting, a generated source's last line changes in the same write as its cursor. Besides its cursors, a relay keeps its name, its mode, its replication cursors, and its sync log: a few hundred bytes per source file, plus the last line of each generated source.
  • Sending. origin sync, and origin mcp as it goes, asks the hub for its cursors (GET /v1/cursors), then reads each source file after the relay's cursor, straight from the agent's file: decompressed for a compressed file, and for a generated source, the lines its adapter writes after the last line sent. The complete lines go in chunks of at most 1 MiB (or one longer line) to POST /v1/ingest, as a device's mirrors do, oldest file first. The hub mirrors and parses them with the same adapters, so its mirrors, events, and event ids are those it would hold had a complete device pushed its mirrors of the same files. A cursor moves only once the hub has acknowledged the chunk.
  • Rewrites. A file that shrank, or whose last 4 KB sent no longer match, was rewritten: the relay sends it as the next generation, from offset 0, and the hub keeps the old generation. Generated sources are never checked for a rewrite.
  • Where the hub stands. When the hub holds more or less of a file than the relay's cursor says (an answer lost on the way, another process of the relay that sent first, a hub restored from a backup, a new relay store under a device name the hub knows), the relay goes on from the hub's cursor if the file has a line ending there; a generated source asks its adapter for lines until it reaches that offset. Otherwise it sends the file as a new generation. A chunk refused for its offset (409) is handled the same way. A file the hub refuses outright (400, 413) is reported and skipped, and the others still go.
  • Offline. Nothing is sent and no cursor moves: the new lines wait in the agents' own logs. Some agents delete old logs (Claude Code after cleanupPeriodDays, 30 days by default), and a relay keeps no copy, so a relay must reach its hub regularly. While an agent runs, its origin mcp sends new lines every minute and when the agent leaves; origin status shows, per source, how many bytes are still to send.
  • Warning before cleanup. A relay warns when the oldest file with complete lines it has not sent nears the age at which its agent deletes it, judged by the file's modification time as Claude Code does: for Claude Code, within a week of cleanupPeriodDays (read from settings.json in its config directory, 30 days if it is not set), or past half of it if that is shorter than two weeks; for agents whose retention is not known, once such a file is a week old and the relay has not reached the hub for a week. origin status shows the warning, origin sync when it could not send everything, and origin mcp, at most once an hour while the hub cannot be reached, in its log and in the errors of the tools, which the agent sees. The check is in the CLI; core only tells the oldest such file (relayPending).
  • Answers and writes. A relay has no events to answer from, or to check the events an entry cites against, so every tool is the hub's (see MCP server). Writes are made on the hub, by its rules: the hub checks that every cited event exists and is visible there (invariant 1), applies forgetting (invariant 2), and records the author as <agent>@<relay> with the hub as origin (see Access). The CLI's query and curation commands (search, timeline, show, entries, correct, forget) ask the hub's /mcp too, as user@<relay>; entries --history, projects, forgotten, and restore are not available on a relay, where restoring is done in the hub's admin UI.
  • Replication. A relay writes no entries or tombstones of its own, and the hub's tombstones find nothing in it to hide, so it keeps none; it still pulls them, so that its replication cursors move like a device's. The entries of its agents' memories are derived on the hub, from the lines the relay sends (see Native memories).

Admin UI

The hub can serve a web interface for its owner. It is off by default, and it is a view onto the same store rather than a separate system: every page and every JSON call goes through the core operations that MCP and the CLI use (invariant 5). The package @openlinkos/origin-admin provides a request handler that the hub mounts. origin ui serves the same interface on a device, for that device's own store, with its own sign-in (see the last two points).

  • Serving. origin serve --admin mounts the UI at /admin on the hub's port; origin serve --admin-listen 127.0.0.1:7078 serves it on a separate listener instead, reached through an SSH tunnel or kubectl port-forward. The hub refuses to start if the UI would be served on a non-loopback address unless --public-url (ORIGIN_PUBLIC_URL) is an https URL, or an http URL on a loopback host such as a container port published on 127.0.0.1. Without a public URL, the UI answers only loopback connections addressed to a loopback host name, which also defeats DNS rebinding. With the UI disabled, /admin is a 404.
  • Sign-in. A hub has one admin password and no user names. When ORIGIN_ADMIN_PASSWORD is set, it is the password and takes precedence; it must have at least 10 characters, or the hub refuses to start. Otherwise the password is set on the hub with origin admin password (typed twice on a terminal, or one line on standard input) or generated with origin admin password --generate. If there is neither when the hub starts with the admin UI enabled, the hub generates one (20 random base32 characters, 100 bits), stores its hash, and prints it once to its log. There is no first-visit setup, so a fresh hub cannot be claimed by whoever reaches it first. The password is stored as a scrypt hash (N = 2^15, r = 8, p = 3, a random 16-byte salt), written scrypt$N$r$p$<salt>$<hash>, in setting, which stays on the hub; the hub keeps such a hash of ORIGIN_ADMIN_PASSWORD too, to notice when it changes. Checks run one at a time and compare in constant time. The password and hub tokens reject each other: a password cannot start with ogn_, /mcp and /v1/* accept only bearer tokens, and /admin accepts only its own session cookie.
  • Sessions. Sessions live on the server, in admin_session, keyed by a hash of the session id. They end after 30 minutes without a request or 12 hours after sign-in, and all of them end when the password changes: when it is set with origin admin password, and when the hub starts with a different ORIGIN_ADMIN_PASSWORD than before or switches between it and the stored password. The cookie is __Host-origin-admin (Secure, HttpOnly, SameSite=Lax) behind an https public URL, and origin-admin (HttpOnly, SameSite=Strict, path /admin) on loopback http. Behind an https public URL the plain cookie counts only over a loopback tunnel, so a cookie planted from a sibling subdomain cannot fix a session. Creating or revoking tokens and ending sessions need the password to have been entered within the last 10 minutes. Forgetting and restoring need only the session: each is a record that the other can undo.
  • Requests. Only GET and POST exist, and every write is a POST. A POST needs the session's CSRF token (form field _csrf or header X-CSRF-Token) and an Origin header equal to the hub's origin, and is refused with Sec-Fetch-Site: cross-site. Request bodies are limited to 64 KiB. Failed password checks are limited to 10 per client and 100 in total per 10 minutes (429 with Retry-After); the client address is taken from ORIGIN_CLIENT_IP_HEADER only when that is set, and otherwise from the connection. Behind a proxy or CDN, that header must be set: otherwise every client shares the proxy's address and one limit, and anyone's failed attempts can lock the owner out.
  • Responses. Pages are rendered on the server by a template that escapes every interpolated value. The agent's messages are rendered as a small subset of Markdown (paragraphs, headings, emphasis, inline code, fenced code, lists, blockquotes, thematic breaks, GitHub's pipe tables, and links), by a renderer in the admin package that only recognises structure and outputs every piece of text through the same template, so that HTML in a message stays text; links go only to http and https URLs, open in a new tab with rel="noopener noreferrer", and show their URL. A small script adds conveniences, such as loading a raw line in place, but no page depends on it. Every response carries a strict Content-Security-Policy (no inline script or style, Trusted Types with no policy), Cache-Control: no-store, private, Vary: Cookie, and headers against framing, sniffing, referrers, and indexing, plus HSTS behind https. Every asset, the design system's stylesheet, fonts, and icons included, is served by the hub itself under a content-hashed name; each page inlines only the icon symbols it uses, and the UI never makes an external request.
  • What it can do. An overview (counts; visible events per day over the last 30 days, by source; recent sessions; the projects with the most events then; per source, its agent's latest version, its last ingestion, and its parse errors over 7 days; devices and their last push; the latest entries, and the latest forgets and restores, with their target ids, reasons, and counts only; storage: the database, the raw mirrors on disk, and the frames in a bucket), tokens (create, with the token shown once next to ready-made connection commands, and revoke), devices, the timeline (sessions by the day of their last activity, filtered by source, project, or words in their name), sessions by turns (see below), events with their provenance chain (the raw line loads on request, truncated to 4,000 characters), search, entries and their history, correct (confidence 1 unless given), remember from an event page (citing that event, author user@ and the hub's name), forget after a preview, with a reason: the request carries the digest of the plan that was shown, and nothing is forgotten if the plan has changed since (409); and the Forgotten page, which lists every forget and restore with its time, origin, author, reason, and what it covers. A forget in effect has a page of its own that shows its owner what it hides, the entries' text and the session events, and restores it with a reason, after saying what will come back.
  • Sessions by turns. A session's page groups its events into turns. A turn starts at a prompt: a visible user message event that the rule of provisional names takes for a prompt the user typed (see Search and timeline), except those of sub-agents (Claude Code marks their threads sidechain). A turn also starts at a command echo (a message of elements only, with Claude Code's <command-name>) that the agent acts on, with a message or a tool call of its own, before the next prompt or command; the command and its arguments are then the turn's prompt, and finding that out costs one look into the (source, session_id, ts) index per command. A command the agent does not act on, such as /clear or /model, starts no turn and is a note in the turn around it; the provisional name still skips every command. A turn holds every event until the next turn starts, and what comes before the first is turn 0. In a turn, tool calls are paired with their results by call id (callId, or Claude Code's toolUseId), or, without one, with the first earlier call that has none and no result yet; a call's outcome is its result's error flag or status. The agent's last message after which it called no tool is the reply, rendered as Markdown; its calls, and what it said before the reply, are folded behind one summary line; command echoes, their output, injected context, and system events are shown as notes. A page holds 20 turns, or fewer if they would pass 1,500 events, and a longer turn comes in parts of 1,500 events. The prompts are read through event_user_message, a partial index of user messages alone, and then only the events from the page's first prompt to the next page's, through the (source, session_id, ts) index, so a session of thousands of events is never read whole. A link from an event (at=<id>) opens the page at the turn that holds it. The days of the session's index of days and of the timeline are those of the server's time zone.
  • Overview queries. Everything on the overview comes from the derived tables (session, sync_log, raw_file, raw_chunk) and from indexes, never from a scan of event. The chart and the busiest projects count the visible events of the last 30 days from the (source, session_id, ts) index alone, one index entry per event (about 100 ms for 270,000 events), for the sessions active in that time, minus the forgotten events, found through excluded_event; the result is kept while the day and every source's totals stay the same. An agent's latest version comes from the latest events of its five latest sessions. With a bucket, storage on disk stats each mirror's frame file, at most once a minute.
  • What it cannot do. Edit or delete events, raw mirrors, entries, or tombstones; show any text of hidden content outside the page of the forget that hides it (lists show a tombstone's target id, its reason, and counts; the audit log keeps ids and counts only); save an entry without a visible source; show a token again; change the admin password or other security settings; run arbitrary queries or read any file other than a visible event's mirror line.
  • Audit log. Sign-ins and failed attempts (collapsed to one record per client and window), token changes, entries written, forgets and restores, and ended sessions from the UI (actor owner, or local in origin ui); token and password changes from the CLI (actor cli); and passwords generated or changed when the hub starts (actor hub) are appended to audit_log with the channel, client address, and user agent. Details are ids and counts, never text, reasons, or passwords, so hidden content stays hidden; triggers reject updates and deletes.
  • On a device: origin ui. The CLI serves the same pages for the store of the machine it runs on (~/.origin or $ORIGIN_HOME), on 127.0.0.1 only, port 7079 unless --port says otherwise; it refuses any other address. It answers only requests whose Host is 127.0.0.1:<port> or localhost:<port>, which defeats DNS rebinding, and keeps the response headers, the body limit, and the checks of every POST above. There is no password, so there are no tokens, devices, connection commands, or admin sessions to manage: those pages and calls do not exist there, and what needs a recent password on a hub needs only the session. It writes what the hub's UI writes (remember, correct, forget after a preview, and restore), as user@<device> and with actor local in the audit log. On a device connected to a hub, every write is followed by a sync in the background, as after origin correct and origin forget, and the overview shows the hub, when the device last finished a sync with it (kept in setting), and whether the last sync of origin ui failed. When it starts, it ingests the device's agents in the background, as origin sync --local does, serving between write transactions as origin mcp does, so that the UI shows what the agents wrote last; stopping it does not wait for the ingest, which the next sync takes up where it stopped. A relay has no store to show, and origin ui refuses to run on one.
  • Signing in to origin ui. When it starts, it prints a link with a one-time secret (32 random bytes) and opens it in the default browser with what the platform has built in (cmd /c start, open, xdg-open), unless --no-open. The browser is given a file that redirects to the link, readable only by its owner and removed on exit, rather than the link itself, since other users of the machine can read command lines. The secret works once, within 5 minutes, and only in that process, which keeps just its hash. Pressing Enter in the terminal it runs in issues a new link, printed and opened the same way, and the previous one stops working, used or not; standard input is read only when it is a terminal, never from a pipe or a file. The secret is exchanged for a session, and the response redirects to /admin/, so the secret stays out of the address bar and referrers. The session lives in the memory of that process, never in admin_session: it ends when origin ui stops, on sign-out, or after 12 hours, and no other server accepts it, a hub's admin UI on the same store included. Its cookie is origin-ui (HttpOnly, SameSite=Lax, path /admin): its own name, since browsers share cookies between the ports of a host; no Secure attribute or __Host- prefix, which Safari drops on plain-http loopback; and Lax, since following the link from the file counts as cross-site.

Open questions

  • Cognition at low cost. How do we build and maintain a model of the person without a dedicated extraction pipeline? For now, agents write cognition entries themselves when they notice something. Whether that is enough is still an open question.
  • Native memory interop. Origin imports the memories of Claude Code, ZCode, and Grok Build, one way (see Native memories). Not designed yet: writing entries back into agents' memory files, and importing Codex's memories (the summaries it merges under ~/.codex/memories/ when they are enabled); Cursor no longer keeps memories on disk.
  • Change notification. origin mcp looks for new log lines every minute while an agent runs, comparing file sizes and times, and syncs once more when the agent leaves, so a device needs no scheduled task while its agents use Origin. What remains: lines written by agents that do not use Origin, or after an agent that kills its server, wait for the next agent or origin sync; and watching files rather than looking every minute would cut the delay, but file watching is unreliable on some platforms.
  • Relays and agents' cleanup. A relay keeps no copy of what it has not sent yet. If its hub stays unreachable for longer than an agent keeps its logs, those sessions are lost. A relay warns before that happens (see Relays), but cannot prevent it: keeping a copy of the logs at risk is not designed yet. Only Claude Code's retention is known, and only from its user settings, not from managed settings, which take precedence; other agents get a warning only after a week without a sync.
  • Restoring with stale versions. A device decides rule 3 with the versions of an entry it holds. When another store has corrected one of its entries on the hub, a restore can leave the device hiding that entry while the hub shows it, until a later forget or restore concerns it again. Sending entries down, or the hub's decisions separately from what a forget covers, would close this; neither is designed.
  • Erasing. Forgetting hides and restoring brings back, so the raw lines stay on disk, in backups, and in a hub's bucket. An explicit erase of forgotten content, with its own record and its own limits (it could not be restored), is not designed.
  • Hub storage backends. A hub can keep the sealed frames of its raw mirrors, most of its data, in an S3-compatible bucket, and keeps the rest on its disk: the open tails and the SQLite database. Not designed yet: moving frames from a bucket back to disk or to another bucket, a frame store that transfers frames without blocking the hub, and the store in PostgreSQL for hubs that outgrow one disk.

Prior art

Surveyed in October 2026. Origin borrows ideas from these projects but depends on none of them.

Project Idea Origin borrows
Hindsight Entries cite their evidence and expand to the original text; an explicit "mental model" layer
Graphiti Bi-temporal validity (valid_from / valid_to), without building a graph
GitHub Copilot Memory Citations that are re-validated before use
MacPaw Portable Memory Deletion tombstones as a first-class concept
Codex memories Memory derived from local session logs, with references back to the session

Other systems in this space include mem0, Letta, Supermemory, Cognee, and the reference MCP memory server. Most of them lack either fine-grained provenance or a forget operation you can verify, and vendor memories (Claude, ChatGPT, Gemini, Copilot, Codex) are each scoped to one product with no third-party read API.