Conversation
- Sanitize search query via flare-search-kit::fts_query to quote tokens so user input like 'PR-123' isn't parsed as column:value reference - Add flare-search-kit dependency to agentflare-backend - Use bm25() with name-weighted (3.0) scoring for better relevance ordering - Fix search function using raw query instead of sanitized safe variant - Make ranking test tolerant of BM25 ties
📝 WalkthroughWalkthroughThe backend adds an FTS5 index and project-scoped ranked item search. The MCP item tool gains a ChangesItem search
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant MCPClient
participant ItemRouter
participant ItemSearchHandler
participant BackendItemSearch
participant SQLiteFTS5
MCPClient->>ItemRouter: search action with query
ItemRouter->>ItemSearchHandler: dispatch request
ItemSearchHandler->>BackendItemSearch: project id, query, limit
BackendItemSearch->>SQLiteFTS5: sanitized MATCH query
SQLiteFTS5-->>BackendItemSearch: ranked matching items
BackendItemSearch-->>ItemSearchHandler: project-scoped items
ItemSearchHandler-->>MCPClient: JSON results
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
src/compact.rs (1)
51-59: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider wrapping per-line inserts in an explicit transaction.
Each
stmt.execute(...)implicitly commits individually; for large transcripts, wrapping the loop in an explicit transaction (conn.transaction()/BEGIN/COMMIT) reduces overhead.🤖 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 `@src/compact.rs` around lines 51 - 59, Wrap the per-line insert loop in the compact transcript insertion flow with an explicit transaction using the existing conn handle, execute the prepared statement through the transaction, and commit once after all lines are inserted. Preserve the current insert SQL, parameters, and error handling while replacing per-line commits with a single transaction commit.src/memory/mcp.rs (1)
310-322: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winO(n²) score lookup via
find()insidemap().For each of
entries.len()lines,scored.iter().find(...)scans up toscored.len(). AHashMap<usize, f64>built once fromscoredwould make this O(n).♻️ Proposed refactor
+ let score_by_index: std::collections::HashMap<usize, f64> = + scored.iter().map(|s| (s.index, s.score)).collect(); let output: Vec<serde_json::Value> = entries .iter() .enumerate() .map(|(i, entry)| { - let score = scored.iter().find(|s| s.index == i).map(|s| s.score); + let score = score_by_index.get(&i).copied();🤖 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 `@src/memory/mcp.rs` around lines 310 - 322, Replace the per-entry scored.iter().find lookup in the output construction with a HashMap<usize, f64> built once from scored before the entries iteration. Use the map to retrieve each score by index while preserving the existing JSON fields and None behavior for missing scores.src/mcp_server.rs (1)
601-603: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
limitdoc doesn't mention its newsearchusage.This field's description is scoped to
(list), butitem_searchnow also consumes it (with a different default/behavior than list's "omit for no limit"). See consolidated comment for the paired validation gap.🤖 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 `@src/mcp_server.rs` around lines 601 - 603, Update the schema description for the limit field in the relevant request type to document both list and item_search usage, including that search applies its own default or behavior when the limit is omitted. Keep the existing serde default and field type unchanged.
🤖 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 `@src/compact.rs`:
- Around line 34-83: Change score_lines to return a Result<Vec<ScoredLine>,
rusqlite::Error> and replace its SQLite-related expect calls, including
connection, table creation, statement preparation, inserts, and query execution,
with error propagation. Update handle_compact and any callers to propagate the
resulting error while preserving the existing empty and invalid-query early
returns.
In `@src/memory/mcp.rs`:
- Around line 273-275: Update the empty-input early return in the memory
response handler to include the same “query” field as the full response, while
preserving the existing empty lines, kept, and total values so both response
paths have a consistent shape.
- Around line 294-308: Update the deduplication of by_relevance in the
keep-quota filling logic so it preserves the original scored order from
score_lines while removing duplicate indices; do not sort the indices
numerically. Keep the existing filled-count and keep[idx] selection behavior
unchanged so highest-relevance entries are retained first.
- Around line 248-255: Update CompactInput and the handle_compact flow so the
scorer option has a real effect: select the requested scoring backend, including
the advertised fts5 and keyword choices, instead of always calling
crate::compact::score_lines. If backend selection is not supported, remove
scorer from the input and tool schema rather than leaving an ineffective field.
---
Nitpick comments:
In `@src/compact.rs`:
- Around line 51-59: Wrap the per-line insert loop in the compact transcript
insertion flow with an explicit transaction using the existing conn handle,
execute the prepared statement through the transaction, and commit once after
all lines are inserted. Preserve the current insert SQL, parameters, and error
handling while replacing per-line commits with a single transaction commit.
In `@src/mcp_server.rs`:
- Around line 601-603: Update the schema description for the limit field in the
relevant request type to document both list and item_search usage, including
that search applies its own default or behavior when the limit is omitted. Keep
the existing serde default and field type unchanged.
In `@src/memory/mcp.rs`:
- Around line 310-322: Replace the per-entry scored.iter().find lookup in the
output construction with a HashMap<usize, f64> built once from scored before the
entries iteration. Use the map to retrieve each score by index while preserving
the existing JSON fields and None behavior for missing scores.
🪄 Autofix (Beta)
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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 61fc90f0-8ff3-4495-bb3e-536fa31b1b45
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (10)
Cargo.tomlcrates/agentflare-backend/Cargo.tomlcrates/agentflare-backend/src/db.rscrates/agentflare-backend/src/item.rscrates/agentflare-backend/src/migrations/0005_items_fts.sqlsrc/compact.rssrc/main.rssrc/mcp_server.rssrc/mcp_server/item.rssrc/memory/mcp.rs
| pub fn score_lines(lines: &[LineEntry], query: &str) -> Vec<ScoredLine> { | ||
| if lines.is_empty() { | ||
| return vec![]; | ||
| } | ||
|
|
||
| // OR mode for broader recall — compaction keeps all potentially relevant lines. | ||
| let Some(safe_query) = fts_query(query, MatchMode::Any) else { | ||
| return vec![]; | ||
| }; | ||
|
|
||
| let conn = Connection::open_in_memory().expect("in-memory SQLite connection"); | ||
|
|
||
| conn.execute_batch( | ||
| "CREATE VIRTUAL TABLE transcript_fts USING fts5(\"ix\" UNINDEXED, text);", | ||
| ) | ||
| .expect("create FTS5 table"); | ||
|
|
||
| { | ||
| let mut stmt = conn | ||
| .prepare("INSERT INTO transcript_fts(\"ix\", text) VALUES(?1, ?2);") | ||
| .expect("prepare insert"); | ||
| for line in lines { | ||
| stmt.execute(rusqlite::params![line.index as i64, &line.text]) | ||
| .expect("insert line"); | ||
| } | ||
| } | ||
|
|
||
| let weights = Bm25Weights::new(vec![]); | ||
| let sql = format!( | ||
| "SELECT \"ix\", text, bm25(transcript_fts{}) AS score \ | ||
| FROM transcript_fts \ | ||
| WHERE transcript_fts MATCH ?1 \ | ||
| ORDER BY score", | ||
| weights.sql_args() | ||
| ); | ||
|
|
||
| let mut stmt = conn.prepare(&sql).expect("prepare query"); | ||
|
|
||
| let results = stmt | ||
| .query_map(rusqlite::params![safe_query], |row| { | ||
| Ok(ScoredLine { | ||
| index: row.get::<_, i64>("ix")? as usize, | ||
| text: row.get("text")?, | ||
| score: row.get("score")?, | ||
| }) | ||
| }) | ||
| .expect("query lines"); | ||
|
|
||
| results.filter_map(|r| r.ok()).collect() | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== compact.rs ==\n'
wc -l src/compact.rs
sed -n '1,140p' src/compact.rs
printf '\n== mcp caller ==\n'
rg -n "handle_compact|score_lines\\(" src -S
printf '\n== Cargo manifests and sqlite features ==\n'
rg -n "rusqlite|bundled|fts5|sqlite" Cargo.toml Cargo.lock . -S --glob '!target/**'Repository: getappz/agentflare
Length of output: 45796
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== handle_compact ==\n'
sed -n '240,310p' src/memory/mcp.rs
printf '\n== compact tests tail ==\n'
sed -n '140,220p' src/compact.rs
printf '\n== rusqlite/libsqlite3-sys feature clues ==\n'
rg -n "libsqlite3-sys|fts5|bundled" Cargo.lock Cargo.toml crates/*/Cargo.toml -SRepository: getappz/agentflare
Length of output: 4641
🌐 Web query:
rusqlite bundled feature FTS5 enabled SQLite bundled libsqlite3-sys
💡 Result:
When using the rusqlite crate with the bundled feature, the bundled SQLite library is compiled from the latest source, which includes support for FTS5 [1][2]. FTS5 is a standard part of the modern SQLite amalgamation, and the bundled feature essentially links against this pre-compiled amalgamation [1][3]. You do not need to perform additional steps to "enable" FTS5 if you are using the bundled feature; it is included by default as part of the bundled SQLite source [4][1]. If you encounter issues where FTS5 is not recognized, ensure that: 1. You are using the bundled feature in your Cargo.toml: [dependencies] rusqlite = { version = "0.39.0", features = ["bundled"] } [1][2] 2. If you are linking against a system SQLite (not using bundled), you must ensure the system's version of SQLite was compiled with FTS5 support enabled [5]. If you require custom compilation flags for the bundled SQLite (e.g., to enable specific extensions or modify the build), you can pass configuration flags via the LIBSQLITE3_FLAGS environment variable during the build process, as libsqlite3-sys (which handles the compilation) respects this variable [4].
Citations:
- 1: https://github.com/rusqlite/rusqlite/blob/890be7ba/README.md
- 2: https://docs.rs/crate/rusqlite/^0.39
- 3: https://github.com/rusqlite/rusqlite/blob/master/Cargo.toml
- 4: add extra features for bundled sqlite rusqlite/rusqlite#845
- 5: https://www.sqlite.org/fts5.html
Propagate SQLite errors from score_lines src/compact.rs:34-83
handle_compact already returns Result, so the .expect(...) calls here should bubble rusqlite::Error instead of panicking on the compact request path.
🤖 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 `@src/compact.rs` around lines 34 - 83, Change score_lines to return a
Result<Vec<ScoredLine>, rusqlite::Error> and replace its SQLite-related expect
calls, including connection, table creation, statement preparation, inserts, and
query execution, with error propagation. Update handle_compact and any callers
to propagate the resulting error while preserving the existing empty and
invalid-query early returns.
| if entries.is_empty() { | ||
| return Ok(serde_json::json!({"lines": [], "kept": 0, "total": 0}).to_string()); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Empty-lines response omits query, unlike the full response.
The full response (lines 324-330) includes "query", but this early return does not. Clients relying on a consistent response shape may break on the empty-input path.
💡 Proposed fix
if entries.is_empty() {
- return Ok(serde_json::json!({"lines": [], "kept": 0, "total": 0}).to_string());
+ return Ok(serde_json::json!({"lines": [], "kept": 0, "total": 0, "query": query}).to_string());
}📝 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.
| if entries.is_empty() { | |
| return Ok(serde_json::json!({"lines": [], "kept": 0, "total": 0}).to_string()); | |
| } | |
| if entries.is_empty() { | |
| return Ok(serde_json::json!({"lines": [], "kept": 0, "total": 0, "query": query}).to_string()); | |
| } |
🤖 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 `@src/memory/mcp.rs` around lines 273 - 275, Update the empty-input early
return in the memory response handler to include the same “query” field as the
full response, while preserving the existing empty lines, kept, and total values
so both response paths have a consistent shape.
- Backfill items_fts for items that already existed before this migration ran -- the AFTER INSERT/UPDATE/DELETE triggers only keep the index in sync going forward, so without a backfill every item created before this migration would be permanently invisible to item(search) until it happened to be touched again. - Clamp item::search's limit via flare_search_kit::clamped_limit instead of a raw `limit as i64` cast -- usize::MAX (or any caller-supplied value that wraps negative on cast) turns into SQL "LIMIT -1", which SQLite treats as unlimited, defeating the cap. Same class of bug this codebase already guards against in gateway-registry/skill-registry's own search() via the same helper. - Fix the limit field's schema description, which said "omit for no limit" -- true for item(list), but item(search) defaults to 20 (clamped to 1000) when omitted, not unlimited. - Resolve merge conflicts against master's already-fixed compact.rs/ memory/mcp.rs (this branch predates #192/#195 and carried duplicate, pre-fix copies of that code) by taking master's versions.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/mcp_server.rs (1)
2187-2200: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDispatch, error message, and tool description are consistent for
search.The
"search" => self.item_search(req)arm, the updated "unknown item action" error list, and the#[tool(...)]description all listsearchconsistently with theactionfield doc at line 554.One gap: I don't see an
item_search-specific unit test in this file's test module (existing item action tests cover create/list/update_state/cancel/claim/done, etc., but not search). Tests for it may live insrc/mcp_server/item.rs(not in this review's file set) — can you confirm coverage exists there for the empty-query rejection and a successful ranked-results case, similar toartifact_search_matches_name_description_and_content?🤖 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 `@src/mcp_server.rs` around lines 2187 - 2200, Add or confirm focused tests for item_search covering rejection of an empty query and successful ranked results matching name, description, and content, following the pattern of artifact_search_matches_name_description_and_content. Place the coverage in the existing item_search test module or the relevant test location without changing the dispatch or tool description.
🤖 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.
Nitpick comments:
In `@src/mcp_server.rs`:
- Around line 2187-2200: Add or confirm focused tests for item_search covering
rejection of an empty query and successful ranked results matching name,
description, and content, following the pattern of
artifact_search_matches_name_description_and_content. Place the coverage in the
existing item_search test module or the relevant test location without changing
the dispatch or tool description.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 37bdb7fd-fecc-43a0-b69f-797ea028ddf3
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (4)
crates/agentflare-backend/src/item.rscrates/agentflare-backend/src/migrations/0005_items_fts.sqlsrc/mcp_server.rssrc/mcp_server/item.rs
🚧 Files skipped from review as they are similar to previous changes (3)
- crates/agentflare-backend/src/item.rs
- crates/agentflare-backend/src/migrations/0005_items_fts.sql
- src/mcp_server/item.rs
…uiring /handoff inbox (#194) * feat: surface pending item queue at session start SessionStart hook now queries backend DB for items assigned to the current agent that are still open (excludes completed/cancelled via state group check). Pending items listed as: Pending items assigned to you (claude-code, 3 open): #42 Review PR #193 #47 MCP prompt argument completion Adds agentflare-backend::item::list_by_assignee_agent(conn, project_id, agent) with a JOIN on states to filter out completed/cancelled state groups. Graceful no-op when backend.db doesn't exist yet (first run). * Fix pending-item project lookup + address CodeRabbit findings - SessionStart's pending-item lookup used "SELECT id FROM projects WHERE deleted_at IS NULL ORDER BY created_at LIMIT 1" -- backend.db is a single shared database across every repo agentflare has ever touched on this machine, so with more than one project this picked an arbitrary (oldest) one instead of the current repo's, surfacing the wrong agent's pending items. Now reuses AgentflareMcp::resolve_project (made pub(crate)), the same repo-linked resolution every other item/artifact/comment call already goes through. - Cap the displayed pending-item list at 10 with a "+N more" note so an agent with a large open queue doesn't blow up the session-start message. - Fix clippy (collapsible_if via let-chains) and fmt. - Update the regression test to route project setup through the same resolve_project() call session_start_message uses internally, so it proves the actual fix instead of a lookup path that no longer matches production.
…ntic warnings in an unrelated crate. Both crates compile, clippy is clean on the changed files, and all tests pass. Implemented the dependency-cascade auto-dispatch mechanism per the spec: 1. **`crates/agentflare-backend/src/item/relations.rs`**: added `dependents_of(conn, item_id)` (reverse of `list_dependencies`) and `all_dependencies_completed(conn, item_id)` (true only when the item has ≥1 dependency and every one is in the `completed` state group; a soft-deleted dependency target counts as unsatisfied, fail-closed). 2. **`src/supervisor.rs`**: added `cascade_unblock_dependents(conn, item_id)` — for each dependent of `item_id` whose dependencies are now all completed, applies `READY_LABEL` via the existing `item::add_label` (idempotent `INSERT OR IGNORE`). Dependents with no `assignee_agent` are skipped with a loud `eprintln!` rather than silently no-op'd, per option (a) in the spec. 3. **`src/mcp_server/item.rs`**: `item_check_merge` now calls `cascade_unblock_dependents` right after `promoted` becomes true — this is the single hook point that catches both the automatic tick (`promote_merged_item`) and manual/reconciliation calls. **Tests** (8 new, all passing): - Backend: `dependents_of_finds_reverse_edges`, `all_dependencies_completed_requires_every_dependency_done`, `all_dependencies_completed_false_for_item_with_no_dependencies`. - Supervisor: labels a dependent once its only dependency completes; leaves a dependent alone with a still-open sibling dependency; skips (doesn't label) an unassigned dependent; idempotent across repeated calls. Ran `cargo test -p agentflare-backend` and `cargo test --bin agentflare -- supervisor:: item_check_merge` — all pass, no regressions in the 37 pre-existing supervisor tests. Clippy clean on both touched crates. **Concerns / judgment calls:** - Went with option (a) (require pre-existing `assignee_agent`, loud log) as recommended — unassigned dependents like #193/#194 won't auto-dispatch from this alone. - Followed the spec literally on "completed" only (not "completed or cancelled") for the all-deps-done check, even though `groom`'s existing `blocked_by_map` treats cancelled dependencies as non-blocking too. This means a dependent stuck behind a *cancelled* dependency won't be auto-unblocked by this cascade — flagging this divergence in case it's not what's wanted. - Didn't add a full `item_check_merge` end-to-end integration test (would require mocking a merged PR via git/worktree state) — coverage is at the `cascade_unblock_dependents` unit level instead, which matches the spec's test list. Agentflare-Agent: claude-code_2-1-245_agent Agentflare-Branch: task/195-auto-dispatch-dependents-when-a-blocking Agentflare-Item: 195-auto-dispatch-dependents-when-a-blocking
…cy-graph-driven dynamic workflows) (#621) * Clean — no warnings from `agentflare-backend`, only pre-existing pedantic warnings in an unrelated crate. Both crates compile, clippy is clean on the changed files, and all tests pass. Implemented the dependency-cascade auto-dispatch mechanism per the spec: 1. **`crates/agentflare-backend/src/item/relations.rs`**: added `dependents_of(conn, item_id)` (reverse of `list_dependencies`) and `all_dependencies_completed(conn, item_id)` (true only when the item has ≥1 dependency and every one is in the `completed` state group; a soft-deleted dependency target counts as unsatisfied, fail-closed). 2. **`src/supervisor.rs`**: added `cascade_unblock_dependents(conn, item_id)` — for each dependent of `item_id` whose dependencies are now all completed, applies `READY_LABEL` via the existing `item::add_label` (idempotent `INSERT OR IGNORE`). Dependents with no `assignee_agent` are skipped with a loud `eprintln!` rather than silently no-op'd, per option (a) in the spec. 3. **`src/mcp_server/item.rs`**: `item_check_merge` now calls `cascade_unblock_dependents` right after `promoted` becomes true — this is the single hook point that catches both the automatic tick (`promote_merged_item`) and manual/reconciliation calls. **Tests** (8 new, all passing): - Backend: `dependents_of_finds_reverse_edges`, `all_dependencies_completed_requires_every_dependency_done`, `all_dependencies_completed_false_for_item_with_no_dependencies`. - Supervisor: labels a dependent once its only dependency completes; leaves a dependent alone with a still-open sibling dependency; skips (doesn't label) an unassigned dependent; idempotent across repeated calls. Ran `cargo test -p agentflare-backend` and `cargo test --bin agentflare -- supervisor:: item_check_merge` — all pass, no regressions in the 37 pre-existing supervisor tests. Clippy clean on both touched crates. **Concerns / judgment calls:** - Went with option (a) (require pre-existing `assignee_agent`, loud log) as recommended — unassigned dependents like #193/#194 won't auto-dispatch from this alone. - Followed the spec literally on "completed" only (not "completed or cancelled") for the all-deps-done check, even though `groom`'s existing `blocked_by_map` treats cancelled dependencies as non-blocking too. This means a dependent stuck behind a *cancelled* dependency won't be auto-unblocked by this cascade — flagging this divergence in case it's not what's wanted. - Didn't add a full `item_check_merge` end-to-end integration test (would require mocking a merged PR via git/worktree state) — coverage is at the `cascade_unblock_dependents` unit level instead, which matches the spec's test list. Agentflare-Agent: claude-code_2-1-245_agent Agentflare-Branch: task/195-auto-dispatch-dependents-when-a-blocking Agentflare-Item: 195-auto-dispatch-dependents-when-a-blocking * cascade_unblock_dependents now inherits completed item's assignee (bare agent id) for unassigned dependents; added regression test; pre-existing unrelated test failure confirmed via baseline stash and flagged, not touched. Agentflare-Agent: claude-code_2-1-245_agent Agentflare-Branch: task/195-auto-dispatch-dependents-when-a-blocking Agentflare-Item: 195-auto-dispatch-dependents-when-a-blocking * style: cargo fmt cascade_unblock_dependents, allowlist supervisor_tests.rs LOC gate Agentflare-Agent: claude-code Agentflare-Branch: task/195-auto-dispatch-dependents-when-a-blocking Agentflare-Item: 195 Agentflare-Session: 3eedea22-3a44-4342-963d-d211427f1fea --------- Co-authored-by: shiva <shiva@gosysinfo.tech>
Auto-opened on
item donefor 019f5bf6-4288-7962-a34e-0b259842d97d.Summary by CodeRabbit
New Features
Bug Fixes