-
Notifications
You must be signed in to change notification settings - Fork 7
Fix daily backup via BrainBar VACUUM INTO #294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,18 @@ final class MCPRouter: @unchecked Sendable { | |
| } | ||
| } | ||
|
|
||
| private struct BackupVacuumResult: Encodable { | ||
| let status: String | ||
| let targetPath: String | ||
| let bytes: Int64 | ||
|
|
||
| enum CodingKeys: String, CodingKey { | ||
| case status | ||
| case targetPath = "target_path" | ||
| case bytes | ||
| } | ||
| } | ||
|
|
||
| private var database: BrainDatabase? | ||
| let entityCache = EntityCache() | ||
| private static let defaultStringMaxLength = 256 | ||
|
|
@@ -39,7 +51,8 @@ final class MCPRouter: @unchecked Sendable { | |
| "reason": 1_024, | ||
| "session_id": 128, | ||
| "source": 32, | ||
| "tag": 128 | ||
| "tag": 128, | ||
| "target_path": 4_096 | ||
| ] | ||
| private static let stringArrayLimits: [String: (maxItems: Int, itemMaxLength: Int)] = [ | ||
| "chunk_ids": (maxItems: 500, itemMaxLength: 128), | ||
|
|
@@ -208,6 +221,8 @@ final class MCPRouter: @unchecked Sendable { | |
| return try handleBrainAck(arguments) | ||
| case "brain_maintenance_rebuild_trigram": | ||
| return try handleBrainMaintenanceRebuildTrigram(arguments) | ||
| case "brain_backup_vacuum_into": | ||
| return try handleBrainBackupVacuumInto(arguments) | ||
| default: | ||
| throw ToolError.unknownTool(name) | ||
| } | ||
|
|
@@ -625,6 +640,22 @@ final class MCPRouter: @unchecked Sendable { | |
| ) | ||
| } | ||
|
|
||
| private func handleBrainBackupVacuumInto(_ args: [String: Any]) throws -> ToolOutput { | ||
| guard let targetPath = args["target_path"] as? String else { | ||
| throw ToolError.missingParameter("target_path") | ||
| } | ||
| guard let db = database else { throw ToolError.noDatabase } | ||
| let bytes = try db.vacuumInto(targetPath: targetPath) | ||
| let payload = BackupVacuumResult(status: "ok", targetPath: targetPath, bytes: bytes) | ||
| return ToolOutput( | ||
| text: jsonEncode(payload), | ||
| metadata: [ | ||
| "target_path": targetPath, | ||
| "bytes": bytes, | ||
| ] | ||
| ) | ||
| } | ||
|
|
||
| /// Safe JSON encoding — never use string interpolation with user data. | ||
| private func jsonEncode<T: Encodable>(_ value: T) -> String { | ||
| guard let data = try? JSONEncoder().encode(value), | ||
|
|
@@ -1023,6 +1054,18 @@ final class MCPRouter: @unchecked Sendable { | |
| "required": ["agent_id", "seq"] | ||
| ] as [String: Any]) | ||
| ], | ||
| [ | ||
| "name": "brain_backup_vacuum_into", | ||
| "description": "Create a SQLite backup snapshot using VACUUM INTO on BrainBar's single-writer connection.", | ||
| "annotations": MCPRouter.writeIdempotentAnnotations, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-idempotent backup tool annotated as idempotentLow Severity The Additional Locations (1)Reviewed by Cursor Bugbot for commit 7034ddc. Configure here. |
||
| "inputSchema": MCPRouter.limitedInputSchema([ | ||
| "type": "object", | ||
| "properties": [ | ||
| "target_path": ["type": "string", "description": "Absolute path for the new SQLite backup file"], | ||
| ] as [String: Any], | ||
| "required": ["target_path"] | ||
| ] as [String: Any]) | ||
| ], | ||
| [ | ||
| "name": "brain_maintenance_rebuild_trigram", | ||
| "description": "Operator-triggered maintenance command to rebuild the trigram FTS table in lock-aware batches.", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,9 @@ | |
| <key>Nice</key> | ||
| <integer>15</integer> | ||
|
|
||
| <key>ExitTimeOut</key> | ||
|
Comment on lines
39
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For runs that hang while the process is active, such as during Drive upload after the snapshot is made, this key does not impose a 5-minute runtime limit. I checked the launchd.plist(5) semantics: Useful? React with 👍 / 👎.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. The launchd wrapper now exports BRAINLAYER_BACKUP_TIMEOUT_SECONDS=300 by default, and backup_daily.main() enforces that wall-clock timeout with SIGALRM and exits 124 on timeout. Added a regression test for the timeout path. |
||
| <integer>300</integer> | ||
|
|
||
| <key>ProcessType</key> | ||
| <string>Background</string> | ||
| </dict> | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium
BrainBar/BrainDatabase.swift:479standardizedFileURLdoes not resolve symlinks, so the safety check at line 481 can be bypassed when paths differ by symlink. For example, on macOS/tmpis a symlink to/private/tmp, so/tmp/db.sqliteand/private/tmp/db.sqlitecompare as different but refer to the same file, allowingVACUUM INTOto overwrite the live database. Consider usingresolvingSymlinksInPath()before comparing.🚀 Reply "fix it for me" or copy this AI Prompt for your agent: