All plugins

chronicle

Local-first long-term memory and memory-aware context compression for Hermes — SQLite, no account, API key or hosted service required. Captures every turn durably, extracts beliefs with provenance, answers from memory (raw-tier fallback and abstention), derives scoped conclusions, and compresses the context window losslessly with a restorable record. Registers the memory-provider AND context-engine slots from one shared core, and ships the Tapestry dashboard. Disclosure — off-host embedding endpoints are refused by default (RFC1918/ULA/CGNAT and https peers); sending memory excerpts off the host requires opting in via embeddings.allow_remote. Disclosure — the git mirror (engine/gitmirror.py) is on by default and writes plaintext JSONL copies of memory into a local git repo at ~/.hermes/commons/db/chronicle/git; it pushes only when git_remote is configured, and its 'encryption' option is a repeating-key XOR obfuscation, not cryptographic encryption.

indigokarasu11Memoryv5.8.35Updated 1 month ago

About

Local-first long-term memory and memory-aware context compression for Hermes — SQLite, no account, API key or hosted service required. Captures every turn durably, extracts beliefs with provenance, answers from memory (raw-tier fallback and abstention), derives scoped conclusions, and compresses the context window losslessly with a restorable record. Registers the memory-provider AND context-engine slots from one shared core, and ships the Tapestry dashboard. Disclosure — off-host embedding endpoints are refused by default (RFC1918/ULA/CGNAT and https peers); sending memory excerpts off the host requires opting in via embeddings.allow_remote. Disclosure — the git mirror (engine/gitmirror.py) is on by default and writes plaintext JSONL copies of memory into a local git repo at ~/.hermes/commons/db/chronicle/git; it pushes only when git_remote is configured, and its 'encryption' option is a repeating-key XOR obfuscation, not cryptographic encryption.

Chronicle

Local-first memory for Hermes Agent that survives restarts, long chats, and context compression.

CI
Release
Python 3.9+
MIT License
No required services

Version: 5.8.35.

Chronicle gives your Hermes agent durable long-term memory and safer working-memory
compression in one install. Names, preferences, decisions, and prior work stay on
your machine in SQLite; relevant memories come back when they are useful.

Why Chronicle

  • Local by default. No account, API key, or hosted memory service is required.
  • Survives context compression. Chronicle persists a span before the context
    engine evicts it, then rehydrates relevant memory later.
  • Inspectable and correctable. Memories are event-sourced with provenance,
    history, correction, forgetting, and access-control tools.
  • Honest when evidence is weak. Retrieval can fall back to captured turns and
    abstains instead of confidently inventing an answer.
  • Works without an embedding model. Full-text recall remains available; a local
    OpenAI-compatible embedding server can improve semantic recall when present.

Try it in two minutes

Install directly from GitHub:

hermes plugins install indigokarasu/chronicle-agent-context-and-memory

Then open hermes plugins and select Chronicle for the Memory Provider.
Selecting it for the Context Engine is optional, but enables memory-aware
compression. The equivalent manual configuration is:

memory:  { provider: chronicle }
context: { engine: chronicle }
plugins: { enabled: [chronicle] }

Start Hermes and try this small persistence test:

  1. Say: Remember that my project launch day is Friday.
  2. Start a new session and ask: When is my project launch day?
  3. Run /chronicle to see the active embedder and local store counts.

The database is created automatically at
~/.hermes/commons/db/chronicle/chronicle.db. Chronicle has no required Python
dependencies and no separate database or migration step.

If Chronicle earns a place in your agent setup, please
star the repository
so other Hermes users can find it.

What gets installed

Two Hermes plugin slots share one in-process core:

  • ChronicleMemoryProvider durably captures conversation history and retrieves
    relevant facts, episodes, and raw turns across sessions.
  • ChronicleContextEngine replaces the default compressor with memory-aware
    compaction that makes spans durable before eviction.

Either slot can run without the other. Under Hermes, capture and lifecycle hooks
run automatically. The agent receives memory tools including chronicle_remember,
chronicle_search, chronicle_answer, chronicle_explain, chronicle_correct,
and chronicle_forget.

Embeddings and graceful degradation

Embeddings default to auto: Chronicle checks local OpenAI-compatible servers
(LM Studio on :1234, Ollama on :11434, and llama.cpp on :8080) and uses the
model they serve. If none is reachable, vector work is queued while full-text
retrieval remains active. Set model: hashing or
CHRONICLE_EMBED_MODEL=hashing to deliberately choose the offline embedder.

Requires Python 3.9 or newer. The optional blake3 package enables BLAKE3
content addressing; otherwise Chronicle uses BLAKE2b-256.

Programmatic quickstart

This smoke test does not require Hermes:

from engine.core import ChronicleCore

core = ChronicleCore.get("/tmp/hermes_home")
core.initialize(session_id="s1", principal_id="assistant")
core.capture.observe(
    "My name is Pat Testley. I work at Acme Fake Co.",
    "Hi Pat!",
    session_id="s1",
)
core.process_pending()

print(core.retrieval.answer("where do I work?"))

Architecture

Both plugins share a process-singleton ChronicleCore that owns:

  • MemoryStore: SQLite WAL mode, single-writer with thread-local connections
  • CaptureEngine: observes turns, extracts salient spans, runs the reaper
  • Reducer: folds events into the belief store (facts, entities, episodes)
  • RetrievalEngine: dual-tier recall: FTS5 + structured lookup over beliefs, plus raw event access

When the context engine is selected, Hermes hands it compaction: it calls the engine's compress() and prune_tool_results_only() itself (the memory provider's on_pre_compress then returns nothing, so nothing is summarised twice). The memory provider hooks into on_session_end, on_turn_start, on_delegation, and on_memory_write.

How a compaction works

  • It follows your compression: settings. Hermes does not pass its policy
    to a plugin engine, so Chronicle reads it: it compacts at threshold of the
    window, down to twice threshold × target_ratio, keeps protect_first_n
    and protect_last_n messages, and also folds down to half of
    hygiene_hard_message_limit when the gateway compacts on message count.
    Explicit context_engine.high_watermark_percent / low_watermark_percent
    still win.
  • What it keeps. The protected head and tail, the user's newest request
    (even behind a long tool loop), pinned spans and the user's own
    "never/always/must" instructions, then the best-scoring older turns that fit.
    A tool call and its results are kept or folded together, never split. When
    even the protected spans do not fit, the newest turn wins, then the user's
    newest request, then the rest of the tail newest first; a turn's stale
    recall block (Hermes' api_content sidecar) goes before the user's words,
    and a span shortened to fit says how to restore it.
  • What it leaves in their place. One message where the folded turns were,
    in a conversation role (a mid-conversation system message would become
    the system prompt on Anthropic's API):
    [CONTEXT COMPACTION — REFERENCE ONLY] Chronicle folded …, listing the
    user's folded requests verbatim, one line per folded tool step (the command
    and its output or error), facts stated in them, memory recalled for a focus
    (or passed by Hermes as memory_context), and the ids of anything there was
    no room to show.
  • Nothing is lost. Every folded message is archived first;
    chronicle_expand(span_id) restores it byte for byte.
  • Cache-friendly. A pass extends the settled prefix byte for byte while it
    is small; once it is most of the budget, the next pass rebases into one
    consolidated handoff (one cache break, as Hermes' own compressor pays).
  • Inspectable. chronicle_context_status reports the policy in force, the
    passes so far and what the handoff carries; each pass logs one
    chronicle compaction: line.

What goes into a turn unasked

The memory provider's per-turn recall is memory about the user and about the
message just sent: an item must share a content word with it — as written or
inflected (book/booked, city/cities), not merely as a prefix — and a message
with more than three content words needs two of them in an item. A shorter
message's single shared word is often a coincidence ("system health check" and
a prescription's health_event), so such a match must also be near the
message in meaning: the item's stored vector against the message's (a short
item with none is embedded on the spot, a few per turn), within a second and a
half of quick requests to the embedder (retrieval.prefetch_min_similarity;
measured per model, 0.65 for nomic-embed-text); a match nothing can vouch for,
with the embedder busy or down, is left out. Two or three shared words get the
same check at their own floor (0.60), and there the words decide when there is
no vector to compare. The gate reads only the person's words, not the host's
framing around them. Only the user's own words in a past exchange are asked;
URLs, short numbers, host framing, tool output and the agent's own memory
writes never count. A scheduled job's turn gets none
(retrieval.prefetch_automation). Nor does it repeat the conversation in
progress: the live session's turns are already in the model's window, so only
what a compaction folded out of it can come back. A credential's value (a
password, API key or token the user handed the agent) is never injected, and
never stored in a belief: the fold masks it, and only the transcript keeps it.
Explicit search, chronicle_answer and the context engine's recall are not
gated.

Maintenance runs on those hooks, not on cron. There is no daemon, no timer
and no background thread: on a hook call the Scheduler (engine/scheduler.py)
compares each schedule's cron string against a persisted watermark
(maintenance_runs) and enqueues at most one due job onto the ordinary
curation queue, inside a wall-clock budget (maintenance.budget_ms, default
5ms; a call with nothing due costs ~0.08ms and executes no SQL at all). The work
itself is then done by the existing bounded per-turn drain, on the ordinary
append_event write path. What is scheduled: session reaping
(reaper.schedule), belief decay (forgetting.decay_schedule), the CSP
consistency sweep (health.consistency_sweep.schedule), the health/self-heal
run (health.schedule), the session-index backfill
(curation.sweep_schedule), and the exact-name identity sweep
(identity.schedule). An empty cron string disables a schedule; so does
an unparseable one, loudly. core.maintenance_status() reports last-run and
next-due per schedule, plus the tasks that are deliberately not scheduled
and why (e.g. derive, which mints new beliefs — a capture path, not
maintenance).

Identity is adjudicated, never inferred. The identity sweep exists to ask
questions, not answer them: an exact (normalized_name, owner, domain)
collision between two entities becomes a merge row on the
identity_candidates queue, and nothing is applied. It used to merge those
entities, which is why it had no schedule until it was fixed — two different
people who share a name are the ordinary case, and merged_into collapses
their provenance chains irreversibly. An explicit merged event still merges,
because that event records a decision someone made.

Memory about the user comes only from the user. Capture records who said
each part of a turn (engine/speaker.py): the person, a scheduled job or bot, the
assistant, a tool, or the host itself. The host context decides the user side:
Hermes' agent_context and platform, and the turn author. Inside a person's
message, Hermes' control frames (compaction handoffs, [System note: ...],
background-process results, the gateway origin header) and Chronicle's own
recalled <memory-context> block count as host text. The spans are stored on
the event, and extraction takes facts, preferences, standing instructions and
entity types only from the person's own words. Everything else stays captured
and searchable, and never becomes memory about the user.
scripts/retract_misattributed.py retracts what earlier builds extracted from
text the user never wrote. It runs as a dry run with a report unless given
--apply.

