Skip to content

About

Schema based memory system

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

memory-system

A schema-driven, time-aware company memory, reverse-engineered from gbrain and rebuilt in Go. You define the shape of your knowledge in one schema.yaml; raw markdown goes into source/; an ingest agent turns it into schema-shaped pages; a read agent answers questions from them. Both agents talk to the memory server over MCP with scoped tokens.

                    ┌──────────────── memory server (memctl serve) ─────────────────┐
 source/*.md ──scan─▶ manifest.json (pending / updated / ingested / failed / removed) │
                    │                                                              │
 ingest-agent ─MCP─▶ source_pending → source_read → kb_route (laya) → LLM extract   │
  (write token)     │   → kb_resolve → LLM merge + conflicts → kb_put_page ─────────┼─▶ knowledge/<folder>/*.md
                    │                                   validate · render · index   │        (system of record)
                    │                                   self-wire links · timeline  │
                    │                                                  │            │
                    │                         Postgres + pgvector ◀────┘            │
 read-agent ──MCP──▶ kb_query / kb_timeline / kb_latest / kb_get_page / kb_neighbors │
  (read token)      └──────────────────────────────────────────────────────────────┘
       ▲
   end user (CLI, chat, HTTP /ask, OpenAI-compatible /v1/chat/completions)

Layout

Path What
cmd/memctl server + admin CLI: serve, scan, reindex, token, search, route, stats
ingest-agent/ source → knowledge agent (MCP client, write token)
read-agent/ question-answering agent (MCP client, read-only token)
internal/schema schema.yaml loader/validator
internal/page page model: frontmatter + compiled-truth sections + timeline; render, validate, chunk
internal/manifest ingestion pipeline ledger (manifest.json)
internal/store Postgres/pgvector index + retrieval SQL
internal/linker self-wiring: typed graph edges without LLM calls
internal/timeparse "last week", "latest", "on 2026-09-20" → date window + ordering
internal/search hybrid retrieval pipeline (gbrain's search stack)
internal/decision laya decision engine client, with LLM and lexical fallbacks
internal/kb knowledge-base service: write, index, back-wire, reindex
internal/mcp MCP server (HTTP + stdio), scoped-token auth, tools, client
internal/llm OpenAI-compatible chat / embeddings / rerank client
example-kb/ example schema and raw sources
docs/ gbrain reverse-engineering notes (pipeline, self-wiring, mapping)

Quickstart

docker compose up -d postgres            # pgvector on localhost:5544
docker compose up -d laya                # optional: laya decision engine on :8000 (first build is slow)
cp config.example.yaml config.yaml       # set llm/embedding endpoints; keys come from env
export OPENAI_API_KEY=…                  # or whatever api_key_env names

make build
bin/memctl -config config.yaml migrate
bin/memctl -config config.yaml scan      # index source/ → manifest.json
bin/memctl -config config.yaml serve &   # MCP on http://localhost:8787/mcp

# tokens: scoped by folder and permission; the secret is printed once
INGEST=$(bin/memctl -config config.yaml token create -name ingest -folders '*' -write)
READER=$(bin/memctl -config config.yaml token create -name reader -folders people,meetings,decisions)

MEMORY_TOKEN=$INGEST bin/ingest-agent -config config.yaml              # one pass
MEMORY_TOKEN=$READER bin/read-agent -config config.yaml ask "When was the latest meeting with Globex, and what was decided?"

To run with no API keys at all, use config.dev.yaml: it has a deterministic offline hash embedder and no LLM. The server, pipeline and search all work, though retrieval is lexical-quality. The agents always need an LLM.

1. Schema: define your knowledge base

<kb>/schema.yaml declares the folders and the exact shape of their pages. Example: example-kb/schema.yaml.

timezone: Asia/Ulaanbaatar
rules: |                                  # global ingestion rules
  - Only record facts stated in the source.
folders:
  - name: meetings                        # → knowledge/meetings/
    type: meeting
    description: Meetings, calls and syncs …   # also what the router reads
    slug: "{date}-{title}"                # file name template
    date_field: date                      # the page's effective date
    recency_halflife_days: 60             # search freshness decay (0 = evergreen)
    source_boost: 1.0                     # ranking prior
    fields:
      - {name: attendees, type: links, folder: people, verb: attended}
      - {name: status, type: enum, values: [planned, done]}
    sections: [Summary, Discussion, Decisions, Action Items]
    timeline: false
    rules: |
      - The date field is the day the meeting happened.
    link_verbs:                           # typed edges inferred from prose
      - {verb: reports_to, to: people, patterns: ["reports to"]}

Field types: string text date number bool enum list link links. link and links fields hold page titles or slugs and become typed graph edges.

The server validates every write against the schema. It returns all problems at once, so the agent can fix them in a single retry. It then renders the page deterministically, with sections in schema order.

A page on disk:

---
title: Bob Martinez
type: person
role: VP of Operations
company: Globex
sources: [2026-09-08-globex-kickoff.md, 2026-09-23-globex-update.md]
---

## Summary
Bob Martinez is VP of Operations at Globex (promoted in September 2026) …

## Timeline
- **2026-09-23** | Promoted to VP of Operations at Globex. [source: 2026-09-23-globex-update.md]
- **2026-09-08** | Kickoff call; wants live ETA tracking. [source: 2026-09-08-globex-kickoff.md]

Sections are compiled truth: the current state, rewritten as facts change. The timeline is append-only evidence. This is gbrain's two-zone page pattern.

2. Time-aware memory

Every page has an effective date: when it is about, not when it was written. It comes from the folder's date_field, then date/event_date/published, then a date in the file name, then (for entity pages) the newest timeline entry. Every timeline line is indexed separately with its own date.

Questions are parsed deterministically, with no LLM call (memctl timeparse "…" shows the result):

Query Understood as
"what happened last week" window [Mon, Mon) of last week → timeline arm + dated pages
"meetings on 2026-09-15" / "on September 15" that day
"in March", "in 2025", "2026-09" that month / year (future months → last year)
"last 10 days", "2 weeks ago", "since 2026-09-01", "from X to Y" windows
"yesterday", "on Monday", "өчигдөр", "өнгөрсөн долоо хоног" windows (English + Mongolian)
"latest meeting with Alice", "most recent …", "сүүлийн уулзалт" ordering: newest relevant first
"first decision about pricing" ordering: oldest relevant first
"recently", "what's new", "currently" soft recency boost using each folder's half-life

"Latest meeting with Alice" finds Alice through the title/alias arm. It then follows her attended edges, and orders only the meetings wired to her by date. A newer meeting she did not attend never wins.

Tools: kb_query (dates in the question), kb_timeline (what happened in a window, or one page's history), kb_latest (newest/first pages in a folder, optionally about an entity), kb_list_pages (exhaustive, date-filtered).

3–4, 8. Source folder and the ingestion pipeline

Raw markdown goes in <kb>/source/ (any depth). A scan runs on memctl scan, and on every source_pending call the ingest agent makes. It hashes each file and reconciles <kb>/manifest.json:

status meaning
pending new, never ingested
updated ingested before, content hash changed since
ingested knowledge is current with this file
failed last attempt errored (the error is stored); retried when the file changes or on source_mark reset
removed file deleted (its pages are kept)

Pending files are returned oldest document first, so the timeline and the "current state" are built in chronological order. When a file is marked ingested, the agent reports the hash it actually read. If the file changed mid-ingestion, it stays updated rather than silently losing the edit. Raw sources are also chunked and embedded, so include_sources: true searches content that has not been ingested yet.

5, 7. The ingest agent

For each pending source the agent:

  1. source_read fetches the content, hash and document date.
  2. kb_route asks laya which folders the source is about. This is used as a hint.
  3. An LLM extraction lists the entities and events in the source, restricted to schema folders; anything outside the schema is dropped.
  4. For each entity, kb_resolve finds an existing page by exact slug, alias or fuzzy title, then falls back to semantic search. Dated folders only match the same date: "Weekly sync" on two days is two pages.
  5. An LLM merge rewrites the compiled-truth sections, appends only new timeline events, and reports conflicts:
    • Newer source contradicts the page → the new value wins, and the old claim is recorded as superseded (and stays in the timeline).
    • Older source → current state is kept; the old fact becomes a dated timeline entry.
    • Genuinely contradictory → the current value is kept and the conflict is recorded as unresolved. It is also flagged on the page under ## Open Questions.
  6. kb_put_page validates, renders, writes and indexes the page. A schema rejection gets one repair round with every error listed.
  7. source_mark records the result: ingested with the pages written, or failed with the error.

Entity pages are written before event pages, so a meeting links to the people created in the same pass. Useful flags: -watch 60s keeps polling, -file ingests a single source, and -dry-run renders pages without writing.

5. The read agent

read-agent runs an OpenAI-compatible tool-calling loop over the read-only MCP tools the token allows. It is told to answer only from retrieved pages, give dates, prefer the newest facts for "current" questions, and cite [folder/slug].

read-agent ask "what happened last week?"
read-agent chat
read-agent serve -addr :8788   # POST /ask  and  POST /v1/chat/completions

The /v1/chat/completions endpoint lets any OpenAI-compatible chat UI use the knowledge base. Client-supplied system and tool messages are stripped.

6. Scoped tokens

memctl token create -name sales-bot -folders companies,people,meetings        # read-only
memctl token create -name ingest -folders '*' -write                          # everything
memctl token create -name meeting-writer -folders meetings,source -write -expires 30d
memctl token list
memctl token revoke -name sales-bot
  • Folders are schema folders, plus source (raw sources and the pipeline tools), or *.
  • Only the SHA-256 of a token is stored. Tokens can expire and be revoked.
  • Enforcement happens server-side on every tool:
    • Write tools are hidden from read-only tokens.
    • Every retrieval arm is filtered to the token's folders.
    • Graph edges are returned only when both ends are in scope, so an out-of-scope slug never leaks through a link.
  • HTTP uses Authorization: Bearer <token>. For stdio, run memctl serve -stdio with MEMORY_TOKEN, or -trusted for local admin use.

9. Decision engine: laya

laya is a non-autoregressive encoder that answers typed questions (choice, score, yes/no) in one forward pass, in about 30 ms. It is Python/PyTorch, so it runs as the laya compose service and is called over POST /v1/systemone.

Routing asks one yes/no question per folder ("does this text belong in meetings (…description…)?"). A text can therefore land in several folders. Folders at or above laya.threshold are searched. If routing finds nothing, the search retries on the whole token scope.

The chain is laya → LLM → lexical, so a missing sidecar costs accuracy, not availability. Routing is used by kb_query (read agent), kb_route (ingest agent) and memctl route.

10. LLM backends

Everything speaks the OpenAI wire format: /chat/completions (tools, JSON mode), /embeddings, and optionally /rerank (the Jina/Cohere/TEI/vLLM shape). OpenAI, vLLM, Ollama (http://localhost:11434/v1), LM Studio, OpenRouter, DeepSeek and MiniMax all work. Reasoning models' <think> blocks are stripped. Agents can also be configured purely from env: LLM_BASE_URL, LLM_MODEL, LLM_API_KEY.

TLS: custom CA and skipping verification

Every outbound HTTP integration (llm, embedding, rerank, laya, and the agents' mcp connection) accepts a tls block. The top-level tls block is the default; a per-integration block overrides only the fields it sets.

tls:                                  # default for all integrations
  ca_file: ./certs/corp-ca.pem        # extra root CAs (PEM), on top of the system roots
  insecure_skip_verify: false
laya:
  tls:
    insecure_skip_verify: true        # just laya: self-signed, trusted network
  • MEMORY_CA_FILE and MEMORY_TLS_INSECURE=true override the default from the environment.
  • A missing or invalid CA file fails at startup and names the integration.
  • Disabling verification logs a warning for each integration it affects.
  • Postgres TLS is configured in database_url (sslmode=verify-full&sslrootcert=…).

11. Postgres + pgvector

Knowledge chunks, raw-source chunks and timeline entries are embedded into vector(dims) columns with HNSW cosine indexes. Filtered queries use pgvector iterative scans. Full-text search uses the simple config by default, which is language-neutral and works for Mongolian. The markdown files are the system of record; memctl reindex [-force] rebuilds the index from them. Tables: pages, page_aliases, chunks, links, timeline_entries, source_docs, conflicts, api_tokens.

12. Search modes and self-wiring

See docs/GBRAIN_REVERSE_ENGINEERING.md for the full pipeline and the mapping to gbrain's source files. In short:

Knob conservative balanced (default) tokenmax
results 10 20 50
token budget 4 000 12 000 unlimited
cross-encoder rerank off on (if configured) on
LLM query expansion off off on, budget-weighted (0.5)
relational graph arm off* on on
graph augment depth 0 0 1

* The graph arm still runs for relational questions and for "latest/first X with Y" in every mode.

Development

make test                # unit tests
make up && make test-integration   # Postgres-backed test (disposable DB)
bin/memctl -config config.dev.yaml search -json "latest meeting with Bob"   # inspect ranking

About

Schema based memory system

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages