feat(apiproxy): session.rename RPC delegating to the session-title service

New unary method in RpcMethodMap with the title-invalid error code; the impl
resolves the agent (cold sessions resume first) and delegates to
ctx.sessionTitle.rename, returning the normalized title plus its event seq so
clients settle the title projection cell ahead of the push frame. session.fork
stays on the reserved-seam list.
This commit is contained in:
imccyu
2026-07-29 18:59:23 +08:00
parent abbcacf42a
commit 19336686a6
20 changed files with 187 additions and 11 deletions
+22
View File
@@ -36,6 +36,8 @@ import type {} from '@deepseek-ai/dsh-session-projection-cache'
import { GoalError } from '@deepseek-ai/dsh-goal'
import type { GoalRef as CoreGoalRef } from '@deepseek-ai/dsh-goal'
// Type-only edges: resolve `ctx.get('commands')`, the `commands/change` event, and `ctx.get('skills')`.
// Type-only edge: resolves `ctx.get('sessionTitle')` for the rename impl.
import type {} from '@deepseek-ai/dsh-session-title'
import type {} from '@deepseek-ai/dsh-commands'
import type {} from '@deepseek-ai/dsh-skill'
import type { CallId } from '@deepseek-ai/dsh-llm/brand'
@@ -1049,6 +1051,26 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
}
},
async rename(request) {
const { sessionId, title } = request.payload
const found = await agentFor(sessionId)
if ('error' in found) return err(request, found.error)
const titles = ctx.get('sessionTitle')
if (titles === undefined) {
return err(request, { code: 'internal', message: 'session-title service is absent: this deployment does not mount @deepseek-ai/dsh-session-title in its composition (cordis.yml or explicit assembly)', details: {} })
}
try {
const accepted = titles.rename(found.agent.session, title)
return ok(request, { title: accepted.title, seq: accepted.eventSeq })
} catch (error: unknown) {
return err(request, {
code: 'title-invalid',
message: `rename rejected for session "${sessionId}": ${String(error)}`,
details: { sessionId },
})
}
},
async prompt(request) {
const { sessionId, mode, content } = request.payload
const found = await agentFor(sessionId)
@@ -23,6 +23,7 @@ export interface RpcMethodMap {
'session.history': SessionsApi['history']
'session.models': SessionsApi['models']
'session.selectModel': SessionsApi['selectModel']
'session.rename': SessionsApi['rename']
'session.prompt': SessionsApi['prompt']
'session.cancel': SessionsApi['cancel']
'host.describe': HostApi['describe']
@@ -49,6 +49,7 @@ export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code',
z.object({ code: z.literal('agent-busy'), message: z.string(), details: z.object({ reason: z.string() }) }),
z.object({ code: z.literal('command-error'), message: z.string(), details: z.object({}) }),
z.object({ code: z.literal('unknown-command'), message: z.string(), details: z.object({}) }),
z.object({ code: z.literal('title-invalid'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
z.object({ code: z.literal('internal'), message: z.string(), details: z.object({}) }),
]) as unknown as z.ZodType<RpcError>
+1
View File
@@ -48,6 +48,7 @@ export interface RpcErrorDetailsMap {
'command-error': {}
/** A leading-/ prompt named no registered command; the message names the token. */
'unknown-command': {}
'title-invalid': { sessionId: SessionId }
'internal': {}
}
@@ -73,6 +73,18 @@ export const sessionCreateValueSchema = z.object({
sessionId: sessionIdSchema,
}) satisfies z.ZodType<Wire<ResponseValue<'session.create'>>>
/** session.rename request payload (raw title; host-side normalization decides acceptance). */
export const sessionRenameRequestSchema = z.object({
sessionId: sessionIdSchema,
title: z.string(),
}) satisfies z.ZodType<Wire<RequestPayload<'session.rename'>>>
/** session.rename response value (the normalized accepted title and its event seq). */
export const sessionRenameValueSchema = z.object({
title: z.string().min(1),
seq: z.number().int().nonnegative(),
}) satisfies z.ZodType<Wire<ResponseValue<'session.rename'>>>
/** session.history request payload (beforeSeq/maxMessages page backwards from the window tail). */
export const sessionHistoryRequestSchema = z.object({
sessionId: sessionIdSchema,
@@ -208,6 +208,16 @@ export interface SessionsApi {
}>):
Promise<RpcResponse<{ selected: ModelTarget }>>
/**
* Renames a session: appends a `session/title` event with the `user`
* source, which pins the title against automatic regeneration. The
* normalized accepted title and the title event's seq return so the caller
* can settle its projection cell without waiting for the push frame. A
* title that normalizes to empty fails with `title-invalid`.
*/
rename(request: RpcRequest<{ sessionId: SessionId; title: string }>):
Promise<RpcResponse<{ title: string; seq: number }>>
/**
* Sends a message. content is core's ContentBlock[] verbatim; mode maps 1:1 — queue→send, steer→steer.
* A prompt whose content is exactly one text block starting with '/' is a slash command: the host
@@ -20,6 +20,7 @@ import {
import {
sessionCancelValueSchema,
sessionCreateValueSchema,
sessionRenameValueSchema,
sessionHistoryValueSchema,
sessionListValueSchema,
sessionModelsValueSchema,
@@ -66,6 +67,7 @@ export interface IApiClient {
history(payload: RequestPayload<'session.history'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.history'>>>
models(payload: RequestPayload<'session.models'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.models'>>>
selectModel(payload: RequestPayload<'session.selectModel'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.selectModel'>>>
rename(payload: RequestPayload<'session.rename'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.rename'>>>
prompt(payload: RequestPayload<'session.prompt'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.prompt'>>>
cancel(payload: RequestPayload<'session.cancel'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.cancel'>>>
}
@@ -116,6 +118,7 @@ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseV
'session.history': sessionHistoryValueSchema,
'session.models': sessionModelsValueSchema,
'session.selectModel': sessionSelectModelValueSchema,
'session.rename': sessionRenameValueSchema,
'session.prompt': sessionPromptValueSchema,
'session.cancel': sessionCancelValueSchema,
'host.describe': hostDescribeValueSchema,
@@ -327,6 +330,7 @@ export abstract class AbstractApiClient implements IApiClient {
history: (payload, signal) => this.callUnary('session.history', payload, signal),
models: (payload, signal) => this.callUnary('session.models', payload, signal),
selectModel: (payload, signal) => this.callUnary('session.selectModel', payload, signal),
rename: (payload, signal) => this.callUnary('session.rename', payload, signal),
prompt: (payload, signal) => this.callUnary('session.prompt', payload, signal),
cancel: (payload, signal) => this.callUnary('session.cancel', payload, signal),
}
@@ -17,6 +17,7 @@ import { clientRequestSchema, clientResponseSchema } from '../api/rpc.schema.ts'
import {
sessionCancelRequestSchema,
sessionCreateRequestSchema,
sessionRenameRequestSchema,
sessionHistoryRequestSchema,
sessionListRequestSchema,
sessionModelsRequestSchema,
@@ -68,6 +69,7 @@ const UNARY_ROUTES: UnaryRoutes = {
'session.history': { schema: sessionHistoryRequestSchema, invoke: (api, r) => api.sessions.history(r) },
'session.models': { schema: sessionModelsRequestSchema, invoke: (api, r) => api.sessions.models(r) },
'session.selectModel': { schema: sessionSelectModelRequestSchema, invoke: (api, r) => api.sessions.selectModel(r) },
'session.rename': { schema: sessionRenameRequestSchema, invoke: (api, r) => api.sessions.rename(r) },
'session.prompt': { schema: sessionPromptRequestSchema, invoke: (api, r) => api.sessions.prompt(r) },
'session.cancel': { schema: sessionCancelRequestSchema, invoke: (api, r) => api.sessions.cancel(r) },
'host.describe': { schema: hostDescribeRequestSchema, invoke: (api, r) => api.host.describe(r) },