Installation

hermes plugins install indigokarasu/chronicle-agent-context-and-memory

Requires Hermes Agent with plugin support. Python 3.9+.

Configuration

Set in ~/.hermes/config.yaml:

memory:
  provider: chronicle    # the HOST's slot selector — Hermes reads this, not Chronicle
  db_path: ~/.hermes/commons/db/chronicle/chronicle.db
  git_repo: ~/.hermes/commons/db/chronicle/git
  embeddings:
    model: auto          # auto-detect the local server's embedding model; or pin an id, or 'hashing'
                         # for offline vectors. Unreachable = degraded (queued embeds), never hashed.
                         # $CHRONICLE_EMBED_MODEL overrides this.
    dimensions: 768
    allow_remote: false  # permission to send memory excerpts OFF this host. See "Where memory
                         # is allowed to go" below. Leave false unless you mean it.
  extraction:
    llm:
      allow_remote: false  # same, for extraction.backend: llm — that path POSTs the raw excerpt
  vector_index:
    backend: bruteforce  # default: paged scan over observed_vectors — always correct, cost
                         # grows with the corpus. 'sqlite-vec' opts into the ANN fast path,
                         # which needs `pip install sqlite-vec` AND a sqlite3 built with
                         # loadable-extension support (Apple's macOS system Python has
                         # neither). Missing either — or a vec0 left over at a different
                         # embedding width — falls back to the paged scan on its own. The
                         # top-k is the same either way; only the time to get it changes.
  maintenance:            # the in-process cadence; see "Maintenance runs on
    enabled: true         # those hooks, not on cron" above
    budget_ms: 5
  reaper:
    enabled: true        # false stops the idle-session reaper and its startup recovery
    schedule: "*/5 * * * *"   # 5-field cron, UTC; "" disables
    idle_threshold: "20m"
    reap_threshold: "45m"
  retrieval:
    fts_weight: 0.4
    vector_weight: 0.6
    rrf_k: 60
    rerank_blend: 0.5
    abstain_gate: focus
    include_drafts: true  # drafts are rendered [DRAFT], not hidden; false shows
                          # only confirmed truth. Read at the one _readable gate.
  context:
    breadth_floor: true          # reserve room for N sessions in the ranked fill
    breadth_floor_sessions: 5
  forgetting:
    decay_schedule: "0 3 * * *"
  identity:
    schedule: "0 5 * * *"   # exact-name sweep: proposes merge CANDIDATES only
  health:
    schedule: "0 4 * * *"
    consistency_sweep:
      enabled: true
      schedule: "0 * * * *"
    census_total_max_age_hours: 24
    self_heal:
      embedder_mismatch_max: 500  # vectors ONE health run may requeue to re-embed
  confidence:
    base:
      user_direct: 0.85  # per source_type; this table decides a stored fact's confidence
    trust_ceiling:
      4: 1.00            # C(level) cap applied after the base
  domains:
    user:
      contradiction_policy: flag_for_review   # flag_for_review | newer_wins | refetch
  curation:
    drain:
      per_turn: 16       # split across task classes by the three share_* weights
      share_write_path: 0.5
      share_embed: 0.3
      share_maintenance: 0.2
    lease_seconds: 900   # a 'running' job older than this is treated as abandoned
    max_attempts: 20
    retention:
      enabled: true      # prune terminal job rows; false accepts unbounded growth
      done_days: 7
      max_rows: 20000
  sweeps:
    row_budget: 5000     # shared per-run pace for every sweep without its own entry
    page_rows: 1000
    budgets:
      consistency: 2000
  learning:
    max_active_deltas: 8
    max_delta_magnitude: 0.15

Keys that engine/config.py declares but no shipped code reads are deliberately
absent from this sample — python3 scripts/audit_config.py lists them as
DORMANT with a reason for each. Setting one does nothing, which is why it is not
shown here as though it did.

Where memory is allowed to go

Chronicle is local-first, and that is now a control, not a claim. Two endpoints in
the engine can carry memory content off the machine: the embeddings server
(embeddings.base_url, or $CHRONICLE_EMBED_BASE_URL) and the optional LLM extractor
(extraction.llm.base_url, which POSTs the raw excerpt in its prompt). Before either
one is used, Chronicle resolves its host and refuses anything that is not on this
host or its private network
:

| Allowed | Refused |
|---------|---------|
| 127.0.0.0/8, ::1, 0.0.0.0/:: | any public IP literal |
| the literal name localhost (and *.localhost) — no DNS needed | a public hostname, e.g. a hosted inference API |
| RFC1918 10/8, 172.16/12, 192.168/16; ULA fc00::/7; CGNAT 100.64/10 | an https:// URL to a third-party endpoint |
| link-local 169.254.0.0/16, fe80::/10 | a hostname that will not resolve (fail closed) |
| a unix: / http+unix: socket URL | a hostname with any public answer in its DNS result |

A hostname is resolved once, when the client is constructed, and the verdict is
cached — the embed path itself never does DNS.

Refusal is not a crash. Embeddings fall to degraded mode (no vectors written, every
embed queued for retry, FTS retrieval unaffected) and the LLM extractor falls back to
the offline heuristic. One WARNING per process explains what was refused and why.
Nothing is sent.

To deliberately send memory excerpts to a remote endpoint, set
embeddings.allow_remote: true (and/or extraction.llm.allow_remote: true). Both
default to false. With them true, Chronicle logs one warning stating plainly that
memory content is leaving the host, and the local-first guarantee no longer applies to
that deployment.

Key options

Option | Default | Purpose
-------|---------|--------
db_path | ~/.hermes/commons/db/chronicle/chronicle.db | SQLite database location
embeddings.allow_remote | false | Permit embedding memory excerpts on an off-host endpoint
extraction.llm.allow_remote | false | Permit POSTing raw excerpts to an off-host LLM extractor
maintenance.enabled | true | Master switch for the in-process maintenance cadence
maintenance.budget_ms | 5 | Wall-clock ceiling for one hook's scheduling decision
reaper.enabled | true | Run the idle-session reaper and its startup recovery
reaper.schedule | */5 * * * * | Cron (UTC) for session reaping; "" disables
forgetting.decay_schedule | 0 3 * * * | Cron (UTC) for the fidelity-decay sweep
health.schedule | 0 4 * * * | Cron (UTC) for the health + self-heal run
health.consistency_sweep.schedule | 0 * * * * | Cron (UTC) for the CSP sweep
curation.sweep_schedule | 0 * * * * | Cron (UTC) for the session-index backfill
reaper.idle_threshold | 20m | Mark session idle after this duration
reaper.reap_threshold | 45m | Finalize idle sessions after this duration
retrieval.fts_weight | 0.4 | FTS5 score weight in hybrid retrieval
retrieval.vector_weight | 0.6 | Vector score weight in hybrid retrieval
confidence.base.<source_type> | see config.py | Starting confidence for a fact from that source
confidence.trust_ceiling.<level> | {0:.40 … 4:1.00} | Cap applied to confidence at each trust level
domains.<domain>.contradiction_policy | per domain | What happens when a new value contradicts a stored one
identity.schedule | 0 5 * * * | Cron (UTC) for the exact-name identity sweep; "" disables
learning.max_active_deltas | 8 | Max concurrent self-improvement deltas
context_engine.high_watermark_percent | host threshold, else 0.75 | Compact when the prompt passes this share of the window
context_engine.low_watermark_percent | 2 × threshold × target_ratio, else 0.55 | What a compaction folds down to
retrieval.prefetch_relevance_gate | true | Per-turn recall only for items that share content words with the message
retrieval.prefetch_automation | false | Per-turn recall on scheduled-job (cron) turns too
embeddings.exclude_session_prefixes | [] | Session prefixes never embedded (e.g. cron_); still full-text searchable

New in 5.7.0

engine/config.py's DEFAULTS gained 35 leaf keys this release and lost 15
(see "Configuration honesty" below for the ones that went). Every key here is
read by shipped code — python3 scripts/audit_config.py prints the file and
line that reads each one, and tests/test_config_honesty.py fails the build if
that ever stops being true.

The curation queue (A7). Before this release the queue was strict FIFO with
no lease and no retention, so one health run's backlog could park the write path
for weeks and curation_jobs grew for the life of the store.

