mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix(kap-server): align archived session restore
This commit is contained in:
parent
d2985e17dd
commit
f91b34dc69
12 changed files with 218 additions and 25 deletions
|
|
@ -95,6 +95,9 @@ export interface ISessionActivityKernel {
|
|||
|
||||
lane(): SessionLane;
|
||||
|
||||
/** Leaves the restore/materialize window and admits normal session commands. */
|
||||
markActive(): void;
|
||||
|
||||
/** Admission table for edge (gateway / rpc / legacy) and `agentLifecycle` commands. */
|
||||
canAccept(command: SessionCommand): boolean;
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ export class SessionActivityKernel extends Disposable implements ISessionActivit
|
|||
return 'active';
|
||||
}
|
||||
|
||||
markActive(): void {
|
||||
// Placeholder kernel is already active.
|
||||
}
|
||||
|
||||
canAccept(_command: SessionCommand): boolean {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* Defines the public contract of session lifecycle: the `CreateSessionOptions`,
|
||||
* `ForkSessionOptions`, `CreateChildSessionOptions`, and the
|
||||
* `ISessionLifecycleService` used to create sessions (`create`), look up the
|
||||
* live ones (`get` / `list`), close them (`close`), archive them (`archive`),
|
||||
* live ones (`get` / `list`), close them (`close`), archive/restore them,
|
||||
* fork them (`fork`), and fork-then-tag them as direct children (`createChild`). Announces
|
||||
* lifecycle transitions through ordered hook slots plus
|
||||
* `onDidCreateSession` / `onDidCloseSession` / `onDidArchiveSession` /
|
||||
|
|
@ -114,6 +114,7 @@ export interface ISessionLifecycleService {
|
|||
resume(sessionId: string): Promise<ISessionScopeHandle | undefined>;
|
||||
close(sessionId: string): Promise<void>;
|
||||
archive(sessionId: string): Promise<void>;
|
||||
restore(sessionId: string): Promise<ISessionScopeHandle | undefined>;
|
||||
fork(opts: ForkSessionOptions): Promise<ISessionScopeHandle>;
|
||||
/**
|
||||
* Fork a session and tag it as a direct child of its source (writes the
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* through the DI scope tree and seeding each with its identity and storage
|
||||
* addressing, running lifecycle hook slots, and tearing them down on
|
||||
* close/archive — archiving flags the session's `sessionMetadata`, removes
|
||||
* its `agentLifecycle` agents, and
|
||||
* its `agentLifecycle` agents, restoring clears the archived flag, and
|
||||
* broadcasts through `event`. Materializes the session's initial metadata on
|
||||
* creation by resolving `sessionMetadata`. Bound at App scope. Persisted
|
||||
* sessions are discovered through the `sessionIndex` read model, and workspace
|
||||
|
|
@ -277,6 +277,13 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
|
|||
this._onDidArchiveSession.fire({ sessionId });
|
||||
}
|
||||
|
||||
async restore(sessionId: string): Promise<ISessionScopeHandle | undefined> {
|
||||
const handle = await this.resume(sessionId);
|
||||
if (handle === undefined) return undefined;
|
||||
await handle.accessor.get(ISessionMetadata).setArchived(false);
|
||||
return handle;
|
||||
}
|
||||
|
||||
private async announceWillClose(event: SessionWillCloseEvent): Promise<void> {
|
||||
await this.hooks.onWillCloseSession.run(event);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export function stubSessionActivityKernel(
|
|||
return {
|
||||
_serviceBrand: undefined,
|
||||
lane: () => lane,
|
||||
markActive: () => undefined,
|
||||
canAccept: (_command: SessionCommand) => lane === 'active',
|
||||
admitTurn(_agentId: string, lease: ActivityLease): IDisposable {
|
||||
leases.add(lease);
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ function stubSessionLifecycle(): ISessionLifecycleService {
|
|||
resume: async () => undefined,
|
||||
close: async () => {},
|
||||
archive: async () => {},
|
||||
restore: async () => undefined,
|
||||
fork: async () => {
|
||||
throw new Error('not implemented');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ function registerSessionExportServices(
|
|||
resume: async () => options.lifecycleHandle,
|
||||
close: async () => {},
|
||||
archive: async () => {},
|
||||
restore: async () => options.lifecycleHandle,
|
||||
fork: async () => {
|
||||
throw new Error('fork should not be called by session export');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
registerScopedService,
|
||||
} from '#/_base/di/scope';
|
||||
import { type ScopedTestHost, createScopedTestHost, stubPair } from '#/_base/di/test';
|
||||
import { ISessionActivityKernel } from '#/activity/activity';
|
||||
import { IBootstrapService } from '#/app/bootstrap/bootstrap';
|
||||
import { IHostEnvironment } from '#/os/interface/hostEnvironment';
|
||||
import { IEventService } from '#/app/event/event';
|
||||
|
|
@ -35,6 +36,7 @@ import { SessionWorkspaceContextService } from '#/session/workspaceContext/works
|
|||
import { IWorkspaceRegistry, type Workspace } from '#/app/workspaceRegistry/workspaceRegistry';
|
||||
import { encodeWorkDirKey } from '#/_base/utils/workdir-slug';
|
||||
import { ISessionContext } from '#/session/sessionContext/sessionContext';
|
||||
import { stubSessionActivityKernel } from '../activity/stubs';
|
||||
|
||||
function bootstrapStub(): IBootstrapService {
|
||||
return {
|
||||
|
|
@ -331,6 +333,7 @@ describe('SessionLifecycleService', () => {
|
|||
stubPair(IAtomicDocumentStore, atomicDocumentStoreStub()),
|
||||
stubPair(IEventService, eventStub()),
|
||||
stubPair(IAgentLifecycleService, agentLifecycleStub()),
|
||||
stubPair(ISessionActivityKernel, stubSessionActivityKernel()),
|
||||
stubPair(IWorkspaceLocalConfigService, workspaceLocalConfigStub()),
|
||||
...extra,
|
||||
]);
|
||||
|
|
@ -485,6 +488,26 @@ describe('SessionLifecycleService', () => {
|
|||
expect(svc.get('s1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('restore clears the archived flag when the session exists on disk', async () => {
|
||||
let archived: boolean | undefined;
|
||||
const svc = build([
|
||||
stubPair(ISessionIndex, sessionIndexWithSummary('s1', '/tmp/proj')),
|
||||
stubPair(IAgentLifecycleService, agentLifecycleWithMainStub()),
|
||||
stubPair(ISessionMetadata, {
|
||||
...metadataStub(),
|
||||
setArchived: (value: boolean) => {
|
||||
archived = value;
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const restored = await svc.restore('s1');
|
||||
|
||||
expect(restored?.id).toBe('s1');
|
||||
expect(archived).toBe(false);
|
||||
});
|
||||
|
||||
it('fires onDidCreateSession with the new handle', async () => {
|
||||
const svc = build();
|
||||
let captured: { readonly sessionId: string } | undefined;
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@
|
|||
* GET /sessions/{session_id}/profile
|
||||
* POST /sessions/{session_id}/profile update title / metadata / agent_config
|
||||
* POST /sessions/{tail} action: fork / compact / undo /
|
||||
* abort / btw / archive
|
||||
* abort / btw / archive / restore
|
||||
* GET /sessions/{session_id}/children list child sessions
|
||||
* POST /sessions/{session_id}/children create child session (fork+tag)
|
||||
* GET /sessions/{session_id}/status best-effort
|
||||
* GET /sessions/{session_id}/warnings agents-md-oversized notice
|
||||
*
|
||||
* 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`,
|
||||
* pass-throughs — `fork` / `compact` / `abort` / `archive` / `restore` — call
|
||||
* the native v2 services directly (`ISessionLifecycleService.fork` / `archive` / `restore`,
|
||||
* `IAgentFullCompactionService.begin`, `IAgentRPCService.cancel`); there is no
|
||||
* v1-only projection to centralize, so no adapter is involved. `undo` likewise
|
||||
* calls `IAgentPromptService.undo` directly (it now throws
|
||||
|
|
@ -55,7 +55,9 @@
|
|||
* is real on every session-producing endpoint here. `GET /sessions` and
|
||||
* `GET /sessions/{id}/children` filter their projected page by the `status`
|
||||
* query param (post-page, matching v1 — `has_more` reflects the pre-filter
|
||||
* page). The `aborted` phase is not derived yet (gap G10); v2 never reports it.
|
||||
* page), except `archived_only` lists filter status before route pagination so
|
||||
* they can drain archived pages the same way v1 does. The `aborted` phase is not
|
||||
* derived yet (gap G10); v2 never reports it.
|
||||
*
|
||||
* **cwd resolution (gap G3 closed)**: the session's frozen work dir is
|
||||
* persisted on its metadata document (`ISessionMetadata`) and surfaced on the
|
||||
|
|
@ -145,6 +147,8 @@ const booleanQueryParam = z.preprocess((value) => {
|
|||
return value;
|
||||
}, z.boolean().optional());
|
||||
|
||||
const DEFAULT_SESSION_LIST_PAGE_SIZE = 20;
|
||||
|
||||
// NOTE: mirrors v1's `GET /sessions` query. `before_id`/`after_id` id-cursors
|
||||
// and `page_size` ARE applied in the route handler (the `FileSessionIndex` does
|
||||
// not implement `cursor`, so we page over its recency-sorted result); `status`
|
||||
|
|
@ -365,18 +369,20 @@ export function registerSessionsRoutes(app: SessionRouteHost, core: Scope): void
|
|||
includeArchived: archivedOnly ? true : raw.include_archive,
|
||||
});
|
||||
|
||||
// Filter down to the sequence the client actually sees BEFORE computing
|
||||
// the cursor position and the page boundary, so a cursor carried over
|
||||
// from a previous page always resolves to the same index. `cwd` is read
|
||||
// from the session's own summary first (gap G3 closed — an unregistered
|
||||
// workspace no longer drops the session); the registry `roots` map is
|
||||
// only a back-compat fallback for sessions written before `cwd` was
|
||||
// persisted. A session with no recoverable cwd is still skipped.
|
||||
const eligible: { summary: (typeof page.items)[number]; cwd: string }[] = [];
|
||||
// Filter down to the sequence the client can page over BEFORE computing
|
||||
// the cursor position. `cwd` is read from the session's own summary first
|
||||
// (gap G3 closed — an unregistered workspace no longer drops the session);
|
||||
// the registry `roots` map is only a back-compat fallback for sessions
|
||||
// written before `cwd` was persisted. A session with no recoverable cwd is
|
||||
// still skipped.
|
||||
const eligible: {
|
||||
readonly summary: (typeof page.items)[number];
|
||||
readonly cwd: string;
|
||||
readonly status?: Session['status'];
|
||||
}[] = [];
|
||||
for (const summary of page.items) {
|
||||
const cwd = summary.cwd ?? roots.get(summary.workspaceId);
|
||||
if (cwd === undefined) continue;
|
||||
if (archivedOnly && summary.archived !== true) continue;
|
||||
if (raw.exclude_empty === true && (summary.lastPrompt ?? '').length === 0) continue;
|
||||
eligible.push({ summary, cwd });
|
||||
}
|
||||
|
|
@ -399,18 +405,30 @@ export function registerSessionsRoutes(app: SessionRouteHost, core: Scope): void
|
|||
}
|
||||
|
||||
const window = eligible.slice(start, end);
|
||||
const limit = pageSize ?? window.length;
|
||||
const hasMore = window.length > limit;
|
||||
const projected: Session[] = window
|
||||
let visible = window;
|
||||
if (archivedOnly) {
|
||||
visible =
|
||||
raw.status === undefined
|
||||
? window.filter((entry) => entry.summary.archived === true)
|
||||
: window.flatMap((entry) => {
|
||||
if (entry.summary.archived !== true) return [];
|
||||
const status = resolveSessionStatus(core, entry.summary.id);
|
||||
return status === raw.status ? [{ ...entry, status }] : [];
|
||||
});
|
||||
}
|
||||
const limit = archivedOnly
|
||||
? (pageSize ?? DEFAULT_SESSION_LIST_PAGE_SIZE)
|
||||
: (pageSize ?? visible.length);
|
||||
const hasMore = visible.length > limit;
|
||||
const projected: Session[] = visible
|
||||
.slice(0, limit)
|
||||
.map(({ summary, cwd }) =>
|
||||
toWireSession(summary, cwd, resolveSessionStatus(core, summary.id)),
|
||||
.map(({ summary, cwd, status }) =>
|
||||
toWireSession(summary, cwd, status ?? resolveSessionStatus(core, summary.id)),
|
||||
);
|
||||
// v1 filters the projected page by `status` (post-page); `has_more` keeps
|
||||
// reflecting the pre-filter page, so a filtered page may be short or empty
|
||||
// while `has_more` is still true — match that exactly.
|
||||
// v1 filters ordinary lists by `status` post-page; `archived_only` already
|
||||
// applied status before pagination above so it can drain to a full page.
|
||||
const items =
|
||||
raw.status !== undefined
|
||||
raw.status !== undefined && !archivedOnly
|
||||
? projected.filter((session) => session.status === raw.status)
|
||||
: projected;
|
||||
reply.send(okEnvelope({ items, has_more: hasMore }, req.id));
|
||||
|
|
@ -593,7 +611,7 @@ export function registerSessionsRoutes(app: SessionRouteHost, core: Scope): void
|
|||
const { tail } = req.params;
|
||||
const parsed = parseActionSuffix({
|
||||
tail,
|
||||
allowedActions: ['fork', 'compact', 'undo', 'abort', 'btw', 'archive'] as const,
|
||||
allowedActions: ['fork', 'compact', 'undo', 'abort', 'btw', 'archive', 'restore'] as const,
|
||||
resourceLabel: 'session',
|
||||
});
|
||||
if (parsed.kind !== 'action') {
|
||||
|
|
@ -697,6 +715,22 @@ export function registerSessionsRoutes(app: SessionRouteHost, core: Scope): void
|
|||
return;
|
||||
}
|
||||
|
||||
if (parsed.action === 'restore') {
|
||||
const restored = await core.accessor.get(ISessionLifecycleService).restore(parsed.id);
|
||||
if (restored === undefined) {
|
||||
throw new KimiError(ErrorCodes.SESSION_NOT_FOUND, `session ${parsed.id} does not exist`);
|
||||
}
|
||||
const meta = await restored.accessor.get(ISessionMetadata).read();
|
||||
const ctx = restored.accessor.get(ISessionContext);
|
||||
const session = toWireSession(
|
||||
{ ...meta, workspaceId: ctx.workspaceId },
|
||||
ctx.cwd,
|
||||
resolveSessionStatus(core, meta.id),
|
||||
);
|
||||
reply.send(okEnvelope(session, req.id));
|
||||
return;
|
||||
}
|
||||
|
||||
// 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`.
|
||||
|
|
|
|||
|
|
@ -324,6 +324,28 @@ describe('server-v2 /api/v1/sessions', () => {
|
|||
expect(got.body.data.archived).toBe(true);
|
||||
});
|
||||
|
||||
it('restores an archived session via :restore and returns it to the default list', async () => {
|
||||
const cwd = home as string;
|
||||
const created = await postJson<SessionWire>('/api/v1/sessions', { metadata: { cwd } });
|
||||
const id = created.body.data.id;
|
||||
|
||||
await postJson<{ archived: boolean }>(`/api/v1/sessions/${id}:archive`);
|
||||
|
||||
const restored = await postJson<SessionWire>(`/api/v1/sessions/${id}:restore`);
|
||||
expect(restored.body.code).toBe(0);
|
||||
expect(restored.body.data.id).toBe(id);
|
||||
expect(restored.body.data.archived).toBe(false);
|
||||
|
||||
const listed = await getJson<PageWire>('/api/v1/sessions');
|
||||
expect(listed.body.code).toBe(0);
|
||||
expect(listed.body.data.items.find((s) => s.id === id)?.archived).toBe(false);
|
||||
});
|
||||
|
||||
it('returns 40401 when restoring a missing session', async () => {
|
||||
const { body } = await postJson<null>('/api/v1/sessions/sess_missing:restore');
|
||||
expect(body.code).toBe(40401);
|
||||
});
|
||||
|
||||
it('cold-loads a persisted session on :undo instead of 40401', async () => {
|
||||
const cwd = home as string;
|
||||
const created = await postJson<SessionWire>('/api/v1/sessions', { metadata: { cwd } });
|
||||
|
|
@ -482,6 +504,42 @@ describe('server-v2 /api/v1/sessions', () => {
|
|||
expect(all.body.data.items.some((s) => s.id === archivedId)).toBe(true);
|
||||
});
|
||||
|
||||
it('paginates archived_only without returning empty filtered pages', async () => {
|
||||
const cwd = home as string;
|
||||
const archivedOlder = await postJson<SessionWire>('/api/v1/sessions', { metadata: { cwd } });
|
||||
await postJson<{ archived: boolean }>(
|
||||
`/api/v1/sessions/${archivedOlder.body.data.id}:archive`,
|
||||
);
|
||||
|
||||
const archivedNewer = await postJson<SessionWire>('/api/v1/sessions', { metadata: { cwd } });
|
||||
await postJson<{ archived: boolean }>(
|
||||
`/api/v1/sessions/${archivedNewer.body.data.id}:archive`,
|
||||
);
|
||||
|
||||
await postJson<SessionWire>('/api/v1/sessions', { metadata: { cwd } });
|
||||
await postJson<SessionWire>('/api/v1/sessions', { metadata: { cwd } });
|
||||
|
||||
const first = await getJson<PageWire>('/api/v1/sessions?archived_only=true&page_size=1');
|
||||
expect(first.body.code).toBe(0);
|
||||
expect(first.body.data.items).toHaveLength(1);
|
||||
expect(first.body.data.items[0]).toMatchObject({
|
||||
id: archivedNewer.body.data.id,
|
||||
archived: true,
|
||||
});
|
||||
expect(first.body.data.has_more).toBe(true);
|
||||
|
||||
const second = await getJson<PageWire>(
|
||||
`/api/v1/sessions?archived_only=true&page_size=1&before_id=${archivedNewer.body.data.id}`,
|
||||
);
|
||||
expect(second.body.code).toBe(0);
|
||||
expect(second.body.data.items).toHaveLength(1);
|
||||
expect(second.body.data.items[0]).toMatchObject({
|
||||
id: archivedOlder.body.data.id,
|
||||
archived: true,
|
||||
});
|
||||
expect(second.body.data.has_more).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects archived_only combined with include_archive (40001)', async () => {
|
||||
const { body } = await getJson<null>(
|
||||
'/api/v1/sessions?archived_only=true&include_archive=true',
|
||||
|
|
@ -489,6 +547,21 @@ describe('server-v2 /api/v1/sessions', () => {
|
|||
expect(body.code).toBe(40001);
|
||||
});
|
||||
|
||||
it('returns a terminal empty page when archived_only status filtering finds no match', async () => {
|
||||
const cwd = home as string;
|
||||
const first = await postJson<SessionWire>('/api/v1/sessions', { metadata: { cwd } });
|
||||
const second = await postJson<SessionWire>('/api/v1/sessions', { metadata: { cwd } });
|
||||
|
||||
await postJson<{ archived: boolean }>(`/api/v1/sessions/${first.body.data.id}:archive`);
|
||||
await postJson<{ archived: boolean }>(`/api/v1/sessions/${second.body.data.id}:archive`);
|
||||
|
||||
const page = await getJson<PageWire>(
|
||||
'/api/v1/sessions?archived_only=true&status=running&page_size=1',
|
||||
);
|
||||
expect(page.body.code).toBe(0);
|
||||
expect(page.body.data).toEqual({ items: [], has_more: false });
|
||||
});
|
||||
|
||||
it('rejects a malformed workspace_id when listing (40001)', async () => {
|
||||
const { body } = await getJson<null>('/api/v1/sessions?workspace_id=not-a-workspace-id');
|
||||
expect(body.code).toBe(40001);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
listSessionChildrenQuerySchema,
|
||||
listSessionChildrenResponseSchema,
|
||||
listSessionsQuerySchema,
|
||||
restoreSessionResponseSchema,
|
||||
sessionStatusResponseSchema,
|
||||
updateSessionProfileRequestSchema,
|
||||
updateSessionRequestSchema,
|
||||
|
|
@ -100,6 +101,15 @@ describe('listSessionsQuerySchema', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('parses archived_only to boolean', () => {
|
||||
expect(listSessionsQuerySchema.parse({ archived_only: 'true' })).toEqual({
|
||||
archived_only: true,
|
||||
});
|
||||
expect(listSessionsQuerySchema.parse({ archived_only: false })).toEqual({
|
||||
archived_only: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('parses exclude_empty to boolean', () => {
|
||||
expect(listSessionsQuerySchema.parse({ exclude_empty: 'true' })).toEqual({
|
||||
exclude_empty: true,
|
||||
|
|
@ -498,6 +508,36 @@ describe('archiveSessionResponseSchema', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('restoreSessionResponseSchema', () => {
|
||||
it('accepts a restored Session payload', () => {
|
||||
const parsed = restoreSessionResponseSchema.parse({
|
||||
id: 'sess_abc',
|
||||
workspace_id: 'wd_kimi_0123456789ab',
|
||||
title: 'Restored',
|
||||
created_at: '2026-01-01T00:00:00.000Z',
|
||||
updated_at: '2026-01-01T00:00:00.000Z',
|
||||
status: 'idle',
|
||||
archived: false,
|
||||
metadata: { cwd: '/tmp/foo' },
|
||||
agent_config: { model: '' },
|
||||
usage: {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_read_tokens: 0,
|
||||
cache_creation_tokens: 0,
|
||||
total_cost_usd: 0,
|
||||
context_tokens: 0,
|
||||
context_limit: 0,
|
||||
turn_count: 0,
|
||||
},
|
||||
permission_rules: [],
|
||||
message_count: 0,
|
||||
last_seq: 0,
|
||||
});
|
||||
expect(parsed.archived).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteSessionResponseSchema (deprecated alias)', () => {
|
||||
it('accepts the canonical { archived: true } shape', () => {
|
||||
expect(deleteSessionResponseSchema.parse({ archived: true })).toEqual({ archived: true });
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
* POST /v1/sessions/{id}:compact body: CompactSession data: {}
|
||||
* POST /v1/sessions/{id}:undo body: UndoSession data: UndoSession
|
||||
* POST /v1/sessions/{id}:archive - data: { archived: true }
|
||||
* POST /v1/sessions/{id}:restore - data: Session
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
|
@ -46,6 +47,7 @@ export const listSessionsQuerySchema = cursorQuerySchema.and(
|
|||
z.object({
|
||||
status: sessionStatusSchema.optional(),
|
||||
include_archive: booleanQueryParam,
|
||||
archived_only: booleanQueryParam,
|
||||
exclude_empty: booleanQueryParam,
|
||||
}),
|
||||
);
|
||||
|
|
@ -161,6 +163,9 @@ export const archiveSessionResponseSchema = z.object({
|
|||
});
|
||||
export type ArchiveSessionResponse = z.infer<typeof archiveSessionResponseSchema>;
|
||||
|
||||
export const restoreSessionResponseSchema = sessionSchema;
|
||||
export type RestoreSessionResponse = z.infer<typeof restoreSessionResponseSchema>;
|
||||
|
||||
/** @deprecated kept as an alias for backward compatibility; prefer archiveSessionResponseSchema. */
|
||||
export const deleteSessionResponseSchema = archiveSessionResponseSchema;
|
||||
/** @deprecated kept as an alias for backward compatibility; prefer ArchiveSessionResponse. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue