v1.1.12 - Katana
[v1.1.12] - Katana - 7/26/2026
Added
- Knowledge-policy architecture documentation.
Added end-to-end scoring-pipeline and visibility-layer guides covering policy
evaluation, score composition, temporal decay, access decisions, caching,
observability, and integration with NornicDB's storage and query layers. - Remote-provider-only Heimdall plugin builds.
Heimdall plugins that use remote providers such as OpenAI or Ollama can now be
built without the local GGUF runtime by using thenolocalllmbuild tag. The
built-in watcher plugin exposes this through
make plugin-heimdall-watcher-remoteor
make plugin-heimdall-watcher NOLOCALLLM=1, with matching deployment and Go
plugin ABI guidance in the documentation.
Changed
- Updated supported runtimes and dependencies.
Upgraded the bundled llama.cpp runtime fromb9835tob10069, refreshed Go
dependencies including BadgerDBv4.9.4, Prometheus clientv1.24.0, gRPC
v1.82.1, andgolang.org/xmodules, and updated UI dependencies including
React19.2.8, Lucide React1.26.0, Three.js0.185.1, Tailwind CSS
4.3.3, and TypeScript7.0.2. Build scripts and container images now use
the matching runtime versions.
Fixed
-
Explicit
auth.enabledvalues in YAML configuration now take precedence.
Anauth.enabled: falsesetting was previously indistinguishable from an
omitted value and could leave authentication enabled by defaults or other
configuration sources. Explicittrueandfalsevalues are now both
honored, while the--no-authstartup flag remains the final override. -
Heimdall native tool-calling follow-up requests now preserve empty assistant
content. OpenAI-compatible and Ollama request payloads now serialize
content: ""for assistant messages containing tool calls instead of
omitting the field, preventing second-round HTTP 400 responses from providers
that require it. Plugin load failures caused by a mismatched Go package build
now also report the host toolchain and build settings that must match. -
Large disjoint UNIQUE-constrained write batches no longer serialize on
hash-lock collisions.
Replaced the fixed 256-stripe commit-lock table with an active,
reference-counted exact-value registry. Transactions touching the same
(label, property, value)still serialize through validation, Badger commit,
and unique-cache publication, while disjoint batches commit concurrently.
Registry-assigned ordering prevents AB-BA deadlocks, entries expire after the
last holder or waiter, and non-reflexive values such as NaN cannot leak lock
entries. -
WHERE relationship-existence predicates now recognize bracket-less and
undirected patterns, and bareCOUNT/EXISTSsubquery bodies.
WHERE (n)--(),WHERE (n)-->(),WHERE (n)<--(), and the bracketed
undirected formWHERE (n)-[r]-()previously fell through to the
relationship-pattern gate's default branch, which treats an unrecognized
expression astrue-- soWHERE NOT (n)--()matched nothing and
WHERE (n)--()was always true, regardless of the graph's actual shape.
Separately,COUNT { (n)--() }andEXISTS { (n)--() }required their
subquery body to start withMATCH, so a bare (unprefixed) body always
returned0/false. Both gates now recognize the full range of
bracket-less and undirected existence patterns. -
Non-DETACH
DELETEof a node that still has relationships now errors
instead of silently cascade-deleting its edges (behavior change).
MATCH (n) DELETE npreviously called the storage engine's
DeleteNodeunconditionally, which cascade-deletes every adjacent edge --
so a plainDELETEon a connected node quietly removed its relationships
too, diverging from openCypher/Neo4j semantics.DELETEnow validates that
every node in the deletion plan has no relationships left outside the
statement's own edge deletions before mutating anything (so a multi-row
DELETEis judged as a whole, not partially applied), and errors with the
same "still has relationships" wording Neo4j uses. Deleting a node together
with its own edge in one statement (DELETE a, r) still works, since the
edge being removed no longer counts as residual. Existing callers relying on
the old silent cascade must switch toDETACH DELETE. -
An
OPTIONAL MATCH-bound relationship variable resolved tonilinside
DELETE/SET/REMOVE's internal match probes, even when theOPTIONAL MATCHgenuinely matched.executeDelete,executeSet, and
executeRemoveeach build an internal
MATCH ... OPTIONAL MATCH (a)-[r:TYPE]->(b) RETURN <vars>probe and execute
it via the low-level match path instead of the dispatcher that normally
detectsOPTIONAL MATCH. That low-level path located the relationship
bracket by scanning forward from the character right after the first node
group's closing paren, an assumption the embeddedOPTIONAL MATCH (n)text
broke: the corrupted substring handed to the relationship-pattern parser no
longer started with[, so the bound variable, type, and properties were
silently dropped. A relationship pattern embedded afterOPTIONAL MATCHis
now routed through the same compound-match handler the top-level dispatcher
already uses, so the relationship variable resolves to the real edge. -
REMOVEdid not support relationship variables at all.REMOVE r.prop
silently no-op'd (it only inspected*storage.Nodevalues in the matched
rows) and, independent of that gap, aREMOVEquery returning more than one
node variable emitted one duplicated result row per node instead of one row
per match.REMOVEnow removes properties from relationship variables the
same waySETalready does, and itsRETURNhandling builds exactly one
row per matched row regardless of how many node/relationship variables are
in scope. The same fix applies to a chainedSET ... REMOVE ...clause in a
single statement. -
Traversal-seeded
OPTIONAL MATCHprojections are now evaluated instead of
echoed as literal expression text.
A read query whose primaryMATCHcontains a relationship pattern followed
by one or more trailingOPTIONAL MATCHclauses (no interveningWITH)
previously resolved itsRETURNitems with a string resolver that only
understoodvar.propand bare variables. Every other expression came back
as its own source text:RETURN type(rel)returned the literal string
"type(rel)",coalesce(...)/labels(...)/head(...)returned their
source text, aggregates likecount(f)returned the string"count(f)",
the primary MATCH's relationship variable was dropped entirely (so
rel.weightreturned"rel.weight"), chained second-levelOPTIONAL MATCH
clauses were silently swallowed (their bindings projected as literal text),
and per-clauseWHEREpredicates were ignored. The path now executes the
seedMATCHwith relationship variables bound, left-outer joins every
chainedOPTIONAL MATCHclause in either direction (seeding from whichever
endpoint is bound), evaluates projections through the real expression
evaluator (with a fast path for plainvar.prop/bare-variable items),
routes aggregate projections through implicit-grouping aggregation with
Cypher null semantics, and appliesORDER BY/SKIP/LIMIT. The clause
semantics mirror Neo4j's runtime operators: a connected single-hop clause
behaves likeOptionalExpandAll(null seeds propagate null bindings), and
every other shape — a disconnected pattern sharing no variable with earlier
clauses, a single-node pattern, a multi-hop chain, or a pre-bound
relationship variable — is evaluated withApply+Optionalsemantics:
matches extend the row, and a row with no match is preserved once with
newly-introduced variables bound to null. No valid shape is rejected.
Aggregation follows Neo4j's model end to end: implicit grouping by the
non-aggregate items, identity values over empty ungrouped input,stdev/
stdevpper Neo4j'sStdevFunction, andRETURNitems that contain
aggregates inside larger expressions (e.g.count(x) + 1,
coalesce(sum(w), 0)) are isolated and substituted exactly like Neo4j's
isolateAggregationrewrite. -
Bolt explicit transactions now bind top-level
UNWINDrows before
routing to mutation handlers.
session.ExecuteWritequeries shaped asUNWIND ... MATCH ... DELETE
previously routed directly to the delete handler before the UNWIND variable
was bound, returned success with zero delete counters, and left matching
relationships intact. The same substring-based routing also intercepted
UNWIND ... MATCH ... SETandUNWIND ... MATCH ... REMOVEin explicit
transactions and sent them toexecuteSet/executeRemovewithout binding
the UNWIND variable. Explicit transactions now use the same UNWIND-first
dispatch order as autocommit for DELETE, DETACH DELETE, SET, and REMOVE, and
aggregate per-row mutation counters (nodes/relationships created/deleted,
properties set, labels added) into Bolt result summaries so downstream
clients observe accurate counters.
What's Changed
- Fix UNIQUE commit lock false sharing by @linuxdynasty in #261
- fix(cypher): bind UNWIND deletes in explicit transactions by @linuxdynasty in #260
- fix(cypher): correctly evaluate relationship-existence predicates and guard non-detach DELETE by @linuxdynasty in #264
- fix(cypher): support every traversal-seeded OPTIONAL MATCH + aggregate shape by mirroring Neo4j by @linuxdynasty in #265
Full Changelog: v1.1.11...v1.1.12