Option | Default | Purpose
-------|---------|--------
curation.drain.per_turn | 16 | Jobs one hook call may drain in total
curation.drain.share_write_path | 0.5 | Relative weight for extract/digest/canonicalize/session_summarize/…
curation.drain.share_embed | 0.3 | Relative weight for deferred vector writes
curation.drain.share_maintenance | 0.2 | Relative weight for sweeps and periodic repair
curation.lease_seconds | 900 | How long a claimed job may stay running before health treats it as abandoned and re-arms it. Set it above your slowest real job — a lease shorter than the work duplicates it
curation.max_attempts | 20 | Claims one job may burn before it is failed with a stated reason. Resets to 0 when the same unit of work is re-enqueued
curation.reclaim_batch | 200 | Rows one lease sweep may reclaim
curation.retention.enabled | true | Prune terminal (done/failed) job rows. false accepts unbounded growth
curation.retention.done_days | 7 | Age bound — catches a quiet store
curation.retention.max_rows | 20000 | Row-count bound — catches a busy store that outruns the age bound
curation.retention.batch | 5000 | Rows one prune pass may delete

The three share_* values are relative weights, not percentages: they are
normalised, so {2, 1, 1} and {0.5, 0.25, 0.25} are the same config. A
positive share too small to round up to a whole job is lent exactly one job
rather than rounded away, which is the anti-starvation guarantee. Pending and
running rows are never pruned, and neither is a done row that a pending job
still depends on.

Sweep pacing (A9). Every periodic sweep used to carry a literal limit=5000
with no cursor, so past 5000 matching rows it re-read the same prefix forever
and reported success. The bound stays; it is now a stated pace with a persisted
cursor behind it and a processed/remaining/bounded/wrapped report in
health.run() in front of it.

Option | Default | Purpose
-------|---------|--------
sweeps.row_budget | 5000 | Shared per-run row budget. Every sweep without its own entry below takes this number, so raising it speeds up decay, ghost_facts and identity together
sweeps.page_rows | 1000 | Memory page size for full-table folds (a paging bound, not a work bound — these never truncate)
sweeps.budgets.ghost_facts | 200 | Rows the ghost-fact report scans, which is now also what it displays
sweeps.budgets.consistency | 2000 | Groups per CSP consistency sweep
sweeps.budgets.canonicalize | 2000 | Groups per canonicalisation sweep
sweeps.budgets.derive_subjects | 500 | Subjects per derivation sweep
sweeps.budgets.backfill | 200 | Rows per session-index backfill
sweeps.budgets.reextract | 200 | Events per re-extraction pass

Only the sweeps whose pace deliberately DIFFERS from row_budget are listed, so
the table stays a list of decisions rather than a restatement of the default.

Vector self-heal (A0/A7).

Option | Default | Purpose
-------|---------|--------
health.self_heal.embedder_mismatch_max | 500 | Vectors ONE health run may requeue for re-embedding. Bounds the expensive action only: re-TAGGING a same-model-different-name row is a metadata UPDATE and is never bounded by it. At the default and the default daily schedule, a corpus of 170k wrongly-embedded vectors converges in ~340 days — a wholesale model change is a MIGRATION, not a heal (see reports/MIGRATION-RUNBOOK.md, whose Step 0 tells you to set this to 1 for the write-back window)
health.census_total_max_age_hours | 24 | How long the heal may reuse a CACHED COUNT(*) of the vector tables as the denominator in "N of M vectors are off-model". The numerator is always live and exact; the total is force-recounted the moment a run finds anything mismatched. 0 counts every run (~300 ms cold on a 100k-row store)

Context packing and retrieval (A18, A16).

Option | Default | Purpose
-------|---------|--------
context.breadth_floor | true | Reserve room for N distinct sessions in the ranked fill, instead of letting the top session take 99–100% of it. false restores the pre-5.7.0 fill exactly
context.breadth_floor_sessions | 5 | N. Swept over the whole ctx_eval corpus at all three budget tiers; 5 was the only value that improved BOTH tight tiers. Clamped to the number of groups that actually have something to emit, so 1 is arithmetically the old behaviour
context.breadth_floor_routes | ["aggregation", "temporal"] | Which E9 routes get it. factual is excluded by construction — a factual question is answered by one session and precision packing depends on concentrating there — and preference already weighs every group against every other
retrieval.include_drafts | true | Include [DRAFT]-marked beliefs in reads. Drafts are the losing side of a flagged contradiction, a high-risk norm awaiting review, or an inference derivation refuses to assert — three producers write one deliberately RATHER than dropping the value, so excluding by default would silently delete them from the reader's view. false is for a deployment that wants only confirmed truth in front of its reader; it is read at the single _readable choke point, so it covers every channel and packing path at once
retrieval.confident_answer_from_drafts | false | May a [DRAFT] belief back answer()'s CONFIDENT path. A DIFFERENT question from retrieval.include_drafts above, and deliberately a separate key: one key for both would mean an operator setting it false to keep drafts out of confident answers instead deleting drafts from every read path. false (the default) still renders drafts, marked, in search()/get_context() — it only stops them being returned with abstain: false and a confidence score, which the [DRAFT] marker in the text does not undo
git.encryption_key | null | Client-side payload key for the git mirror. Declared because engine/gitmirror.py reads it, so the config surface promises what the code consults; CHRONICLE_GIT_ENCRYPTION_KEY takes precedence and remains where an actual secret belongs. null means no client-side payload encryption

