Skip to content
5 changes: 5 additions & 0 deletions crates/khive-mcp/src/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,11 @@ pub fn build_registry_for_multi_backend(
}
registry.call_register_embedders(&default_runtime);
registry.call_register_entity_type_validators(&default_runtime);
// #750 fix-round 1: install pack-owned note-mutation hooks (currently
// only khive-pack-memory's warm-ANN-cache invalidation) so KG's
// update/delete verbs notify caching packs even though there is no
// crate-level dependency between them.
registry.call_register_note_mutation_hooks(&default_runtime);

let backend_for_pack: HashMap<&str, &StorageBackend> = per_pack_runtimes_local
.iter()
Expand Down
5 changes: 5 additions & 0 deletions crates/khive-mcp/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,11 @@ impl KhiveMcpServer {
// entity-type validation is active at the runtime layer for all write
// paths, including direct `create_many` callers that bypass the handler.
registry.call_register_entity_type_validators(&runtime);
// #750 fix-round 1: install pack-owned note-mutation hooks (currently
// only khive-pack-memory's warm-ANN-cache invalidation) so KG's
// update/delete verbs notify caching packs even though there is no
// crate-level dependency between them.
registry.call_register_note_mutation_hooks(&runtime);
// Apply pack-auxiliary schema plans at startup so pack tables are
// present before any handler runs. Errors are logged but not propagated
// so a single pack's schema failure cannot abort startup.
Expand Down
32 changes: 32 additions & 0 deletions crates/khive-pack-kg/src/handlers/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,38 @@ pub(crate) fn tags_match_any(entity_tags: &[String], wanted: &[String]) -> bool
.any(|tag| wanted.iter().any(|w| tag.eq_ignore_ascii_case(w)))
}

/// Merge the top-level `tags` create-param into `properties["tags"]` for a
/// note. Notes have no dedicated tags column (see search.rs's `tag_filter`
/// handling) — `properties["tags"]` is the storage convention already used
/// by `memory.remember` (khive-pack-memory/src/handlers/remember.rs) and by
/// this pack's own `search`/`list` note-tag filters. Without this merge,
/// `create(kind=note, tags=[...])` silently dropped the tags (#747).
///
/// Precedence: an empty/absent `tags` param leaves `properties` untouched.
/// A non-empty `tags` param always WINS over any `properties["tags"]` the
/// caller also supplied — the top-level, typed param is the more explicit
/// signal, so it overwrites rather than merges with a same-named nested key.
pub(crate) fn merge_note_tags(
properties: Option<Value>,
tags: Option<Vec<String>>,
) -> Result<Option<Value>, RuntimeError> {
let tags = match tags {
Some(t) if !t.is_empty() => t,
_ => return Ok(properties),
};
let mut obj = match properties {
None => serde_json::Map::new(),
Some(Value::Object(m)) => m,
Some(other) => {
return Err(RuntimeError::InvalidInput(format!(
"create: note `tags` cannot be merged into non-object `properties` (got {other})"
)));
}
};
obj.insert("tags".to_string(), json!(tags));
Ok(Some(Value::Object(obj)))
}

// ---- Handler helpers ----

pub(crate) fn parse_entity_policy(s: &str) -> Result<EntityDedupMergePolicy, RuntimeError> {
Expand Down
3 changes: 2 additions & 1 deletion crates/khive-pack-kg/src/handlers/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ impl KgPack {
for s in p.annotates.unwrap_or_default() {
annotates.push(resolve_uuid_unfiltered(&s, &self.runtime, token).await?);
}
let properties = super::common::merge_note_tags(p.properties, p.tags)?;
let note = self
.runtime
.create_note(
Expand All @@ -343,7 +344,7 @@ impl KgPack {
p.name.as_deref(),
&content,
p.salience,
p.properties,
properties,
annotates,
)
.await?;
Expand Down
Loading
Loading