From b645a3a7f1e4647c675b78bdca33dd5c4758fa00 Mon Sep 17 00:00:00 2001 From: Ben Younes <2910651+ousamabenyounes@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:20:21 +0100 Subject: [PATCH] fix(serve): suppress the query truncation banner when no nodes were cut (#2601) --- graphify/serve.py | 10 ++++++++++ tests/test_serve.py | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/graphify/serve.py b/graphify/serve.py index e899c32c..53fcad68 100644 --- a/graphify/serve.py +++ b/graphify/serve.py @@ -1082,6 +1082,16 @@ def _subgraph_to_text(G: nx.Graph, nodes: set[str], edges: list[tuple], token_bu total_nodes = sum(1 for l in lines if l.startswith("NODE ")) shown_nodes = output[:cut_at].count("\nNODE ") + (1 if output.startswith("NODE ") else 0) cut_count = total_nodes - shown_nodes + # Nodes render before edges, so a char-budget overflow whose cut lands + # past the last NODE line drops only trailing edges — no whole node is + # lost. Announcing "showing N of N nodes … among the 0 cut nodes" then + # reads as a false truncation warning that teaches an agent to distrust a + # complete answer and burn follow-up narrowing calls for nodes that were + # never cut (#2601). When every node is shown the answer is complete: + # return the full output with no banner rather than silently dropping + # edges under a misleading notice. + if cut_count == 0: + return output # Prominent notice at the TOP so a truncated answer can never be mistaken # for a complete one — silence used to read as absence (#BUG2). The # notice + end marker sit OUTSIDE char_budget by design (two bounded diff --git a/tests/test_serve.py b/tests/test_serve.py index ea49c3c7..e366fc0f 100644 --- a/tests/test_serve.py +++ b/tests/test_serve.py @@ -1496,6 +1496,33 @@ def test_subgraph_to_text_no_notice_when_under_budget(): assert "TRUNCATED" not in text and "truncated" not in text +def test_subgraph_to_text_no_banner_when_only_edges_overflow(): + """#2601: nodes render before edges, so a budget overflow that only trims + trailing edges cuts zero whole nodes. The banner must not fire with a + misleading "showing N of N nodes … among the 0 cut nodes" — that pushes an + agent to distrust a complete answer and issue pointless narrowing calls.""" + import itertools + + G = nx.Graph() + labels = [f"n{i}" for i in range(4)] + for lbl in labels: + G.add_node(lbl, label=lbl, source_file="f.py", source_location="L1", community="c") + edges = list(itertools.combinations(labels, 2)) + for u, v in edges: + G.add_edge(u, v, relation="calls", confidence="high") + # Budget large enough for every NODE line but not the trailing EDGE lines. + text = _subgraph_to_text(G, set(G.nodes), edges, token_budget=60) + node_lines = [l for l in text.splitlines() if l.startswith("NODE ")] + edge_lines = [l for l in text.splitlines() if l.startswith("EDGE ")] + assert len(node_lines) == len(labels), "every node must still be shown" + assert "TRUNCATED" not in text and "truncated" not in text + assert "cut nodes" not in text + # A complete answer renders the whole subgraph: suppressing the banner must + # not silently truncate the trailing edges either (a `return output[:cut_at]` + # would drop them and still pass the assertions above). + assert len(edge_lines) == len(edges), "all edges must survive a complete answer" + + def test_subgraph_to_text_order_is_deterministic(): """Equal-degree nodes render in a stable order regardless of set iteration.""" G = nx.Graph()