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
Open
fix(runtime,brain,kg): resolve feedback (visible-set) + by-ID (unfiltered) targets under Rev 6 (#391)#393ohdearquant wants to merge 3 commits into
ohdearquant wants to merge 3 commits into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Outage
brain.auto_feedback/brain.feedbackhas been erroringno record matches id prefix/target_id … not found in namespace …for every episodic memorywritten 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 theactor id namespace (e.g.
lambda:leo), but feedback resolved the targetusing the caller token namespace (
local) only. The row was invisible tofeedback resolution, so the fleet-wide feedback-discipline loop has been down.
Pre-Rev-6 rows (
ns=local) still resolved, which is why old memories workedand new ones didn't.
Two distinct resolution policies land in this PR — kept deliberately separate
resolve_auto_feedback_target, leg C =exact-UUID in
handle_feedback) → visible-set-aware resolution. Feedbacktargets are recall outputs, and recall is already visible-set-aware
(
{local} ∪ {actor.id} ∪ {actor.visible_namespaces}, exposed astoken.visible_namespaces()). Feedback now resolves over that same setinstead of the primary token namespace only — one visibility principle, one
code path as recall.
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 alreadynamespace-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_asyncinkhive-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_innernow takesnamespaces: Option<&[String]>(operations.rs) —Some([primary])for theexisting strict resolvers,
Some(visible_set)for the newresolve_prefix_visible,Nonefor the newresolve_prefix_unfiltered/resolve_prefix_unfiltered_including_deleted.khive-pack-kg/src/handlers/common.rsgets matchingresolve_uuid_unfiltered/resolve_uuid_unfiltered_including_deletedwrappers,wired only into
get.rs,update.rs(update + soft/hard delete), andmerge.rs.Deliberately untouched
resolve_primaryand strictresolve_prefix(viaresolve_uuid_async) —used by gtd
depends_onvalidation and link/edge endpoint validation. Theirprimary-namespace-only strictness is a tested contract for those callers,
not a bug. Confirmed still enforced:
resolve_primary_rejects_visible_only_{task,entity}(gtddependencies.rs)stay green.
resolve_uuid_asyncprefix resolution inlink.rs,create.rs,list.rs,graph.rs— same root-cause family (primary-ns-only prefix resolution), butstaying 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.
fix locus (reload-on-miss), tracked separately from this id-resolution fix.
Tests
khive-pack-brain/src/tests.rs: 3 positive shapes (oldlocalrow, newactor-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 bothbrain.auto_feedbackandbrain.feedback.khive-pack-brain/tests/dispatch_hook.rs: the stalebrain_feedback_rejects_visible_only_target_idtest (which asserted thebug's own behavior, added by PR feat(runtime): multi-namespace read visibility (visible-set tokens) #127) is renamed
brain_feedback_accepts_visible_only_target_idand inverted to assert thecorrected behavior.
khive-pack-kg/tests/integration.rs: 8 new tests for by-ID CRUD's unfilteredpolicy — 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 0cargo clippy --workspace --all-targets -- -D warnings— rc 0cargo test --workspace --locked— rc 0 (0 failed across every test binary)RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace— rc 0cargo 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_namespacesstay 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