skip to content
$worker

memory

v0.2.4

Durable cross-session agent memory — named banks of always-injected markdown rules and auto-extracted memories, hybrid BM25 + entity recall, pinning, and supersede-never-delete history. Plain files on disk; every operation is a traced function.

Experimental

Published as experimental by its release pipeline. It installs and resolves like any other worker, but its interface may change without notice.

iiiverified
148 installs14 in 7d2 today
install
$iii worker add memory
binarylicense: Apache-2.0memoryragbm25recallagentcontext
  • macOS: arm64 · x64
  • Linux: arm64 · armv7 · x64
  • Windows: arm64 · x64 · x86

exact versions are immutable; binary and bundle artifacts are digest-pinned.

README.md

memory

Durable cross-session agent memory. Named banks hold two kinds of content: rules (markdown documents injected whole into every turn's system prompt) and memories (auto-extracted records recalled on demand). Everything is a plain file you can open, a function you can call, and an event you can watch: memory that acts visibly, not magically.

Install

iii worker add memory

The default bank main materializes on first use. No configuration required; without llm-router extraction degrades to explicit memory::save calls, and without the harness the worker still serves its full RPC surface.

Quickstart

Save something, then see exactly what a turn on that topic would be given:

iii trigger memory::save text="User publishes blog posts on Tuesday mornings" pinned=true tags=blog
iii trigger memory::recall query="when do I publish articles"

Give the bank a standing instruction — injected into the system prompt of every session using it:

iii trigger memory::rule::set name=style content="# Style\nFormal register. No em-dashes."

With the harness running there is nothing else to wire: chat normally and memory captures in the background. Say "never start replies with the word Certainly" in a conversation and it lands in the bank's auto-managed learned rule; state something durable and it appears as a memory after the turn. If memory seems absent, iii trigger memory::doctor runs a real save→recall roundtrip and names what is degraded.

Configuration

All fields hot-reload through the configuration worker (rendered as a form in the console); data_dir reopens the store on the fly. Fields: data_dir, default_bank, inject_rules, inject_memories, recall_limit, recall_budget_tokens, extraction_enabled, extraction_model, extraction_window, extraction_timeout_ms, max_memories_per_turn, rule_learning_enabled, max_rule_chars, decay_half_life_days, embeddings_enabled, embedding_model.

Why this shape

  • Files are the source of truth. rules/*.md and memories.jsonl under the data dir. Edit them in any editor; memory::reload (or a restart) picks the edits up. The search index is a RAM-only cache rebuilt from the files at boot, so store and index can never diverge across restarts.
  • Crash-safe by construction. Every mutation appends one fsynced JSONL line before touching RAM. There is no shutdown flush to get wrong. An unwritable data dir is boot-fatal: this worker never silently runs in RAM.
  • Supersede, never delete. Updates append revisions; deletes append tombstones; trashed banks move to .trash/. Any state is recoverable.
  • Pinning. A pinned memory ranks higher in recall and is untouchable by every automatic path.
  • Tags. Topical labels WITHIN a bank ("iii", "billing") for filtering — organization, not ranking. Set them on save/update, extraction suggests them automatically, and memory::list/memory::recall take a tag filter.
  • One LLM call per turn, zero at query time. Extraction runs in the background after a turn completes (ADD-only, content-fingerprinted so redelivery reinforces instead of duplicating). Recall is BM25 + entity match + corroboration + recency, plus a semantic signal when router::embed is available: sub-millisecond at this scale.
  • Rules learn from corrections. Extraction classifies standing instructions (style directives, workflow corrections) separately from memories and appends them to the bank's auto-managed learned rule — correcting the agent in chat updates the system prompt for every later turn. Hand-authored rules are never touched; dedup by content fingerprint; rule_learning_enabled turns it off.
  • Honest health. memory::doctor runs a real save→recall→trash roundtrip and reports sibling reachability. memory::recall names the retrieval mode it ran. Degradation is explicit, never silent.

How it hooks into the harness

Seam What happens
harness::hook::pre-generate (fail-open, priority 100) Injects the session bank's rules into the system prompt (stable per session: keeps the provider prompt cache warm) and up to recall_limit recalled memories as one appended message
harness::turn-completed Spawns one background router::complete extraction pass over the last extraction_window user/assistant messages

Bank selection order: turn metadata memory_bank → session metadata memory_bank (session::set-meta) → configured default_bank. A session-lookup failure injects nothing rather than falling back across banks.

All seam bindings are one-shot at startup and rely on the engine's recoverable triggers (iii #1962, engine ≥ 0.21.8): bound before the owning sibling (harness, session-manager, queue) is up, they park as pending intents and activate when the trigger type registers. On older engines the binds are silently dropped.

Functions

Function Purpose
memory::bank::create / list / delete Banks as first-class named scopes ("blog" vs "coding" vs "personal"); delete moves to .trash/
memory::save Explicit save ("remember this"): fingerprinted, reinforces on repeat
memory::get / list / update / delete / pin Memory CRUD; delete tombstones; update bumps a revision in the log
memory::recall The exact scorer the hook uses: preview what a turn would be given; tag narrows to one label
memory::tags Distinct tags across a bank's live memories with counts (the filter source)
memory::preview Full injection dry-run for a hypothetical chat message: rules section (budgets applied), memories post ambient floor and token budget, appended message verbatim — the hook's exact code path
memory::rule::list / set The always-injected markdown rules; empty content removes
memory::supersede Retire one memory in favor of another (tombstone + pointer) — the consolidation seam
memory::doctor End-to-end self-test (roundtrip + sibling reachability)
memory::reload Reload every bank from disk after hand-editing files

Custom trigger types

Bind live views here instead of polling; both are filterable by bank in the binding config, delivery is fire-and-forget, at-least-once, unordered.

  • memory::item-changed — a memory was created / updated / superseded / deleted. Payload: {event_type, bank, memory}.
  • memory::bank-changed — a bank was created / trashed, or its rules changed. Payload: {event_type, bank}.

Storage layout

~/.iii/data/memory/
  main/
    bank.yaml          # description
    rules/style.md     # always-injected markdown
    memories.jsonl     # append-only memory log (full records, last-wins by id)
    vectors.jsonl      # derived embedding sidecar (rebuildable)
  .trash/              # trashed banks, timestamped

Permissions

Suggested defaults ship in iii-permissions.yaml: agents get memory::recall, memory::save, memory::get, memory::list, memory::update, memory::delete, memory::pin; the hook/pipeline internals are denied, and bank/rule writes plus memory::reload are human-owned surfaces (console, REST, CLI) by default.

The memory family

Like llm-router and its provider workers, memory is a family of narrow siblings rather than one grown monolith — this worker owns the store, per-turn injection, capture, and recall, and stays deliberately small:

Worker Role Status
memory (this) Banks, rules, memories: storage, harness injection, background capture, hybrid recall, live trigger types shipped
memory-consolidate Background hygiene on a schedule: deterministic dedup of near-duplicate memories, catch-up-on-boot scheduling. Supersede-only through memory::supersede, never touches pinned records; every change lands as a memory::item-changed event shipped

The split is the point: consolidation can be installed, stopped, or removed without touching stored memory, and this worker never grows a scheduler.

Boundaries

Context-manager compresses one history for one turn; this worker is the durable cross-session sibling its spec reserves (see tech-specs/2026-06-agentic/memory.md). Recall fuses BM25 with a semantic signal when router::embed has an embed-capable provider; memory::recall.retrieval names the mode either way. Consolidation belongs to the memory-consolidate sibling, not here.