pub mod event;
pub mod fold;
pub mod persist;
pub mod section;
pub mod state;
pub mod tunable;
use std::{sync::Mutex, time::Instant};
use async_trait::async_trait;
use chrono::Utc;
use serde::Deserialize;
use serde_json::{json, Value};
use khive_fold::{Fold, FoldContext};
use khive_runtime::pack::PackRuntime;
use khive_runtime::{
micros_to_iso, DispatchHook, EventView, KhiveRuntime, NamespaceToken, RuntimeError,
VerbRegistry,
};
use khive_storage::event::{Event, EventFilter};
use khive_storage::types::PageRequest;
use khive_types::{HandlerDef, Pack, ParamDef, VerbCategory, Visibility};
use crate::fold::{BalancedRecallFold, SectionPosteriorFold};
use crate::section::derive_deterministic_weights;
use crate::state::{
BrainState, ProfileBinding, ProfileLifecycle, ProfileRecord, SectionPosteriorState, SectionType,
};
const ENTITY_CACHE_CAPACITY: usize = 10_000;
pub(crate) fn sync_balanced_recall_record(state: &mut BrainState) {
let total_ev = state.balanced_recall.total_events;
let snap_val = serde_json::to_value(state.balanced_recall.to_snapshot()).ok();
if let Some(record) = state.profiles.get_mut("balanced-recall-v1") {
record.total_events = total_ev;
record.state_snapshot = snap_val;
}
}
static BRAIN_HANDLERS: &[HandlerDef] = &[
HandlerDef {
name: "brain.state",
description: "Return current BrainState snapshot for inspection",
visibility: Visibility::Subhandler,
category: VerbCategory::Assertive,
params: &[],
},
HandlerDef {
name: "brain.config",
description: "Return projected config for a named pack parameter",
visibility: Visibility::Subhandler,
category: VerbCategory::Assertive,
params: &[ParamDef {
name: "parameter",
param_type: "string",
required: false,
description: "Specific parameter to query: \"recall::relevance_weight\" | \"recall::salience_weight\" | \"recall::temporal_weight\". Omit to return all.",
}],
},
HandlerDef {
name: "brain.events",
description: "List recent brain-relevant events for debugging",
visibility: Visibility::Subhandler,
category: VerbCategory::Assertive,
params: &[ParamDef {
name: "limit",
param_type: "integer",
required: false,
description: "Maximum events to return (default 20, max 100).",
}],
},
HandlerDef {
name: "brain.profiles",
description: "List profiles, optionally filtered by lifecycle",
visibility: Visibility::Verb,
category: VerbCategory::Assertive,
params: &[ParamDef {
name: "lifecycle",
param_type: "string",
required: false,
description: "Filter profiles by lifecycle state: \"active\" | \"inactive\" | \"archived\". Omit to return all.",
}],
},
HandlerDef {
name: "brain.profile",
description: "Profile metadata, latest snapshot, current state summary",
visibility: Visibility::Verb,
category: VerbCategory::Assertive,
params: &[ParamDef {
name: "profile_id",
param_type: "string",
required: true,
description: "Profile ID string (e.g. \"balanced-recall-v1\"). NOT a UUID — use the string identifier. Alias: id.",
}],
},
HandlerDef {
name: "brain.resolve",
description: "Show which profile would serve a caller context",
visibility: Visibility::Verb,
category: VerbCategory::Assertive,
params: &[
ParamDef {
name: "consumer_kind",
param_type: "string",
required: true,
description: "Verb or operation type the caller is about to perform (e.g. \"recall\").",
},
ParamDef {
name: "actor",
param_type: "string",
required: false,
description: "Caller actor identifier. Defaults to \"*\" wildcard match.",
},
ParamDef {
name: "namespace",
param_type: "string",
required: false,
description: "Namespace for binding resolution. Defaults to \"*\" wildcard match.",
},
],
},
HandlerDef {
name: "brain.activate",
description: "Move a profile to Active (start live update loop)",
visibility: Visibility::Verb,
category: VerbCategory::Commissive,
params: &[ParamDef {
name: "profile_id",
param_type: "string",
required: true,
description: "Profile ID to activate (e.g. \"balanced-recall-v1\").",
}],
},
HandlerDef {
name: "brain.deactivate",
description: "Move a profile to Inactive (stop live updates, retain state)",
visibility: Visibility::Verb,
category: VerbCategory::Commissive,
params: &[ParamDef {
name: "profile_id",
param_type: "string",
required: true,
description: "Profile ID to deactivate.",
}],
},
HandlerDef {
name: "brain.archive",
description: "Move a profile to Archived (read-only, audit-retained)",
visibility: Visibility::Verb,
category: VerbCategory::Declaration,
params: &[ParamDef {
name: "profile_id",
param_type: "string",
required: true,
description: "Profile ID to archive.",
}],
},
HandlerDef {
name: "brain.reset",
description: "Reset posteriors to priors (preserves event history)",
visibility: Visibility::Verb,
category: VerbCategory::Declaration,
params: &[ParamDef {
name: "profile_id",
param_type: "string",
required: false,
description: "Profile ID to reset (must exist and be active). Defaults to \"balanced-recall-v1\". Use brain.profiles() to list profiles.",
}],
},
HandlerDef {
name: "brain.feedback",
description: "Emit a FeedbackExplicit event into the shared log",
visibility: Visibility::Verb,
category: VerbCategory::Commissive,
params: &[
ParamDef {
name: "target_id",
param_type: "uuid",
required: true,
description: "UUID of the memory note or entity the feedback applies to.",
},
ParamDef {
name: "signal",
param_type: "string",
required: true,
description: "Feedback signal: \"useful\" | \"not_useful\" | \"wrong\".",
},
ParamDef {
name: "served_by_profile_id",
param_type: "string",
required: false,
description: "Profile ID that served the result being rated. Recorded in the event payload.",
},
ParamDef {
name: "section_signals",
param_type: "object",
required: false,
description: "Per-section feedback signals: {\"section_name\": \"useful\"|\"not_useful\"|\"wrong\"}. For knowledge_compose profiles.",
},
],
},
HandlerDef {
name: "brain.auto_feedback",
description: "Emit implicit feedback for recall results supplied by an agent. \
Convenience verb: agents call this after memory.recall instead of constructing \
a brain.feedback call manually. Keeps memory and brain packs decoupled (#517).",
visibility: Visibility::Verb,
category: VerbCategory::Commissive,
params: &[
ParamDef {
name: "query",
param_type: "string",
required: true,
description: "Recall query that produced the results.",
},
ParamDef {
name: "results",
param_type: "array",
required: true,
description: "Recall result objects; the first object's note_id is credited.",
},
ParamDef {
name: "signal",
param_type: "string",
required: false,
description: "Feedback signal. Defaults to \"implicit_positive\".",
},
ParamDef {
name: "served_by_profile_id",
param_type: "string",
required: false,
description: "Profile ID that served the recall. Defaults like brain.feedback.",
},
],
},
HandlerDef {
name: "brain.bind",
description: "Write a row in the profile resolution table",
visibility: Visibility::Verb,
category: VerbCategory::Declaration,
params: &[
ParamDef {
name: "profile_id",
param_type: "string",
required: true,
description: "Profile ID to bind (must exist).",
},
ParamDef {
name: "actor",
param_type: "string",
required: false,
description: "Actor identifier to match. Default \"*\" (all actors). Cannot contain \"*\" inside a real value.",
},
ParamDef {
name: "namespace",
param_type: "string",
required: false,
description: "Namespace to match. Default \"*\" (all namespaces).",
},
ParamDef {
name: "consumer_kind",
param_type: "string",
required: false,
description: "Verb / operation kind to match. Default \"*\" (all kinds).",
},
ParamDef {
name: "priority",
param_type: "integer",
required: false,
description: "Binding priority; higher wins when multiple bindings match (default 0).",
},
],
},
HandlerDef {
name: "brain.unbind",
description: "Remove rows from the profile resolution table. At least one filter (profile_id, actor, namespace, or consumer_kind) is required.",
visibility: Visibility::Verb,
category: VerbCategory::Declaration,
params: &[
ParamDef {
name: "profile_id",
param_type: "string",
required: false,
description: "Remove bindings for this profile ID. All filters use AND semantics. At least one filter is required.",
},
ParamDef {
name: "actor",
param_type: "string",
required: false,
description: "Remove bindings for this actor.",
},
ParamDef {
name: "namespace",
param_type: "string",
required: false,
description: "Remove bindings for this namespace.",
},
ParamDef {
name: "consumer_kind",
param_type: "string",
required: false,
description: "Remove bindings for this consumer_kind.",
},
],
},
HandlerDef {
name: "brain.bindings",
description: "List rows in the profile resolution table, optionally filtered",
visibility: Visibility::Verb,
category: VerbCategory::Assertive,
params: &[
ParamDef {
name: "profile_id",
param_type: "string",
required: false,
description: "Filter bindings by profile ID.",
},
ParamDef {
name: "actor",
param_type: "string",
required: false,
description: "Filter bindings by actor.",
},
ParamDef {
name: "namespace",
param_type: "string",
required: false,
description: "Filter bindings by namespace.",
},
ParamDef {
name: "consumer_kind",
param_type: "string",
required: false,
description: "Filter bindings by consumer_kind.",
},
],
},
HandlerDef {
name: "brain.create_profile",
description: "Create a new brain profile with given name and optional seed priors",
visibility: Visibility::Verb,
category: VerbCategory::Declaration,
params: &[
ParamDef {
name: "name",
param_type: "string",
required: true,
description: "Profile ID / name (alphanumeric, hyphens allowed, e.g. \"my-profile-v1\"). Must be unique.",
},
ParamDef {
name: "description",
param_type: "string",
required: false,
description: "Human-readable description for this profile.",
},
ParamDef {
name: "consumer_kind",
param_type: "string",
required: false,
description: "Operation kind this profile targets (e.g. \"recall\"). Default \"recall\".",
},
ParamDef {
name: "seed_priors",
param_type: "object",
required: false,
description: "Seed priors object. For knowledge_compose: {\"section_posteriors\": {\"overview\": {\"alpha\": 2.0, \"beta\": 2.0}, ...}}. For recall: {\"relevance\": {\"alpha\": 7.0, \"beta\": 3.0}, ...}.",
},
],
},
HandlerDef {
name: "brain.emit",
description: "Manually emit a feedback event (deprecated; use brain.feedback)",
visibility: Visibility::Subhandler,
category: VerbCategory::Commissive,
params: &[
ParamDef {
name: "target_id",
param_type: "uuid",
required: true,
description: "UUID of the record the feedback applies to.",
},
ParamDef {
name: "signal",
param_type: "string",
required: true,
description: "Feedback signal: \"useful\" | \"not_useful\" | \"wrong\". Deprecated: use brain.feedback instead.",
},
ParamDef {
name: "served_by_profile_id",
param_type: "string",
required: false,
description: "Profile ID that served the result.",
},
],
},
];
pub struct BrainPack {
runtime: KhiveRuntime,
state: Mutex<BrainState>,
fold: BalancedRecallFold,
section_fold: SectionPosteriorFold,
persistence: Mutex<persist::PersistenceTracker>,
}
impl Pack for BrainPack {
const NAME: &'static str = "brain";
const NOTE_KINDS: &'static [&'static str] = &[];
const ENTITY_KINDS: &'static [&'static str] = &[];
const HANDLERS: &'static [HandlerDef] = BRAIN_HANDLERS;
const REQUIRES: &'static [&'static str] = &["kg"];
}
impl BrainPack {
pub fn new(runtime: KhiveRuntime) -> Self {
let fold = BalancedRecallFold::new(ENTITY_CACHE_CAPACITY);
let section_fold = SectionPosteriorFold::new();
let state = BrainState::new(ENTITY_CACHE_CAPACITY);
Self {
runtime,
state: Mutex::new(state),
fold,
section_fold,
persistence: Mutex::new(persist::PersistenceTracker::new()),
}
}
async fn ensure_loaded(&self, token: &NamespaceToken) -> Result<(), RuntimeError> {
persist::ensure_loaded(
&self.runtime,
token,
&self.persistence,
&self.state,
&self.fold,
&self.section_fold,
ENTITY_CACHE_CAPACITY,
)
.await
}
pub fn snapshot(&self) -> crate::state::BrainStateSnapshot {
self.state.lock().unwrap().to_snapshot()
}
async fn handle_state(&self, _params: Value) -> Result<Value, RuntimeError> {
let state = self.state.lock().unwrap();
let snapshot = state.to_snapshot();
serde_json::to_value(&snapshot).map_err(|e| RuntimeError::InvalidInput(e.to_string()))
}
async fn handle_config(&self, params: Value) -> Result<Value, RuntimeError> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ConfigParams {
parameter: Option<String>,
}
let p: ConfigParams = serde_json::from_value(params)
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
let state = self.state.lock().unwrap();
let br = &state.balanced_recall;
let param_map = [
("recall::relevance_weight", &br.relevance),
("recall::salience_weight", &br.salience),
("recall::temporal_weight", &br.temporal),
];
match p.parameter {
Some(key) => {
let posterior = param_map
.iter()
.find(|(k, _)| *k == key)
.map(|(_, p)| *p)
.ok_or_else(|| {
RuntimeError::NotFound(format!(
"parameter {key:?}; valid: {}",
param_map
.iter()
.map(|(k, _)| *k)
.collect::<Vec<_>>()
.join(", ")
))
})?;
Ok(json!({
"parameter": key,
"mean": posterior.mean(),
"variance": posterior.variance(),
"ess": posterior.effective_sample_size(),
"alpha": posterior.alpha,
"beta": posterior.beta,
}))
}
None => {
let configs: serde_json::Map<String, Value> = param_map
.iter()
.map(|(k, p)| {
(
(*k).to_owned(),
json!({
"mean": p.mean(),
"variance": p.variance(),
"ess": p.effective_sample_size(),
}),
)
})
.collect();
Ok(Value::Object(configs))
}
}
}
async fn handle_events(
&self,
token: &NamespaceToken,
params: Value,
) -> Result<Value, RuntimeError> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct EventsParams {
limit: Option<u32>,
}
let p: EventsParams = serde_json::from_value(params)
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
let limit = p.limit.unwrap_or(20).min(100);
let ns = token.namespace().as_str().to_string();
let store = self.runtime.events(token)?;
let filter = EventFilter {
verbs: vec![
"recall".into(),
"search".into(),
"brain.feedback".into(),
"brain.emit".into(), "get".into(),
"remember".into(),
],
..EventFilter::default()
};
let _ = ns;
let page = store
.query_events(filter, PageRequest { offset: 0, limit })
.await
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
let events: Vec<Value> = page
.items
.iter()
.map(|e| {
json!({
"id": e.id.to_string(),
"verb": e.verb,
"outcome": e.outcome,
"target_id": e.target_id.map(|t| t.to_string()),
"duration_us": e.duration_us,
"created_at": micros_to_iso(e.created_at),
"payload": e.payload,
})
})
.collect();
Ok(json!({
"count": events.len(),
"events": events,
}))
}
async fn handle_profiles(&self, params: Value) -> Result<Value, RuntimeError> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ProfilesParams {
lifecycle: Option<String>,
}
let p: ProfilesParams = serde_json::from_value(params)
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
let state = self.state.lock().unwrap();
let filter_lc: Option<ProfileLifecycle> = p
.lifecycle
.as_deref()
.map(|s| match s {
"active" => Ok(ProfileLifecycle::Active),
"inactive" => Ok(ProfileLifecycle::Inactive),
"archived" => Ok(ProfileLifecycle::Archived),
other => Err(RuntimeError::InvalidInput(format!(
"invalid lifecycle {other:?}; expected one of 'active', 'inactive', 'archived'"
))),
})
.transpose()?;
let profiles: Vec<&ProfileRecord> = state
.profiles
.values()
.filter(|r| filter_lc.as_ref().is_none_or(|lc| &r.lifecycle == lc))
.collect();
let items: Vec<Value> = profiles
.iter()
.map(|r| {
json!({
"id": r.id,
"description": r.description,
"consumer_kind": r.consumer_kind,
"state_class": r.state_class,
"lifecycle": r.lifecycle,
"total_events": r.total_events,
"exploration_epoch": r.exploration_epoch,
"created_at": r.created_at,
})
})
.collect();
Ok(json!({ "count": items.len(), "profiles": items }))
}
async fn handle_profile(&self, params: Value) -> Result<Value, RuntimeError> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ProfileParams {
profile_id: Option<String>,
id: Option<String>,
}
let p: ProfileParams = serde_json::from_value(params)
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
let profile_id = p
.profile_id
.or(p.id)
.ok_or_else(|| RuntimeError::InvalidInput("missing field `profile_id`".into()))?;
let state = self.state.lock().unwrap();
let record = state
.profiles
.get(&profile_id)
.ok_or_else(|| RuntimeError::NotFound(format!("profile {:?}", profile_id)))?;
let section_summary = if let Some(ss) = state.section_states.get(&profile_id) {
let weights = derive_deterministic_weights(ss);
let mut sections_json: serde_json::Map<String, Value> =
serde_json::Map::with_capacity(ss.posteriors.len());
for (section, posterior) in &ss.posteriors {
let w = weights.get(section).copied().unwrap_or(0.0);
sections_json.insert(
section.as_str().to_owned(),
json!({
"alpha": posterior.alpha,
"beta": posterior.beta,
"mean": posterior.mean(),
"variance": posterior.variance(),
"ess": posterior.effective_sample_size(),
"weight": w,
}),
);
}
Value::Object(sections_json)
} else {
Value::Null
};
Ok(json!({
"id": record.id,
"description": record.description,
"consumer_kind": record.consumer_kind,
"state_class": record.state_class,
"lifecycle": record.lifecycle,
"total_events": record.total_events,
"exploration_epoch": record.exploration_epoch,
"created_at": record.created_at,
"state_snapshot": record.state_snapshot,
"section_posteriors": section_summary,
}))
}
async fn handle_resolve(&self, params: Value) -> Result<Value, RuntimeError> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ResolveParams {
actor: Option<String>,
namespace: Option<String>,
consumer_kind: String,
}
let p: ResolveParams = serde_json::from_value(params)
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
let state = self.state.lock().unwrap();
match state.resolve_with_match(p.actor.as_deref(), p.namespace.as_deref(), &p.consumer_kind)
{
Some((record, matched_kind)) => Ok(json!({
"resolved_profile_id": record.id,
"lifecycle": record.lifecycle,
"requested_consumer_kind": p.consumer_kind,
"matched_consumer_kind": matched_kind,
})),
None => Err(RuntimeError::NotFound(format!(
"no profile resolved for consumer_kind={:?}",
p.consumer_kind
))),
}
}
async fn handle_activate(&self, params: Value) -> Result<Value, RuntimeError> {
self.set_lifecycle(params, ProfileLifecycle::Active).await
}
async fn handle_deactivate(&self, params: Value) -> Result<Value, RuntimeError> {
self.set_lifecycle(params, ProfileLifecycle::Inactive).await
}
async fn handle_archive(&self, params: Value) -> Result<Value, RuntimeError> {
self.set_lifecycle(params, ProfileLifecycle::Archived).await
}
async fn set_lifecycle(
&self,
params: Value,
lifecycle: ProfileLifecycle,
) -> Result<Value, RuntimeError> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct LifecycleParams {
profile_id: String,
}
let p: LifecycleParams = serde_json::from_value(params)
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
let mut state = self.state.lock().unwrap();
let record = state
.profiles
.get_mut(&p.profile_id)
.ok_or_else(|| RuntimeError::NotFound(format!("profile {:?}", p.profile_id)))?;
match (&record.lifecycle, &lifecycle) {
(ProfileLifecycle::Archived, _) => {
return Err(RuntimeError::InvalidInput(format!(
"profile {:?} is archived; archive is terminal and no transition is permitted",
p.profile_id
)));
}
(ProfileLifecycle::Active, ProfileLifecycle::Archived) => {
return Err(RuntimeError::InvalidInput(format!(
"profile {:?} is active; deactivate it before archiving",
p.profile_id
)));
}
_ => {}
}
record.lifecycle = lifecycle.clone();
Ok(json!({
"profile_id": p.profile_id,
"lifecycle": lifecycle,
}))
}
async fn handle_reset(&self, params: Value) -> Result<Value, RuntimeError> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ResetParams {
profile_id: Option<String>,
}
let p: ResetParams = serde_json::from_value(params)
.map_err(|e| RuntimeError::InvalidInput(format!("brain.reset: {e}")))?;
let profile_id = p
.profile_id
.unwrap_or_else(|| "balanced-recall-v1".to_string());
let mut state = self.state.lock().unwrap();
let lifecycle = state
.profiles
.get(&profile_id)
.ok_or_else(|| RuntimeError::NotFound(format!("profile {:?}", profile_id)))?
.lifecycle
.clone();
if lifecycle == ProfileLifecycle::Archived {
return Err(RuntimeError::InvalidInput(format!(
"profile {:?} is archived; archive is terminal and reset is not permitted",
profile_id
)));
}
if profile_id == "balanced-recall-v1" {
state.reset_posteriors();
sync_balanced_recall_record(&mut state);
} else if state.profile_states.contains_key(&profile_id) {
state.reset_profile_posteriors(&profile_id);
} else {
if let Some(record) = state.profiles.get_mut(&profile_id) {
record.exploration_epoch += 1;
}
}
let epoch = if profile_id == "balanced-recall-v1" {
state.balanced_recall.exploration_epoch
} else {
state.profiles[&profile_id].exploration_epoch
};
Ok(json!({
"reset": true,
"profile_id": profile_id,
"exploration_epoch": epoch,
}))
}
async fn handle_feedback(
&self,
token: &NamespaceToken,
params: Value,
) -> Result<Value, RuntimeError> {
let feedback_start = Instant::now();
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct FeedbackParams {
target_id: String,
signal: String,
served_by_profile_id: Option<String>,
section_signals: Option<serde_json::Value>,
}
let p: FeedbackParams = serde_json::from_value(params)
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
let target: uuid::Uuid = p
.target_id
.parse()
.map_err(|e| RuntimeError::InvalidInput(format!("invalid target_id: {e}")))?;
let signal = match p.signal.as_str() {
"useful" => "useful",
"not_useful" => "not_useful",
"wrong" => "wrong",
"explicit_positive" => "explicit_positive",
"explicit_negative" => "explicit_negative",
"implicit_positive" => "implicit_positive",
"implicit_negative" => "implicit_negative",
"correction" => "correction",
other => {
return Err(RuntimeError::InvalidInput(format!(
"unknown signal {other:?}; valid: useful | not_useful | wrong | \
explicit_positive | explicit_negative | implicit_positive | \
implicit_negative | correction"
)))
}
};
let resolved = self
.runtime
.resolve(token, target)
.await
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
if resolved.is_none() {
return Err(RuntimeError::NotFound(format!(
"target_id {:?} not found in namespace {:?}",
target,
token.namespace().as_str()
)));
}
let effective_profile = p
.served_by_profile_id
.as_deref()
.unwrap_or("balanced-recall-v1");
{
let state = self.state.lock().unwrap();
match state.profiles.get(effective_profile) {
None => {
return Err(RuntimeError::NotFound(format!(
"serving profile {:?} not found in profile registry",
effective_profile
)));
}
Some(rec) if rec.lifecycle == crate::state::ProfileLifecycle::Archived => {
return Err(RuntimeError::InvalidInput(format!(
"serving profile {:?} is archived; feedback cannot credit archived profiles",
effective_profile
)));
}
Some(_) => {}
}
}
let mut data = json!({"signal": signal});
if let Some(ref profile_id) = p.served_by_profile_id {
data["served_by_profile_id"] = json!(profile_id);
}
if let Some(ref ss) = p.section_signals {
data["section_signals"] = ss.clone();
}
let duration_us = feedback_start.elapsed().as_micros().max(1) as i64;
let event = Event::new(
token.namespace().as_str().to_string(),
"brain.feedback",
khive_types::EventKind::FeedbackExplicit,
khive_types::SubstrateKind::Event,
"brain",
)
.with_target(target)
.with_payload(data)
.with_duration_us(duration_us);
let store = self.runtime.events(token)?;
store
.append_event(event.clone())
.await
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
let serving_profile_owned = p
.served_by_profile_id
.as_deref()
.unwrap_or("balanced-recall-v1")
.to_string();
{
let ctx = FoldContext::new();
let mut state = self.state.lock().unwrap();
let serving_profile = serving_profile_owned.as_str();
if serving_profile == "balanced-recall-v1" {
let current_recall = std::mem::replace(
&mut state.balanced_recall,
crate::state::BalancedRecallState::new(0),
);
let updated = self.fold.reduce(current_recall, &event, &ctx);
state.balanced_recall = updated;
sync_balanced_recall_record(&mut state);
} else if state.profile_states.contains_key(serving_profile) {
let current = state
.profile_states
.remove(serving_profile)
.expect("key checked above");
let updated = self.fold.reduce(current, &event, &ctx);
let snap = serde_json::to_value(updated.to_snapshot()).ok();
let total = updated.total_events;
state
.profile_states
.insert(serving_profile.to_string(), updated);
if let Some(record) = state.profiles.get_mut(serving_profile) {
record.total_events = total;
record.state_snapshot = snap;
}
} else {
let current_recall = std::mem::replace(
&mut state.balanced_recall,
crate::state::BalancedRecallState::new(0),
);
let updated = self.fold.reduce(current_recall, &event, &ctx);
state.balanced_recall = updated;
sync_balanced_recall_record(&mut state);
}
if let Some(section_state) = state.section_states.remove(serving_profile) {
let updated = self.section_fold.reduce(section_state, &event, &ctx);
state
.section_states
.insert(serving_profile.to_string(), updated);
}
}
if let Err(e) = persist::persist_after_feedback(
&self.runtime,
token,
&self.persistence,
&self.state,
&event,
&serving_profile_owned,
)
.await
{
eprintln!("[brain] persistence failed (non-fatal): {e}");
}
Ok(json!({
"emitted": true,
"event_id": event.id.to_string(),
"verb": "brain.feedback",
"signal": signal,
"target_id": target.to_string(),
}))
}
async fn handle_auto_feedback(
&self,
token: &NamespaceToken,
params: Value,
) -> Result<Value, RuntimeError> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct AutoFeedbackParams {
query: String,
results: Vec<AutoFeedbackResult>,
signal: Option<String>,
served_by_profile_id: Option<String>,
}
#[derive(Deserialize)]
struct AutoFeedbackResult {
note_id: String,
}
let p: AutoFeedbackParams = serde_json::from_value(params)
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
if p.query.trim().is_empty() {
return Err(RuntimeError::InvalidInput(
"auto_feedback: `query` must not be empty".into(),
));
}
let Some(first) = p.results.first() else {
return Ok(json!({
"emitted": false,
"verb": "brain.auto_feedback",
"reason": "no_results",
}));
};
let target = resolve_auto_feedback_target(&self.runtime, token, &first.note_id).await?;
let mut feedback_params = json!({
"target_id": target.to_string(),
"signal": p.signal.as_deref().unwrap_or("implicit_positive"),
});
if let Some(ref profile_id) = p.served_by_profile_id {
feedback_params["served_by_profile_id"] = json!(profile_id);
}
let mut out = self.handle_feedback(token, feedback_params).await?;
out["verb"] = json!("brain.auto_feedback");
out["feedback_verb"] = json!("brain.feedback");
out["result_count"] = json!(p.results.len());
Ok(out)
}
async fn handle_emit(
&self,
token: &NamespaceToken,
params: Value,
) -> Result<Value, RuntimeError> {
self.handle_feedback(token, params).await
}
async fn handle_bind(&self, params: Value) -> Result<Value, RuntimeError> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct BindParams {
profile_id: String,
actor: Option<String>,
namespace: Option<String>,
consumer_kind: Option<String>,
priority: Option<i32>,
}
let p: BindParams = serde_json::from_value(params)
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
let mut state = self.state.lock().unwrap();
match state.profiles.get(&p.profile_id) {
None => {
return Err(RuntimeError::NotFound(format!(
"profile {:?}",
p.profile_id
)));
}
Some(record) if record.lifecycle == ProfileLifecycle::Archived => {
return Err(RuntimeError::InvalidInput(format!(
"profile {:?} is archived; bindings to archived profiles are not permitted",
p.profile_id
)));
}
Some(_) => {}
}
let actor = p.actor.unwrap_or_else(|| "*".into());
let namespace = p.namespace.unwrap_or_else(|| "*".into());
let consumer_kind = p.consumer_kind.unwrap_or_else(|| "*".into());
for (field, val) in [
("actor", &actor),
("namespace", &namespace),
("consumer_kind", &consumer_kind),
] {
if val.as_str() != "*" && val.contains('*') {
return Err(RuntimeError::InvalidInput(format!(
"{field}: '*' is reserved as the wildcard sentinel and cannot appear inside a real value"
)));
}
}
state.bindings.retain(|b| {
!(b.actor == actor && b.namespace == namespace && b.consumer_kind == consumer_kind)
});
state.bindings.push(ProfileBinding {
actor: actor.clone(),
namespace: namespace.clone(),
consumer_kind: consumer_kind.clone(),
profile_id: p.profile_id.clone(),
priority: p.priority.unwrap_or(0),
created_at: Utc::now(),
});
Ok(json!({
"bound": true,
"profile_id": p.profile_id,
"actor": actor,
"namespace": namespace,
"consumer_kind": consumer_kind,
}))
}
async fn handle_unbind(&self, params: Value) -> Result<Value, RuntimeError> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct UnbindParams {
profile_id: Option<String>,
actor: Option<String>,
namespace: Option<String>,
consumer_kind: Option<String>,
}
let p: UnbindParams = serde_json::from_value(params)
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
if p.profile_id.is_none()
&& p.actor.is_none()
&& p.namespace.is_none()
&& p.consumer_kind.is_none()
{
return Err(RuntimeError::InvalidInput(
"unbind requires at least one filter; pass profile_id, actor, namespace, or consumer_kind".into(),
));
}
let mut state = self.state.lock().unwrap();
let before = state.bindings.len();
state.bindings.retain(|b| {
let pid_match = p.profile_id.as_ref().is_none_or(|id| &b.profile_id == id);
let actor_match = p.actor.as_ref().is_none_or(|a| &b.actor == a);
let ns_match = p.namespace.as_ref().is_none_or(|n| &b.namespace == n);
let kind_match = p
.consumer_kind
.as_ref()
.is_none_or(|k| &b.consumer_kind == k);
!(pid_match && actor_match && ns_match && kind_match)
});
let removed = before - state.bindings.len();
Ok(json!({ "unbound": removed }))
}
async fn handle_bindings(&self, params: Value) -> Result<Value, RuntimeError> {
#[derive(Deserialize)]
struct BindingsParams {
profile_id: Option<String>,
actor: Option<String>,
namespace: Option<String>,
consumer_kind: Option<String>,
}
let p: BindingsParams = serde_json::from_value(params)
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
let state = self.state.lock().unwrap();
let rows: Vec<Value> = state
.bindings
.iter()
.filter(|b| {
p.profile_id.as_ref().is_none_or(|id| &b.profile_id == id)
&& p.actor.as_ref().is_none_or(|a| &b.actor == a)
&& p.namespace.as_ref().is_none_or(|n| &b.namespace == n)
&& p.consumer_kind
.as_ref()
.is_none_or(|k| &b.consumer_kind == k)
})
.map(|b| {
json!({
"profile_id": b.profile_id,
"actor": b.actor,
"namespace": b.namespace,
"consumer_kind": b.consumer_kind,
"priority": b.priority,
"created_at": b.created_at,
})
})
.collect();
Ok(json!({ "count": rows.len(), "bindings": rows }))
}
async fn handle_create_profile(&self, params: Value) -> Result<Value, RuntimeError> {
#[derive(Deserialize)]
struct CreateProfileParams {
name: String,
description: Option<String>,
consumer_kind: Option<String>,
seed_priors: Option<serde_json::Value>,
}
let p: CreateProfileParams = serde_json::from_value(params)
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
let name = p.name.trim().to_string();
if name.is_empty() {
return Err(RuntimeError::InvalidInput(
"name must not be empty or whitespace-only".into(),
));
}
if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
return Err(RuntimeError::InvalidInput(format!(
"name {:?} is invalid; profile IDs must match [a-zA-Z0-9-]+ (alphanumeric and hyphens only)",
name
)));
}
let p_name = name;
let consumer_kind = p.consumer_kind.unwrap_or_else(|| "recall".into());
let ck_trimmed = consumer_kind.trim();
if ck_trimmed.is_empty() {
return Err(RuntimeError::InvalidInput(
"consumer_kind must not be empty or whitespace".into(),
));
}
if ck_trimmed == "*" {
return Err(RuntimeError::InvalidInput(
"consumer_kind '*' is the wildcard sentinel and is not permitted for profile creation; provide a specific operation kind (e.g. \"recall\", \"search\")"
.into(),
));
}
let description = p
.description
.unwrap_or_else(|| format!("User-created profile: {}", p_name));
let mut state = self.state.lock().unwrap();
if state.profiles.contains_key(&p_name) {
return Err(RuntimeError::InvalidInput(format!(
"profile {:?} already exists",
p_name
)));
}
let ps = crate::state::BalancedRecallState::new(ENTITY_CACHE_CAPACITY);
let snap = serde_json::to_value(ps.to_snapshot()).ok();
let record = ProfileRecord {
id: p_name.clone(),
description: description.clone(),
consumer_kind: consumer_kind.clone(),
state_class: "Bayesian".into(),
lifecycle: ProfileLifecycle::Inactive,
created_at: Utc::now(),
state_snapshot: snap,
total_events: 0,
exploration_epoch: 0,
};
let section_state = if let Some(ref seed) = p.seed_priors {
if let Some(sp_obj) = seed.get("section_posteriors").and_then(|v| v.as_object()) {
let mut priors = std::collections::HashMap::new();
for (key, val) in sp_obj {
let st: SectionType = key.parse().map_err(|_| {
RuntimeError::InvalidInput(format!("unknown section type: {key:?}"))
})?;
let alpha = val.get("alpha").and_then(|v| v.as_f64()).ok_or_else(|| {
RuntimeError::InvalidInput(format!(
"missing or invalid alpha for section {key:?}"
))
})?;
let beta = val.get("beta").and_then(|v| v.as_f64()).ok_or_else(|| {
RuntimeError::InvalidInput(format!(
"missing or invalid beta for section {key:?}"
))
})?;
if alpha <= 0.0 || beta <= 0.0 {
return Err(RuntimeError::InvalidInput(format!(
"alpha and beta must be positive for section {key:?}; got alpha={alpha}, beta={beta}"
)));
}
priors.insert(st, crate::state::BetaPosterior::new(alpha, beta));
}
SectionPosteriorState::from_priors(priors)
} else {
return Err(RuntimeError::InvalidInput(
"seed_priors must contain a 'section_posteriors' object".into(),
));
}
} else {
SectionPosteriorState::new()
};
state.profiles.insert(p_name.clone(), record);
state.profile_states.insert(p_name.clone(), ps);
state.section_states.insert(p_name.clone(), section_state);
Ok(json!({
"created": true,
"profile_id": p_name,
"consumer_kind": consumer_kind,
"lifecycle": "inactive",
"description": description,
}))
}
}
struct BrainPackFactory;
impl khive_runtime::PackFactory for BrainPackFactory {
fn name(&self) -> &'static str {
"brain"
}
fn requires(&self) -> &'static [&'static str] {
&["kg"]
}
fn create(&self, runtime: KhiveRuntime) -> Box<dyn PackRuntime> {
Box::new(BrainPack::new(runtime))
}
}
inventory::submit! { khive_runtime::PackRegistration(&BrainPackFactory) }
#[async_trait]
impl PackRuntime for BrainPack {
fn name(&self) -> &str {
<BrainPack as Pack>::NAME
}
fn note_kinds(&self) -> &'static [&'static str] {
<BrainPack as Pack>::NOTE_KINDS
}
fn entity_kinds(&self) -> &'static [&'static str] {
<BrainPack as Pack>::ENTITY_KINDS
}
fn handlers(&self) -> &'static [HandlerDef] {
BRAIN_HANDLERS
}
fn requires(&self) -> &'static [&'static str] {
<BrainPack as Pack>::REQUIRES
}
async fn dispatch(
&self,
verb: &str,
params: Value,
_registry: &VerbRegistry,
token: &NamespaceToken,
) -> Result<Value, RuntimeError> {
self.ensure_loaded(token).await?;
match verb {
"brain.state" => self.handle_state(params).await,
"brain.config" => self.handle_config(params).await,
"brain.events" => self.handle_events(token, params).await,
"brain.profiles" => self.handle_profiles(params).await,
"brain.profile" => self.handle_profile(params).await,
"brain.resolve" => self.handle_resolve(params).await,
"brain.bindings" => self.handle_bindings(params).await,
"brain.activate" => self.handle_activate(params).await,
"brain.deactivate" => self.handle_deactivate(params).await,
"brain.archive" => self.handle_archive(params).await,
"brain.reset" => self.handle_reset(params).await,
"brain.feedback" => self.handle_feedback(token, params).await,
"brain.auto_feedback" => self.handle_auto_feedback(token, params).await,
"brain.bind" => self.handle_bind(params).await,
"brain.unbind" => self.handle_unbind(params).await,
"brain.create_profile" => self.handle_create_profile(params).await,
"brain.emit" => self.handle_emit(token, params).await,
_ => Err(RuntimeError::InvalidInput(format!(
"brain pack does not handle verb {verb:?}"
))),
}
}
}
async fn resolve_auto_feedback_target(
runtime: &KhiveRuntime,
token: &NamespaceToken,
raw: &str,
) -> Result<uuid::Uuid, RuntimeError> {
if let Ok(uuid) = raw.parse::<uuid::Uuid>() {
return Ok(uuid);
}
if raw.len() >= 8 && raw.chars().all(|c| c.is_ascii_hexdigit()) {
return runtime
.resolve_prefix(token, raw)
.await
.map_err(|e| RuntimeError::InvalidInput(e.to_string()))?
.ok_or_else(|| {
RuntimeError::InvalidInput(format!(
"auto_feedback: no record matches note_id prefix: {raw:?}"
))
});
}
Err(RuntimeError::InvalidInput(format!(
"auto_feedback: invalid note_id {raw:?}; expected full UUID or 8-char hex prefix"
)))
}
#[async_trait]
impl DispatchHook for BrainPack {
async fn on_dispatch(&self, view: &EventView) {
if view.event.verb.starts_with("brain.") {
return;
}
let ctx = FoldContext::new();
let mut state = self.state.lock().unwrap();
let current = std::mem::replace(
&mut state.balanced_recall,
crate::state::BalancedRecallState::new(0),
);
let updated = self.fold.reduce(current, &view.event, &ctx);
state.balanced_recall = updated;
sync_balanced_recall_record(&mut state);
}
}
#[cfg(test)]
mod tests {
use super::*;
use khive_runtime::{Namespace, VerbRegistryBuilder};
use serde_json::json;
fn make_pack() -> (BrainPack, KhiveRuntime) {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let pack = BrainPack::new(rt.clone());
(pack, rt)
}
fn empty_registry() -> VerbRegistry {
VerbRegistryBuilder::new()
.build()
.expect("empty registry builds successfully")
}
async fn create_test_entity(rt: &KhiveRuntime, token: &NamespaceToken) -> String {
let entity = rt
.create_entity(token, "concept", None, "test-target", None, None, vec![])
.await
.expect("create test entity");
entity.id.to_string()
}
#[tokio::test]
async fn dispatch_unknown_verb_returns_invalid_input() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let err = pack
.dispatch(
"brain.unknown",
json!({}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.unwrap_err();
if let RuntimeError::InvalidInput(msg) = &err {
assert!(
msg.contains("brain.unknown"),
"expected verb name in error: {msg}"
);
} else {
panic!("expected InvalidInput, got {err:?}");
}
}
#[tokio::test]
async fn dispatch_reset_returns_true_and_increments_epoch() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let result = pack
.dispatch(
"brain.reset",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.unwrap();
assert_eq!(result["reset"], json!(true));
assert_eq!(result["exploration_epoch"], json!(1u64));
assert_eq!(result["profile_id"], json!("balanced-recall-v1"));
}
#[tokio::test]
async fn dispatch_reset_no_args_resets_default_profile() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let result = pack
.dispatch(
"brain.reset",
json!({}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.expect("reset with no args must succeed (defaults to balanced-recall-v1)");
assert_eq!(result["reset"], json!(true));
assert_eq!(result["profile_id"], json!("balanced-recall-v1"));
}
#[tokio::test]
async fn dispatch_reset_nonexistent_profile_returns_not_found() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let err = pack
.dispatch(
"brain.reset",
json!({"profile_id": "ghost-profile"}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.unwrap_err();
assert!(
matches!(err, RuntimeError::NotFound(_)),
"reset on nonexistent profile must return NotFound, got {err:?}"
);
}
#[tokio::test]
async fn dispatch_reset_archived_profile_returns_invalid_input() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
pack.dispatch(
"brain.deactivate",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
pack.dispatch(
"brain.archive",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
let err = pack
.dispatch(
"brain.reset",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap_err();
if let RuntimeError::InvalidInput(msg) = &err {
assert!(
msg.contains("archived") || msg.contains("terminal"),
"reset on archived profile must mention 'archived' or 'terminal'; got: {msg}"
);
} else {
panic!("reset on archived profile must return InvalidInput, got {err:?}");
}
}
#[tokio::test]
async fn dispatch_feedback_invalid_signal_returns_invalid_input() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let target = "00000000-0000-0000-0000-000000000001";
let err = pack
.dispatch(
"brain.feedback",
json!({"target_id": target, "signal": "bad_signal"}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.unwrap_err();
if let RuntimeError::InvalidInput(msg) = &err {
assert!(
msg.contains("bad_signal"),
"expected signal name in error: {msg}"
);
assert!(
msg.contains("valid"),
"expected hint about valid values: {msg}"
);
} else {
panic!("expected InvalidInput, got {err:?}");
}
}
#[tokio::test]
async fn dispatch_state_returns_snapshot_fields() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let result = pack
.dispatch(
"brain.state",
json!({}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.unwrap();
assert!(result.get("profiles").is_some(), "missing profiles");
assert!(
result.get("balanced_recall").is_some(),
"missing balanced_recall"
);
assert!(result.get("bindings").is_some(), "missing bindings");
}
#[tokio::test]
async fn dispatch_profiles_returns_default_profile() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let result = pack
.dispatch(
"brain.profiles",
json!({}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.unwrap();
let profiles = result["profiles"].as_array().unwrap();
assert!(!profiles.is_empty(), "expected at least one profile");
assert_eq!(profiles[0]["id"], json!("balanced-recall-v1"));
}
#[tokio::test]
async fn dispatch_profiles_filtered_by_lifecycle() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let result = pack
.dispatch(
"brain.profiles",
json!({"lifecycle": "active"}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.unwrap();
let profiles = result["profiles"].as_array().unwrap();
for p in profiles {
assert_eq!(p["lifecycle"], json!("active"));
}
}
#[tokio::test]
async fn dispatch_profile_returns_profile_details() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let result = pack
.dispatch(
"brain.profile",
json!({"id": "balanced-recall-v1"}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.unwrap();
assert_eq!(result["id"], json!("balanced-recall-v1"));
assert_eq!(result["state_class"], json!("Bayesian"));
assert_eq!(result["consumer_kind"], json!("recall"));
}
#[tokio::test]
async fn dispatch_profile_not_found_returns_not_found() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let err = pack
.dispatch(
"brain.profile",
json!({"id": "nonexistent"}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.unwrap_err();
assert!(matches!(err, RuntimeError::NotFound(_)));
}
#[tokio::test]
async fn dispatch_resolve_returns_default_profile_for_recall() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let result = pack
.dispatch(
"brain.resolve",
json!({"consumer_kind": "recall"}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.unwrap();
assert_eq!(result["resolved_profile_id"], json!("balanced-recall-v1"));
}
#[tokio::test]
async fn dispatch_activate_and_deactivate_profile() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let result = pack
.dispatch(
"brain.deactivate",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(result["lifecycle"], json!("inactive"));
let state = pack
.dispatch(
"brain.profile",
json!({"id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(state["lifecycle"], json!("inactive"));
let result = pack
.dispatch(
"brain.activate",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(result["lifecycle"], json!("active"));
}
#[tokio::test]
async fn dispatch_archive_profile() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
pack.dispatch(
"brain.deactivate",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
let result = pack
.dispatch(
"brain.archive",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(result["lifecycle"], json!("archived"));
}
#[tokio::test]
async fn dispatch_activate_nonexistent_profile_returns_not_found() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let err = pack
.dispatch(
"brain.activate",
json!({"profile_id": "ghost-profile"}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.unwrap_err();
assert!(matches!(err, RuntimeError::NotFound(_)));
}
#[tokio::test]
async fn dispatch_bind_and_resolve_explicit_binding() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let result = pack
.dispatch(
"brain.bind",
json!({
"profile_id": "balanced-recall-v1",
"actor": "agent-x",
"consumer_kind": "recall"
}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(result["bound"], json!(true));
assert_eq!(result["actor"], json!("agent-x"));
let resolved = pack
.dispatch(
"brain.resolve",
json!({"actor": "agent-x", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(resolved["resolved_profile_id"], json!("balanced-recall-v1"));
}
#[tokio::test]
async fn dispatch_bind_nonexistent_profile_returns_not_found() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let err = pack
.dispatch(
"brain.bind",
json!({"profile_id": "ghost", "consumer_kind": "recall"}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.unwrap_err();
assert!(matches!(err, RuntimeError::NotFound(_)));
}
#[tokio::test]
async fn dispatch_unbind_removes_binding() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
pack.dispatch(
"brain.bind",
json!({"profile_id": "balanced-recall-v1", "actor": "agent-y", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap();
let result = pack
.dispatch(
"brain.unbind",
json!({"actor": "agent-y"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(result["unbound"], json!(1u64));
}
#[tokio::test]
async fn ue5_h1_invalid_lifecycle_error_lists_only_public_states() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let err = pack
.dispatch(
"brain.profiles",
json!({"lifecycle": "deleted"}),
®istry,
&token,
)
.await
.unwrap_err();
if let RuntimeError::InvalidInput(msg) = &err {
assert!(
msg.contains("active"),
"UE5-H1: error must list 'active'; got: {msg}"
);
assert!(
msg.contains("inactive"),
"UE5-H1: error must list 'inactive'; got: {msg}"
);
assert!(
msg.contains("archived"),
"UE5-H1: error must list 'archived'; got: {msg}"
);
assert!(
!msg.contains("defined"),
"UE5-H1: error must NOT expose internal 'defined' state; got: {msg}"
);
assert!(
!msg.contains("registered"),
"UE5-H1: error must NOT expose internal 'registered' state; got: {msg}"
);
} else {
panic!("UE5-H1: expected InvalidInput, got {err:?}");
}
}
#[tokio::test]
async fn ue5_h1_internal_lifecycle_values_rejected() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
for internal_state in ["defined", "registered"] {
let err = pack
.dispatch(
"brain.profiles",
json!({"lifecycle": internal_state}),
®istry,
&token,
)
.await
.unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidInput(_)),
"UE5-H1: internal lifecycle '{internal_state}' must be rejected, got {err:?}"
);
}
}
#[tokio::test]
async fn b_c1_archived_activate_is_rejected() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
pack.dispatch(
"brain.deactivate",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
pack.dispatch(
"brain.archive",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
let err = pack
.dispatch(
"brain.activate",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap_err();
if let RuntimeError::InvalidInput(msg) = &err {
assert!(
msg.contains("terminal") || msg.contains("archived"),
"B-C1: error must mention 'terminal' or 'archived'; got: {msg}"
);
} else {
panic!("B-C1: expected InvalidInput, got {err:?}");
}
}
#[tokio::test]
async fn b_c1_archived_deactivate_is_rejected() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
pack.dispatch(
"brain.deactivate",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
pack.dispatch(
"brain.archive",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
let err = pack
.dispatch(
"brain.deactivate",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidInput(_)),
"B-C1: deactivate on archived must return InvalidInput, got {err:?}"
);
}
#[tokio::test]
async fn b_c1_active_to_archived_direct_is_rejected() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let err = pack
.dispatch(
"brain.archive",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap_err();
if let RuntimeError::InvalidInput(msg) = &err {
assert!(
msg.contains("deactivate") || msg.contains("inactive"),
"B-C1: active→archived error must hint at deactivate; got: {msg}"
);
} else {
panic!("B-C1: expected InvalidInput for active→archived, got {err:?}");
}
}
#[tokio::test]
async fn b_c1_inactive_to_archived_is_permitted() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
pack.dispatch(
"brain.deactivate",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
let result = pack
.dispatch(
"brain.archive",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(
result["lifecycle"],
json!("archived"),
"B-C1: inactive→archived must succeed"
);
}
#[tokio::test]
async fn dispatch_unbind_uses_and_not_or() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
pack.dispatch(
"brain.bind",
json!({"profile_id": "balanced-recall-v1", "namespace": "ns-a", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap();
pack.dispatch(
"brain.bind",
json!({"profile_id": "balanced-recall-v1", "namespace": "ns-b", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap();
let result = pack
.dispatch(
"brain.unbind",
json!({"namespace": "ns-a", "profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(
result["unbound"],
json!(1u64),
"should remove exactly one binding"
);
let state = pack.state.lock().unwrap();
let remaining: Vec<_> = state
.bindings
.iter()
.filter(|b| b.namespace == "ns-b")
.collect();
assert_eq!(remaining.len(), 1, "ns-b binding must survive the unbind");
}
#[tokio::test]
async fn dispatch_config_all_parameters() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let result = pack
.dispatch(
"brain.config",
json!({}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.unwrap();
let obj = result.as_object().unwrap();
assert!(obj.contains_key("recall::relevance_weight"));
assert!(obj.contains_key("recall::salience_weight"));
assert!(obj.contains_key("recall::temporal_weight"));
}
#[tokio::test]
async fn dispatch_config_single_parameter() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let result = pack
.dispatch(
"brain.config",
json!({"parameter": "recall::relevance_weight"}),
®istry,
&rt.authorize(Namespace::local()).unwrap(),
)
.await
.unwrap();
assert_eq!(result["parameter"], json!("recall::relevance_weight"));
let mean = result["mean"].as_f64().unwrap();
assert!((mean - 0.7).abs() < 1e-6);
}
#[tokio::test]
async fn test_356_profile_record_total_events_synced_after_feedback() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let target = create_test_entity(&rt, &token).await;
for _ in 0..3 {
pack.dispatch(
"brain.feedback",
json!({"target_id": target, "signal": "useful"}),
®istry,
&token,
)
.await
.unwrap();
}
let snap = pack.snapshot();
let live_total = snap.balanced_recall.total_events;
let record_result = pack
.dispatch(
"brain.profile",
json!({"id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
let record_total = record_result["total_events"].as_u64().unwrap();
assert_eq!(
live_total, record_total,
"#356 part-A: profile_record.total_events ({record_total}) must equal \
balanced_recall.total_events ({live_total}) after feedback calls"
);
assert_eq!(live_total, 3, "expected exactly 3 events from part A");
{
let mut state = pack.state.lock().unwrap();
state.balanced_recall.total_events += 7; }
let hook_event = {
use khive_types::{EventKind, SubstrateKind};
let mut e = khive_storage::event::Event::new(
"local",
"search",
EventKind::Audit,
SubstrateKind::Event,
"kg",
);
e.outcome = khive_types::EventOutcome::Success;
e
};
let hook_view = khive_runtime::EventView {
event: hook_event,
observations: Vec::new(),
};
pack.on_dispatch(&hook_view).await;
let after_hook = pack
.dispatch(
"brain.profile",
json!({"id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
let after_total = after_hook["total_events"].as_u64().unwrap();
let live_after = pack.snapshot().balanced_recall.total_events;
assert_eq!(
after_total, live_after,
"#356 part-B: on_dispatch sync must correct desync; \
record shows {after_total}, live state shows {live_after}"
);
}
#[tokio::test]
async fn test_357_feedback_no_double_count() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let target = create_test_entity(&rt, &token).await;
pack.dispatch(
"brain.feedback",
json!({"target_id": target, "signal": "useful"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(
pack.snapshot().balanced_recall.total_events,
1,
"#357 pre-hook: handle_feedback must fold exactly once"
);
let hook_event = {
use khive_types::{EventKind, SubstrateKind};
khive_storage::event::Event::new(
"local",
"brain.feedback",
EventKind::FeedbackExplicit,
SubstrateKind::Event,
"brain",
)
};
let hook_view = khive_runtime::EventView {
event: hook_event,
observations: Vec::new(),
};
pack.on_dispatch(&hook_view).await;
assert_eq!(
pack.snapshot().balanced_recall.total_events,
1,
"#357: total_events must remain 1 after on_dispatch(brain.feedback); \
guard absent if this reads 2"
);
}
#[tokio::test]
async fn test_295_reset_restores_domain_priors_not_uniform() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let hook_event = |verb: &str| {
use khive_types::{EventKind, SubstrateKind};
let mut e = khive_storage::event::Event::new(
"local",
verb,
EventKind::Audit,
SubstrateKind::Event,
"kg",
);
e.outcome = khive_types::EventOutcome::Success;
e
};
for _ in 0..4 {
let view = khive_runtime::EventView {
event: hook_event("search"),
observations: Vec::new(),
};
pack.on_dispatch(&view).await;
}
let target = create_test_entity(&rt, &token).await;
for _ in 0..5 {
pack.dispatch(
"brain.feedback",
json!({"target_id": target, "signal": "useful"}),
®istry,
&token,
)
.await
.unwrap();
}
let before = pack.snapshot();
assert!(
before.balanced_recall.salience.alpha > 2.0,
"salience.alpha must have grown past prior after useful feedback"
);
assert!(
before.balanced_recall.total_events >= 9,
"expected at least 9 total events (4 hook + 5 feedback), got {}",
before.balanced_recall.total_events
);
let pre_reset_record = pack
.dispatch(
"brain.profile",
json!({"id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(
pre_reset_record["total_events"].as_u64().unwrap(),
before.balanced_recall.total_events,
"#295 pre-reset: profile record total_events out of sync before reset"
);
let reset_result = pack
.dispatch(
"brain.reset",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(reset_result["reset"], json!(true));
let epoch_after = reset_result["exploration_epoch"].as_u64().unwrap();
assert!(
epoch_after > 0,
"#295: exploration_epoch must increment after reset"
);
let after = pack.snapshot();
assert!(
(after.balanced_recall.salience.alpha - 2.0).abs() < 1e-12,
"#295: salience.alpha must be 2.0 after reset, got {}",
after.balanced_recall.salience.alpha
);
assert!(
(after.balanced_recall.salience.beta - 8.0).abs() < 1e-12,
"#295: salience.beta must be 8.0 after reset, got {}",
after.balanced_recall.salience.beta
);
assert!(
(after.balanced_recall.temporal.alpha - 1.0).abs() < 1e-12,
"#295: temporal.alpha must be 1.0 after reset, got {}",
after.balanced_recall.temporal.alpha
);
assert!(
(after.balanced_recall.temporal.beta - 9.0).abs() < 1e-12,
"#295: temporal.beta must be 9.0 after reset, got {}",
after.balanced_recall.temporal.beta
);
assert!(
(after.balanced_recall.relevance.alpha - 7.0).abs() < 1e-12,
"#295: relevance.alpha must be 7.0 after reset"
);
let record = pack
.dispatch(
"brain.profile",
json!({"id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
let record_total = record["total_events"].as_u64().unwrap();
assert_eq!(
record_total, after.balanced_recall.total_events,
"#295: profile record total_events ({record_total}) must match \
live state ({}) after reset",
after.balanced_recall.total_events
);
let record_epoch = record["exploration_epoch"].as_u64().unwrap();
assert_eq!(
record_epoch, epoch_after,
"#295: profile record exploration_epoch ({record_epoch}) must match \
reset result ({epoch_after})"
);
let snap = &record["state_snapshot"];
let sal_alpha = snap["salience"]["alpha"].as_f64().unwrap();
assert!(
(sal_alpha - 2.0).abs() < 1e-12,
"#295: brain.profile state_snapshot salience.alpha must be 2.0 after reset, \
got {sal_alpha}"
);
}
#[tokio::test]
async fn brain_reset_rejects_unknown_kwargs() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let err = pack
.dispatch(
"brain.reset",
json!({"unknownkw": "oops"}),
®istry,
&token,
)
.await
.unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidInput(_)),
"brain.reset with unknown kwargs must return InvalidInput, got: {err:?}"
);
if let RuntimeError::InvalidInput(msg) = &err {
assert!(
msg.contains("brain.reset"),
"error message must mention brain.reset, got: {msg}"
);
}
}
#[tokio::test]
async fn brain_reset_accepts_empty_params() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let result = pack
.dispatch("brain.reset", json!({}), ®istry, &token)
.await
.expect("brain.reset() must succeed with empty params");
assert_eq!(result["reset"], json!(true));
}
#[tokio::test]
async fn test_355_posteriors_update_after_dispatch_via_hook() {
let (pack, _rt) = make_pack();
let before = pack.snapshot();
let tmp_alpha_before = before.balanced_recall.temporal.alpha;
let tmp_beta_before = before.balanced_recall.temporal.beta;
let target_id = uuid::Uuid::new_v4();
let fast_hit_event = {
use khive_types::{EventKind, SubstrateKind};
let mut e = khive_storage::event::Event::new(
"local",
"recall",
EventKind::Audit,
SubstrateKind::Event,
"memory",
);
e.outcome = khive_types::EventOutcome::Success;
e.target_id = Some(target_id);
e.duration_us = 10_000; e
};
let view = khive_runtime::EventView {
event: fast_hit_event,
observations: Vec::new(),
};
pack.on_dispatch(&view).await;
let after_fast = pack.snapshot();
assert!(
(after_fast.balanced_recall.temporal.alpha - (tmp_alpha_before + 1.0)).abs() < 1e-12,
"#355: fast recall hit must increment temporal.alpha via hook: expected {}, got {}",
tmp_alpha_before + 1.0,
after_fast.balanced_recall.temporal.alpha
);
assert!(
(after_fast.balanced_recall.temporal.beta - tmp_beta_before).abs() < 1e-12,
"#355: fast hit must NOT increment temporal.beta"
);
let slow_hit_event = {
use khive_types::{EventKind, SubstrateKind};
let mut e = khive_storage::event::Event::new(
"local",
"recall",
EventKind::Audit,
SubstrateKind::Event,
"memory",
);
e.outcome = khive_types::EventOutcome::Success;
e.target_id = Some(target_id);
e.duration_us = 100_000; e
};
let view2 = khive_runtime::EventView {
event: slow_hit_event,
observations: Vec::new(),
};
pack.on_dispatch(&view2).await;
let after_slow = pack.snapshot();
assert!(
(after_slow.balanced_recall.temporal.beta - (tmp_beta_before + 1.0)).abs() < 1e-12,
"#355: slow recall hit must increment temporal.beta via hook: expected {}, got {}",
tmp_beta_before + 1.0,
after_slow.balanced_recall.temporal.beta
);
let miss_event = {
use khive_types::{EventKind, SubstrateKind};
let mut e = khive_storage::event::Event::new(
"local",
"recall",
EventKind::Audit,
SubstrateKind::Event,
"memory",
);
e.outcome = khive_types::EventOutcome::Success;
e
};
let view3 = khive_runtime::EventView {
event: miss_event,
observations: Vec::new(),
};
pack.on_dispatch(&view3).await;
let after_miss = pack.snapshot();
assert!(
(after_miss.balanced_recall.temporal.beta - (tmp_beta_before + 2.0)).abs() < 1e-12,
"#355: recall miss must further increment temporal.beta: expected {}, got {}",
tmp_beta_before + 2.0,
after_miss.balanced_recall.temporal.beta
);
}
#[tokio::test]
async fn w4_c2_unbind_no_filter_is_rejected() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
pack.dispatch(
"brain.bind",
json!({"profile_id": "balanced-recall-v1", "actor": "agent-z", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap();
let err = pack
.dispatch("brain.unbind", json!({}), ®istry, &token)
.await
.unwrap_err();
if let RuntimeError::InvalidInput(msg) = &err {
assert!(
msg.contains("filter") || msg.contains("profile_id") || msg.contains("actor"),
"C2: zero-filter unbind must mention required filter; got: {msg}"
);
} else {
panic!("C2: zero-filter unbind must return InvalidInput, got {err:?}");
}
let state = pack.state.lock().unwrap();
assert!(
!state.bindings.is_empty(),
"C2: binding must survive the rejected unbind"
);
}
#[tokio::test]
async fn w4_c3_bind_archived_profile_is_rejected() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
pack.dispatch(
"brain.deactivate",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
pack.dispatch(
"brain.archive",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
let err = pack
.dispatch(
"brain.bind",
json!({"profile_id": "balanced-recall-v1", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap_err();
if let RuntimeError::InvalidInput(msg) = &err {
assert!(
msg.contains("archived"),
"C3: bind to archived profile must mention 'archived'; got: {msg}"
);
} else {
panic!("C3: bind to archived profile must return InvalidInput, got {err:?}");
}
}
#[tokio::test]
async fn w4_c3_resolve_skips_archived_binding() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
{
let mut state = pack.state.lock().unwrap();
state.bindings.push(crate::state::ProfileBinding {
actor: "*".into(),
namespace: "*".into(),
consumer_kind: "recall".into(),
profile_id: "balanced-recall-v1".into(),
priority: 100,
created_at: chrono::Utc::now(),
});
state
.profiles
.get_mut("balanced-recall-v1")
.unwrap()
.lifecycle = ProfileLifecycle::Archived;
}
let err = pack
.dispatch(
"brain.resolve",
json!({"consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap_err();
assert!(
matches!(err, RuntimeError::NotFound(_)),
"C3: resolve with only archived binding must return NotFound, got {err:?}"
);
}
#[tokio::test]
async fn w4_c4_feedback_rejects_nonexistent_target() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let err = pack
.dispatch(
"brain.feedback",
json!({"target_id": "00000000-0000-0000-0000-000000000000", "signal": "useful"}),
®istry,
&token,
)
.await
.unwrap_err();
assert!(
matches!(err, RuntimeError::NotFound(_)),
"C4: feedback with nonexistent target_id must return NotFound, got {err:?}"
);
}
#[tokio::test]
async fn w4_c4_feedback_rejects_nonexistent_profile() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let target = create_test_entity(&rt, &token).await;
let err = pack
.dispatch(
"brain.feedback",
json!({"target_id": target, "signal": "useful", "served_by_profile_id": "fake-profile-xyz"}),
®istry,
&token,
)
.await
.unwrap_err();
assert!(
matches!(err, RuntimeError::NotFound(_)),
"C4: feedback with nonexistent served_by_profile_id must return NotFound, got {err:?}"
);
}
#[tokio::test]
async fn w4_c4_feedback_accepts_valid_target_and_profile() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let target = create_test_entity(&rt, &token).await;
let result = pack
.dispatch(
"brain.feedback",
json!({"target_id": target, "signal": "useful", "served_by_profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(result["emitted"], json!(true));
assert_eq!(result["signal"], json!("useful"));
}
#[tokio::test]
async fn w4_h1_create_profile_creates_new_profile() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let result = pack
.dispatch(
"brain.create_profile",
json!({"name": "my-profile-v1", "consumer_kind": "search", "description": "Custom search profile"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(result["created"], json!(true));
assert_eq!(result["profile_id"], json!("my-profile-v1"));
assert_eq!(result["lifecycle"], json!("inactive"));
assert_eq!(result["consumer_kind"], json!("search"));
let profiles = pack
.dispatch("brain.profiles", json!({}), ®istry, &token)
.await
.unwrap();
let ids: Vec<&str> = profiles["profiles"]
.as_array()
.unwrap()
.iter()
.filter_map(|p| p["id"].as_str())
.collect();
assert!(
ids.contains(&"my-profile-v1"),
"new profile must appear in brain.profiles"
);
}
#[tokio::test]
async fn w4_h1_create_profile_duplicate_rejected() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let err = pack
.dispatch(
"brain.create_profile",
json!({"name": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidInput(_)),
"H1: duplicate profile name must return InvalidInput, got {err:?}"
);
}
#[tokio::test]
async fn w4_h2_bindings_lists_rows() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let result = pack
.dispatch("brain.bindings", json!({}), ®istry, &token)
.await
.unwrap();
assert_eq!(result["count"], json!(0u64));
assert_eq!(result["bindings"], json!([]));
pack.dispatch(
"brain.bind",
json!({"profile_id": "balanced-recall-v1", "actor": "agent-a", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap();
let result2 = pack
.dispatch("brain.bindings", json!({}), ®istry, &token)
.await
.unwrap();
assert_eq!(result2["count"], json!(1u64));
let rows = result2["bindings"].as_array().unwrap();
assert_eq!(rows[0]["actor"], json!("agent-a"));
assert_eq!(rows[0]["profile_id"], json!("balanced-recall-v1"));
}
#[tokio::test]
async fn w4_h2_bindings_filtered() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
pack.dispatch("brain.bind", json!({"profile_id": "balanced-recall-v1", "actor": "agent-1", "consumer_kind": "recall"}), ®istry, &token).await.unwrap();
pack.dispatch("brain.bind", json!({"profile_id": "balanced-recall-v1", "actor": "agent-2", "consumer_kind": "search"}), ®istry, &token).await.unwrap();
let result = pack
.dispatch(
"brain.bindings",
json!({"actor": "agent-1"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(result["count"], json!(1u64));
assert_eq!(result["bindings"][0]["actor"], json!("agent-1"));
}
#[tokio::test]
async fn w4_h3_resolve_returns_both_requested_and_matched_kind() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
pack.dispatch(
"brain.bind",
json!({"profile_id": "balanced-recall-v1", "consumer_kind": "*", "priority": 1}),
®istry,
&token,
)
.await
.unwrap();
let result = pack
.dispatch(
"brain.resolve",
json!({"consumer_kind": "search"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(
result["requested_consumer_kind"],
json!("search"),
"H3: requested_consumer_kind must equal the query"
);
assert_eq!(
result["matched_consumer_kind"],
json!("*"),
"H3: matched_consumer_kind must show the wildcard binding"
);
assert_eq!(result["resolved_profile_id"], json!("balanced-recall-v1"));
}
#[tokio::test]
async fn w4_h3_resolve_exact_match_returns_exact_kind() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let result = pack
.dispatch(
"brain.resolve",
json!({"consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(result["requested_consumer_kind"], json!("recall"));
assert_eq!(result["matched_consumer_kind"], json!("recall"));
}
#[tokio::test]
async fn r2_archived_exact_binding_defers_to_live_wildcard() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
pack.dispatch(
"brain.create_profile",
json!({"name": "search-v1", "consumer_kind": "search"}),
®istry,
&token,
)
.await
.unwrap();
pack.dispatch(
"brain.activate",
json!({"profile_id": "search-v1"}),
®istry,
&token,
)
.await
.unwrap();
{
let mut state = pack.state.lock().unwrap();
state.bindings.push(crate::state::ProfileBinding {
actor: "*".into(),
namespace: "*".into(),
consumer_kind: "search".into(),
profile_id: "balanced-recall-v1".into(),
priority: 100,
created_at: chrono::Utc::now(),
});
state
.profiles
.get_mut("balanced-recall-v1")
.unwrap()
.lifecycle = crate::state::ProfileLifecycle::Archived;
}
pack.dispatch(
"brain.bind",
json!({"profile_id": "search-v1", "consumer_kind": "*", "priority": 1}),
®istry,
&token,
)
.await
.unwrap();
let result = pack
.dispatch(
"brain.resolve",
json!({"consumer_kind": "search"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(
result["resolved_profile_id"],
json!("search-v1"),
"r2 fix 3: archived high-priority binding must not suppress the live wildcard binding"
);
}
#[tokio::test]
async fn r2_feedback_rejects_archived_served_by_profile() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let target = create_test_entity(&rt, &token).await;
pack.dispatch(
"brain.deactivate",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
pack.dispatch(
"brain.archive",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
let err = pack
.dispatch(
"brain.feedback",
json!({
"target_id": target,
"signal": "useful",
"served_by_profile_id": "balanced-recall-v1"
}),
®istry,
&token,
)
.await
.unwrap_err();
if let RuntimeError::InvalidInput(msg) = &err {
assert!(
msg.contains("archived"),
"r2 fix 4: feedback to archived profile must mention 'archived'; got: {msg}"
);
} else {
panic!("r2 fix 4: feedback to archived served_by_profile_id must return InvalidInput, got {err:?}");
}
}
#[tokio::test]
async fn r2_create_profile_rejects_empty_consumer_kind() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let err = pack
.dispatch(
"brain.create_profile",
json!({"name": "bad-profile", "consumer_kind": ""}),
®istry,
&token,
)
.await
.unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidInput(_)),
"r2 fix 5: empty consumer_kind must return InvalidInput, got {err:?}"
);
}
#[tokio::test]
async fn r2_create_profile_rejects_wildcard_consumer_kind() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let err = pack
.dispatch(
"brain.create_profile",
json!({"name": "wildcard-profile", "consumer_kind": "*"}),
®istry,
&token,
)
.await
.unwrap_err();
if let RuntimeError::InvalidInput(msg) = &err {
assert!(
msg.contains("wildcard") || msg.contains("sentinel") || msg.contains("*"),
"r2 fix 5: wildcard consumer_kind rejection must explain the issue; got: {msg}"
);
} else {
panic!("r2 fix 5: wildcard consumer_kind must return InvalidInput, got {err:?}");
}
}
#[tokio::test]
async fn r2_create_profile_rejects_whitespace_consumer_kind() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let err = pack
.dispatch(
"brain.create_profile",
json!({"name": "ws-profile", "consumer_kind": " "}),
®istry,
&token,
)
.await
.unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidInput(_)),
"r2 fix 5: whitespace consumer_kind must return InvalidInput, got {err:?}"
);
}
#[tokio::test]
async fn r2_bindings_and_semantics_multi_filter() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
for name in ["alpha-v1", "beta-v1"] {
pack.dispatch(
"brain.create_profile",
json!({"name": name, "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap();
pack.dispatch(
"brain.activate",
json!({"profile_id": name}),
®istry,
&token,
)
.await
.unwrap();
}
pack.dispatch(
"brain.bind",
json!({"profile_id": "balanced-recall-v1", "actor": "agent-A", "namespace": "ns-1", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap();
pack.dispatch(
"brain.bind",
json!({"profile_id": "alpha-v1", "actor": "agent-A", "namespace": "ns-2", "consumer_kind": "search"}),
®istry,
&token,
)
.await
.unwrap();
pack.dispatch(
"brain.bind",
json!({"profile_id": "beta-v1", "actor": "agent-B", "namespace": "ns-1", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap();
let r1 = pack
.dispatch(
"brain.bindings",
json!({"profile_id": "balanced-recall-v1", "namespace": "ns-1"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(
r1["count"],
json!(1u64),
"AND filter profile_id+namespace must return 1 row"
);
assert_eq!(r1["bindings"][0]["profile_id"], json!("balanced-recall-v1"));
assert_eq!(r1["bindings"][0]["namespace"], json!("ns-1"));
let r2 = pack
.dispatch(
"brain.bindings",
json!({"actor": "agent-A", "consumer_kind": "search"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(
r2["count"],
json!(1u64),
"AND filter actor+consumer_kind must return 1 row"
);
assert_eq!(r2["bindings"][0]["profile_id"], json!("alpha-v1"));
let r3 = pack
.dispatch(
"brain.bindings",
json!({"actor": "agent-B", "consumer_kind": "search"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(
r3["count"],
json!(0u64),
"AND filter with no matches must return count=0"
);
assert_eq!(r3["bindings"], json!([]));
}
#[tokio::test]
async fn r2_user_profile_reset_mutates_posteriors() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let target = create_test_entity(&rt, &token).await;
pack.dispatch(
"brain.create_profile",
json!({"name": "custom-v1", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap();
pack.dispatch(
"brain.activate",
json!({"profile_id": "custom-v1"}),
®istry,
&token,
)
.await
.unwrap();
pack.dispatch(
"brain.feedback",
json!({"target_id": target, "signal": "useful", "served_by_profile_id": "custom-v1"}),
®istry,
&token,
)
.await
.unwrap();
let mutated = pack
.dispatch(
"brain.profile",
json!({"profile_id": "custom-v1"}),
®istry,
&token,
)
.await
.unwrap();
let salience_alpha_before = mutated["state_snapshot"]["salience"]["alpha"]
.as_f64()
.expect("state_snapshot.salience.alpha must be a number");
assert!(
salience_alpha_before > 2.0,
"r3 fix 2: feedback must have moved salience alpha above prior 2.0; got {salience_alpha_before}"
);
let epoch_before = mutated["exploration_epoch"].as_u64().unwrap();
let reset_result = pack
.dispatch(
"brain.reset",
json!({"profile_id": "custom-v1"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(reset_result["reset"], json!(true));
assert_eq!(reset_result["profile_id"], json!("custom-v1"));
let after = pack
.dispatch(
"brain.profile",
json!({"profile_id": "custom-v1"}),
®istry,
&token,
)
.await
.unwrap();
let epoch_after = after["exploration_epoch"].as_u64().unwrap();
assert!(
epoch_after > epoch_before,
"r3 fix 2: reset must increment exploration_epoch on user-created profile; before={epoch_before} after={epoch_after}"
);
let snap = &after["state_snapshot"];
assert!(
!snap.is_null(),
"r3 fix 2: state_snapshot must be non-null after reset"
);
let rel_alpha = snap["relevance"]["alpha"]
.as_f64()
.expect("relevance.alpha");
let rel_beta = snap["relevance"]["beta"].as_f64().expect("relevance.beta");
assert!(
(rel_alpha - 7.0).abs() < 1e-9 && (rel_beta - 3.0).abs() < 1e-9,
"r3 fix 2: relevance must be Beta(7,3) after reset; got ({rel_alpha},{rel_beta})"
);
let sal_alpha = snap["salience"]["alpha"].as_f64().expect("salience.alpha");
let sal_beta = snap["salience"]["beta"].as_f64().expect("salience.beta");
assert!(
(sal_alpha - 2.0).abs() < 1e-9 && (sal_beta - 8.0).abs() < 1e-9,
"r3 fix 2: salience must be Beta(2,8) after reset; got ({sal_alpha},{sal_beta})"
);
let tmp_alpha = snap["temporal"]["alpha"].as_f64().expect("temporal.alpha");
let tmp_beta = snap["temporal"]["beta"].as_f64().expect("temporal.beta");
assert!(
(tmp_alpha - 1.0).abs() < 1e-9 && (tmp_beta - 9.0).abs() < 1e-9,
"r3 fix 2: temporal must be Beta(1,9) after reset; got ({tmp_alpha},{tmp_beta})"
);
}
#[tokio::test]
async fn r2_user_profile_feedback_routes_to_profile_state() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let target = create_test_entity(&rt, &token).await;
pack.dispatch(
"brain.create_profile",
json!({"name": "custom-v1", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap();
pack.dispatch(
"brain.activate",
json!({"profile_id": "custom-v1"}),
®istry,
&token,
)
.await
.unwrap();
let before = pack
.dispatch(
"brain.profile",
json!({"profile_id": "custom-v1"}),
®istry,
&token,
)
.await
.unwrap();
let events_before = before["total_events"].as_u64().unwrap();
pack.dispatch(
"brain.feedback",
json!({"target_id": target, "signal": "useful", "served_by_profile_id": "custom-v1"}),
®istry,
&token,
)
.await
.unwrap();
let after = pack
.dispatch(
"brain.profile",
json!({"profile_id": "custom-v1"}),
®istry,
&token,
)
.await
.unwrap();
let events_after = after["total_events"].as_u64().unwrap();
assert!(
events_after > events_before,
"r2 fix 2: feedback routed to custom profile must increment its total_events; before={events_before} after={events_after}"
);
}
#[tokio::test]
async fn w4_h4_profile_accepts_profile_id_and_id_alias() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let r1 = pack
.dispatch(
"brain.profile",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(r1["id"], json!("balanced-recall-v1"));
let r2 = pack
.dispatch(
"brain.profile",
json!({"id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
assert_eq!(r2["id"], json!("balanced-recall-v1"));
}
#[tokio::test]
async fn r3_feedback_default_profile_archived_rejected() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let target = create_test_entity(&rt, &token).await;
pack.dispatch(
"brain.deactivate",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
pack.dispatch(
"brain.archive",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
let snap_before = pack
.dispatch(
"brain.profile",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
let events_before = snap_before["total_events"].as_u64().unwrap_or(0);
let log_before = pack
.dispatch("brain.events", json!({"limit": 1000}), ®istry, &token)
.await
.unwrap();
let log_count_before = log_before["events"]
.as_array()
.map(|a| a.len())
.unwrap_or(0);
let err = pack
.dispatch(
"brain.feedback",
json!({"target_id": target, "signal": "useful"}),
®istry,
&token,
)
.await
.unwrap_err();
match &err {
RuntimeError::InvalidInput(msg) => {
assert!(
msg.contains("archived"),
"r3-1: error must mention 'archived'; got: {msg}"
);
}
other => panic!("r3-1: expected InvalidInput(archived), got {other:?}"),
}
let snap_after = pack
.dispatch(
"brain.profile",
json!({"profile_id": "balanced-recall-v1"}),
®istry,
&token,
)
.await
.unwrap();
let events_after = snap_after["total_events"].as_u64().unwrap_or(0);
assert_eq!(
events_after, events_before,
"r3-1: archived default profile must not have events appended; before={events_before} after={events_after}"
);
let log_after = pack
.dispatch("brain.events", json!({"limit": 1000}), ®istry, &token)
.await
.unwrap();
let log_count_after = log_after["events"].as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(
log_count_after, log_count_before,
"r3-1: rejected feedback must not append a FeedbackExplicit event; before={log_count_before} after={log_count_after}"
);
}
#[tokio::test]
async fn r3_create_profile_id_grammar_enforced() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let err = pack
.dispatch(
"brain.create_profile",
json!({"name": " ", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidInput(_)),
"r3-3: whitespace-only name must return InvalidInput; got {err:?}"
);
pack.dispatch(
"brain.create_profile",
json!({"name": " my-profile ", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.expect("r3-3: name with leading/trailing spaces should be accepted after trim");
let err = pack
.dispatch(
"brain.create_profile",
json!({"name": "bad.profile", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidInput(_)),
"r3-3: dot in name must return InvalidInput; got {err:?}"
);
let err = pack
.dispatch(
"brain.create_profile",
json!({"name": "bad_profile", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidInput(_)),
"r3-3: underscore in name must return InvalidInput; got {err:?}"
);
let err = pack
.dispatch(
"brain.create_profile",
json!({"name": "*", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidInput(_)),
"r3-3: asterisk name must return InvalidInput; got {err:?}"
);
pack.dispatch(
"brain.create_profile",
json!({"name": "valid-profile-123", "consumer_kind": "recall"}),
®istry,
&token,
)
.await
.expect("r3-3: valid alphanumeric-hyphen name must succeed");
}
#[tokio::test]
async fn test_289_feedback_event_records_nonzero_duration() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let target = create_test_entity(&rt, &token).await;
let result = pack
.dispatch(
"brain.feedback",
json!({"target_id": target, "signal": "useful"}),
®istry,
&token,
)
.await
.unwrap();
let event_id = result["event_id"].as_str().unwrap().to_string();
let log = pack
.dispatch("brain.events", json!({"limit": 100}), ®istry, &token)
.await
.unwrap();
let event = log["events"]
.as_array()
.unwrap()
.iter()
.find(|e| e["id"].as_str() == Some(event_id.as_str()))
.expect("#289: feedback event must appear in brain.events");
assert!(
event["duration_us"].as_i64().unwrap() > 0,
"#289: feedback event duration_us must be non-zero, got {}",
event["duration_us"]
);
}
#[tokio::test]
async fn brain_auto_feedback_emits_implicit_positive_for_first_result() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let target = create_test_entity(&rt, &token).await;
let result = pack
.dispatch(
"brain.auto_feedback",
json!({
"query": "recall calibration target",
"results": [{ "note_id": target }]
}),
®istry,
&token,
)
.await
.expect("auto_feedback succeeds");
assert_eq!(result["emitted"], json!(true), "emitted must be true");
assert_eq!(
result["signal"],
json!("implicit_positive"),
"default signal must be implicit_positive"
);
let returned_target_id = result["target_id"].as_str().unwrap_or("");
assert_eq!(
returned_target_id.len(),
36,
"target_id in auto_feedback response must be full 36-char UUID"
);
assert_eq!(
returned_target_id, target,
"target_id must match the created entity"
);
assert_eq!(
pack.snapshot().balanced_recall.total_events,
1,
"auto_feedback must increment total_events"
);
}
#[tokio::test]
async fn brain_auto_feedback_empty_results_returns_no_emit() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let result = pack
.dispatch(
"brain.auto_feedback",
json!({
"query": "empty recall results",
"results": []
}),
®istry,
&token,
)
.await
.expect("auto_feedback with empty results succeeds");
assert_eq!(result["emitted"], json!(false));
assert_eq!(result["reason"], json!("no_results"));
}
#[tokio::test]
async fn brain_auto_feedback_accepts_short_note_id_prefix() {
let (pack, rt) = make_pack();
let registry = empty_registry();
let token = rt.authorize(Namespace::local()).unwrap();
let target = create_test_entity(&rt, &token).await;
let prefix = &target[..8];
let result = pack
.dispatch(
"brain.auto_feedback",
json!({
"query": "prefix resolution test",
"results": [{ "note_id": prefix }]
}),
®istry,
&token,
)
.await
.expect("auto_feedback with 8-char prefix succeeds");
assert_eq!(result["emitted"], json!(true));
assert_eq!(result["target_id"].as_str().unwrap_or("").len(), 36);
}
}
#[cfg(test)]
mod help_tests {
use super::*;
fn find_handler(name: &str) -> &'static HandlerDef {
BRAIN_HANDLERS
.iter()
.find(|h| h.name == name)
.unwrap_or_else(|| panic!("handler {name:?} not found in BRAIN_HANDLERS"))
}
#[test]
fn brain_feedback_params_non_empty_and_has_target_and_signal() {
let h = find_handler("brain.feedback");
assert!(!h.params.is_empty(), "brain.feedback must have params");
assert!(
h.params.iter().any(|p| p.name == "target_id" && p.required),
"brain.feedback must have required target_id param"
);
assert!(
h.params.iter().any(|p| p.name == "signal" && p.required),
"brain.feedback must have required signal param"
);
assert!(
h.params.iter().any(|p| p.name == "served_by_profile_id"),
"brain.feedback must document served_by_profile_id"
);
}
#[test]
fn brain_auto_feedback_handler_is_declared() {
let h = find_handler("brain.auto_feedback");
assert!(
h.params.iter().any(|p| p.name == "query" && p.required),
"brain.auto_feedback must have required query param"
);
assert!(
h.params.iter().any(|p| p.name == "results" && p.required),
"brain.auto_feedback must have required results param"
);
}
#[test]
fn brain_profile_params_has_required_profile_id() {
let h = find_handler("brain.profile");
assert!(!h.params.is_empty(), "brain.profile must have params");
assert!(
h.params
.iter()
.any(|p| p.name == "profile_id" && p.required),
"brain.profile must have required profile_id param (H4 fix)"
);
}
#[test]
fn brain_profiles_params_has_lifecycle_filter() {
let h = find_handler("brain.profiles");
assert!(!h.params.is_empty(), "brain.profiles must have params");
assert!(
h.params.iter().any(|p| p.name == "lifecycle"),
"brain.profiles must document lifecycle filter param"
);
}
#[test]
fn brain_resolve_params_has_consumer_kind_required() {
let h = find_handler("brain.resolve");
assert!(!h.params.is_empty(), "brain.resolve must have params");
assert!(
h.params
.iter()
.any(|p| p.name == "consumer_kind" && p.required),
"brain.resolve must have required consumer_kind"
);
assert!(
h.params.iter().any(|p| p.name == "actor"),
"brain.resolve must document optional actor"
);
assert!(
h.params.iter().any(|p| p.name == "namespace"),
"brain.resolve must document optional namespace"
);
}
#[test]
fn brain_bind_params_has_required_profile_id_and_optionals() {
let h = find_handler("brain.bind");
assert!(!h.params.is_empty(), "brain.bind must have params");
assert!(
h.params
.iter()
.any(|p| p.name == "profile_id" && p.required),
"brain.bind must have required profile_id"
);
assert!(
h.params.iter().any(|p| p.name == "actor"),
"brain.bind must document actor"
);
assert!(
h.params.iter().any(|p| p.name == "namespace"),
"brain.bind must document namespace"
);
assert!(
h.params.iter().any(|p| p.name == "consumer_kind"),
"brain.bind must document consumer_kind"
);
assert!(
h.params.iter().any(|p| p.name == "priority"),
"brain.bind must document priority"
);
}
#[test]
fn brain_unbind_params_non_empty_all_optional() {
let h = find_handler("brain.unbind");
assert!(!h.params.is_empty(), "brain.unbind must have params");
assert!(
h.params.iter().all(|p| !p.required),
"brain.unbind params must all be optional (filter semantics)"
);
assert!(
h.params.iter().any(|p| p.name == "profile_id"),
"brain.unbind must document profile_id filter"
);
assert!(
h.params.iter().any(|p| p.name == "actor"),
"brain.unbind must document actor filter"
);
}
#[test]
fn brain_activate_deactivate_archive_each_have_profile_id() {
for verb in ["brain.activate", "brain.deactivate", "brain.archive"] {
let h = find_handler(verb);
assert!(!h.params.is_empty(), "{verb} must have params");
assert!(
h.params
.iter()
.any(|p| p.name == "profile_id" && p.required),
"{verb} must have required profile_id param"
);
}
}
#[test]
fn brain_reset_params_has_optional_profile_id() {
let h = find_handler("brain.reset");
assert!(!h.params.is_empty(), "brain.reset must have params");
assert!(
h.params
.iter()
.any(|p| p.name == "profile_id" && !p.required),
"brain.reset profile_id must be optional (defaults to balanced-recall-v1)"
);
}
#[test]
fn brain_config_params_has_parameter() {
let h = find_handler("brain.config");
assert!(
!h.params.is_empty(),
"brain.config must document the parameter arg"
);
assert!(
h.params
.iter()
.any(|p| p.name == "parameter" && !p.required),
"brain.config parameter must be optional"
);
}
#[test]
fn brain_events_params_has_limit() {
let h = find_handler("brain.events");
assert!(
!h.params.is_empty(),
"brain.events must document the limit arg"
);
assert!(
h.params.iter().any(|p| p.name == "limit" && !p.required),
"brain.events limit must be optional"
);
}
#[test]
fn brain_emit_params_non_empty_with_target_and_signal() {
let h = find_handler("brain.emit");
assert!(
!h.params.is_empty(),
"brain.emit must have params (mirrors brain.feedback)"
);
assert!(
h.params.iter().any(|p| p.name == "target_id" && p.required),
"brain.emit must have required target_id"
);
assert!(
h.params.iter().any(|p| p.name == "signal" && p.required),
"brain.emit must have required signal"
);
}
#[test]
fn brain_bindings_params_all_optional() {
let h = find_handler("brain.bindings");
assert!(
h.params.iter().all(|p| !p.required),
"brain.bindings: all params must be optional filter args"
);
assert!(
h.params.iter().any(|p| p.name == "profile_id"),
"brain.bindings must document profile_id filter"
);
assert!(
h.params.iter().any(|p| p.name == "consumer_kind"),
"brain.bindings must document consumer_kind filter"
);
}
#[test]
fn brain_create_profile_params_has_required_name() {
let h = find_handler("brain.create_profile");
assert!(
!h.params.is_empty(),
"brain.create_profile must have params"
);
assert!(
h.params.iter().any(|p| p.name == "name" && p.required),
"brain.create_profile must have required name param"
);
assert!(
h.params
.iter()
.any(|p| p.name == "consumer_kind" && !p.required),
"brain.create_profile consumer_kind must be optional"
);
}
fn make_brain_registry() -> (VerbRegistry, KhiveRuntime) {
use khive_pack_kg::KgPack;
use khive_runtime::VerbRegistryBuilder;
let rt = KhiveRuntime::memory().expect("in-memory runtime for brain registry");
let mut builder = VerbRegistryBuilder::new();
builder.register(KgPack::new(rt.clone()));
builder.register(BrainPack::new(rt.clone()));
let registry = builder.build().expect("kg+brain registry builds");
(registry, rt)
}
#[tokio::test]
async fn r2_h1_bind_via_registry_preserves_namespace() {
let (registry, _rt) = make_brain_registry();
let result = registry
.dispatch(
"brain.bind",
json!({
"profile_id": "balanced-recall-v1",
"actor": "alice",
"namespace": "team-a",
"consumer_kind": "recall",
}),
)
.await
.expect("brain.bind must succeed");
assert_eq!(
result["namespace"],
json!("team-a"),
"brain.bind response must echo the caller-supplied namespace"
);
let bindings = registry
.dispatch(
"brain.bindings",
json!({
"profile_id": "balanced-recall-v1",
"namespace": "team-a",
}),
)
.await
.expect("brain.bindings must succeed");
assert_eq!(
bindings["count"],
json!(1u64),
"must find exactly one binding for namespace=team-a"
);
assert_eq!(
bindings["bindings"][0]["namespace"],
json!("team-a"),
"stored binding namespace must be team-a, not wildcard"
);
}
#[tokio::test]
async fn r2_h1_resolve_via_registry_uses_namespace() {
let (registry, _rt) = make_brain_registry();
registry
.dispatch(
"brain.bind",
json!({
"profile_id": "balanced-recall-v1",
"actor": "alice",
"namespace": "team-a",
"consumer_kind": "recall",
}),
)
.await
.expect("brain.bind team-a");
let resolved = registry
.dispatch(
"brain.resolve",
json!({
"actor": "alice",
"namespace": "team-a",
"consumer_kind": "recall",
}),
)
.await
.expect("brain.resolve must succeed for team-a");
assert_eq!(
resolved["resolved_profile_id"],
json!("balanced-recall-v1"),
"resolve must return the profile bound for team-a"
);
}
#[tokio::test]
async fn r2_h1_unbind_via_registry_uses_namespace() {
let (registry, _rt) = make_brain_registry();
registry
.dispatch(
"brain.bind",
json!({
"profile_id": "balanced-recall-v1",
"actor": "alice",
"namespace": "team-a",
"consumer_kind": "recall",
}),
)
.await
.expect("bind team-a");
registry
.dispatch(
"brain.bind",
json!({
"profile_id": "balanced-recall-v1",
"actor": "alice",
"namespace": "team-b",
"consumer_kind": "recall",
}),
)
.await
.expect("bind team-b");
let unbound = registry
.dispatch(
"brain.unbind",
json!({
"actor": "alice",
"namespace": "team-a",
}),
)
.await
.expect("unbind team-a");
assert_eq!(
unbound["unbound"],
json!(1u64),
"must remove exactly one binding (team-a)"
);
let remaining = registry
.dispatch(
"brain.bindings",
json!({
"actor": "alice",
"namespace": "team-b",
}),
)
.await
.expect("bindings after unbind");
assert_eq!(
remaining["count"],
json!(1u64),
"team-b binding must survive the team-a unbind"
);
}
}