Automatic reference capture.

Option | Default | Purpose
-------|---------|--------
capture.tool_reference.allowlist | [web_fetch, webfetch, fetch, web_search, websearch, file_read, read_file, readfile, read] | Tool names whose successful results are cached as a reference belief. Chronicle's own tools are never re-captured
capture.tool_reference.ttl_days | 30 | Freshness horizon recorded on a captured reference

Near-duplicate detection.

Option | Default | Purpose
-------|---------|--------
curation.novelty_top_k | 25 | Nearest same-kind neighbours the novelty scan examines before storing a belief

Configuration honesty

Chronicle's premise is that config must never promise what code does not
deliver
, so the key list is audited mechanically rather than by eye:

python3 scripts/audit_config.py          # WIRED / DORMANT / UNREAD per key
python3 scripts/audit_config.py --unread-only

Every leaf key in engine/config.py's DEFAULTS is in exactly one of three
states, and there is no fourth:

  • WIRED — shipped code reads it. The audit finds the read through the AST,
    including the accessor shapes this codebase actually uses (_clamp_cfg(cfg, "path", …), a sub-dict fetched once and indexed later, and dynamic key
    templates like cfg.get(f"sweeps.budgets.{name}")).
  • DORMANT — nothing reads it, and engine.config.DECLARED_DORMANT says so
    with a reason. These are admitted gaps, listed in the audit output. Where
    the value itself makes a false promise at runtime (a protection that is on but
    does not run, or an off-switch that does not switch anything off) booting logs
    a warning naming the flag once per process.
  • UNREAD — nothing reads it and nothing declares it. This is a defect, and
    tests/test_config_honesty.py fails the build on it. A knob that nothing
    reads cannot land quietly, and a declaration that goes stale fails too.

Security: Chronicle does not encrypt anything at rest

Stated plainly because a previous release shipped security.encrypt_at_rest: true as a default while nothing anywhere encrypted anything. The SQLite
database, its WAL, the git mirror and every vector blob are written in the
clear. Both security.encrypt_at_rest and principals.encryption. restricted_partition_keys have been removed, and setting either one now
makes Config() raise rather than ignore it: silently ignoring a security key
leaves an operator believing in protection they do not have.

Use full-disk or filesystem-level encryption on the host. Read isolation
between principals is enforced by access.can_read — an ACL, not cryptography.

Database

Stored at ~/.hermes/commons/db/chronicle/chronicle.db. The database is self-contained: events, beliefs, principals, and FTS indices in a single file. WAL mode means readers don't block writers.

Back up with SQLite, not with cp:

sqlite3 ~/.hermes/commons/db/chronicle/chronicle.db ".backup /path/to/copy.db"

.backup is safe against a running writer and folds in the WAL. cp of a live
WAL-mode database gives a torn copy that silently loses committed frames, and
VACUUM INTO may renumber rowids, which breaks anything keyed on them. If you
would rather copy files, stop Hermes first and take .db, .db-wal and
.db-shm together. MemoryStore.close() / ChronicleCore.close() checkpoint
the WAL and release the sidecars if you are shutting down from code.

Tools

The memory provider exposes these tools to the agent:

  • chronicle_remember: Store a fact or observation explicitly
  • chronicle_search: Search the belief store and raw events
  • chronicle_answer: Ask a question against stored memory
  • chronicle_forget: Remove a memory entry
  • chronicle_list_directives: List active memory directives

The context engine adds:

  • chronicle_pin_context: Pin a context span so compression never evicts it
  • chronicle_focus: Set the focus topic for memory-aware compression

Dashboard

Chronicle ships a Hermes dashboard tab (dashboard/) with two views.

Overview shows store counts, embedding coverage, recent activity, and a
button that queues extraction for turns that have none.

Tapestry is how the memory itself is browsed, by what it is about rather
than by how it arrived. The rail counts what Chronicle holds — people, places,
things, events, ideas, and whatever it cannot classify — and picking one lists
them, biggest first. An entity's page shows what memory says about it now, what
it used to say, the events it appears in, and every captured turn that names it,
each labelled with who was speaking in that turn (you, a scheduled job, the
assistant, a tool, the host), so nothing reads as yours that was not.

Above it, the weave puts the dated events on one time axis with a thread per
entity they involve: a knot on your thread and on the person's, joined where an
event names them both. Threads are ordered by how much of your memory runs
through them, and the quieter ones are counted rather than drawn a pixel high.

