Skip to content

get_related_nodes 'both' direction fan-out double-counts for a reverse-resolved relationship name #2823

Closed

Description

@mstomar125

Summary

packages/agent/src/local_agent/tools.rs's exec_get_related_nodes tool (the local AI agent's get_related_nodes tool) supports direction: "both" (also its own default when the model omits direction), implemented by calling rel_ops::get_related_nodes twice — once with direction: "out", once with direction: "in" — and concatenating related_nodes from both calls with no deduplication:

let directions: Vec<&str> = match direction.as_str() {
    "out" => vec!["out"],
    "in" => vec!["in"],
    "both" => vec!["out", "in"],
    ...
};
let mut all_nodes: Vec<Value> = Vec::new();
for dir in &directions {
    let output = rel_ops::get_related_nodes(&ns, input).await?;
    for node_val in &output.related_nodes { ... all_nodes.push(...) }
}

This relies on an assumption that a Reverse-resolved relationship name (a schema-declared reverseName, or a built-in's fixed inverse like child_of) returns disjoint, non-overlapping node sets for direction: "out" vs direction: "in".

That assumption was only ever true by accident, as a side effect of the double-reversal bug fixed in #2791 / PR #2822: before that fix, direction: "out" on a Reverse-resolved name returned the real (correct) data, while direction: "in" almost always returned an incorrect, silent-zero result (the bug). Concatenating a real set with an empty one never double-counted.

PR #2822 fixes #2791 by making rel_ops::get_related_nodes return the SAME correct traversal for a Reverse-resolved name regardless of the requested direction (a reverse name only ever names one real traversal — see that PR's description for why this is the only design compatible with the issue's own requirement that --type invoices with no --direction flag, i.e. the CLI's default "out", must keep returning real data). As a direct, unavoidable consequence, exec_get_related_nodes's "both" fan-out now issues two calls that return the IDENTICAL node set for any reverse-resolved relationship name, so every related node is reported TWICE (and the returned count is doubled) whenever the local agent calls this tool with a reverse relationship name and direction omitted or set to "both".

This is a real, model-visible regression in tool output (duplicate rows, inflated counts), not a crash — the tool still returns a result, just a doubled one.

Why this can't be fixed at the rel_ops layer

The CLI's own default for --direction is also "out" (clap's default_value_t = Direction::Out), and by the time a direction reaches rel_ops::get_related_nodes/the daemon, there is no signal distinguishing "caller explicitly asked for out" from "caller's default filled in out" — both arrive as the literal string "out". Any fix that makes an explicit/default "out" return empty for a Reverse-resolved name would also break the CLI's plain, no-flag nodespace relationship get <id> --type invoices call, which #2791 explicitly requires to keep returning real data. So this has to be fixed on the packages/agent side, not in rel_ops.

Suggested fix

In exec_get_related_nodes's "both" branch, deduplicate all_nodes by node id before returning (or, more precisely: resolve the relationship name once, and if it resolves to a Reverse traversal, only query it once instead of fanning out to both directions — the two are otherwise guaranteed to be duplicates for that specific case).

Filed as a follow-up rather than fixed alongside #2791 because packages/agent is a separate ownership lane needing its own sign-off (per this repo's working conventions) — out of scope for a core-layer bug fix PR.

Repro

  1. Declare a schema relationship with a reverseName (e.g. Invoice.billed_to -> Customer, reverseName: "invoices"), create 2+ edges.
  2. Call the local agent's get_related_nodes tool with relationship_type: "invoices" and no direction (or direction: "both").
  3. Observe each related node appears twice in the result.

Activity

  1. malibio commented on Sep 25, 2026

    @malibio
    Collaborator

    Frontend: 264 files / 5286 tests passed

  2. self-assigned this
    on Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions