Empty-query enumeration filtered tools by exact integration slug after listing everything. Pass it as the read filter instead, so the read refreshes only that integration. Ranked search stays unscoped: its namespace match is token-prefix.
Unified · Split
| @@ -13,6 +13,7 @@ import { · 12 unmodified lines | ||
| 13 | 13 | authToolFailure, |
| 14 | 14 | isUserActionableError, |
| 15 | 15 | isToolResult, |
| 16 | + IntegrationSlug, | |
| 16 | 17 | ToolResult, |
| 17 | 18 | ToolAddress, |
| 18 | 19 | parseToolAddress, |
| @@ -665,13 +666,17 @@ export const searchTools = Effect.fn("executor.tools.search")(function* ( · 646 unmodified lines | ||
| 665 | 666 | }); |
| 666 | 667 | |
| 667 | 668 | const emptyQuery = normalizeSearchText(query).length === 0; |
| 668 | − const hasNamespace = | |
| 669 | − options?.namespace !== undefined && normalizeSearchText(options.namespace).length > 0; | |
| 669 | + // The exact integration slug an empty-query enumeration reads under; null | |
| 670 | + // when the caller named no usable namespace. | |
| 671 | + const scope = | |
| 672 | + options?.namespace !== undefined && normalizeSearchText(options.namespace).length > 0 | |
| 673 | + ? IntegrationSlug.make(options.namespace.trim()) | |
| 674 | + : null; | |
| 670 | 675 | |
| 671 | 676 | // An empty query with no namespace stays empty: it carries neither a |
| 672 | 677 | // ranking signal nor a scope, and listing the whole workspace "by default" |
| 673 | 678 | // is exactly the arbitrary dump the ranked search refuses to be. |
| 674 | − if (emptyQuery && !hasNamespace) { | |
| 679 | + if (emptyQuery && scope === null) { | |
| 675 | 680 | return { |
| 676 | 681 | items: [], |
| 677 | 682 | total: 0, |
| @@ -680,15 +685,26 @@ export const searchTools = Effect.fn("executor.tools.search")(function* ( · 2 unmodified lines | ||
| 680 | 685 | } satisfies PagedResult<ToolDiscoveryResult>; |
| 681 | 686 | } |
| 682 | 687 | |
| 683 | − const all = yield* executor.tools.list({ includeAnnotations: false }).pipe( | |
| 684 | − Effect.mapError( | |
| 685 | − (cause) => | |
| 686 | − new ExecutionToolError({ | |
| 687 | − message: "Failed to list tools for search", | |
| 688 | − cause, | |
| 689 | − }), | |
| 690 | − ), | |
| 691 | − ); | |
| 688 | + // Enumeration's scope is an exact integration slug (see below), so it is the | |
| 689 | + // read's filter, not a post-read predicate: pushing it down lets the list | |
| 690 | + // narrow its own catalog refresh to that one integration instead of | |
| 691 | + // re-listing every stale connection in the workspace. Ranked search is | |
| 692 | + // deliberately NOT scoped here — `matchesNamespace` is token-prefix, so a | |
| 693 | + // scoped read would drop the prefix-sibling integrations it means to match. | |
| 694 | + const all = yield* executor.tools | |
| 695 | + .list({ | |
| 696 | + includeAnnotations: false, | |
| 697 | + ...(emptyQuery && scope !== null ? { integration: scope } : {}), | |
| 698 | + }) | |
| 699 | + .pipe( | |
| 700 | + Effect.mapError( | |
| 701 | + (cause) => | |
| 702 | + new ExecutionToolError({ | |
| 703 | + message: "Failed to list tools for search", | |
| 704 | + cause, | |
| 705 | + }), | |
| 706 | + ), | |
| 707 | + ); | |
| 692 | 708 | const searchable = all.map(toSearchableTool); |
| 693 | 709 | |
| 694 | 710 | // An empty query WITH a namespace is enumeration, not search: there is no |
| @@ -698,10 +714,11 @@ export const searchTools = Effect.fn("executor.tools.search")(function* ( · 3 unmodified lines | ||
| 698 | 714 | // sweep in prefix-sibling integrations (namespace "google" matching |
| 699 | 715 | // google_gmail and google_sheets), which would silently break the census |
| 700 | 716 | // guarantee: `total` here must reconcile against |
| 701 | − // `executor.integrations.list`'s per-integration toolCount. | |
| 717 | + // `executor.integrations.list`'s per-integration toolCount. That exact match | |
| 718 | + // is now the `integration` filter on the read above, so `searchable` is | |
| 719 | + // already the namespace's catalog and re-filtering it here would be a no-op. | |
| 702 | 720 | const ranked: readonly ToolDiscoveryResult[] = emptyQuery |
| 703 | 721 | ? searchable |
| 704 | − .filter((tool) => tool.integration === options?.namespace?.trim()) | |
| 705 | 722 | .sort((left, right) => left.path.localeCompare(right.path)) |
| 706 | 723 | .map((tool) => ({ |
| 707 | 724 | path: tool.path, |