From b5142661e203cdea2570941db6ef17908eed870f Mon Sep 17 00:00:00 2001 From: Xule Lin <43122877+linxule@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:49:18 +0100 Subject: [PATCH] fix(kap-server): defer global search activation --- .../kap-server/src/search/searchService.ts | 15 +++++--- packages/kap-server/src/start.ts | 12 +++--- .../test/search/searchService.test.ts | 37 +++++++++++++++++-- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/packages/kap-server/src/search/searchService.ts b/packages/kap-server/src/search/searchService.ts index 64f5c91781..654631d8b3 100644 --- a/packages/kap-server/src/search/searchService.ts +++ b/packages/kap-server/src/search/searchService.ts @@ -26,9 +26,11 @@ * * Registration: this module is side-effect-imported by `start.ts` BEFORE * `bootstrap()` runs, so the module-level `registerScopedService` below lands - * in the DI registry in time and the service is instantiated (App scope, - * OnScopeCreated) with the rest — which also exposes it on the `/api/v1/debug` - * reflection surface as `globalSearch` with zero extra code. + * in the DI registry in time. The service is App-scoped but OnDemand: the + * server explicitly resolves it after bootstrap, while other App scopes do + * not start the cross-session indexer merely because this module was loaded. + * Registration still exposes it on the `/api/v1/debug` reflection surface as + * `globalSearch` with zero extra code. */ import { createHash } from 'node:crypto'; @@ -510,8 +512,9 @@ export class GlobalSearchService implements IGlobalSearchService { @IBootstrapService private readonly bootstrap: IBootstrapService, @ILogService private readonly log: ILogService, ) { - // App-scope OnScopeCreated activation: kick the first full sync off in the - // background so server bootstrap never blocks on indexing. + // App-scope OnDemand activation: once the server explicitly resolves this + // service, kick the first full sync off in the background so server + // bootstrap never blocks on indexing. this.kickBackgroundSync(); } @@ -1442,6 +1445,6 @@ registerScopedService( LifecycleScope.App, IGlobalSearchService, GlobalSearchService, - ScopeActivation.OnScopeCreated, + ScopeActivation.OnDemand, 'globalSearch', ); diff --git a/packages/kap-server/src/start.ts b/packages/kap-server/src/start.ts index 79610a3d95..74f1c6c947 100644 --- a/packages/kap-server/src/start.ts +++ b/packages/kap-server/src/start.ts @@ -80,9 +80,9 @@ import { resolvePasswordHash } from './services/auth/password'; import { createTokenStore } from './services/auth/tokenStore'; // Temporary feature: global message search. Importing this module registers -// `IGlobalSearchService` (App scope) into the DI registry as a side effect, so -// it MUST stay above any `bootstrap()` call — registration happens at module -// evaluation time. +// `IGlobalSearchService` (App scope, OnDemand) into the DI registry as a side +// effect, so it MUST stay above any `bootstrap()` call — registration happens +// at module evaluation time. The server explicitly resolves it after bootstrap. import { drainGlobalSearchDisposals, IGlobalSearchService } from './search/searchService'; export interface ServerStartOptions { @@ -361,8 +361,10 @@ export async function startServer(opts: ServerStartOptions = {}): Promise { return service; } + it('does not start indexing until an App scope first resolves the service', async () => { + let listCalls = 0; + const index = makeSessionIndex(async () => { + listCalls++; + return { items: [], nextCursor: undefined }; + }); + const seeds: ScopeSeed = [ + ...logSeed(resolveLoggingConfig({ homeDir: home!, env: {} })), + [ISessionIndex, index], + ]; + const { app } = bootstrap({ homeDir: home! }, seeds); + + try { + expect(listCalls).toBe(0); + const first = app.accessor.get(IGlobalSearchService); + expect(listCalls).toBe(1); + expect(app.accessor.get(IGlobalSearchService)).toBe(first); + } finally { + await new Promise((resolve) => setImmediate(resolve)); + app.dispose(); + await drainGlobalSearchDisposals(); + } + }); + it('indexes user and assistant text and finds Chinese and English terms', async () => { const s1 = summary('s1', '搜索重构讨论', T1); await writeWire(home!, 's1', 'main', [