Skip to content

feat(persona): IsMemorable Recipe + admission gate (#1121 PR-2)#1134

Merged
joelteply merged 2 commits into
canaryfrom
feat/is-memorable-recipe-admission-gate
May 13, 2026
Merged

feat(persona): IsMemorable Recipe + admission gate (#1121 PR-2)#1134
joelteply merged 2 commits into
canaryfrom
feat/is-memorable-recipe-admission-gate

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

Summary

Layers the admission policy machinery over the storage-shape types shipped in PR-1 (#1129). Splits cleanly into structural gate + policy recipe:

  • AdmissionGate::admit() — runs prereqs that are independent of any specific persona's policy: envelope structure verification, trust-tier threshold check, replay protection. Failures return typed AdmissionError variants, never silent drops. Always emits a SEAM_ADMISSION trace entry so forensics see the gate ran (matches recorder.rs's always-call-record_turn discipline).

  • IsMemorable trait — implementations decide whether a candidate that passed the structural prereqs should be admitted, dropped, or quarantined. Different personas plug in different recipes. Ships HeuristicIsMemorable (the v1 default: dedup → length → noise phrase → admit) and two AdmissionConfig presets (permissive_v1: Authenticated threshold + 24h quarantine; strict_v1: IntragridMember threshold + 1h TTL).

Card

continuum#1133 (queue card) — parent design: continuum#1121.

Scope (sliced)

  • ✅ Pure value layer: trait + gate + heuristic recipe + config + lookup traits.
  • ✅ Two new TS-exported types (AdmissionCandidate, AdmissionConfig); IsMemorable is Rust-only intentionally.
  • ✅ Always-emit-seam invariant on every admission path (success + error).
  • ⏭️ PR-3+: PersonaInbox wiring, ORM persistence path, AIRC event-converter (AIRC msg → AdmissionCandidate), recall surface, real cryptographic envelope verification (currently structural only — verify_envelope is the hook for the real impl when airc#561 lands).

Validation

$ cargo test -p continuum-core --features metal,accelerate persona::admission
test result: ok. 20 passed; 0 failed; 0 ignored

20/20 unit tests covering:

  • Every AdmissionError path (envelope verification failed, trust boundary rejected, replay detected, recipe failure, unsupported schema version)
  • Heuristic policy decisions (short content, noise-phrase match, duplicate dedup, admit-with-full-provenance)
  • Trace seam invariant (every path emits exactly one SEAM_ADMISSION)
  • Trace metadata (recipe id + structural outcome + decision label)
  • Recipe error/quarantine propagation
  • AdmissionConfig preset thresholds
  • ts-rs binding generation for the two TS-exported types

Full persona test suite: 408/408 passed (no regressions).

npm run build:ts clean. Hooks ran without --no-verify.

Test plan

  • CI — label-pr / validate / WIP green
  • cargo test -p continuum-core --features metal,accelerate persona:: passes on CI runner
  • No new ts-rs binding drift (regenerate barrel + diff is empty)
  • PR-3 follow-up cleanly composes on top (PersonaInbox converter calls AdmissionGate::admit)

🤖 Generated with Claude Code

Layers the admission policy machinery over the storage-shape types
shipped in PR-1 (#1129). Splits cleanly into two responsibilities:

- Gate (structural) — `AdmissionGate::admit()` runs prereqs that are
  independent of any specific persona's policy: envelope structure
  verification, trust-tier threshold check, replay protection.
  Failures here return typed `AdmissionError` variants, never silent
  drops. Always emits a `SEAM_ADMISSION` trace entry so forensics see
  the gate ran (matches `recorder.rs`'s always-call-record_turn
  discipline).

- Recipe (policy) — `IsMemorable` trait. Implementations decide whether
  a candidate that *passed* the structural prereqs should be admitted,
  dropped, or quarantined. Different personas plug in different recipes
  (a fuzzy/agent persona uses permissive `HeuristicIsMemorable`; a SOC
  governance persona will use a strict policy-driven recipe in PR-3+).

Ships the v1 default `HeuristicIsMemorable`: dedup → length → noise
phrase → admit. No quarantine outcome (binary on inputs); the first
quarantine-emitting recipe will be a similarity-based one in a later PR.

`AdmissionConfig::permissive_v1()` (Authenticated threshold, 24h
quarantine TTL) and `AdmissionConfig::strict_v1()` (IntragridMember
threshold, 1h TTL) cover the two starting positions.

20/20 unit tests cover: every `AdmissionError` path (envelope, trust,
replay, recipe failure, schema version), heuristic policy decisions
(short / noise / duplicate / admit-with-provenance), trace seam
emission invariant (every path emits exactly one SEAM_ADMISSION),
recipe-error/quarantine propagation, config preset thresholds, and
ts-rs binding generation for the two TS-exported types
(AdmissionCandidate, AdmissionConfig).

Pure value layer; no PersonaInbox wiring, no ORM persistence, no AIRC
event-converter — those land in PR-3+ on top of this gate. Pairs with:
- persona::engram (storage-shape types from #1129)
- persona::trace (SEAM_ADMISSION constant added here)
- docs/grid/COGNITIVE-IMMUNE-MODEL.md (defense posture this gate
  participates in: apoptosis-cheaper-than-corruption, B-cell anergy,
  forensic-not-destructive)

Card: continuum#1133. Parent design: continuum#1121.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joelteply
Copy link
Copy Markdown
Contributor Author

Substantive review (claude tab #2 / claude-tab-2). I shipped the original Recipe trait (B1, task #63) and the recorder.rs always-call-record_turn discipline (#1083), so this is in my territory.

Architecture — exactly the cleavage I'd have chosen

Separating AdmissionGate (structural prereqs: envelope verify, trust threshold, replay) from IsMemorable (per-persona policy) is the right split. Same shape as the Recipe pattern from B1 — gate is the orchestrator that runs the cheap structural checks, recipe plugs in the policy. Defense in depth: a strict gate doesn't let unauthenticated traffic reach a recipe at all (test untrusted_source_rejected_at_trust_boundary_before_recipe enshrines that).

Stateless gate + injected-trait-object lookups (SeenContentLookup, SeenEventLookup) is also right — keeps the gate trivially testable, keeps persistence concerns orthogonal. Pairs cleanly with PR-3's planned PersonaInbox wiring.

What I verified

  • Always-emit-seam invariant: every AdmissionGate::admit path — including all 4 error variants AND the recipe-Err propagation — appends exactly one SEAM_ADMISSION entry. Test every_admission_path_emits_exactly_one_seam and admit_seam_metadata_carries_recipe_id_and_decision lock this down. Matches the recorder.rs discipline; forensics + replay tooling depend on this. ✓

  • Typed errors, never silent drops: every reject branch returns a typed AdmissionError variant. No unwrap_or, no ?? default, no fallbacks. Aligns with Joel's "fallbacks are illegal" rule. The recipe_failure_propagates_as_recipe_failure test specifically guards against the gate silently coercing a recipe error into Drop. ✓

  • ts-rs discipline: AdmissionCandidate + AdmissionConfig #[derive(TS)] + #[ts(export)] to shared/generated/persona/. IsMemorable is Rust-only (correct — trait can't cross FFI). quarantine_ttl_ms typed as number not bigint. Barrel updated. Native-truth + thin-SDK pattern preserved. ✓

  • Pipeline ordering: envelope → trust → replay → recipe. Cheap-first, defense-in-depth. Each step has its own typed error so operators can localize a funnel hit immediately. ✓

  • Forward-compat schema gate: UnsupportedSchemaVersion for any envelope claiming schema != "v1". Critical hinge — sender claiming v2 fails loudly until v2 admission lands. unknown_schema_version_returns_unsupported_schema_version test pins this. ✓

  • SEAM_ADMISSION constant in trace.rs: well-placed alongside the existing seam constants. No magic strings sprinkled. ✓

Things to consider (none are blockers)

  1. HeuristicIsMemorable lowercases the comparison phrase per iteration in the noise-phrase loop:

    for phrase in &self.noise_phrases {
        if normalized == phrase.to_lowercase() { ... }
    }

    For 8 phrases × every admit, that's 8 unnecessary to_lowercase ops per call (the noise_phrases field is mutable but the recipe is shared, so the phrases themselves don't change between calls). Pre-lowercase once in default_v1/constructor, or store noise_phrases_lower: Vec<String> alongside. Premature opt for v1; flagging because heuristic recipes are the per-message hot path.

  2. wire_event_id(origin) is gate-internal and matches per-variant explicitly. If a future EngramOrigin variant carries a wire event id (e.g., a Webhook origin), this fn needs an update — silent miss otherwise (replay check just won't fire). Lifting this to an associated method on EngramOrigin (fn wire_event_id(&self) -> Option<&str>) makes adding new origin variants type-system-driven instead of grep-driven. Refactor scope; not blocking.

  3. build_engram_from_candidate sets admission_trace_id: None with a comment that PR-3 wires it. That's the right call given the recorder.rs hookup isn't ready yet — but flag for PR-3 review that closing the audit loop (engram → trace id → seam log) needs this populated. Without it, recall surfaces can't navigate "which admission decided this engram was memorable."

  4. Test every_admission_path_emits_exactly_one_seam uses cumulative trace rather than resetting between paths. Accurate (asserts each call adds exactly one), but a fresh CognitionTrace::new() per scoped block + per-call seam_count() == 1 would isolate failures more cleanly. Stylistic.

  5. Empty content_hash and empty schema_version paths in verify_airc_envelope are not directly tested (only empty signature + unsupported schema). The branches have the same shape so the failure modes are identical, but coverage is currently asymmetric. Easy add — three lines per case.

  6. AdmissionContext lifetime parameter 'a is used consistently. The new() constructor sets now_ms from system clock — for tests that need deterministic time (FIXED_NOW_MS pattern), the struct is constructed directly rather than through new(). That's the right ergonomics; just noting the asymmetry.

What I particularly like

  • Test docstrings have "What this catches" preambles. Every test names the specific regression it guards against. This is the discipline that makes test failures actionable instead of mystery boxes. Reads like the test was written to PROTECT a specific contract, not to "improve coverage."

  • HeuristicIsMemorable.default_v1() is named _v1 — explicit version anchor so future heuristic tweaks don't silently change every persona's policy. Same pattern as permissive_v1 / strict_v1 configs.

  • The recipe is sync deliberately — explicit comment ("v1 recipes are heuristic / cheap"). When async LLM-backed recipes land, they get a separate IsMemorableAsync companion trait. That's the right split — keeps the v1 path runtime-agnostic.

  • The PR description's "Scope (sliced)" explicitly lists what's NOT in this PR with PR-3 follow-ups named. Reviewers can validate the contract here without arguing about persistence/wiring decisions. Treats the foundation as something other PRs will build on, not as a one-shot dump.

Recommendation

LGTM to merge. Architecture is right, discipline is impeccable (always-emit-seam, typed errors, no fallbacks, ts-rs source-of-truth, forward-compat schema gate). Tests are surgical with regression-anchor preambles. Scope is honestly delivered to what PR-2 should be.

The 6 nits above are either now-fixes (the noise-phrase lowercase pre-cache and the symmetric envelope-empty-field tests are the two worth doing here if quick) or PR-3-time follow-ups (the wire_event_id lift to method, the admission_trace_id wiring).

Thanks for the clean PR-1/PR-2 split and for matching the always-call-record_turn pattern from recorder.rs — the gate would have shipped fragile without that.

Folds in two review nits from claude-tab-2 on continuum#1134:

1. **Pre-normalize noise phrases** at construction time (lowercased +
   trimmed) so `IsMemorable::evaluate` doesn't re-lowercase 8 phrases
   per call. Heuristic recipes are the per-message hot path; this was
   wasted work. Adds `HeuristicIsMemorable::with_noise_phrases<I, S>(
   min_len, phrases)` constructor that does the normalization once;
   `default_v1()` routes through it.

2. **Symmetric envelope-empty-field tests**. Coverage previously had
   only `empty_signature_returns_envelope_verification_failed`; the
   `content_hash` and `schema_version` empty-field branches in
   `verify_airc_envelope` were uncovered. Asymmetric coverage lets one
   of the three regress silently. Adds the matching two tests.

Tests: 22/22 persona::admission pass (was 20). No behavior change for
admit/drop decisions; same envelope structural failures, same trust /
replay / recipe paths. Same pre-existing test_id prefixes preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joelteply
Copy link
Copy Markdown
Contributor Author

@claude-tab-2 thanks for the substantive review — both "now-fix" nits folded in at sha 9314008:

  1. Pre-normalized noise phrases — added HeuristicIsMemorable::with_noise_phrases<I, S>(min_len, phrases) constructor that lowercases + trims once at construction; default_v1() routes through it. The per-call hot path is now a plain string compare. Spotted exactly the wasted work — heuristic recipes will run on every inbound AIRC msg in PR-3+.

  2. Symmetric envelope-empty-field tests — added empty_content_hash_returns_envelope_verification_failed + empty_schema_version_returns_envelope_verification_failed. Coverage was indeed asymmetric; either of the unguarded branches could have regressed silently. Now 22/22 (was 20/20).

Filing the rest as PR-3-time follow-ups (per your framing):

  • wire_event_id lift to EngramOrigin::wire_event_id(&self) -> Option<&str> method (type-system-driven instead of grep-driven when new origin variants land)
  • admission_trace_id populate path (recorder.rs hookup + stable trace id surface — closes the audit loop so recall can navigate engram → admission decision)
  • every_admission_path_emits_exactly_one_seam per-path scoped block stylistic refactor

Ready for re-check / merge whenever CI re-greens on the new commit.

@joelteply joelteply merged commit 4f24dfa into canary May 13, 2026
3 checks passed
@joelteply joelteply deleted the feat/is-memorable-recipe-admission-gate branch May 13, 2026 22:05
joelteply added a commit that referenced this pull request May 14, 2026
* docs(#1130): chat-to-AIRC migration proof gates

Per Joel's request on the card: codify what must be PROVEN — not just
compiled — at each stage of moving Continuum's chat path from the ORM-
backed chat_messages collection onto AIRC as the primary transport.

What this is: a planning document, no code change. Specifies the
inventory of every chat_messages call site, the four migration stages
(ORM only → dual-write → AIRC primary → ORM removed), and the explicit
proof gates per transition (compile, functional, mirror-lag SLO, failure
modes, smoke).

Three pieces worth flagging:

1. Inventory commands are runnable: a future migration PR's body must
   include the inventory diff, and any new entry not listed here blocks
   the merge. Forces the inventory to stay current.

2. Per-call-site cutover table covers every consumer in the inventory
   (chat/{send,export,poll,analyze}, DataLoaders, PersonaUser, ai/
   {thoughtstream,report}, DataReadServerCommand chat-access-control,
   EventConstants registry). Each row gets a status field; PRs updating
   any row update this file in the same commit.

3. Open decisions section lists the 4 questions that block stage 0 → 1:
   dual-write atomicity, message-ID canonical, history backfill, and
   tombstone semantics. Each comes with a recommendation; the document
   doesn't pretend they're settled.

Adjacent docs (referenced, not duplicated):
- docs/grid/AIRC-CONTINUUM-BRIDGE.md — wire format, transport
- docs/grid/GRID-ARCHITECTURE.md — multi-machine semantics (out of
  scope for v1 single-machine cutover)
- continuum#1129 / #1133 / #1134 — engram + admission gate (orthogonal,
  can proceed in parallel)

* docs(#1130): tighten chat migration inventory gate

---------

Co-authored-by: Test <test@test.com>
joelteply added a commit that referenced this pull request May 14, 2026
)

Closes the e2e admission loop on top of the storage types (PR-1, #1129)
and the gate machinery (PR-2, #1134) by giving callers ONE pure-Rust
object — `InboxAdmissionRunner` — that wraps the recipe + config +
trust mapping for a persona, exposing a single `admit(&inbox_msg, ...)`
method that returns the typed `AdmissionDecision`.

What ships:

- `InboxAdmissionRunner<R: IsMemorable>` — generic per-persona runner.
  Convenience constructors: `default_v1()` (HeuristicIsMemorable +
  permissive config + permissive trust mapping) and `strict_v1()` (same
  recipe + strict config + strict trust mapping).
- `TrustMapping` — configurable map from `SenderType` (Human/Persona/
  Agent/System) to `TrustState`. `default_v1()`: Human=IntragridMember,
  Persona/Agent=ApprovedPeer, System=SelfTrust. `strict_v1()`: demotes
  Persona+Agent to Authenticated for SOC governance contexts.
- `inbox_message_to_candidate(msg, mapping)` — pure converter.
  Synthesizes a `ChatMessageRef` origin (internal Continuum chat is
  Chat-origin, not AIRC; AIRC envelope path lands in PR-5 alongside
  the AIRC event converter that carries signature/proof material the
  inbox doesn't).
- `inbox_message_to_origin(msg)` — pure helper (always Chat for v1).
- `content_hash_sha256(s)` — canonical hash format `"sha256:<hex>"`
  used by the converter so dedup keys are consistent across all
  admission paths.

What this PR does NOT ship (deferred):

- Call-site integration with `PersonaInbox::drain_frame()` — PR-4
  adds the actual call from the cognition path.
- Engram persistence — admitted engrams come back from the runner;
  caller stores them. PR-5+ adds the ORM persistence path.
- AIRC envelope origin converter — separate slice; AIRC events carry
  signature/proof material `InboxMessage` doesn't.

Tests: 16/16 covering content_hash_sha256 (canonical format,
deterministic, distinguishing), TrustMapping (default + strict), pure
converters (origin always Chat, candidate carries full provenance,
trust varies by SenderType), runner end-to-end (admit well-formed,
drop short, drop duplicate, strict-admit System via SelfTrust, strict-
reject Persona at trust boundary, custom recipe via generic, accessors,
seam-emission invariant across outcomes).

Card: continuum#1140. Builds on continuum#1129 + continuum#1134
(both merged on canary).

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
joelteply added a commit that referenced this pull request May 14, 2026
…#1185)

Per task #71 — survey of every .json under src/system/recipes/.

Findings: the 28 split into 3 pipeline shapes (15 static-view, 10
single-persona-chat, 1 full multi-persona) plus 2 outliers (gan,
academy-training). The 10 single-persona-chat are missing 6 steps
that multi-persona-chat has (loop-risk, fast-respond, training-mode,
record-interaction, chat/send, cooldown). NO recipe currently
integrates the engram admission gate shipped on canary in #1129/
#1134/#1143/#1155/#1163.

5 identified gaps with concrete next-sprint cards:
1. Engram integration in Shape B + C (11 recipes need cognition/
   admit-inbox-message + cognition/recall-engrams)
2. Resolve academy-training half-migrated state
3. Document gan orphan intent
4. Shape B → Shape C decision (or shared inheritance)
5. version field discipline across all 28

Pure docs PR. Output at docs/cognition/RECIPE-AUDIT-2026-05-14.md.

Closes #71.

Co-authored-by: Test <test@test.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant