@@ -691,21 +691,28 @@ def _explore_skeleton_enabled() -> bool:
691691 "direction",
692692 "related",
693693 "related_count",
694+ # A consumer decides whether the list is whole from these, so trimming a
695+ # large response must never drop them.
696+ "truncated",
697+ "related_total",
698+ "related_total_exact",
694699 "data_status",
695700 "provenance",
696701]
697702_CALL_GRAPH_OPTIONAL_KEYS = [
698703 "depth",
699704 "related",
700705 "related_count",
701- "truncated",
702706 "edges",
703707 "edge_count",
704708 "data_status",
705709 "ambiguity",
706710 "message",
707711 "snapshot",
708712]
713+ # Row ceiling of one symbol's caller lookup (`_find_callers_local`). A lookup that
714+ # returns this many rows may have stopped early, so traversal treats it as cut.
715+ _CALLER_LOOKUP_ROW_CAP = 1000
709716_BLAME_ESSENTIAL_KEYS = [
710717 "symbol_name",
711718 "file_path",
@@ -7325,7 +7332,9 @@ def tool_callers_batch(
73257332
73267333 placeholders = ",".join("?" for _ in names)
73277334 target_rows: dict[str, list[sqlite3.Row]] = {name: [] for name in names}
7328- caller_rows: dict[str, list[sqlite3.Row]] = {name: [] for name in names}
7335+ # Kept rows are keyed by caller symbol id; ``related_ids`` counts every caller, kept or not.
7336+ caller_rows: dict[str, dict[str, sqlite3.Row]] = {name: {} for name in names}
7337+ related_ids: dict[str, set[str]] = {name: set() for name in names}
73297338 with self._connect() as conn:
73307339 self._init_schema(conn)
73317340 rows = conn.execute(
@@ -7366,11 +7375,24 @@ def tool_callers_batch(
73667375 """,
73677376 (self.repo_id, *names),
73687377 ).fetchall()
7369- # Keep one extra unique row per name to preserve truncation metadata.
7378+ # The edge query has no LIMIT, so every caller is counted before the limit.
7379+ # Keep one extra unique caller per name to preserve truncation metadata.
73707380 for row in edge_rows:
73717381 name = str(row["callee_short_name"])
7372- if name in caller_rows and len(caller_rows[name]) <= bounded_limit:
7373- caller_rows[name].append(row)
7382+ if name not in caller_rows:
7383+ continue
7384+ hydrated_id = row["hydrated_symbol_id"]
7385+ if hydrated_id is not None:
7386+ caller_id = str(hydrated_id)
7387+ else:
7388+ cf = str(row["caller_file_path"])
7389+ cs = int(row["caller_start_line"])
7390+ cq = str(row["caller_qualified_name"])
7391+ caller_id = "local-call::" + hashlib.sha1(f"{cf}:{cs}:{cq}".encode()).hexdigest()[:16]
7392+ related_ids[name].add(caller_id)
7393+ kept = caller_rows[name]
7394+ if caller_id not in kept and len(kept) <= bounded_limit:
7395+ kept[caller_id] = row
73747396
73757397 payloads: dict[str, dict[str, Any]] = {}
73767398 for name in names:
@@ -7382,15 +7404,14 @@ def tool_callers_batch(
73827404 symbol = _row_to_symbol(dict(row))
73837405 targets.append({**symbol.model_dump(mode="json"), "provenance": _LOCAL_PROVENANCE})
73847406 nodes_by_id: dict[str, CallGraphNode] = {}
7385- for row in caller_rows.get(name, () ):
7407+ for caller_id, row in caller_rows[name].items( ):
73867408 cf = str(row["caller_file_path"])
73877409 cs = int(row["caller_start_line"])
73887410 cn = str(row["caller_symbol_name"])
73897411 cq = str(row["caller_qualified_name"])
7390- hydrated_id = row["hydrated_symbol_id"]
7391- if hydrated_id is not None:
7412+ if row["hydrated_symbol_id"] is not None:
73927413 node = CallGraphNode(
7393- symbol_id=str(hydrated_id) ,
7414+ symbol_id=caller_id ,
73947415 symbol_name=cn,
73957416 qualified_name=str(row["hydrated_qualified_name"] or cq),
73967417 file_path=cf,
@@ -7400,9 +7421,8 @@ def tool_callers_batch(
74007421 provenance="local_index",
74017422 )
74027423 else:
7403- synthetic_id = "local-call::" + hashlib.sha1(f"{cf}:{cs}:{cq}".encode()).hexdigest()[:16]
74047424 node = CallGraphNode(
7405- symbol_id=synthetic_id ,
7425+ symbol_id=caller_id ,
74067426 symbol_name=cn,
74077427 qualified_name=cq,
74087428 file_path=cf,
@@ -7411,7 +7431,7 @@ def tool_callers_batch(
74117431 end_line=int(row["caller_end_line"]),
74127432 provenance="local_index",
74137433 )
7414- nodes_by_id.setdefault(node.symbol_id, node)
7434+ nodes_by_id[caller_id] = node
74157435
74167436 all_nodes = sorted(nodes_by_id.values(), key=lambda item: (item.file_path, item.start_line, item.symbol_id))
74177437 truncated = len(all_nodes) > bounded_limit
@@ -7426,6 +7446,9 @@ def tool_callers_batch(
74267446 traversal = CallGraphTraversalResult(
74277447 nodes=nodes,
74287448 edges=edges,
7449+ # Every call edge for the name was read, so the pre-limit count is exact.
7450+ related_symbol_ids=sorted(related_ids[name]),
7451+ related_total_exact=True,
74297452 truncated=truncated,
74307453 data_status="available" if edges else "empty",
74317454 message=None if edges else "no related call edges were found",
@@ -10599,6 +10622,7 @@ def _tool_call_graph(
1059910622 limit=limit,
1060010623 snapshot=snapshot,
1060110624 lookup_neighbors=lambda current_symbol_id: lookup(symbol_id=current_symbol_id),
10625+ neighbor_cap=_CALLER_LOOKUP_ROW_CAP if direction == "callers" else None,
1060210626 )
1060310627 if traversal.data_status == "unavailable" and direction == "callers":
1060410628 fallback = self._fallback_callers_from_references(
@@ -10614,10 +10638,16 @@ def _tool_call_graph(
1061410638 nodes_by_identity: dict[tuple[str, str, int, int, str], CallGraphNode] = {}
1061510639 edges_by_key: dict[tuple[str, str, int], CallGraphEdge] = {}
1061610640 merged_truncated = False
10641+ # The edge store is name-keyed, so same-named targets share callers:
10642+ # union the totals by symbol id, never sum them.
10643+ related_ids: set[str] = set()
10644+ merged_exact = True
1061710645 status_rank = {"unavailable": 0, "empty": 1, "available": 2}
1061810646 merged_status = "unavailable"
1061910647 for current in traversals:
1062010648 merged_truncated = merged_truncated or current.truncated
10649+ related_ids.update(current.related_symbol_ids)
10650+ merged_exact = merged_exact and current.related_total_exact
1062110651 if status_rank[current.data_status] > status_rank[merged_status]:
1062210652 merged_status = current.data_status
1062310653 for node in current.nodes:
@@ -10660,6 +10690,8 @@ def _tool_call_graph(
1066010690 traversal = CallGraphTraversalResult(
1066110691 nodes=merged_nodes,
1066210692 edges=merged_edges,
10693+ related_symbol_ids=sorted(related_ids),
10694+ related_total_exact=merged_exact,
1066310695 truncated=merged_truncated,
1066410696 data_status=cast(Any, merged_status),
1066510697 message=merged_message,
@@ -10684,6 +10716,7 @@ def _tool_call_graph(
1068410716 edges_before = len(cast(list[dict[str, Any]], payload.get("edges", [])))
1068510717 payload["related"] = cast(list[dict[str, Any]], payload.get("related", []))[:max_related]
1068610718 payload["edges"] = cast(list[dict[str, Any]], payload.get("edges", []))[:max_related]
10719+ # Only the returned rows are recounted; related_total stays the pre-limit count.
1068710720 payload["related_count"] = len(cast(list[dict[str, Any]], payload.get("related", [])))
1068810721 payload["edge_count"] = len(cast(list[dict[str, Any]], payload.get("edges", [])))
1068910722 payload["truncated"] = (
@@ -12837,9 +12870,9 @@ def _find_callers_local(
1283712870 FROM call_edges
1283812871 WHERE repo_id = ? AND callee_short_name = ?
1283912872 ORDER BY caller_file_path, caller_start_line
12840- LIMIT 1000
12873+ LIMIT ?
1284112874 """,
12842- (self.repo_id, target_name),
12875+ (self.repo_id, target_name, _CALLER_LOOKUP_ROW_CAP ),
1284312876 ).fetchall()
1284412877 if not rows:
1284512878 return []
@@ -13060,13 +13093,15 @@ def _fallback_callers_from_references(
1306013093 nodes_by_id: dict[str, CallGraphNode] = {}
1306113094 edges: list[CallGraphEdge] = []
1306213095 seen_edges: set[tuple[str, str, int]] = set()
13096+ related_ids: set[str] = set()
1306313097 truncated = False
1306413098 for reference in references:
1306513099 if reference.file_path == target_file and target_start <= reference.line <= target_end:
1306613100 continue
1306713101 node = self._caller_node_from_reference(reference, target_symbol_id=target_symbol_id)
1306813102 if node is None:
1306913103 continue
13104+ related_ids.add(node.symbol_id)
1307013105 if node.symbol_id not in nodes_by_id:
1307113106 if len(nodes_by_id) >= limit:
1307213107 truncated = True
@@ -13088,6 +13123,8 @@ def _fallback_callers_from_references(
1308813123 return CallGraphTraversalResult(
1308913124 nodes=[],
1309013125 edges=[],
13126+ related_symbol_ids=[],
13127+ related_total_exact=False,
1309113128 truncated=False,
1309213129 data_status="unavailable",
1309313130 message="routed call edge data is unavailable",
@@ -13096,6 +13133,10 @@ def _fallback_callers_from_references(
1309613133 return CallGraphTraversalResult(
1309713134 nodes=ordered_nodes,
1309813135 edges=ordered_edges,
13136+ related_symbol_ids=sorted(related_ids),
13137+ # References come through a row-capped lookup whose saturation is not
13138+ # visible here, so this count is never claimed exact.
13139+ related_total_exact=False,
1309913140 truncated=truncated,
1310013141 data_status="available",
1310113142 message="fallback caller graph derived from symbol references",
0 commit comments