Abstract: OpenHuman compiles personal data into lossy summaries that retain a path back to evidence.

Snapshot: OpenHuman c480e6d, vendored TinyCortex daaaf6b, inspected on July 23, 2026. The repository and latest release are both at v0.63.1; the project still labels itself Early Beta.

Most AI memory systems are described as a storage recipe: split text, compute embeddings, insert vectors, retrieve top-k. That finds similar paragraphs, but does not decide what deserves long-term storage, what survives a bad summary, how to return to an original message, or how partial ingestion recovers.

OpenHuman's Memory Tree treats those as first-class systems problems. Its current implementation behaves less like a larger vector database and more like an evidence compiler:

raw source
  → canonical Markdown
  → deterministic chunks
  → scoring and admission
  → durable jobs
  → hierarchical source summaries
  → graph/dense retrieval
  → leaf evidence with source_ref

The summaries are deliberately lossy. The compiler's job is not to pretend otherwise; it is to constrain where loss occurs and preserve a route back to evidence.

Scope and ingestion architecture

Older OpenHuman material describes three parallel hierarchies:

  • Source trees organized by origin;
  • Topic trees organized by entity;
  • A global tree organized over time.

That is no longer the production architecture. PR #3059, merged in May 2026, removed global and topic trees. They were derived projections over source trees: expensive copies of retrieval structure that owned no unique original content.

The change also removed the daily digest scheduler, topic-routing fan-out, query_global, query_topic, and their queue jobs. A version-gated migration purges legacy rows, buffers, summaries, sidecars, jobs, and on-disk directories. TreeKind::Global and TreeKind::Topic remain as inert serialization plumbing so old rows can be read and deleted.

The current shape is simpler:

source-owned summary trees
        +
entity occurrence index
        +
weighted entity co-occurrence edges
        +
dense summary retrieval

This encodes an architectural opinion: cross-source discovery does not require copying every source into another authoritative hierarchy. Sources retain ownership; entities provide links. Some documentation still describes the retired design, so migrations, call sites, and executable tests are stronger evidence than diagrams or enum variants.

Memory is downstream of a product request.

OpenHuman is not a memory-library demo. It is a React application inside Tauri v2, with an authoritative Rust core running in-process as a Tokio task.

A normal desktop chat send follows approximately this path:

chatService.chatSend()
  → callCoreRpc("openhuman.channel_web_chat")
    → authenticated loopback POST /rpc
      → jsonrpc::rpc_handler
        → static controller registry validation
          → web_chat::handle_chat
            → start_chat()
              → validation, queue policy, cancellation token
              → spawned run_chat_task()
                → config/profile/session Agent
                → Agent::run_single()
                  → Agent::turn()
                    → per-turn memory and SuperContext
                    → tinyagents model/tool loop
                    → transcript and post-turn hooks
                → chat_done, segments, usage, citations

The Tauri WebView normally calls the loopback core directly with a per-launch bearer. The shell relay is reserved for non-loopback cleartext self-hosted runtimes where WebView mixed-content or CORS rules would otherwise intervene.

start_chat() is a control boundary, not a thin wrapper around an inference request. It validates attachments fail-closed, applies prompt guards, resolves approval replies, and chooses among queue semantics such as interrupt, steer, follow-up, collect, and parallel. It also owns cancellation and the background task.

run_chat_task() resumes the exact provider-ready transcript when possible. A lossy conversation view is a fallback, not the preferred source. Agent::turn() then adds per-turn memory, profile context, citations, and SuperContext before entering the model/tool loop.

The important ownership rule is that the interactive response does not synchronously own all memory work. Transcript persistence establishes what happened in the turn; post-turn hooks and a durable queue continue archival, extraction, embedding, and sealing. A personal assistant that blocks every message on its entire autobiographical pipeline would be unusable.

Ingestion begins with an editorial decision.

OpenHuman's host layer canonicalizes chat, email, and document payloads into a shared Markdown representation. TinyCortex chunks that representation deterministically.

Full bodies live in an inspectable Markdown content store. SQLite stores metadata, previews, lifecycle state, raw references, indexes, and durable jobs. This is not a primary/backup relationship:

  • Markdown is the human-readable evidence body;
  • SQLite is the concurrency and recovery control plane.

The hot ingestion path is intentionally bounded:

canonicalize
→ chunk
→ atomically stage Markdown bodies + hashes
→ score_chunks_fast() with no LLM extractor
→ one SQLite transaction:
   claim document gate
   upsert chunk metadata
   persist cheap score
   store raw refs
   set pending-extraction lifecycle
   enqueue extraction job

Documents claim a transactional key based on source_id or source_id@version. Chat and email are streams, so they rely on content-derived chunk IDs and queue deduplication to make overlapping deliveries idempotent.

