Skip to content

Round 2: time-aware retrieval, coverage mode, roll-up cards, grounded honesty#270

Merged
samrusani merged 21 commits into
mainfrom
lme2-integration
Jul 10, 2026
Merged

Round 2: time-aware retrieval, coverage mode, roll-up cards, grounded honesty#270
samrusani merged 21 commits into
mainfrom
lme2-integration

Conversation

@samrusani

Copy link
Copy Markdown
Owner

Nine features from the round-2 roadmap, built in parallel worktrees, integrated with cross-branch wiring, and measured through the full ladder (free coverage probe → targeted paired subset → full 172-question paired slice). No new benchmark claim: the published 79.4% stands; the paired slice measures this branch at parity (+1 net, p=1.0) with the movement where the features aim (temporal +2, multi-session +1, abstention +1). These ship for product value.

Features

  • Time-aware retrieval — temporal anchors ("two weeks ago", "in March 2023") as an RRF list against event dates; dormant on date-free queries, byte-identity tested.
  • Coverage mode — aggregation-intent gate + clause decomposition + instance-diversity fusion keyed (source_id, source_chunk_id).
  • Consolidation roll-up cards — review-gated same-topic aggregation cards with per-instance dates/values/provenance; zero commit-path cost (SQL statement sequence proven identical).
  • Fact-augmented keys (migration 20260707_0082) — derived category/attribute keys at low FTS weight, both backends; identifier-shaped attributes excluded.
  • Entity-grounding note — packs state "no stored memories mention X" when lexically true.
  • Supersession validity annotations — packs carry validity metadata; current versions outrank superseded ancestors.
  • Speaker-provenance capture — USER/ASSISTANT origin recorded, promotion tie-breaks favor user-asserted values, UI labels, cross-batch dedupe.
  • Query-anchored excerpts + disclosed grounding gate + pack-provenance instrumentation (benchmark harness).

Found-and-fixed along the way

Adversarial loss-forensics on the first slice run attributed every regression to a mechanism and produced surgical fixes, all verified: an excerpt-anchoring gate hole, a too-coarse diversity key, UUID pollution in derived keys (897/897 memories), and dead confidence-delta config. Stage-B paired verification: 6 of 7 convicted questions flipped back correct, zero gain regressions, all guards held.

Verification

1950 unit tests, 80 harness tests, 388 integration tests on fresh migrated Postgres (non-superuser role), six-suite eval battery, honesty audit (no benchmark-label code paths; official templates sha-pinned byte-identical). Paid measurement: 52-question stage-B (+6 net vs baseline on fix targets) and full 172 paired slice (parity, guards held).

Upgrade Overview

Protected Areas

  • memory schema
  • continuity APIs
  • trust rules
  • promotion logic
  • evidence pipeline

Compatibility Impact

Migration 20260707_0082 adds nullable memories.fact_keys and extends both FTS indexes (Postgres tsvector rebuild with weight-D term; SQLite FTS5 column with stale-shape rebuild at bootstrap). mcp_tools review-accept now attaches deterministic fact keys at promotion (never a model call). trusted_fact_promotions/capture record speaker provenance; promotion ordering prefers user-asserted values on ties — no suppression of assistant content (tested). Retrieval gains gated stages (temporal anchor, coverage mode, grounding note) that are dormant unless their query-surface trigger fires; dormant paths byte-identical (tested). Pack items may carry a new optional validity field.

Migration / Rollout

Postgres: alembic upgrade head (0081 → 0082; tsvector rebuild may pause on very large memories tables). SQLite: automatic at next open. Optional backfill CLI derives keys for pre-existing rows. Normal code rollout otherwise.

Operator Action

None required. Optional: run the fact-keys backfill for older stores; set ALICE_FACT_KEYS_* env to enable the model tier at backfill time.

Validation

Full suites as above; offline coverage probe on all 500 benchmark questions (evidence coverage 84.6% → 85.4% overall, multi-session 74.4% → 76.7%, temporal 76.7% → 79.0%); paired slice guards: single-session-user and assistant at 100%, abstention +1.

Rollback

alembic downgrade -1 removes the column/index cleanly; SQLite FTS rebuilds idempotently. Reverting the merge restores prior retrieval exactly (all new stages are additive and gated); no data loss — fact keys and provenance metadata are derived state.

🤖 Generated with Claude Code

Sami Rusani and others added 21 commits July 9, 2026 22:31
…preference, adapter rendering

