Skip to content

v1.1.12 - Katana

Choose a tag to compare

@orneryd orneryd released this 26 Jul 22:32
· 43 commits to main since this release

[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 the nolocalllm build tag. The
    built-in watcher plugin exposes this through
    make plugin-heimdall-watcher-remote or
    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 from b9835 to b10069, refreshed Go
    dependencies including BadgerDB v4.9.4, Prometheus client v1.24.0, gRPC
    v1.82.1, and golang.org/x modules, and updated UI dependencies including
    React 19.2.8, Lucide React 1.26.0, Three.js 0.185.1, Tailwind CSS
    4.3.3, and TypeScript 7.0.2. Build scripts and container images now use
    the matching runtime versions.

Fixed

  • Explicit auth.enabled values in YAML configuration now take precedence.
    An auth.enabled: false setting was previously indistinguishable from an
    omitted value and could leave authentication enabled by defaults or other
    configuration sources. Explicit true and false values are now both
    honored, while the --no-auth startup 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 bare COUNT/EXISTS subquery bodies.

    WHERE (n)--(), WHERE (n)-->(), WHERE (n)<--(), and the bracketed
    undirected form WHERE (n)-[r]-() previously fell through to the
    relationship-pattern gate's default branch, which treats an unrecognized
    expression as true -- so WHERE NOT (n)--() matched nothing and
    WHERE (n)--() was always true, regardless of the graph's actual shape.
    Separately, COUNT { (n)--() } and EXISTS { (n)--() } required their
    subquery body to start with MATCH, so a bare (unprefixed) body always
    returned 0 / false. Both gates now recognize the full range of
    bracket-less and undirected existence patterns.

  • Non-DETACH DELETE of a node that still has relationships now errors
    instead of silently cascade-deleting its edges (behavior change).

    MATCH (n) DELETE n previously called the storage engine's
    DeleteNode unconditionally, which cascade-deletes every adjacent edge --
    so a plain DELETE on a connected node quietly removed its relationships
    too, diverging from openCypher/Neo4j semantics. DELETE now 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
    DELETE is 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 to DETACH DELETE.

  • An OPTIONAL MATCH-bound relationship variable resolved to nil inside
    DELETE/SET/REMOVE's internal match probes, even when the OPTIONAL MATCH genuinely matched.
    executeDelete, executeSet, and
    executeRemove each 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
    detects OPTIONAL 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 embedded OPTIONAL 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 after OPTIONAL MATCH is
    now routed through the same compound-match handler the top-level dispatcher
    already uses, so the relationship variable resolves to the real edge.

  • REMOVE did not support relationship variables at all. REMOVE r.prop
    silently no-op'd (it only inspected *storage.Node values in the matched
    rows) and, independent of that gap, a REMOVE query returning more than one
    node variable emitted one duplicated result row per node instead of one row
    per match. REMOVE now removes properties from relationship variables the
    same way SET already does, and its RETURN handling builds exactly one
    row per matched row regardless of how many node/relationship variables are
    in scope. The same fix applies to a chained SET ... REMOVE ... clause in a
    single statement.

  • Traversal-seeded OPTIONAL MATCH projections are now evaluated instead of
    echoed as literal expression text.

    A read query whose primary MATCH contains a relationship pattern followed
    by one or more trailing OPTIONAL MATCH clauses (no intervening WITH)
    previously resolved its RETURN items with a string resolver that only
    understood var.prop and 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 like count(f) returned the string "count(f)",
    the primary MATCH's relationship variable was dropped entirely (so
    rel.weight returned "rel.weight"), chained second-level OPTIONAL MATCH
    clauses were silently swallowed (their bindings projected as literal text),
    and per-clause WHERE predicates were ignored. The path now executes the
    seed MATCH with relationship variables bound, left-outer joins every
    chained OPTIONAL MATCH clause in either direction (seeding from whichever
    endpoint is bound), evaluates projections through the real expression
    evaluator (with a fast path for plain var.prop/bare-variable items),
    routes aggregate projections through implicit-grouping aggregation with
    Cypher null semantics, and applies ORDER BY/SKIP/LIMIT. The clause
    semantics mirror Neo4j's runtime operators: a connected single-hop clause
    behaves like OptionalExpandAll (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 with Apply + Optional semantics:
    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/
    stdevp per Neo4j's StdevFunction, and RETURN items that contain
    aggregates inside larger expressions (e.g. count(x) + 1,
    coalesce(sum(w), 0)) are isolated and substituted exactly like Neo4j's
    isolateAggregation rewrite.

  • Bolt explicit transactions now bind top-level UNWIND rows before
    routing to mutation handlers.

    session.ExecuteWrite queries shaped as UNWIND ... 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 ... SET and UNWIND ... MATCH ... REMOVE in explicit
    transactions and sent them to executeSet / executeRemove without 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