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 8fa4d47597..b47a9f3d79 100644 --- a/packages/kap-server/src/start.ts +++ b/packages/kap-server/src/start.ts @@ -83,9 +83,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 ServerHostIdentity extends KimiHostIdentity { @@ -379,8 +379,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', [