feat(continuum-core/persona): L0-2-respond-context — required ResponderConfig, NeedsResponse outcome, no empty defaults#1467
Merged
joelteply merged 1 commit intoMay 29, 2026
Conversation
…erConfig, NeedsResponse outcome, no empty defaults Reworked from the earlier L0-2-respond attempt (#1466, self-closed) after auditing three doctrine violations: 1. std::Mutex held across respond().await — blocks status/enroll/other personas' ticks for the full inference roundtrip 2. Empty-default fields on RespondInput (model: String::new(), etc.) wrapped as 'fail loudly at inference' — that's the silent-default- substitution pattern this migration is deleting on the TS side 3. RespondError as Ok outcome — circuit breaker never trips on repeated inference failures (silent degradation) This slice fixes them all by SHRINKING the scope: no respond() call yet. That's the next slice, which can rely on RespondInput being honestly constructed. What this slice does: - New ResponderConfig struct (model, system_prompt, capabilities, specialty). All required at enrollment time; validated non-empty with named errors for model + specialty - EnrolledPersona extends with responder_config field - enroll signature requires ResponderConfig as a parameter; rejected enrollments don't mutate state (validate before lock) - persona/enroll command parses model/system_prompt/specialty/ capabilities from JSON params; requires model loud - ServiceOnceOutcome updated: - SilentByDecision { message_id, decision } — gate said no - NeedsResponse { message_id, decision, respond_input } — gate said yes; respond_input is fully-formed from real config - UnsupportedItem unchanged - Idle unchanged - Evaluated REMOVED - service_once_for: pops + evaluates; if should_respond, builds RespondInput from real persona config + per-message context; no empty-string defaults - build_respond_input populates EVERY required field from responder_config + the chat wire. The genuinely-empty Vec fields (recent_history, known_specialties, other_persona_names, message_media, recalled_engrams) are LEGITIMATELY empty for first-turn fresh context, not silently-substituted defaults What this slice does NOT do: - Call respond(). Next slice owns that, plus the lock-around-await discipline + inference-error-trips-circuit-breaker contract - Wire persona/enroll from production code. L0-2-cutover Tests: 19/19 passing. 16 pre-existing + 3 new doctrine pins: - enroll_with_empty_model_is_rejected_loud - enroll_with_empty_specialty_is_rejected_loud - enroll_command_requires_model - service_once_for dispatch test extended to verify the RespondInput carries the persona's real model/specialty/ system_prompt, not empty defaults Verified on Xcode 26.3 + llama/metal feature. Card: 8d11027b Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
joelteply
added a commit
that referenced
this pull request
May 29, 2026
…around-await, inference CB threshold (#1468) Stacks on L0-2-respond-context (#1467). Three contracts the previous attempt got wrong, all specified properly + tested here: 1. **Lock discipline.** std::sync::Mutex on personas — the compiler forces correctness: can't be held across .await. drain_all_personas does the lock-decide-drop-respond-relock dance. Production safety: status/enroll/other personas don't block across multi-second inference calls. 2. **Inference errors trip CB with HIGHER threshold than service.** Two counters per persona: - consecutive_service_failures (threshold 5) for deserialization / channel access / lock failures - consecutive_inference_failures (threshold 15) for respond() errors Preserves 'transient hiccup ≠ broken persona' while still surfacing 'model never loads' as back-pressure at the 15-error mark. 3. **Responder trait for DI.** Production uses DefaultResponder which calls persona::response::respond. Tests inject MockResponder that records calls + returns scripted outcomes (PersonaResponse::Spoke or Err) without loading a real model. What changes: - New Responder trait + DefaultResponder impl - PersonaServiceModule holds Arc<dyn Responder>; new() defaults to DefaultResponder; with_responder() for test injection - EnrolledPersona: consecutive_failures split into consecutive_service_failures + consecutive_inference_failures - ServiceOnceOutcome (the caller-facing variants) restructured: Idle | SilentByDecision | Responded{response: PersonaResponse} | UnsupportedItem - ServicePopDecision (NEW, sync-step output): Idle | Silent | NeedsResponse | UnsupportedItem — what service_once_for returns inside the lock - service_once_for: signature changes to return ServicePopDecision (sync step). Same body, just renamed outcome - drain_all_personas: rewritten with proper lock discipline. async, drops lock around responder.respond().await - New helper with_persona(): briefly lock the map and mutate the named persona; closure runs sync inside lock - tick: awaits drain_all_personas What does NOT change yet: - No production code calls persona/enroll. Tick still runs over empty map. - TS PersonaAutonomousLoop still drives production. L0-2-cutover. - Real inference still requires model loading — tests use mock. Tests: 24/24 passing. Pre-existing 19 + 5 new: - drain_calls_responder_when_gate_says_yes - drain_does_not_call_responder_when_gate_says_no - inference_errors_eventually_trip_circuit_at_inference_threshold - inference_failure_below_threshold_does_not_trip_circuit - successful_response_resets_inference_failure_counter Verified on Xcode 26.3 + llama/metal feature. Card: 34f28611 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Reworked from the earlier L0-2-respond attempt (#1466, self-closed after audit). Card 8d11027b.
Why the rewrite
Audit found three doctrine violations in the first attempt:
std::Mutexheld acrossrespond().await— blocks status/enroll/other personas for the full inference roundtripRespondErrorasOkoutcome — circuit breaker never trips on repeated inference failures (silent degradation)This slice fixes them by SHRINKING the scope: no
respond()call yet. That's the next slice, which can rely onRespondInputbeing honestly constructed.What this slice DOES
ResponderConfig(model, system_prompt, capabilities, specialty). All required at enrollment. Validated non-empty with named errors for model + specialty.EnrolledPersonaextends withresponder_configfieldenrollsignature requiresResponderConfigparameter; rejected enrollments don't mutate statepersona/enrollcommand parses model/system_prompt/specialty/capabilities from JSON; requires model loudServiceOnceOutcomeupdated:SilentByDecision { message_id, decision }— gate said noNeedsResponse { message_id, decision, respond_input }— gate said yes;respond_inputis fully-formed from real configUnsupportedItemunchangedEvaluatedREMOVEDservice_once_forbuildsRespondInputfrom real persona config + per-message context; no empty-string defaultsWhat this slice does NOT do
respond()— next slice owns the lock-around-await discipline + inference-error-trips-circuit-breaker contractpersona/enrollfrom production code — L0-2-cutoverTests — 19/19 passing
16 pre-existing + 3 new doctrine pins:
enroll_with_empty_model_is_rejected_loudenroll_with_empty_specialty_is_rejected_loudenroll_command_requires_modelservice_once_fordispatch test extended to verifyRespondInputcarries the persona's real model/specialty/system_prompt, NOT empty defaults🤖 Generated with Claude Code