Skip to content

feat(brainbar): port MCP parity tools#270

Merged
EtanHey merged 4 commits intomainfrom
feat/brainbar-mcp-parity-port
May 2, 2026
Merged

feat(brainbar): port MCP parity tools#270
EtanHey merged 4 commits intomainfrom
feat/brainbar-mcp-parity-port

Conversation

@EtanHey
Copy link
Copy Markdown
Owner

@EtanHey EtanHey commented May 2, 2026

Summary

  • port brain_get_person, brain_supersede, brain_archive, and brain_enrich to the Swift BrainBar MCP surface
  • add the missing source parameter to Swift brain_search and thread it through search filtering
  • align BrainBar MCP/socket tests with the expanded 15-tool surface and add coverage for lifecycle and enrichment behavior

Test plan

  • swift test --package-path brain-bar
  • live MCP verification against the modified BrainBar binary on a temp socket/temp DB: initialize, tools/list, brain_store, brain_search
  • pre-push repo harness passed with isolated DB to avoid colliding with the user's live Desktop benchmark on the canonical BrainLayer DB:
    • pytest unit suite: 1819 passed, 9 skipped, 75 deselected, 1 xfailed
    • pytest MCP tool registration
    • pytest isolated eval and hook routing
    • bun test
    • tests/regression/test_fts5_determinism.sh

Notes

  • #268 is still open/dirty, so this branch is based on current main, not post-#268 main.
  • cr review --plain was attempted pre-commit but blocked by the org hourly cap, not by code findings.
  • @orcClaude admin-merge after CI.

Note

Medium Risk
Adds new MCP tools that mutate chunk state (archive/supersede) and changes default search to exclude archived/superseded rows, plus schema migrations; mistakes could hide data unexpectedly or break search queries.

Overview
Expands the Swift BrainBar MCP surface from 11 to 15 tools by adding brain_get_person, brain_supersede, brain_archive, and brain_enrich, including input validation bounds, new handler implementations, and updated tool annotations.

Search behavior changes: brain_search gains an optional source filter (with all treated as no filter), and all FTS/exact-ID/candidate search paths now exclude chunks that are archived or superseded (archived_at IS NULL and superseded_by IS NULL).

Database + lifecycle additions: chunks migrations add source, enriched_at, archived, archived_at, and superseded_by; new DB APIs support getChunk, archiveChunk, supersedeChunk, person context assembly (entity-type filtered lookup + related memories), and summary backfill enrichment with stats. Tests are updated/extended to cover the new tools, source filtering, and archive/supersede/enrich behavior (including socket tool listing count).

Reviewed by Cursor Bugbot for commit 04c35b7. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add brain_get_person, brain_supersede, brain_archive, and brain_enrich MCP tools to BrainBar

  • Adds four new MCP tools (brain_get_person, brain_supersede, brain_archive, brain_enrich) to MCPRouter.swift, routing through new handlers in the dispatcher.
  • Extends brain_search to accept a source parameter; KG augmentation is suppressed only when source acts as an active filter (not all or empty).
  • Migrates the database schema in BrainDatabase.swift to add source, enriched_at, archived, superseded_by, and archived_at columns to the chunks table.
  • All search paths (FTS, trigram, exact chunk ID, and candidate search) now exclude superseded and archived chunks by default and can be filtered by source.
  • Superseding personal content (detected by content_type or keyword) requires an explicit confirmation step before the operation proceeds.
  • Risk: schema migration adds columns on startup; existing rows get default values (source = 'claude_code', archived = 0), which may affect source-filtered queries on older data.

Macroscope summarized 04c35b7.

Summary by CodeRabbit

  • New Features
    • Added source-based search filtering to narrow search results
    • Added chunk archiving and superseding capabilities for content management
    • Added person context lookup with entity type filtering
    • Added enrichment statistics and expanded enrichment functionality
    • Enhanced metadata tracking for archived, superseded, and enriched content

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

📝 Walkthrough

Walkthrough

This PR adds source-filtered search, chunk lifecycle management (archive/supersede), person-context retrieval, and enrichment statistics to BrainDatabase, exposes these via four new MCP tools (brain_get_person, brain_archive, brain_supersede, brain_enrich), and extends the test suite to verify the new tool set and behaviors.

Changes

Search, Chunk Lifecycle, and Enrichment Feature

Layer / File(s) Summary
Data Shape & Schema
brain-bar/Sources/BrainBar/BrainDatabase.swift
chunks table schema extended with source, enriched_at, archived, superseded_by, and archived_at columns via ensureChunkColumns(). New EnrichmentStatsSummary struct added.
Core Search & Lookup
brain-bar/Sources/BrainBar/BrainDatabase.swift
search() and searchCandidates() now accept optional source filter and forward it to FTS. runFTSSearch() adds c.source to WHERE conditions and SELECT columns when filter is active. exactChunkIDSearchResult() also receives and applies source filter. lookupEntity() now accepts optional entityType parameter. New normalizedSourceFilter() and decodedJSONObject() helpers added.
Chunk Lifecycle & Enrichment Methods
brain-bar/Sources/BrainBar/BrainDatabase.swift
New methods: getChunk() returns chunk with lifecycle fields; archiveChunk() marks chunks as archived; supersedeChunk() links old chunk to replacement; getPersonContext() assembles entity profile, relations, and memories via fetchEntityMemories(); enrichmentStats() and enrichChunks() manage enrichment eligibility and backfilling.
MCP Tool Dispatch & Handlers
brain-bar/Sources/BrainBar/MCPRouter.swift
dispatchTool() routes four new tools. handleBrainSearch() parses source, computes active-filter status (treats empty/"all" as inactive), includes source in hasActiveFilters check, and passes it to db.search(). New handlers: handleBrainGetPerson() fetches person context; handleBrainSupersede() with safety gating; handleBrainArchive() marks chunks archived; handleBrainEnrich() triggers enrichment/stats. ToolError extended with notFound() case.
Tool Registry & Schemas
brain-bar/Sources/BrainBar/MCPRouter.swift
toolDefinitions updated to add source (enum) parameter to brain_search schema and to register four new tool definitions (brain_get_person, brain_supersede, brain_archive, brain_enrich) with their respective input schemas and annotations.
Test Coverage
brain-bar/Tests/BrainBarTests/MCPRouterTests.swift, brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift
Tool count expectations updated from 11 to 15. Tool annotations map expanded to cover all 15 tools. New tests added: testBrainSearchSchemaIncludesSourceParameter, testBrainSearchPassesSourceFilter, testBrainArchiveHidesChunkFromDefaultSearch, testBrainSupersedeHidesOldChunkAndKeepsReplacement, testBrainGetPersonReturnsRelationsAndMemories, testBrainEnrichRealtimeBackfillsEligibleChunksAndStatsReflectIt, testBrainEnrichClampsNegativeLimit, testBrainEnrichEmptyChunkIDsDoesNotBroadenScope. New chunkEnrichedAt() helper for enrichment assertions. Socket and stdio tool list counts updated to 15.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The PR introduces multiple interconnected features: schema modifications, source-based filtering affecting FTS query construction and result mapping, four new MCP tool handlers with distinct business logic (archive, supersede, enrichment, person retrieval), and comprehensive test coverage. The changes span diverse areas (database schema, search logic, tool dispatch, enrichment backfill) requiring separate reasoning for each, though patterns repeat within feature areas. FTS column reordering in particular demands careful index verification.

