feat(memory): typed confidence and symmetric conflict provenance (v6.6.0) - #208
Conversation
A note could express exactly one provenance fact — "this replaced that", through `Supersedes:`. That is a clean, ordered replacement. Real recall failures are messier: two notes disagree, neither has been reviewed, and the agent trusts whichever one the ranker happened to surface. There was no way to say "these disagree" or "this was never verified". Two optional fields, round-tripping through Markdown, CLI, MCP, and the mesh merge driver exactly as Supersedes does: - `Confidence: high|medium|low`. Absent means unknown — which is every note that already exists, so absence stays the default. An unrecognised value is dropped rather than raising: it arrives from hand-edited Markdown and from mesh peers on older code, and a typo must never make a note unreadable. - `Conflicts with: [[Other]]`, symmetric in effect even when only one side declares it. The whole point is that the agent sees the disagreement, so whichever note retrieval surfaces has to carry the warning. Retrieval surfaces conflicts, it does not resolve them: both notes come back marked with the other's name and no ranking preference between them. Low confidence gets a gentle 0.8 penalty so a comparable verified note wins a tie — deliberately nothing like the 0.35 superseded penalty, because low confidence is not obsolescence. `recall-note` emits the fields and a warning naming the other note, but only when the note declares them, so notes without them cost what they always did. Lint reports conflicts that point at nothing and conflicts the other side never acknowledged. Scoped as the issue asked: two fields, not a research-grade two-ledger system with authority tiers and independent-source requirements. omind is an agent's memory, not a citation manager. Closes #195. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe change adds optional ChangesProvenance-aware notes
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant NoteStore
participant SearchIndex
participant Recall
Client->>NoteStore: create or edit note metadata
NoteStore->>SearchIndex: persist confidence and conflict fields
SearchIndex->>SearchIndex: rank and resolve conflict metadata
SearchIndex->>Recall: return enriched search hit
Recall->>Client: return provenance fields and conflict warning
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Line 10: Add a blank line immediately after each changelog heading, including
“Added” and “Changed”, to satisfy markdownlint MD022 while preserving the
existing entries.
In `@src/omind/lint.py`:
- Around line 248-280: Update the conflict validation around ids_to_note and the
valid-target branch so every target recognized by known, including archived and
subfolder notes from known_extra, is loaded into the reciprocal lookup while
remaining excluded from the outer lint-source iteration. Ensure valid targets
without reciprocal declarations can reach the existing conflict-one-sided
validation, and add a regression test covering an active note targeting an
archived or subfolder note.
In `@src/omind/searchindex.py`:
- Around line 138-143: Ensure every search path returns provenance: update
src/omind/searchindex.py lines 138-143 in _listing to populate confidence and
resolve conflicts_with symmetrically; update src/omind/store.py lines 1040-1048
in _scan_search to provide the same provenance when the index returns None; add
tests in tests/test_searchindex.py lines 476-509 covering OMI_INDEX_DISABLE=1
and index-failure fallback behavior.
In `@src/omind/store.py`:
- Around line 607-610: Update the note-rendering logic around the confidence
output to pass f.confidence through _clean_confidence before deciding whether to
append the Confidence field, so unsupported values are omitted while valid
normalized values render. Extend test_unknown_confidence_is_dropped_not_fatal to
assert the raw note does not contain “Confidence:”.
- Around line 1356-1362: Update the provenance inheritance logic in the note
edit flow around fields.confidence and fields.conflicts_with to distinguish
omitted fields from explicitly supplied empty strings. Preserve current values
only when the caller omitted each field (using the existing None-based presence
semantics), while allowing explicit empty strings to clear them; add MCP
coverage verifying that clearing each field persists.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 27c01de2-aefd-4c6f-acd7-cf5f4bcb55eb
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (17)
BACKLOG.mdCHANGELOG.mdREADME.mddocs/retrieval.mdpyproject.tomlsrc/omind/__init__.pysrc/omind/cli.pysrc/omind/lint.pysrc/omind/merge.pysrc/omind/recall.pysrc/omind/searchindex.pysrc/omind/server.pysrc/omind/store.pytests/test_lint.pytests/test_searchindex.pytests/test_server.pytests/test_store.py
📜 Review details
⏰ Context from checks skipped due to timeout. (10)
- GitHub Check: test (ubuntu-latest, 3.14)
- GitHub Check: test (macos-latest, 3.14)
- GitHub Check: test (ubuntu-latest, 3.12)
- GitHub Check: test (windows-latest, 3.10)
- GitHub Check: test (ubuntu-latest, 3.10)
- GitHub Check: test (ubuntu-latest, 3.13)
- GitHub Check: test (macos-latest, 3.10)
- GitHub Check: test (windows-latest, 3.14)
- GitHub Check: test (ubuntu-latest, 3.11)
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{py,md}
📄 CodeRabbit inference engine (AGENTS.md)
For retrieval changes, consult
docs/retrieval.mdand preserve the documented search-index design: FTS5/BM25, chunk vectors, wikilink graph resolution, and Reciprocal Rank Fusion.
Files:
README.mdsrc/omind/__init__.pyCHANGELOG.mdsrc/omind/merge.pydocs/retrieval.mdBACKLOG.mdtests/test_server.pysrc/omind/recall.pytests/test_lint.pysrc/omind/cli.pysrc/omind/lint.pytests/test_searchindex.pysrc/omind/server.pytests/test_store.pysrc/omind/store.pysrc/omind/searchindex.py
**/*.md
📄 CodeRabbit inference engine (AGENTS.md)
Documentation files must include the specified Nebraska footer; the README must use the centered banner version.
Files:
README.mdCHANGELOG.mddocs/retrieval.mdBACKLOG.md
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Treat Markdown notes as the source of truth; keep indexes, caches, and vectors inpaths.state_dir(), never in the vault.
Retrieval must fail open: every search layer returnsNoneon errors and callers fall back to the older search path. Test failure branches as well as successful retrieval.
All note writes must go throughOmiStore; external writers should usenotes.upsert_note. Preserve flocking, atomic rename, LamportRev:stamping, and soft deletes. Deletes archive withDisabled: true; onlyomind mesh purgeremoves notes permanently.
UseOmiStore.safe_namefor every note read and write; never bypass it because path traversal must remain impossible.
Keepstore.pyframework-free: it must not depend on FastAPI or MCP; both the CLI and web app build on it.
Credential notes must be de-prioritized in search and gate suggestions unless the query concerns credentials, usingretrieve._CREDENTIAL_PENALTY. The gate must never steer agents toward secrets notes.
MCP tools must never return unbounded output. Every list-shaped tool must paginate withlimit,offset,total, andhas_morethroughserver._page.
Treatindex.mdandMemory Template.mdas scaffolding rather than memories; reading them must not clear the consult gate. Usepaths.NON_CONSULT_FILENAMES.
search()must refresh the index on every call so results remain correct after another process writes a note.
Recency is only a re-ranking leg: it may reorder notes matched by content legs but must never add otherwise-unmatched notes.
Do not replacelint.py's independent full-vault scanning with index wikilink handling; the index does not strip code fences, whilelint.pydoes.
link_targets()must preserve the author’s link casing for dangling-link reports; only resolution may lowercase links.
Never mutate aNoteSummaryreturned from_cached_summary; it is shared cached state. Usedataclasses.replace, as instore._indexed_search.
Coerce embedding results through `se...
Files:
src/omind/__init__.pysrc/omind/merge.pytests/test_server.pysrc/omind/recall.pytests/test_lint.pysrc/omind/cli.pysrc/omind/lint.pytests/test_searchindex.pysrc/omind/server.pytests/test_store.pysrc/omind/store.pysrc/omind/searchindex.py
tests/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Retrieval changes must test both indexed search and the fallback path with
OMI_INDEX_DISABLE=1, including failure behavior. MCP list tools must remain bounded, covered bytest_every_list_tool_is_bounded.
Files:
tests/test_server.pytests/test_lint.pytests/test_searchindex.pytests/test_store.py
🪛 markdownlint-cli2 (0.23.1)
CHANGELOG.md
[warning] 10-10: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 40-40: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🔇 Additional comments (15)
src/omind/lint.py (1)
54-54: LGTM!Also applies to: 193-193
tests/test_lint.py (1)
11-11: LGTM!Also applies to: 193-216
docs/retrieval.md (1)
47-56: LGTM!BACKLOG.md (1)
78-82: LGTM!pyproject.toml (1)
3-3: LGTM!src/omind/__init__.py (1)
5-5: LGTM!src/omind/store.py (1)
183-188: LGTM!Also applies to: 236-237, 268-295, 498-568
src/omind/cli.py (1)
341-351: LGTM!Also applies to: 1331-1332
src/omind/server.py (1)
267-270: LGTM!Also applies to: 281-296, 321-346
src/omind/merge.py (1)
271-272: LGTM!tests/test_store.py (1)
830-853: LGTM!Also applies to: 864-871
README.md (1)
225-230: LGTM!src/omind/searchindex.py (1)
62-83: LGTM!Also applies to: 201-234, 695-714, 838-838, 1037-1093, 1140-1160, 1193-1223, 1316-1326
src/omind/recall.py (1)
84-105: LGTM!tests/test_server.py (1)
438-459: LGTM!
|
|
||
| ## [6.6.0] - 2026-08-02 | ||
|
|
||
| ### Added |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a blank line after each heading.
markdownlint-cli2 reports MD022 because ### Added and ### Changed have no blank line below them. Add one blank line after each heading.
Proposed fix
### Added
+
- **Two optional provenance fields on a note: `Confidence:` and
### Changed
+
- Ranking: `Confidence: low` applies a gentle 0.8 penalty, so a comparableAlso applies to: 40-40
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)
[warning] 10-10: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@CHANGELOG.md` at line 10, Add a blank line immediately after each changelog
heading, including “Added” and “Changed”, to satisfy markdownlint MD022 while
preserving the existing entries.
Source: Linters/SAST tools
| # `Conflicts with:` is symmetric in meaning but written on one note at a | ||
| # time, so the two ways it goes wrong are a target that does not exist and | ||
| # a claim the other side never acknowledged. Retrieval treats a one-sided | ||
| # claim as binding on both notes; lint says so out loud. | ||
| declared: dict[str, str] = {} | ||
| ids_to_note: dict[str, str] = {} | ||
| for n in notes: | ||
| for note_id in n.ids: | ||
| ids_to_note[note_id] = n.path.name | ||
| for n in notes: | ||
| claim = n.fields.conflicts_with.strip() | ||
| if not claim: | ||
| continue | ||
| target = claim.strip("[]").split("|", 1)[0].split("#", 1)[0].strip().lower() | ||
| if target in n.ids: | ||
| issues.append( | ||
| LintIssue( | ||
| "conflict-self", "warn", n.path.name, "`Conflicts with:` points at itself" | ||
| ) | ||
| ) | ||
| continue | ||
| if target not in known: | ||
| issues.append( | ||
| LintIssue( | ||
| "conflict-broken", | ||
| "error", | ||
| n.path.name, | ||
| f"`Conflicts with: [[{claim}]]` resolves to no note", | ||
| ) | ||
| ) | ||
| continue | ||
| if resolved := ids_to_note.get(target): | ||
| declared[n.path.name] = resolved |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Validate reciprocity for every valid conflict target.
known includes archived and subfolder note IDs through known_extra, but ids_to_note contains only active top-level notes. If an active note conflicts with an archived or subfolder note, the target is valid, but ids_to_note.get(target) returns nothing. The code then emits neither conflict-broken nor conflict-one-sided, even when the target has no reciprocal declaration.
Load conflict metadata for valid non-source targets into the reciprocal lookup. Keep those notes excluded as lint sources. Add a regression test for this path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/omind/lint.py` around lines 248 - 280, Update the conflict validation
around ids_to_note and the valid-target branch so every target recognized by
known, including archived and subfolder notes from known_extra, is loaded into
the reciprocal lookup while remaining excluded from the outer lint-source
iteration. Ensure valid targets without reciprocal declarations can reach the
existing conflict-one-sided validation, and add a regression test covering an
active note targeting an archived or subfolder note.
| #: The note this one declares (or is declared to be) in conflict with, if | ||
| #: any. Surfaced so an agent sees a disagreement instead of silently | ||
| #: trusting whichever side the ranker happened to put first (#195). | ||
| conflicts_with: str = "" | ||
| #: "high" | "medium" | "low"; "" when the note declares none. | ||
| confidence: str = "" |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Return provenance on every search path.
Ranked indexed hits carry provenance. Empty-query indexed listings create default Hit values. Index-disabled fallback searches create default NoteSummary values. Therefore search-vault hides confidence and conflict metadata when callers browse by tag or when retrieval fails open. The fallback also cannot surface one-sided conflicts symmetrically.
src/omind/searchindex.py#L138-L143: Populateconfidenceand resolvedconflicts_within_listing.src/omind/store.py#L1040-L1048: Populate equivalent symmetric provenance during_scan_searchwhen the index returnsNone.tests/test_searchindex.py#L476-L509: Add coverage withOMI_INDEX_DISABLE=1and an index-failure path.
As per coding guidelines, “Retrieval changes must test both indexed search and the fallback path with OMI_INDEX_DISABLE=1, including failure behavior.”
📍 Affects 3 files
src/omind/searchindex.py#L138-L143(this comment)src/omind/store.py#L1040-L1048tests/test_searchindex.py#L476-L509
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/omind/searchindex.py` around lines 138 - 143, Ensure every search path
returns provenance: update src/omind/searchindex.py lines 138-143 in _listing to
populate confidence and resolve conflicts_with symmetrically; update
src/omind/store.py lines 1040-1048 in _scan_search to provide the same
provenance when the index returns None; add tests in tests/test_searchindex.py
lines 476-509 covering OMI_INDEX_DISABLE=1 and index-failure fallback behavior.
Source: Coding guidelines
| if f.confidence: | ||
| out.append(f"- Confidence: {f.confidence}") | ||
| if f.conflicts_with: | ||
| out.append(f"- Conflicts with: {f.conflicts_with}") |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Normalize confidence before rendering.
Line 607 writes any nonempty direct API value. create-note accepts free text, so an unsupported value is persisted as an invalid Confidence: field even though _clean_confidence defines that unsupported values must be dropped.
Render only _clean_confidence(f.confidence). Extend test_unknown_confidence_is_dropped_not_fatal to assert that the raw note omits Confidence:.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/omind/store.py` around lines 607 - 610, Update the note-rendering logic
around the confidence output to pass f.confidence through _clean_confidence
before deciding whether to append the Confidence field, so unsupported values
are omitted while valid normalized values render. Extend
test_unknown_confidence_is_dropped_not_fatal to assert the raw note does not
contain “Confidence:”.
| # Same inheritance for the provenance fields (#195): a partial edit | ||
| # that carries neither must not silently clear a note's confidence | ||
| # or drop a recorded conflict. | ||
| if not fields.confidence: | ||
| fields.confidence = current.confidence | ||
| if not fields.conflicts_with: | ||
| fields.conflicts_with = current.conflicts_with |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Allow explicit provenance clearing.
Line 1359 treats "" as an omitted value. server.edit_note distinguishes omission with None, then assigns an explicit empty string to clear a field. This branch restores the old value, so MCP clients cannot clear confidence or conflicts_with.
Track field presence separately from field value. Preserve these fields only when the caller omitted them. Add MCP coverage for clearing each field.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/omind/store.py` around lines 1356 - 1362, Update the provenance
inheritance logic in the note edit flow around fields.confidence and
fields.conflicts_with to distinguish omitted fields from explicitly supplied
empty strings. Preserve current values only when the caller omitted each field
(using the existing None-based presence semantics), while allowing explicit
empty strings to clear them; add MCP coverage verifying that clearing each field
persists.
Closes #195.
The gap
An OMI note could express exactly one provenance fact: "this replaced that" (
Supersedes:). That is a clean, ordered replacement. Real recall failures aren't clean — two notes disagree, neither has been reviewed, and the agent trusts whichever one the ranker surfaced. There was no way to say "these disagree" or "this was never verified".Two fields, not a ledger
The issue explicitly scoped this away from claude-obsidian's two-ledger system (authority tiers, independent-source requirements for high-risk claims, 1,351 lines). omind is an agent's memory, not a citation manager. So:
Confidence: high|medium|low— absent means unknown, which is every note that already exists. An unrecognised value is dropped, not raised: this arrives from hand-edited Markdown and from mesh peers running older code, and a typo in one note must never render that note unreadable.Conflicts with: [[Other]]— symmetric in effect even when only one side declares it. The point of the field is that the agent sees the disagreement, so whichever note retrieval happens to surface must carry the warning. Writing it on both is good hygiene, not a requirement.Both round-trip through Markdown, CLI (
--confidence,--conflicts-with), MCP (create-note/edit-note), and the mesh merge driver, exactly asSupersedes:does. Partial edits no longer clear them (same inheritance rule asrev/created).Retrieval surfaces conflicts; it does not resolve them
Both notes come back marked with the other's name, and ranking expresses no preference between them. Picking a winner would recreate the bug — the agent silently trusting one side.
Confidence: lowgets a gentle 0.8 penalty so a comparable verified note wins a tie. Deliberately nothing like the 0.35 superseded penalty: low confidence is not obsolescence, and a hedged memory is still worth recalling.recall-noteemitsconfidence,conflicts_with, and awarningnaming the other note — only when declared, so a note without these fields costs exactly the tokens it did before.Lint
conflict-broken(error) — points at no note.conflict-one-sided(info) — the other side never acknowledged it. Retrieval treats a one-sided claim as binding on both notes, so lint says so out loud rather than leaving it implicit.conflict-self(warn).Tests
10 new, across four suites: round-trip; absent fields render nothing (every pre-existing note untouched); an unknown confidence is dropped but leaves the note readable; a partial edit doesn't clear provenance; low confidence loses a tie but stays recalled; a one-sided conflict surfaces on both notes; recall-note's warning and its absence; and the three lint cases.
Migration
Search index schema → v5 for the two new columns. It rebuilds itself on first use; nothing to do.
Gates
ruff check .·mypy src(strict) ·pytest(866 passed, 1 skipped) ·pip-audit— green locally.🤖 Generated with Claude Code