Skip to content
Merged
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
---

Add session deletion to the server API via `POST /api/v1/sessions/{id}:delete`.
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 @@ -31,6 +31,7 @@ export interface ISessionManager {
readonly onDidCreateSession?: Event<SessionCreatedEvent & IWaitUntil>;
readonly onWillCloseSession?: Event<SessionWillCloseEvent & IWaitUntil>;
readonly onDidCloseSession?: Event<SessionClosedEvent>;
readonly onWillDeleteSession?: Event<{ readonly sessionId: string } & IWaitUntil>;
readonly onDidArchiveSession?: Event<SessionArchivedEvent>;
readonly onDidForkSession?: Event<SessionForkedEvent>;
create(options: CreateManagedSessionOptions): Promise<ISessionScopeHandle>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export class SessionManager implements ISessionManager {
readonly onWillCloseSession = this.willCloseEmitter.event;
private readonly didCloseEmitter = new Emitter<SessionClosedEvent>();
readonly onDidCloseSession = this.didCloseEmitter.event;
private readonly willDeleteEmitter = new Emitter<{ readonly sessionId: string } & IWaitUntil>();
readonly onWillDeleteSession = this.willDeleteEmitter.event;
private readonly didArchiveEmitter = new Emitter<SessionArchivedEvent>();
readonly onDidArchiveSession = this.didArchiveEmitter.event;
private readonly didForkEmitter = new Emitter<SessionForkedEvent>();
Expand All @@ -66,9 +68,9 @@ export class SessionManager implements ISessionManager {
? { root: options.workDir }
: { workspaceId: options.workspaceId, root: options.workDir },
);
const controller = this.controllerForWorkspace(workspace.id);
if (options.sessionId === undefined) return controller.create(options);
return this.serializeLifecycle(options.sessionId, () => controller.create(options));
const create = () => this.controllerForWorkspace(workspace.id).create(options);
if (options.sessionId === undefined) return create();
return this.serializeLifecycle(options.sessionId, create);
}

async resume(sessionId: string, options?: ResumeSessionOptions): Promise<ISessionScopeHandle | undefined> {
Expand Down Expand Up @@ -169,6 +171,20 @@ export class SessionManager implements ISessionManager {
if (controller === undefined) {
throw new Error2(ErrorCodes.SESSION_NOT_FOUND, `session ${sessionId} does not exist`);
}
await controller.close(sessionId);
const cleanups: Promise<unknown>[] = [];
this.willDeleteEmitter.fire({
sessionId,
signal: new AbortController().signal,
waitUntil: (cleanup) => {
if (Object.isFrozen(cleanups)) throw new Error('waitUntil must be called synchronously');
cleanups.push(cleanup);
},
});
void Object.freeze(cleanups);
const settled = await Promise.allSettled(cleanups);
const failed = settled.find((result) => result.status === 'rejected');
if (failed?.status === 'rejected') throw failed.reason;
await controller.delete(sessionId);
});
}
Expand Down Expand Up @@ -218,6 +234,7 @@ export class SessionManager implements ISessionManager {
this.didCreateEmitter.dispose();
this.willCloseEmitter.dispose();
this.didCloseEmitter.dispose();
this.willDeleteEmitter.dispose();
this.didArchiveEmitter.dispose();
this.didForkEmitter.dispose();
}
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 @@ -97,7 +97,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 @@ -475,6 +475,11 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
await dropFileHistorySession({ docs: this.docs, workspaceId: this.workspaceId, 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 @@ -1056,6 +1061,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
20 changes: 19 additions & 1 deletion packages/kap-server/src/routes/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
compactSessionResponseSchema,
createSessionChildRequestSchema,
createSessionRequestSchema,
deleteSessionResponseSchema,
forkSessionRequestSchema,
getSessionGoalResponseSchema,
listSessionChildrenResponseSchema,
Expand Down Expand Up @@ -606,6 +607,7 @@ export function registerSessionsRoutes(
sessionAbortResponseSchema,
startBtwSessionResponseSchema,
archiveSessionResponseSchema,
deleteSessionResponseSchema,
]),
},
errors: {
Expand Down Expand Up @@ -854,7 +856,15 @@ export function registerSessionsRoutes(
);
}

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 @@ -875,6 +885,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 @@ -990,6 +1001,13 @@ 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;
await core.accessor.get(ISessionManager).delete(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
6 changes: 6 additions & 0 deletions packages/kap-server/src/transport/ws/v1/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export interface SessionArchivedEvent {
readonly workspace_id: string;
}

export interface SessionDeletedEvent {
readonly type: 'event.session.deleted';
readonly workspace_id: string;
}

export interface WorkspaceCreatedEvent {
readonly type: 'event.workspace.created';
readonly workspace: Workspace;
Expand Down Expand Up @@ -218,6 +223,7 @@ export type AgentEvent =
| SessionMetaUpdatedEvent
| SessionCreatedEvent
| SessionArchivedEvent
| SessionDeletedEvent
| WorkspaceCreatedEvent
| WorkspaceUpdatedEvent
| WorkspaceDeletedEvent
Expand Down
Loading
Loading