Possibly related PRs

Poem

🐰 A rabbit hops through search with newfound grace,
Filtering chunks by source, keeping pace,
Archive, supersede, enrich anew—
Four tools budding where none grew!
Knowledge blooms richer with each schema change,
Enrichment stats and person contexts range! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "feat(brainbar): port MCP parity tools" clearly and concisely summarizes the main objective of this PR: porting four MCP tools to the Swift BrainBar implementation.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/brainbar-mcp-parity-port

Tip

💬 Introducing [Slack Agent](https://www.coderabbit.ai/agent): Turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get your free trial and get 200 agent minutes per Slack user (a $50 value).


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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 2, 2026

@coderabbitai review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 2, 2026

@codex review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 2, 2026

@cursor @BugBot review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cursor
Copy link
Copy Markdown

cursor Bot commented May 2, 2026

Now I'll perform a thorough code review focused on finding bugs:

Bug Review Summary

After reviewing the PR changes, I've identified several potential issues:

🔴 CRITICAL BUG: SQL Injection Vulnerability in enrichChunks

Location: BrainDatabase.swift:3207-3208

3206|        if let chunkIDs, !chunkIDs.isEmpty {
3207|            let placeholders = Array(repeating: "?", count: chunkIDs.count).joined(separator: ", ")
3208|            conditions.append("id IN (\(placeholders))")

Issue: The placeholders string is directly interpolated into the SQL query instead of being safely parameterized. While this particular usage seems safe (it's just creating "?" strings), the pattern is risky. However, the actual binding happens correctly later at lines 3229-3234.

Verdict: False alarm - the code is actually safe. The placeholders are just "?" strings, and proper binding occurs later.


⚠️ MEDIUM: Missing Error Handling in enrichChunks

Location: BrainDatabase.swift:3245-3257

3245|            let summary = Self.previewText(summary: nil, content: target.content)
3246|            do {
3247|                try executeUpdate(
3248|                    "UPDATE chunks SET summary = ?, enriched_at = ? WHERE id = ?"
3249|                ) { stmt in
3250|                    bindText(summary, to: stmt, index: 1)
3251|                    bindText(Self.timestamp(), to: stmt, index: 2)
3252|                    bindText(target.id, to: stmt, index: 3)
3253|                }
3254|                enriched += 1
3255|            } catch {
3256|                failed += 1
3257|            }

Issue: The catch block silently swallows errors without logging them. This makes debugging enrichment failures very difficult.

Recommendation: Add logging:

} catch {
    failed += 1
    NSLog("[BrainBar] Enrichment failed for chunk %@: %@", target.id, String(describing: error))
}

⚠️ MEDIUM: Inconsistent NULL Handling in getPersonContext

Location: BrainDatabase.swift:3119-3121

3119|                WHERE ec.entity_id = ?
3120|                  AND c.superseded_by IS NULL
3121|                  AND c.archived_at IS NULL

Issue: The query filters out superseded and archived chunks, which is correct. However, the search() function also checks for these conditions. Make sure this filtering is consistently applied across all memory retrieval paths.

Verdict: This is actually correct and consistent with search behavior.


⚠️ MEDIUM: Potential Integer Overflow in Test

Location: MCPRouterTests.swift:1086-1109

1086|        let iterations = 24
...
1090|        DispatchQueue.concurrentPerform(iterations: iterations) { index in

Issue: No issue with 24 iterations, but the test name suggests "concurrent first writes" - should verify that the file locking actually prevents race conditions.

Verdict: Test looks appropriate for the concurrency scenario.


🟡 LOW: Unused Parameter in archiveChunk

Location: BrainDatabase.swift:3046

3046|    func archiveChunk(id: String, reason _: String? = nil) throws -> Bool {

Issue: The reason parameter is declared but explicitly ignored with _. The function doesn't store the reason anywhere. This might be intentional for API parity with Python, but it could confuse callers who expect the reason to be persisted.

Recommendation: Either:

  1. Store the reason in a database column (preferred)
  2. Remove the parameter if it's not needed
  3. Add a comment explaining why it's ignored

🟡 LOW: Missing Bounds Check

Location: MCPRouter.swift:514

514|        let limit = min(args["limit"] as? Int ?? 25, 5_000)

Issue: No check for negative values. If someone passes limit: -100, it becomes min(-100, 5_000) = -100, which could cause issues in the SQL query.

Recommendation: Add max(1, ...):

let limit = max(1, min(args["limit"] as? Int ?? 25, 5_000))

🟡 LOW: Test Flakiness Risk

Location: SocketIntegrationTests.swift:566

566|        Thread.sleep(forTimeInterval: 0.2)

Issue: Multiple tests use fixed sleep durations which could cause flakiness on slow CI systems. The test at line 528 specifically tests that a stalled client is disconnected, and uses a 200ms sleep to verify the serial queue isn't blocked.

Recommendation: Consider using a more robust synchronization mechanism or at least document why 200ms is sufficient.


Summary

Critical Issues: 0
Medium Issues: 1 (missing error logging)
Low Issues: 3 (unused parameter, missing bounds check, test timing)

The code is generally well-written and safe. The main actionable item is adding error logging to the enrichChunks function. The other issues are minor and could be addressed in follow-up PRs.

Open in Web Open in Cursor 

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 2, 2026

@greptileai review

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Comment thread brain-bar/Sources/BrainBar/MCPRouter.swift Outdated
Comment on lines +493 to +499
private func handleBrainArchive(_ args: [String: Any]) throws -> ToolOutput {
guard let chunkID = args["chunk_id"] as? String else {
throw ToolError.missingParameter("chunk_id")
}
let reason = args["reason"] as? String
guard let db = database else { throw ToolError.noDatabase }
guard try db.archiveChunk(id: chunkID, reason: reason) else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium BrainBar/MCPRouter.swift:493

handleBrainArchive accepts a reason parameter and echoes it back in the JSON response at lines 506–508, but BrainDatabase.archiveChunk() declares the parameter as reason _: String? = nil and never writes it to the database. This causes user-provided archive reasons to be silently discarded while the response misleadingly implies they were stored.

     private func handleBrainArchive(_ args: [String: Any]) throws -> ToolOutput {
         guard let chunkID = args["chunk_id"] as? String else {
             throw ToolError.missingParameter("chunk_id")
         }
         let reason = args["reason"] as? String
         guard let db = database else { throw ToolError.noDatabase }
-        guard try db.archiveChunk(id: chunkID, reason: reason) else {
+        guard try db.archiveChunk(id: chunkID, reason: reason) else {
Also found in 1 other location(s)

brain-bar/Sources/BrainBar/BrainDatabase.swift:3265

The skipped value in the returned dictionary at line 3265 will always be 0 because the loop at lines 3244-3257 increments either enriched or failed for every target—there is no code path that skips a target. The calculation max(0, targets.count - enriched - failed) is therefore always 0, making this statistic meaningless and potentially misleading to callers who expect it to reflect actually skipped chunks.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/Sources/BrainBar/MCPRouter.swift around lines 493-499:

`handleBrainArchive` accepts a `reason` parameter and echoes it back in the JSON response at lines 506–508, but `BrainDatabase.archiveChunk()` declares the parameter as `reason _: String? = nil` and never writes it to the database. This causes user-provided archive reasons to be silently discarded while the response misleadingly implies they were stored.

Evidence trail:
brain-bar/Sources/BrainBar/MCPRouter.swift lines 493-510 (REVIEWED_COMMIT): handleBrainArchive accepts reason, passes it to archiveChunk, and echoes it in response payload.
brain-bar/Sources/BrainBar/BrainDatabase.swift line 3046 (REVIEWED_COMMIT): `func archiveChunk(id: String, reason _: String? = nil)` — underscore discards the parameter; SQL UPDATE on lines 3048-3055 only sets `archived=1` and `archived_at`, no reason column.

Also found in 1 other location(s):
- brain-bar/Sources/BrainBar/BrainDatabase.swift:3265 -- The `skipped` value in the returned dictionary at line 3265 will always be 0 because the loop at lines 3244-3257 increments either `enriched` or `failed` for every target—there is no code path that skips a target. The calculation `max(0, targets.count - enriched - failed)` is therefore always 0, making this statistic meaningless and potentially misleading to callers who expect it to reflect actually skipped chunks.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Investigated, but intentionally not changing this in the Swift parity PR.

Python currently has the same behavior in src/brainlayer/mcp/store_handler.py:_brain_archive(...): it echoes reason in the MCP response without persisting it to the DB. I kept Swift aligned with the existing cross-runtime contract here. Persisting archive reasons would be a broader schema/contract change, not a Swift-only parity port.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Understood — maintaining consistency with the Python implementation makes sense for a parity port. Thanks for the clarification.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0c70dfddc2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


private func handleBrainEnrich(_ args: [String: Any]) throws -> ToolOutput {
let mode = args["mode"] as? String ?? "realtime"
let limit = min(args["limit"] as? Int ?? 25, 5_000)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Clamp brain_enrich limit to positive values

handleBrainEnrich only caps limit at 5000, so callers can pass 0 or a negative value. In SQLite, a negative LIMIT removes the row cap, which means a malformed or adversarial request like {"limit": -1} can cause brain_enrich to update every eligible chunk (summary + enriched_at) instead of a bounded batch. This is a write-safety regression for a destructive tool and should enforce a lower bound (for example max(1, ...)) before calling the DB layer.

Useful? React with 👍 / 👎.

Comment on lines +3199 to +3202
if chunkIDs == nil {
conditions.append("enriched_at IS NULL")
conditions.append("char_count >= 50")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat empty chunk_ids as no targets in enrichment query

When chunk_ids is an empty array, enrichChunks takes the chunkIDs != nil path, so it skips the default enriched_at IS NULL/char_count >= 50 guards and also skips the id IN (...) clause (because the array is empty). That broadens the query to unrelated chunks and enriches arbitrary rows, which violates expected tool contract for explicit IDs and can silently mutate memory state.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed on 92df6d94.

Swift brain_enrich now treats chunk_ids: [] as an explicit empty target set and returns attempted=0 instead of broadening to unrelated eligible chunks. I also added a regression test for that path in MCPRouterTests.

Comment thread brain-bar/Sources/BrainBar/BrainDatabase.swift
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 2, 2026

@cursor @BugBot re-review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 2, 2026

@codex review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 2, 2026

Addressing the Cursor review on the latest push (7a1eb90c):

  • fixed: brain_enrich.limit now clamps to 1...5000, so negative limits can no longer flow into the SQL path
  • fixed: enrichChunks now logs per-chunk backfill failures instead of swallowing them silently
  • clarified: archiveChunk(..., reason:) now documents that the reason argument is accepted for Python MCP contract parity but is not yet persisted by BrainBar
  • covered: added a regression test for the negative-limit path and reran swift test --package-path brain-bar (325 tests passed)

Not changing in this PR:

  • the Thread.sleep(0.2) socket test timing, because it is pre-existing and unrelated to the parity surface here
  • archive-reason persistence schema, because that would expand this parity PR into a DB contract change

@cursor
Copy link
Copy Markdown

cursor Bot commented May 2, 2026

Based on the PR comments, the user mentioned pushing commit 7a1eb90c with fixes, but we're reviewing 0c70dfd. Let me analyze the current code state and identify remaining issues:

🔍 Comprehensive Bug Re-Review

🔴 CRITICAL: Empty chunk_ids array bypasses enrichment eligibility filters

Location: BrainDatabase.swift:3198-3209

3198|        var conditions = ["superseded_by IS NULL", "archived_at IS NULL"]
3199|        if chunkIDs == nil {
3200|            conditions.append("enriched_at IS NULL")
3201|            conditions.append("char_count >= 50")
3202|        }
...
3206|        if let chunkIDs, !chunkIDs.isEmpty {
3207|            let placeholders = Array(repeating: "?", count: chunkIDs.count).joined(separator: ", ")
3208|            conditions.append("id IN (\(placeholders))")
3209|        }

Issue: When chunk_ids: [] (empty array, not nil):

  • Line 3199: chunkIDs == nil is FALSE → eligibility filters NOT added
  • Line 3206: !chunkIDs.isEmpty is FALSE → IN clause NOT added
  • Result: Query becomes WHERE superseded_by IS NULL AND archived_at IS NULL LIMIT ?
  • This processes arbitrary chunks, potentially overwriting good summaries

Fix Required:

if chunkIDs == nil || chunkIDs?.isEmpty == true {
    conditions.append("enriched_at IS NULL")
    conditions.append("char_count >= 50")
}

Or return early:

if let chunkIDs, chunkIDs.isEmpty {
    return [
        "mode": mode,
        "attempted": 0,
        "enriched": 0,
        "skipped": 0,
        "failed": 0,
    ]
}

🔴 CRITICAL: brain_enrich limit not clamped to positive values

Location: MCPRouter.swift:514

514|        let limit = min(args["limit"] as? Int ?? 25, 5_000)

Issue: Negative limits pass through unchecked. SQLite interprets negative LIMIT as "no limit", causing brain_enrich to potentially update ALL eligible chunks instead of a bounded batch.

Test case: {"limit": -1} → processes all chunks

Fix Required:

let limit = max(1, min(args["limit"] as? Int ?? 25, 5_000))

Status: User claimed this was fixed in 7a1eb90c, but we're reviewing 0c70dfd which doesn't have the fix.


🔴 CRITICAL: source="all" disables KG entity detection

Location: MCPRouter.swift:239

239|        let hasActiveFilters = project != nil || source != nil || tag != nil || subscriberID != nil || importanceMin != nil

Issue: When user passes source="all" (meaning "search everything"), the condition source != nil evaluates to TRUE, which disables KG entity enhancement. But semantically, "all" means "no restriction" and should allow KG detection.

Evidence: normalizedSourceFilter() at line 1622-1627 correctly converts "all" to nil for the database layer, but the KG gate happens before that normalization.

Fix Required:

let hasActiveFilters = project != nil || (source != nil && source != "all") || tag != nil || subscriberID != nil || importanceMin != nil

Or normalize earlier:

let normalizedSource = source == "all" ? nil : source
let hasActiveFilters = project != nil || normalizedSource != nil || tag != nil || subscriberID != nil || importanceMin != nil

⚠️ MEDIUM: archiveChunk reason parameter silently discarded

Location: BrainDatabase.swift:3046 + MCPRouter.swift:497-508

// BrainDatabase.swift
3046|    func archiveChunk(id: String, reason _: String? = nil) throws -> Bool {

// MCPRouter.swift
497|        let reason = args["reason"] as? String
...
506|        if let reason {
507|            payload["reason"] = reason
508|        }

Issue: The router accepts a reason, passes it to the DB function, and echoes it back in the response—but the DB function discards it with _. This misleads users into thinking their reason was persisted.

Fix Options:

  1. Store it (requires schema change):
    ALTER TABLE chunks ADD COLUMN archive_reason TEXT
  2. Remove from response if not persisting:
    // Don't include reason in the response payload
  3. Document clearly that it's accepted but not persisted

Status: User added documentation in 7a1eb90c but we're reviewing the version without it.


⚠️ MEDIUM: Silent error swallowing in enrichChunks

Location: BrainDatabase.swift:3255-3257

3255|            } catch {
3256|                failed += 1
3257|            }

Issue: Errors are silently counted but not logged, making debugging impossible.

Fix Required:

} catch {
    failed += 1
    NSLog("[BrainBar] Enrichment failed for chunk %@: %@", target.id, String(describing: error))
}

Status: User claimed this was fixed in 7a1eb90c.


🟡 LOW: Misleading "skipped" statistic

Location: BrainDatabase.swift:3265

3265|            "skipped": max(0, targets.count - enriched - failed),

Issue: The loop always increments either enriched or failed, so skipped is always 0. This is misleading to callers who expect it to reflect chunks that were intentionally skipped.

Fix: Either remove the statistic or make it meaningful by actually skipping some chunks (e.g., those already enriched when explicitly requested).


Summary for Commit 0c70dfd

Severity Issue Fixed?
🔴 CRITICAL Empty chunk_ids array bypasses filters ❌ No
🔴 CRITICAL Negative limit not clamped ❌ No
🔴 CRITICAL source="all" disables KG detection ❌ No
⚠️ MEDIUM Archive reason silently discarded ❌ No
⚠️ MEDIUM Silent error swallowing ❌ No
🟡 LOW Misleading "skipped" count ❌ No

Recommendation: The user mentioned fixing some issues in commit 7a1eb90c, but that commit is not present in the current branch. The PR should not be merged until these critical issues are addressed:

  1. Empty array handling in enrichChunks
  2. Negative limit clamping in handleBrainEnrich
  3. KG detection logic for source="all"

The medium/low issues can be addressed in follow-up work if documented properly.

Open in Web Open in Cursor 

Comment thread brain-bar/Sources/BrainBar/BrainDatabase.swift
Comment thread brain-bar/Sources/BrainBar/MCPRouter.swift Outdated
Comment thread brain-bar/Sources/BrainBar/MCPRouter.swift Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a1eb90c9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3201 to +3204
if chunkIDs == nil {
conditions.append("enriched_at IS NULL")
conditions.append("char_count >= 50")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat empty chunk_ids as no-op in enrichment targeting

In enrichChunks, an explicitly empty chunkIDs array skips the default eligibility filters because they are gated on chunkIDs == nil, but it also skips the id IN (...) clause because that is only added when the array is non-empty. That means brain_enrich calls with chunk_ids: [] can unexpectedly target arbitrary non-archived/non-superseded chunks (up to limit) instead of zero chunks, which is a risky write-path expansion for lifecycle tooling.

Useful? React with 👍 / 👎.

// Entity detection → KG fact lookup
var kgSection = ""
let hasActiveFilters = project != nil || tag != nil || subscriberID != nil || importanceMin != nil
let hasActiveFilters = project != nil || source != nil || tag != nil || subscriberID != nil || importanceMin != nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep KG augmentation when source filter is set to all

handleBrainSearch now includes source != nil in hasActiveFilters, so passing source: "all" disables entity/KG augmentation even though normalizedSourceFilter("all") removes the DB filter and search scope is unchanged. This creates a behavior regression where explicit all returns less context than omitting source, despite both representing the same query scope.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 2, 2026

@codex review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 2, 2026

@cursor @BugBot re-review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 2, 2026

Follow-up push on 92df6d94 addressed the remaining correctness findings from the last review round.

Changes:

  • fixed brain_search(source: "all") so the unfiltered case still gets KG augmentation instead of being treated as an active filter
  • fixed brain_enrich(chunk_ids: []) so an explicit empty list returns zero targets instead of broadening to unrelated eligible chunks
  • added Swift regressions for both paths

Verification:

  • swift test --package-path brain-bar327 tests passed
  • push hook with isolated DB passed: 1819 passed, 9 skipped, 75 deselected, 1 xfailed
  • hook follow-ups also passed: 3 MCP registration tests, 32 eval/hook-routing tests, bun test, and test_fts5_determinism.sh

Left intentionally unchanged in this PR:

  • archive-reason persistence, because Python currently mirrors the same non-persisted reason behavior and changing it would expand this parity port into a broader schema/contract update

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 2, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cursor
Copy link
Copy Markdown

cursor Bot commented May 2, 2026

Now let me create a comprehensive summary of the current state:

✅ Re-Review Complete: All Critical Issues Resolved

Summary of Fixes Applied

Based on my review of commits 7a1eb90c and 92df6d94, all previously identified critical and medium-severity bugs have been fixed:


FIXED: Empty chunk_ids array edge case

Commit: 92df6d94
Location: BrainDatabase.swift:3199-3207

if let chunkIDs, chunkIDs.isEmpty {
    return [
        "mode": mode,
        "attempted": 0,
        "enriched": 0,
        "skipped": 0,
        "failed": 0,
    ]
}

Status: ✅ Now correctly returns early with zero counts instead of processing arbitrary chunks.


FIXED: Negative limit bypassing bounds check

Commit: 7a1eb90c
Location: MCPRouter.swift:521

let limit = max(1, min(args["limit"] as? Int ?? 25, 5_000))

Status: ✅ Now clamps to range [1, 5000], preventing SQLite from treating negative values as unlimited.


FIXED: source="all" disables KG entity detection

Commit: 92df6d94
Location: MCPRouter.swift:226-231, 246

let sourceCountsAsFilter: Bool
if let source {
    let trimmed = source.trimmingCharacters(in: .whitespacesAndNewlines)
    sourceCountsAsFilter = !trimmed.isEmpty && trimmed != "all"
} else {
    sourceCountsAsFilter = false
}
let hasActiveFilters = project != nil || sourceCountsAsFilter || tag != nil || subscriberID != nil || importanceMin != nil

Status: ✅ Now treats "all" as equivalent to no filter, preserving KG augmentation.


FIXED: Silent error swallowing in enrichChunks

Commit: 7a1eb90c
Location: BrainDatabase.swift:3266-3272

} catch {
    failed += 1
    NSLog(
        "[BrainBar] Enrichment summary backfill failed for chunk %@: %@",
        target.id,
        String(describing: error)
    )
}

Status: ✅ Now logs enrichment failures for debugging.


DOCUMENTED: archiveChunk reason parameter

Commit: 7a1eb90c
Location: BrainDatabase.swift:3047-3048

// Keep the reason parameter for Python MCP contract parity; BrainBar
// does not persist archive reasons yet.

Status: ✅ Clearly documents that reason is accepted for API parity but not persisted.


🟡 Remaining Low-Priority Issue: Misleading schema description

Location: MCPRouter.swift:790

"source": [..., "description": "Filter by data source (default: claude_code). Use 'all' to search everything."]

Issue: The description says "default: claude_code" but when source is omitted, no filter is applied (equivalent to "all"). This could mislead MCP clients.

Suggested Fix:

"description": "Filter by data source. Omit to search all sources, or use 'all' explicitly. Options: claude_code, whatsapp, youtube."

Impact: Low - This is a documentation inconsistency rather than a behavior bug. The actual behavior is correct; only the description is misleading.


Final Verdict

Safe to Merge with one optional documentation improvement

Category Count
🔴 Critical Issues 0 (all fixed)
⚠️ Medium Issues 0 (all fixed)
🟡 Low Issues 1 (documentation only)

Recommendation:

  • ✅ The PR is functionally correct and safe to merge
  • 📝 Consider updating the source parameter description in a follow-up for clarity
  • ✅ All destructive operations (archive/supersede/enrich) now have proper bounds checking and edge case handling
  • ✅ Error logging is in place for debugging
  • ✅ Test coverage expanded (327 tests passing per user comment)

Excellent work addressing all the critical findings! The code is now production-ready. 🎉

Open in Web Open in Cursor 

Comment on lines +3277 to +3283
return [
"mode": mode,
"attempted": targets.count,
"enriched": enriched,
"skipped": max(0, targets.count - enriched - failed),
"failed": failed,
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Low BrainBar/BrainDatabase.swift:3277

The skipped count in the returned dictionary is always 0 because the loop increments either enriched or failed for every target, so enriched + failed == targets.count always holds. If tracking skipped chunks is intentional, the logic needs to identify and count actually-skipped cases; otherwise consider removing the field to avoid misleading consumers.

        return [
            "mode": mode,
            "attempted": targets.count,
            "enriched": enriched,
-            "skipped": max(0, targets.count - enriched - failed),
            "failed": failed,
        ]
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/Sources/BrainBar/BrainDatabase.swift around lines 3277-3283:

The `skipped` count in the returned dictionary is always 0 because the loop increments either `enriched` or `failed` for every target, so `enriched + failed == targets.count` always holds. If tracking skipped chunks is intentional, the logic needs to identify and count actually-skipped cases; otherwise consider removing the field to avoid misleading consumers.

Evidence trail:
brain-bar/Sources/BrainBar/BrainDatabase.swift lines 3254-3282 at REVIEWED_COMMIT: The for loop over `targets` increments `enriched` in the do block or `failed` in the catch block for every element. The return dictionary at line ~3277 computes `skipped` as `max(0, targets.count - enriched - failed)` which is always 0.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92df6d9484

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

throw ToolError.missingParameter("name")
}
let context = args["context"] as? String
let numMemories = min(args["num_memories"] as? Int ?? 10, 50)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Clamp brain_get_person num_memories to a positive range

handleBrainGetPerson only applies an upper bound, so a caller can pass num_memories <= -1, which is then bound directly into LIMIT ? in fetchEntityMemories; in SQLite, a negative LIMIT removes the cap and returns all rows. This makes the tool unexpectedly unbounded for malformed or adversarial inputs, increasing latency and potentially returning far more personal memory content than requested.

Useful? React with 👍 / 👎.

enriched: try queryInt("SELECT COUNT(*) FROM chunks WHERE enriched_at IS NOT NULL"),
unenrichedEligible: try queryInt("SELECT COUNT(*) FROM chunks WHERE enriched_at IS NULL AND char_count >= 50"),
skippedTooShort: try queryInt("SELECT COUNT(*) FROM chunks WHERE enriched_at IS NULL AND char_count < 50"),
enrichedLast24Hours: try queryInt("SELECT COUNT(*) FROM chunks WHERE enriched_at > datetime('now', '-24 hours')")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Parse enriched_at before applying the 24-hour cutoff

enrichedLast24Hours compares enriched_at text directly to datetime('now', '-24 hours'), but enriched_at is written via ISO8601DateFormatter (YYYY-MM-DDTHH:MM:SSZ). Lexicographic comparison against SQLite's space-separated datetime format overcounts rows (for example, same-date timestamps earlier than the cutoff still compare greater due to 'T' vs ' '). This makes enrichment progress stats inaccurate.

Useful? React with 👍 / 👎.

totalChunks: try queryInt("SELECT COUNT(*) FROM chunks"),
enriched: try queryInt("SELECT COUNT(*) FROM chunks WHERE enriched_at IS NOT NULL"),
unenrichedEligible: try queryInt("SELECT COUNT(*) FROM chunks WHERE enriched_at IS NULL AND char_count >= 50"),
skippedTooShort: try queryInt("SELECT COUNT(*) FROM chunks WHERE enriched_at IS NULL AND char_count < 50"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Enrichment stats overcount eligible chunks by including archived/superseded

Medium Severity

enrichmentStats() counts unenrichedEligible and skippedTooShort without filtering out archived or superseded chunks, while enrichChunks() explicitly adds superseded_by IS NULL and archived_at IS NULL conditions. This causes the stats to report more eligible chunks than will actually be processed during enrichment, making progress reporting inaccurate.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 92df6d9. Configure here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Leaving this unchanged in the parity PR. Swift is currently matching Python here: src/brainlayer/mcp/enrich_handler.py:_enrich_stats() also counts unenriched_eligible / skipped_too_short without lifecycle filters. I agree the metric semantics could be tightened, but that would be a cross-server behavior change rather than a Swift-port fix, so I’d treat it as follow-up work if we want both implementations updated together.

"new_chunk_id": newChunkID,
"instruction": "Re-call with confirm=true to proceed",
] as [String: 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.

Supersede safety gate bypassed with direct confirm parameter

Medium Severity

The personal-content safety gate in handleBrainSupersede only triggers when safety_check is "auto". A caller can completely bypass the personal-content detection by passing safety_check="confirm" and confirm=true in a single initial call, since the requiresSupersedeConfirmation check at line 470 is never evaluated when safetyCheck != "auto". The confirmation gate provides no actual protection when a caller opts into "confirm" mode directly.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 92df6d9. Configure here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Leaving this unchanged for parity with the existing Python MCP contract. src/brainlayer/mcp/store_handler.py:_brain_supersede(...) has the same semantics today: safety_check="confirm" + confirm=True is treated as the explicit operator override, not as a two-step challenge token. If we want to harden that flow, I’d prefer to do it as a deliberate cross-server behavior change rather than quietly diverging Swift from Python in the parity port.

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 2, 2026

@codex review
@cursor @BugBot re-review
@coderabbitai review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 2, 2026

Follow-up push on 04c35b73 addressed the remaining client-facing schema drift in this parity PR.

Changes on this head:

  • fixed brain_search.source schema so it includes mcp
  • fixed the source description so omitted source is described correctly as unfiltered / all-sources search
  • kept the earlier lifecycle/search/enrich parity fixes intact

Verification on this head:

  • full pre-push gate passed after cleaning local temp-space exhaustion in /private/var/folders/.../T
  • pytest unit suite: 1819 passed, 9 skipped, 75 deselected, 1 xfailed
  • pytest MCP tool registration: 3 passed
  • pytest isolated eval/hook routing: 32 passed
  • bun test: passed
  • test_fts5_determinism.sh: passed

Reviewer notes:

  • replied on-thread for the schema issue now fixed on 04c35b73
  • replied on-thread for the two newer Bugbot notes that are currently Python-contract parity and would need a coordinated cross-server change if we want different behavior

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

Tip

For best results, initiate chat on the files or code changes.

@EtanHey: I'll kick off a review of the latest changes on 04c35b73.

[review]

Comment on lines +489 to +491

guard try db.supersedeChunk(oldChunkID: oldChunkID, newChunkID: newChunkID) else {
throw ToolError.notFound("Supersede failed for: \(oldChunkID)")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium BrainBar/MCPRouter.swift:489

handleBrainSupersede does not validate that old_chunk_id and new_chunk_id are different. When both parameters have the same value, the chunk's superseded_by field is set to itself, creating a self-referential record that excludes the chunk from search without providing a valid replacement. Code that follows superseded_by chains could also loop infinitely. Consider rejecting the request with an error when the IDs are equal.

-        guard try db.getChunk(id: newChunkID) != nil else {
+        guard oldChunkID != newChunkID else {
+            throw ToolError.schemaValidation("old_chunk_id and new_chunk_id must be different")
+        }
+        guard try db.getChunk(id: newChunkID) != nil else {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/Sources/BrainBar/MCPRouter.swift around lines 489-491:

`handleBrainSupersede` does not validate that `old_chunk_id` and `new_chunk_id` are different. When both parameters have the same value, the chunk's `superseded_by` field is set to itself, creating a self-referential record that excludes the chunk from search without providing a valid replacement. Code that follows `superseded_by` chains could also loop infinitely. Consider rejecting the request with an error when the IDs are equal.

Evidence trail:
brain-bar/Sources/BrainBar/MCPRouter.swift lines 452-497 (handleBrainSupersede — no equality check between oldChunkID and newChunkID); brain-bar/Sources/BrainBar/BrainDatabase.swift lines 3065-3077 (supersedeChunk — no equality check, directly executes UPDATE); brain-bar/Sources/BrainBar/BrainDatabase.swift lines 576, 831, 1731 (search queries filter on `c.superseded_by IS NULL`, confirming self-referential record would exclude chunk from all searches)

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 04c35b7. Configure here.

"old_chunk_id": oldChunkID,
"new_chunk_id": newChunkID,
] as [String: 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.

Self-supersede permanently hides chunk from search

Medium Severity

handleBrainSupersede never validates that old_chunk_id and new_chunk_id are different. If both are the same, supersedeChunk sets superseded_by to the chunk's own ID, permanently hiding it from all default search paths (which filter on superseded_by IS NULL) with no recovery mechanism through the MCP surface.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 04c35b7. Configure here.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
brain-bar/Sources/BrainBar/MCPRouter.swift (1)

784-798: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Allow arbitrary source values in the MCP schema.

chunks.source is free-form and the backend already writes values outside this list (for example, brain_digest stores "digest"), but the schema only permits claude_code, whatsapp, youtube, mcp, and all. That makes valid source filters fail schema validation before they ever reach the database.

Suggested fix
-                    "source": ["type": "string", "enum": ["claude_code", "whatsapp", "youtube", "mcp", "all"], "description": "Filter by data source. Omit or use 'all' to search everything."],
+                    "source": ["type": "string", "description": "Filter by data source. Omit or use 'all' to search everything."],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/MCPRouter.swift` around lines 784 - 798, The
"source" field in the MCPRouter.limitedInputSchema currently restricts values to
a fixed enum which blocks valid free-form sources (e.g. "digest"); update the
"source" property in the MCPRouter.limitedInputSchema to accept arbitrary
strings instead of the enum — replace the enum constraint with a simple "type":
"string" (or add a permissive pattern) and keep the description, so valid
backend-written chunk.source values pass validation before DB filtering logic in
MCPRouter runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@brain-bar/Sources/BrainBar/BrainDatabase.swift`:
- Around line 3065-3077: supersedeChunk currently does a separate
read-then-write and allows oldChunkID == newChunkID; make it atomic by
performing the validation inside the update (or a transaction) and return false
if the replacement is invalid or not active. Specifically, in supersedeChunk
replace the guard + executeUpdate flow with a single transactional update (or a
single SQL UPDATE) that: requires oldChunkID != newChunkID, requires the old
chunk to still be unsuperseded/active (e.g. superseded_by IS NULL) and requires
the new chunk to be active/not archived/not already superseded (check the same
column or state used by getChunk), and only sets superseded_by when those
conditions hold; if the update affects 0 rows return false, otherwise call
refreshSearchStatisticsBestEffort() and return true. Use the existing
executeUpdate, getChunk semantics, and refreshSearchStatisticsBestEffort
references to locate and modify the code.

In `@brain-bar/Sources/BrainBar/MCPRouter.swift`:
- Around line 220-225: The current computation of limit allows 0 or negative
values; change the calculation of limit so it is clamped to the range 1...100
(e.g. compute an Int from args["num_results"], then apply max(1, min(value,
100))) so BrainDatabase.search() always receives at least 1 row requested;
update the line that sets limit (the variable named limit built from
args["num_results"]) to enforce this min of 1 while preserving the existing 100
upper bound and existing fallback when the arg is missing or not an Int.

---

Outside diff comments:
In `@brain-bar/Sources/BrainBar/MCPRouter.swift`:
- Around line 784-798: The "source" field in the MCPRouter.limitedInputSchema
currently restricts values to a fixed enum which blocks valid free-form sources
(e.g. "digest"); update the "source" property in the
MCPRouter.limitedInputSchema to accept arbitrary strings instead of the enum —
replace the enum constraint with a simple "type": "string" (or add a permissive
pattern) and keep the description, so valid backend-written chunk.source values
pass validation before DB filtering logic in MCPRouter runs.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 225e7490-1b78-4168-bf76-e183bc888f66

📥 Commits

Reviewing files that changed from the base of the PR and between 22af0f1 and 04c35b7.

📒 Files selected for processing (4)
  • brain-bar/Sources/BrainBar/BrainDatabase.swift
  • brain-bar/Sources/BrainBar/MCPRouter.swift
  • brain-bar/Tests/BrainBarTests/MCPRouterTests.swift
  • brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Macroscope - Correctness Check
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-03-18T00:12:08.774Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 87
File: brain-bar/Sources/BrainBar/BrainBarServer.swift:118-129
Timestamp: 2026-03-18T00:12:08.774Z
Learning: In Swift files under brain-bar/Sources/BrainBar, enforce that when a critical dependency like the database is nil due to startup ordering (socket before DB), any tool handler that accesses the database must throw an explicit error (e.g., ToolError.noDatabase) instead of returning a default/empty value. Do not allow silent defaults (e.g., guard let db else { return ... }). Flag patterns that silently return defaults when db is nil, as this masks startup timing issues. This guidance applies broadly to similar Swift files in the BrainBar module, not just this one location.

Applied to files:

  • brain-bar/Sources/BrainBar/MCPRouter.swift
  • brain-bar/Sources/BrainBar/BrainDatabase.swift
📚 Learning: 2026-03-29T18:45:40.988Z
Learnt from: EtanHey
Repo: EtanHey/brainlayer PR: 133
File: brain-bar/Sources/BrainBar/BrainDatabase.swift:0-0
Timestamp: 2026-03-29T18:45:40.988Z
Learning: In the BrainBar module’s Swift database layer (notably BrainDatabase.swift), ensure that the `search()` function’s `unreadOnly=true` path orders results by the delivery frontier cursor so the watermark `maxRowID` stays contiguous. Specifically, when `unreadOnly` is enabled, the query must include `ORDER BY c.rowid ASC` (e.g., via `let orderByClause = unreadOnly ? "c.rowid ASC" : "f.rank"`). Do not replace the unread-only ordering with relevance-based sorting (e.g., `f.rank`) unconditionally or for the unread-only path, as it can introduce gaps in the watermark and incorrectly mark unseen rows as delivered. Flag any future change to the `ORDER BY` clause in this function that makes relevance sorting apply to the unread-only case.

Applied to files:

  • brain-bar/Sources/BrainBar/MCPRouter.swift
  • brain-bar/Sources/BrainBar/BrainDatabase.swift
🪛 SwiftLint (0.63.2)
brain-bar/Sources/BrainBar/BrainDatabase.swift

[Warning] 2707-2707: Prefer empty collection over optional collection

(discouraged_optional_collection)


[Warning] 3019-3019: Prefer empty collection over optional collection

(discouraged_optional_collection)


[Warning] 3079-3079: Prefer empty collection over optional collection

(discouraged_optional_collection)


[Warning] 3196-3196: Prefer empty collection over optional collection

(discouraged_optional_collection)

Comment on lines +3065 to +3077
func supersedeChunk(oldChunkID: String, newChunkID: String) throws -> Bool {
guard try getChunk(id: oldChunkID) != nil, try getChunk(id: newChunkID) != nil else {
return false
}
try executeUpdate(
"UPDATE chunks SET superseded_by = ? WHERE id = ?"
) { stmt in
bindText(newChunkID, to: stmt, index: 1)
bindText(oldChunkID, to: stmt, index: 2)
}
refreshSearchStatisticsBestEffort()
return true
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make supersedeChunk atomic and reject invalid replacements.

This does a read-then-write on a shared database, so newChunkID can become archived/superseded after the guards pass and before the update runs. It also currently accepts oldChunkID == newChunkID, which hides the chunk with no visible replacement. Fold the validation into the update (or a transaction) and return false unless the replacement is distinct and still active.

Suggested direction
 func supersedeChunk(oldChunkID: String, newChunkID: String) throws -> Bool {
-    guard try getChunk(id: oldChunkID) != nil, try getChunk(id: newChunkID) != nil else {
+    guard let db else { throw DBError.notOpen }
+    guard oldChunkID != newChunkID else {
         return false
     }
     try executeUpdate(
-        "UPDATE chunks SET superseded_by = ? WHERE id = ?"
+        """
+        UPDATE chunks
+        SET superseded_by = ?
+        WHERE id = ?
+          AND EXISTS (
+              SELECT 1
+              FROM chunks replacement
+              WHERE replacement.id = ?
+                AND replacement.archived_at IS NULL
+                AND replacement.superseded_by IS NULL
+          )
+        """
     ) { stmt in
         bindText(newChunkID, to: stmt, index: 1)
         bindText(oldChunkID, to: stmt, index: 2)
+        bindText(newChunkID, to: stmt, index: 3)
     }
-    refreshSearchStatisticsBestEffort()
-    return true
+    let updated = sqlite3_changes(db) == 1
+    if updated {
+        refreshSearchStatisticsBestEffort()
+    }
+    return updated
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/BrainDatabase.swift` around lines 3065 - 3077,
supersedeChunk currently does a separate read-then-write and allows oldChunkID
== newChunkID; make it atomic by performing the validation inside the update (or
a transaction) and return false if the replacement is invalid or not active.
Specifically, in supersedeChunk replace the guard + executeUpdate flow with a
single transactional update (or a single SQL UPDATE) that: requires oldChunkID
!= newChunkID, requires the old chunk to still be unsuperseded/active (e.g.
superseded_by IS NULL) and requires the new chunk to be active/not archived/not
already superseded (check the same column or state used by getChunk), and only
sets superseded_by when those conditions hold; if the update affects 0 rows
return false, otherwise call refreshSearchStatisticsBestEffort() and return
true. Use the existing executeUpdate, getChunk semantics, and
refreshSearchStatisticsBestEffort references to locate and modify the code.

Comment on lines 220 to 225
let limit = min(args["num_results"] as? Int ?? 5, 100)
let project = args["project"] as? String
let source = args["source"] as? String
let tag = args["tag"] as? String
let subscriberID = (args["agent_id"] as? String) ?? (args["subscriber_id"] as? String)
let unreadOnly = args["unread_only"] as? Bool ?? 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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clamp num_results to at least 1.

min(..., 100) still accepts 0 and negative values. BrainDatabase.search() normalizes the SQL LIMIT back up to 1 internally, so in unread_only mode the delivery cursor can advance even though the caller receives no rows.

Suggested fix
-        let limit = min(args["num_results"] as? Int ?? 5, 100)
+        let limit = max(1, min(args["num_results"] as? Int ?? 5, 100))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@brain-bar/Sources/BrainBar/MCPRouter.swift` around lines 220 - 225, The
current computation of limit allows 0 or negative values; change the calculation
of limit so it is clamped to the range 1...100 (e.g. compute an Int from
args["num_results"], then apply max(1, min(value, 100))) so
BrainDatabase.search() always receives at least 1 row requested; update the line
that sets limit (the variable named limit built from args["num_results"]) to
enforce this min of 1 while preserving the existing 100 upper bound and existing
fallback when the arg is missing or not an Int.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04c35b73d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +490 to +492
guard try db.supersedeChunk(oldChunkID: oldChunkID, newChunkID: newChunkID) else {
throw ToolError.notFound("Supersede failed for: \(oldChunkID)")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject self-supersede requests

brain_supersede currently allows old_chunk_id == new_chunk_id; in that case supersedeChunk writes superseded_by to the same chunk ID, which immediately removes that chunk from default search and effectively self-hides data after a simple caller typo. Add an explicit equality check before calling db.supersedeChunk and return a validation error for identical IDs.

Useful? React with 👍 / 👎.

throw ToolError.missingParameter("name")
}
let context = args["context"] as? String
let numMemories = min(args["num_memories"] as? Int ?? 10, 50)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clamp num_memories to a positive range

num_memories is only capped with min(..., 50) and can still be negative; that negative value is passed to SQL LIMIT in fetchEntityMemories, and in SQLite a negative LIMIT means no upper bound, so a malformed request can return an unbounded number of memories and blow up response size/latency. Clamp with max(1, min(..., 50)) before calling the DB layer.

Useful? React with 👍 / 👎.


var bindIndex: Int32 = 1
if mode == "realtime", chunkIDs == nil {
bindText("-\(sinceHours) hours", to: stmt, index: bindIndex)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate since_hours before composing SQL modifier

since_hours is accepted as any integer and then interpolated as "-\(sinceHours) hours"; negative input produces an invalid modifier like --5 hours, causing datetime('now', ?) to evaluate to NULL and silently filter out all realtime enrichment candidates. Enforce since_hours >= 1 (or clamp) before building the modifier string.

Useful? React with 👍 / 👎.

@EtanHey EtanHey merged commit 71d0290 into main May 2, 2026
7 checks passed
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