From bebeb389f1fecf51a81f4b040facbb1a4fba638b Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Sat, 17 Oct 2026 18:39:11 -0700 Subject: [PATCH] perf(server): do not watch bundled plugins for file changes (#16677) Bundled sandbox-provider plugins are installed from local paths inside the image, so production started a chokidar dev watcher for each of them over files that never change. createPluginDevWatcher now takes unwatchedRoots (compared in resolved and real-path form), and the app passes the bundled plugin catalog root; plugins installed elsewhere are watched as before, and a plugin moving into an unwatched root closes its old watcher and cancels any pending restart. Co-Authored-By: Paperclip --- .../src/__tests__/plugin-dev-watcher.test.ts | 71 ++++++++++++++++++- server/src/app.ts | 2 + server/src/services/plugin-dev-watcher.ts | 20 +++++- 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/server/src/__tests__/plugin-dev-watcher.test.ts b/server/src/__tests__/plugin-dev-watcher.test.ts index c066e8908d1..c2cec20a17b 100644 --- a/server/src/__tests__/plugin-dev-watcher.test.ts +++ b/server/src/__tests__/plugin-dev-watcher.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { EventEmitter } from "node:events"; import os from "node:os"; import path from "node:path"; @@ -134,6 +134,75 @@ describe("createPluginDevWatcher", () => { devWatcher.close(); }); + it("never watches plugins installed from an unwatched root such as the bundled catalog", async () => { + const catalogRoot = makeTempPluginDir(); + const bundledDir = path.join(catalogRoot, "sandbox-providers", "example"); + writePluginPackage(bundledDir); + const localDir = makeTempPluginDir(); + writePluginPackage(localDir); + installMockFsWatcher(); + const lifecycle = createLifecycle(); + + const devWatcher = createPluginDevWatcher( + lifecycle as never, + async (pluginId) => (pluginId === "bundled" ? bundledDir : localDir), + undefined, + { unwatchedRoots: [catalogRoot] }, + ); + + lifecycle.emit("plugin.loaded", { pluginId: "bundled", pluginKey: "bundled" }); + devWatcher.watch("bundled-direct", bundledDir); + lifecycle.emit("plugin.loaded", { pluginId: "local", pluginKey: "local" }); + + await vi.waitFor(() => expect(chokidarMock.watch).toHaveBeenCalledTimes(1)); + const [watchedPaths] = chokidarMock.watch.mock.calls[0] ?? []; + expect(watchedPaths).toContain(path.join(localDir, "dist", "worker.js")); + const everyWatched = chokidarMock.watch.mock.calls.flatMap(([paths]) => paths as string[]); + expect(everyWatched.some((watched) => watched.startsWith(catalogRoot))).toBe(false); + + devWatcher.close(); + }); + + it("matches unwatched roots through symbolic links", async () => { + const realRoot = makeTempPluginDir(); + const linkParent = makeTempPluginDir(); + const linkedRoot = path.join(linkParent, "catalog"); + symlinkSync(realRoot, linkedRoot, "dir"); + const bundledDir = path.join(realRoot, "sandbox-providers", "example"); + writePluginPackage(bundledDir); + installMockFsWatcher(); + + const devWatcher = createPluginDevWatcher(createLifecycle() as never, undefined, undefined, { unwatchedRoots: [linkedRoot] }); + devWatcher.watch("bundled", bundledDir); + devWatcher.watch("bundled-through-link", path.join(linkedRoot, "sandbox-providers", "example")); + + expect(chokidarMock.watch).not.toHaveBeenCalled(); + devWatcher.close(); + }); + + it("closes the old watcher and cancels a pending restart when a plugin moves into an unwatched root", async () => { + vi.useFakeTimers(); + const localDir = makeTempPluginDir(); + writePluginPackage(localDir); + const catalogRoot = makeTempPluginDir(); + const bundledDir = path.join(catalogRoot, "sandbox-providers", "example"); + writePluginPackage(bundledDir); + const { fakeWatcher, handlers } = installMockFsWatcher(); + const lifecycle = createLifecycle(); + + const devWatcher = createPluginDevWatcher(lifecycle as never, undefined, undefined, { unwatchedRoots: [catalogRoot] }); + devWatcher.watch("plugin-1", localDir); + handlers.all?.("change", path.join(localDir, "dist", "worker.js")); + devWatcher.watch("plugin-1", bundledDir); + await vi.advanceTimersByTimeAsync(1_000); + + expect(fakeWatcher.close).toHaveBeenCalled(); + expect(lifecycle.restartWorker).not.toHaveBeenCalled(); + expect(chokidarMock.watch).toHaveBeenCalledTimes(1); + + devWatcher.close(); + }); + it("debounces watched file changes and restarts the plugin worker", async () => { vi.useFakeTimers(); const pluginDir = makeTempPluginDir(); diff --git a/server/src/app.ts b/server/src/app.ts index 14814f64419..e6bfec9b6e5 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -1369,6 +1369,8 @@ export async function createApp( lifecycle, async (pluginId) => (await pluginRegistry.getById(pluginId))?.packagePath ?? null, + undefined, + { unwatchedRoots: [bundledCatalogRoot] }, ); // Auto-provision bundled plugins so their providers are registered for // agent runs. Bundles are excluded from the pnpm diff --git a/server/src/services/plugin-dev-watcher.ts b/server/src/services/plugin-dev-watcher.ts index fbfa4a5425f..d12d629327d 100644 --- a/server/src/services/plugin-dev-watcher.ts +++ b/server/src/services/plugin-dev-watcher.ts @@ -14,7 +14,7 @@ * @see PLUGIN_SPEC.md §27.2 — Local Development Workflow */ import chokidar, { type FSWatcher } from "chokidar"; -import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs"; import path from "node:path"; import { logger } from "../middleware/logger.js"; import type { PluginLifecycleManager } from "./plugin-lifecycle.js"; @@ -160,6 +160,7 @@ export function createPluginDevWatcher( lifecycle: PluginLifecycleManager, resolvePluginPackagePath?: ResolvePluginPackagePath, fsDeps?: PluginDevWatcherFsDeps, + options: { unwatchedRoots?: readonly string[] } = {}, ): PluginDevWatcher { const watchers = new Map(); const debounceTimers = new Map>(); @@ -167,6 +168,19 @@ export function createPluginDevWatcher( // Never reuse an identity after unwatching: an older lookup may still finish. let registrationGeneration = 0; const fileExists = fsDeps?.existsSync ?? existsSync; + const pathForms = (target: string) => { + const resolved = path.resolve(target); + try { + return [resolved, realpathSync(resolved)]; + } catch { + return [resolved]; + } + }; + const unwatchedRoots = (options.unwatchedRoots ?? []).flatMap(pathForms); + const isUnwatched = (absPath: string) => pathForms(absPath).some((candidate) => unwatchedRoots.some((root) => { + const relative = path.relative(root, candidate); + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); + })); log.info( { resolvesInstalledPlugins: Boolean(resolvePluginPackagePath) }, "plugin-dev-watcher: initialized", @@ -179,6 +193,10 @@ export function createPluginDevWatcher( // watcher only while it still targets the installed package path. if (watchers.get(pluginId)?.packagePath === absPath) return; unwatchPlugin(pluginId); + if (isUnwatched(absPath)) { + log.debug({ pluginId, packagePath: absPath }, "plugin-dev-watcher: plugin path is not watched"); + return; + } if (!fileExists(absPath)) { log.warn(