Skip to content

feat: let recall choose which of two disagreeing hits comes first - #15

Merged
doronp merged 2 commits into
mainfrom
feat/recall-conflict-policy
Oct 2, 2026
Merged

doronp merged 2 commits into
mainfrom
feat/recall-conflict-policy

Conversation

@doronp

@doronp doronp commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Why

If a decision is reversed weeks later, the original and the reversal both match the same search. recall listed them by BM25 score with no date, so the stale decision could come first, and the agent would act on it with no hint of a conflict.

What

gitmemory recall --policy decides which hit comes first. Nothing is deleted.

Policy Order
latest-user (default) The user's own words newest first, then agent turns and tool output newest first
latest Newest first, whoever said it; relevance breaks ties; undated last
relevance Best BM25 match first (the old order); every line is dated, so the agent can decide
module:function Your own rule: a function from list[Hit] to list[Hit]
  • Settable with --policy or GITMEMORY_POLICY; the flag wins.
  • Every recall line now shows the turn's UTC date. Hit gained ts, and index.when(hit) parses it.
  • index.search is unchanged, so bench/ still measures plain BM25.
  • A policy name that doesn't resolve exits 2 with a message, never a silent fallback.
  • docs/USAGE.md has a new section, "When memories disagree". It shows a validity window ("ignore everything before date X") written as a custom policy.

Behaviour change

recall now lists the user's own words newest first by default. To get the old order, pass --policy relevance.

Review (Gemini 3.1 Pro + Claude)

  • Agreed and fixed: plain latest as the default let the agent's newer, mistaken proposal (and captured recall output) outrank the user's rule, which breaks the README demo. The founder picked latest-user as the default. Also fixed: a policy that drops every hit is reported instead of printing nothing, a policy that crashes gives an error instead of a traceback, and the README recall row no longer says "best first".
  • Refuted: "b.ts may not exist" (it's in _DDL); "bad policy gives a traceback" (main catches ValueError).

Known ceiling

latest reorders only the top -k hits, so a reversal that ranks below them can't win. That's documented and noted in the code. The upgrade is to search a wider pool than is shown.

Tests

  • 10 new tests in tests/test_index.py:
    • the reversal leads under latest and comes second under relevance
    • timezone offsets, naive and undated timestamps
    • the CLI default, the env var, module:function
    • four bad-policy errors
  • I ran the documented custom-policy example end to end against a scratch store.
  • Full offline suite passes. The README count is updated to 1,036.

Overlaps in docs with #14 (recall skill); whichever merges second needs a rebase.

🤖 Generated with Claude Code

doronp and others added 2 commits October 2, 2026 08:38
A decision reversed weeks later and the reversal both match the same
search, and recall printed them by BM25 with no date, so the stale one
could lead. `recall --policy` now picks the order: `latest` (default,
newest first), `relevance` (the old order; every line is dated so the
agent can decide), or a `module:function` of your own. Also settable
with GITMEMORY_POLICY. Nothing is deleted, and `index.search` is
unchanged, so bench/ still measures plain BM25.

`Hit` carries the turn's `ts`, read through `index.when`. USAGE.md
documents the options and a validity-window rule as a custom policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… drops

Review (Gemini 3.1 Pro + Claude, both agreeing) found plain `latest`
breaks the README's own story: after compaction drops "don't add a retry
loop", the agent proposes one, and that newer proposal, like a captured
earlier recall, outranks the rule. The default is now `latest-user`: the
user's own words newest first (derive's prose admission rule, so tool
results do not count), then the rest newest first. `latest` stays as the
strict option.

Also: a custom policy that keeps nothing says so instead of printing
silence, and one that returns None or raises is an error, not a
traceback. README's recall row no longer says "best first".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@doronp
doronp merged commit 2032c40 into main Oct 2, 2026
5 checks passed
@doronp
doronp deleted the feat/recall-conflict-policy branch October 2, 2026 05:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant