From 7d01244a034611421d563d90a854eda48eeeb036 Mon Sep 17 00:00:00 2001 From: AromalBiju1 <178022850+AromalBiju1@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:40:56 +0100 Subject: [PATCH] fix(serve): honest overshoot notice when edges push a complete answer over budget (#2784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An unconditional early return once no whole node line was cut returned the full edge section unchecked, so a query could emit ~6x the requested budget with no indication — and the truncation banner advised raising the budget, the exact trigger. When the node set fits but appending edges overruns the budget, prepend an honest 'complete answer over budget' notice (real counts, estimated vs requested tokens) without truncating edges, preserving the #2601 completeness guarantee. The genuinely-fits case is byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) --- 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 _subgraph_to_text(G: nx.Graph, nodes: set[str], edges: list[tuple], token_bu # 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()