From 1d6f7c7d67daeaaf9828f310fb6ef2ff98c4b08c Mon Sep 17 00:00:00 2001 From: mikeohuo Date: Tue, 20 Oct 2026 21:39:39 -0400 Subject: [PATCH 1/4] fix(server): finished runs no longer force a full file-index rescan Run finalization called WorkspaceEntries.refresh, which forces fff's scanFiles() on the server's JS thread. That call waits on fff's picker lock and froze the server for 15-52 s on large repositories. fff's watcher already keeps the search indexes current. Fixes #22586 --- .../src/orchestration-v2/RunFinalizationQueue.test.ts | 2 -- .../src/orchestration-v2/RunFinalizationService.test.ts | 2 -- .../src/orchestration-v2/RunFinalizationService.ts | 9 +++------ 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/apps/server/src/orchestration-v2/RunFinalizationQueue.test.ts b/apps/server/src/orchestration-v2/RunFinalizationQueue.test.ts index c2b9e0aabcd5b..3f21b20917850 100644 --- a/apps/server/src/orchestration-v2/RunFinalizationQueue.test.ts +++ b/apps/server/src/orchestration-v2/RunFinalizationQueue.test.ts @@ -8,7 +8,6 @@ import * as SqlitePersistence from "../persistence/Sqlite.ts"; import * as PullRequestService from "../pullRequest/PullRequestService.ts"; import * as ServerSettings from "../serverSettings.ts"; import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts"; -import * as WorkspaceEntries from "../workspace/WorkspaceEntries.ts"; import * as CheckpointCapture from "./CheckpointCaptureService.ts"; import * as CheckpointRollback from "./CheckpointRollbackService.ts"; import * as EffectOutbox from "./EffectOutbox.ts"; @@ -48,7 +47,6 @@ it.effect.each([ Layer.provide( Layer.mergeAll( projections, - Layer.mock(WorkspaceEntries.WorkspaceEntries)({ refresh: () => Effect.void }), Layer.mock(PullRequestService.PullRequestService)({ refreshAfterTurn: () => Effect.void }), Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({ refreshLocalStatus: () => diff --git a/apps/server/src/orchestration-v2/RunFinalizationService.test.ts b/apps/server/src/orchestration-v2/RunFinalizationService.test.ts index 38a18192531a5..4ac0fcabd7b90 100644 --- a/apps/server/src/orchestration-v2/RunFinalizationService.test.ts +++ b/apps/server/src/orchestration-v2/RunFinalizationService.test.ts @@ -10,7 +10,6 @@ import * as Layer from "effect/Layer"; import * as PullRequestService from "../pullRequest/PullRequestService.ts"; import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts"; -import * as WorkspaceEntries from "../workspace/WorkspaceEntries.ts"; import * as CheckpointCapture from "./CheckpointCaptureService.ts"; import * as ProjectionStore from "./ProjectionStore.ts"; import * as RunFinalization from "./RunFinalizationService.ts"; @@ -98,7 +97,6 @@ it.effect.each( const layer = RunFinalization.layerObserver.pipe( Layer.provide( Layer.mergeAll( - Layer.mock(WorkspaceEntries.WorkspaceEntries)({ refresh: () => Effect.void }), Layer.mock(PullRequestService.PullRequestService)({ refreshAfterTurn: () => Effect.void, }), diff --git a/apps/server/src/orchestration-v2/RunFinalizationService.ts b/apps/server/src/orchestration-v2/RunFinalizationService.ts index 40882033ba527..b6f0e1852eb13 100644 --- a/apps/server/src/orchestration-v2/RunFinalizationService.ts +++ b/apps/server/src/orchestration-v2/RunFinalizationService.ts @@ -6,7 +6,6 @@ import * as Schema from "effect/Schema"; import * as PullRequestService from "../pullRequest/PullRequestService.ts"; import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts"; -import * as WorkspaceEntries from "../workspace/WorkspaceEntries.ts"; import * as CheckpointCapture from "./CheckpointCaptureService.ts"; import * as ProjectionStore from "./ProjectionStore.ts"; @@ -92,7 +91,6 @@ export const layer = Layer.effect(RunFinalizationService, make); export const layerObserver = Layer.effect( RunFinalizationObserver, Effect.gen(function* () { - const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const vcsStatus = yield* VcsStatusBroadcaster.VcsStatusBroadcaster; const projections = yield* ProjectionStore.ProjectionStoreV2; const pullRequests = yield* PullRequestService.PullRequestService; @@ -100,10 +98,9 @@ export const layerObserver = Layer.effect( refreshAfterTurn: pullRequests.refreshAfterTurn, refresh: Effect.fn("RunFinalizationService.refresh")( function* ({ cwd, threadId, runId }) { - const [, local] = yield* Effect.all( - [workspaceEntries.refresh(cwd), vcsStatus.refreshLocalStatus(cwd)], - { concurrency: "unbounded" }, - ); + // The search indexes already watch file changes. A forced `scanFiles()` here waits on + // fff's picker lock on the event loop, freezing the server while the watcher holds it. + const local = yield* vcsStatus.refreshLocalStatus(cwd); if (local.refName === null || local.isDefaultRef) return; const thread = yield* projections.getThreadShell(threadId); if (!thread || thread.branch !== local.refName) return; From 6ef99ea8ef709a94abda70638b0029c7c9fee073 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 12:51:53 +0000 Subject: [PATCH 2/4] fix(server): recover missed file events without rescanning every turn --- .../workspace/WorkspaceSearchIndex.test.ts | 50 +++++++++++++++++++ .../src/workspace/WorkspaceSearchIndex.ts | 17 +++++++ 2 files changed, 67 insertions(+) diff --git a/apps/server/src/workspace/WorkspaceSearchIndex.test.ts b/apps/server/src/workspace/WorkspaceSearchIndex.test.ts index 2c41e4ed0b3d0..bab60b7567324 100644 --- a/apps/server/src/workspace/WorkspaceSearchIndex.test.ts +++ b/apps/server/src/workspace/WorkspaceSearchIndex.test.ts @@ -7,6 +7,7 @@ import { } from "@ff-labs/fff-node"; import { afterEach, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; +import * as TestClock from "effect/testing/TestClock"; import { vi } from "vite-plus/test"; import * as WorkspaceSearchIndex from "./WorkspaceSearchIndex.ts"; @@ -28,6 +29,55 @@ function fileItem(relativePath: string): FileItem { }; } +it.effect("recovers missed watcher changes on reads without rescanning every read", () => + Effect.scoped( + Effect.gen(function* () { + let indexed = "old.ts"; + let onDisk = indexed; + const scanFiles = vi.fn(() => { + indexed = onDisk; + return { ok: true as const, value: undefined }; + }); + const finder = { + destroy: vi.fn(), + waitForIndexReady: vi.fn(async () => ({ ok: true as const, value: true })), + scanFiles, + fileSearch: () => ({ + ok: true as const, + value: { items: [fileItem(indexed)], scores: [], totalMatched: 1, totalFiles: 1 }, + }), + } as unknown as FileFinder; + vi.spyOn(FileFinder, "create").mockReturnValueOnce({ ok: true, value: finder }); + const index = yield* WorkspaceSearchIndex.make("/workspace/project"); + onDisk = "new.ts"; + expect((yield* index.search("", 10, "file")).entries).toEqual([ + { path: "old.ts", kind: "file" }, + ]); + expect(scanFiles).not.toHaveBeenCalled(); + yield* TestClock.adjust("5 minutes"); + const results = yield* Effect.all( + [index.search("", 10, "file"), index.search("", 10, "file")], + { concurrency: "unbounded" }, + ); + for (const result of results) { + expect(result.entries).toEqual([{ path: "new.ts", kind: "file" }]); + } + expect(scanFiles).toHaveBeenCalledTimes(1); + yield* index.search("", 10, "file"); + expect(scanFiles).toHaveBeenCalledTimes(1); + scanFiles.mockImplementationOnce(() => { + throw new Error("scan unavailable"); + }); + yield* TestClock.adjust("5 minutes"); + expect((yield* index.search("", 10, "file")).entries).toEqual([ + { path: "new.ts", kind: "file" }, + ]); + yield* index.search("", 10, "file"); + expect(scanFiles).toHaveBeenCalledTimes(2); + }), + ), +); + it.effect("filters image searches before applying the result limit", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/workspace/WorkspaceSearchIndex.ts b/apps/server/src/workspace/WorkspaceSearchIndex.ts index 8f2b64cf26a34..5955b1315cdc6 100644 --- a/apps/server/src/workspace/WorkspaceSearchIndex.ts +++ b/apps/server/src/workspace/WorkspaceSearchIndex.ts @@ -12,10 +12,12 @@ import type { SearchResult, } from "@ff-labs/fff-node"; import * as Context from "effect/Context"; +import * as Clock from "effect/Clock"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as LayerMap from "effect/LayerMap"; import * as Schema from "effect/Schema"; +import * as Semaphore from "effect/Semaphore"; import type { ProjectEntry, @@ -36,6 +38,7 @@ const WORKSPACE_INDEX_PAGE_SIZE = WORKSPACE_INDEX_MAX_ENTRIES + 2; const WORKSPACE_INDEX_SCAN_TIMEOUT = "15 seconds"; const WORKSPACE_INDEX_SCAN_TIMEOUT_MS = 15_000; const WORKSPACE_INDEX_IDLE_TTL = "15 minutes"; +const WORKSPACE_INDEX_RECOVERY_INTERVAL_MS = 5 * 60_000; const CONTENT_SEARCH_TIME_BUDGET_MS = 250; const CONTENT_SEARCH_MAX_MATCHES_PER_FILE = 100; @@ -372,6 +375,8 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( ); const hasIncompleteInitialScan = () => initialScanTimedOut && finder.isScanning(); + const refreshPermit = yield* Semaphore.make(1); + let refreshedAt = yield* Clock.currentTimeMillis; const runSearch = Effect.fn("WorkspaceSearchIndex.runSearch")(function* ( query: string, @@ -379,6 +384,17 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( operation: "directorySearch" | "fileSearch" | "grep" | "mixedSearch", execute: () => Result, ): Effect.fn.Return { + // Watchers can lose events; repair hot indexes occasionally, rather than after every turn. + yield* Effect.gen(function* () { + const now = yield* Clock.currentTimeMillis; + if (now - refreshedAt < WORKSPACE_INDEX_RECOVERY_INTERVAL_MS) return; + refreshedAt = now; + yield* refresh().pipe( + Effect.catch((cause) => + Effect.logWarning("Periodic workspace index recovery failed", { cwd, cause }), + ), + ); + }).pipe(refreshPermit.withPermits(1)); const result = yield* Effect.try({ try: execute, catch: (cause) => @@ -429,6 +445,7 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( cause, }), ); + refreshedAt = yield* Clock.currentTimeMillis; }); const list: WorkspaceSearchIndex["Service"]["list"] = Effect.fn("WorkspaceSearchIndex.list")( From 7f970597ec475836315ece16312e03eaeaea85e6 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 12:56:07 +0000 Subject: [PATCH 3/4] fix(server): keep index recovery outside the content search budget --- .../workspace/WorkspaceSearchIndex.test.ts | 6 +++++ .../src/workspace/WorkspaceSearchIndex.ts | 26 +++++++++++-------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/apps/server/src/workspace/WorkspaceSearchIndex.test.ts b/apps/server/src/workspace/WorkspaceSearchIndex.test.ts index bab60b7567324..0e6bc977e867a 100644 --- a/apps/server/src/workspace/WorkspaceSearchIndex.test.ts +++ b/apps/server/src/workspace/WorkspaceSearchIndex.test.ts @@ -157,14 +157,20 @@ it.effect("continues whole-word searches after a filtered grep page", () => value: grepResult("needleSuffix", [[0, 6]], nextCursor), }, ); + using now = vi.spyOn(performance, "now").mockReturnValue(0); const finder = { destroy: vi.fn(), waitForIndexReady: vi.fn(async () => ({ ok: true as const, value: true })), + scanFiles: () => { + now.mockReturnValue(1_000); + return { ok: true as const, value: undefined }; + }, grep, } as unknown as FileFinder; vi.spyOn(FileFinder, "create").mockReturnValueOnce({ ok: true, value: finder }); const searchIndex = yield* WorkspaceSearchIndex.make("/workspace/project", "content"); + yield* TestClock.adjust("5 minutes"); const result = yield* searchIndex.searchContents({ query: "needle", limit: 1, diff --git a/apps/server/src/workspace/WorkspaceSearchIndex.ts b/apps/server/src/workspace/WorkspaceSearchIndex.ts index 5955b1315cdc6..a66fcca63d8ba 100644 --- a/apps/server/src/workspace/WorkspaceSearchIndex.ts +++ b/apps/server/src/workspace/WorkspaceSearchIndex.ts @@ -384,17 +384,6 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( operation: "directorySearch" | "fileSearch" | "grep" | "mixedSearch", execute: () => Result, ): Effect.fn.Return { - // Watchers can lose events; repair hot indexes occasionally, rather than after every turn. - yield* Effect.gen(function* () { - const now = yield* Clock.currentTimeMillis; - if (now - refreshedAt < WORKSPACE_INDEX_RECOVERY_INTERVAL_MS) return; - refreshedAt = now; - yield* refresh().pipe( - Effect.catch((cause) => - Effect.logWarning("Periodic workspace index recovery failed", { cwd, cause }), - ), - ); - }).pipe(refreshPermit.withPermits(1)); const result = yield* Effect.try({ try: execute, catch: (cause) => @@ -448,8 +437,21 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( refreshedAt = yield* Clock.currentTimeMillis; }); + // Watchers can lose events; repair hot indexes occasionally, rather than after every turn. + const recover = Effect.fn("WorkspaceSearchIndex.recover")(function* () { + const now = yield* Clock.currentTimeMillis; + if (now - refreshedAt < WORKSPACE_INDEX_RECOVERY_INTERVAL_MS) return; + refreshedAt = now; + yield* refresh().pipe( + Effect.catch((cause) => + Effect.logWarning("Periodic workspace index recovery failed", { cwd, cause }), + ), + ); + }, refreshPermit.withPermits(1)); + const list: WorkspaceSearchIndex["Service"]["list"] = Effect.fn("WorkspaceSearchIndex.list")( function* () { + yield* recover(); const incompleteBeforeQuery = hasIncompleteInitialScan(); const result = yield* runSearch("", WORKSPACE_INDEX_PAGE_SIZE, "mixedSearch", () => finder.mixedSearch("", { pageSize: WORKSPACE_INDEX_PAGE_SIZE }), @@ -470,6 +472,7 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( const search: WorkspaceSearchIndex["Service"]["search"] = Effect.fn( "WorkspaceSearchIndex.search", )(function* (query, limit, kind, imageOnly) { + yield* recover(); const pageSize = imageOnly ? WORKSPACE_INDEX_PAGE_SIZE : Math.max(1, limit + 1); const incompleteBeforeQuery = hasIncompleteInitialScan(); if (kind === "file" || imageOnly) { @@ -496,6 +499,7 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( const searchContents: WorkspaceSearchIndex["Service"]["searchContents"] = Effect.fn( "WorkspaceSearchIndex.searchContents", )(function* (input) { + yield* recover(); const { searchQuery, regexMode } = buildContentSearchQuery(input); const deadline = performance.now() + CONTENT_SEARCH_TIME_BUDGET_MS; // Grep cursors advance per file; whole-word filtering needs enough raw candidates before advancing. From 3540c6ea25a8c4a35f39f60714c25516f7966c11 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 05:37:37 +0000 Subject: [PATCH 4/4] fix(server): remove read-triggered workspace rescans --- .../workspace/WorkspaceSearchIndex.test.ts | 53 +++++++------------ .../src/workspace/WorkspaceSearchIndex.ts | 21 -------- 2 files changed, 18 insertions(+), 56 deletions(-) diff --git a/apps/server/src/workspace/WorkspaceSearchIndex.test.ts b/apps/server/src/workspace/WorkspaceSearchIndex.test.ts index 0e6bc977e867a..460562022241e 100644 --- a/apps/server/src/workspace/WorkspaceSearchIndex.test.ts +++ b/apps/server/src/workspace/WorkspaceSearchIndex.test.ts @@ -29,51 +29,40 @@ function fileItem(relativePath: string): FileItem { }; } -it.effect("recovers missed watcher changes on reads without rescanning every read", () => +it.effect("reads never rescan after the former recovery interval", () => Effect.scoped( Effect.gen(function* () { - let indexed = "old.ts"; - let onDisk = indexed; - const scanFiles = vi.fn(() => { - indexed = onDisk; - return { ok: true as const, value: undefined }; - }); + const scanFiles = vi.fn(() => ({ ok: true as const, value: undefined })); const finder = { destroy: vi.fn(), waitForIndexReady: vi.fn(async () => ({ ok: true as const, value: true })), scanFiles, + mixedSearch: () => ({ ok: true as const, value: { items: [], totalMatched: 0 } }), fileSearch: () => ({ ok: true as const, - value: { items: [fileItem(indexed)], scores: [], totalMatched: 1, totalFiles: 1 }, + value: { items: [fileItem("watched.ts")], scores: [], totalMatched: 1, totalFiles: 1 }, }), + grep: () => ({ ok: true as const, value: { items: [], nextCursor: null } }), } as unknown as FileFinder; vi.spyOn(FileFinder, "create").mockReturnValueOnce({ ok: true, value: finder }); const index = yield* WorkspaceSearchIndex.make("/workspace/project"); - onDisk = "new.ts"; + yield* TestClock.adjust("10 minutes"); + expect(yield* index.list()).toEqual({ entries: [], truncated: false }); expect((yield* index.search("", 10, "file")).entries).toEqual([ - { path: "old.ts", kind: "file" }, + { path: "watched.ts", kind: "file" }, ]); + expect( + yield* index.searchContents({ + query: "needle", + limit: 1, + caseSensitive: true, + wholeWord: false, + useRegex: false, + }), + ).toEqual({ matches: [], truncated: false }); expect(scanFiles).not.toHaveBeenCalled(); - yield* TestClock.adjust("5 minutes"); - const results = yield* Effect.all( - [index.search("", 10, "file"), index.search("", 10, "file")], - { concurrency: "unbounded" }, - ); - for (const result of results) { - expect(result.entries).toEqual([{ path: "new.ts", kind: "file" }]); - } - expect(scanFiles).toHaveBeenCalledTimes(1); - yield* index.search("", 10, "file"); + yield* index.refresh(); expect(scanFiles).toHaveBeenCalledTimes(1); - scanFiles.mockImplementationOnce(() => { - throw new Error("scan unavailable"); - }); - yield* TestClock.adjust("5 minutes"); - expect((yield* index.search("", 10, "file")).entries).toEqual([ - { path: "new.ts", kind: "file" }, - ]); - yield* index.search("", 10, "file"); - expect(scanFiles).toHaveBeenCalledTimes(2); }), ), ); @@ -157,20 +146,14 @@ it.effect("continues whole-word searches after a filtered grep page", () => value: grepResult("needleSuffix", [[0, 6]], nextCursor), }, ); - using now = vi.spyOn(performance, "now").mockReturnValue(0); const finder = { destroy: vi.fn(), waitForIndexReady: vi.fn(async () => ({ ok: true as const, value: true })), - scanFiles: () => { - now.mockReturnValue(1_000); - return { ok: true as const, value: undefined }; - }, grep, } as unknown as FileFinder; vi.spyOn(FileFinder, "create").mockReturnValueOnce({ ok: true, value: finder }); const searchIndex = yield* WorkspaceSearchIndex.make("/workspace/project", "content"); - yield* TestClock.adjust("5 minutes"); const result = yield* searchIndex.searchContents({ query: "needle", limit: 1, diff --git a/apps/server/src/workspace/WorkspaceSearchIndex.ts b/apps/server/src/workspace/WorkspaceSearchIndex.ts index a66fcca63d8ba..8f2b64cf26a34 100644 --- a/apps/server/src/workspace/WorkspaceSearchIndex.ts +++ b/apps/server/src/workspace/WorkspaceSearchIndex.ts @@ -12,12 +12,10 @@ import type { SearchResult, } from "@ff-labs/fff-node"; import * as Context from "effect/Context"; -import * as Clock from "effect/Clock"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as LayerMap from "effect/LayerMap"; import * as Schema from "effect/Schema"; -import * as Semaphore from "effect/Semaphore"; import type { ProjectEntry, @@ -38,7 +36,6 @@ const WORKSPACE_INDEX_PAGE_SIZE = WORKSPACE_INDEX_MAX_ENTRIES + 2; const WORKSPACE_INDEX_SCAN_TIMEOUT = "15 seconds"; const WORKSPACE_INDEX_SCAN_TIMEOUT_MS = 15_000; const WORKSPACE_INDEX_IDLE_TTL = "15 minutes"; -const WORKSPACE_INDEX_RECOVERY_INTERVAL_MS = 5 * 60_000; const CONTENT_SEARCH_TIME_BUDGET_MS = 250; const CONTENT_SEARCH_MAX_MATCHES_PER_FILE = 100; @@ -375,8 +372,6 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( ); const hasIncompleteInitialScan = () => initialScanTimedOut && finder.isScanning(); - const refreshPermit = yield* Semaphore.make(1); - let refreshedAt = yield* Clock.currentTimeMillis; const runSearch = Effect.fn("WorkspaceSearchIndex.runSearch")(function* ( query: string, @@ -434,24 +429,10 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( cause, }), ); - refreshedAt = yield* Clock.currentTimeMillis; }); - // Watchers can lose events; repair hot indexes occasionally, rather than after every turn. - const recover = Effect.fn("WorkspaceSearchIndex.recover")(function* () { - const now = yield* Clock.currentTimeMillis; - if (now - refreshedAt < WORKSPACE_INDEX_RECOVERY_INTERVAL_MS) return; - refreshedAt = now; - yield* refresh().pipe( - Effect.catch((cause) => - Effect.logWarning("Periodic workspace index recovery failed", { cwd, cause }), - ), - ); - }, refreshPermit.withPermits(1)); - const list: WorkspaceSearchIndex["Service"]["list"] = Effect.fn("WorkspaceSearchIndex.list")( function* () { - yield* recover(); const incompleteBeforeQuery = hasIncompleteInitialScan(); const result = yield* runSearch("", WORKSPACE_INDEX_PAGE_SIZE, "mixedSearch", () => finder.mixedSearch("", { pageSize: WORKSPACE_INDEX_PAGE_SIZE }), @@ -472,7 +453,6 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( const search: WorkspaceSearchIndex["Service"]["search"] = Effect.fn( "WorkspaceSearchIndex.search", )(function* (query, limit, kind, imageOnly) { - yield* recover(); const pageSize = imageOnly ? WORKSPACE_INDEX_PAGE_SIZE : Math.max(1, limit + 1); const incompleteBeforeQuery = hasIncompleteInitialScan(); if (kind === "file" || imageOnly) { @@ -499,7 +479,6 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* ( const searchContents: WorkspaceSearchIndex["Service"]["searchContents"] = Effect.fn( "WorkspaceSearchIndex.searchContents", )(function* (input) { - yield* recover(); const { searchQuery, regexMode } = buildContentSearchQuery(input); const deadline = performance.now() + CONTENT_SEARCH_TIME_BUDGET_MS; // Grep cursors advance per file; whole-word filtering needs enough raw candidates before advancing.