Repository navigation
Conversation
query_methodology listed the retracted memory next to its replacement: after remember(..., supersedes_id=4383843, force=True) returned "superseded", both 4383843 and 4383844 were in hotMemories. The shared listing primitives (get_memories_for_domain, get_memories_for_directory, get_hot_memories, get_memories_mentioning_entity, get_recently_accessed_memories) read the physical memories table unless a caller passed heads_only=True, so every content-serving caller had to remember to opt in; query_methodology, narrative, get_project_story, sync_instructions, checkpoint, assess_coverage and detect_gaps did not, and the get_recent_memories fallback that curate_wiki and curate_distill use bypassed the opt-in their primary branch had. The safe read is now the default on both backends. A maintenance caller that needs the physical chain (validate_memory, the pruning and plasticity consolidation passes) passes heads_only=False, and a test pins that list. get_memories_for_directory gains the parameter; get_memories_by_tag and get_recent_memories read current_memories because every caller serves or authors from the content. wiki_extract selects its candidates from current_memories so claims are not mined from a retracted memory. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXMXvHLm94B6pA9FkWWmzu Signed-off-by: cdeust <cdeust@icloud.com>
The review of the listing change found sibling readers that either still read the physical table or silently changed behaviour with the new default. Readers that serve or count content now read chain heads: the SessionStart pending-curation count (shown to the user) and the cached code-graph lookup in session_start.py, memify_derive's provenance lookup, and the whole-store listing used by assess_coverage and change_impact (get_all_memories_for_validation). Synaptic tagging in write_post_store states heads_only=True: a boost only matters for memories a recall can serve. Readers that need the physical chain say so explicitly: the derived-rel and distill-of idempotency marker scans (a corrected fact need not carry the marker, so the relationship would be derived again), validate_memory's whole-store selection and the staleness sweep. get_memories_by_tag, get_memories_for_entity and get_all_memories_for_validation gain heads_only on both backends. The caller pin now derives the listing set from the store signatures, covers every maintenance caller, and fails on any listing call in a maintenance module that does not state heads_only. New tests: a limit of N returns N heads when retracted rows outrank them, on both backends; validate_memory keeps the superseded row on every selection path. The pr2 read-path audit no longer records the opposite default. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXMXvHLm94B6pA9FkWWmzu Signed-off-by: cdeust <cdeust@icloud.com>
…ntenance sites ADR-1100: listings that serve content return supersession chain heads by default. Adds the wiki page and its mirror, cites the ADR from the stats primitives, the three consolidation and validation opt-out comments and the pin test docstring. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXMXvHLm94B6pA9FkWWmzu
66aa07d to
d3f04bb
Compare
|
ZETETIC-REVIEW: REQUEST_CHANGES Scope: code, tests and text of PR #698 at the head above, base origin/main 02c3cf0. Read-only review in a registered throwaway worktree (review-698d). Load average 3.4 to 4.6, targeted tests only. How tests were run: tests_py/conftest.py connects to PostgreSQL at import time (create_isolated_test_database at conftest.py:139, _pg_available at :181) and a server was listening on 5432, so the normal pytest entry would have touched the local server. I therefore ran pytest with --noconftest, a stub module standing in for tests_py.conftest (_USE_PG=False), HOME and every Cortex data path pointed at a throwaway dir, CORTEX_MEMORY_STORE_BACKEND=sqlite, DATABASE_URL unset, no database URL set. Every PostgreSQL leg was skipped; PostgreSQL behaviour below is by reading the SQL, not by execution. The PostgreSQL store classes were imported for signature inspection (and by the pin test); importing them opens no connection. BlockingB1. scripts/memory_staleness_revalidate.py:46 ( B2. Two idempotency lookups silently moved from the physical chain to heads, the same class the PR itself opts out for memify derived-rel and curate_distill distill-of (ADR-1100 point 3: "must still see a superseded carrier so a corrected fact is not derived twice"):
Verified (with command or source)
Non-blockingN1. The PostgreSQL defaults are pinned only by the PG legs. A static check comparing inspect.signature defaults of every heads_only parameter across both stores would catch the surviving mutant without a server; the pin test today compares names only (test_the_listing_set_is_the_same_on_both_backends). Not verified
|
…e physical chain, and the listing pin covers the whole repo The staleness script's dry-run wrapper now accepts and forwards heads_only. find_existing_memory and already_ingested pass heads_only=False so a superseded carrier still blocks a second write. The pin walks every non-test Python file, compares the heads_only defaults across both stores, and requires every double that defines a listing to accept the keyword. ADR-1100 point 3 names the two ingest lookups; the audit JSON no longer claims search_vectors defaults to True. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXMXvHLm94B6pA9FkWWmzu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXMXvHLm94B6pA9FkWWmzu
Listings that serve memory content now return supersession chain heads by default, on both backends, so query_methodology no longer lists a retracted memory next to its replacement.
Symptom
On 2026-10-09, after
remember(..., supersedes_id=4383843, force=True)answered"action":"superseded"and created 4383844,query_methodologyreturned both rows inhotMemories. The old row still carried a statement the new one corrects.Root cause
The shared listing primitives defaulted to the physical table:
heads_only: bool = Falsein pg_store_queries.py (get_memories_for_domain line 24, get_hot_memories line 54), sqlite_store_queries.py (lines 38 and 66), pg_store_entities.py, sqlite_store_entities.py, pg_store_stats.py and sqlite_store_stats.py, and get_memories_for_directory, get_memories_by_tag and get_recent_memories had no heads option at all. Each content-serving caller had to opt in; the earlier fix (docs/program/pr2-read-path-supersession-audit.json) opted in recall_hierarchical, drill_down and a few more. query_methodology.py:135-139 (get_memories_for_domain, get_memories_for_directory, get_hot_memories) did not, nor did narrative, get_project_story, sync_instructions, checkpoint, assess_coverage, detect_gaps. curate_wiki and curate_distill opted in on their first branch and then fell back to get_recent_memories, which read the whole table.Fix
derived-rel, curate_distilldistill-of, ingest_findings_writersfind_existing_memory, ingest_document_writersalready_ingested: a corrected fact need not carry the marker or dedup tag, so reading heads would derive, distill or write the same thing a second time; all four were physical reads before this PR).heads_onlydefaults are compared between the SQLite and PostgreSQL stores from their declared signatures (no server needed), and every non-test class that defines a listing must accept the keyword. Its limit is in the test docstring: it reads source text, so it cannot see a run-time value.Review round 4 (verdict Cortex-698d: B1, B2)
_DryRunStore.get_all_memories_for_validationin scripts/memory_staleness_revalidate.py lackedheads_only, so the script's default dry-run mode raisedTypeError. It now accepts and forwards the keyword. Test: tests_py/scripts/test_memory_staleness_revalidate_script.py drivesrevalidate_stalenessthrough the real script wrapper over a real SQLite store (2 tests).find_existing_memoryandalready_ingestedinherited the heads default and lost the retracted carrier, so after a finding was superseded, writing it again inserted the retracted text as a fresh head. Both passheads_only=False(ADR-1100 cited at the sites), both are in the pin's_MAINTENANCE_SITES, one test per site: tests_py/handlers/test_ingest_dedup_after_supersession.py (real SQLite store, supersede the carrier, assert no second write).get_recently_accessed_memoriesdefault flipped to False failstest_the_heads_only_defaults_are_the_same_on_both_backends)._connect_pg, a PG connection argument), so on a SQLite leg the substring tests pinned text that never executed; the new tests skip without PostgreSQL like the other hook SQL tests. tests_py/hooks/test_session_start.py is back to its content on main.get_all_memories_for_validationis in the limit test (mutant: calling it with heads_only=False fails sqlite and pg legs).New mutants, each applied and run (raw output 698-r4-pin-mutants.txt and 698-r4-session-start-mutant.txt in /Users/cdeust/.claude/fleet/work/Cortex/): ingest opt-out removed fails the maintenance-sites pin; wrapper call dropping the keyword fails the silent-call pin; PostgreSQL default flipped fails the static defaults test; session_start SQL back to
memoriesfails both behaviour tests.Failing before, passing after (raw output in the same directory): B1
2 failed(698-r4-B1-before.txt,TypeError: _DryRunStore.get_all_memories_for_validation() got an unexpected keyword argument 'heads_only') then2 passed(698-r4-B1-after.txt); B22 failed(698-r4-B2-before.txt,assert None == 1) then2 passed(698-r4-B2-after.txt).Round 4 dispositions (write-path existence, duplicate, marker and idempotency checks, whole repository)
find_existing_memoryalready_ingestedfind_existing_doc_memory_existing_derived_markers, curate_distill_existing_distill_markersload_existing_hashes_existing_memory_ids, write_gate_collect_existing_embeddings, pgsupersede_to_existinglist_exact_duplicate_groupsUncertain:
load_existing_hashesbuilds {path: (id, hash)} from every physical row and the last row wins, so a stale hash on a superseded codebase memo could in principle win. It is behaviour on main, not changed by the default flip, and I did not reproduce it.Sibling search (review round 2)
Commands:
grep -rnE "(FROM|JOIN)\s+(memories|current_memories)\b" mcp_server | grep -v pg_schemafor raw SQL, plus an AST walk over mcp_server/**/*.py that lists, per function, which of the two tables it reads (117 functions) and every call of a listing primitive outside infrastructure/ with its heads_only keyword (97 calls). Per-site dispositions: /Users/cdeust/.claude/fleet/work/Cortex/698-sibling-dispositions.md._count_pending_curations(count shown to the user)_lookup_cached_graph_path(serves the cached graph path)derived-relmarker scan (get_memories_by_tag)distill-ofmarker scan_fetch_grooming_stalenessOwner decision left open: session_start
_count_memoriesprints "(total: N)" from COUNT(*) over memories, so it includes retracted versions, and memory_stats.total has the same definition. Kept as a storage total; changing both is a statistics-semantics change outside this PR.Failing before, passing after
Before (main with the new tests; raw output in /Users/cdeust/.claude/fleet/work/Cortex/B-before.txt):
After, on the FINAL head d967c82 (PostgreSQL legs RAN, see the note below),
pytest -q -p no:cacheprovider(full suite, raw output 698-r4-full-pytest.txt):On the final head: ruff check and ruff format --check clean, check_craftsmanship.py --base origin/main OK, check_project_wiki.py OK, check_no_deps_invariant.py clean, pyright mcp_server/ 0 errors in the locked environment (698-r4-gates.txt).
PostgreSQL, per head (this settles the two earlier contradictory notes): on 66aa07d the suite ran the PostgreSQL legs (9633 passed); on d3f04bb no PostgreSQL was reachable for the author's run, so 112 tests skipped (9584 passed) and the reviewer ran SQLite-only; on the final head d967c82 the suite ran against the repo's own throwaway database (conftest create_isolated_test_database, dropped at session end, no live database touched): 9685 passed, 19 skipped. The first full run on dfd18dc (698-r4-full-pytest-first-dfd18dc0.txt) was 9682 passed, 3 failed, 19 skipped: the new whole-repo pin tried to read a non-UTF-8 vendored file under the gitignored deps/ tree that the launcher tests create in the checkout; d967c82 excludes that tree and the three tests pass.
New in round 2, each shown to fail on a mutant:
0 = len([]).PostgreSQL (head 66aa07d and d967c82): the pg legs of test_superseded_listing_defaults.py ran against the suite's throwaway database; they failed before and pass after (on d3f04bb they skipped). Two existing tests (test_supersession_read_path.py and its SQLite twin) asserted the old default and now pass heads_only=False explicitly.
Completion Ledger
Decision points for an ADR
Round 4: ADR-1100 point 3 now names
find_existing_memoryandingest_document_writers.already_ingestedamong the physical-chain readers; point 4 no longer lists tag lookups as exceptions.ADR-1100 is registered ("Listings that serve content return supersession chain heads by default"; amends ADR-0258, ADR-0537, ADR-0602, ADR-1014). It is applied on this branch (rebased on main after #697 merged). docs/program/pr2-read-path-supersession-audit.json, which recorded the opposite default, is updated here (the five entries that stated or implied it, plus an
amendmentsnote); no test reads that file.Not verified
Windows. query_methodology still wraps its store read in a catch-and-return-empty block (pre-existing, outside this change).
Fixes #687
🤖 Generated with Claude Code
https://claude.ai/code/session_01DXMXvHLm94B6pA9FkWWmzu