LoreKitLoreKit docs

Retention policies

Save scoped rules that automatically archive lessons you no longer need — reviewed manually or swept nightly — without ever hard-deleting anything. Plus the read-only Duplicate Clusters sidebar for spotting the same lesson written twice.

Retention policies automate the manual archiving you would otherwise do by hand: a saved rule ("lessons in repo::acme/app unseen for 90 days") that you either run yourself whenever you like, or that runs automatically every night. Either way it only ever archives — the same reversible soft-delete memory.archive and lorekit archive already use. A policy can never hard-delete; that stays a separate, manual, confirm-or---yes step (memory.purge).

Preview before you save anything

Every rule is the AND of four age/activity conditions, all optional, plus the same eight dimension filters the Lore Explorer's filter bar offers:

ConditionMeaning
min_age_daysOnly lessons at least this many days old
unseen_daysOnly lessons no agent has individually opened (over MCP or the lorekit CLI) in at least this many days. A lesson never opened this way counts from its creation date, so it matches only once it is itself that old. A bulk memory.list/memory.search appearance or a human viewing the dashboard does NOT count as "opened"; see the note below
max_seen_countOnly lessons that have recurred at most this many times. Counts writes, not reads
max_read_countOnly lessons read back at most this many times. Counts every read — a bulk memory.list/memory.search result page and a human opening the dashboard's detail sheet both count, unlike unseen_days. Use it to find lore nothing has ever actually used
tags / tags_modeLabels a lesson must carry — any (default), all, or none
source_agent / source_agent_modeThe writing agent — in (default) or nin
trigger / trigger_modeThe write trigger — in (default) or nin
kind / kind_modelesson / bus / signalin (default) or nin
host / host_modeThe owning skill or agent — in (default) or nin
origin_repo / origin_repo_modeowner/repoin (default) or nin
origin_branch / origin_branch_modeThe branch name — in (default) or nin
origin_pr / origin_pr_modePull-request numbers, as digit strings — in (default) or nin

max_read_count counts a DIFFERENT direction from max_seen_count: the first counts how many times a lesson was read back, the second how many times it was written. A lesson written once and read fifty times matches max_seen_count: 1 but not max_read_count: 1. Neither is called "seen" in the dashboard for that reason — the Explorer labels them "Recurrence" and "Consumption", the same two words the lesson detail sheet uses.

One caveat on max_read_count: reads have only been counted since the read_count column shipped, so a long-lived lesson can show a low count it never earned. Pair it with min_age_days only once your store has been counting for a while.

unseen_days is deliberately the NARROWEST of the three "has this been touched" signals a lesson carries, and the Lore Explorer's detail sheet shows all three side by side so they're never confused for one another: "Recurrence" (seen_count — how many times the lesson has been written, unrelated to reading), "Consumption" (read_count / last_read_at — how many times it has been read back, by ANY surface: a bulk memory.list/.search result page, GET /memories/:id, or a human opening the dashboard's own detail sheet all bump it), and "Last agent open" (last_opened_at). unseen_days reads only the last of those — it only moves when an agent deliberately retrieves this ONE lesson over MCP (memory.read) or the CLI (lorekit read/show), never from riding along in a list page or from a person browsing the dashboard. That is what keeps "unseen for 90 days" honest: reloading the Explorer, or a memory.list call that happens to include the lesson, does not reset the clock.

A lesson no agent has ever opened has no last_opened_at to measure from, so the clock starts at its creation date. A lesson written a week ago therefore does not match unseen_days: 90 — it has not yet had 90 days in which to go unread. This matters most right after last_opened_at was introduced, when every pre-existing lesson has no recorded open: without the fallback, unseen_days would match the entire store regardless of threshold.

Preview what a rule would catch before saving it as a policy, with the CLI:

lorekit groom --scope repo::acme/app --unseen-days 90

or the REST API:

curl -X POST https://pqokxlhvnosogizsjztg.supabase.co/functions/v1/memories/groom/preview \
  -H "Authorization: Bearer $LOREKIT_TOKEN" -H "Content-Type: application/json" \
  -d '{"scope": "repo::acme/app", "unseen_days": 90}'

Both answer { "count": <n>, "keys": [{ "scope", "key" }] } — the exact set a run would archive, because a saved run and a preview resolve through the IDENTICAL candidate query. Nothing is changed by a preview.

The Lore Explorer previews the same conditions a different way: its "Age & activity" filter (min_age_days / unseen_days / max_seen_count / max_read_count) combines with its own filter bar (the same eight dimension filters above) to narrow the list to the actual lesson ROWS a policy with those conditions would catch, not just a count — so you can scroll through them before saving anything. Once at least one condition or filter is set, a Create retention policy action hands the current scope, conditions AND filter bar to Settings → Retention Policies, which opens the New policy dialog pre-filled with all of it, using the identical filter control to edit it further.

Save it as a policy

lorekit policy create --scope repo::acme/app --name "stale repo lore" \
  --unseen-days 90 --mode review

Each condition has its own flag — --min-age-days, --unseen-days, --max-seen-count, --max-read-count — and lorekit policy update pairs each with a --clear-* form for removing one from a saved policy:

# never actually read back, and old
lorekit policy create --scope repo::acme/app --name "unused lore" \
  --min-age-days 90 --max-read-count 0 --mode review

lorekit policy update <id> --clear-max-read-count

mode: review is the default — the policy is saved and shows up in the dashboard's Retention Policies settings for you to run by hand whenever you like (lorekit groom --policy-id <id> --run). mode: auto gets swept nightly, but only once you also pass --enabled — a saved auto policy always starts disabled, so creating one never archives anything unattended by itself:

lorekit policy create --scope repo::acme/app --name "stale repo lore" \
  --unseen-days 90 --mode auto --enabled

lorekit policy list / update <id> / delete <id> round out the set. Deleting a policy deletes the RULE only — it never touches the lessons it matched.

The eight dimension filters (tags, source_agent, trigger, kind, host, origin_repo, origin_branch, origin_pr and their *_mode pairs) are reachable today through the dashboard, POST /policies/PATCH /policies/:id, and the MCP policy.create/policy.update/groom.preview/groom.run tools — lorekit policy/lorekit groom have no CLI flags for them yet.

Protect a lesson from every policy

Some lessons should never be swept, no matter what rule matches them — a load-bearing convention, a hard-won gotcha you want permanent. Mark it protected and every policy (and every manual groom --run) skips it:

lorekit pin repo::acme/app::the-load-bearing-lesson
# …later, if it's no longer special:
lorekit unpin repo::acme/app::the-load-bearing-lesson

lorekit protect <scope::key> [--off] is the longer spelling of the same call. Protection is per-lesson and persists until you explicitly clear it — it survives every policy change, and a disabled policy or one in review mode obviously can't reach it either.

Run it, or let the sweep run it

lorekit groom --policy-id <id> --run

prompts for confirmation (or pass --yes non-interactively) and then archives every match — recoverable via lorekit restore or memory.restore, exactly like any other archive. An auto + enabled policy runs on its own every night; nothing else needs to happen for it to take effect once you flip --enabled on.

All server-side, no local-store equivalent. groom, policy, protect and pin/unpin are remote-only commands — retention policies are account-wide state, the same posture as lorekit purge.

Spotting the same lesson written twice

Retention rules catch lore that has gone stale. A different kind of clutter is the same lesson learned two or three times under slightly different keys — which no age condition can see, because every copy keeps recurring.

The Lore Explorer has a Duplicate clusters trigger for that. It is rolling out behind a feature flag, so you may not see it yet — the CLI's lorekit dedupe (below) answers the same question in the meantime, and answers it over your whole store. Once it's on, click the trigger and a sidebar opens beside the list — not a popup, so you can keep clicking lessons while it's open. It groups the current scope's near-duplicate lessons by how much vocabulary they share (the same Jaccard heuristic lorekit dedupe uses), ranks the groups by how often their members have recurred, and — where the keys match a known recurrence class — names it. Pick a cluster and its members replace the list, so opening one is the same click you'd use on any lesson.

It is read-only, by design: nothing in it merges, edits or deletes lore. Deciding that three near-duplicate lessons are really one entry is a judgment call about meaning, so the sidebar gets you to the evidence and stops. Rewriting them into one lesson is an ordinary memory.write (the surviving key) plus an archive of the others.

Two things worth knowing before you read it as a clean bill of health:

  • It is a recency window, not your whole store. The sidebar clusters the most recently updated lessons in the scope and says so in its footnote when that window was full. A duplicate pair whose members were both last touched long ago will not appear.
  • lorekit dedupe is the whole-store answer. It streams every lesson in the scope through the identical clustering, so it has no window at all — use it when you want the complete list rather than "what have I written lately that duplicates something else lately".
# The whole scope, not just the recent window
npx @lorekit/cli dedupe --scope repo::acme/app