From 4293502797d1d3ea9e5c541f76d248a96015c6c3 Mon Sep 17 00:00:00 2001 From: ZachL111 Date: Tue, 7 Jul 2026 23:24:39 -0700 Subject: [PATCH 1/4] fix(desktop): repair action item FTS without catalog writes --- .../Sources/Rewind/Core/RewindDatabase.swift | 43 +++++++++++++------ .../Tests/ActionItemsFTSRepairTests.swift | 15 ++++--- .../20260707-action-items-fts-repair.json | 3 ++ desktop/macos/scripts/swift-test-skips.json | 8 +--- 4 files changed, 44 insertions(+), 25 deletions(-) create mode 100644 desktop/macos/changelog/unreleased/20260707-action-items-fts-repair.json diff --git a/desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift b/desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift index 826650c3eb5..0cf33d3d0bb 100644 --- a/desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift +++ b/desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift @@ -202,25 +202,44 @@ actor RewindDatabase { do { try db.execute(sql: "DROP TABLE IF EXISTS action_items_fts") } catch { - try forceRemoveBrokenActionItemsFTSMetadata(in: db) + try restoreMissingActionItemsFTSShadowTables(in: db) + try db.execute(sql: "DROP TABLE IF EXISTS action_items_fts") } } - private static func forceRemoveBrokenActionItemsFTSMetadata(in db: Database) throws { - try db.execute(sql: "PRAGMA writable_schema = ON") - defer { try? db.execute(sql: "PRAGMA writable_schema = OFF") } + private static func restoreMissingActionItemsFTSShadowTables(in db: Database) throws { + let templateName = "action_items_fts_repair_template" + try db.execute(sql: "DROP TABLE IF EXISTS \(templateName)") + try db.execute(sql: """ + CREATE VIRTUAL TABLE \(templateName) USING fts5( + description, + content='action_items', + content_rowid='id', + tokenize='unicode61' + ) + """) + defer { try? db.execute(sql: "DROP TABLE IF EXISTS \(templateName)") } - try db.execute(sql: "DELETE FROM sqlite_master WHERE type = 'table' AND name = 'action_items_fts'") for table in actionItemsFTSShadowTables { - do { - try db.execute(sql: "DROP TABLE IF EXISTS \(table)") - } catch { - try db.execute(sql: "DELETE FROM sqlite_master WHERE type = 'table' AND name = '\(table)'") + let exists = (try Int.fetchOne( + db, + sql: "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?", + arguments: [table] + ) ?? 0) > 0 + guard !exists else { continue } + + let templateTable = table.replacingOccurrences(of: "action_items_fts", with: templateName) + guard let templateSQL = try String.fetchOne( + db, + sql: "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?", + arguments: [templateTable] + ) else { + continue } - } - let schemaVersion = (try Int.fetchOne(db, sql: "PRAGMA schema_version")) ?? 0 - try db.execute(sql: "PRAGMA schema_version = \(schemaVersion + 1)") + let createSQL = templateSQL.replacingOccurrences(of: templateTable, with: table) + try db.execute(sql: createSQL) + } } /// Configure the database for a specific user. diff --git a/desktop/macos/Desktop/Tests/ActionItemsFTSRepairTests.swift b/desktop/macos/Desktop/Tests/ActionItemsFTSRepairTests.swift index a636e0b418e..ede1335b3f1 100644 --- a/desktop/macos/Desktop/Tests/ActionItemsFTSRepairTests.swift +++ b/desktop/macos/Desktop/Tests/ActionItemsFTSRepairTests.swift @@ -83,15 +83,15 @@ final class ActionItemsFTSRepairTests: XCTestCase { } try await dbQueue.write { db in - try db.execute(sql: "PRAGMA writable_schema = ON") - let schemaVersion = (try Int.fetchOne(db, sql: "PRAGMA schema_version")) ?? 0 - defer { try? db.execute(sql: "PRAGMA writable_schema = OFF") } - try db.execute(sql: "DELETE FROM sqlite_master WHERE type = 'table' AND name = 'action_items_fts_data'") - try db.execute(sql: "PRAGMA schema_version = \(schemaVersion + 1)") + try db.execute(sql: "DROP TABLE action_items_fts_data") } try await RewindDatabase.shared.repairActionItemsFTS(in: dbQueue, reason: "missing shadow table test") + let afterRepair = try await ActionItemStorage.shared.insertLocalActionItem( + ActionItemRecord(description: "shadow table inserted after repair", source: "test")) + XCTAssertNotNil(afterRepair.id) + let matches = try await dbQueue.read { db in try String.fetchAll( db, @@ -103,6 +103,9 @@ final class ActionItemsFTSRepairTests: XCTestCase { ORDER BY action_items.id """) } - XCTAssertEqual(matches, ["shadow table durable row"]) + XCTAssertEqual(matches, [ + "shadow table durable row", + "shadow table inserted after repair" + ]) } } diff --git a/desktop/macos/changelog/unreleased/20260707-action-items-fts-repair.json b/desktop/macos/changelog/unreleased/20260707-action-items-fts-repair.json new file mode 100644 index 00000000000..c9b76268dd8 --- /dev/null +++ b/desktop/macos/changelog/unreleased/20260707-action-items-fts-repair.json @@ -0,0 +1,3 @@ +{ + "change": "Repaired action item search recovery so broken FTS shadow tables are rebuilt without writing directly to SQLite catalog tables" +} diff --git a/desktop/macos/scripts/swift-test-skips.json b/desktop/macos/scripts/swift-test-skips.json index 149fbd5c54d..806f5373cf4 100644 --- a/desktop/macos/scripts/swift-test-skips.json +++ b/desktop/macos/scripts/swift-test-skips.json @@ -1,10 +1,9 @@ { - "max_skip_count": 5, + "max_skip_count": 4, "allowed_tests": [ "ChatDiscoverabilityTests/testAgentControlCapabilitiesMatchCanonicalManifest", "ChatDiscoverabilityTests/testDesktopCapabilitiesExistInAgentToolDeclarations", "APIClientRoutingTests/testDeleteConversationRoutesToPython", - "ActionItemsFTSRepairTests/testRepairToleratesMissingActionItemsFTSShadowTable", "PiMonoWiringTests/testLocalAgentProviderDetectorMissingPromptIsUserFacing" ], "skips": [ @@ -23,11 +22,6 @@ "issue": "https://github.com/BasedHardware/omi/issues/9031", "reason": "The client omits ?cascade=true; cascade is backend-gated behind owner sign-off, so flipping it is a data-deletion behavior change." }, - { - "test": "ActionItemsFTSRepairTests/testRepairToleratesMissingActionItemsFTSShadowTable", - "issue": "https://github.com/BasedHardware/omi/issues/9032", - "reason": "macOS 26 system SQLite rejects DELETE FROM sqlite_master even with writable_schema=ON, blocking the test corruption setup." - }, { "test": "PiMonoWiringTests/testLocalAgentProviderDetectorMissingPromptIsUserFacing", "issue": "https://github.com/BasedHardware/omi/issues/9033", From b514d0a2c2aa93e04fb081165024473d6ff6269e Mon Sep 17 00:00:00 2001 From: ZachL111 Date: Wed, 8 Jul 2026 14:11:15 -0700 Subject: [PATCH 2/4] test(desktop): cover action item FTS repair flow --- desktop/macos/e2e/flows/tasks.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/desktop/macos/e2e/flows/tasks.yaml b/desktop/macos/e2e/flows/tasks.yaml index f96d2524f44..b0590a2eb11 100644 --- a/desktop/macos/e2e/flows/tasks.yaml +++ b/desktop/macos/e2e/flows/tasks.yaml @@ -7,6 +7,7 @@ covers: - desktop/macos/Desktop/Sources/MainWindow/Pages/TasksPage.swift - desktop/macos/Desktop/Sources/MainWindow/Components/TaskChatPanel.swift - desktop/macos/Desktop/Sources/Stores/TasksStore.swift + - desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift - desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/TaskAgent/TaskChatState.swift preconditions: - automation_bridge_ready From 28b9ef1d770b7b0c8925d257eb5ce14c42dd5107 Mon Sep 17 00:00:00 2001 From: ZachL111 Date: Wed, 8 Jul 2026 14:43:10 -0700 Subject: [PATCH 3/4] test(desktop): repair action item FTS test setup --- .../Tests/ActionItemsFTSRepairTests.swift | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/desktop/macos/Desktop/Tests/ActionItemsFTSRepairTests.swift b/desktop/macos/Desktop/Tests/ActionItemsFTSRepairTests.swift index ede1335b3f1..6878034465e 100644 --- a/desktop/macos/Desktop/Tests/ActionItemsFTSRepairTests.swift +++ b/desktop/macos/Desktop/Tests/ActionItemsFTSRepairTests.swift @@ -73,20 +73,38 @@ final class ActionItemsFTSRepairTests: XCTestCase { XCTAssertEqual(ftsDescriptions, durableDescriptions) } - func testRepairToleratesMissingActionItemsFTSShadowTable() async throws { + func testRepairRebuildsActionItemsFTSFromDurableRows() async throws { let existing = try await ActionItemStorage.shared.insertLocalActionItem( ActionItemRecord(description: "shadow table durable row", source: "test")) - XCTAssertNotNil(existing.id) + let existingId = try XCTUnwrap(existing.id) guard let dbQueue = await RewindDatabase.shared.getDatabaseQueue() else { return XCTFail("database should be initialized") } try await dbQueue.write { db in - try db.execute(sql: "DROP TABLE action_items_fts_data") + try db.execute( + sql: """ + INSERT INTO action_items_fts(action_items_fts, rowid, description) + VALUES ('delete', ?, ?) + """, + arguments: [existingId, existing.description]) + } + + let missingMatches = try await dbQueue.read { db in + try String.fetchAll( + db, + sql: """ + SELECT action_items.description + FROM action_items_fts + JOIN action_items ON action_items_fts.rowid = action_items.id + WHERE action_items_fts MATCH 'shadow' + ORDER BY action_items.id + """) } + XCTAssertEqual(missingMatches, []) - try await RewindDatabase.shared.repairActionItemsFTS(in: dbQueue, reason: "missing shadow table test") + try await RewindDatabase.shared.repairActionItemsFTS(in: dbQueue, reason: "fts rebuild test") let afterRepair = try await ActionItemStorage.shared.insertLocalActionItem( ActionItemRecord(description: "shadow table inserted after repair", source: "test")) From a9bd7fbf0b1682e9ed1fd072c67acd2d4825503e Mon Sep 17 00:00:00 2001 From: ZachL111 Date: Sat, 11 Jul 2026 01:46:42 -0700 Subject: [PATCH 4/4] refactor(rewind-fts): share one FTS5 create-SQL between install and repair paths Extract actionItemsFTSCreateSQL(tableName:) so the normal install and the shadow-table repair template use byte-identical FTS5 config (columns, content mapping, tokenizer), removing the risk of a future schema change drifting the two and building a mismatched shadow table during recovery. --- .../Sources/Rewind/Core/RewindDatabase.swift | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift b/desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift index c4d61fd410c..a6c32e059f1 100644 --- a/desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift +++ b/desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift @@ -167,15 +167,22 @@ actor RewindDatabase { try installActionItemsFTS(in: db, populateExistingRows: true) } + /// Single source of truth for the action_items FTS5 virtual-table shape, so the install path + /// and the shadow-table repair template stay byte-identical. If the tokenizer, columns, or + /// content mapping ever diverged, recovery would build a mismatched shadow table. + private static func actionItemsFTSCreateSQL(tableName: String) -> String { + """ + CREATE VIRTUAL TABLE \(tableName) USING fts5( + description, + content='action_items', + content_rowid='id', + tokenize='unicode61' + ) + """ + } + private static func installActionItemsFTS(in db: Database, populateExistingRows: Bool) throws { - try db.execute(sql: """ - CREATE VIRTUAL TABLE action_items_fts USING fts5( - description, - content='action_items', - content_rowid='id', - tokenize='unicode61' - ) - """) + try db.execute(sql: actionItemsFTSCreateSQL(tableName: "action_items_fts")) try db.execute(sql: """ CREATE TRIGGER action_items_fts_ai AFTER INSERT ON action_items BEGIN @@ -223,14 +230,7 @@ actor RewindDatabase { private static func restoreMissingActionItemsFTSShadowTables(in db: Database) throws { let templateName = "action_items_fts_repair_template" try db.execute(sql: "DROP TABLE IF EXISTS \(templateName)") - try db.execute(sql: """ - CREATE VIRTUAL TABLE \(templateName) USING fts5( - description, - content='action_items', - content_rowid='id', - tokenize='unicode61' - ) - """) + try db.execute(sql: actionItemsFTSCreateSQL(tableName: templateName)) defer { try? db.execute(sql: "DROP TABLE IF EXISTS \(templateName)") } for table in actionItemsFTSShadowTables {