feat: separate recall admission profiles and signals - #184
Merged
Conversation
Introduce explicit recall profiles (legacy/governed/exploratory) for semantic recall so admission stops being an implicit active+proposed blend ranked by a single trust_score multiplier. - engram/recall_profiles.py: versioned profile registry (contract recall-profiles-v1). legacy preserves pre-#160 behavior byte-for-byte and stays the safe default; governed admits only the reviewed corpus (active + governed-disputed stay kinds); exploratory admits proposals as machine-readable unknown evidence under tighter budget caps. - engram/recall_signals.py: separated signal model (recall-signals-v1 / recall-admission-v1): relevance-dominant rank with utility as the ordering term, epistemic state (supported/contested/insufficient_evidence/unknown), structured warning codes, and an admission gate that reads governance state only and never similarity/importance/exposure. Served items carry relevance_score, utility_score, epistemic_state, warning_codes, and an admission block bound to the durable admission assessment; no blended trust_score on this path. - admission_assessment.resolve_bulk_admissions: shared digest-verified bulk resolution; the review queue now uses it too, so recall admission and review can never disagree about staleness. Governed withholds on stale/blocked; exploratory marks stale, honors blocked. - POST /v1/recall: additive recall_profile request field and recall_profile/signals_version/omitted_by_admission response fields; 422 for unknown or mode-incompatible profiles. SDK mirrors the additive fields; the MCP tool intentionally exposes no profile parameter so exploratory cannot be smuggled through defaults. - recall_logs.recall_profile (migration 040, backfilled 'legacy') records the effective profile per packet. - Relationship expansion remains legacy-only: admission must precede graph/tunnel expansion and the expansion rescorer still speaks the blended score (documented in docs/adr-160-recall-profiles.md). - Tests: pure unit suites for the registry and signal invariants (importance never moves epistemic state or admission; admission never sees relevance/utility), plus PostgreSQL integration tests covering governed exclusion of a highly similar proposal, exploratory marking, stale-assessment withholding/marking, legacy compatibility, budget caps, validation, audit rows, and determinism.
Correction pass on PR #184 (issue #160 / ENG-RECALL-003): * Serving gate: recall_profiles.CERTIFIED_SERVING_PROFILES (legacy-only) is the single choke point for authoritative serving. Requesting governed/exploratory on POST /v1/recall is a 422; an uncertified recall_default_profile is refused with a warning, never honored — configuration alone cannot promote a candidate profile. * Shadow comparison surface: POST /v1/recall/shadow-compare evaluates legacy vs governed/exploratory read-only (no recall_logs row, no exposure counters, no promotion/evidence inputs) via the shared evaluate_semantic_profile core. REVIEW_SCOPE capability AND tenant_config.recall_profile_shadow_enabled (migration 041, default FALSE) are both required; tenant denial wins and absent config fails closed. * Assessment reads: load_admission_bindings no longer conditions on admission_assessment_capture_enabled — disabling capture (the #159 rollback) stops new capture but persisted blocked/stale projections keep enforcing recall. * Central assessment precedence: blocked withholds on every profile/review-status branch (including stale+blocked and exploratory); strict-stale withholds for governed on active AND disputed stay-kind items; exploratory marks instead. * Pre-LIMIT corpus eligibility: signal profiles retrieve through the new neutral semantic.retrieve_candidates primitive (no trust blend) with the full mechanically-expressible predicate (active OR disputed stay-kind) applied in SQL before the bounded HNSW window, so ineligible disputed rows cannot starve eligible active memories. * Removed the misleading low_confidence warning derived from memory_confidence (a source-policy prior, not epistemic confidence) from the separated-signal path. * ADR-160 rewritten around the shadow-only rollout boundary with the deferred #160 follow-ups kept explicit; SDK/MCP docs updated. make check: lint + strict mypy clean; root suite green except 37 failures byte-identical to HEAD under the local ad-hoc DB (privilege tests needing the compose roles). make compose-ci: 3823 passed. Runtime-image smoke: /health + /ready 200.
…40 backfill Two follow-up corrections on PR #184 (issue #160): * Shadow comparison preflight no longer gates on the legacy corpus alone. It counts every requested packet's eligible corpus (legacy baseline + each candidate) through the shared _profile_candidate_count helper and generates the embedding iff any is non-empty. A tenant whose only eligible item is a disputed governed stay kind now gets a truthful comparison — legacy evaluates to an empty packet (its window is active + proposed) while governed evaluates the item. The top-level candidate_count reports the union; each packet keeps its own count. * Migration 040 reconstructs historical profiles from the mode rows already record: startup rows backfill 'startup' (exactly what new startup logs write), semantic rows 'legacy'. The unconditional UPDATE also self-heals databases that applied the earlier revision of 040 that mislabeled startup rows as 'legacy'. * De-flaked test_doctor_postgres's slightly-overdue job test: it froze NOW at module import, so any suite run longer than the lease-stale threshold turned its 10-second-overdue job into a warn. It now takes a fresh timestamp (pre-existing flake surfaced by a long loaded run). make compose-ci: 3825 passed. Runtime smoke: /health + /ready 200.
This was referenced Sep 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #160 (ENG-RECALL-003, parent #153). This is the first implementation slice: explicit recall-profile contracts, separated recall signals, and a bounded shadow-comparison path. No uncertified recall profile becomes authoritative in this PR.
Problem
Semantic recall previously blended semantic relevance, importance/popularity, source priors, and review status into a single
trust_scoremultiplier (similarity * trust_score) and silently meant “active plus proposed”. That allowed highly similar proposals with unknown evidence into ordinary packets and let utility/popularity raise a value presented as trust. #157–#159 added the evidence/admission substrate, but recall did not consume or expose it cleanly.What changed
Recall profiles and certification boundary
engram/recall_profiles.pydefines the versionedrecall-profiles-v1contract:legacysemantic-v3blendgovernedsemantic-signals-v1exploratorysemantic-signals-v1CERTIFIED_SERVING_PROFILESis currently{ "legacy" }. OrdinaryPOST /v1/recalltherefore serves only the certified legacy semantic packet (startup remains its own profile). Explicit governed/exploratory serving requests fail closed with 422; an uncertifiedrecall_default_profileis refused rather than honored. Configuration alone cannot promote a candidate profile into production authority. #162D/#176 remainsNOT_CERTIFIED.Authorized shadow comparison
POST /v1/recall/shadow-comparecompares the authoritative legacy packet with governed/exploratory candidates without changing what is served.The surface:
REVIEW_SCOPE;tenant_config.recall_profile_shadow_enabled(migration 041, defaultFALSE);recall_logsrow;The comparison runs whenever any requested profile has an eligible corpus. Legacy may correctly evaluate to an empty packet while governed evaluates a candidate-only corpus (for example, a disputed governed stay-kind).
Separated signal model
engram/recall_signals.pyaddsrecall-signals-v1/recall-admission-v1. Governed/exploratory candidate items expose separate:relevance_score;utility_score;epistemic_state(supported | contested | insufficient_evidence | unknown);warning_codes;There is no blended
trust_scoreon the signal path. Utility is0.7 * importance + 0.3 * freshness, ranking issimilarity * (0.5 + 0.5 * utility), and admission never consumes similarity, importance, or exposure counters. The legacymemory_confidencesource-policy prior is not emitted as a generic epistemiclow_confidencewarning.Neutral retrieval and pre-LIMIT eligibility
Signal profiles use
semantic.retrieve_candidates, a neutral vector retrieval primitive that returns relevance/embedding identity without the legacy trust blend.Mechanically expressible corpus eligibility is applied in SQL before the bounded HNSW
LIMIT. For governed recall that means:active OR (disputed AND kind is configured to stay in recall while disputed)plus existing tenant/visibility/workspace/validity/embedding/RLS constraints. Ineligible disputed rows therefore cannot consume the bounded relevance window and starve eligible memories.
Durable admission assessment handling
Recall uses the shared digest-verified
admission_assessment.resolve_bulk_admissionsresolver. Persisted assessments remain readable whenENGRAM_ADMISSION_ASSESSMENT_CAPTURE_ENABLED=false; disabling capture stops new capture but does not erase enforcement of existing blocked/stale projections.Assessment precedence is centralized:
blockedwithholds in every candidate profile/review-status branch;Audit and compatibility
recall_logs.recall_profile.mode: startup →startup, semantic →legacy; replay self-heals databases that applied the earlier migration revision.trust_score,semantic-v3ranking, and relationship expansion.Scope / deferred #160 work
This PR does not complete #160. The issue remains open for follow-up slices including:
semantic_exploratory,semantic_governed,startup) as the candidate policy authority;No production-default change is authorized here.
Verification
Final reviewed head:
4fd3c56ae1b760c6126a96635ffc913295106086.make compose-ci: 3825 passed./healthand/readypass.