Skip to content

Commit de53f98

Browse files
feat(kap-server): purge deleted sessions from the global search index
Deleting a session removed the core session and its session-index entry but left its prompt and assistant text in the search MiniDb until a later writer sync happened to re-scan, so a search right after a permanent deletion could still return the deleted content. The delete route now purges the session's documents immediately through a new IGlobalSearchService.deleteSession, wired through the inline backend and the search worker protocol, with failures logged rather than failing the delete. Also classify the changeset as minor (new user-visible capability) and drop a redundant undefined union from the kimi-inspect callback type. Co-authored-by: qer <wbxl2000@outlook.com>
1 parent a3a10a8 commit de53f98

9 files changed

Lines changed: 54 additions & 2 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
"@moonshot-ai/kimi-code": patch
2+
"@moonshot-ai/kimi-code": minor
33
---
44

55
Expose permanent session deletion via `POST /api/v1/sessions/{session_id}:delete` and broadcast `event.session.deleted` over WebSocket.

apps/kimi-inspect/src/activity/ws.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export interface GlobalEventsWsHandlers {
6767
/** A session was permanently deleted (list-level signal). Same envelope
6868
* shape as `event.session.archived`: the real session id rides in the
6969
* payload. */
70-
onSessionDeleted?: ((sessionId: string) => void) | undefined;
70+
onSessionDeleted?: (sessionId: string) => void;
7171
/** A workspace was created / updated / deleted (list-level signal). */
7272
onWorkspaceChanged?: (() => void) | undefined;
7373
/** A DI unit of the engine's scope tree changed state (debug feed). */

packages/kap-server/src/routes/sessions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
type SessionSummary,
3333
} from '@moonshot-ai/agent-core-v2';
3434
import { SessionMetaUpdated } from '@moonshot-ai/agent-core-v2/session/sessionMetadata/sessionMetaEvents';
35+
import { IGlobalSearchService } from '../search/searchService';
3536
import { ErrorCode } from '../protocol/error-codes';
3637
import { pageResponseSchema } from '../protocol/pagination';
3738
import { toProtocolMessage } from '../services/messages/messageProjection';
@@ -996,6 +997,7 @@ async function archiveSessionAction(ctx: SessionActionCtx): Promise<void> {
996997
async function deleteSessionAction(ctx: SessionActionCtx): Promise<void> {
997998
const { core, req, reply, id } = ctx;
998999
await core.accessor.get(ISessionManager).delete(id);
1000+
await core.accessor.get(IGlobalSearchService).deleteSession(id);
9991001
requestLog(req)?.info({ session_id: id, action: 'delete' }, 'session action completed');
10001002
reply.send(okEnvelope({ deleted: true }, req.id));
10011003
}

packages/kap-server/src/search/indexCore.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,14 @@ export class SearchIndexCore {
438438
return { ...outcome, lockToken: this.lockToken, lifecycle: this.lifecycleState() };
439439
}
440440

441+
async deleteSession(sessionId: string): Promise<void> {
442+
if (this.disposed) return;
443+
await this.ensureOpen();
444+
const db = this.db;
445+
if (!db || db.readOnly || this.disposed) return;
446+
await this.deleteSessionDocs(db, sessionId);
447+
}
448+
441449
private async runSync(sessions: readonly SyncSessionInput[]): Promise<CoreSyncPassOutcome> {
442450
if (this.disposed) return { noop: true, sessions: 0, documents: 0 };
443451
this.syncReplaced = false;

packages/kap-server/src/search/searchService.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export interface IGlobalSearchService {
101101
readonly _serviceBrand: undefined;
102102
search(query: GlobalSearchQuery): Promise<GlobalSearchPage>;
103103
reindex(): Promise<{ sessions: number; documents: number }>;
104+
deleteSession(sessionId: string): Promise<void>;
104105
status(): Promise<{
105106
sessions: number;
106107
documents: number;
@@ -163,6 +164,7 @@ export interface SearchBackend {
163164
refresh(): Promise<unknown>;
164165
reindex(): Promise<unknown>;
165166
status(): Promise<CoreStatus>;
167+
deleteSession(sessionId: string): Promise<void>;
166168
dispose(): Promise<void>;
167169
}
168170

@@ -209,6 +211,10 @@ export class InlineSearchBackend implements SearchBackend {
209211
return this.core.status();
210212
}
211213

214+
deleteSession(sessionId: string): Promise<void> {
215+
return this.core.deleteSession(sessionId);
216+
}
217+
212218
dispose(): Promise<void> {
213219
dropLiveLockToken(this.core.lockTokenView);
214220
return this.core.close();
@@ -262,6 +268,19 @@ export class GlobalSearchService implements IGlobalSearchService {
262268
this.liveSource = source;
263269
}
264270

271+
async deleteSession(sessionId: string): Promise<void> {
272+
if (this.disposed) return;
273+
this.summaries.delete(sessionId);
274+
try {
275+
await this.backend.deleteSession(sessionId);
276+
} catch (error) {
277+
this.log.warn('global search: failed to purge a deleted session from the index', {
278+
sessionId,
279+
error: error instanceof Error ? error.message : String(error),
280+
});
281+
}
282+
}
283+
265284
private get indexDir(): string {
266285
return join(this.bootstrap.homeDir, INDEX_DIR_NAME);
267286
}

packages/kap-server/src/search/worker/entry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ async function dispatch(request: SearchWorkerCall): Promise<unknown> {
9494
}
9595
case 'status':
9696
return core.status();
97+
case 'deleteSession':
98+
await core.deleteSession(
99+
(request.params as { sessionId: string }).sessionId,
100+
);
101+
return null;
97102
case 'close':
98103
return null;
99104
}

packages/kap-server/src/search/worker/host.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@ export class SearchWorkerHost {
165165
return this.call('status');
166166
}
167167

168+
async deleteSession(sessionId: string): Promise<void> {
169+
await this.call('deleteSession', { sessionId });
170+
}
171+
168172
async killWorkerForTest(): Promise<void> {
169173
const worker = this.worker;
170174
if (worker === null) return;

packages/kap-server/src/search/worker/protocol.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type SearchWorkerCall =
2323
| { readonly id: number; readonly v: number; readonly type: 'refresh' }
2424
| { readonly id: number; readonly v: number; readonly type: 'reindex' }
2525
| { readonly id: number; readonly v: number; readonly type: 'status' }
26+
| { readonly id: number; readonly v: number; readonly type: 'deleteSession'; readonly params: { readonly sessionId: string } }
2627
| { readonly id: number; readonly v: number; readonly type: 'close' };
2728

2829
export type SearchWorkerCallType = SearchWorkerCall['type'];
@@ -47,6 +48,7 @@ export interface SearchWorkerResultMap {
4748
readonly refresh: SearchWorkerOpenResult;
4849
readonly reindex: SearchWorkerOpenResult;
4950
readonly status: CoreStatus;
51+
readonly deleteSession: null;
5052
readonly close: null;
5153
}
5254

packages/kap-server/test/search/searchService.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,18 @@ describe('GlobalSearchService', () => {
298298
expect(injected.items).toEqual([]);
299299
});
300300

301+
it('purges a deleted session from the index immediately via deleteSession', async () => {
302+
const s1 = summary('s1', '删除测试', T1);
303+
await writeWire(home!, 's1', 'main', [userLine('即将被删除的苹果', T1)]);
304+
const service = track(makeService(home!, staticIndex([s1])));
305+
await service.reindex();
306+
expect((await service.search({ query: '苹果' })).items.length).toBeGreaterThan(0);
307+
308+
await service.deleteSession('s1');
309+
310+
expect((await service.search({ query: '苹果' })).items).toEqual([]);
311+
});
312+
301313
it('hits session titles as title docs', async () => {
302314
const s1 = summary('s1', '季度总结报告', T1);
303315
await writeWire(home!, 's1', 'main', [userLine('随便说点什么', T1)]);

0 commit comments

Comments
 (0)