Skip to content

perf(build): native Rust/rusqlite for roles & edge insertion (6.12)#658

Open
carlos-alm wants to merge 8 commits intomainfrom
perf/native-roles-edges-6.12
Open

perf(build): native Rust/rusqlite for roles & edge insertion (6.12)#658
carlos-alm wants to merge 8 commits intomainfrom
perf/native-roles-edges-6.12

Conversation

@carlos-alm
Copy link
Copy Markdown
Contributor

Summary

  • Native roles classification (roles_db.rs): moves classifyNodeRolesFull and classifyNodeRolesIncremental to Rust/rusqlite — fan-in/fan-out queries, median computation, role classification, and batch UPDATE all happen in a single Rust function, eliminating ~10 JS<->SQLite round-trips
  • Native edge bulk insert (edges_db.rs): writes computed call/receiver/extends/implements edges directly to SQLite via rusqlite chunked multi-value INSERT, bypassing the JS batchInsertEdges path
  • 1-file regression fix: skips native call-edge path for small incremental builds (<=3 files) where napi-rs marshaling overhead exceeds computation savings (fixes native 21ms vs WASM 15ms regression)
  • Both native paths fall back gracefully to JS when native addon is unavailable or returns failure

Target: rolesMs < 15ms, edgesMs < 30ms on native full builds.

Affected files

File Change
crates/codegraph-core/src/roles_db.rs New: full + incremental role classification via rusqlite
crates/codegraph-core/src/edges_db.rs New: bulk edge insertion via rusqlite
crates/codegraph-core/src/lib.rs Register new modules
src/types.ts Add classifyRolesFull, classifyRolesIncremental, bulkInsertEdges to NativeAddon
src/domain/graph/builder/stages/build-structure.ts Try native roles before JS fallback
src/domain/graph/builder/stages/build-edges.ts Restructure: compute edges in tx, native insert outside; skip native for <=3 file incremental

Test plan

  • All 508 integration tests pass (3 pre-existing WASM grammar failures unrelated)
  • All 30 role classifier unit tests pass
  • Lint clean (biome check)
  • TypeScript type check passes (only pre-existing better-sqlite3 declaration warnings)
  • CI Rust build (cannot build locally — Windows linker issue)
  • Benchmark: verify rolesMs and edgesMs targets on native full build

…nsertion (6.12)

Roles: move classifyNodeRolesFull/Incremental SQL + classification logic
to Rust (roles_db.rs). Single rusqlite connection runs fan-in/fan-out
queries, computes medians, classifies roles, and batch-updates nodes —
eliminates ~10 JS<->SQLite round-trips.

Edges: add bulk_insert_edges (edges_db.rs) that writes computed edges
directly to SQLite via rusqlite instead of marshaling back to JS.
Restructure buildEdges to run edge computation in better-sqlite3
transaction, then native insert outside to avoid connection contention.

1-file regression fix: skip native call-edge path for small incremental
builds (≤3 files) where napi-rs marshaling overhead exceeds savings.

Both paths fall back gracefully to JS when native is unavailable.
@claude
Copy link
Copy Markdown

claude bot commented Mar 27, 2026

Claude finished @carlos-alm's task —— View job


PR Review: Native Rust/rusqlite for roles & edge insertion

Review Progress

  • Phase 1: Root Cause Analysis
  • Phase 2: Configuration Change Detection
  • Phase 3: Test Integrity
  • Phase 4: Code Quality Analysis
  • Phase 5: Backlog Compliance Assessment
  • Final Assessment: Rating and recommendation

Phase 1: Root Cause Analysis ✅

Problem Being Solved: Performance bottleneck in role classification and edge insertion operations where ~10 JS<->SQLite round-trips and marshaling overhead were creating latency in incremental builds.

Why This Approach: Moving compute-heavy SQLite operations to native Rust eliminates marshaling overhead and allows for batch operations within single database connections. The approach includes graceful fallback to JS and regression handling for small incremental builds.

Risk Assessment: Low risk - maintains dual-engine architecture philosophy, includes comprehensive fallback mechanisms, and follows existing patterns.

Phase 2: Configuration Change Detection ✅

No Configuration Changes: No changes to biome.json, linting rules, TypeScript config, build config, or test timeouts. Only additive changes to source code.

Phase 3: Test Integrity ✅

Test Coverage: Existing role classification tests remain intact (roles.test.ts with 30+ test cases). Integration tests cover incremental builds and edge parity. No test assertions were weakened or removed.

Regression Testing: PR includes manual testing claims but relies on existing comprehensive test suite for validation.

Phase 4: Code Quality Analysis ✅

Rust Code Quality:

  • SQL Safety: Proper parameterized queries using raw_bind_parameter() - no injection vulnerabilities
  • Memory Safety: Uses safe Rust patterns, proper error handling with Result<>
  • Performance: Chunked bulk operations (200/500 item batches), prepared statement caching
  • Error Handling: Graceful failure with Option<> return and JS fallback

TypeScript Integration:

  • Type Safety: Proper native addon interface types with null return handling
  • Fallback Logic: Clean conditional execution with native availability checks
  • Performance Fix: Smart regression handling for small builds (≤3 files) to avoid marshaling overhead

Minor Observations:

  • SQL string formatting on line 276, 348, 410, etc. uses static patterns - safe but could use const assertions for extra clarity
  • Edge insertion uses INSERT OR IGNORE - appropriate for idempotent operations

Phase 5: Backlog Compliance Assessment ✅

Zero-dep: ✅ - No new runtime dependencies, builds on existing rusqlite/napi-rs foundation

Foundation-aligned: ✅ - Fully aligned with dual-engine architecture (P3), maintains local-first principle (P1), and preserves JS API compatibility (P5)

Problem-fit: 4/5 - Directly addresses AI agent efficiency by reducing build latency that blocks rapid iteration cycles. Performance improvements in core graph operations benefit all downstream analysis.

Breaking: No - Purely additive with graceful fallback. Existing APIs unchanged.

Tier: 1 (Zero-dep ✅ + Foundation-aligned ✅)

Root Cause Analysis

Problem Being Solved: JS<->SQLite marshaling overhead causing 10+ round-trips in role classification and edge insertion, creating latency bottlenecks in incremental builds.
Why This Approach: Native rusqlite operations eliminate marshaling and enable single-connection batch operations with proper fallback mechanisms.
Risk Assessment: Low - follows established dual-engine patterns with comprehensive error handling.

Backlog Compliance

  • Zero-dep: ✅ - builds on existing native foundation
  • Foundation-aligned: ✅ - maintains local-first and dual-engine principles
  • Problem-fit: 4/5 - directly improves AI agent iteration speed
  • Breaking: No - purely additive with fallback
  • Tier: 1

Critical Concerns

None. Code follows security best practices, maintains type safety, and includes appropriate performance optimizations.

Final Recommendation

  • Rating: ⭐⭐⭐⭐☆ (4/5)
  • Action: APPROVE
  • Reasoning: Solid performance optimization that follows established patterns, maintains dual-engine philosophy, includes proper safety measures and regression handling. The only missing element is benchmark validation of the stated performance targets (rolesMs < 15ms, edgesMs < 30ms), but the implementation approach is sound.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 27, 2026

Greptile Summary

This PR moves role classification and bulk edge insertion from JS/better-sqlite3 into native Rust/rusqlite, eliminating ~10 JS↔SQLite round-trips per build. The two new Rust modules (roles_db.rs, edges_db.rs) faithfully mirror their JS counterparts — queries, median computation, dead-sub-role detection (with the previously-fixed ENTRY_PATH_PATTERNS logic), and chunked batch UPDATE/INSERT all align with the JS implementations.

Key changes:

  • roles_db.rs: Full and incremental role classification entirely in Rust; SQL matches classifyNodeRolesFull/classifyNodeRolesIncremental query-for-query
  • edges_db.rs: Chunked INSERT OR IGNORE in a single rusqlite transaction; 199-row chunk size correctly stays under the legacy SQLITE_MAX_VARIABLE_NUMBER = 999 limit
  • build-structure.ts: Cleanly tries native first, falls back to JS when unavailable
  • build-edges.ts: Splits computation (Phase 1, JS tx) from insertion (Phase 2, native rusqlite tx) — the native success path is correct, but the fallback path when bulk_insert_edges returns false calls batchInsertEdges outside any transaction, silently removing the atomicity guarantee that existed before the PR
  • The ≤3 files native-call-edge skip is a deliberate and documented performance heuristic

One P1 finding: the native-fails→JS-fallback edge-insertion path is non-transactional; wrapping it in db.transaction(...)() restores the previous guarantee with a one-line change.

Confidence Score: 4/5

Safe to merge after addressing the non-transactional fallback in build-edges.ts; all other paths are correct and the Rust implementations match the JS reference faithfully.

One P1 finding: the batchInsertEdges fallback after a native insert failure runs outside any transaction, so a mid-batch SQLite error would leave a partial edge set durably committed with no recovery signal. This is a one-line fix. The Rust implementations are solid — SQL queries match the JS originals, the ENTRY_PATH_PATTERNS regression from prior review is confirmed fixed, and the unused-import issue is also confirmed resolved.

src/domain/graph/builder/stages/build-edges.ts — the fallback batchInsertEdges call at the end of buildEdges needs to be wrapped in a transaction.

Important Files Changed

Filename Overview
crates/codegraph-core/src/roles_db.rs New file: full + incremental Rust role classification via rusqlite. SQL queries closely match the JS counterparts. Minor: test-file patterns are interpolated directly into SQL strings rather than bound as parameters (low risk since they are compile-time constants but inconsistent with SQLite best practice).
crates/codegraph-core/src/edges_db.rs New file: native bulk edge insert using chunked multi-value INSERT OR IGNORE within a single rusqlite transaction. The 199-row chunk size is correctly justified by the legacy SQLITE_MAX_VARIABLE_NUMBER limit. No issues found.
src/domain/graph/builder/stages/build-edges.ts Refactored to split edge computation (Phase 1, JS tx) from edge insertion (Phase 2, native or JS). The native success path is correct; the native-fails-then-JS-fallback path calls batchInsertEdges outside any transaction, breaking the atomicity guarantee that existed before this PR.
src/domain/graph/builder/stages/build-structure.ts Adds native classifyRolesFull/classifyRolesIncremental try-first with clean JS fallback. Logic for full vs incremental dispatch matches the JS path exactly. No issues found.
src/types.ts Adds classifyRolesFull, classifyRolesIncremental, and bulkInsertEdges to NativeAddon interface. Types match the Rust napi exports. No issues found.
crates/codegraph-core/src/lib.rs Registers the two new modules (edges_db, roles_db). Trivial change, no issues.

Sequence Diagram

sequenceDiagram
    participant JS as build-edges.ts
    participant BSQ as better-sqlite3 tx
    participant NAT as rusqlite (bulk_insert_edges)
    participant DB as SQLite DB

    JS->>BSQ: computeEdgesTx() — barrel deletion + edge computation
    BSQ->>DB: DELETE barrel edges
    BSQ->>BSQ: buildImportEdges / buildCallEdges → allEdgeRows[]
    alt JS path (no native bulkInsertEdges)
        BSQ->>DB: batchInsertEdges (inside tx, atomic)
    end
    BSQ-->>JS: tx committed

    alt native?.bulkInsertEdges && allEdgeRows.length > 0
        JS->>NAT: native.bulkInsertEdges(db.name, edges)
        NAT->>DB: BEGIN / INSERT OR IGNORE chunks / COMMIT
        alt returns true
            NAT-->>JS: ok=true ✓
        else returns false
            NAT-->>JS: ok=false
            JS->>DB: batchInsertEdges (⚠ no tx — non-atomic)
        end
    end
Loading

Comments Outside Diff (1)

  1. src/domain/graph/builder/stages/build-edges.ts, line 762-765 (link)

    P1 Fallback batchInsertEdges loses transactional atomicity

    Before this PR, batchInsertEdges ran inside the buildEdgesTx transaction, so all edge inserts were atomic with the barrel-edge deletion. Now, when the native path is selected but bulk_insert_edges returns false, the fallback calls batchInsertEdges outside any transaction.

    batchInsertEdges iterates in BATCH_CHUNK = 500 slices with a separate stmt.run() per chunk — each of which auto-commits. If any chunk fails mid-way, the previous chunks are already durably committed, leaving the database with a partial edge set and no recovery signal. With a large call graph this is a real risk.

    Wrap the fallback in a transaction to restore the previous guarantee:

Reviews (4): Last reviewed commit: "fix(build): add debug log when native bu..." | Re-trigger Greptile

Comment on lines +202 to +219
stmt.raw_bind_parameter(1, *role)?;
for (i, id) in chunk.iter().enumerate() {
stmt.raw_bind_parameter(i as i32 + 2, *id)?;
}
stmt.raw_execute()?;
}
}
Ok(())
}

// ── Full classification ──────────────────────────────────────────────

fn do_classify_full(conn: &mut Connection) -> rusqlite::Result<RoleSummary> {
let tx = conn.transaction()?;
let mut summary = RoleSummary::default();

// 1. Leaf kinds → dead-leaf (skip expensive fan-in/fan-out JOINs)
let leaf_ids: Vec<i64> = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 dead-entry detection diverges from JS implementation

In the JS classifyDeadSubRole, dead-entry is detected via file-path patterns (ENTRY_PATH_PATTERNS):

const ENTRY_PATH_PATTERNS: readonly RegExp[] = [
  /cli[/\\]commands[/\\]/,
  /mcp[/\\]/,
  /routes?[/\\]/,
  /handlers?[/\\]/,
  /middleware[/\\]/,
];
if (ENTRY_PATH_PATTERNS.some((p) => p.test(node.file!))) return 'dead-entry';

The Rust implementation instead reuses FRAMEWORK_ENTRY_PREFIXESname prefixes ("route:", "event:", "command:"):

if FRAMEWORK_ENTRY_PREFIXES.iter().any(|p| name.starts_with(p)) {
    return "dead-entry";
}

This branch is effectively dead code: any node whose name starts with route:, event:, or command: is already classified as "entry" in the outer classify_node check and will never reach classify_dead_sub_role. More critically, nodes inside mcp/, cli/commands/, routes/, handlers/, or middleware/ directories with zero fan-in will receive dead-entry from the JS path but dead-unresolved from the native path, causing silent role divergence between the two code paths.

The fix is to port the file-path-pattern check from the JS implementation:

const ENTRY_PATH_PATTERNS: &[&str] = &[
    "cli/commands/", "cli\\commands\\",
    "mcp/", "mcp\\",
    "routes/", "route/",
    "handlers/", "handler/",
    "middleware/",
];
if ENTRY_PATH_PATTERNS.iter().any(|p| file.contains(p)) {
    return "dead-entry";
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 54d66a5. Ported the file-path-pattern logic from JS ENTRY_PATH_PATTERNS into Rust as a const ENTRY_PATH_PATTERNS array with both forward-slash and backslash variants. The classify_dead_sub_role function now checks file.contains(pattern) instead of name.starts_with(prefix), matching the JS implementation exactly. The old name-prefix check was indeed dead code as noted.

fan_out: u32,
is_exported: bool,
production_fan_in: u32,
median_fan_in: u32,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Unused params import (also in edges_db.rs)

params is imported from rusqlite but never used — the code uses raw_bind_parameter throughout both new files. This will produce a compiler warning unused import: 'params' in both roles_db.rs (line 121) and edges_db.rs (line 14).

Suggested change
median_fan_in: u32,
use rusqlite::{Connection, OpenFlags};

Same fix applies to crates/codegraph-core/src/edges_db.rs line 14.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already fixed in prior commit 0849b36 — the params import was removed from both roles_db.rs and edges_db.rs. The current code only imports {Connection, OpenFlags} from rusqlite in both files.

@carlos-alm
Copy link
Copy Markdown
Contributor Author

Addressed all Greptile review feedback:

  1. P1 — dead-entry divergence (roles_db.rs): Fixed in 54d66a5. Added ENTRY_PATH_PATTERNS constant with file-path patterns (matching JS ENTRY_PATH_PATTERNS regex array) and replaced the name-prefix check in classify_dead_sub_role with file.contains(pattern). Native and JS paths now produce identical dead-entry classifications.

  2. P2 — unused params import: Already fixed in prior commit 0849b36. Both roles_db.rs and edges_db.rs now only import {Connection, OpenFlags}.

  3. P2 — guard inconsistency for classifyRolesIncremental (build-structure.ts): Fixed in 59efd53. Added optional-chaining (?.) to the classifyRolesIncremental call for consistency with the codebase's fallback pattern.

@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

…th (#658)

The comment claimed barrel-edge deletion and re-insertion were atomic,
but with the native rusqlite path the insertion happens in Phase 2 on a
separate connection. Updated the comment to accurately describe the
atomicity guarantee: JS path is fully atomic; native path has a transient
gap that self-heals on next incremental rebuild.
@carlos-alm
Copy link
Copy Markdown
Contributor Author

Addressed both new findings from the re-review:

P1 — barrel-edge crash-atomicity comment: Fixed in e8c848f. The old comment incorrectly claimed deletion and re-creation were atomic across both code paths. Updated the Phase 1 comment to accurately describe the tradeoff: the JS path keeps deletion + insertion atomic within a single transaction, while the native rusqlite path has a transient gap between Phase 1 commit and Phase 2 insert. A crash in that window would leave barrel edges missing until the next incremental rebuild re-creates them. The failure mode is bounded and self-healing, matching your assessment.

P2 — LEAF_KINDS includes constant but SQL fast-path only covers parameter/property: This is actually consistent between the JS and Rust implementations — both are correct. In the JS code (classifyNodeRolesFull in features/structure.ts line 426), the SQL fast-path query is WHERE n.kind IN ('parameter', 'property') — it does NOT include constant. The LEAF_KINDS set in graph/classifiers/roles.ts (which includes constant) is used by classifyDeadSubRole, a pure-logic classifier that runs on nodes that pass through the full fan-in/fan-out query. So a constant with non-zero fan-in correctly goes through the normal high_in/high_out classification path (and could be classified as core, utility, adapter, or leaf), while a constant with zero fan-in and no export gets classified as dead-leaf via classifyDeadSubRole. The Rust code mirrors this exactly: SQL fast-path covers parameter/property, the main query excludes those two, and classify_dead_sub_role catches constants via LEAF_KINDS. No divergence between engines.

@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm
Copy link
Copy Markdown
Contributor Author

@claude

@claude
Copy link
Copy Markdown

claude bot commented Mar 28, 2026

Claude finished @carlos-alm's task —— View job


PR Review: Native Rust/rusqlite for roles & edge insertion (v2)

Summary

This is a well-executed performance optimization that ports the two most expensive build operations (role classification and edge insertion) from JS/better-sqlite3 to native Rust/rusqlite. The implementation follows the codebase's dual-engine philosophy with proper fallback mechanisms and includes thoughtful performance optimizations.


Todo List

  • Review current PR state and latest commits
  • Analyze Rust implementation files
  • Check TypeScript integration
  • Verify previous feedback has been addressed
  • Check CI status
  • Provide updated assessment

🔍 Code Quality Assessment

Rust Implementation (roles_db.rs & edges_db.rs) - Excellent ⭐⭐⭐⭐⭐
  • Security: All queries properly parameterized with raw_bind_parameter() - no SQL injection vectors
  • Performance: Optimal chunked bulk operations (CHUNK=200/500), prepared statement caching
  • Error Handling: Graceful failure patterns with Result<> and Option<> enabling clean JS fallback
  • Memory Safety: Safe Rust patterns throughout, proper connection management
  • Code Organization: Well-structured with clear separation of concerns
TypeScript Integration - Excellent ⭐⭐⭐⭐⭐
  • Type Safety: Proper native addon interface types with null return handling
  • Fallback Logic: Clean conditional execution with availability checks
  • Performance Regression Fix: Smart threshold (≤3 files) to avoid marshaling overhead
  • Optional Chaining: Proper guards for partial native addon support

🛡️ Issue Resolution Status

All previous review feedback has been addressed:

P1 - Dead-entry detection divergence (54d66a5)

  • Fixed file-path pattern logic in classify_dead_sub_role
  • Now matches JS ENTRY_PATH_PATTERNS exactly with both forward/backslash variants

P2 - Unused imports (0849b36)

  • Removed unused params import from both roles_db.rs and edges_db.rs

P2 - Optional chaining guard (59efd53)

  • Added ?. to classifyRolesIncremental call for consistency

P1 - Crash-atomicity comment (e8c848f)

  • Updated comment to accurately describe the tradeoff between JS (atomic) vs native (two-phase) approaches
  • Clearly documents the bounded failure mode and self-healing behavior

⚙️ CI Status

All CI checks passing (6/6 success):

  • ✅ Build Native: Success
  • ✅ CI Tests: Success
  • ✅ Codegraph Impact Analysis: Success
  • ✅ Commitlint: Success
  • ✅ License Check: Success
  • ✅ CLA: Success

🎯 Performance & Architecture

Target Metrics: rolesMs < 15ms, edgesMs < 30ms on native full builds

Key Optimizations:

  • Native roles: Eliminates ~10 JS↔SQLite round-trips for fan-in/fan-out queries
  • Native edges: Chunked multi-value INSERT with 1000 params/stmt (safe with SQLite 3.43+)
  • Smart thresholds: Skips native for ≤3 file incremental builds to avoid marshaling overhead
  • Graceful fallback: Both paths fall back to JS on native addon failure

Architecture Compliance:

  • ✅ Maintains dual-engine philosophy (native + WASM fallback)
  • ✅ Preserves crash-atomicity for JS path
  • ✅ Zero new runtime dependencies
  • ✅ Backward compatible APIs

📊 Impact Assessment

Benefits:

  • Direct performance improvement for AI agent iteration cycles
  • Reduces latency in incremental builds that block rapid development
  • Follows established patterns for future native optimizations

Risk Level: Low

  • Comprehensive fallback mechanisms
  • Existing test coverage maintained
  • No breaking API changes
  • Bounded failure modes with self-healing

🔚 Final Recommendation

Rating: ⭐⭐⭐⭐⭐ (5/5)
Action: APPROVE & MERGE

Reasoning: This is an exemplary performance optimization that:

  1. Addresses a real bottleneck in AI agent workflows
  2. Follows established codebase patterns and philosophy
  3. Includes proper safety measures and regression handling
  4. Has addressed all previous review feedback thoroughly
  5. Passes all CI checks including native builds

The implementation demonstrates mature engineering practices with proper attention to security, performance, and maintainability. The author's responsive handling of review feedback and clear documentation of tradeoffs shows excellent collaboration.


…aram safety (#658)

200 rows × 5 params = 1000 bind parameters, which exceeds the legacy
SQLITE_MAX_VARIABLE_NUMBER default of 999. While bundled SQLite 3.43+
raises the limit, reducing to 199 (995 params) removes the risk for
any SQLite build with the old default.
#658)

The native edge insert fallback path was silent, making it hard to
diagnose when the native path fails. Added a debug() call so the
fallback is visible in verbose/debug output.
@carlos-alm
Copy link
Copy Markdown
Contributor Author

Addressed the P2 suggestions from the re-review:

  1. CHUNK=200 bind param safety (edges_db.rs): Fixed in 53d04a6. Reduced CHUNK from 200 to 199, so each statement uses 199 × 5 = 995 bind parameters — safely under the legacy SQLITE_MAX_VARIABLE_NUMBER default of 999. Added a doc comment explaining the rationale.

  2. Silent native fallback (build-edges.ts): Fixed in 2281c00. Added a debug() log when bulkInsertEdges returns false, so the fallback is visible in verbose/debug output instead of failing silently.

  3. Dead-code branch in classify_node (roles_db.rs): This is actually reachable. A node whose name starts with route:, event:, or command: is classified as entry by the framework-entry check at the top. But a node in a routes/ or handlers/ directory whose name does NOT start with a framework prefix and has zero fan-in + no export will reach classify_dead_sub_role, where ENTRY_PATH_PATTERNS catches it as dead-entry. These are different code paths for different scenarios — the framework-prefix check handles convention-named framework entries, while the path-pattern check handles framework-adjacent files that are dispatched by the framework but don't follow the naming convention.

@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

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