The asynchronous extraction worker then reloads the full Markdown body. The SQLite content field is only a short preview after the content-store migration; scoring that preview would silently change admission semantics.

The scorer combines:

  • token count;
  • unique-word ratio;
  • source and metadata priors;
  • interaction signals;
  • entity density;
  • optional priority tags;
  • optional LLM importance.

The LLM does not arbitrate every chunk. Cheap signals define a definite-drop band, a definite-keep band, and a borderline band:

let in_band =
    cheap_total > cfg.definite_drop_threshold
    && cheap_total < cfg.definite_keep_threshold;

let llm_consulted = if in_band {
    // Run the optional extractor. Count it only if it produced
    // an actual importance signal.
} else {
    false
};

That last condition is subtler than it looks. The LLM extractor soft-falls back on transport errors and malformed output. An Ok(empty) result must not be treated as a zero importance value and included in the full weighted denominator; doing so would push borderline chunks below the admission threshold simply because the optional model failed.

Very short, entity-free chatter is dropped even when metadata priors are non-zero. Priority-tagged input bypasses that tiny-message guard. Dropped chunks still receive score rows and reasons for diagnostics, but do not enter the tree.

Kept chunks populate the canonical entity occurrence index. Indexing canonical entity pairs also increments undirected weighted co-occurrence edges in the same transaction. A re-score clears stale occurrence rows before optional re-indexing, preventing dropped facts from remaining as phantom graph hits.

This is the most consequential part of the design. Long-term memory is not primarily a capacity problem. It is an editorial policy about what future turns are allowed to rediscover.

Durable background state.

TinyCortex owns the persistent job store and single-step dispatch engine. OpenHuman retains the Tokio worker pool, scheduler, observability, and product-specific degradation policy through HostQueueDelegates.

The worker does not treat all failures as equivalent retries:

| Failure class | Backoff | Product response | |---|---:|---| | SQLite busy/locked | 1 second | silent | | transient SQLite I/O | 30 seconds | silent | | disk full | 300 seconds | wait for user remediation | | corruption / not-a-database | 300 seconds | quarantine and rebuild, report once | | host filesystem / read-only / ENOSPC | 300 seconds | mark storage degraded, report once | | unknown | 1 second | report every occurrence |

This distinction is operationally important. Paging on every SQLITE_BUSY creates noise; retrying corruption forever creates fiction. “Memory happens in the background” is only a product feature when background failure has durable state, a retry contract, and a visible degradation posture.

Bucket sealing as a two-phase protocol.

Admitted leaves enter a source tree. Level zero and upper levels use different gates:

pub fn should_seal(config: &MemoryConfig, buf: &Buffer) -> bool {
    if buf.level == 0 {
        buf.token_sum >= config.tree.input_token_budget as i64
    } else {
        (buf.item_ids.len() as u32) >= config.tree.summary_fanout
    }
}

Leaves are bounded by model input volume. Summary levels are bounded by sibling count, keeping fan-in stable even when one summary is unusually verbose.

The gate is simple; the seal protocol is not.

append_to_buffer() opens a transaction, verifies the tree is active, and makes (tree_id, level, item_id) idempotent. It updates the item list, token sum, and oldest timestamp before committing.

Sealing then proceeds in two phases.

Phase 1: expensive work without a database write lock.

The engine reads an exact buffer snapshot, hydrates leaf or summary bodies, calls the summarizer, resolves labels, and optionally computes an embedding. No SQLite write lock is held while a model is running.

Blank output or summarizer failure falls back to deterministic concatenation. The semantic artifact may be lower quality, but the tree can still make progress.

Phase 2: conditional commit.

A new transaction verifies that the snapshot prefix still matches. Only then does it:

  1. insert the summary;
  2. index summary entities;
  3. back-link children;
  4. consume exactly the sealed prefix;
  5. append the new summary to the parent buffer;
  6. optionally enqueue the next seal.

Items appended while the summarizer was running remain in the buffer. A competing seal that already consumed the same snapshot causes the late commit to abort cleanly rather than double-consume children. Cascades are capped at 32 levels.

This is a useful general pattern for model-backed systems: treat model output as an expensive proposal, then conditionally commit it against the state it observed. The model may be nondeterministic; the ledger cannot be.

Each summary stores child_ids. Leaves retain source_ref. The resulting contract is:

lossy navigation
      +
lossless escape hatch

A root summary is not truth. It is a map that should make the truth cheaper to find.

Retrieval and evidence provenance

PR #3947 replaced walk and smart_walk loops that could consume 12–25 sequential LLM turns with deterministic E2GraphRAG-style routing. Compatibility names remain, but the implementation now returns structured hits rather than synthesized prose.