Context packs now tell the reader which facts are current, superseded, or
time-bounded, derived entirely from values the rows already carry (no extra
store queries):

- compile_context_pack attaches a compact `validity` field to pack memories
  with temporal/supersession signal only: valid_from/valid_to (the
  far-future unbounded sentinel and NULL are omitted), superseded +
  superseded_by_memory_id (own column, migration 20260704_0077, or a
  pack-mate's supersedes back-pointer for one-sided patches),
  supersedes_memory_id, and corrected_at from the agentic in-place
  correction history. Plain memories keep their exact shape.
- Current-version preference (demote-not-drop): the store search stages
  already exclude retired rows (status outside active/accepted, closed
  validity windows), so supersession pairs only co-occur in a pack via
  one-sided pointer state. When that happens the replacement packs directly
  above its superseded ancestor while every other item keeps fused (RRF)
  order; each move is cycle-guarded and counted in the pack trace as
  supersession_reorders. The RRF ranking itself and its trace ranks are
  untouched.
- LongMemEval adapter renders the annotation as a compact factual suffix on
  the facts-section item lines ("[valid 2023-05-30 -> 2023-08-01]",
  "[superseded by a newer entry]", "[updated 2023-08-01; supersedes an
  earlier value]"); the official reading templates stay byte-identical and
  are now pinned by a test.
- Tests: annotation only-on-signal (schema stability incl. the 9999
  sentinel), pair reorder + trace counter + honest fused trace ranks,
  one-sided pointer annotation, corrupt-cycle termination, proper-flow
  supersede exclusion on real SQLite, KU-shaped leak scenario on real
  SQLite and through the real adapter end-to-end, adapter suffix shapes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nswer line

The LongMemEval context packer guaranteed each retrieved source its best
chunk by whole-chunk term overlap, always shown from the chunk head. When
the query's best-matching line sat near the chunk tail (a chess record
matching "27. Kg2 Bd5+"), the answer just past the chunk boundary
("28. Kg3") was excerpted away; enumerated lists were likewise cut mid-run
before the asked-about item.

Now, when a gated query anchor fires, the source's guaranteed excerpt
becomes a window centered on the best-matching line over the session's
chunk stream, and a window edge that cuts an enumerated run extends
through the run. Discipline:

- Triggers are query-surface/content-shape only (weighted query-term line
  overlap; line-initial/inline numbering density). No benchmark labels.
- Gated: single-chunk sources, weak matches (weighted score < 3), and
  prose whose match already sits in the best chunk take the byte-identical
  old path (tested via anchoring-off comparison).
- Round-robin preserved: the anchored window is capped at the baseline
  chunk's entry cost, so pass-1 admissions are unchanged; enumeration
  extensions spend only the global second-pass pool, and pass 2 skips
  chunks a window already shows.
- Deterministic: ties break to the earliest line; expansion prefers the
  lighter side, then downward.
- Official reading templates untouched (byte-frozen test added).

Keyless replay on the two verified failures: 1568498a's context now
contains "28. Kg3" (previously cut at move 27); 1903aded keeps
"Transcriptionist". A 16-question before/after sweep shows one gold-answer
gain, zero gold-answer losses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tention

New vnext_grounding module computes a conservative, deterministic,
read-only retrieval statistic: salient query entities (capitalized
spans, quoted titles, domains, honorific surnames, mid-sentence
capitalized tokens -- never acronyms, contractions, or blocklist words)
with ZERO corpus support. Support = entity substrate (names + aliases)
OR a one-row match_any FTS probe over source chunks and memories with
morphological token variants ("Hawaiian" supports "Hawaii"), so every
error leans toward NOT claiming absence.

compile_context_pack attaches pack["grounding"] = {unsupported_entities,
checked} via one clearly-marked block, mirrored into the trace, ONLY
when at least one entity is unsupported; ungated queries take the
byte-identical old path (no key, zero probes -- test-proven). Skipped at
minimal depth. The LongMemEval adapter renders the field as factual
'Note: no stored memories mention "X".' lines inside the retrieval
output (budget-reserved); reading templates stay byte-frozen and are now
pinned by a test.

Offline FTS-only replay over all 181 entity-bearing longmemeval_s
questions: the note fires on 5/15 abstention questions (all truthful,
incl. 2 previously judged wrong) and 2/166 answerable ones (both
lexically accurate). Retrieval-quality eval suite metrics byte-identical
to main; full unit suite (1708) and harness (46) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the verified LongMemEval failure shape where a user's stated value
(gold '$50') loses to assistant fare-chatter arithmetic ('¥20,000-30,000
(approximately $180-270)'): user-asserted facts now capture with speaker
provenance and outrank assistant estimates for the same topic slot.

- New alicebot_api.memory_provenance module derives provenance_role from
  leading "[USER]:"/"[ASSISTANT]:" transcript tags (content shape only;
  no benchmark metadata or question types are ever read) and classifies
  user_asserted (first-person pronoun + assertion verb + concrete value:
  currency, percent, unit-bearing quantity, or thousands-separated
  number) vs assistant_estimate (hedged or ranged figures).
- vnext_capture annotates candidates with role/assertion class, applies
  a bounded confidence bias (user-asserted +0.08 capped at 0.95,
  assistant estimates -0.06 floored at 0.30 -- bias, not suppression),
  stamps provenance_role/assertion_class into memory metadata_json only
  when a role is derived, and adds one gated extraction rule
  (user_asserted_value) so speaker-tagged first-person user lines with
  concrete values become semantic candidates. Untagged content takes the
  byte-identical legacy path (tested).
- trusted_fact_promotions orders each pattern group by provenance rank
  (user-asserted first, assistant estimates last); provenance-free
  groups keep the legacy (memory_key, id) order byte-for-byte (tested).
- Web memory cards (memories list/detail and the vNext candidate list)
  show a "You said" / "Assistant suggested" chip when the role is known,
  via the shared lib/memory-provenance helper.

Offline verification: 1750 unit + LME harness tests pass; eval suite
(--suite all, sqlite) passes with check statuses byte-identical to main;
FTS-only coverage probe on the 103 currently-wrong questions keeps
any-coverage at 94.2% with one enumeration-question all-coverage flip,
and a 100-question currently-correct sample shows zero coverage flips
(any 98.0 / all 90.0, identical to main).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Date-bearing query text ("in March 2023", "on May 3", "between March and
May 2023", "two months ago", "before today") now parses into one UTC
[start, end) window that joins RRF fusion as ONE MORE ranked list next to
fts/vector/graph — never a hard filter, so a wrong parse cannot evict
lexical, vector, or graph hits.

- vnext_temporal_query.py (new): deterministic parse_temporal_anchor
  (explicit dates, month-year, yearless forms resolved against a
  caller-supplied reference_time, closed/open ranges, relative phrases;
  ambiguous phrases like bare "May" return None) plus
  parse_event_datetime for stored/connector date strings. No LLM, no
  wall-clock reads, no benchmark metadata.
- search_memories_by_time on both stores: event time is
  COALESCE(valid_from, first_seen_at, created_at), window-intersection
  match including closed [valid_from, valid_to) overlap, ordered by
  proximity to a pivot (window midpoint; open windows pivot on their
  closed edge). Same scoping discipline as the sibling search methods.
- Retrieval integration: temporal_anchor stage record in the trace
  ({source, status, window, parsed_from, candidate_count}; absent when no
  anchor parses; honest disabled statuses for minimal depth and stores
  without the method), and a temporal_anchor rank-boost list in the fused
  sources stage re-ranking already-found candidates by connector-stamped
  event dates (source_created_at, then metadata session_date/event_date/
  date, then captured_at).
- VNextRetrievalRequest.reference_time: the caller's now for resolving
  relative phrases; defaults to the current UTC time.

Tests: table-driven parser suite (69 cases), store method coverage on
both backends (scoping, ordering, boundaries, expiry, pivot), retrieval
fusion proofs (right-dated beats lexically-stronger wrong-dated; no
anchor leaves trace and behavior untouched; a wrong window cannot evict
strong hits; "before today" excludes same-day ingests). Validated against
real docker Postgres (migrated to head) and the LongMemEval coverage
probe: byte-identical per-question coverage on a 140-question slice at
reference=now, and a diagnostic with reference_time=question_date lifts
multi-session all-coverage 85.7%→100% and temporal any-coverage
94.7%→95.5% with zero worsened questions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Aggregation questions (how many / how much total / list every / each of /
in what order / which of my ... was the most) lose evidence instances to
redundant same-source memories in the pack's fixed slots. This adds a
query-surface-gated coverage mode to compile_context_pack:

- vnext_coverage_query.py (new): conservative aggregation-intent detector
  (dormant = None; query text only, never benchmark labels), clause
  decomposition, fair clause-row interleaving, and a generic post-fusion
  instance-diversity pass (near-verbatim text Jaccard and/or provenance
  group key) that only ever reorders and reselects fused candidates.
- vnext_retrieval.py: marked coverage blocks — deepen the memory pool x3
  under intent, demote same-source memory re-statements (group-key only)
  behind distinct-source instances over the whole deepened pool, backfill
  clause sub-retrieval rows behind fused winners (never score-fused),
  near-verbatim source demotion, honest trace stage (absent when dormant),
  provenance winners captured pre-diversity so the source stage stays
  decoupled. Selected sessions are a superset of the ungated pack's.

Measured (free FTS-only coverage probe, paired on identical stores):
- 55 judged-wrong multi-session questions: all-evidence coverage
  58.2% -> 67.3% (+9.1pt; second ingest seed 60.0% -> 69.1%, also +9.1pt),
  5 wins / 0 losses, evidence-session hits 123 -> 132.
- All 172 detector-fired questions: all-coverage 79.7% -> 83.1%,
  any-coverage 98.8% -> 99.4%, 6 wins / 0 losses, 0 questions lost hits.
- Naive design variants measured and rejected: RRF-fused clause lists
  (-3.7pt), source-pool deepening (-1.9pt), memory text-similarity
  demotion (evidence losses via provenance coupling).

Guards: dormant path proven byte-identical (uuid-pinned A/B test plus
hard-disabled control); eval suite passes with retrieval_quality metrics
unchanged; 1734 unit tests and the LongMemEval harness tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New eval/longmemeval/verification.py: a separate post-generation chat call
checks the answer's concrete claims against the retrieved context (never the
gold answer or any benchmark label) and, only when a LOAD-BEARING claim is
unsupported, replaces the hypothesis with the dataset-style abstention
phrasing. Off by default; enabling it stamps verify_grounding plus the
verifier model into the config fingerprint, every gated row records the
original hypothesis and the full verdict, and any verifier error or
unparseable reply fails open with the original answer byte-identical.
The official reading and judge templates are untouched and now byte-frozen
by sha256 tests; mock-client tests cover pass-through, gating, fail-open,
fingerprint disclosure, and verifier-payload hygiene.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roll-up cards pre-aggregate same-topic memory instances ("hours played -
5 instances in total: ...") so aggregation-phrased recall hits ONE
canonical card instead of needing every instance to win a pack slot.

- vnext_rollups.py (new): deterministic same-entity (vnext_entities
  substrate, alias-folded via one bulk find_entities_by_names call) and
  lexical-topic grouping - no LLM required. Optional model refinement of
  the card summary rides the existing routing seam with a grounding
  check and deterministic refusal fallback; no API key needed anywhere.
- Cards are status="candidate" proposals carrying the standard
  metadata_json.consolidation block, so the existing review surface and
  accept_consolidation_candidate handle them unchanged: acceptance
  promotes the card to a first-class FTS/vector-indexed memory and
  supersedes NOTHING - members stay individually recallable. A new
  member in an accepted topic produces a REVISION proposal whose
  acceptance retires only the previous card, never the members.
- The pass rides the scheduled memory_consolidation workflow (never the
  commit path); a SQL-trace test proves the commit statement sequence
  is identical with and without roll-up state present.
- Near-duplicate clusters are excluded from grouping so the dedup/merge
  pipeline keeps owning them; a Jaccard guard covers provider-less runs.
- No migration: cards reuse the memories table (candidate rows + JSON
  value/metadata) and the existing memory_consolidation artifact type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Strict lexical search cannot bridge category-phrased questions to
instance-phrased memories: "charity event fundraising total" shares no
token with a memory that only says "Bike-a-Thon raised $5,000". This
gives every memory a capped set of derived retrieval keys and folds
them into full-text search on both backends, weighted so derived keys
make rows FINDABLE without outranking direct text matches.

- vnext_fact_keys.py (new): derive_fact_keys with two tiers --
  (a) deterministic entity/attribute recombination (generic hypernym
  lexicon, currency/percent/unit phrasings, shallow value attribute
  pairs, linked-entity names/aliases/type words, human-authored
  memory_key path words) and (b) an optional OpenAI-compatible chat
  endpoint behind the ALICE_FACT_KEYS_BASE_URL/_MODEL/_API_KEY env
  seam, mirroring vnext_embeddings (unconfigured => tier (a) only, no
  network anywhere; provider failures degrade to tier (a) and log
  memory.fact_keys_failed). Keys are sanitized indexable text only,
  capped at 8 keys x 80 chars, novelty-filtered against the memory's
  own indexed text.
- Migration 20260707_0082 + sqlite_schema: nullable memories.fact_keys
  (NULL = never derived, '' = derived, nothing to add). Postgres
  search_tsv is dropped and re-added with a setweight 'D' term
  (ts_rank default weight 0.1 vs title's 1.0) and the GIN index is
  rebuilt; ADD COLUMN computes it for existing rows. SQLite
  memories_fts gains a fact_keys column at bm25 weight 0.1, and
  bootstrap now drops-and-recreates an FTS table (plus its sync
  triggers) whose column set predates the current DDL, so pre-existing
  database files upgrade in place with the one-shot rebuild.
- Stores: update_memory_fact_keys / list_memories_missing_fact_keys on
  both backends, placed beside the embedding indexing surface; search
  method signatures unchanged. True redaction now clears fact_keys
  alongside the embedding -- derived keys echo what the memory said.
- Write path: one marked integration line in the memory-commit
  post-create hook, pinned to the deterministic tier so commits never
  make a synchronous model call. Measured +~0.12ms per commit, flat
  across 0/1k/5k-row tables (no O(N) work). Capture/promotion sites
  are owned by a parallel workstream; until wired, those rows are
  covered by the backfill.
- Backfill: scripts/backfill_memory_fact_keys.py (documented one-shot;
  sqlite:/// and postgresql:// URLs; --deterministic-only flag).

Verified: full unit suite 1761 passed; full live-Postgres integration
suite passed including a real 0081->0082->0081 migration round trip
with pre-existing rows; LongMemEval harness tests pass; retrieval
quality eval suite (ALICEBOT_EVAL_DATABASE_URL=sqlite:///:memory:,
--suite all) has statuses, failing-case sets, and non-latency metrics
byte-identical to main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	apps/api/src/alicebot_api/vnext_retrieval.py
#	tests/unit/test_vnext_retrieval.py
# Conflicts:
#	eval/longmemeval/test_harness.py
# Conflicts:
#	eval/longmemeval/adapter.py
#	eval/longmemeval/test_harness.py
- adapter passes the question's own date as the temporal-anchor
  reference clock (official benchmark input the reading template
  already presents), unlocking relative-phrase anchors
- fact keys attach at both promotion moments (product review-accept in
  mcp_tools, harness candidate promotion), deterministic tier only so
  neither path ever makes a synchronous model call
- tests/unit/test_vnext_retrieval.py rebuilt via three-way merge +
  anchored splicing after interleaved-insertion conflicts tore two
  functions across hunk boundaries

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hygiene, capture cleanup, pack provenance

Fix 1 (eval/longmemeval/adapter.py): query-anchored excerpts now require
enumeration shape for EVERY anchor move (cross-chunk prose anchors were
displacing the head-biased best chunk that carried the answer), and the
window grew a symmetric UPWARD enumeration extension so a list cut at its
top edge recovers items 1..k-1 into the pass-2 pool.

Fix 2 (vnext_coverage_query.py): coverage-mode memory diversity key
refined from bare source_id to (source_id, source_chunk_id) with a
source-id fallback -- different turns of one session are distinct facts,
not restatements.

Fix 3 (vnext_fact_keys.py): _value_attribute_keys skips provenance and
identifier attributes (source_id/source_chunk_id, *_id/*_ids names) and
uuid/hex-shaped string values, so captured rows stop indexing
"source chunk id <uuid>" garbage into FTS (was 897/897 memories).

Fix 4 (vnext_capture.py): deleted the dead user-asserted/assistant-
estimate confidence deltas (pack ranking never reads confidence; the
promotion-rank ordering stays), and user-asserted-value promotions now
dedupe against existing canonical memory texts store-wide instead of
batch-locally.

Fix 5 (adapter.py RetrievalOutcome): checkpoint rows persist pack
provenance -- retrieved source session ids, selected memory ids, and the
sha256 of the rendered context block (ids + hash, never the text) -- so
future paired flips are offline-attributable. Config fingerprint
unchanged.

Offline verification: loss replays recover 2c63a862 (2/15 turn),
3e321797 (tomato list items via upward extension), 0a34ad58 (Suica
ticket-gate head chunk), 89941a93 partial ("hybrid bike" row back);
16-question gain-guard sweep shows zero evidence regressions; fresh
ingests carry zero uuid fact-key echoes across 10 spot stores; official
reading/judge templates byte-identical; full unit suite (1950), harness
(80), and eval battery all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@samrusani samrusani merged commit 076690d into main Jul 10, 2026
8 checks passed
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