From f82ba8d2ecaf88fe6360f4b552c7015af29f4128 Mon Sep 17 00:00:00 2001 From: Aromal Biju Date: Sun, 16 Aug 2026 20:50:26 +0530 Subject: [PATCH] fix(serve): honest overshoot notice when edges push a complete answer over budget (#2784) --- graphify/serve.py | 26 ++++++++++++++++++++++---- tests/test_serve.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/graphify/serve.py b/graphify/serve.py index 53fcad68b..392368f88 100644 --- a/graphify/serve.py +++ b/graphify/serve.py @@ -1087,11 +1087,29 @@ def _adj(n): # 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. + # never cut (#2601). When every node is shown the answer is complete, so + # edges are never dropped either (returning output[:cut_at] here would + # silently truncate them) — but that completeness guarantee is exactly + # why a query can quietly cost 4-6x its requested budget once the last + # node crosses the fit line (#2784): the check above only ever compared + # the FULL output (nodes+edges) against char_budget, so this branch was + # already known to be over budget, yet said nothing about it. Report the + # real size instead of silence — still the complete, non-truncated + # answer, just an honest one. if cut_count == 0: - return output + if len(output) <= char_budget: + return output + total_edges = sum(1 for l in lines if l.startswith("EDGE ")) + est_tokens = len(output) // 3 + return ( + f"[i] Complete answer over budget: all {total_nodes} nodes and " + f"{total_edges} edges shown (~{est_tokens} tokens vs the " + f"requested ~{token_budget}-token budget). Edges are never " + f"dropped once every node fits, so this is already the full " + f"answer — raising --budget further will not shrink it. Narrow " + f"with context_filter=['call'] or use get_node for a specific " + f"symbol to reduce size instead.\n\n" + ) + 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 e366fc0f2..7f24e974e 100644 --- a/tests/test_serve.py +++ b/tests/test_serve.py @@ -1523,6 +1523,51 @@ def test_subgraph_to_text_no_banner_when_only_edges_overflow(): assert len(edge_lines) == len(edges), "all edges must survive a complete answer" +def test_subgraph_to_text_overshoot_notice_when_edges_exceed_budget(): + """#2784: once every node fits, edges are never dropped (#2601) — but that + used to mean the char_budget check silently stopped applying, so a query + could cost 4-6x its requested budget with zero indication. The complete + answer must still be returned whole, but the overshoot must be visible and + must not repeat the "raise the budget" advice that caused the blow-up.""" + import itertools + + G = nx.Graph() + labels = [f"n{i}" for i in range(20)] + 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") + node_chars = len("\n".join(f"NODE {l} [src=f.py loc=L1 community=c]" for l in labels)) + budget = (node_chars // 3) + 5 # fits every node, nowhere near every edge + text = _subgraph_to_text(G, set(G.nodes), edges, token_budget=budget) + 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) + assert len(edge_lines) == len(edges), "complete answer: edges must not be dropped" + assert "Complete answer over budget" in text + assert str(len(labels)) in text and str(len(edges)) in text + assert "raise" not in text.lower(), "must not repeat the advice that caused the overshoot" + assert "TRUNCATED" not in text and "truncated" not in text + + +def test_subgraph_to_text_no_overshoot_notice_when_edges_fit_too(): + """The honest-overshoot notice is additive: a complete answer that already + fits the budget must render exactly as before, with no notice at all.""" + import itertools + + G = nx.Graph() + labels = [f"n{i}" for i in range(3)] + 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") + text = _subgraph_to_text(G, set(G.nodes), edges, token_budget=2000) + assert "Complete answer over budget" not in text + assert "TRUNCATED" not in text and "truncated" not in text + + def test_subgraph_to_text_order_is_deterministic(): """Equal-degree nodes render in a stable order regardless of set iteration.""" G = nx.Graph()