Query entities are extracted through a spaCy sidecar by default. If Python or spaCy is unavailable, OpenHuman falls back to Rust extraction. Mechanical patterns such as email addresses, URLs, handles, and hashtags survive that fallback; richer entity grounding may degrade.

Canonicalization must match ingestion. A graph keyed by person:alice cannot help a query extractor that produces an unrelated surface form.

The current fast_retrieve() algorithm has three branches:

No entities: dense retrieval.

It calls query_source() across source-tree summaries, optionally constrained by a time window, and semantically reranks them.

Without a time window, this is not constant-time magic. The implementation selects all non-deleted summaries in the relevant source trees and hydrates embeddings in batches. The final limit bounds output, not necessarily the scan.

Entities without related graph pairs.

It runs dense retrieval with an expanded limit, then stably reranks candidates by how many query entities appear in each hit.

This prevents an absent graph path from turning into total recall failure while still allowing entity occurrence to influence order.

Related entities within bounded hops.

The graph performs bounded shortest-path search over persisted weighted co-occurrence edges. For each related pair, retrieval intersects the nodes where both endpoints occur.

If candidate count exceeds the limit, it tightens the hop bound stepwise. Results are ordered by:

  1. number of matched query entities;
  2. newest occurrence timestamp;
  3. node ID as a deterministic tie-breaker.

Leaf IDs are hydrated in bounded batches; summary IDs are loaded separately. Profile source-scope filtering happens before truncation, avoiding a subtle bug where disallowed hits consume the entire top-k.

There is some architectural sediment in TinyCortex: a newer storage-agnostic graph abstraction derives co-occurrence through an occurrence-index self-join, while the production fast retriever still exports and uses the persisted edge-store BFS. Module comments do not all agree about which graph has been “ported.” The executable call path is the reliable source here.

The second fast path.

The deterministic retriever existed, but production retrieve_memory still delegated to agent_memory. That sub-agent used a model to call retrieval tools, drill into the tree, fetch leaves, and synthesize an answer.

Issue #4677 recorded roughly 30–40 seconds per retrieval even when data was present. Four calls in one turn consumed about 141 seconds. The retrieval primitives were not necessarily the bottleneck; model round trips around them were.

PR #4768 added a production fast path at the shared run_subagent() seam:

agent definition is agent_memory
→ fast path enabled
→ non-empty query
→ entity/topic extraction is non-empty
→ fast_retrieve(limit = 8)
→ hits present:
   return compact evidence block
   iterations = 0
   usage = zero model usage

otherwise:
   run the full model-driven memory agent

The entity-grounding guard is essential. A vague query against a populated profile can always produce some dense top-k. Short-circuiting that result as “completed retrieval” would optimize latency by removing the model's relevance judgment. Ungrounded, empty, failed, and no-hit cases deliberately retain the slower fallback.

The fast path is on by default and can be disabled with OPENHUMAN_MEMORY_FAST_PATH=0 (also false, no, or off).

The PR reports a data-present target of roughly one to three seconds instead of about 35 seconds. I did not reproduce that claim against a real provider and large personal corpus, so it should be read as project-reported performance, not an independent benchmark.

Archivist separates evidence from interpretation.

After a turn, OpenHuman's Archivist:

  • indexes user and assistant text in FTS5;
  • dual-writes a Markdown-backed archive;
  • segments the conversation;
  • generates a recap, embedding, events, and profile updates;
  • ingests closed segments into the Memory Tree.

The final bullet has a strong policy: tree ingestion uses raw prose, never the LLM recap. Tool JSON and base64 image payloads are stripped. Each message receives episodic provenance shaped like:

agent://session/{session}/segment/{segment}#ep{episode}

The tree-ingest failure is non-fatal to the chat path.

This separation prevents a dangerous feedback loop:

raw conversation
→ model recap
→ ingest recap as evidence
→ recap the previous recap
→ repeat

After several cycles, provenance might still look complete while the content has become the system paraphrasing itself. OpenHuman treats the recap as interpretation and raw prose as evidence. That is a better foundation for personal memory than merely attaching a URL to generated text.

Provenance and citation limits.

Memory Tree RetrievalHit objects expose child_ids; leaf hits carry source_ref. The tool contract calls leaf references the authoritative quote source, and the orchestrator prompt asks agents to produce footnotes from retrieved node IDs and references.

However, the generic citations automatically collected during Agent::turn() mostly come from a separate Memory::recall path. Those citations contain memory ID, key, namespace, score, timestamp, and snippet.

So the accurate claim is not “every OpenHuman answer automatically cites its original documents.” It is:

  • the tree data model preserves a path to provenance;
  • retrieval tools can fetch authoritative leaves;
  • correct citation rendering depends on the caller drilling down;
  • the general UI recall citations and tree source_ref path are not yet one unified trust surface.

The distinction matters because provenance hidden in a backend object is not the same product feature as a source the user can inspect beside a claim.