Classification comes from the sources, never from a guess: an entity's kind from
its own recorded type or its predicates, a contact's person-or-company from the
people store that owns that record (an unreviewed row stays "unreviewed"), and
an event's date from the date the importer wrote into its title.

Provenance is a drill-down, not the way in. From any fact or mention,
"provenance" opens the log: every event drawn as a point on the row of whatever
wrote it — a cron job, a chat session, the curator — with the chart above
counting events over time, inspectors for an event, a run or a belief, and
lenses for open contradictions, replaced facts and duplicate notes.

The Tapestry reads the store with a mode=ro SQLite connection per request, so it
cannot write memory and never holds a read transaction open. Drawing is done on
the GPU with deck.gl, which keeps the whole log (hundreds of thousands of events)
interactive. The deck.gl bundle (dist/tapestry.js) is loaded only when the Tapestry
tab is opened.

The UI is built from dashboard/web/src, and the built files in
dashboard/dist are committed, because the dashboard loads them with no build
step on the host:

cd dashboard/web && npm install && npm run build

dashboard/web/harness/server.py --db <chronicle.db> serves the UI and the
plugin API against a local store behind a stand-in for the plugin SDK, for
working on the UI without the OAuth-gated dashboard. It never writes.

Development

git clone https://github.com/indigokarasu/chronicle-agent-context-and-memory.git
cd chronicle-agent-context-and-memory
pip install -e ".[dev]"
/usr/bin/python3 -m pytest tests/ -q --ignore=tests/exercise/test_manual.py
ruff check .

Tests run against temporary SQLite databases in a sandbox the harness removes.
The suite is hermetic — no network, no $HOME, no ambient environment, no
collection-order dependence — and it has four modes, all of which must be
green. tests/README.md is the reference; tests/exercise/ holds acceptance
scripts rather than unit tests, and test_manual.py is excluded from the gate
by design.

Project structure

chronicle/             # installs to ~/.hermes/plugins/chronicle/
  __init__.py          # plugin entry: register(ctx) registers both slots + __version__
  provider.py          # ChronicleMemoryProvider (memory-provider slot)
  context.py           # ChronicleContextEngine (context-engine slot, I17)
  _base.py             # minimal ABCs so the adapters import offline (dev/tests)
  plugin.yaml          # Hermes plugin manifest (name/version/description/hooks)
  pyproject.toml       # dev/test metadata
  engine/              # shared core (relative-imported by both slots)
    core.py            # ChronicleCore singleton + Scope, wires every subsystem (§11)
    config.py          # Configuration reference + defaults (§27)
    serialize.py       # CJSON + content addressing, BLAKE3/BLAKE2b (§5)
    store.py           # MemoryStore: atomic append = reduce+git+curation (§6/§24, I7)
    reducer.py         # Pure projection: events → belief store (§7)
    trust.py           # Trust ceilings + confidence + calibration (§10)
    criticality.py     # Criticality rules floor (§20.1)
    access.py          # ACL logic: default-allow within a user (§15)
    capture.py         # CaptureEngine + Reaper (§12)
    extraction.py      # Pluggable Extractor + heuristic default (§16)
    derivation.py      # Guarded compositional inference + TMS (§9, I24)
    curation.py        # Curation worker + DAG (§17)
    scheduler.py       # Hook-driven maintenance cadence, no daemon (§17.4)
    sweeps.py          # Resumable, observably-bounded sweep pacing (§A9)
    retrieval.py       # Dual-tier + read-and-answer + promote-on-read (§18)
    federation.py      # Capability registry: reference, don't own (§14, I20)
    forgetting.py      # Asymmetric decay + fidelity ladder + unlearning (§20)
    health.py          # Auditor + consistency sweep + self-heal (§21)
    learning.py        # Bounded learning loop, champion/challenger (§22, I19)
    reasoning.py       # Procedures, reflections, plan_context, epistemic (§19, §23)
    gitmirror.py       # Git mirror flusher + disk recovery (§26)
    embeddings.py      # Pluggable embedder + offline default (§24.4)
    tools.py           # Full agent tool surface (§23)
    errors.py          # Error codes (§32)
  dashboard/           # Hermes dashboard tab
    manifest.json      # tab registration
    plugin_api.py      # FastAPI routes: status, recent activity, extraction queue
    tapestry_api.py       # read-only Tapestry routes (event stream, inspectors, lenses)
    dist/              # built UI the dashboard loads (index.js, tapestry.js); committed
    web/               # UI source + build script + local harness
  tests/
    test_build.py      # Unit + property tests P1–P21 + worked examples B.1–B.6

Implementation status

All six build phases (§31) are implemented and exercised by the test suite:
data plane + capture + principals + federation (Phase 1); recall-oriented
extraction + dual-tier retrieval + read-and-answer (Phase 2); TMS + guarded
derivation + provenance/trust/ACL (Phase 3); curation + representation +
health/self-heal (Phase 4); context engine + bounded learning + reasoning +
epistemic + procedures (Phase 5); git-mirror recovery + the property suite
(Phase 6). Extraction and read-and-answer use a deterministic offline heuristic
behind a pluggable interface — a real deployment swaps in a local model without
touching the pipeline. Deferred per spec: the distributed CRDT tier (§24.5),
L3 parametric adapters (§20.4), and the TLA⁺ models (§29).

How Chronicle compares

Mem0 and Hindsight are native Hermes memory providers. The meaningful difference
is which Hermes extension points a system owns and what must run behind it.

Hermes integration

| System | Memory provider | Context engine | Auto-capture | Auto-recall |
|---|:---:|:---:|:---:|:---:|
| Chronicle | ✓ | ✓ | Every turn | ✓ |
| Hermes files | Built in | — | Agent-directed | Always injected |
| Mem0 | ✓ | — | Every turn | ✓ |
| Hindsight | ✓ | — | Default on | Default on |
| Graphiti | — | — | MCP/custom | MCP/custom |

Auto-capture means Hermes stores completed turns without the model choosing a
tool. Auto-recall means memory is added to turn context automatically, not
merely exposed through a search tool.

Deployment and controls

| System | Default Hermes setup | No model/service | Hermes-facing controls |
|---|---|:---:|---|
| Chronicle | Local SQLite | ✓ | Remember, search, correct, forget |
| Hermes files | Local Markdown | ✓ | Add, replace, remove |
| Mem0 | Mem0 Platform | — | Search, add, update, delete |
| Hindsight | Hindsight Cloud | — | Retain, recall, reflect |
| Graphiti | No provider | — | MCP episode/graph tools |

Among these systems, Chronicle alone fills both the Hermes memory-provider and
context-engine slots. Its context engine rescues valuable spans into durable
memory before compaction, then rebuilds the working context. Mem0 and Hindsight
also capture every turn, but do not control Hermes compression. Hermes files are
always injected and have built-in editing tools, but are agent-curated rather
than automatically extracted. Graphiti currently requires MCP or custom wiring.

Chronicle also runs without a model, API key, or separate service. If embeddings
are unavailable, it keeps recalling through local full-text, structured, and
raw-turn fallbacks. Mem0 additionally offers self-hosted and in-process OSS
modes, which require its model, embedder, and vector-store stack. Hindsight also
offers embedded-local and external-server modes; embedded-local includes its
database but still requires a configured LLM. Graphiti supports local components
but still needs model, embedding, and graph backends.

This compares documented integration and deployment surfaces, not retrieval
quality: Hermes provider API,
Mem0 for Hermes,
Hindsight provider,
and Graphiti MCP.

Contributing

Bug reports, use cases, and focused pull requests are welcome. See
CONTRIBUTING.md for the development workflow and the best
places to start.

License

MIT. See LICENSE.

README from the reviewed commit 92e4351; it updates when the author re-pins.

More by indigokarasu

4 plugins by indigokarasu in the catalog.

indigokarasucustodianAutonomous operations monitor for a Hermes install: scans gateway logs, cron jobs, skill journals and agent data directories, classifies findings against a 27-fingerprint registry, auto-repairs Tier 1 problems, escalates the rest, and keeps an append-only issue ledger. Ships /custodian (status, scan, issues, repair, verify, schedule, confidence, init, escalation-runner), four scan/health tools and a grid dashboard. Disclosure — reads gateway logs, cron job state and per-skill journal files under HERMES_HOME to build its findings, and writes repair actions and an issues ledger back there; it performs no network I/O and holds no credentials. Disclosure — /custodian init registers 3 recurring agent cron jobs (deep scan 4×/day, cron-health 4×/day, escalation runner every 30 min on weekdays).Communityautomation9indigokarasupocket-watchDashboard-only cron companion: a Pocket Watch pane showing cron jobs by schedule (daily/weekly/monthly), a health view of run outcomes and consecutive failures, a timeline, and maintenance actions — a read-oriented window onto the scheduler rather than another agent-facing toolset. Disclosure — the pane's API reads the cron jobs file under HERMES_HOME to render schedules and run history; it registers no tools, hooks or middleware, performs no network I/O and holds no credentials.Communityautomation1indigokarasuidentityView and edit the identity files (SOUL.md, MEMORY.md, USER.md, AGENT.md/HERMES.md) from chat, switch the active profile, and see per-line usage with colour-coded staleness timestamps. Adds six tools, /identity, /identity-switch and /identity-dashboard commands, and a utilization dashboard pane. Disclosure — reads and writes the identity files and a line-utilization record inside the selected profile directory under HERMES_HOME, and can change which profile is active; it performs no network I/O and holds no credentials. Disclosure — the model-callable identity_edit tool rewrites SOUL.md/MEMORY.md/USER.md/AGENT.md and identity_switch_profile writes HERMES_HOME/active_profile.Communitytools