Skip to content

fix(runtime,brain,kg): resolve feedback (visible-set) + by-ID (unfiltered) targets under Rev 6 (#391)#393

Open
ohdearquant wants to merge 3 commits into
mainfrom
fix-391-feedback-visible-set
Open

fix(runtime,brain,kg): resolve feedback (visible-set) + by-ID (unfiltered) targets under Rev 6 (#391)#393
ohdearquant wants to merge 3 commits into
mainfrom
fix-391-feedback-visible-set

Conversation

@ohdearquant

Copy link
Copy Markdown
Owner

Outage

brain.auto_feedback / brain.feedback has been erroring no record matches id prefix / target_id … not found in namespace … for every episodic memory
written since 2026-06-19
. Root cause (live-reproduced on a 20-hour-old memory
fd61fef5): under ADR-007 Rev 6, EPISODIC memory notes are stamped with the
actor id namespace (e.g. lambda:leo), but feedback resolved the target
using the caller token namespace (local) only. The row was invisible to
feedback resolution, so the fleet-wide feedback-discipline loop has been down.
Pre-Rev-6 rows (ns=local) still resolved, which is why old memories worked
and new ones didn't.

Two distinct resolution policies land in this PR — kept deliberately separate

  1. feedback (leg B = hex-prefix in resolve_auto_feedback_target, leg C =
    exact-UUID in handle_feedback) → visible-set-aware resolution. Feedback
    targets are recall outputs, and recall is already visible-set-aware
    ({local} ∪ {actor.id} ∪ {actor.visible_namespaces}, exposed as
    token.visible_namespaces()). Feedback now resolves over that same set
    instead of the primary token namespace only — one visibility principle, one
    code path as recall.
  2. by-ID CRUD (get/update/delete/merge) prefix path →
    fully unfiltered. Empirical finding: the full-UUID by-ID path
    (resolve_by_id, get_note, update_note, merge_*) was already
    namespace-agnostic — ADR-007 Rev 6 states by-ID ops carry no visibility
    boundary; the Gate is the authz seam, not storage-layer filtering. Only the
    hex-prefix expansion step (resolve_uuid_async in
    khive-pack-kg/src/handlers/common.rs) was still primary-namespace-only,
    silently narrowing prefix lookups to a boundary the full-UUID path never
    had. This is a separate policy from feedback's — by-ID has no visibility
    boundary at all, feedback's boundary is the visible set.

Both share one implementation: resolve_prefix_inner now takes
namespaces: Option<&[String]> (operations.rs) — Some([primary]) for the
existing strict resolvers, Some(visible_set) for the new
resolve_prefix_visible, None for the new
resolve_prefix_unfiltered/resolve_prefix_unfiltered_including_deleted.
khive-pack-kg/src/handlers/common.rs gets matching
resolve_uuid_unfiltered/resolve_uuid_unfiltered_including_deleted wrappers,
wired only into get.rs, update.rs (update + soft/hard delete), and
merge.rs.

Deliberately untouched

  • resolve_primary and strict resolve_prefix (via resolve_uuid_async) —
    used by gtd depends_on validation and link/edge endpoint validation. Their
    primary-namespace-only strictness is a tested contract for those callers,
    not a bug. Confirmed still enforced:
    resolve_primary_rejects_visible_only_{task,entity} (gtd dependencies.rs)
    stay green.
  • resolve_uuid_async prefix resolution in link.rs, create.rs, list.rs,
    graph.rs — same root-cause family (primary-ns-only prefix resolution), but
    staying strict here is a deliberate, separate design question (can an edge
    endpoint or a list/graph filter root cross a namespace boundary?). Noted as
    adjacent, intentionally out of scope for this PR.
  • merge_entity's namespace-ownership check (curation.rs) — a separate,
    pre-existing mutation-safety mechanism downstream of id resolution. With
    this fix, merge-by-prefix now resolves cross-namespace (proven by the
    changed error: resolution succeeds, then the ownership check legitimately
    rejects the cross-namespace merge — no longer a resolution-layer NotFound).
    The ownership check itself is out of scope.
  • Leg A (profile-registry per-session staleness) — a different root cause and
    fix locus (reload-on-miss), tracked separately from this id-resolution fix.

Tests

  • khive-pack-brain/src/tests.rs: 3 positive shapes (old local row, new
    actor-ns row by both prefix and full UUID, explicit namespace= override) +
    1 negative (third-party namespace not in the caller's visible set still
    NotFound) for both brain.auto_feedback and brain.feedback.
  • khive-pack-brain/tests/dispatch_hook.rs: the stale
    brain_feedback_rejects_visible_only_target_id test (which asserted the
    bug's own behavior, added by PR feat(runtime): multi-namespace read visibility (visible-set tokens) #127) is renamed
    brain_feedback_accepts_visible_only_target_id and inverted to assert the
    corrected behavior.
  • khive-pack-kg/tests/integration.rs: 8 new tests for by-ID CRUD's unfiltered
    policy — get/update/soft-delete/hard-delete by prefix cross-namespace now
    succeed; full-UUID cross-namespace guard retained; merge by prefix
    cross-namespace resolves and then correctly hits the separate ownership
    check (not a resolution error); merge by prefix same-namespace succeeds;
    a prefix matching nothing still raises NotFound.

All tests use an isolated in-process runtime over a temp/in-memory SQLite
database — never the production khive.db.

Gates (all green, run from crates/)

  • cargo fmt --all -- --check — rc 0
  • cargo clippy --workspace --all-targets -- -D warnings — rc 0
  • cargo test --workspace --locked — rc 0 (0 failed across every test binary)
  • RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace — rc 0
  • cargo test -p khive-pack-gtd --test dependencies --locked — rc 0 (no-regression anchor: resolve_primary_rejects_visible_only_* stay green)
  • cargo test -p khive-runtime --test integration --locked — rc 0 (no-regression anchor: visible_set_reads_primary_and_extra_not_third, resolve_uses_visible_set_for_note_in_extra_namespace, hybrid_search_surfaces_all_visible_namespaces stay green)

Do NOT merge

This PR is gated by λ:khive: codex review → λ second-look on the two review
anchors above → Ocean HC-7 approval.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

🤖 Generated with Claude Code

ohdearquant and others added 2 commits July 1, 2026 22:51
…ered) targets under Rev 6 (#391)

brain.auto_feedback/brain.feedback have errored ("no record matches id
prefix" / "target_id ... not found in namespace ...") for every episodic
memory written since 2026-06-19. Root cause: two independent id-resolution
seams never picked up the ADR-007 Rev 6 visible-set model.

Two distinct policies are fixed here, kept deliberately separate:

1. Feedback (legs B/C) -- visible-set-aware resolution.
   `resolve_auto_feedback_target` (hex-prefix, leg B) and `handle_feedback`
   (exact UUID, leg C) now resolve against the token's visible-namespace
   set, mirroring how memory.recall already reads across
   {local} union {actor.id} union {actor.visible_namespaces}. New
   `resolve_prefix_visible` sits alongside the pre-existing `resolve`
   (exact-UUID visible-set variant), completing the strict/visible pair
   for prefix resolution the same way `resolve_primary`/`resolve` already
   pairs for exact UUIDs.

2. By-ID CRUD (get/update/delete/merge) -- fully unfiltered resolution.
   Empirical finding: full-UUID by-ID lookups (`resolve_by_id`,
   `get_note`, `update_note`, `merge_*`) were already namespace-agnostic,
   per ADR-007 Rev 6's own rule that by-ID ops carry no visibility
   boundary (the Gate is the authz seam, not storage-layer filtering).
   Only the hex-prefix expansion step (`resolve_uuid_async`, common.rs)
   was still primary-namespace-only, silently narrowing prefix lookups to
   a boundary the full-UUID path doesn't have. New
   `resolve_prefix_unfiltered`/`resolve_prefix_unfiltered_including_deleted`
   (operations.rs) and `resolve_uuid_unfiltered`/
   `resolve_uuid_unfiltered_including_deleted` (kg common.rs) close that
   gap; wired into get.rs, update.rs (update + soft/hard delete), and
   merge.rs only.

`resolve_prefix_inner` takes `namespaces: Option<&[String]>` so a single
implementation serves all three resolution policies (`Some([primary])`
for strict, `Some(visible_set)` for feedback, `None` for by-ID CRUD) via
thin wrappers, rather than duplicating the prefix-matching SQL a third
time.

Deliberately untouched:
- `resolve_primary` and strict `resolve_prefix` (via `resolve_uuid_async`)
  for gtd `depends_on` validation and link/edge endpoint validation --
  their primary-namespace-only strictness is a tested contract for those
  callers, not a bug.
- `resolve_uuid_async` call sites in link.rs, create.rs, list.rs, graph.rs
  -- same root cause family, deliberately out of scope; list/search keep
  their namespace filter by design.
- `merge_entity`'s namespace-ownership check (curation.rs) -- a separate,
  pre-existing mutation-safety mechanism downstream of id resolution.
  Merge-by-prefix now resolves cross-namespace (proven by the changed
  error: resolution success -> ownership-check rejection, not
  resolution failure), but the ownership check itself still legitimately
  blocks a cross-namespace merge. Not in scope for #391.
- Leg A (profile-registry per-session staleness) -- tracked separately,
  unrelated to id resolution.

Regression coverage:
- khive-pack-brain: `resolve_auto_feedback_target`/`handle_feedback`
  visible-set behavior -- 3 positive shapes (primary, extra-visible,
  hex-prefix-in-extra-visible) + 1 negative (third-party, non-visible
  namespace) (src/tests.rs). dispatch_hook.rs's stale
  `brain_feedback_rejects_visible_only_target_id` test (asserting the
  bug's own behavior, from PR #127) is renamed
  `brain_feedback_accepts_visible_only_target_id` and inverted to assert
  the corrected behavior.
- khive-pack-kg integration.rs: 8 new tests for by-ID CRUD's unfiltered
  policy -- get/update/soft-delete/hard-delete by prefix cross-namespace
  now succeed; full-UUID cross-namespace guard retained; merge by prefix
  cross-namespace resolves then correctly hits the separate ownership
  check (not a resolution error); merge by prefix same-namespace
  succeeds; prefix matching nothing still raises NotFound.

Gates (all from crates/, rc 0):
  cargo fmt --all -- --check
  cargo clippy --workspace --all-targets -- -D warnings
  cargo test --workspace --locked
  RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace
  cargo test -p khive-pack-gtd --test dependencies --locked   (no-regression)
  cargo test -p khive-runtime --test integration --locked     (no-regression)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (codex r1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ltered by-ID contract

test_read_isolation_between_namespaces asserted the pre-#391 policy: an
8-char prefix get from a foreign namespace must return not-found because
prefix expansion was namespace-scoped. This PR deliberately corrects that
contract — by-ID prefix resolution is now unfiltered, matching the
full-UUID by-ID path (ADR-007 Rev 6 Rule 2). The assertion now expects the
cross-namespace prefix to resolve and return the entity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant