chronicle

chronicle

Aggregates personal events from multiple sources into a unified timeline and exposes them via MCP for AI assistant retrieval.

Category
Visit Server

README

chronicle

Personal event store and retrieval brain. Ingests Telegram, wakapi, dawarich (and whatever comes next) into one timeline, and serves it to agent-runner over MCP so the assistant can actually look things up.

Deploys into homelab-gitops as the chronicle stack.


Why this exists

The Telegram archive is 681,331 messages across 487 chats and 457 senders, 2018-12-30 to now. The existing telegram-sync stack embeds every message into Qdrant. Measured against the real corpus:

messages under 20 chars 65.0% (442,954)
messages under 60 chars 94.0%
messages over 200 chars 1.5%
messages with reply_to_id 7.9%

So ~442,000 of those vectors represent ок, ага, +1, да, 😂. They are not retrieval units — they crowd every neighbourhood they land in and bury the 1.5% of messages that carry a proposition.

Chronicle's core move is aggregation before indexing. Events are grouped into episodes by a per-thread fitted time gap: 681k units become ~50k. The index gets ~11× smaller and retrieval gets better at the same time.

Evidence, from SeCom (ICLR 2025), measuring retrieval quality by memory unit on conversational data — at ~30 tokens/turn, where ours are 5–10:

segment-level   71.57   <- what this builds
turn-level      65.58
session-level   63.16
summaries       53.87-56.25   <- worst. Do not build a summary pyramid.

Full reasoning, benchmarks and citations: docs/RESEARCH.md.

It is not a Telegram tool

Telegram is the richest channel, not the only one. 18 adapters, five storage shapes, one Event shape downstream.

tier sources why
1 · core telegram wakapi dawarich calendar the archive is worth having with only these
2 · behaviour firefly lastfm forgejo jira what you did, as opposed to what you said
3 · artifact immich paperless gmail notion karakeep github linkedin slack things you made, saved, or were sent
4 · ambient miniflux owntracks weak signal; on last, off first if precision drops

Storage shapes, because the homelab is not uniform and assuming it was is how the first version broke: PostgreSQL (telegram, immich, paperless, miniflux, forgejo, dawarich) · MariaDB (firefly) · SQLite (wakapi, karakeep) · flat JSONL files (owntracks) · HTTP/MCP (gmail, calendar, notion, slack, jira, linkedin, lastfm, github).

The policy layer is the point

"Use all possible channels" has a failure mode that looks exactly like the one Chronicle was built to fix. Indexing 681k sub-20-char messages was the wrong unit. Turning on twenty sources without a policy is the wrong source mix — miniflux alone can contribute 100k article rows you never opened, immich has 400 near-identical burst frames per moment, and the archive becomes millions of events that are ~90% chaff. Precision collapses the same way.

So every source declares a density, and density decides how hard the adapter aggregates before anything reaches the episode layer:

  • NARRATIVE — deliberate human text. Segmented into episodes.
  • DISCRETE — one row really is one thing that happened. Passed through.
  • TELEMETRY — meaningful only in aggregate. The adapter rolls it up: wakapi heartbeats → coding sessions, dawarich points → stays, lastfm scrobbles → listening sessions, immich photos → photo sessions.
  • AMBIENT — stored, but kept off the default retrieval surface.

chronicle/sources.py also lists the ~40 homelab stacks that are explicitly not sources, so the boundary is documented rather than rediscovered. All 57 stacks is not the goal; monitoring, qdrant and vaultwarden describe the machine, not the life.

make test enforces this: every adapter must have a policy, densities must match, and dawarich/owntracks are flagged as mutually exclusive (same GPS signal — enabling both double-counts every trip and the duplicate reads as corroboration).

Why more channels actually helps

"What was happening before I got depressed" — your stated goal. Telegram tells you what you said. Location tells you whether you stopped leaving the house, wakapi whether you stopped coding, firefly whether spending changed, lastfm what you played at 3am, immich whether you stopped taking photos. The behavioural signals are more honest than the conversational ones, because you don't curate them.

And cross-source corroboration turns a guess into evidence: a trip mentioned in Telegram, confirmed by dawarich coordinates, photographed in immich, and paid for in firefly is a fact you can trust.

Relationship to Hindsight

They are opposites, which is why they compose.

Hindsight Chronicle
origin you decided it mattered you never chose to save any of it
volume ~5,200 facts 681k events / ~50k episodes
precision high, curated low, exhaustive
evidence trail none nothing but evidence
shape a notebook you write in a recording that ran the whole time

Chronicle does not replace Hindsight and must not flood it. The personal bank is already at 2,726 facts and times out on sync_retain; piping ~50k episodes of extracted facts into it would 40× the bank and make recall useless.

Two narrow flows instead:

  • ground (Chronicle → answer), every recall. Hindsight facts are unsourced assertions. Chronicle attaches the conversations behind them — including ones that contradict. This replaces time-based staleness rules (ticket >14d, finance >30d) with a measurement.
  • Promotion (Chronicle → Hindsight), rare. v_promotable_facts requires support across ≥3 episodes and ≥2 threads at confidence ≥0.7. Target hundreds per year. Watch get_bank_stats after each run.

Architecture

sources ──► adapters ──► event (immutable, partitioned by year)
                            │
                            ▼
                     SEGMENTATION            per-thread fitted time gap
                     681k ──► ~50k           + caps + reply-edge anchors
                            │
                            ▼
                     episode                 raw_text (returned)
                            │                embed_text (indexed)
                            ▼
                     PostgreSQL              halfvec(1024) exact scan
                     one system              tsvector + trgm + B-tree
                            │
                            ▼
                     MCP ──► agent-runner ──► tg-assistant

Everything is in one PostgreSQL. At ~50k episodes for one user, ANN solves a problem that doesn't exist: exact cosine over ~123 MB is single-digit ms, and it keeps every date filter exact — sidestepping the HNSW percolation failure that bites hardest at the ~1%-cardinality date ranges you query most.

Quick start

git clone <this repo> chronicle && cd chronicle
make test                     # 57 unit tests, no DB or models needed
cp .env.example .env          # fill in, then `make encrypt STACK=chronicle` in homelab
make smoke                    # migrations + every SQL function, throwaway DB

make doctor                   # ← ALWAYS. validates sources before you ingest
make ingest                   # sources -> event -> episode -> embedding
make eval-init && make eval   # chronicle vs ripgrep, on your questions

make doctor is not optional

Two adapter assumptions were already wrong on first contact — wakapi is SQLite not Postgres, firefly is MariaDB with amounts on transactions — and each would have surfaced hours into a backfill, after the worker had written wrong rows. Doctor finds that class of problem in ~10 seconds, read-only.

It checks: driver reachability, timestamps that are actually datetimes (SQLite returns TEXT), ascending order, duplicate ids, unaggregated telemetry, empty narrative text, degenerate thread_key, and import-time-masquerading-as- event-time (the immich createdAt vs EXIF dateTimeOriginal trap). Run it again after upgrading any source stack — an upstream migration is exactly what breaks an adapter quietly.

Deploy: see docs/DEPLOY.md. Short version — clone into /opt/stacks/chronicle, resolve the chronicle-db digest into PINS.md, register 10.211.71.0/24 in NETWORKS.md, make validate in the homelab repo, then docker compose up -d.

Requirements

  • PostgreSQL 16 with pgvector ≥ 0.7halfvec does not exist in 0.6. The pinned pgvector/pgvector:pg16 image is fine; a distro postgresql-16-pgvector package may not be.
  • ~2.5 GB resident (api + db). The worker is restart: "no" and needs 8 GB while it runs — the box is 32 GB with 61.4 GB of mem_limit committed and has hit 96% swap, so it is scheduled, not resident.

Status

Working: segmentation, gap fitting, cross-script entity resolution, intent routing, RRF fusion, bi-temporal facts with deterministic conflict resolution, the full schema, the source-policy layer, 18 adapters across 5 storage shapes, MCP tool surface.

The SQL-backed adapters (telegram, wakapi, dawarich, immich, paperless, firefly, karakeep, miniflux, forgejo) carry real queries against real schemas but have only been run against fixtures — verify each one against your data before trusting its output. The API adapters take an injected fetch_page callable and need wiring to the corresponding MCP tool.

Stubbed: only worker.py enrich — the local-LLM pass for summaries, topics and facts. Everything else runs. Enrichment is deliberately last: retrieval works without it, so ship and measure before spending weeks of CPU there.

Tested: 57 unit tests, plus migrations and every SQL function exercised against real PostgreSQL 16 + pgvector 0.8.0 in CI.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured