Skip to content

fix(graph): get_neighbors may not leave its owner, and stop rescanning per hop - #188

Merged
Cipher208 merged 1 commit into
masterfrom
fix/neighbors-owner-scope
Nov 3, 2026
Merged

Cipher208 merged 1 commit into
masterfrom
fix/neighbors-owner-scope

Conversation

@Cipher208

Copy link
Copy Markdown
Owner

Что

get_neighbors возвращал содержимое соседа, фильтруя только по слою — ни user_id на возвращаемом узле, ни на стартовом. Поэтому граф слоя agent одного владельца отдавал текст другого.

Замер на живых базах (read-only): depth-1 от узла default возвращал 14 строк, принадлежащих test_hooks, во всех трёх базах; depth-2 в CowAgent — 301.

база старт было стало
Hermes 3 (agent) 53 строки, 14 чужих 39, 0
CowAgent 3, depth 2 1 119, 301 чужой 818, 0
CowAgent 117 (lily) 14, 14 чужих 0, 0

Контракт

Владелец берётся из самого стартового узла, а не из нового параметра: забыть передать параметр — ровно тот дефект, который чиним.

  • каждый хоп, а не только финальная строка, обязан попасть в member (тот же слой И тот же user_id) — чужой узел не может сработать мостом;
  • стартового узла нет или он в другом слое → [], без исключения: не знаем владельца — не отдаём ничего;
  • depth, relation, tag работают как раньше.

Оптимизация

Первая версия фикса была 6× медленнее на depth-2 в самом большом user-слое (0.22 с → 1.30 с): IN (SELECT node_id FROM member) считался на каждом хопе.

member AS MATERIALIZED + JOIN по target_id:

сценарий IN JOIN + MATERIALIZED
Hermes user depth 2 (start 15988) 1 105.8 мс 58.6 мс
Hermes user depth 3 27.2 с 7.0 с
Hermes user depth 1 12.6 мс 16.2 мс

Наборы строк идентичны (сверено хэшами). Без MATERIALIZED JOIN инлайнится в рекурсивный шаг и пересканирует epi_nodes — 4.7 с, хуже исходного. Подсказка несущая, не косметика. Depth-1 платит 3.6 мс.

Про латентность

Сейчас метод зовут только тесты. rag/multi_source.py и rag/edm.py делают свой SQL и user_id на возвращаемом узле уже проверяют. То есть утечка латентная — но метод публичный, и он вернул бы чужое в первый же день, когда его позвали бы.

Проверка

  • 2836 passed (2832 + 4 новых);
  • ruff check чисто, ruff format 751 файл, mypy 63 = baseline;
  • blind verifier: 5/5 PASS, «Can get_neighbors return another owner's content on any path? — NO»;
  • adversarial: self-loop, цикл A→B→C→A на depth 5 (завершается), висячее ребро, depth=0, два владельца в одном слое — чужое не возвращается;
  • foreign-row check на 9 комбинациях база×узел: 0 чужих, 0 кросс-слойных на depth 1 и 2.

…g per hop

`get_neighbors` returned a neighbour's content filtered by LAYER only — no
user_id on the returned node, none on the start node. So the agent graph of one
owner handed back text belonging to another: measured on the live bases, a
depth-1 walk from a `default` node returned 14 rows owned by `test_hooks` in
every one of the three, and a depth-2 walk in CowAgent returned 301.

The owner is now taken from the start node itself, not from a new parameter —
forgetting to pass one is exactly the defect being fixed. Every hop, not just
the final row, must land inside `member` (same layer AND same user_id), so a
foreign node cannot bridge two of the owner's nodes. A missing start node, or
one in another layer, returns `[]` rather than raising: if the owner is unknown,
nothing is returned.

The first version of this fix was 6x slower at depth 2 on the largest user layer
(0.22 s -> 1.30 s) because `IN (SELECT node_id FROM member)` was evaluated per
hop. `member AS MATERIALIZED` joined on `target_id` brings that to 58.6 ms on
Hermes depth 2 against 1105.8 ms for the `IN` form (identical row sets), and
depth 3 from 27.2 s to 7.0 s. Without `MATERIALIZED` the join is inlined into the
recursive step and re-scans `epi_nodes` — 4.7 s, worse than what it replaced —
so the hint is load-bearing, not decoration. Depth 1 pays 3.6 ms for it.

Only tests call this method today (`rag/multi_source.py` and `rag/edm.py` use
their own SQL and already scope user_id on the returned node), so the leak was
latent — but it is a public method and would have returned foreign content the
first day anything called it.
@coderabbitai

coderabbitai Bot commented Nov 3, 2026

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

@github-actions github-actions Bot added the fix label Nov 3, 2026
@Cipher208
Cipher208 merged commit b5060fe into master Nov 3, 2026
19 checks passed
@Cipher208
Cipher208 deleted the fix/neighbors-owner-scope branch November 3, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant