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
27 changes: 14 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ members = [
]

[workspace.package]
version = "2.2.2"
version = "2.2.3"
edition = "2021"
rust-version = "1.80"
license = "MIT"
Expand Down
59 changes: 58 additions & 1 deletion crates/codegraph-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub struct ResumeDesc {
#[derive(Debug, Clone)]
pub enum ResumeCursor {
Name(SearchCursor),
Callers(codegraph_graph::CallersCursor),
Offset { next: usize, desc: ResumeDesc },
}

Expand Down Expand Up @@ -135,7 +136,9 @@ impl SearchSessionStore {
/// Đọc cursor theo id — `None` nếu không có / quá TTL.
pub fn get(&self, id: &str) -> Option<(u64, ResumeCursor)> {
let map = self.inner.lock().unwrap();
map.get(id).map(|s| (s.index_version, s.cursor.clone()))
map.get(id)
.filter(|s| s.created.elapsed() < self.ttl)
.map(|s| (s.index_version, s.cursor.clone()))
}

/// Xoá session (khi search hoàn tất, không còn page nào).
Expand Down Expand Up @@ -387,6 +390,60 @@ impl GraphApi {
self.index().await.callers(id, depth as usize).await
}

/// Resume caller traversal on the same query and index version.
pub async fn callers_resumable(
&self,
id: u64,
depth: u32,
resume: Option<String>,
timeout_ms: u64,
) -> Result<ResumeSearchOutcome> {
let idx = self.index().await;
let version = idx.version();
let cursor = match &resume {
Some(token) => {
let (stored_version, cursor) = self.sessions.get(token).ok_or_else(|| {
Error::Invalid("resume id expired or unknown — retry without resume".into())
})?;
if stored_version != version {
return Err(Error::Invalid(
"index changed — retry without resume".into(),
));
}
match cursor {
ResumeCursor::Callers(c) if c.id == id && c.depth == depth.max(1) as usize => {
Some(c)
}
_ => {
return Err(Error::Invalid(
"resume id was created for a different query — retry without resume"
.into(),
));
}
}
}
None => None,
};
let out = idx
.callers_resumable(id, depth as usize, cursor, deadline_from(timeout_ms))
.await?;
let timed_out = out.cursor.is_some();
if let Some(token) = &resume {
self.sessions.remove(token);
}
let token = out
.cursor
.map(|c| self.sessions.put(ResumeCursor::Callers(c), version));
Ok(ResumeSearchOutcome {
total: out.callers.len(),
page: out.callers,
timed_out,
progress: out.progress,
resume: token,
index_version: version,
})
}

/// Callees trực tiếp (đọc chain, skip marker/self).
pub async fn callees(&self, id: u64) -> Result<Vec<Symbol>> {
self.index().await.callees(id).await
Expand Down
10 changes: 5 additions & 5 deletions crates/codegraph-api/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,24 +60,24 @@ pub enum OutputStyle {
/// Mặc định — nhỏ gọn nhất: symbol thành mảng vị trí cố định (chỉ value,
/// order được document; value thiếu = sentinel null/0/""/[]).
#[default]
Minimize,
Minimal,
/// Giữ key, lược bỏ field có value mặc định (None/0/""/[]/{}).
Medium,
}

impl OutputStyle {
/// Parse từ tên arg (`minimize`/`medium`) — `None` nếu lạ.
/// Parse từ tên arg (`minimal`/`medium`) — `None` nếu lạ.
pub fn parse(s: &str) -> Option<Self> {
Some(match s {
"minimize" => Self::Minimize,
"minimal" => Self::Minimal,
"medium" => Self::Medium,
_ => return None,
})
}

pub fn as_str(self) -> &'static str {
match self {
Self::Minimize => "minimize",
Self::Minimal => "minimal",
Self::Medium => "medium",
}
}
Expand Down Expand Up @@ -221,7 +221,7 @@ impl Session {
*self.detail.read().await
}

/// Output format hiện tại (minimize/medium) cho mọi response.
/// Output format hiện tại (minimal/medium) cho mọi response.
pub async fn format(&self) -> OutputStyle {
*self.format.read().await
}
Expand Down
16 changes: 11 additions & 5 deletions crates/codegraph-api/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,7 @@ pub fn relativize_paths(v: &mut Value, root: &str) {
/// Serialize payload JSON kèm relativize path theo root — mọi response tool
/// đi qua đây để `file`/`path` trả về tương đối so với workspace root.
pub fn emit_value(root: &str, v: Value) -> Result<String> {
let mut v = v;
relativize_paths(&mut v, root);
omit_defaults(&mut v);
serde_json::to_string_pretty(&v).map_err(|e| Error::Invalid(e.to_string()))
emit_unpruned(root, v)
}

/// `emit_value` cho bất kỳ type serializable nào (chuyển qua `to_value`).
Expand All @@ -69,6 +66,15 @@ pub fn emit<T: Serialize>(root: &str, v: &T) -> Result<String> {
emit_value(root, value)
}

/// Serialize giữ nguyên structure (không lược default): các frontend formatter
/// (vd MCP `format_response`) cần sentinel gốc (0 / [] / "") để dựng layout
/// `minimal` chi tiết-correct; pruning ở đây sẽ làm mất data trước formatter.
pub fn emit_unpruned(root: &str, v: Value) -> Result<String> {
let mut v = v;
relativize_paths(&mut v, root);
serde_json::to_string(&v).map_err(|e| Error::Invalid(e.to_string()))
}

/// Keys có `0` = "absent" (sentinel) — value 0 bị lược như default. Các số khác
/// (counts/totals như `total`, `symbols`, `lines`, ...) giữ nguyên 0 vì ý nghĩa.
const ZERO_SENTINEL_KEYS: [&str; 3] = ["scope_id", "type_ref", "end_line"];
Expand All @@ -87,7 +93,7 @@ fn is_default_value(key: &str, v: &Value) -> bool {
}

/// Lược bỏ key có value mặc định trong mọi OBJECT (in-place). ARRAY không bao
/// giờ bị xóa phần tử — schema mảng vị trí cố định (style `minimize`) phải giữ
/// giờ bị xóa phần tử — schema mảng vị trí cố định (style `minimal`) phải giữ
/// nguyên độ dài; chỉ object con bên trong được xử lý tiếp.
pub fn omit_defaults(v: &mut Value) {
match v {
Expand Down
87 changes: 87 additions & 0 deletions crates/codegraph-api/tests/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,93 @@ async fn search_and_symbol_by_id() {
assert!(api.symbol_by_id(9999).await.is_none());
}

#[tokio::test]
async fn callers_resume_roundtrip_and_validation() {
use codegraph_api::TIMEOUT_EXPIRE_IMMEDIATELY as EXPIRED;
let dir = tempfile::tempdir().unwrap();
let dsn = format!("sqlite://{}", dir.path().join("resume.db").display());
let (caller, callee, helper) = seed_index(&dsn).await;
let api = api(&dsn).await;
let first = api
.callers_resumable(helper, 2, None, EXPIRED)
.await
.unwrap();
assert!(first.timed_out && first.page.is_empty());
let token = first.resume.unwrap();
for (id, depth) in [(callee, 2), (helper, 1)] {
assert!(api
.callers_resumable(id, depth, Some(token.clone()), 0)
.await
.is_err());
}
assert!(api
.callers_resumable(helper, 2, Some("unknown".into()), 0)
.await
.is_err());
assert!(api
.search_symbol_paged_resumable(
"helper",
None,
SymbolMatch::Contains,
Pagination {
limit: 5,
offset: 0
},
Some(token.clone()),
0
)
.await
.is_err());
let again = api
.callers_resumable(helper, 2, Some(token.clone()), EXPIRED)
.await
.unwrap();
assert!(again.timed_out);
assert!(api
.callers_resumable(helper, 2, Some(token), 0)
.await
.is_err());
let token = again.resume.unwrap();
let done = api
.callers_resumable(helper, 2, Some(token.clone()), 0)
.await
.unwrap();
assert!(!done.timed_out && done.resume.is_none());
assert_eq!(
done.page.iter().map(|s| s.id).collect::<Vec<_>>(),
vec![callee, caller]
);
assert!(api
.callers_resumable(helper, 2, Some(token), 0)
.await
.is_err());
let name = api
.search_symbol_paged_resumable(
"helper",
None,
SymbolMatch::Contains,
Pagination {
limit: 5,
offset: 0,
},
None,
EXPIRED,
)
.await
.unwrap();
assert!(api
.callers_resumable(helper, 2, name.resume, 0)
.await
.is_err());
let stale = api
.callers_resumable(helper, 2, None, EXPIRED)
.await
.unwrap()
.resume;
seed_index(&dsn).await;
assert!(api.callers_resumable(helper, 2, stale, 0).await.is_err());
}

#[tokio::test]
async fn callers_callees_and_flow() {
let dir = tempfile::tempdir().unwrap();
Expand Down
5 changes: 5 additions & 0 deletions crates/codegraph-bench/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ description = "Benchmark codegraph-extract + codegraph-graph trên các repo th
codegraph-extract = { path = "../codegraph-extract" }
codegraph-graph = { path = "../codegraph-graph", features = ["sqlite", "lmdb"] }
codegraph-core = { path = "../codegraph-core" }
codegraph-context = { path = "../codegraph-context" }

anyhow = { workspace = true }
camino = { workspace = true }
Expand Down Expand Up @@ -45,3 +46,7 @@ harness = false
[[bench]]
name = "storage"
harness = false

[[bench]]
name = "context"
harness = false
Loading
Loading