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
5 changes: 5 additions & 0 deletions .changeset/session-delete-action.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Expose permanent session deletion via `POST /api/v1/sessions/{session_id}:delete` and broadcast `event.session.deleted` over WebSocket.
17 changes: 16 additions & 1 deletion apps/kimi-inspect/src/activity/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,14 +199,29 @@ describe('SessionActivityHub', () => {
expect(hub.store.get('s1')).toBeUndefined();
expect(onListChanged).toHaveBeenCalledTimes(1);

instances[0]!.emitFrame({
type: 'event.session.work_changed',
session_id: 's2',
payload: { type: 'event.session.work_changed', busy: true },
});
expect(hub.store.get('s2')).toBeDefined();

instances[0]!.emitFrame({
type: 'event.session.deleted',
session_id: '__global__',
payload: { type: 'event.session.deleted', sessionId: 's2', workspace_id: 'wd_1' },
});
expect(hub.store.get('s2')).toBeUndefined();
expect(onListChanged).toHaveBeenCalledTimes(2);

for (const type of [
'event.workspace.created',
'event.workspace.updated',
'event.workspace.deleted',
]) {
instances[0]!.emitFrame({ type, session_id: '__global__', payload: {} });
}
expect(onListChanged).toHaveBeenCalledTimes(4);
expect(onListChanged).toHaveBeenCalledTimes(5);
hub.close();
});
});
4 changes: 4 additions & 0 deletions apps/kimi-inspect/src/activity/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export class SessionActivityHub {
this.store.remove(sessionId);
opts.onListChanged();
},
onSessionDeleted: (sessionId) => {
this.store.remove(sessionId);
opts.onListChanged();
},
onWorkspaceChanged: () => opts.onListChanged(),
onReconnected: () => void this.seed(),
},
Expand Down
12 changes: 12 additions & 0 deletions apps/kimi-inspect/src/activity/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export interface GlobalEventsWsHandlers {
* carries the `__global__` watermark; the real session id rides in the
* payload. */
onSessionArchived?: ((sessionId: string) => void) | undefined;
/** A session was permanently deleted (list-level signal). Same envelope
* shape as `event.session.archived`: the real session id rides in the
* payload. */
onSessionDeleted?: (sessionId: string) => void;
/** A workspace was created / updated / deleted (list-level signal). */
onWorkspaceChanged?: (() => void) | undefined;
/** A DI unit of the engine's scope tree changed state (debug feed). */
Expand Down Expand Up @@ -195,6 +199,14 @@ export class GlobalEventsWs {
}
return;
}
case 'event.session.deleted': {
const payload = frame.payload as { sessionId?: unknown } | undefined;
const deletedId = payload?.sessionId;
if (typeof deletedId === 'string' && deletedId !== '') {
this.handlers.onSessionDeleted?.(deletedId);
}
return;
}
case 'event.workspace.created':
case 'event.workspace.updated':
case 'event.workspace.deleted': {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ export interface SessionArchived {
readonly payload: SessionArchivedPayload;
}

export interface SessionDeletedPayload {
readonly sessionId: string;
readonly workspaceId: string;
}

export class SessionDeleted extends Event2<{ readonly payload: SessionDeletedPayload }> {
static override readonly type = 'event.session.deleted';
}
export interface SessionDeleted {
readonly payload: SessionDeletedPayload;
}

export interface SessionCreatedPayload {
readonly agentId: string;
readonly sessionId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ import { IWorkspaceMcpService } from '#/workspace/workspaceMcp/workspaceMcp';
import { PLUGIN_SKILL_SOURCE_ID } from '#/features/skill/catalog/skillSource';

import { agentScopeOf, sessionDirOf, sessionScopeOf } from './internal/addressing';
import { SessionArchived } from './sessionLifecycleEvents';
import { SessionArchived, SessionDeleted } from './sessionLifecycleEvents';
import {
assertForkTurnIndex,
sliceMainRecordsAtTurn,
Expand Down Expand Up @@ -449,6 +449,11 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
await this.index.remove(sessionId);
this.appendLogStore.append('', 'session_index.jsonl', { sessionId, deleted: true });
await this.appendLogStore.flush();
this.event.publish(
new SessionDeleted({
payload: { sessionId, workspaceId: this.workspaceContext.workspaceId },
}),
);
}

private async announceWillClose(event: SessionWillCloseEvent): Promise<void> {
Expand Down
33 changes: 25 additions & 8 deletions packages/kap-server/src/openapi/transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ import {
questionResolveRequestSchema,
questionResolveResultSchema,
} from '../protocol/rest-question';
import { archiveSessionResponseSchema } from '../protocol/rest-session';
import {
archiveSessionResponseSchema,
deleteSessionResponseSchema,
} from '../protocol/rest-session';

const binarySchema = {
type: 'string',
Expand Down Expand Up @@ -183,18 +186,32 @@ function patchSessionAction(paths: Record<string, unknown>): void {
const operation = asRecord(pathItem?.['post']);
if (pathItem === undefined || operation === undefined) return;

projectSessionAction(paths, pathItem, 'archive', 'runSessionArchiveAction', {
description: 'Session archive response',
content: jsonContent(openApiDocumentEnvelopeJsonSchema(archiveSessionResponseSchema)),
});
projectSessionAction(paths, pathItem, 'delete', 'runSessionDeleteAction', {
description: 'Session delete response',
content: jsonContent(openApiDocumentEnvelopeJsonSchema(deleteSessionResponseSchema)),
});
delete paths[internalPath];
}

function projectSessionAction(
paths: Record<string, unknown>,
pathItem: Record<string, unknown>,
action: string,
operationId: string,
okResponse: Record<string, unknown>,
): void {
const cloned = cloneRecord(pathItem);
replacePathParamName(cloned, 'tail', 'session_id');
const clonedOperation = asRecord(cloned['post']);
if (clonedOperation !== undefined) {
clonedOperation['operationId'] = 'runSessionArchiveAction';
setResponse(clonedOperation, '200', {
description: 'Session archive response',
content: jsonContent(openApiDocumentEnvelopeJsonSchema(archiveSessionResponseSchema)),
});
clonedOperation['operationId'] = operationId;
setResponse(clonedOperation, '200', okResponse);
}
paths['/api/v1/sessions/{session_id}:archive'] = cloned;
delete paths[internalPath];
paths[`/api/v1/sessions/{session_id}:${action}`] = cloned;
}

function patchFsAction(paths: Record<string, unknown>): void {
Expand Down
6 changes: 6 additions & 0 deletions packages/kap-server/src/protocol/events-zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,11 @@ export const sessionArchivedEventSchema = z.object({
workspace_id: z.string().min(1),
});

export const sessionDeletedEventSchema = z.object({
type: z.literal('event.session.deleted'),
workspace_id: z.string().min(1),
});

export const workspaceCreatedEventSchema = z.object({
type: z.literal('event.workspace.created'),
workspace: workspaceSchema,
Expand Down Expand Up @@ -1055,6 +1060,7 @@ export const agentEventSchema = z.discriminatedUnion('type', [
sessionMetaUpdatedEventSchema,
sessionCreatedEventSchema,
sessionArchivedEventSchema,
sessionDeletedEventSchema,
workspaceCreatedEventSchema,
workspaceUpdatedEventSchema,
workspaceDeletedEventSchema,
Expand Down
6 changes: 4 additions & 2 deletions packages/kap-server/src/protocol/rest-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,10 @@ export type ArchiveSessionResponse = z.infer<typeof archiveSessionResponseSchema
export const restoreSessionResponseSchema = sessionSchema;
export type RestoreSessionResponse = z.infer<typeof restoreSessionResponseSchema>;

export const deleteSessionResponseSchema = archiveSessionResponseSchema;
export type DeleteSessionResponse = ArchiveSessionResponse;
export const deleteSessionResponseSchema = z.object({
deleted: z.literal(true),
});
export type DeleteSessionResponse = z.infer<typeof deleteSessionResponseSchema>;

export const sessionAbortResponseSchema = z.object({
aborted: z.boolean(),
Expand Down
28 changes: 27 additions & 1 deletion packages/kap-server/src/routes/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
type SessionSummary,
} from '@moonshot-ai/agent-core-v2';
import { SessionMetaUpdated } from '@moonshot-ai/agent-core-v2/session/sessionMetadata/sessionMetaEvents';
import { IGlobalSearchService } from '../search/searchService';
import { ErrorCode } from '../protocol/error-codes';
import { pageResponseSchema } from '../protocol/pagination';
import { toProtocolMessage } from '../services/messages/messageProjection';
Expand All @@ -41,6 +42,7 @@ import {
compactSessionResponseSchema,
createSessionChildRequestSchema,
createSessionRequestSchema,
deleteSessionResponseSchema,
forkSessionRequestSchema,
getSessionGoalResponseSchema,
listSessionChildrenResponseSchema,
Expand Down Expand Up @@ -594,6 +596,7 @@ export function registerSessionsRoutes(app: SessionRouteHost, core: Scope): void
sessionAbortResponseSchema,
startBtwSessionResponseSchema,
archiveSessionResponseSchema,
deleteSessionResponseSchema,
]),
},
errors: {
Expand Down Expand Up @@ -844,7 +847,15 @@ export function registerSessionsRoutes(app: SessionRouteHost, core: Scope): void
);
}

type SessionAction = 'fork' | 'compact' | 'undo' | 'abort' | 'btw' | 'restore' | 'archive';
type SessionAction =
| 'fork'
| 'compact'
| 'undo'
| 'abort'
| 'btw'
| 'restore'
| 'archive'
| 'delete';

interface SessionActionExtra {
readonly core: Scope;
Expand All @@ -865,6 +876,7 @@ const sessionActions: ActionTable<SessionAction, SessionActionExtra> = {
btw: { handle: btwSessionAction },
restore: { handle: restoreSessionAction },
archive: { handle: archiveSessionAction },
delete: { handle: deleteSessionAction },
};

async function forkSessionAction(
Expand Down Expand Up @@ -982,6 +994,20 @@ async function archiveSessionAction(ctx: SessionActionCtx): Promise<void> {
reply.send(okEnvelope({ archived: true }, req.id));
}

async function deleteSessionAction(ctx: SessionActionCtx): Promise<void> {
const { core, req, reply, id } = ctx;
try {
await core.accessor.get(ISessionManager).delete(id);
} catch (error) {
if (!isError2(error) || error.code !== ErrorCodes.SESSION_NOT_FOUND) throw error;
await core.accessor.get(IGlobalSearchService).deleteSession(id);
throw error;
}
await core.accessor.get(IGlobalSearchService).deleteSession(id);
requestLog(req)?.info({ session_id: id, action: 'delete' }, 'session action completed');
reply.send(okEnvelope({ deleted: true }, req.id));
}

export interface SessionWireFields {
readonly id: string;
readonly workspaceId: string;
Expand Down
Loading
Loading