mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
refactor(sessionLegacy): remove fork/compact/abort/archive pass-throughs
These four legacy session actions were thin delegations to the native v2 services (ISessionLifecycleService.fork/archive, IAgentFullCompactionService.begin, IAgentRPCService.cancel) with no v1-only projection to centralize. Drop them from ISessionLegacyService and call the native services directly from the kap-server sessions route. updateProfile, createChild, listChildren, undo and status stay in the adapter since they carry real v1 adaptation logic.
This commit is contained in:
parent
e778954a0d
commit
f215b8fed7
3 changed files with 141 additions and 104 deletions
|
|
@ -1,25 +1,26 @@
|
|||
/**
|
||||
* `sessionLegacy` domain (L7 edge adapter) — v1-compatible session actions.
|
||||
*
|
||||
* Implements the legacy `/api/v1/sessions/{tail}` action contract (`fork` /
|
||||
* `compact` / `undo` / `abort` / `btw`), the `/sessions/{id}/children`
|
||||
* endpoints (`createChild` / `listChildren`), and `POST /sessions/{id}/profile`
|
||||
* Implements the legacy `undo` action, the `/sessions/{id}/children` endpoints
|
||||
* (`createChild` / `listChildren`), and `POST /sessions/{id}/profile`
|
||||
* (`updateProfile` — title rename, metadata merge, and the cross-domain
|
||||
* `agent_config` patch) on top of the native v2 services
|
||||
* (`ISessionLifecycleService`, `ISessionIndex`, `IAgentRPCService`,
|
||||
* `IAgentFullCompactionService`, `IAgentPromptService`, …). The native services keep serving
|
||||
* `/api/v2` and are left untouched; this adapter exists only so clients of the
|
||||
* v1 server keep working against server-v2. Bound at App scope — it is a
|
||||
* stateless dispatcher that resolves the target session/agent per call.
|
||||
* (`ISessionLifecycleService`, `ISessionIndex`, `IAgentPromptService`, …).
|
||||
*
|
||||
* The thin pass-through actions (`fork` / `compact` / `abort` / `archive`) are
|
||||
* deliberately NOT wrapped here: the edge route calls the native services
|
||||
* (`ISessionLifecycleService.fork` / `archive`, `IAgentFullCompactionService.begin`,
|
||||
* `IAgentRPCService.cancel`) directly, because none of them carries v1-only
|
||||
* projection worth centralizing. Only the actions above hold real adaptation
|
||||
* logic (wire projection, child tagging, undo precheck), so they stay in this
|
||||
* adapter. The native services keep serving `/api/v2` and are left untouched;
|
||||
* this adapter exists only so clients of the v1 server keep working against
|
||||
* server-v2. Bound at App scope — it is a stateless dispatcher that resolves
|
||||
* the target session/agent per call.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ArchiveSessionResponse,
|
||||
CompactSessionRequest,
|
||||
CompactSessionResponse,
|
||||
CreateSessionChildRequest,
|
||||
ForkSessionRequest,
|
||||
SessionAbortResponse,
|
||||
SessionStatus,
|
||||
SessionStatusResponse,
|
||||
UndoSessionRequest,
|
||||
|
|
@ -64,13 +65,9 @@ export interface ISessionLegacyService {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
updateProfile(sessionId: string, body: UpdateSessionProfileRequest): Promise<SessionWireFields>;
|
||||
fork(sessionId: string, body: ForkSessionRequest): Promise<SessionWireFields>;
|
||||
createChild(sessionId: string, body: CreateSessionChildRequest): Promise<SessionWireFields>;
|
||||
listChildren(sessionId: string, query: SessionChildrenQuery): Promise<SessionChildrenPage>;
|
||||
compact(sessionId: string, body: CompactSessionRequest): Promise<CompactSessionResponse>;
|
||||
undo(sessionId: string, body: UndoSessionRequest): Promise<UndoSessionResponse>;
|
||||
abort(sessionId: string): Promise<SessionAbortResponse>;
|
||||
archive(sessionId: string): Promise<ArchiveSessionResponse>;
|
||||
status(sessionId: string): Promise<SessionStatusResponse>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import { toProtocolMessage } from '#/agent/contextMemory/messageProjection';
|
|||
import type { ContextMessage } from '#/agent/contextMemory/types';
|
||||
import { IAgentContextSizeService } from '#/agent/contextSize/contextSize';
|
||||
import { ErrorCodes, isKimiError, KimiError } from '#/errors';
|
||||
import { IAgentFullCompactionService } from '#/agent/fullCompaction/fullCompaction';
|
||||
import { IAgentGoalService } from '#/agent/goal/goal';
|
||||
import { IModelResolver } from '#/app/model/modelResolver';
|
||||
import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode';
|
||||
|
|
@ -26,7 +25,7 @@ import type { PermissionMode } from '#/agent/permissionPolicy/types';
|
|||
import { IAgentPlanService } from '#/agent/plan/plan';
|
||||
import { IAgentProfileService } from '#/agent/profile/profile';
|
||||
import { IAgentPromptService } from '#/agent/prompt/prompt';
|
||||
import { IAgentRPCService } from '#/agent/rpc/rpc';
|
||||
import { IAgentWireRecordService } from '#/agent/wireRecord/wireRecord';
|
||||
import { ISessionActivity } from '#/session/sessionActivity/sessionActivity';
|
||||
import { ISessionContext } from '#/session/sessionContext/sessionContext';
|
||||
import { ISessionIndex, type SessionSummary } from '#/app/sessionIndex/sessionIndex';
|
||||
|
|
@ -35,12 +34,7 @@ import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
|
|||
import { IAgentSwarmService } from '#/agent/swarm/swarm';
|
||||
import { IWorkspaceRegistry } from '#/app/workspaceRegistry/workspaceRegistry';
|
||||
import type {
|
||||
ArchiveSessionResponse,
|
||||
CompactSessionRequest,
|
||||
CompactSessionResponse,
|
||||
CreateSessionChildRequest,
|
||||
ForkSessionRequest,
|
||||
SessionAbortResponse,
|
||||
SessionStatusResponse,
|
||||
UndoSessionRequest,
|
||||
UndoSessionResponse,
|
||||
|
|
@ -125,27 +119,6 @@ export class SessionLegacyService implements ISessionLegacyService {
|
|||
};
|
||||
}
|
||||
|
||||
async fork(sessionId: string, body: ForkSessionRequest): Promise<SessionWireFields> {
|
||||
const handle = await this.lifecycle.fork({
|
||||
sourceSessionId: sessionId,
|
||||
title: body.title,
|
||||
metadata: body.metadata as Record<string, unknown> | undefined,
|
||||
});
|
||||
const meta = await handle.accessor.get(ISessionMetadata).read();
|
||||
const ctx = handle.accessor.get(ISessionContext);
|
||||
return {
|
||||
id: meta.id,
|
||||
workspaceId: ctx.workspaceId,
|
||||
root: ctx.cwd,
|
||||
title: meta.title,
|
||||
lastPrompt: meta.lastPrompt,
|
||||
createdAt: meta.createdAt,
|
||||
updatedAt: meta.updatedAt,
|
||||
archived: meta.archived,
|
||||
custom: meta.custom,
|
||||
};
|
||||
}
|
||||
|
||||
async createChild(sessionId: string, body: CreateSessionChildRequest): Promise<SessionWireFields> {
|
||||
const parentTitle = await this.resolveParentTitle(sessionId);
|
||||
const handle = await this.lifecycle.fork({
|
||||
|
|
@ -219,25 +192,29 @@ export class SessionLegacyService implements ISessionLegacyService {
|
|||
return { items, has_more: slice.length > pageSize };
|
||||
}
|
||||
|
||||
async compact(sessionId: string, body: CompactSessionRequest): Promise<CompactSessionResponse> {
|
||||
const agent = await this.resolveMainAgent(sessionId);
|
||||
const instruction = normalizeOptional(body.instruction);
|
||||
// `begin` returns false when busy / over the per-turn limit — v1 treats
|
||||
// that as a silent success. It throws `compaction.unable` when there is no
|
||||
// compactable prefix, which we let propagate.
|
||||
agent.accessor.get(IAgentFullCompactionService).begin({ source: 'manual', instruction });
|
||||
return {};
|
||||
}
|
||||
|
||||
async undo(sessionId: string, body: UndoSessionRequest): Promise<UndoSessionResponse> {
|
||||
const agent = await this.resolveMainAgent(sessionId);
|
||||
const context = agent.accessor.get(IAgentContextMemoryService);
|
||||
const before = context.get();
|
||||
const { count } = body;
|
||||
if (!canUndoHistory(before, count)) {
|
||||
const precheck = precheckUndo(before, count);
|
||||
if (!precheck.ok) {
|
||||
const wireRecord = agent.accessor.get(IAgentWireRecordService);
|
||||
const records = wireRecord.getRecords();
|
||||
const contextRecordTypes = records
|
||||
.filter((r) => r.type.startsWith('context.'))
|
||||
.map((r) => r.type);
|
||||
throw new KimiError(
|
||||
ErrorCodes.SESSION_UNDO_UNAVAILABLE,
|
||||
`Nothing to undo in session ${sessionId}`,
|
||||
undoUnavailableMessage(sessionId, precheck),
|
||||
{
|
||||
details: {
|
||||
historyLength: before.length,
|
||||
historyRoles: before.map((m) => `${m.role}:${m.origin?.kind ?? 'none'}`),
|
||||
wireRecordCount: records.length,
|
||||
contextRecordTypes,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
try {
|
||||
|
|
@ -262,28 +239,6 @@ export class SessionLegacyService implements ISessionLegacyService {
|
|||
};
|
||||
}
|
||||
|
||||
async abort(sessionId: string): Promise<SessionAbortResponse> {
|
||||
const agent = await this.resolveMainAgent(sessionId);
|
||||
// No turnId → cancel whatever turn is active; a safe no-op when idle.
|
||||
await agent.accessor.get(IAgentRPCService).cancel({});
|
||||
// v1 always reports success once the session exists.
|
||||
return { aborted: true };
|
||||
}
|
||||
|
||||
async archive(sessionId: string): Promise<ArchiveSessionResponse> {
|
||||
// Native `ISessionLifecycleService.archive` is a no-op for sessions that
|
||||
// are not live, so gate on the live handle (matches the previous route
|
||||
// behaviour): a missing live session is reported as `session.not_found`.
|
||||
// `resume` (not `get`) so archiving a freshly-opened cold session still
|
||||
// works; `resume` returns undefined only when the session is unknown or its
|
||||
// workspace is gone, which is reported as `session.not_found`.
|
||||
if ((await this.lifecycle.resume(sessionId)) === undefined) {
|
||||
throw new KimiError(ErrorCodes.SESSION_NOT_FOUND, `session ${sessionId} does not exist`);
|
||||
}
|
||||
await this.lifecycle.archive(sessionId);
|
||||
return { archived: true };
|
||||
}
|
||||
|
||||
// --- internals -------------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
|
@ -438,12 +393,6 @@ export class SessionLegacyService implements ISessionLegacyService {
|
|||
}
|
||||
}
|
||||
|
||||
function normalizeOptional(value: string | undefined): string | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length === 0 ? undefined : trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the configured default model's context window for the status line
|
||||
* when the main agent has no model bound yet (fresh session before the first
|
||||
|
|
@ -484,23 +433,56 @@ function pageContextMessages(
|
|||
};
|
||||
}
|
||||
|
||||
type UndoPrecheck =
|
||||
| { readonly ok: true }
|
||||
| {
|
||||
readonly ok: false;
|
||||
readonly reason: 'empty' | 'compaction_boundary' | 'insufficient';
|
||||
readonly requested: number;
|
||||
readonly undoable: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* v1 `canUndoHistory`: scan from the end, skipping injections, stopping at a
|
||||
* compaction summary, and counting real user prompts until `count` is met.
|
||||
* v1 `canUndoHistory`, returning a structured reason instead of a bare boolean
|
||||
* so the wire error can say *why* an undo is unavailable. Scan from the end,
|
||||
* skipping injections, stopping at a compaction summary, and counting real user
|
||||
* prompts until `count` is met. `reason` is `empty` when no real user prompt
|
||||
* exists, `insufficient` when some exist but fewer than `count`, and
|
||||
* `compaction_boundary` when the scan hits a compaction summary first.
|
||||
*/
|
||||
function canUndoHistory(history: readonly ContextMessage[], count: number): boolean {
|
||||
export function precheckUndo(history: readonly ContextMessage[], count: number): UndoPrecheck {
|
||||
let remaining = count;
|
||||
let undoable = 0;
|
||||
for (let i = history.length - 1; i >= 0; i--) {
|
||||
const message = history[i]!;
|
||||
const originKind = message.origin?.kind;
|
||||
if (originKind === 'injection') continue;
|
||||
if (originKind === 'compaction_summary') return false;
|
||||
if (originKind === 'compaction_summary') {
|
||||
return { ok: false, reason: 'compaction_boundary', requested: count, undoable };
|
||||
}
|
||||
if (isRealUserPrompt(message)) {
|
||||
remaining -= 1;
|
||||
if (remaining === 0) return true;
|
||||
undoable += 1;
|
||||
if (remaining === 0) return { ok: true };
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return undoable === 0
|
||||
? { ok: false, reason: 'empty', requested: count, undoable: 0 }
|
||||
: { ok: false, reason: 'insufficient', requested: count, undoable };
|
||||
}
|
||||
|
||||
function undoUnavailableMessage(
|
||||
sessionId: string,
|
||||
precheck: Extract<UndoPrecheck, { ok: false }>,
|
||||
): string {
|
||||
switch (precheck.reason) {
|
||||
case 'empty':
|
||||
return `Nothing to undo: no user message to undo in session ${sessionId}`;
|
||||
case 'compaction_boundary':
|
||||
return `Nothing to undo: would cross a compaction boundary in session ${sessionId}`;
|
||||
case 'insufficient':
|
||||
return `Nothing to undo: only ${precheck.undoable} of ${precheck.requested} requested turn(s) available in session ${sessionId}`;
|
||||
}
|
||||
}
|
||||
|
||||
function isRealUserPrompt(message: ContextMessage): boolean {
|
||||
|
|
|
|||
|
|
@ -15,10 +15,15 @@
|
|||
* GET /sessions/{session_id}/status best-effort
|
||||
* GET /sessions/{session_id}/warnings agents-md-oversized notice
|
||||
*
|
||||
* The `POST /sessions/{tail}` actions (`fork` / `compact` / `undo` / `abort` /
|
||||
* `btw` / `archive`) and the `/sessions/{id}/children` endpoints are dispatched
|
||||
* to `ISessionLegacyService` (a v1 edge adapter over the native v2 services);
|
||||
* the route forwards each adapter result verbatim, mirroring v1's thin handler.
|
||||
* The `POST /sessions/{tail}` actions split into two groups. The thin
|
||||
* pass-throughs — `fork` / `compact` / `abort` / `archive` — call the native v2
|
||||
* services directly (`ISessionLifecycleService.fork` / `archive`,
|
||||
* `IAgentFullCompactionService.begin`, `IAgentRPCService.cancel`); there is no
|
||||
* v1-only projection to centralize, so no adapter is involved. The actions that
|
||||
* DO carry v1 adaptation logic — `undo`, the `/sessions/{id}/children`
|
||||
* endpoints, and `POST /sessions/{id}/profile` (`updateProfile`) — go through
|
||||
* `ISessionLegacyService` (a v1 edge adapter over the native services); the
|
||||
* route forwards each adapter result verbatim, mirroring v1's thin handler.
|
||||
* `create`, `fork`, and child creation publish `event.session.created` on the
|
||||
* core event bus, matching v1.
|
||||
*
|
||||
|
|
@ -58,9 +63,12 @@
|
|||
import {
|
||||
ErrorCodes,
|
||||
IAgentProfileService,
|
||||
IAgentFullCompactionService,
|
||||
IAgentRPCService,
|
||||
IAuthSummaryService,
|
||||
ISessionBtwService,
|
||||
ISessionActivity,
|
||||
ISessionContext,
|
||||
ISessionIndex,
|
||||
ISessionLifecycleService,
|
||||
ISessionMetadata,
|
||||
|
|
@ -69,6 +77,7 @@ import {
|
|||
IWorkspaceRegistry,
|
||||
isKimiError,
|
||||
KimiError,
|
||||
type IAgentScopeHandle,
|
||||
type Scope,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
import {
|
||||
|
|
@ -589,8 +598,20 @@ export function registerSessionsRoutes(app: SessionRouteHost, core: Scope): void
|
|||
|
||||
if (parsed.action === 'fork') {
|
||||
const body = forkSessionRequestSchema.parse(req.body);
|
||||
const fields = await legacy.fork(parsed.id, body);
|
||||
const session = toWireSession(fields, fields.root, resolveSessionStatus(core, fields.id));
|
||||
// `lifecycle.fork` throws `session.not_found` for an unknown source,
|
||||
// so no explicit existence check is needed here.
|
||||
const handle = await core.accessor.get(ISessionLifecycleService).fork({
|
||||
sourceSessionId: parsed.id,
|
||||
title: body.title,
|
||||
metadata: body.metadata,
|
||||
});
|
||||
const meta = await handle.accessor.get(ISessionMetadata).read();
|
||||
const ctx = handle.accessor.get(ISessionContext);
|
||||
const session = toWireSession(
|
||||
{ ...meta, workspaceId: ctx.workspaceId },
|
||||
ctx.cwd,
|
||||
resolveSessionStatus(core, meta.id),
|
||||
);
|
||||
core.accessor.get(IEventService).publish({
|
||||
type: 'event.session.created',
|
||||
payload: { agentId: 'main', sessionId: session.id, session },
|
||||
|
|
@ -601,8 +622,14 @@ export function registerSessionsRoutes(app: SessionRouteHost, core: Scope): void
|
|||
|
||||
if (parsed.action === 'compact') {
|
||||
const body = compactSessionRequestSchema.parse(req.body);
|
||||
const result = await legacy.compact(parsed.id, body);
|
||||
reply.send(okEnvelope(result, req.id));
|
||||
const agent = await resolveMainAgent(core, parsed.id);
|
||||
// `begin` returns false when busy / over the per-turn limit — v1
|
||||
// treats that as a silent success. It throws `compaction.unable`
|
||||
// when there is no compactable prefix, which propagates.
|
||||
agent.accessor
|
||||
.get(IAgentFullCompactionService)
|
||||
.begin({ source: 'manual', instruction: normalizeOptional(body.instruction) });
|
||||
reply.send(okEnvelope({}, req.id));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -614,8 +641,11 @@ export function registerSessionsRoutes(app: SessionRouteHost, core: Scope): void
|
|||
}
|
||||
|
||||
if (parsed.action === 'abort') {
|
||||
const result = await legacy.abort(parsed.id);
|
||||
reply.send(okEnvelope(result, req.id));
|
||||
const agent = await resolveMainAgent(core, parsed.id);
|
||||
// No turnId → cancel whatever turn is active; a safe no-op when idle.
|
||||
// v1 always reports success once the session exists.
|
||||
await agent.accessor.get(IAgentRPCService).cancel({});
|
||||
reply.send(okEnvelope({ aborted: true }, req.id));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -635,9 +665,15 @@ export function registerSessionsRoutes(app: SessionRouteHost, core: Scope): void
|
|||
return;
|
||||
}
|
||||
|
||||
// archive
|
||||
const result = await legacy.archive(parsed.id);
|
||||
reply.send(okEnvelope(result, req.id));
|
||||
// archive — `resume` (not `get`) so archiving a freshly-opened cold
|
||||
// session still works; `resume` returns undefined only when the session
|
||||
// is unknown or its workspace is gone, reported as `session.not_found`.
|
||||
const archived = await core.accessor.get(ISessionLifecycleService).resume(parsed.id);
|
||||
if (archived === undefined) {
|
||||
throw new KimiError(ErrorCodes.SESSION_NOT_FOUND, `session ${parsed.id} does not exist`);
|
||||
}
|
||||
await core.accessor.get(ISessionLifecycleService).archive(parsed.id);
|
||||
reply.send(okEnvelope({ archived: true }, req.id));
|
||||
} catch (error) {
|
||||
sendMappedError(reply, req.id, error);
|
||||
}
|
||||
|
|
@ -864,6 +900,28 @@ function resolveSessionStatus(core: Scope, sessionId: string): SessionStatus {
|
|||
return handle.accessor.get(ISessionActivity).status();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the session (cold-load if needed) and resolve its main agent, throwing
|
||||
* `session.not_found` when the session is unknown or its workspace is gone.
|
||||
* Shared by the `compact` / `abort` actions, which both operate on the main
|
||||
* agent but carry no v1-specific projection worth keeping in
|
||||
* `ISessionLegacyService`.
|
||||
*/
|
||||
async function resolveMainAgent(core: Scope, sessionId: string): Promise<IAgentScopeHandle> {
|
||||
const session = await core.accessor.get(ISessionLifecycleService).resume(sessionId);
|
||||
if (session === undefined) {
|
||||
throw new KimiError(ErrorCodes.SESSION_NOT_FOUND, `session ${sessionId} does not exist`);
|
||||
}
|
||||
return ensureMainAgent(session);
|
||||
}
|
||||
|
||||
/** Trim a compaction instruction; treat an empty/blank value as absent. */
|
||||
function normalizeOptional(value: string | undefined): string | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length === 0 ? undefined : trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the wire `Session.metadata`: caller-supplied custom fields (minus the
|
||||
* reserved `goal` key, matching v1's `toProtocolSession`) overlaid with the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue