diff --git a/REPORT.md b/REPORT.md new file mode 100644 index 00000000..b78d298b --- /dev/null +++ b/REPORT.md @@ -0,0 +1,37 @@ +## A1 live implementation + +Two implementations exist at base `cc1d21f66534747ee4a50367c290858972629a70`: Python `src/brainlayer/mcp/store_handler.py:88` (dispatched by `src/brainlayer/mcp/__init__.py:1482`) and Swift `brain-bar/Sources/BrainBar/BrainDatabase.swift:6457` (called by `brain-bar/Sources/BrainBar/MCPRouter.swift:993`). The live path is Swift: the running `/Applications/BrainBar.app/Contents/MacOS/BrainBarDaemon` owned `/tmp/brainbar.sock`, which is the socket default at `brain-bar/Sources/BrainBar/BrainBarServer.swift:181`; the stdio bridges connected to that socket. `brain-bar/Sources/BrainBarDaemon/BrainDatabase.swift` is a symlink to the same Swift source, not a third implementation. + +## A2 measured cutoff + +At base `cc1d21f66534747ee4a50367c290858972629a70`, a `tools/call` to `brain_digest` with exactly 701 characters returned success but the temporary database contained exactly 503 characters: the first 500 input characters plus `...`. Measured cutoff: 500. The RED assertion reported `brain_digest stored 503 characters from a 701-character input`; the defect is the prefix construction at `brain-bar/Sources/BrainBar/BrainDatabase.swift:6507` plus the normal success formatting at `brain-bar/Sources/BrainBar/MCPRouter.swift:1001`. + +## A3 stub count + +298 truncated stubs, six fewer than the unverified estimate of about 304. + +```sql +SELECT COUNT(*) AS truncated_stubs FROM chunks WHERE created_at >= '2026-04-01' AND source = 'digest' AND length(content) >= 503 AND substr(content, -3) = '...'; +``` + +Detection rule: rows created on/after 2026-04-01 with `source = 'digest'`, at least 503 characters, and a terminal `...`. This matches the base Swift behavior at `cc1d21f66534747ee4a50367c290858972629a70` (`brain-bar/Sources/BrainBar/BrainDatabase.swift:6507`): 500 content characters plus `...`, optionally preceded by a title. The query was run read-only with `sqlite3 "file:$HOME/.local/share/brainlayer/brainlayer.db?mode=ro"`. + +## A4 recoverable + +recoverable. The chunk rows contain only the stub (`source_file = 'brainbar-store'`, no original-content column), but matching stored prefixes and post-title body prefixes against pre-2026-08-01 local Claude/Codex/Cursor/Gemini transcripts located full tool-call inputs for 198 of 298 rows. The configured Google Drive `claude-jsonl` backup folder contains daily transcript archives through 2026-07-31, covering the source class for the remainder; the remaining 100 were not individually extracted from those archives during this task. + +## A5 fix chosen + +full digest — storage now receives the full input (with the optional title prepended), because the schema already accepts up to 200,000 characters and silent data loss is unnecessary. + +## B false ack + +Reproduction at base `cc1d21f66534747ee4a50367c290858972629a70`: `brain_ack({"agent_id":"cmuxlayer-owned-agent","seq":42})` returned `{"status":"acked"}` and created a BrainBar agent row with both cursors at 42. The cause is `brain-bar/Sources/BrainBar/BrainBarServer.swift:866`, which called an acknowledge path that auto-created unknown agents. The fix requires an existing BrainBar subscription; otherwise the MCP result has `isError: true`, text `No BrainBar subscription for agent: ...`, and no row is created. Dropped/missed messages: undetermined. The production DB has three acknowledged agent rows with zero subscription tags, but cursor state cannot show whether a caller discarded or overlooked an external cmux message; settling that requires the cmux inbox/consumer event history at each ack time. + +## tests + +RED on `cc1d21f66534747ee4a50367c290858972629a70`: `swift test --package-path brain-bar --filter 'MCPRouterTests.testBrainDigestStoresTheFullInputContent|SocketIntegrationTests.testMCPBrainAckRejectsAgentWithoutBrainBarSubscription'` executed 2 tests with 5 assertion failures: digest 503 versus 701, and false ack success plus created cursor row. GREEN after the fix: the same command executed 2 tests with 0 failures. Full suite command `swift test --package-path brain-bar` executed 842 tests with 10 skipped and 1 unrelated failure in `DashboardTests.testHeartbeatMarksStatsSnapshotPendingDuringCoalescedRefresh`; that test passed when rerun alone with `swift test --package-path brain-bar --filter 'DashboardTests.testHeartbeatMarksStatsSnapshotPendingDuringCoalescedRefresh'` (1 test, 0 failures). The changed suites passed in the full run: `MCPRouterTests` 74/74 and `SocketIntegrationTests` 21/21. + +## uncertainties + +The remaining 100 truncated inputs were not individually extracted from the Drive transcript archives. The full-suite dashboard timing failure is reproducible only in the full run so far and is outside Items A and B. Whether any caller missed an external message after a false ack is undetermined because no per-message cmux consumption history was available in the BrainLayer DB. diff --git a/brain-bar/Sources/BrainBar/BrainBarServer.swift b/brain-bar/Sources/BrainBar/BrainBarServer.swift index b6611598..394ee257 100644 --- a/brain-bar/Sources/BrainBar/BrainBarServer.swift +++ b/brain-bar/Sources/BrainBar/BrainBarServer.swift @@ -863,6 +863,9 @@ final class BrainBarServer: @unchecked Sendable { } do { + guard try database.subscription(agentID: agentID) != nil else { + return toolErrorResponse(id: id, message: "No BrainBar subscription for agent: \(agentID)") + } try database.acknowledge(agentID: agentID, seq: seq) return jsonRPCTextResult(id: id, text: #"{"status":"acked"}"#) } catch { diff --git a/brain-bar/Sources/BrainBar/BrainDatabase.swift b/brain-bar/Sources/BrainBar/BrainDatabase.swift index 7c804312..fc89a86e 100644 --- a/brain-bar/Sources/BrainBar/BrainDatabase.swift +++ b/brain-bar/Sources/BrainBar/BrainDatabase.swift @@ -6504,9 +6504,8 @@ final class BrainDatabase: @unchecked Sendable { // Store the digest as a chunk let digestSummary = "Digest: \(entities.count) entities, \(urls.count) URLs, \(codeIds.count) code refs" let titledContent: String = { - let body = String(content.prefix(500)) + (content.count > 500 ? "..." : "") - if let title, !title.isEmpty { return "\(title)\n\n\(body)" } - return body + if let title, !title.isEmpty { return "\(title)\n\n\(content)" } + return content }() do { diff --git a/brain-bar/Tests/BrainBarTests/MCPRouterTests.swift b/brain-bar/Tests/BrainBarTests/MCPRouterTests.swift index 60419d55..7dddf0b7 100644 --- a/brain-bar/Tests/BrainBarTests/MCPRouterTests.swift +++ b/brain-bar/Tests/BrainBarTests/MCPRouterTests.swift @@ -1113,6 +1113,35 @@ No results found. XCTAssertNotNil(entity, "Digest-extracted entity should resolve via brain_entity lookup") } + func testBrainDigestStoresTheFullInputContent() throws { + let tempDB = NSTemporaryDirectory() + "brainbar-digest-full-content-\(UUID().uuidString).db" + defer { try? FileManager.default.removeItem(atPath: tempDB) } + let db = BrainDatabase(path: tempDB) + defer { db.close() } + + let input = "DIGEST-CUTOFF " + String(repeating: "x", count: 677) + " FULL-TAIL" + XCTAssertEqual(input.count, 701) + + let router = MCPRouter(profile: "full") + router.setDatabase(db) + let response = router.handle([ + "jsonrpc": "2.0", + "id": 9, + "method": "tools/call", + "params": [ + "name": "brain_digest", + "arguments": ["content": input] + ] as [String: Any] + ]) + + let result = try XCTUnwrap(response["result"] as? [String: Any]) + XCTAssertNil(result["isError"] as? Bool) + let matches = try db.search(query: "DIGEST-CUTOFF", limit: 1) + let stored = try XCTUnwrap(matches.first?["content"] as? String) + XCTAssertEqual(stored.count, input.count, "brain_digest stored \(stored.count) characters from a \(input.count)-character input") + XCTAssertEqual(stored, input) + } + func testBrainSubscribeToolIsServerHandled() throws { let router = MCPRouter(profile: "full") let request: [String: Any] = [ diff --git a/brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift b/brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift index 2929e00d..c77b82a4 100644 --- a/brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift +++ b/brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift @@ -445,6 +445,34 @@ final class SocketIntegrationTests: XCTestCase { XCTAssertEqual(payload?["agent_id"] as? String, "agent-1") } + func testMCPBrainAckRejectsAgentWithoutBrainBarSubscription() throws { + _ = try sendMCPRequest([ + "jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": ["protocolVersion": "2024-11-05", "capabilities": [:] as [String: Any], + "clientInfo": ["name": "ack-owner-check", "version": "1.0"]] + ]) + + let response = try sendMCPRequest([ + "jsonrpc": "2.0", + "id": 5, + "method": "tools/call", + "params": [ + "name": "brain_ack", + "arguments": [ + "agent_id": "cmuxlayer-owned-agent", + "seq": 42 + ] as [String: Any] + ] + ]) + + let result = try XCTUnwrap(response["result"] as? [String: Any]) + let content = try XCTUnwrap(result["content"] as? [[String: Any]]) + let text = try XCTUnwrap(content.first?["text"] as? String) + XCTAssertEqual(result["isError"] as? Bool, true) + XCTAssertTrue(text.contains("No BrainBar subscription")) + XCTAssertNil(try db.subscription(agentID: "cmuxlayer-owned-agent")) + } + func testMatchingStorePushesChannelNotificationAndRequiresAckToClearUnread() throws { let subscriberFD = try connectClient() defer { close(subscriberFD) }