From 80a73969132119ffa7347fcd11d1977fce5109fa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 27 May 2026 04:37:51 +0000 Subject: [PATCH 1/2] ToolSearch: load only candidate rows from tool_index via IN(?, ?, ?) ToolSearchService.search ran an embedding query that already returned a small ranked set of tool IDs (topK, with a search-time fan-out of ~3*topK), then called ToolDatabase.shared.loadAllEntries() and filtered the result in Swift. On a workspace with a large tool catalogue (plugin-heavy setups, multiple MCP bundles) every RAG search paid the cost of reading and decoding every row in the tool_index table just to discard most of them, on the agent hot path. Add ToolDatabase.loadEntries(ids:) that issues 'SELECT ... WHERE id IN (?, ?, ?, ...)' with one placeholder per id. SQLite's default SQLITE_MAX_VARIABLE_NUMBER (999) bounds the safe batch size well above any plausible search candidate count. Update ToolSearchService.search to call loadEntries(ids: toolIdSet) instead of loadAllEntries().filter, then apply only the enabled-name filter (which depends on MainActor state and can't be expressed in SQL). Behavior change is purely performance: same rows returned in the same order downstream, just without the O(catalog_size) scan-and-filter. Tests added: * loadEntriesByIdsReturnsOnlyRequested - happy path * loadEntriesByIdsEmptyInputSkipsQuery - guard avoids invalid SQL * loadEntriesByIdsIgnoresUnknownIds - missing ids are dropped Co-authored-by: Michael Meding --- .../Services/Tool/ToolSearchService.swift | 8 +++-- .../OsaurusCore/Storage/ToolDatabase.swift | 31 +++++++++++++++++++ .../Tests/Tool/ToolIndexServiceTests.swift | 29 +++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/Packages/OsaurusCore/Services/Tool/ToolSearchService.swift b/Packages/OsaurusCore/Services/Tool/ToolSearchService.swift index 94bc01920..baa409483 100644 --- a/Packages/OsaurusCore/Services/Tool/ToolSearchService.swift +++ b/Packages/OsaurusCore/Services/Tool/ToolSearchService.swift @@ -134,8 +134,12 @@ public actor ToolSearchService { } let toolIdSet = Set(toolIds) - let entries = try ToolDatabase.shared.loadAllEntries() - .filter { toolIdSet.contains($0.id) && enabledNames.contains($0.name) } + // Pull just the candidate rows from the tool_index table instead + // of paging in every row and filtering in Swift. On a large tool + // catalogue (plugin-heavy workspaces, large MCP bundles) this + // changes per-query cost from O(catalog_size) to O(candidates). + let entries = try ToolDatabase.shared.loadEntries(ids: toolIdSet) + .filter { enabledNames.contains($0.name) } let entryById = Dictionary(entries.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first }) return Array( diff --git a/Packages/OsaurusCore/Storage/ToolDatabase.swift b/Packages/OsaurusCore/Storage/ToolDatabase.swift index 9ceebf592..b94094eb4 100644 --- a/Packages/OsaurusCore/Storage/ToolDatabase.swift +++ b/Packages/OsaurusCore/Storage/ToolDatabase.swift @@ -335,6 +335,37 @@ public final class ToolDatabase: @unchecked Sendable { return entries } + /// Load only the entries whose `id` is in `ids`. Used by `ToolSearchService` + /// which already has a small candidate set returned by VecturaKit; without + /// this method it has to call `loadAllEntries()` and filter in Swift — + /// every RAG-style tool query then pays the cost of reading and decoding + /// every row in `tool_index`, which is O(catalog_size) per query. + /// + /// The generated SQL uses `IN (?, ?, ?, …)` with a placeholder per id; + /// SQLite's default `SQLITE_MAX_VARIABLE_NUMBER` (999) bounds the safe + /// batch size, far above any realistic search-candidate count. + public func loadEntries(ids: Set) throws -> [ToolIndexEntry] { + guard !ids.isEmpty else { return [] } + let idArray = Array(ids) + let placeholders = Array(repeating: "?", count: idArray.count).joined(separator: ",") + let sql = "SELECT \(Self.columns) FROM tool_index WHERE id IN (\(placeholders))" + var entries: [ToolIndexEntry] = [] + try prepareAndExecute( + sql, + bind: { stmt in + for (i, id) in idArray.enumerated() { + Self.bindText(stmt, index: Int32(i + 1), value: id) + } + }, + process: { stmt in + while sqlite3_step(stmt) == SQLITE_ROW { + entries.append(Self.readEntry(from: stmt)) + } + } + ) + return entries + } + public func entryCount() throws -> Int { var count = 0 try prepareAndExecute( diff --git a/Packages/OsaurusCore/Tests/Tool/ToolIndexServiceTests.swift b/Packages/OsaurusCore/Tests/Tool/ToolIndexServiceTests.swift index 2994837a6..cbcfcc203 100644 --- a/Packages/OsaurusCore/Tests/Tool/ToolIndexServiceTests.swift +++ b/Packages/OsaurusCore/Tests/Tool/ToolIndexServiceTests.swift @@ -83,6 +83,35 @@ struct ToolDatabaseTests { #expect(all.count == 3) } + @Test func loadEntriesByIdsReturnsOnlyRequested() throws { + let db = try makeTempDB() + try db.upsertEntry(sampleEntry(id: "a", name: "alpha")) + try db.upsertEntry(sampleEntry(id: "b", name: "beta")) + try db.upsertEntry(sampleEntry(id: "c", name: "gamma")) + + let subset = try db.loadEntries(ids: ["a", "c"]) + #expect(subset.count == 2) + let ids = Set(subset.map { $0.id }) + #expect(ids == ["a", "c"]) + } + + @Test func loadEntriesByIdsEmptyInputSkipsQuery() throws { + let db = try makeTempDB() + try db.upsertEntry(sampleEntry(id: "a", name: "alpha")) + + let result = try db.loadEntries(ids: []) + #expect(result.isEmpty) + } + + @Test func loadEntriesByIdsIgnoresUnknownIds() throws { + let db = try makeTempDB() + try db.upsertEntry(sampleEntry(id: "a", name: "alpha")) + + let result = try db.loadEntries(ids: ["a", "does-not-exist", "also-not-here"]) + #expect(result.count == 1) + #expect(result.first?.id == "a") + } + @Test func entryCountIsAccurate() throws { let db = try makeTempDB() #expect(try db.entryCount() == 0) From dabd1d46ed089e547ad97d18584279ceeed4c870 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 27 May 2026 05:08:34 +0000 Subject: [PATCH 2/2] Fix flake: skip ModelManager launch-time HF fetch under xctest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ModelManager.init kicks off an unstructured Task that calls loadOsaurusAIOrgModels(), which fetches the OsaurusAI organization listing from Hugging Face and feeds the result through applyOsaurusOrgFetch. The unit-test runner repeatedly constructs ModelManager() to drive applyOsaurusOrgFetch directly. The background launch-time fetch races with those test calls — whichever finishes last wins, and the merge result is non-deterministic. That's the root cause of the flaky ModelManagerSuggestedTests failures seen across many of the recent PR CI runs (applyOsaurusOrgFetch_dropsStaleAutoFetched OnReapply, applyOsaurusOrgFetch_addsNewEntriesAfterCurated, etc.). Gate the launch-time fetch on a small isRunningInTestEnvironment helper that checks for any of XCTestConfigurationFilePath, XCTestBundlePath, or XCTestSessionIdentifier in the process environment. Those variables are only present inside an xctest host process; production app launches still get the HF fetch exactly as before. This is a network call, so removing it under tests also has the side benefit of making the test suite work offline / on hermetic CI runners. Co-authored-by: Michael Meding --- .../Managers/Model/ModelManager.swift | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Packages/OsaurusCore/Managers/Model/ModelManager.swift b/Packages/OsaurusCore/Managers/Model/ModelManager.swift index c87d515f6..dc6595695 100644 --- a/Packages/OsaurusCore/Managers/Model/ModelManager.swift +++ b/Packages/OsaurusCore/Managers/Model/ModelManager.swift @@ -188,7 +188,27 @@ final class ModelManager: NSObject, ObservableObject { // Pull the OsaurusAI HF org listing once on launch so newly published // models surface in the Recommended tab without requiring a code push. - Task { [weak self] in await self?.loadOsaurusAIOrgModels() } + // + // The unit-test runner constructs `ModelManager()` repeatedly to drive + // `applyOsaurusOrgFetch` directly. If the launch-time HF fetch races + // with those test calls, whichever finishes last wins and the merge + // result is non-deterministic — that's the regression class behind + // `ModelManagerSuggestedTests/applyOsaurusOrgFetch_*` flaking in CI. + // Skip the background fetch under XCTest; production launches still + // get it because `XCTestConfigurationFilePath` is only set inside + // a test host. + if !Self.isRunningInTestEnvironment { + Task { [weak self] in await self?.loadOsaurusAIOrgModels() } + } + } + + /// True when the current process was launched by xctest. Used to gate + /// network-touching launch-time side effects so tests can drive the + /// affected code paths deterministically. + nonisolated private static var isRunningInTestEnvironment: Bool { + ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil + || ProcessInfo.processInfo.environment["XCTestBundlePath"] != nil + || ProcessInfo.processInfo.environment["XCTestSessionIdentifier"] != nil } // MARK: - Public Methods