Privacy, deployment fit, and design constraints

OpenHuman stores the Memory Tree database, Markdown vault, workspace configuration, and local runtime state on the device. The current Rust core also implements a live Privacy Mode:

  • local_only blocks external inference and external egress carrying user data;
  • local runtimes are allowed;
  • narrowly classified control-plane requests without user content can be exempt;
  • policy changes are installed into active sessions without requiring restart.

But the default privacy mode is standard, not local_only. The default setup can still use OpenHuman-hosted sign-in, model routing, and search proxying, plus managed Composio OAuth and tool calls. BYOK, local providers, and direct integration modes change parts of that route; some real-time integration features still require managed services.

Issue #2422 publicly challenged the earlier gap between local-first marketing and those managed paths. The current README is more explicit and Privacy Mode is now enforced in core. Both are meaningful improvements. Neither makes “local-first” synonymous with “nothing leaves the machine after installation.”

For software that ingests mail, calendars, documents, and intimate conversation, privacy posture should be inspectable like a schema or network policy, not inferred from an adjective.

Deployment fit.

OpenHuman's design is compelling for researchers, managers, archivists, and long-running project owners who need years of source-owned material to become navigable without losing the path back to original records.

It is not automatically the best memory design for every product:

| System shape | Better fit | |---|---| | Small FAQ or support corpus | Conventional vector top-k is simpler | | Atomic user facts with explicit CRUD | A record-oriented system such as mem0 is more direct | | Document-to-entity knowledge graph | A graph-first RAG pipeline is closer to the core problem | | Personal, multi-source, inspectable history | OpenHuman's source trees and provenance path are distinctive |

The cost of OpenHuman's choice is substantial:

  • Markdown bodies, SQLite state, embedding sidecars, jobs, and migrations must remain aligned;
  • unwindowed dense retrieval scales with summary count;
  • spaCy and fallback extraction do not have identical recall;
  • hierarchical summary drift can compound;
  • the citation experience is not fully unified;
  • current documentation and code comments lag fast architectural changes.

Those are not reasons to reject the design. They are the bill for making memory inspectable and incrementally maintainable rather than treating it as an opaque vector service.

Reusable design constraints.

Three ideas are broadly reusable beyond OpenHuman.

Separate evidence, navigation, and interpretation.

Raw records answer what happened. Summaries and indexes answer where to look. Recaps and final responses answer what it means now. Do not let the third silently overwrite the first.

Use models outside the transaction, then commit conditionally.

Snapshot state, release locks, perform expensive nondeterministic work, and verify the snapshot before committing. This pattern applies to summarization, code generation, planning, and document transformation.

Design degradation before success.

No entities, no graph path, no embedding provider, blank summarizer output, storage corruption, and network prohibition all have explicit fallback behavior. A memory system is defined as much by what it does when a component is absent as by its ideal demo.

Operational conclusion.

The social change here is not that an assistant can retrieve old mail. Search already did that. The change is that software begins deciding which events are durable, which are compressed, and which should reappear at a future moment.

That gives a personal agent editorial power over its user. A system can avoid inventing a dramatic falsehood and still distort a life by repeatedly selecting which details deserve to survive.

The corresponding product requirement is neither “more storage” nor “a larger model,” but memory governance:

  • retention windows by source and relationship;
  • non-compressible commitments;
  • disputed or contradictory interpretations;
  • deletion propagation across summaries and indexes;
  • per-item egress policy;
  • an explanation of why a memory was admitted or recalled.

Knowledge workers overwhelmed by information will value compression. Privacy-sensitive users will value local formats and controllable routes. OpenHuman is trying to serve both groups, which means it must prove two things at once: that its memory is useful enough to delegate to, and governable enough to trust.

A mature personal agent should expose the following provenance claim instead of merely promising comprehensive memory:

This is why I kept it, this is how I summarized it, this is the source I used, and this is how you can delete or dispute it.

Three questions remain open for me:

  1. When a summary conflicts with a leaf, should the system silently prefer the leaf or expose the disagreement?
  2. Which memories should be non-compressible and quote-only?
  3. Should Privacy Mode remain a global switch, or become an auditable egress policy attached to each memory and tool call?

Validation and sources

I ran 165 focused tests on the pinned snapshots:

  • 139 TinyCortex scoring, admission, entity-index, and embedding tests;
  • 9 TinyCortex bucket-seal tests;
  • 7 TinyCortex deterministic retrieval tests;
  • 10 OpenHuman host fast-path tests.

All passed. The host suite used --no-default-features; its warnings did not represent test failures.

This is not a full product certification. I did not run the complete near-10,000-test OpenHuman library suite, desktop E2E, live OAuth, cloud-provider tests, or a large-corpus performance benchmark.

Sources.