Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions internal/mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,12 @@ type Server struct {
equivalence *search.EquivalenceTable
contractRegistry *contracts.Registry
semanticMgr *semantic.Manager
// refsConfirmed is the lazy-enrichment ledger: symbol IDs whose incoming
// references have been confirmed on demand (via ConfirmSymbolRefs) this
// daemon session, so a repeat usages query serves from the confirmed graph
// instead of re-spawning the language server. An entry is recorded after
// the first attempt (success or empty) to bound per-query LSP work.
refsConfirmed sync.Map // symbolID → struct{}
feedback *feedbackManager
notes *notesManager
memories *memoryManager
Expand Down
9 changes: 9 additions & 0 deletions internal/mcp/tools_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -2278,6 +2278,10 @@ func (s *Server) handleGetCallers(ctx context.Context, req mcp.CallToolRequest)
RepoAllow: resolved.RepoAllow,
ExcludeTests: req.GetBool("exclude_tests", false),
}
// Lazy enrichment: confirm this symbol's callers on demand before
// answering, so a graph indexed without the eager LSP sweep still returns
// compiler-grade callers. No-op when already confirmed or eager ran.
s.confirmSymbolRefsOnDemand(eng.GetSymbol(id))
s.hydrateProxyTargets(ctx, id)
sg := eng.GetCallers(id, opts)
sg = filterSubGraphByResolvedScope(sg, resolved)
Expand Down Expand Up @@ -2505,6 +2509,11 @@ func (s *Server) handleFindUsages(ctx context.Context, req mcp.CallToolRequest)
if node != nil && !resolvedScopeAllowsNode(resolved, node) {
return symbolNotFoundGuidance(id), nil
}
// Lazy enrichment: confirm this symbol's incoming references on demand
// before answering, so a graph indexed without the eager LSP sweep still
// converges to compiler-grade usages. No-op when already confirmed or eager
// ran.
s.confirmSymbolRefsOnDemand(node)
opts := query.QueryOptions{
WorkspaceID: resolved.WorkspaceID,
ProjectID: resolved.ProjectID,
Expand Down
33 changes: 33 additions & 0 deletions internal/mcp/tools_lsp.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,39 @@ func (s *Server) enrichNodeOnDemand(node *graph.Node) {
_, _ = provider.EnrichNode(s.graph, s.workspaceRootFor(absPath), node)
}

// confirmSymbolRefsOnDemand faults in a callable symbol's INCOMING references
// on a usages-shaped query (find_usages / get_callers) when the synchronous
// LSP sweep is off. It lazy-spawns the language server, confirms this one
// symbol's callers via call hierarchy, and lands the lsp_resolved edges into
// the graph so the query answer — and every repeat — reflects compiler-grade
// usages accuracy. Idempotent per session via the refsConfirmed ledger; a
// no-op for non-callable nodes, when no call-hierarchy server serves the
// language, or when already confirmed.
func (s *Server) confirmSymbolRefsOnDemand(node *graph.Node) {
if node == nil || s.semanticMgr == nil || s.graph == nil {
return
}
if node.Kind != graph.KindFunction && node.Kind != graph.KindMethod {
return
}
if _, done := s.refsConfirmed.Load(node.ID); done {
return
}
absPath, err := s.absolutePath(node.FilePath)
if err != nil {
return
}
provider, _, err := s.lspProviderForPath(absPath)
if err != nil || provider == nil {
return
}
_, _ = provider.ConfirmSymbolRefs(s.graph, s.workspaceRootFor(absPath), node)
// Record after the attempt (even on 0/err) so a query doesn't re-spawn the
// server every call; a file re-index clears staleness through the normal
// restub path. Durable-ledger + retry semantics are the fuller version.
s.refsConfirmed.Store(node.ID, struct{}{})
}

// registerLSPTools wires the LSP-action MCP surface:
//
// get_diagnostics — most recent publishDiagnostics for a file
Expand Down
69 changes: 69 additions & 0 deletions internal/semantic/lsp/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2573,6 +2573,75 @@ func (p *Provider) EnsureFileOpen(repoRoot, relPath string) error {
// Returns (true, nil) when a type was stamped, (false, nil) when the server had
// no type at the node's position (nothing is written), and (false, err) on a
// transport failure. The file is opened on the server first (idempotent).
// ConfirmSymbolRefs confirms the incoming references to ONE symbol on demand.
// It prepares a call hierarchy at the symbol's definition and, for every
// server-verified incoming call, mints or promotes an lsp_resolved edge —
// the per-symbol unit of the whole-repo confirm + call-hierarchy sweep. A
// find_usages / get_callers answer for symbol S needs S's callers confirmed,
// not the whole repo's, so this lets lazy enrichment restore compiler-grade
// usages accuracy at query time without paying a cold-path sweep. It reuses
// recordHierarchyCall, so every hard-won correctness rule (callable-kind
// matching, per-site promotion, declaration-identity) applies unchanged.
//
// Scoped to callable nodes (the call-hierarchy unit); returns (0, nil) for
// other kinds or a server without call-hierarchy. LSP round-trips run
// unlocked; the graph mutations are batched under the resolve mutex.
func (p *Provider) ConfirmSymbolRefs(g graph.Store, repoRoot string, n *graph.Node) (int, error) {
if n == nil || (n.Kind != graph.KindFunction && n.Kind != graph.KindMethod) {
return 0, nil
}
if !p.Supports("textDocument/prepareCallHierarchy") {
return 0, nil
}
line, ok := lspLine(n)
if !ok {
return 0, nil
}
rel := nodeRelPath(n)
if err := p.EnsureFileOpen(repoRoot, rel); err != nil {
return 0, err
}
absRoot, err := filepath.Abs(repoRoot)
if err != nil {
return 0, err
}
col := 0
if src := p.getSource(repoRoot, rel); src != nil {
col = identifierColumn(src, n.StartLine, n.Name)
}
items, err := p.prepareCallHierarchy(repoRoot, rel, line, col)
if err != nil {
return 0, err
}
// LSP round-trips first, unlocked.
type hop struct {
other CallHierarchyItem
ranges []Range
}
var hops []hop
for _, item := range items {
ins, ierr := p.incomingCalls(item)
if ierr != nil {
continue
}
for _, ic := range ins {
hops = append(hops, hop{other: ic.From, ranges: ic.FromRanges})
}
}
if len(hops) == 0 {
return 0, nil
}
// Graph mutations under the resolve lock, batched.
result := &semantic.EnrichResult{Provider: p.Name()}
rmu := g.ResolveMutex()
rmu.Lock()
for _, h := range hops {
p.recordHierarchyCall(g, n.RepoPrefix, absRoot, n, h.other, false, h.ranges, result)
}
rmu.Unlock()
return result.EdgesConfirmed + result.EdgesAdded, nil
}

func (p *Provider) EnrichNode(g graph.Store, repoRoot string, n *graph.Node) (bool, error) {
if n == nil {
return false, nil
Expand Down
Loading