Org Memory is a workplace memory service. It stores organizational content as searchable passages and as a governed graph of people, teams, projects, glossary terms, claims, and relationships. Agents retrieve viewer-scoped context under the caller's access controls.
Sync systems push structured change events into an ingress API. The default
agent tool is retrieve_context: one call that composes hybrid passage
search, structured facts, and bounded relationship paths. Lower-level
primitives remain for single-channel needs.
| Layer | Role |
|---|---|
| Postgres (with pgvector) | System of record: documents, chunks, graph, jobs, audits, spend |
| Object store (Supabase Storage or S3) | Raw ChangeEnvelope blobs — choose one backend and its credentials |
| Vendor HTTP APIs | Embeddings, rerank, and optional synthesis, configured in settings |
Org Memory owns this Postgres instance and object store for memory workloads. Missing required settings or object-store credentials stop the process at boot. Missing or failing vendor calls on the request path raise errors instead of substituting invented results.
Host connectors / sync workers
│
▼
POST /ingress/envelope (ChangeEnvelope)
│
├─► Postgres: document, chunks, participants, jobs
└─► Object store: raw envelope bytes
│
▼
Worker process (job queue in Postgres)
├─ embed_chunks (vendor embedder)
├─ extract_graph (LLM → claims / relationships / entities)
├─ identity / conflicts (merge, exclusivity)
└─ taxonomy / collaboration refreshes
│
▼
Agent (via trusted gateway)
│
▼
POST /tools/retrieve_context
├─ hybrid passages (vector + Postgres FTS → RRF → optional rerank)
├─ query_facts (subjects / about, as_of / believed_as_of)
└─ query_paths (bounded walk, same temporal axes)
Write path (ingest) decisions
- Postgres first, then object store. If the blob write fails, the DB transaction rolls back so every committed document row has a matching archived envelope. If the blob write succeeds and the DB transaction later rolls back, ingest best-effort deletes that blob.
- Deletes are tombstones: the document id stays; search stops returning it; evidence lists on graph facts drop that doc; facts with no remaining evidence are retracted.
- Embed and extract run on the worker; ingress only accepts and persists the envelope.
- Registry-bound
structured_fieldswrite person ground-truth claims when the author resolves to a person; unresolved author fails the ingest with 422 instead of accepting with zero structured writes.
Read path (retrieve_context) decisions
- One query embedding is shared with passage search.
- Graph expansion uses explicit
subjectsand/or anaboutname resolved under ACL; passage hits supply text, and subject seeds come from those explicit inputs. - Optional
max_tokenspacks the returned context so agents stay inside a budget. - Diagnostics report which channels contributed so operators can inspect fusion and packing behavior.
POST /tools/retrieve_context
| Input | Role |
|---|---|
query |
Required natural-language question |
mode |
vector_first (default), graph_first, or joint |
subjects |
Explicit {type, id} seeds for facts/paths |
about |
Viewer-scoped name resolved into subject seeds |
as_of / believed_as_of |
World-time and belief-time filters; when omitted, compose derives a temporal plan from the query when it can |
as_of_grain |
World-time matching grain (day | month | quarter | year | unknown); invalid values fail with 422; host value wins over planner grain |
max_tokens |
Optional packing budget |
| filters | Passage filters (source_*, dates, author, …) |
Modes
vector_first— search passages, then expand from subjects /about.graph_first— resolve subjects and pull facts/paths, then search. Requires at least one subject seed (subjectsand/orabout).joint— search and graph expansion for the same query together.
Outputs include passages, hybrid search_facts, per-subject structured_facts
and paths, and fact_diffs when the temporal plan is a snapshot range
(“what changed between A and B”).
Contract: contracts/tools/retrieve_context.request.schema.json.
| Tool | Purpose |
|---|---|
POST /tools/search_knowledge_base |
Hybrid passage search (MCP envelope) |
POST /tools/worldbuilder_kb |
Same search, Worldbuilder response shape |
POST /tools/query_facts |
Claims for one subject |
POST /tools/diff_facts |
Snapshot diff of claims between two as-of (or belief) points |
POST /tools/query_paths |
Bounded relationship walks |
POST /tools/worldbuilder_lookup |
Synthesized person/team/project/glossary profile |
POST /tools/search_procedural_memory |
Procedural memory search |
Auth: X-Api-Key for the calling service; X-Principal-Id /
X-Principal-Groups for the human viewer. Treat SERVICE_API_KEY as root.
Compose over a second product surface. Agents get one default tool
(retrieve_context) built from primitives (search, query_facts,
query_paths). Callers pass subjects / about when they want graph
expansion alongside passages.
Postgres as the memory store. Documents, vectors, FTS, graph, and jobs live in one database operators already run (including managed Postgres with pgvector). Path walks enforce all-visible evidence ACL inside the recursive SQL so private edges never consume the path budget.
Hybrid search inside Postgres. Dense (pgvector / HNSW) plus lexical
(Postgres full-text search with ts_rank), merged with reciprocal rank fusion,
then an optional cross-encoder rerank. Recency decay nudges newer content when
scores are close. Ranking stays in-repo; vendor embed/rerank errors surface as
failures.
Parent/child chunks + content-hash carry-over. Children are embedded and ranked; parents supply readable section text. Unchanged chunk text under the same embedding model reuses stored vectors on re-ingest. Changing embedding model or dimensions re-embeds affected chunks.
Bi-temporal facts. World time (valid_from / valid_to) and belief time
(recorded_at / invalidated_at) are first-class on claims and
relationships. Superseded facts stay for as-of reads. Document event_time
grounds extracted windows; optional time_grain records source precision and
is returned on query_facts, hybrid search facts, and graph cards.
Registry-exclusive slots close on the write path so “current” answers see one
winner. retrieve_context accepts as_of / believed_as_of / as_of_grain,
and derives a temporal plan from the query when timestamps are omitted
(ambiguous questions return structured ambiguity). Snapshot range plans also
fill fact_diffs. Ledger: docs/temporal-model.md. Pipeline:
docs/temporal-truth.md.
Two ACL rules. Passages use any-visible (see the doc → see its chunks). Claims/relationships/entities use all-visible (every evidence doc must be visible). Entity browse/search applies that filter in SQL so private entities never consume result limits. Partial evidence omits the fact from that viewer's graph reads.
Closed taxonomy registry. Extraction and promotions bind to registry predicates and types on the write path.
Single squashed Alembic revision (0001). Schema changes edit that file
in place. Already-migrated databases are recreated to pick up in-place DDL
(indexes, columns); CI and Docker always start fresh.
Object-store backends are peers. Set OBJECT_STORE_BACKEND to supabase
or s3 and supply that backend's credentials.
Worldbuilder profiles are derived. Cached by evidence doc set and
re-grounded on every hit so ACL changes reshape the visible profile. Host User
identity flows through the platform_user alias bridge instead.
- Embed the query (OpenAI-compatible
/embeddingsby default). - Dense candidates: pgvector cosine over HNSW, ACL in SQL.
- Lexical candidates: Postgres FTS (
tsvector+ts_rank), same ACL. - Reciprocal rank fusion (RRF).
- Recency decay (
half_life_days/min_decay). - Cross-encoder rerank when the shortlist is larger than the requested limit.
- Retrieval audit row (who searched, what returned).
On worldbuilder_kb, about scopes to documents where a resolved person
participates; author filters authorship. Those are distinct.
- World time — when the fact held in reality.
- System time — when the service believed it.
query_facts, query_paths, retrieve_context, and graph person/entity
cards (GET /v1/graph/persons/{id}, GET /v1/graph/entities/{id}) accept
as_of, believed_as_of, and as_of_grain. Belief-only reads also apply world
validity at the belief instant (host as_of still wins when both are set).
When those axes are set, the hybrid fact channel inside retrieve_context
uses the same windows (active + superseded). Subject claim and edge viewer
reads apply those filters (and all-visible evidence ACL) in SQL alongside
hybrid candidates and path walks. When both timestamps are omitted,
compose derives a temporal plan from the query text; when that plan is
ambiguous, a spend-gated synthesis assist may resolve it (or leave ambiguity /
surface vendor errors). Explicit host timestamps always win; host as_of_grain
wins over planner grain. Snapshot questions (“what changed between A and B”)
produce a plan with range_end; compose then includes fact_diffs (and hosts
can call POST /tools/diff_facts directly). Point as_of upper-bounds passage
event_time (belief upper-bounds updated_at); range plans also set the
matching lower bound when the host
omits those filters. Omitting both axes on structured reads means currently
active facts whose validity contains now.
Schema 0001 indexes subject/endpoint plus temporal ranges. Storage:
docs/temporal-model.md. Pipeline: docs/temporal-truth.md.
On re-evidence, temporal merge fills open ends only; corrections use supersession or a new object value.
Active facts also rank with exponential freshness
(FACT_FRESHNESS_HALF_LIFE_DAYS / FACT_FRESHNESS_MIN_DECAY; per-predicate
override in the ontology).
All application code lives under src/org_memory/.
| Package | Responsibility |
|---|---|
api/ |
FastAPI routes, auth dependencies, tool JSON envelopes |
services/ |
Product logic: ingest, retrieval, compose, extraction, worldbuilder, proposals, retention |
db/ |
Engine, ORM (orm.py), repositories; ACL in SQL where it belongs |
domain/ |
Pure models, principals, emails, fact lifecycle, job type names |
adapters/ |
HTTP embedder, reranker, synthesizer; Supabase Storage; S3 |
ports/ |
Protocols for object store, embedder, reranker, chunk search |
workers/ |
Job poll loop; workers/handlers/ one module per job family |
core/ |
Settings, wiring, errors, metrics, logging |
taxonomy_registry/ |
Load/validate closed knowledge ontology JSON |
Supporting trees: contracts/ (ChangeEnvelope, taxonomy meta-schema, tool
schemas), config/taxonomy_registry/ (live ontology instances),
alembic/versions/0001_initial_schema.py (only schema revision),
docs/temporal-model.md.
| Module | Does |
|---|---|
services/ingest.py |
Apply one ChangeEnvelope: docs/chunks/jobs + object-store archive |
services/chunking.py |
Parent/child text segmentation for documents |
services/retrieval.py |
Hybrid search orchestration, diagnostics builders |
services/retrieve_context.py |
Compose search + facts + paths; packing |
services/facts_query.py |
Shared subject-fact fetch used by HTTP facts and retrieve |
services/facts_diff.py |
Two-snapshot subject fact diff (world or belief) |
services/temporality/ |
Grounding, grain match, intent (+ LLM assist), eager close, diff |
services/ranking.py |
RRF / score helpers with deterministic ties |
services/extraction.py |
LLM extract loop + apply entities/claims/relationships |
services/extraction_windows.py |
Pure overlapping window split for long documents |
services/worldbuilder/ |
Resolve subject, gather evidence, synthesize profile |
services/worldbuilder/resolution.py |
Name → person/entity under viewer ACL |
services/worldbuilder/read_source.py |
Load cited docs/records with explicit ok/denied outcomes |
services/worldbuilder/profile_structure.py |
Pure parse/ground/seed of profile JSON |
services/worldbuilder/synthesis.py |
Cache, LLM call, profile payload |
services/worldbuilder/service.py |
Orchestration facade |
services/identity_merge.py / entity_resolution.py |
Person identity candidates and merges |
services/taxonomy_proposals.py / promotions.py |
Host field-value proposals and agent promote |
services/procedural_memory.py |
Procedural memory create/search |
services/retention.py / collaboration.py |
Retention purge; collaboration edge aggregation |
services/structured_writers.py |
Registry-bound structured fields → claims (fail closed if person unresolved) |
| Module | Does |
|---|---|
db/orm.py |
SQLAlchemy models aligned with 0001 |
db/repositories/documents.py |
Documents, chunks replace, embedding carry-over |
db/repositories/chunks.py |
Vector + FTS candidate queries with ACL |
db/repositories/graph/ |
Graph package: base, search, writes, traversal |
db/repositories/people.py |
People and aliases |
db/repositories/jobs.py |
Enqueue / claim job queue |
db/repositories/audit.py, versions.py, spend.py, … |
Audits, doc versions, token spend, legal hold, proposals |
| Handler | Job |
|---|---|
handlers/embedding.py |
Embed chunks; refresh identity embeddings |
handlers/graph_extraction.py |
Run extraction; enqueue conflict / proposal follow-ups |
handlers/identity.py |
Adjudicate person merges |
handlers/conflicts.py |
Resolve claim / relationship exclusivity |
handlers/proposals.py |
Generate / webhook taxonomy proposals |
handlers/collaboration.py |
Rebuild collaboration edges |
| Area | Routes |
|---|---|
| Ingress | POST /ingress/envelope |
| Agent tools | retrieve_context, search, facts, paths, worldbuilder, procedural |
| Graph cards | /v1/graph/persons/..., /v1/graph/entities/... (as_of / believed_as_of / as_of_grain) |
| Admin | jobs, spend, legal holds, retention, connectors |
| Proposals / promotions | /v1/taxonomy-proposals, /v1/promotions |
Copy .env.example to .env and fill every required value. The process names
the missing setting and exits if configuration is invalid.
cp .env.example .env # fill in real keys
docker compose up --buildCompose starts: Postgres (pgvector) → one-shot alembic upgrade head → API on
http://localhost:8000 → worker. The migrate service must succeed before API
and worker start.
python -m venv .venv
# Windows: .venv\Scripts\Activate.ps1
pip install -e ".[dev,s3]"
alembic upgrade head
uvicorn org_memory.main:app
python -m org_memory.workers.run| Endpoint | Meaning |
|---|---|
GET /healthz |
Process is up |
GET /readyz |
Postgres SELECT 1 + object-store ping |
GET /metrics |
Prometheus text (API-key trust like admin) |
GET /v1/admin/health |
Spend alerts, retention warning, worker lag |
| Marker / path | What it covers | Needs |
|---|---|---|
tests/unit/ (not integration and not postgres) |
Ranking, retrieve modes, packing, extraction windows/ontology, worldbuilder profile structure, handlers smoke, wire shapes, settings, retrieval eval metrics/harness | Nothing external |
tests/postgres/ (-m postgres) |
ACL SQL, chunk embed carry-over, facts/paths temporal contracts, retrieve_context, worldbuilder, temporal indexes | DATABASE_URL |
tests/integration/ |
Real vendor calls when credentials exist | Vendor keys; skipped when absent |
Gold questions and expected document/claim ids live in
evals/retrieval/gold_set.json. Labels are evaluation-only.
Offline scoring (you already have ranked ids):
python -m org_memory.eval.score_retrieval --predictions evals/retrieval/example_predictions.jsonLive eval (seeds a hermetic workspace, runs retrieve_context, scores):
# DATABASE_URL required. Uses a fixture embedder (planted vectors), not a vendor.
python -m org_memory.eval.run_live
python -m org_memory.eval.run_live --predictions-out /tmp/preds.jsonWhat run_live does:
- Creates an isolated
eval-*workspace in Postgres. - Seeds the gold documents, chunks (with planted embeddings), people/entities, and claims with the ids named in the gold set.
- For each gold case, calls
retrieve_contextwith the case query / mode / subjects /as_of. - Collects ranked
doc_idsandclaim_ids, optionally writes them, and prints hit/recall/precision@k and MRR.
Grow the gold set with real questions as you find failures. Re-seed and re-run before treating Step 12 retrieval changes as improvements.
pytest -m "not integration and not postgres" # default local / CI verify
pytest -m "not integration and not postgres" --cov
pytest -m postgres # hermetic SQL; set DATABASE_URLPostgres tests create isolated hermetic-* workspaces against a real
database. CI runs unit and postgres jobs against a fresh migrated Postgres.
Because schema is a single squashed 0001, a database stamped before a schema
edit (for example new temporal indexes) will fail index assertions until you
recreate the DB and run alembic upgrade head again.
Point host agent tools at retrieve_context first; keep primitives for
narrow flows.
Workers emit ChangeEnvelope JSON to POST /ingress/envelope. Principals use
user:<uuid> and group:<uuid>. event_time must be timezone-aware (UTC
recommended), year ≥ 1990, and not more than one day in the future — it is
t_ref for temporal grounding. Permission-change envelopes update ACL
fields and ACL event times (last-writer-wins on those times). Connectors
(Slack, Gmail, …) stay on the host; Org Memory consumes their envelopes.
Trusted gateway verifies the human session, then calls Org Memory with:
X-Api-Key: $SERVICE_API_KEYX-Principal-Id: user:<uuid>X-Principal-Groups: group:<uuid>,...when needed- Admin routes also need
X-Principal-Roles: admin
Also: POST /v1/procedural-memories, POST /v1/promotions,
GET /v1/graph/persons/by-platform-user/{platform_user_id}.
Send a verified ChangeEnvelope identity key with
namespace: "platform_user" and value: "<host User UUID>" (same UUID as
user:<uuid> principals). Stored as PersonAlias with
source_system = identity:platform_user. Person cards expose
platform_user_id when present. Promotions can auto-fill host_entity_id
from that alias.
Field-value proposals for ontology fields with a platform_binding
(GET /v1/taxonomy-proposals; optional TAXONOMY_PROPOSAL_WEBHOOK_URL).
Knowledge fields (title, manager, team) use this path. Operational
workflow/task mutations stay on the host.
| Write class | Path |
|---|---|
| Knowledge fields | POST /v1/promotions or proposals → host apply/reject |
| Operational process | Host APIs directly |
Conflict rule: login / permissions / assignment → host User wins.
Inferred knowledge inside OM → OM wins until a proposal is applied or
rejected. Worldbuilder profiles are derived read models; the identity bridge
is the platform_user alias on persons.
When TAXONOMY_PROPOSAL_WEBHOOK_SECRET is set, webhooks carry
X-Org-Memory-Signature: sha256=<hex>. Host apply should be idempotent on
proposal_id (or slot + value), then callback
POST /v1/taxonomy-proposals/{id}/applied or /rejected.
Host platforms own workflows, tasks, SMS, automations, and last-writer-wins storage for host ontology fields. Org Memory supplies memory, graph facts, and field-value proposals for bound knowledge fields.