Skip to content

feat: separate recall admission profiles and signals - #184

Merged
ezutfen merged 3 commits into
mainfrom
feat/160-recall-signals
Sep 7, 2026
Merged

feat: separate recall admission profiles and signals#184
ezutfen merged 3 commits into
mainfrom
feat/160-recall-signals

Conversation

@ezutfen

@ezutfen ezutfen commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

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_score multiplier (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.py defines the versioned recall-profiles-v1 contract:

Profile Corpus window Admission gate Ranking Budget caps Expansion Serving status
legacy active + proposed none semantic-v3 blend none yes authoritative
governed active + governed-disputed stay kinds yes semantic-signals-v1 none no shadow-only
exploratory active + proposed yes semantic-signals-v1 item 20 / byte 2048 no shadow-only

CERTIFIED_SERVING_PROFILES is currently { "legacy" }. Ordinary POST /v1/recall therefore serves only the certified legacy semantic packet (startup remains its own profile). Explicit governed/exploratory serving requests fail closed with 422; an uncertified recall_default_profile is refused rather than honored. Configuration alone cannot promote a candidate profile into production authority. #162D/#176 remains NOT_CERTIFIED.

Authorized shadow comparison

POST /v1/recall/shadow-compare compares the authoritative legacy packet with governed/exploratory candidates without changing what is served.

The surface:

  • requires REVIEW_SCOPE;
  • additionally requires tenant opt-in via tenant_config.recall_profile_shadow_enabled (migration 041, default FALSE);
  • writes no recall_logs row;
  • increments no recall/exposure counters;
  • writes no receipts;
  • enqueues no jobs;
  • feeds no promotion/evidence input.

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.py adds recall-signals-v1 / recall-admission-v1. Governed/exploratory candidate items expose separate:

  • relevance_score;
  • utility_score;
  • epistemic_state (supported | contested | insufficient_evidence | unknown);
  • structured warning_codes;
  • admission block with profile, decision, reason codes, policy version, and durable assessment binding.

There is no blended trust_score on the signal path. Utility is 0.7 * importance + 0.3 * freshness, ranking is similarity * (0.5 + 0.5 * utility), and admission never consumes similarity, importance, or exposure counters. The legacy memory_confidence source-policy prior is not emitted as a generic epistemic low_confidence warning.

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_admissions resolver. Persisted assessments remain readable when ENGRAM_ADMISSION_ASSESSMENT_CAPTURE_ENABLED=false; disabling capture stops new capture but does not erase enforcement of existing blocked/stale projections.

Assessment precedence is centralized:

  • blocked withholds in every candidate profile/review-status branch;
  • governed fails closed on stale assessments, including disputed stay-kind items;
  • exploratory may mark stale state but never treats it as trusted authority.

Audit and compatibility

  • Migration 040 adds recall_logs.recall_profile.
  • Historical rows are reconstructed truthfully from existing mode: startup → startup, semantic → legacy; replay self-heals databases that applied the earlier migration revision.
  • Migration 041 adds the fail-closed tenant shadow-inspection policy.
  • SDK response/request types preserve the additive profile fields.
  • MCP intentionally exposes no candidate-profile/shadow selector.
  • Legacy semantic recall remains behavior-compatible: same active+proposed window, trust_score, semantic-v3 ranking, and relationship expansion.

Scope / deferred #160 work

This PR does not complete #160. The issue remains open for follow-up slices including:

No production-default change is authorized here.

Verification

Final reviewed head: 4fd3c56ae1b760c6126a96635ffc913295106086.

  • make compose-ci: 3825 passed.
  • Exact-head GitHub CI: run #522, success.
  • Runtime image smoke: /health and /ready pass.
  • Real PostgreSQL/RLS shards pass.
  • Regression coverage includes certification-gated serving, reviewer+tenant shadow authorization, read-only shadow evaluation, assessment-capture rollback, stale/blocked precedence, pre-LIMIT eligibility, neutral retrieval, truthful migration backfill, legacy compatibility, exposure-loop separation, and the candidate-only-corpus shadow case.

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.
@ezutfen
ezutfen merged commit d70ec51 into main Sep 7, 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