Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions packages/kap-server/src/search/searchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -1442,6 +1445,6 @@ registerScopedService(
LifecycleScope.App,
IGlobalSearchService,
GlobalSearchService,
ScopeActivation.OnScopeCreated,
ScopeActivation.OnDemand,
'globalSearch',
);
12 changes: 7 additions & 5 deletions packages/kap-server/src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -379,8 +379,10 @@ export async function startServer(opts: ServerStartOptions): Promise<RunningServ

const connectionRegistry = new ConnectionRegistry();
const transcriptService = new TranscriptService({ homeDir, core, logger });
// The global search service is DI-managed (App scope) while the transcript
// service is constructed here by hand — wire the former to the latter so
// The global search service is DI-managed (App scope, OnDemand) while the
// transcript service is constructed here by hand. This resolve is what ACTIVATES
// the search service — it starts background indexing for the server, so it must
// stay ahead of route registration. Wire the former to the latter so
// container-scoped searches on live sessions scan the in-memory transcript.
core.accessor.get(IGlobalSearchService).setLiveTranscriptSource(transcriptService);
const broadcaster = new SessionEventBroadcaster({
Expand Down
37 changes: 33 additions & 4 deletions packages/kap-server/test/search/searchService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@ import { appendFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import type {
IBootstrapService,
ILogService,
import {
bootstrap,
ISessionIndex,
SessionSummary,
logSeed,
resolveLoggingConfig,
type IBootstrapService,
type ILogService,
type ScopeSeed,
type SessionSummary,
} from '@moonshot-ai/agent-core-v2';
import { TranscriptStore, type TranscriptOperation } from '@moonshot-ai/transcript';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import {
GlobalSearchError,
GlobalSearchService,
IGlobalSearchService,
drainGlobalSearchDisposals,
type LiveTranscriptSource,
} from '../../src/search/searchService';
Expand Down Expand Up @@ -144,6 +149,30 @@ describe('GlobalSearchService', () => {
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', [
Expand Down