Skip to content

Finetune to reduce LLM token - #5

Merged
hungpham10 merged 3 commits into
mainfrom
feature/fintune-to-reduce-llm-token
Aug 12, 2026
Merged

hungpham10 merged 3 commits into
mainfrom
feature/fintune-to-reduce-llm-token

Conversation

@hungpham10

@hungpham10 hungpham10 commented Aug 12, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added resumable searches with timeouts, progress reporting, pagination, and continuation IDs.
    • Added configurable response detail levels and output formats.
    • Added compact responses with omitted default fields and relative file paths.
    • Added path prefix stripping for Markdown context output.
    • Added CLI support for selecting output format.
  • Bug Fixes

    • Improved search ordering, paging consistency, resume validation, and source-size calculations.
  • Documentation

    • Updated MCP guidance and examples for resumable searches, formatting, and compact responses.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The change adds deadline-aware resumable graph searches with server-side session storage. MCP sessions now support configurable symbol detail and output formats, normalized paths, omitted default fields, and resumable search responses. Context Markdown rendering supports optional root-prefix removal.

Changes

Resumable search and session storage

Layer / File(s) Summary
Graph resumable search implementation
crates/codegraph-graph/src/lib.rs, crates/codegraph-graph/src/search.rs, crates/codegraph-graph/src/radix.rs
Graph searches now support deadlines, checkpoints, paging, deduplication, deterministic ordering, and resume state.
GraphApi search session orchestration
crates/codegraph-api/src/lib.rs, crates/codegraph-api/tests/api.rs
GraphApi stores cursors, validates resume parameters and index versions, expires sessions, evicts old sessions, and reports progress. Tests cover timeout recovery and pagination resumes.

MCP output and path handling

Layer / File(s) Summary
MCP session settings and server wiring
crates/codegraph-mcp/src/session.rs, crates/codegraph-mcp/src/lib.rs, crates/codegraph/src/main.rs
Sessions and servers now accept detail and output-format settings. The CLI adds serve --format.
MCP response formatting
crates/codegraph-mcp/src/tools.rs, crates/codegraph-mcp/src/usage.rs, Cargo.toml
Tool responses use shared serializers, configurable formats, root-relative paths, omitted default fields, and resumable search metadata. JSON key order preservation is enabled.
Context Markdown path rendering
crates/codegraph-context/src/lib.rs, crates/codegraph-api/tests/api.rs
Context requests accept strip_prefix. Markdown paths remove the prefix with boundary-aware matching.
Supporting contracts and compatibility
crates/codegraph-core/src/semgraph.rs, crates/codegraph-graph/src/shared.rs, crates/codegraph-mcp/src/stdio.rs
ScopeLevel::as_str exposes stable values. Disabled storage features allow the probe path variable. Stdio formatting does not change behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MCPTools
  participant GraphApi
  participant SearchSessionStore
  participant GraphIndex
  Client->>MCPTools: codegraph_search_symbol(query, timeout, resume)
  MCPTools->>GraphApi: search_symbol_paged_resumable(...)
  GraphApi->>SearchSessionStore: get(resume)
  GraphApi->>GraphIndex: search_symbol_paged_resumable(cursor, deadline)
  GraphIndex-->>GraphApi: page, progress, continuation
  GraphApi->>SearchSessionStore: put or remove session
  GraphApi-->>MCPTools: ResumeSearchOutcome
  MCPTools-->>Client: formatted response with resume metadata
Loading

Possibly related PRs

  • hungpham10/codegraph-rs#1: Extends the graph search APIs introduced by this PR with deadline-aware pagination and resumable cursors.
  • hungpham10/codegraph-rs#4: Extends the MCP session and tool infrastructure with configurable output and resumable searches.

Suggested reviewers: cleboost, hungpham7-tiki

Poem

A rabbit hops through indexed names,
Saving cursors between search frames.
MCP replies grow neat and bright,
With paths trimmed to the root just right.
Timeout? I’ll resume at dawn—
Then find each symbol before I’m gone.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title identifies the primary goal of reducing LLM token usage through compact output and resumable search features.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/fintune-to-reduce-llm-token

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/codegraph-mcp/src/usage.rs (1)

120-128: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Deduplicate on the normalized path, not the raw path.

The lookup normalizes with strip_root_prefix, but seen stores the raw answer path p. If the same file appears once as an absolute path and once already relativized, both forms map to the same entry and its bytes are counted twice.

🐛 Proposed fix
     let mut seen = std::collections::HashSet::new();
     let mut total = 0u64;
     for p in paths {
-        if let Some(b) = bytes_by_path.get(crate::tools::strip_root_prefix(&p, root)) {
-            if seen.insert(p) {
+        let key = crate::tools::strip_root_prefix(&p, root).to_string();
+        if let Some(b) = bytes_by_path.get(key.as_str()) {
+            if seen.insert(key) {
                 total += b;
             }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-mcp/src/usage.rs` around lines 120 - 128, Update the
deduplication in the path-totaling loop to use the normalized path produced by
strip_root_prefix as the HashSet key, while using that same normalized value for
bytes_by_path lookup. Preserve counting each file only once when absolute and
relative representations resolve to the same path.
crates/codegraph-mcp/src/session.rs (1)

184-213: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A re-run of codegraph_init without detail resets the session detail level. Session::init accepts detail as a required DetailLevel and always writes it, while it accepts format as an Option<OutputStyle> and writes it only when present. The caller converts an absent detail argument into DetailLevel::default(), so the two settings behave differently for the same "argument omitted" case.

  • crates/codegraph-mcp/src/session.rs#L184-L213: change the parameter to detail: Option<DetailLevel> and write self.detail only when it is Some, matching the existing format handling.
  • crates/codegraph-mcp/src/lib.rs#L131-L153: drop .unwrap_or_default() so the parsed value stays Option<DetailLevel>, pass it through to init, and report self.session.detail().await.as_str() in the response instead of the local detail.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-mcp/src/session.rs` around lines 184 - 213, Update
Session::init in crates/codegraph-mcp/src/session.rs:184-213 to accept detail:
Option<DetailLevel> and only write self.detail when it is Some, matching format
handling. Update crates/codegraph-mcp/src/lib.rs:131-153 to remove
unwrap_or_default(), pass the optional detail through to init, and report
self.session.detail().await.as_str() rather than the local value.
🧹 Nitpick comments (5)
crates/codegraph-context/src/lib.rs (1)

146-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression coverage for prefix stripping.

The supplied crates/codegraph-api/tests/api.rs test passes strip_prefix: None. It checks only the unchanged behavior. Add a Markdown test with Some(root) and assert relative paths for the primary symbol, callers, and callees. Include a boundary case such as /repo-old/src/a.ts with /repo.

Also applies to: 158-176

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-context/src/lib.rs` at line 146, Add regression coverage for
prefix stripping in the Markdown/API test flow by using strip_prefix:
Some(root). Assert that relative paths are correct for the primary symbol,
callers, and callees, and add a boundary case such as /repo-old/src/a.ts with
/repo to ensure only a complete path prefix is stripped.
crates/codegraph-graph/src/lib.rs (3)

1759-1793: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Exact and Prefix scan every distinct name.

The ScanNames phase walks sorted_name_keys element by element for all non-Contains modes. Exact can resolve through name_index in constant time. Prefix can use partition_point on the sorted vector and stop at the first non-matching key. Only Suffix needs a full scan.

The current form makes an exact-name lookup O(number of distinct names) per call, and it also creates unnecessary resume round-trips on large indexes.

♻️ Suggested fast paths
             SearchCursorPhase::ScanNames { name_pos } => {
+                // Exact: hash lookup — không cần quét.
+                if mode == SymbolMatch::Exact {
+                    let matched = if self.name_index.contains_key(&q) {
+                        vec![q.clone()]
+                    } else {
+                        Vec::new()
+                    };
+                    phase = SearchCursorPhase::Expand {
+                        names: matched,
+                        name_idx: 0,
+                        id_idx: 0,
+                        collected: Vec::new(),
+                    };
+                } else {
                 let mut matched: Vec<String> = Vec::new();
                 let mut pos = *name_pos;

For Prefix, start pos at self.sorted_name_keys.partition_point(|n| n.as_str() < q.as_str()) and stop as soon as a key no longer starts with q.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-graph/src/lib.rs` around lines 1759 - 1793, Optimize the
ScanNames branch in the search flow: resolve SymbolMatch::Exact directly through
name_index instead of scanning sorted_name_keys, and for SymbolMatch::Prefix
initialize pos with partition_point and stop when keys no longer start with q.
Preserve the existing resumable scan behavior for timeouts and retain the full
scan only for SymbolMatch::Suffix.

141-143: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

sorted_name_keys duplicates name_records.

rebuild_name_engine builds sorted_name_keys from distinct and then pushes the same strings, in the same order, into name_records. Both vectors hold identical content after the loop. On a large index this doubles the memory used for distinct names.

The scan phase in search_symbol_paged_resumable can index name_records directly, because it is already sorted by construction.

♻️ Suggested consolidation
-        let mut record = 0usize;
-        self.sorted_name_keys = distinct.iter().map(|s| s.to_string()).collect();
+        let mut record = 0usize;

Then replace self.sorted_name_keys reads in search_symbol_paged_resumable with self.name_records, and drop the field plus its reset at line 650.

Also applies to: 597-597

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-graph/src/lib.rs` around lines 141 - 143, Remove the
redundant sorted_name_keys field and its reset, since rebuild_name_engine
already populates name_records with the sorted distinct names. Update
search_symbol_paged_resumable to use name_records for all existing sorted-name
scan and indexing reads, preserving the current ordering and pagination
behavior.

1798-1839: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Check the deadline in batches, not per symbol id.

The phase B loop calls Instant::now() for every single id it appends. On a large expansion this clock read dominates the useful work. A counter that checks the deadline every N iterations (for example 1024) keeps cancellation responsive and removes most clock reads.

♻️ Suggested batching
                 loop {
-                    if let Some(dl) = deadline
-                        && Instant::now() >= dl
-                    {
-                        timed_out = true;
-                        break;
-                    }
+                    steps += 1;
+                    if steps % 1024 == 0
+                        && let Some(dl) = deadline
+                        && Instant::now() >= dl
+                    {
+                        timed_out = true;
+                        break;
+                    }

Declare let mut steps = 0usize; before the loop. Apply the same pattern to the ScanNames loop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-graph/src/lib.rs` around lines 1798 - 1839, Update the Phase
B expansion loop in the SearchCursorPhase::Expand branch to track a steps
counter and check the deadline only once per batch, such as every 1024
iterations, while preserving timeout behavior. Apply the same batched
deadline-check pattern to the ScanNames loop, using a counter initialized before
each loop and incremented for processed entries.
crates/codegraph-api/tests/api.rs (1)

262-316: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for repeated paging through the session store.

This test performs three calls, so the store never reaches its 512-entry cap. Each successful call with remaining pages stores a new session entry (see crates/codegraph-api/src/lib.rs lines 261-271). A longer paging loop would expose the eviction of the still-used resume_id.

Add a test that pages through the full result set with the same resume_id and asserts that the id stays valid until completion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-api/tests/api.rs` around lines 262 - 316, Extend
search_symbol_paged_resume_paging into a loop that requests every page using the
original resume_id, advancing the offset by the page size and asserting each
call succeeds with the expected page size until the final page. Verify the same
resume_id remains valid throughout paging and that the final response has no
resume token, covering session-store eviction beyond 512 entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/codegraph-api/src/lib.rs`:
- Around line 90-94: Update CodegraphCursorStore::get to enforce the configured
TTL before returning an entry, removing expired cursors and returning None when
their age exceeds the TTL; preserve the existing tuple and clone behavior for
valid entries. Keep the documentation consistent with this behavior.
- Around line 261-271: Update the session handling around resume_id so continued
pages refresh the existing session identified by resume in place when it is
valid, rather than always calling sessions.put to create a new id. Preserve
creation of a new session when no resume id was supplied, and continue removing
the session when out.cursor is None.

In `@crates/codegraph-context/src/lib.rs`:
- Around line 121-133: Update rel_path to use camino::Utf8Path::strip_prefix for
platform-aware prefix matching, handling both slash styles and trailing
separators while returning the original p when stripping fails. Add focused
tests covering trailing `/`, Windows `\` separators, and unmatched prefixes.

In `@crates/codegraph-graph/src/lib.rs`:
- Around line 1729-1758: Update the search flow around codegraph_search_symbol
and the SearchCursorPhase::Engine branch to detect an empty query when the
search mode is Contains, after validating the resume state, and return an empty
PagedSearchOutcome without invoking search_resumable. Preserve normal engine
behavior for non-empty queries and other search modes; alternatively, map the
resulting NotFound error to an empty result consistently with the legacy helper.

In `@crates/codegraph-graph/src/radix.rs`:
- Around line 837-855: Update the DfsState::Collect handling in
Radix::search_dfs so each node’s record is appended only on its initial stack
visit, while subsequent child-index resumes process children without rereading
or pushing the record. Preserve pre-order traversal, MAX_RESULTS accounting, and
checkpoint contents without duplicate records.

In `@crates/codegraph-graph/src/search.rs`:
- Around line 572-613: Fix the depth-filter resume flow in the search resolve
block so resume state preserves the original unfiltered record_ids while also
retaining the already-filtered output. Add a dedicated resolved-results field to
SearchResume and restore/use it across retries, or restart filtering from
resolve_idx zero; ensure resumed searches do not discard previously accepted
records or report premature completion.

In `@crates/codegraph-mcp/src/server-instructions.md`:
- Around line 85-89: Update the fenced code block in the instructions text to
declare the text language, using a text fence for the plain error message and
preserving its contents unchanged.

In `@crates/codegraph-mcp/src/tools.rs`:
- Around line 658-693: Normalize the not-found response in the
codegraph_function_scope handler so the "function" key has the same shape in
both branches. In the None branch, emit the resolved symbol through symbol_json
using the existing detail and format values, or use a distinct key for the plain
name instead; prefer preserving "function" with symbol_json.

---

Outside diff comments:
In `@crates/codegraph-mcp/src/session.rs`:
- Around line 184-213: Update Session::init in
crates/codegraph-mcp/src/session.rs:184-213 to accept detail:
Option<DetailLevel> and only write self.detail when it is Some, matching format
handling. Update crates/codegraph-mcp/src/lib.rs:131-153 to remove
unwrap_or_default(), pass the optional detail through to init, and report
self.session.detail().await.as_str() rather than the local value.

In `@crates/codegraph-mcp/src/usage.rs`:
- Around line 120-128: Update the deduplication in the path-totaling loop to use
the normalized path produced by strip_root_prefix as the HashSet key, while
using that same normalized value for bytes_by_path lookup. Preserve counting
each file only once when absolute and relative representations resolve to the
same path.

---

Nitpick comments:
In `@crates/codegraph-api/tests/api.rs`:
- Around line 262-316: Extend search_symbol_paged_resume_paging into a loop that
requests every page using the original resume_id, advancing the offset by the
page size and asserting each call succeeds with the expected page size until the
final page. Verify the same resume_id remains valid throughout paging and that
the final response has no resume token, covering session-store eviction beyond
512 entries.

In `@crates/codegraph-context/src/lib.rs`:
- Line 146: Add regression coverage for prefix stripping in the Markdown/API
test flow by using strip_prefix: Some(root). Assert that relative paths are
correct for the primary symbol, callers, and callees, and add a boundary case
such as /repo-old/src/a.ts with /repo to ensure only a complete path prefix is
stripped.

In `@crates/codegraph-graph/src/lib.rs`:
- Around line 1759-1793: Optimize the ScanNames branch in the search flow:
resolve SymbolMatch::Exact directly through name_index instead of scanning
sorted_name_keys, and for SymbolMatch::Prefix initialize pos with
partition_point and stop when keys no longer start with q. Preserve the existing
resumable scan behavior for timeouts and retain the full scan only for
SymbolMatch::Suffix.
- Around line 141-143: Remove the redundant sorted_name_keys field and its
reset, since rebuild_name_engine already populates name_records with the sorted
distinct names. Update search_symbol_paged_resumable to use name_records for all
existing sorted-name scan and indexing reads, preserving the current ordering
and pagination behavior.
- Around line 1798-1839: Update the Phase B expansion loop in the
SearchCursorPhase::Expand branch to track a steps counter and check the deadline
only once per batch, such as every 1024 iterations, while preserving timeout
behavior. Apply the same batched deadline-check pattern to the ScanNames loop,
using a counter initialized before each loop and incremented for processed
entries.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b81d5972-e9f1-43c3-8d25-031d64a0efa4

📥 Commits

Reviewing files that changed from the base of the PR and between 40c4ac9 and a7691f1.

📒 Files selected for processing (16)
  • Cargo.toml
  • crates/codegraph-api/src/lib.rs
  • crates/codegraph-api/tests/api.rs
  • crates/codegraph-context/src/lib.rs
  • crates/codegraph-core/src/semgraph.rs
  • crates/codegraph-graph/src/lib.rs
  • crates/codegraph-graph/src/radix.rs
  • crates/codegraph-graph/src/search.rs
  • crates/codegraph-graph/src/shared.rs
  • crates/codegraph-mcp/src/lib.rs
  • crates/codegraph-mcp/src/server-instructions.md
  • crates/codegraph-mcp/src/session.rs
  • crates/codegraph-mcp/src/stdio.rs
  • crates/codegraph-mcp/src/tools.rs
  • crates/codegraph-mcp/src/usage.rs
  • crates/codegraph/src/main.rs

Comment on lines +90 to +94
/// Đọc cursor theo id — `None` nếu không có / quá TTL.
pub fn get(&self, id: &str) -> Option<(u64, SearchCursor)> {
let map = self.inner.lock().unwrap();
map.get(id).map(|s| (s.index_version, s.cursor.clone()))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

get does not apply the TTL.

The doc comment states that get returns None when the entry is older than the TTL. The body only performs a map lookup. Expiry happens only inside put. If no put runs, an expired cursor stays reachable.

Either enforce the TTL in get, or correct the doc comment.

🔧 Proposed fix
     /// Đọc cursor theo id — `None` nếu không có / quá TTL.
     pub fn get(&self, id: &str) -> Option<(u64, SearchCursor)> {
         let map = self.inner.lock().unwrap();
-        map.get(id).map(|s| (s.index_version, s.cursor.clone()))
+        let s = map.get(id)?;
+        if Instant::now().duration_since(s.created) >= self.ttl {
+            return None;
+        }
+        Some((s.index_version, s.cursor.clone()))
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Đọc cursor theo id — `None` nếu không có / quá TTL.
pub fn get(&self, id: &str) -> Option<(u64, SearchCursor)> {
let map = self.inner.lock().unwrap();
map.get(id).map(|s| (s.index_version, s.cursor.clone()))
}
/// Đọc cursor theo id — `None` nếu không có / quá TTL.
pub fn get(&self, id: &str) -> Option<(u64, SearchCursor)> {
let map = self.inner.lock().unwrap();
let s = map.get(id)?;
if Instant::now().duration_since(s.created) >= self.ttl {
return None;
}
Some((s.index_version, s.cursor.clone()))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-api/src/lib.rs` around lines 90 - 94, Update
CodegraphCursorStore::get to enforce the configured TTL before returning an
entry, removing expired cursors and returning None when their age exceeds the
TTL; preserve the existing tuple and clone behavior for valid entries. Keep the
documentation consistent with this behavior.

Comment on lines +261 to +271
// ── Quản lý session: lưu khi còn tiếp tục (timeout / còn page), xoá
// khi xong hẳn. ──
let resume_id = match &out.cursor {
Some(c) => Some(self.sessions.put(c.clone(), version)),
None => {
if let Some(id) = &resume {
self.sessions.remove(id);
}
None
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Each continued page stores a new session and never releases the previous one.

When out.cursor is Some, this code always calls put and returns a fresh id. The id supplied in resume is removed only when the search completes. The integration test crates/codegraph-api/tests/api.rs (lines 283-316) reuses the same resume_id across pages, so the caller keeps using the original id while the store fills with unused clones.

Two consequences:

  • Every paged call consumes one slot of the 512-slot store.
  • Eviction removes the oldest entry first. The original id that the caller still uses is the oldest, so it can be evicted by its own clones. The next retry then fails with "resume id expired or unknown".

Refresh the existing session in place instead of creating a new one when the caller passed a valid resume.

🔧 Proposed direction
         let resume_id = match &out.cursor {
-            Some(c) => Some(self.sessions.put(c.clone(), version)),
+            // Cùng một search đang tiếp tục → cập nhật session cũ, giữ nguyên id.
+            Some(c) => match &resume {
+                Some(id) => {
+                    self.sessions.update(id, c.clone(), version);
+                    Some(id.clone())
+                }
+                None => Some(self.sessions.put(c.clone(), version)),
+            },
             None => {
                 if let Some(id) = &resume {
                     self.sessions.remove(id);
                 }
                 None
             }
         };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-api/src/lib.rs` around lines 261 - 271, Update the session
handling around resume_id so continued pages refresh the existing session
identified by resume in place when it is valid, rather than always calling
sessions.put to create a new id. Preserve creation of a new session when no
resume id was supplied, and continue removing the session when out.cursor is
None.

Comment on lines +121 to +133
/// Strip `root/` prefix khỏi path (boundary-aware) — `None` giữ nguyên.
fn rel_path<'a>(p: &'a str, strip: Option<&str>) -> &'a str {
if let Some(root) = strip {
if let Some(rest) = p.strip_prefix(root) {
if let Some(rest) = rest.strip_prefix('/') {
return rest;
}
}
}
p
}

fn render_markdown(resp: &ContextResponse, strip: Option<&str>) -> String {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(crates/codegraph-context|.*context.*|.*path.*|.*test.*)$' | head -200

printf '%s\n' '--- rel_path and related symbols ---'
rg -n -C 8 'fn rel_path|render_markdown|strip_prefix|ContextRequest|ContextResponse' crates/codegraph-context 2>/dev/null || true

printf '%s\n' '--- file outline ---'
if command -v ast-grep >/dev/null 2>&1; then
  ast-grep outline crates/codegraph-context/src/lib.rs
else
  wc -l crates/codegraph-context/src/lib.rs
fi

Repository: hungpham10/codegraph-rs

Length of output: 9201


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- rendering implementation ---'
sed -n '133,240p' crates/codegraph-context/src/lib.rs

printf '%s\n' '--- context request usages ---'
rg -n -C 5 'ContextRequest\s*\{|strip_prefix|file:' crates --glob '*.rs' --glob '*.md' --glob '*.toml' | head -300

printf '%s\n' '--- path conventions and targets ---'
rg -n -C 3 'target|cfg\(target|PathBuf|std::path::Path|replace\(.*\\\\|replace\(.*\/|canonical|workspace root|workspace_root' Cargo.toml crates docs --glob '*.rs' --glob '*.toml' --glob '*.md' | head -300

printf '%s\n' '--- package metadata ---'
cat crates/codegraph-context/Cargo.toml

Repository: hungpham10/codegraph-rs

Length of output: 44828


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import PurePosixPath, PureWindowsPath

def current(p, root):
    rest = p[len(root):] if p.startswith(root) else None
    if rest is not None and rest.startswith('/'):
        return rest[1:]
    return p

cases = [
    ("/workspace/src/a.ts", "/workspace/"),
    ("/workspace/src/a.ts", "/workspace"),
    ("/src/a.ts", "/"),
    (r"C:\\workspace\\src\\a.ts", r"C:\\workspace\\"),
    (r"C:\\workspace\\src\\a.ts", r"C:\\workspace"),
]
for p, root in cases:
    print(f"current({p!r}, {root!r}) -> {current(p, root)!r}")
    print(f"posix.relative_to -> ", end="")
    try:
        print(repr(str(PurePosixPath(p).relative_to(PurePosixPath(root)))))
    except ValueError as e:
        print(type(e).__name__)
    print(f"windows.relative_to -> ", end="")
    try:
        print(repr(str(PureWindowsPath(p).relative_to(PureWindowsPath(root)))))
    except ValueError as e:
        print(type(e).__name__)
PY

Repository: hungpham10/codegraph-rs

Length of output: 865


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- MCP root state and initialization ---'
rg -n -C 8 'struct .*root|root:|Utf8PathBuf|codegraph_context|strip_root_prefix' crates/codegraph-mcp/src --glob '*.rs' | head -400

printf '%s\n' '--- extraction path flow ---'
sed -n '1,120p' crates/codegraph-extract/src/walker.rs
sed -n '1,120p' crates/codegraph-extract/src/orchestrator.rs
rg -n -C 5 'FileMatch|\.path|parse_file|parse_project|to_string\(\)|as_str\(\)' crates/codegraph-extract/src --glob '*.rs' | head -400

printf '%s\n' '--- workspace camino dependency ---'
rg -n -C 3 'camino' Cargo.toml crates --glob 'Cargo.toml'

Repository: hungpham10/codegraph-rs

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -eu

if command -v rustc >/dev/null 2>&1; then
  cat >/tmp/rel_path_probe.rs <<'RS'
use std::path::Path;

fn main() {
    let cases = [
        ("/workspace/src/a.ts", "/workspace/"),
        ("/workspace/src/a.ts", "/workspace"),
        ("/src/a.ts", "/"),
        ("C:\\workspace\\src\\a.ts", "C:\\workspace\\"),
        ("C:\\workspace\\src\\a.ts", "C:\\workspace"),
    ];
    for (p, root) in cases {
        let result = Path::new(p)
            .strip_prefix(Path::new(root))
            .and_then(|rest| rest.to_str())
            .map(str::to_owned);
        println!("{p:?} / {root:?} => {result:?}");
    }
}
RS
  rustc /tmp/rel_path_probe.rs -o /tmp/rel_path_probe
  /tmp/rel_path_probe
else
  printf '%s\n' 'rustc unavailable; exact Rust probe skipped'
fi

Repository: hungpham10/codegraph-rs

Length of output: 752


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- focused root initialization ---'
rg -n -C 12 'pub async fn init|fn init|with_root_and_format|Session::with_root|root =|root:' crates/codegraph-mcp/src/session.rs crates/codegraph-mcp/src/lib.rs

printf '%s\n' '--- focused parse path handoff ---'
rg -n -C 10 'fn parse_one|parse_file\(' crates/codegraph-extract/src/orchestrator.rs crates/codegraph-extract/src/lib.rs

printf '%s\n' '--- context tests and workspace test declarations ---'
rg -n -C 5 'mod tests|#\[test\]|render_markdown|rel_path' crates/codegraph-context/src/lib.rs crates/codegraph-context/tests 2>/dev/null || true

printf '%s\n' '--- root normalization calls ---'
rg -n -C 4 'canonicalize|normalize|clean|trim_end_matches|Utf8PathBuf::from\(path\)|init\(' crates/codegraph-mcp/src/session.rs crates/codegraph-mcp/src/lib.rs crates/codegraph-mcp/src/main.rs 2>/dev/null || true

Repository: hungpham10/codegraph-rs

Length of output: 36694


🏁 Script executed:

#!/bin/bash
set -eu

if command -v rustc >/dev/null 2>&1; then
  cat >/tmp/rel_path_probe.rs <<'RS'
use std::path::Path;

fn main() {
    let cases = [
        ("/workspace/src/a.ts", "/workspace/"),
        ("/workspace/src/a.ts", "/workspace"),
        ("/src/a.ts", "/"),
        ("C:\\workspace\\src\\a.ts", "C:\\workspace\\"),
        ("C:\\workspace\\src\\a.ts", "C:\\workspace"),
    ];
    for (p, root) in cases {
        let result = Path::new(p)
            .strip_prefix(Path::new(root))
            .ok()
            .and_then(|rest| rest.to_str())
            .map(str::to_owned);
        println!("{p:?} / {root:?} => {result:?}");
    }
}
RS
  rustc /tmp/rel_path_probe.rs -o /tmp/rel_path_probe
  /tmp/rel_path_probe
else
  printf '%s\n' 'rustc unavailable; exact Rust probe skipped'
fi

Repository: hungpham10/codegraph-rs

Length of output: 422


Use platform-aware path prefix matching.

rel_path removes only /. It fails for a trailing separator, /, and Windows \ separators. Use camino::Utf8Path::strip_prefix and preserve p when stripping fails. Add tests for these cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-context/src/lib.rs` around lines 121 - 133, Update rel_path
to use camino::Utf8Path::strip_prefix for platform-aware prefix matching,
handling both slash styles and trailing separators while returning the original
p when stripping fails. Add focused tests covering trailing `/`, Windows `\`
separators, and unmatched prefixes.

Comment on lines +1729 to 1758
match &mut phase {
SearchCursorPhase::Engine(sr) => {
let page = self
.names
.search_resumable(q.as_bytes(), None, Some(sr.clone()), deadline)
.await?;
if page.timed_out {
phase = SearchCursorPhase::Engine(page.resume.unwrap_or_default());
timed_out = true;
} else {
// record → tên, sort → Expand.
let mut names: Vec<String> = page
.record_ids
.iter()
.filter_map(|&r| {
if r == 0 {
return None;
}
self.name_records.get(r - 1).cloned()
})
.collect();
names.sort();
phase = SearchCursorPhase::Expand {
names,
name_idx: 0,
id_idx: 0,
collected: Vec::new(),
};
for &id in name_ids {
if seen.insert(id) {
ids.push(id);
}
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find callers of the paged/resumable search and check query validation upstream.
rg -nP -C4 '\bsearch_symbol_paged(_resumable)?\s*\(' --type=rust
rg -nP -C4 '\bsearch_resumable\s*\(' --type=rust crates/codegraph-api crates/codegraph-mcp

Repository: hungpham10/codegraph-rs

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '== candidate files =='
fd -t f 'lib.rs|.*\.rs$' crates/codegraph-graph crates/codegraph-api crates/codegraph-mcp 2>/dev/null | head -200
printf '%s\n' '== paged search references =='
rg -n -C5 'search_symbol_paged|search_symbol_filtered|search_resumable|struct SearchCursor|PagedSearchOutcome' crates --glob '*.rs' || true
printf '%s\n' '== target outline =='
ast-grep outline crates/codegraph-graph/src/lib.rs --match 'search_symbol_paged_resumable' --view expanded 2>/dev/null || true

Repository: hungpham10/codegraph-rs

Length of output: 39514


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '== generic search implementation =='
sed -n '430,590p' crates/codegraph-graph/src/search.rs
printf '%s\n' '== old helper =='
sed -n '1128,1170p' crates/codegraph-graph/src/lib.rs
printf '%s\n' '== paged implementation setup/finalization =='
sed -n '1660,1900p' crates/codegraph-graph/src/lib.rs
printf '%s\n' '== API forwarding =='
sed -n '150,270p' crates/codegraph-api/src/lib.rs
printf '%s\n' '== MCP query extraction =='
sed -n '285,340p' crates/codegraph-mcp/src/tools.rs
sed -n '465,515p' crates/codegraph-mcp/src/tools.rs

Repository: hungpham10/codegraph-rs

Length of output: 26800


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path

files = [
    Path("crates/codegraph-graph/src/search.rs"),
    Path("crates/codegraph-graph/src/lib.rs"),
    Path("crates/codegraph-api/src/lib.rs"),
    Path("crates/codegraph-mcp/src/tools.rs"),
]
terms = ("is_empty", "trim", "query", "search_symbol_paged_resumable", "search_resumable")
for path in files:
    text = path.read_text()
    print(f"== {path} ==")
    for i, line in enumerate(text.splitlines(), 1):
        if any(term in line for term in terms):
            print(f"{i}: {line}")
PY

Repository: hungpham10/codegraph-rs

Length of output: 8671


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '== argument parser =='
sed -n '930,975p' crates/codegraph-mcp/src/tools.rs
printf '%s\n' '== match-mode parsing =='
rg -n -C8 'impl.*SymbolMatch|enum SymbolMatch|SymbolMatch::parse|fn parse' crates/codegraph-graph/src/lib.rs crates/codegraph-api/src/lib.rs crates/codegraph-mcp/src/tools.rs
printf '%s\n' '== focused behavioral verifier =='
python3 - <<'PY'
from pathlib import Path

search = Path("crates/codegraph-graph/src/search.rs").read_text()
graph = Path("crates/codegraph-graph/src/lib.rs").read_text()
mcp = Path("crates/codegraph-mcp/src/tools.rs").read_text()
api = Path("crates/codegraph-api/src/lib.rs").read_text()

assert "if pattern.is_empty() {\n            return Err(Error::NotFound);\n        }" in search
assert "let page = self.search_resumable(q.as_bytes(), None, Some(sr.clone()), deadline)" in graph
assert "let q = query.to_lowercase();" in graph
paged = graph[graph.index("pub async fn search_symbol_paged_resumable"):graph.index("/// Resumable + deadline-aware", graph.index("pub async fn search_symbol_paged_resumable"))]
assert "if q.is_empty()" not in paged
old = graph[graph.index("async fn search_symbol_filtered"):graph.index("/// Search symbol nâng cao", graph.index("async fn search_symbol_filtered"))]
assert "Err(_) => return Ok(Vec::new())" in old
assert "let q = arg_str(&args, \"query\")?;" in mcp
assert ".search_symbol_paged_resumable(q, kind, mode" in mcp
assert "SymbolMatch::Contains" in api and ".search_symbol_paged_resumable(" in api

print("Contains + empty query: Err(Error::NotFound)")
print("Prefix + empty query: scan path; starts_with(\"\") is true")
print("Suffix + empty query: scan path; ends_with(\"\") is true")
print("Exact + empty query: scan path; only name == \"\" can match")
print("Legacy helper: engine errors map to Ok(Vec::new())")
print("MCP/API callers: no empty-query validation found in the forwarding paths")
PY

Repository: hungpham10/codegraph-rs

Length of output: 5198


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '== arg_str definition and calls =='
rg -n -C8 'fn arg_str|arg_str\s*\(' crates/codegraph-mcp/src/tools.rs || true
printf '%s\n' '== exact generic call lines =='
nl -ba crates/codegraph-graph/src/lib.rs | sed -n '1728,1736p'
printf '%s\n' '== exact source assertions =='
python3 - <<'PY'
from pathlib import Path
import re

search = Path("crates/codegraph-graph/src/search.rs").read_text()
graph = Path("crates/codegraph-graph/src/lib.rs").read_text()
mcp = Path("crates/codegraph-mcp/src/tools.rs").read_text()
api = Path("crates/codegraph-api/src/lib.rs").read_text()

checks = [
    ("generic empty pattern returns NotFound",
     "pattern.is_empty()" in search and "return Err(Error::NotFound);" in search[search.index("pub async fn search_resumable"):]),
    ("paged calls generic search with q bytes",
     re.search(r"\.search_resumable\(\s*q\.as_bytes\(\)", graph) is not None),
    ("paged has no empty guard",
     "if q.is_empty()" not in graph[graph.index("pub async fn search_symbol_paged_resumable"):graph.index("/// Resumable + deadline-aware", graph.index("pub async fn search_symbol_paged_resumable"))]),
    ("legacy helper maps engine errors to empty",
     "Err(_) => return Ok(Vec::new())" in graph[graph.index("async fn search_symbol_filtered"):]),
    ("MCP extracts query directly",
     'let q = arg_str(&args, "query")?;' in mcp),
    ("MCP forwards paged query",
     ".search_symbol_paged_resumable(q, kind, mode" in mcp),
    ("API defaults resumable search to Contains",
     "SymbolMatch::Contains" in api[api.index("pub async fn search_resumable"):api.index("pub async fn search_symbol_paged"):]),
]
for name, result in checks:
    print(f"{'PASS' if result else 'FAIL'}: {name}")
    assert result, name
PY

Repository: hungpham10/codegraph-rs

Length of output: 8647


Handle empty Contains queries before calling the name engine.

arg_str accepts "", and codegraph_search_symbol defaults to Contains. The empty byte pattern reaches Search::search_resumable at line 1734, which returns Error::NotFound; the legacy helper maps this error to an empty result. Return an empty PagedSearchOutcome for empty Contains queries after resume validation, or map this error to an empty result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-graph/src/lib.rs` around lines 1729 - 1758, Update the
search flow around codegraph_search_symbol and the SearchCursorPhase::Engine
branch to detect an empty query when the search mode is Contains, after
validating the resume state, and return an empty PagedSearchOutcome without
invoking search_resumable. Preserve normal engine behavior for non-empty queries
and other search modes; alternatively, map the resulting NotFound error to an
empty result consistently with the legacy helper.

Comment on lines +837 to 855
DfsState::Collect { root, mut stack } => {
// Collect subtree theo pre-order (record của node trước, sau
// đó mới children — giống bản đệ quy cũ).
if let Some((node_id, child_idx)) = stack.pop() {
let (_prefix_bytes, record) =
{ self.storage.read().await.get_node(node_id).await? };
if record != EMPTY {
records.push(record);
}
let children = { self.storage.read().await.get_children(node_id).await? };
if child_idx < children.len() {
stack.push((node_id, child_idx + 1));
stack.push((children[child_idx], 0));
}
Some(DfsState::Collect { root, stack })
} else {
None // Collect xong — candidate đã có records, dừng.
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Collect pushes a node record once per child visit.

Each stack entry (node_id, child_idx) is re-pushed as (node_id, child_idx + 1) before descending into a child. When that entry pops again, the code re-reads the node and pushes record a second time. A node with k children therefore contributes its record k + 1 times (k times when child_idx < children.len(), plus the final pop).

The recursive implementation pushed the record exactly once. Search::search_resumable hides the effect because it deduplicates through seen, but Radix::search_dfs returns duplicates directly, MAX_RESULTS accounting inflates, and each duplicate is carried inside the checkpoint.

Push the record only on the first visit to the node.

🐛 Proposed fix
                 DfsState::Collect { root, mut stack } => {
                     // Collect subtree theo pre-order (record của node trước, sau
                     // đó mới children — giống bản đệ quy cũ).
                     if let Some((node_id, child_idx)) = stack.pop() {
                         let (_prefix_bytes, record) =
                             { self.storage.read().await.get_node(node_id).await? };
-                        if record != EMPTY {
+                        // Chỉ lần thăm đầu tiên (child_idx == 0) mới ghi record —
+                        // các lần pop sau chỉ để duyệt tiếp children.
+                        if child_idx == 0 && record != EMPTY {
                             records.push(record);
                         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DfsState::Collect { root, mut stack } => {
// Collect subtree theo pre-order (record của node trước, sau
// đó mới children — giống bản đệ quy cũ).
if let Some((node_id, child_idx)) = stack.pop() {
let (_prefix_bytes, record) =
{ self.storage.read().await.get_node(node_id).await? };
if record != EMPTY {
records.push(record);
}
let children = { self.storage.read().await.get_children(node_id).await? };
if child_idx < children.len() {
stack.push((node_id, child_idx + 1));
stack.push((children[child_idx], 0));
}
Some(DfsState::Collect { root, stack })
} else {
None // Collect xong — candidate đã có records, dừng.
}
}
DfsState::Collect { root, mut stack } => {
// Collect subtree theo pre-order (record của node trước, sau
// đó mới children — giống bản đệ quy cũ).
if let Some((node_id, child_idx)) = stack.pop() {
let (_prefix_bytes, record) =
{ self.storage.read().await.get_node(node_id).await? };
// Chỉ lần thăm đầu tiên (child_idx == 0) mới ghi record —
// các lần pop sau chỉ để duyệt tiếp children.
if child_idx == 0 && record != EMPTY {
records.push(record);
}
let children = { self.storage.read().await.get_children(node_id).await? };
if child_idx < children.len() {
stack.push((node_id, child_idx + 1));
stack.push((children[child_idx], 0));
}
Some(DfsState::Collect { root, stack })
} else {
None // Collect xong — candidate đã có records, dừng.
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-graph/src/radix.rs` around lines 837 - 855, Update the
DfsState::Collect handling in Radix::search_dfs so each node’s record is
appended only on its initial stack visit, while subsequent child-index resumes
process children without rereading or pushing the record. Preserve pre-order
traversal, MAX_RESULTS accounting, and checkpoint contents without duplicate
records.

Comment on lines +572 to +613
// ── Resolve: filter `depth` (key length trong storage) — deadline-aware.
// Bỏ qua nếu không giới hạn depth (name engine — chiếm đa số query).
if let Some(m) = max_len {
let mut out = Vec::new();
let mut ridx = resolve_idx;
let storage = self.storage.read().await;
for &rid in &record_ids {
loop {
if let Some(dl) = deadline
&& Instant::now() >= dl
{
return Ok(SearchPage {
record_ids: out.clone(),
resume: Some(SearchResume {
cand_idx: candidates.len(),
dfs: None,
record_ids: out.clone(),
resolve_idx: ridx,
}),
timed_out: true,
});
}
if ridx >= record_ids.len() {
break;
}
let rid = record_ids[ridx];
ridx += 1;
if rid == EMPTY {
continue;
}
if let Some(m) = max_len
&& storage.get_key_len(rid).await?.unwrap_or(usize::MAX) > m
{
if storage.get_key_len(rid).await?.unwrap_or(usize::MAX) > m {
continue;
}
let meta = storage.get_meta(rid).await?;
results.push((rid, meta));
if results.len() >= MAX_RESULTS {
break;
}
out.push(rid);
}
record_ids = out;
}

if results.is_empty() {
Err(Error::NotFound)
} else {
Ok(results)
}
Ok(SearchPage {
record_ids,
resume: None,
timed_out: false,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The depth-resolve resume state loses every record.

At the resolve timeout the code stores record_ids: out.clone() together with resolve_idx: ridx. out holds only the records that passed the filter, so its length is at most ridx, while ridx indexes the original unfiltered record_ids.

On resume, record_ids is restored from out and ridx from resolve_idx. The condition ridx >= record_ids.len() is then true on the first iteration, the loop breaks immediately, and record_ids = out assigns a fresh empty vector. The call returns zero results and reports completion.

Today no caller combines depth = Some(..) with a deadline, so the path is latent. The state machine is still incorrect and will silently drop results as soon as such a caller appears.

Keep the unfiltered list in the resume state and carry the filtered prefix separately.

🐛 Proposed fix
         if let Some(m) = max_len {
-            let mut out = Vec::new();
+            // `out` = phần đã filter; `record_ids` giữ nguyên danh sách gốc để
+            // `resolve_idx` luôn trỏ đúng vị trí khi resume.
+            let mut out: Vec<usize> = Vec::new();
             let mut ridx = resolve_idx;
             let storage = self.storage.read().await;
             loop {
                 if let Some(dl) = deadline
                     && Instant::now() >= dl
                 {
                     return Ok(SearchPage {
                         record_ids: out.clone(),
                         resume: Some(SearchResume {
                             cand_idx: candidates.len(),
                             dfs: None,
-                            record_ids: out.clone(),
+                            record_ids: record_ids.clone(),
                             resolve_idx: ridx,
                         }),
                         timed_out: true,
                     });
                 }

This alone still discards the already-filtered out across the resume boundary. Either add a dedicated resolved: Vec<usize> field to SearchResume, or drop the incremental out and re-filter from resolve_idx = 0 on resume.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-graph/src/search.rs` around lines 572 - 613, Fix the
depth-filter resume flow in the search resolve block so resume state preserves
the original unfiltered record_ids while also retaining the already-filtered
output. Add a dedicated resolved-results field to SearchResume and restore/use
it across retries, or restart filtering from resolve_idx zero; ensure resumed
searches do not discard previously accepted records or report premature
completion.

Comment on lines +85 to +89
```
codegraph_search_symbol timed out after 2000ms (collected 134 symbols so far).
Retry the same call with the same arguments plus "resume": "<id>" to continue
the search from where it stopped.
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language to the fenced code block.

markdownlint reports MD040 for this block. The block holds plain error text, so text is appropriate.

📝 Proposed fix
-```
+```text
 codegraph_search_symbol timed out after 2000ms (collected 134 symbols so far).
 Retry the same call with the same arguments plus "resume": "<id>" to continue
 the search from where it stopped.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
codegraph_search_symbol timed out after 2000ms (collected 134 symbols so far).
Retry the same call with the same arguments plus "resume": "<id>" to continue
the search from where it stopped.
```
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 85-85: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-mcp/src/server-instructions.md` around lines 85 - 89, Update
the fenced code block in the instructions text to declare the text language,
using a text fence for the plain error message and preserving its contents
unchanged.

Source: Linters/SAST tools

Comment on lines 658 to +693
"codegraph_function_scope" => {
let target = resolve_target(api, &args, "id", "func_name", &[]).await?;
match target {
Target::Ambiguous(v) => Ok(json_str(v)),
Target::Ambiguous(v) => emit_value(root.as_str(), v),
Target::Symbol(sym) => match api.function_scope(sym.id).await {
Some(scope) => serde_json::to_string_pretty(&scope)
.map_err(|e| Error::Invalid(e.to_string())),
None => Ok(serde_json::to_string_pretty(&json!({
"function": sym.name,
"parameters": [],
"locals": [],
"total": 0,
}))
.map_err(|e| Error::Invalid(e.to_string()))?),
Some(scope) => {
let detail = detail_from_args(&args, session_detail);
let format = format_from_args(&args, session_format);
let parameters: Vec<Value> = scope
.parameters
.iter()
.map(|s| symbol_json(root.as_str(), s, detail, format))
.collect();
let locals: Vec<Value> = scope
.locals
.iter()
.map(|s| symbol_json(root.as_str(), s, detail, format))
.collect();
emit_value(
root.as_str(),
json!({
"function": symbol_json(root.as_str(), &scope.function, detail, format),
"parameters": parameters,
"locals": locals,
}),
)
}
None => emit_value(
root.as_str(),
json!({
"function": sym.name,
"parameters": [],
"locals": [],
"total": 0,
}),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

codegraph_function_scope returns two different shapes for the function key.

The Some(scope) branch sets "function" to the output of symbol_json, which is an array in minimize style or an object in medium style. The None branch sets "function" to a plain name string. A consumer that parses the response must handle three shapes for one key.

Use a distinct key for the not-found case, or emit the resolved symbol through symbol_json in both branches.

🔧 Proposed fix
                     None => emit_value(
                         root.as_str(),
                         json!({
-                            "function": sym.name,
+                            "function": symbol_json(
+                                root.as_str(),
+                                &sym,
+                                detail_from_args(&args, session_detail),
+                                format_from_args(&args, session_format),
+                            ),
                             "parameters": [],
                             "locals": [],
                             "total": 0,
                         }),
                     ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"codegraph_function_scope" => {
let target = resolve_target(api, &args, "id", "func_name", &[]).await?;
match target {
Target::Ambiguous(v) => Ok(json_str(v)),
Target::Ambiguous(v) => emit_value(root.as_str(), v),
Target::Symbol(sym) => match api.function_scope(sym.id).await {
Some(scope) => serde_json::to_string_pretty(&scope)
.map_err(|e| Error::Invalid(e.to_string())),
None => Ok(serde_json::to_string_pretty(&json!({
"function": sym.name,
"parameters": [],
"locals": [],
"total": 0,
}))
.map_err(|e| Error::Invalid(e.to_string()))?),
Some(scope) => {
let detail = detail_from_args(&args, session_detail);
let format = format_from_args(&args, session_format);
let parameters: Vec<Value> = scope
.parameters
.iter()
.map(|s| symbol_json(root.as_str(), s, detail, format))
.collect();
let locals: Vec<Value> = scope
.locals
.iter()
.map(|s| symbol_json(root.as_str(), s, detail, format))
.collect();
emit_value(
root.as_str(),
json!({
"function": symbol_json(root.as_str(), &scope.function, detail, format),
"parameters": parameters,
"locals": locals,
}),
)
}
None => emit_value(
root.as_str(),
json!({
"function": sym.name,
"parameters": [],
"locals": [],
"total": 0,
}),
),
"codegraph_function_scope" => {
let target = resolve_target(api, &args, "id", "func_name", &[]).await?;
match target {
Target::Ambiguous(v) => emit_value(root.as_str(), v),
Target::Symbol(sym) => match api.function_scope(sym.id).await {
Some(scope) => {
let detail = detail_from_args(&args, session_detail);
let format = format_from_args(&args, session_format);
let parameters: Vec<Value> = scope
.parameters
.iter()
.map(|s| symbol_json(root.as_str(), s, detail, format))
.collect();
let locals: Vec<Value> = scope
.locals
.iter()
.map(|s| symbol_json(root.as_str(), s, detail, format))
.collect();
emit_value(
root.as_str(),
json!({
"function": symbol_json(root.as_str(), &scope.function, detail, format),
"parameters": parameters,
"locals": locals,
}),
)
}
None => emit_value(
root.as_str(),
json!({
"function": symbol_json(
root.as_str(),
&sym,
detail_from_args(&args, session_detail),
format_from_args(&args, session_format),
),
"parameters": [],
"locals": [],
"total": 0,
}),
),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-mcp/src/tools.rs` around lines 658 - 693, Normalize the
not-found response in the codegraph_function_scope handler so the "function" key
has the same shape in both branches. In the None branch, emit the resolved
symbol through symbol_json using the existing detail and format values, or use a
distinct key for the plain name instead; prefer preserving "function" with
symbol_json.

@codspeed

codspeed Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing feature/fintune-to-reduce-llm-token (18dae6f) with main (40c4ac9)

Open in CodSpeed

@hungpham10
hungpham10 merged commit 5bb721f into main Aug 12, 2026
8 of 10 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
crates/codegraph-mcp/src/tools.rs (6)

734-744: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not expose the limited result count as total.

GraphApi::references(call_name, limit) truncates the result vector before this handler computes hits.len(). The field therefore reports the returned count, not the total number of matching callers. Return a real total from the API, or rename the field to count. (raw.githubusercontent.com)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-mcp/src/tools.rs` around lines 734 - 744, Update the
references handler around GraphApi::references and the emitted JSON so total
does not use the truncated hits.len() value. Prefer extending the API result to
provide the true total number of matching callers and emit that value; otherwise
rename the field to count to accurately represent the limited result count.

459-475: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make path-prefix filtering boundary-aware.

starts_with(prefix) matches sibling names. For example, path="src" also matches src_old/..., and /workspace/src matches /workspace/src2/.... Match the exact path or a path followed by / for both absolute and relative candidates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-mcp/src/tools.rs` around lines 459 - 475, Update the
filtering closure in the "codegraph_files" branch to use boundary-aware prefix
matching for both f.path and strip_root_prefix(&f.path, root.as_str()). Accept
only an exact path match or a candidate beginning with the prefix followed by
"/", preventing sibling names such as src_old or src2 from matching.

551-552: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Apply the active serializer to ambiguous target responses.

Target::Ambiguous contains raw Symbol values, and these branches pass them directly to emit_value. A format="minimize" request therefore returns object-shaped matches, while resolved responses return minimized arrays. Serialize matches with symbol_json before emitting the ambiguous response. The codegraph_symbol path already follows this pattern. (raw.githubusercontent.com)

Also applies to: 602-603, 667-668

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-mcp/src/tools.rs` around lines 551 - 552, Update the
Target::Ambiguous response branches to serialize each raw Symbol in matches with
symbol_json before passing the result to emit_value, including the corresponding
branches near the other referenced locations. Match the existing
codegraph_symbol serialization pattern so format="minimize" produces the same
minimized array shape as resolved responses.

325-345: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Return the continuation token after a non-timeout page.

GraphApi::search_resumable returns resume when a search times out or when more pages remain. This branch uses the token only in the timeout error and discards it on the normal path. Queries with more matches than limit therefore lose their continuation token, and the server-side cursor becomes unreachable. Return a stable envelope with results, total, and resume, and update the tool description. (raw.githubusercontent.com)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-mcp/src/tools.rs` around lines 325 - 345, Update the normal
completion path of the search handler around GraphApi::search_resumable to
return a stable envelope containing the mapped results, total, and the optional
resume token instead of emitting the bare result array. Preserve the timeout
error’s continuation token, include the non-timeout out.resume value in the
response, and revise the tool description to document the new response shape and
continuation behavior.

949-958: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve semantic false fields during default omission.

is_default_value removes every false boolean. This removes compact=false, even though the class-method schema defaults compact to true. It can also remove present=false, which identifies an absent simulation result. Restrict omission to fields with an explicit false default, or preserve protocol and status fields explicitly. (raw.githubusercontent.com)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-mcp/src/tools.rs` around lines 949 - 958, Update
is_default_value and its callers to preserve semantic false booleans such as
compact=false and present=false; only omit false values for fields with an
explicit false default. Use the existing field/key context to distinguish
defaulted fields, while retaining omission of other default values and existing
protocol/status semantics.

563-589: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Honor the declared format parameter in codegraph_class_methods.

The tool schema exposes format, but this handler only reads compact and always emits object values. format="minimize" and the session OutputStyle::Minimize are ignored. Add format-aware MemberInfo serialization, or remove format from the schema and documentation. (raw.githubusercontent.com)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-mcp/src/tools.rs` around lines 563 - 589, Update the
codegraph_class_methods handler to honor the declared format parameter,
including session OutputStyle::Minimize, when serializing MemberInfo results.
Add format-aware minimized serialization alongside the existing compact/full
behavior, or remove format from the tool schema and documentation if it is not
intended to be supported; ensure the emitted methods match the selected format.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/codegraph-graph/src/lib.rs`:
- Around line 1714-1716: Update the ScanNames cursor flow to persist the
accumulated matched-name vector alongside name_pos in
SearchCursorPhase::ScanNames, restore it when handling resume, and continue
scanning without discarding prior matches. Add partial-scan deadline tests
covering Prefix, Suffix, and Exact modes, verifying resumed pages and totals
include matches found before timeout.

---

Outside diff comments:
In `@crates/codegraph-mcp/src/tools.rs`:
- Around line 734-744: Update the references handler around GraphApi::references
and the emitted JSON so total does not use the truncated hits.len() value.
Prefer extending the API result to provide the true total number of matching
callers and emit that value; otherwise rename the field to count to accurately
represent the limited result count.
- Around line 459-475: Update the filtering closure in the "codegraph_files"
branch to use boundary-aware prefix matching for both f.path and
strip_root_prefix(&f.path, root.as_str()). Accept only an exact path match or a
candidate beginning with the prefix followed by "/", preventing sibling names
such as src_old or src2 from matching.
- Around line 551-552: Update the Target::Ambiguous response branches to
serialize each raw Symbol in matches with symbol_json before passing the result
to emit_value, including the corresponding branches near the other referenced
locations. Match the existing codegraph_symbol serialization pattern so
format="minimize" produces the same minimized array shape as resolved responses.
- Around line 325-345: Update the normal completion path of the search handler
around GraphApi::search_resumable to return a stable envelope containing the
mapped results, total, and the optional resume token instead of emitting the
bare result array. Preserve the timeout error’s continuation token, include the
non-timeout out.resume value in the response, and revise the tool description to
document the new response shape and continuation behavior.
- Around line 949-958: Update is_default_value and its callers to preserve
semantic false booleans such as compact=false and present=false; only omit false
values for fields with an explicit false default. Use the existing field/key
context to distinguish defaulted fields, while retaining omission of other
default values and existing protocol/status semantics.
- Around line 563-589: Update the codegraph_class_methods handler to honor the
declared format parameter, including session OutputStyle::Minimize, when
serializing MemberInfo results. Add format-aware minimized serialization
alongside the existing compact/full behavior, or remove format from the tool
schema and documentation if it is not intended to be supported; ensure the
emitted methods match the selected format.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a498c66a-fa34-42ac-b7c9-2ee2af7da7a0

📥 Commits

Reviewing files that changed from the base of the PR and between a7691f1 and 18dae6f.

📒 Files selected for processing (6)
  • crates/codegraph-api/src/lib.rs
  • crates/codegraph-api/tests/api.rs
  • crates/codegraph-graph/src/lib.rs
  • crates/codegraph-graph/src/radix.rs
  • crates/codegraph-mcp/src/lib.rs
  • crates/codegraph-mcp/src/tools.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/codegraph-api/src/lib.rs
  • crates/codegraph-graph/src/radix.rs
  • crates/codegraph-mcp/src/lib.rs
  • crates/codegraph-api/tests/api.rs

Comment on lines +1714 to +1716
pagination: Pagination,
resume: Option<SearchCursor>,
deadline: Option<Instant>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Persist matched names in the ScanNames cursor.

When a Prefix, Suffix, or Exact scan reaches its deadline after finding matches, the cursor retains only name_pos. The next call creates a new matched vector and expands only names after that position. This drops earlier matches and returns an incorrect page and total.

Store the accumulated matched names in SearchCursorPhase::ScanNames, restore them on resume, and add a partial-scan timeout test for each scan-based mode.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codegraph-graph/src/lib.rs` around lines 1714 - 1716, Update the
ScanNames cursor flow to persist the accumulated matched-name vector alongside
name_pos in SearchCursorPhase::ScanNames, restore it when handling resume, and
continue scanning without discarding prior matches. Add partial-scan deadline
tests covering Prefix, Suffix, and Exact modes, verifying resumed pages and
totals include matches found before timeout.

@hungpham10
hungpham10 deleted the feature/fintune-to-reduce-llm-token branch August 12, 2026 09:43
hungpham10 added a commit that referenced this pull request Oct 8, 2026
* Finetune to reduce LLM token

* style: apply rustfmt

* Fix lint
hungpham10 added a commit that referenced this pull request Oct 25, 2026
…eaning magic features (#51)

* Implement dynamic dispatch flag + fan-out

* Enrich Go semantics: method-set matching, function values, imports, concurrency markers

- Method-set matching cho implicit interface: receiver_fn hook lưu receiver
  base type vào Symbol.type_name (`func (f *File) Write` → "File"); khai báo
  method trong interface body là node `method_elem` (không phải
  method_declaration) — giờ được extract với scope đúng vào interface symbol.
  build_go_method_sets tính structural typing: T implements I ⟺ method
  names(I) ⊆ method names(T); w.Write() fan-out chỉ tới các type thỏa thay vì
  naive CHA (fallback giữ nguyên khi method-set chưa đủ dữ liệu).
- Function value tracking: go_fn_ptr_assign bắt `handler := process` /
  `handler = process` → PointerAssign fan-out (dùng chung machinery C).
- Package/import: FileDeps { package, imports } mới trong ParseResult;
  go_file_deps đọc package_clause + import_spec (alias→path);
  alias_qualified_name resolve `orders.Get` qua import alias cho in-repo
  packages (match segment cuối path).
- Concurrency markers: MARKER_GO/MARKER_DEFER (Marker::Go/Defer) vào chain cho
  `go f()`/`defer f()`; select_statement wired vào switch machinery
  (communication_case); EffectType::Panic cho panic/log.Fatal/os.Exit.

Tests: 5 extractor (receiver type, interface method scope, fn value, file
deps, markers) + 2 e2e graph (method-set precision, import alias). Clippy
sạch, fmt xong.

* Go method-set: match signature, không chỉ tên + gom LangSpec thành nhóm

- Symbol.sig_key: signature key chuẩn hóa `(param_types)(result_types)` (bỏ
  tên param) qua sig_key_fn hook; go_signature_key dùng chung cho
  method_declaration/method_elem/function_declaration. build_go_method_sets
  thỏa interface ⟺ (name, sig_key) khớp — `Wrong.Read(string) string` không
  còn bị coi là implement `Reader.Read([]byte) (int, error)`. Thiếu key nào →
  fallback name-only (lenient); interface 0 method → skip.
- LangSpec refactor: gom 3 nhóm base / hooks (HookSpec — các Option<fn>,
  HookSpec::none()) / markers (MarkerSpec — control-flow kinds + field names,
  MarkerSpec::dflt()); LangSpec::dflt() const + FRU hợp lệ trong static.
  14 language spec chỉ khai field KHÁC default — thêm hook mới không còn phải
  sửa 14 file.

* Rust semantic enrichment: field/param typing, use indexing, cross-file impl linking, sig_key

- decls += field_declaration/parameter/let_declaration: struct field, tham số
  và local giờ là symbol có type_name (từ field `type`) — tie-break
  field_declared_type resolve `db.query()` theo type của `db`. Parameter lấy
  tên từ field `pattern` TRƯỚC name_type_fallback (tránh lấy nhầm tên kiểu).
- rust_file_deps: extract `use` (alias = `as` alias hoặc segment cuối);
  import_alias_candidates hỗ trợ `Type::method` — `use crate::storage::Database`
  + `Database::new()` resolve về method của class Database (Alias).
- link_impls_globally: impl linking chuyển lên global ingest — `impl Worker`
  khác file với `struct Worker` vẫn nối (type_ref → def, re-parent methods).
- rust_signature_key: `(params)(return)` cho method/fn (self → "self") —
  nguyên liệu trait matching theo signature.

* Python semantic enrichment + persist dynamic indexes qua reopen

Python (theo review gaps):
- param_container_kinds: `parameters` container → Parameter symbols (identifier
  / typed_parameter / default_parameter, type từ type hint) — field/param
  typing cho tie-break resolution.
- field_from_self_attr: `self.attr = Database()` → Field symbol scope vào
  class, type từ callee text — instance-attr typing.
- py_file_deps: import/import-as/from-import → FileDeps (alias→path), ăn vào
  import_alias_candidates sẵn có.
- py_superclasses: `class File(Writer)` → impl_trait "Writer" — inheritance
  fan-out qua machinery impl_index (giống C++ base class).

Persistence (fix: dynamic indexes chỉ ở RAM, MCP restart là mất):
- Storage trait + sqlite (sg_file_deps/sg_fn_ptr_assigns) + in_memory:
  persist ĐẦU VÀO thô FileDeps + FnPtrAssigns theo file path.
- rebuild() khi open: rebuild file_imports/fn_ptr_index từ blob +
  link_impls_globally/impl_index/go_impls từ sg_symbols — resolution quality
  giữ nguyên sau restart. E2e: dynamic_indexes_survive_reopen.

* Rust config review fixes: trait-declared methods + macro invocations

Review bằng fixture "thật" (trait + impl + macro + closure + async) phát hiện:
- function_signature_item (method KHAI BÁO trong trait) không được extract —
  không có symbol scope vào Interface nên fan-out Rust không bao giờ fire trên
  code thật (các test trước dùng synthetic symbols). Thêm vào decls → trigger
  Interface như Go method_elem.
- macro_invocation không phải call (println!/vec! mất hoàn toàn) — thêm
  CallRule với rust_macro_call_name (`println!`).
- E2E extraction thật: trait Repo{ping} + 2 impl → r.ping() fan-out cả hai
  (HierarchyFanOut) — chứng minh path thật chạy, không chỉ synthetic.
- Cập nhật chain expectation test cũ: cg_t!/cg_d! giờ là call thật.

Còn lại (defer): closure/callable tracking (cb()), generic instantiation,
associated type semantics, UFCS.

* Persist file_deps/fn_ptr_assigns cho MỌI storage backend

Trước đó chỉ sqlite + in_memory implement — lmdb/postgres/mysql/redis rơi về
no-op default = mất dynamic indexes khi reopen trên các backend đó.

- lmdb: 2 named DBI (sg_file_deps/sg_fn_ptr_assigns) + set/all (cursor iter)
  + clear_entities.
- postgres/mysql: bảng sg_file_deps/sg_fn_ptr_assigns (repo_id, path) PK,
  CREATE TABLE IF NOT EXISTS idempotent khi mở repo (không cần migration tay);
  mysql dùng ON DUPLICATE KEY UPDATE; clear_entities thêm 2 bảng.
- redis: 2 hash keys (filedeps/fnptpassigns) + DEL trong clear_entities.
- cached.rs: forward 4 methods xuống inner storage (trước đây rơi về default
  no-op → bypass backend thật!).
- Test roundtrip in_memory (set/all/clear); build + test --all-features.

* callers()/impact xuyên dyn-trait: fan-out fire theo receiver_text + dyn_callers index

Review qua MCP index thật (codegraph-rs tự index chính nó) phát hiện:
- `callers(set_file_deps)` trả rỗng dù ingest_with_progress gọi nó qua
  `dyn Storage` — call qua trait object bị resolve Single vì is_method_dispatch
  check `call_name.contains('.')` nhưng extractor đã rút call_name còn bare,
  trong khi receiver_text vẫn giữ receiver.
- Fix 1: is_method_dispatch = receiver_text.is_some() || call_name.contains('.')
  → fan-out fire cho dyn-trait call.
- Fix 2: dyn_callers (callee → callers) build từ call_meta khi ingest, scan
  call records khi rebuild; callers_resumable merge vào pending (dedup qua
  visited) → callers()/impact() thấy caller xuyên dyn-trait/interface/fn-ptr.
- E2E: callers() của từng impl method thấy dyn caller (trước đây rỗng).

Note vận hành: MCP server đang chạy bị MDB_BAD_RSLOT sau re-index in-process —
reader locktable stale, cần restart MCP server (issue riêng, chưa xử lý).

* Java + JavaScript/TypeScript semantic enrichment

Review setup vs machinery Go/Rust/Python đã có — lấp các gap:

Java (typing đã tốt nhất: field/param/local + annotations + implements):
- java_supers: gộp extends (superclass) + implements vào impl_trait —
  inheritance fan-out qua machinery C++-base (superclass đứng trước).
- java_signature_key: (params)(return) — tên param bỏ.
- java_file_deps: import (bỏ static/wildcard) → FileDeps; strip keyword
  "extends" khỏi superclass text.

JavaScript/TypeScript:
- param_container_kinds formal_parameters: js identifier/rest_pattern, ts
  required/optional_parameter (name từ field pattern, type từ type_annotation
  — strip ':' + '?'). Trước đây params KHÔNG được extract.
- js_file_deps (share TS): import default/named-as/namespace → FileDeps.
  Bug kèm theo: class_type_name cũ không bao giờ match JS thuần (class_heritage
  không có extends_clause — chỉ TS có); js_extends xử lý cả 2 grammar.
- js_extends → impl_trait: inheritance fan-out cho class extends.
- js_fn_value_assign: `handler = process` / `const h = process` (identifier
  thuần) → PointerAssign machinery.

Tests: 3 fixture mới (java supers/sig/imports; js params/imports/extends/
fn-value; ts typed params + interface method scope). 20/21 suite (Redis env).

* PHP/Ruby/Lua/Scala semantic enrichment — hoàn thiện fan-out toàn ngôn ngữ

PHP (typing đã tốt: params/property/variable):
- php_supers: base_clause (extends) + class_interface_clause (implements) —
  LÀ NODE KIND không phải field name (child_by_field_name trả None!) —
  inheritance fan-out.
- php_signature_key + php_use_deps (namespace_use_declaration, bỏ
  function/const/wildcard).

Ruby (mỏng nhất — params chưa từng extract):
- param_container_kinds method_parameters: identifier/optional_parameter/
  keyword_parameter/splat_parameter.
- ruby_superclass → impl_trait (bỏ dynamic superclass `< Struct.new(..)`).
- ruby_require_deps: require/require_relative → FileDeps.

Lua:
- param_container_kinds "parameters" (kind thật, không phải parameter_list).
- lua_require_deps: `local mod = require('mod')` ghép variable_list với
  expression_list theo vị trí.
- lua_fn_value_assign: `handler = process` → PointerAssign.

Scala:
- scala_supers: field "extend" (extends_clause là node kind, field tên
  "extend") — extends B with T → "B,T".
- scala_signature_key + scala_file_deps (import_declaration, selector
  `.{a, b => c}`); token keyword "import" có cùng kind — guard child_count.

Bài học chung: field name vs node kind khác nhau từng grammar — luôn dump AST
trước khi wire hook.

* Swift/C# semantic enrichment — đủ 14/14 ngôn ngữ

Swift (trước đây gần trống — không hooks):
- decls += protocol_function_declaration: method KHAI BÁO trong protocol
  scope vào Interface → fan-out (đúng bug Rust từng mắc).
- swift_supers: inheritance_specifier (`class FileRepo: BaseRepo, Repo`) →
  impl_trait — protocol conformance đi cùng cơ chế fan-out.
- swift_signature_key: Swift dùng TRÙNG tên field "name" cho tên param VÀ
  type (lấy field "name" cuối); match cả protocol_function_declaration;
  return = user_type sau token "->".
- swift_import_deps: import_declaration → FileDeps.

C# (typing/annotations sẵn tốt):
- csharp_bases: base_list (node kind, unnamed child) → impl_trait
  (base class + interfaces, strip generic).
- csharp_signature_key: return field là "returns" (không phải "type").
- csharp_using_deps: `using App.Repos` + alias form `using Db = App.Data`.

Tests: 2 fixture mới (swift protocol/supers/sig/imports; csharp bases/sig/
using). 25/25 dynamic_dispatch, 20/21 workspace (Redis env-only).

* FunctionValue + member assignment + MARKER_AWAIT (priority 1/3/6)

A. FunctionValue cho 4 ngôn ngữ còn thiếu:
- Python: `handler = process` (identifier=identifier) + `self.handler = process`
  (member, var = "self.handler").
- Ruby: CHỈ `handler = method(:process)` — plain `x = process` là LỜI GỌI
  trong Ruby, không track.
- Java: `Runnable r = this::run` — method_reference, target = identifier cuối
  (không phụ thuộc field name grammar).
- C#: `Action handler = Process` — value là identifier UNNAMED (không có
  field "value") — lấy identifier con cuối khác name.

B. Member fn assignment `obj.handler = fn`:
- Mở guard fn_ptr_index (bỏ `!contains('.')`) — lookup theo call_name bare
  lẫn dotted.
- js/ts: member_expression left → key full dotted text (`Foo.prototype.run`).
- lua: text chứa '.' → full text; bracket `handlers["read"]` skip.
- python: self.attr.

C. MARKER_AWAIT (Marker::Await = 27): MarkerSpec.await_kinds — python "await",
js/ts/csharp/swift "await_expression" (dump AST xác nhận); walk_chain push
AWAIT trước call như GO/DEFER → edge mang ngữ cảnh async boundary.

E2E: member_fn_assignment_resolves_dotted_call — `obj.handler = process` +
`obj.handler()` → PointerAssign (trước đây guard chặn). 25/25 dynamic_dispatch
(2 test mới), 20/21 workspace (Redis env-only).

* C/C++ enrichment batch 1-4: include graph, fn-ptr mở rộng, sig_key, virtual precision

1. c_include_deps: #include "foo.h" / <openssl/ssl.h> → FileDeps (path nguyên
   văn, alias = tên file không extension) — include graph qua machinery
   file_imports; C + C++ share.
2. c_fn_ptr_assign mở rộng: LHS field_expression (`obj.handler` — dotted key,
   ăn guard fn_ptr_index mới) + pointer_expression (`*handler` → "handler");
   RHS `&f` address-of + `&ns::cb` qualified.
3. cpp_signature_key: params từ function_declarator (function_definition 1
   tầng declarator; method inline field_declaration cũng 1 tầng — dump mới
   thấy, không phải 2); return field "type".
4. Virtual precision trong collect_fanout (C++ path): nếu CÓ candidate mang
   keyword virtual/override (signature chứa — extractor lưu dòng khai báo)
   → chỉ giữ virtual candidates + base decl; không keyword nào → giữ hết
   (soundness fallback). Loại non-virtual cùng tên trong hierarchy.

E2E: cpp_virtual_filter_prefers_override_candidates (D2 non-virtual bị loại).
Tests: 26/26 dynamic_dispatch (2 mới), 121/121 graph lib, 20/21 workspace
(Redis env-only).

* Web UI: dynamic-dispatch badges, Call sites/Pattern/Trace tabs, search pagination + annotation

Explore UI (SvelteKit + GraphQL) lộ đầy đủ query surface mới:

- Flow tab: mỗi call hiện badge resolvedVia ([dyn]/[fn-ptr]/[unresolved]/…)
  + candidate chips click-to-navigate (searchExact). GraphQL enum là
  SCREAMING_SNAKE_CASE — smoke query bắt được bug casing lowercase ban đầu.
- Tab Call sites (references): tìm callers theo call name — thấy cả
  unresolved/library + dynamic calls; prefill theo symbol name.
- Tab Pattern (searchFlow): tìm hàm theo chain pattern (markers/tên/id).
- Tab Trace (traceFullFlow — targetId arg): debug-style paths caller → target,
  indent theo stackDepth, icon theo action (CALL/RETURN/THROW/BRANCH/LOOP),
  màu theo PathEnding; chọn caller depth.
- SymbolSearch: Load more (offset pagination), "continue" khi timed_out
  (resumable cursor), source by name | by annotation
  (graphcodeSearchByAnnotation).

Smoke qua server thật: flow(ingest_with_progress) có 11 HIERARCHY_FAN_OUT
calls với 7 candidates (dyn Storage backends), trace 3 paths,
annotation "derive" 196 kết quả, references set_file_deps 2 callers.
svelte-check 0 errors, build-web assets embed, workspace tests 20/21
(Redis env-only).

* C/C++ batch #6 + #8: declaration–definition linking + namespace-qualified call

#6 link_cpp_decl_def (global, chạy cả ingest lẫn rebuild):
- `void Service::run() {}` trong .cpp (definition, có chain) nối với method
  khai báo trong .hpp (declaration, không chain): cùng tên + cùng sig_key
  (khi cả hai có) + khác file → def re-parent về scope của decl (class/
  namespace) + reclassify theo kind của decl; decl.type_ref → def.
- Hiệu ứng: fan-out/hierarchy thấy definition (có body/flow), class_info
  theo type_ref tới nơi có chain.

#8 qualified call `orders::run()`:
- Short-name fallback split cả '::' (rsplit ['.', ':'] bỏ segment rỗng) —
  trước đây "::" không split → qualified call unresolved hoàn toàn.
- Namespace preference: prefix trước '::' lọc candidate theo tổ tiên Module
  (namespace) cùng tên hoặc file chứa prefix — `foo::run` không còn nhầm
  `bar::run`; không subset khớp → giữ tất cả (soundness).

E2E: cpp_decl_def_linked_across_files (link đúng, pick_best ưu tiên def
has-chain); cpp_qualified_call_prefers_namespace (orders::run chọn đúng
namespace, không chọn bar). 123/123 graph lib, 20/21 workspace (Redis env).

* C/C++ batch cuối: lambda có tên (#5), macro theo thiết kế ghi-nhận (#9-lite), typedef signature (#4-lite)

#5 C++ lambda thành Function symbol:
- decls/func_kinds += lambda_expression → chain riêng cho calls trong lambda
  body (attribution chính xác, không gộp vào enclosing fn).
- anonymous_name_fn cpp_lambda_name: `auto f = [](){}` mượn tên biến —
  LƯU Ý init_declarator đặt tên ở field `declarator`, không phải `name`
  như JS variable_declarator (grammar khác nhau).

#9-lite macro THEO THIẾT KẾ GHI-NHẬN (không resolve):
- preproc_function_def + preproc_def → Constant symbol, signature = định
  nghĩa nguyên văn (`CALL(x) x()`, `MAX 10`) — macro KHÔNG phải function,
  invocation giữ raw call → usage tra qua call-name index (references).
- Hành vi semantic do config khai báo ("LLM tự định nghĩa"): `[[effect_rules]]`
  trong .codegraph/config.toml (matcher + EffectType, xét trước bảng default)
  — test chứng minh rule Contains "CALL" → Panic (exception-like).
- Không làm (đúng defer): macro expansion, conditional compilation.

#4-lite typedef signature:
- HookSpec thêm typedef_sig_fn; c_typedef_sig (C + C++ share): typedef +
  macro → signature = text định nghĩa (strip typedef/#define + ;). Hook
  ƯU TIÊN extract_signature (nó trả full text trước khiến hook không chạy).
- fn-ptr typedef giờ giữ underlying type: `int (*handler_t)(int)` thay vì
  chỉ `int` từ field type.

Tests: cpp_lambda_macro_typedef_batch (lambda symbol + chain + calls, macro
Constant + signature + raw invocation + effect-rule config, typedef sig).
20/21 workspace (Redis env-only), clippy sạch.

* MCP + GraphQL tool cấu hình effect rules runtime (LLM tự định nghĩa macro semantics)

Trước đây `[[effect_rules]]` chỉ đọc được từ .codegraph/config.toml — LLM phải
sửa file ngoài band rồi re-index. Giờ có tool chính tắc:

- config.rs CRUD API: list_effect_rules (raw — kể cả rule lỗi) /
  add_effect_rule (validate matcher + EffectType, toml_edit preserve mọi
  section khác) / remove_effect_rule (theo index). Sau ghi: install_current
  cho thread hiện tại; orchestrator reload config mỗi index nên rule áp dụng
  từ lần re-index kế tiếp (re-classify call records).
- MCP tool `codegraph_config_effects`: op list|add|remove; mô tả tool kèm
  ví dụ macro exception-like (contains CHECK → panic) + ghi chú áp dụng.
- GraphQL mutation `configEffects(op, matcher, text, effect, index)` mirror
  cho web UI.

Test: effect_rule_crud_roundtrip — add 2 rules → list đúng, classifier load
từ file classify CHECK_OK → Panic / db.Query → SqlQuery, effect sai bị chặn
(file không đổi), remove theo index, section khác (storage) nguyên vẹn.
Fix bug bắt được bởi test: or_insert tạo ArrayOfTables với table rỗng dummy
→ rule rỗng đầu danh sách.

* Bump version to v2.2.11

* Dọn debug eprintln sót (DBG by_name/DBG/DBG3/PHPDBG/SCALADBG) sau các batch debug
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant