diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index 8ed681e239..4f5336323c 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -36,10 +36,10 @@ 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' +// Value edge: the rename impl narrows the title service's validation failure; the import also resolves `ctx.get('sessionTitle')`. +import { SessionTitleInvalidError } from '@deepseek-ai/dsh-session-title' import type { CallId } from '@deepseek-ai/dsh-llm/brand' import type { ApprovalOutcome, ApprovalRequestId } from '@deepseek-ai/dsh-user-approval' // Side-effect type import: resolves the `approval/request` waterfall and @@ -1057,16 +1057,26 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro 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: {} }) + return err(request, { code: 'internal', message: 'renaming is unavailable: this deployment mounts no session-title service', details: {} }) } try { const accepted = titles.rename(found.agent.session, title) return ok(request, { title: accepted.title, seq: accepted.eventSeq }) } catch (error: unknown) { + // Only the input's fault maps to title-invalid (the message is + // product-user-visible in the rename dialog); liveness and disposal + // races are deployment trouble, not a bad title. + if (error instanceof SessionTitleInvalidError) { + return err(request, { + code: 'title-invalid', + message: error.message, + details: { sessionId }, + }) + } return err(request, { - code: 'title-invalid', - message: `rename rejected for session "${sessionId}": ${String(error)}`, - details: { sessionId }, + code: 'internal', + message: `failed to rename session "${sessionId}": ${String(error)}`, + details: {}, }) } }, diff --git a/packages/host/apiproxy/src/fetch/client.ts b/packages/host/apiproxy/src/fetch/client.ts index 5940775499..5b81d1c4ea 100644 --- a/packages/host/apiproxy/src/fetch/client.ts +++ b/packages/host/apiproxy/src/fetch/client.ts @@ -20,11 +20,11 @@ import { import { sessionCancelValueSchema, sessionCreateValueSchema, - sessionRenameValueSchema, sessionHistoryValueSchema, sessionListValueSchema, sessionModelsValueSchema, sessionPromptValueSchema, + sessionRenameValueSchema, sessionSelectModelValueSchema, } from '../api/sessions.schema.ts' import { diff --git a/packages/host/apiproxy/src/fetch/handler.ts b/packages/host/apiproxy/src/fetch/handler.ts index c14e680119..46e3703f3a 100644 --- a/packages/host/apiproxy/src/fetch/handler.ts +++ b/packages/host/apiproxy/src/fetch/handler.ts @@ -17,11 +17,11 @@ import { clientRequestSchema, clientResponseSchema } from '../api/rpc.schema.ts' import { sessionCancelRequestSchema, sessionCreateRequestSchema, - sessionRenameRequestSchema, sessionHistoryRequestSchema, sessionListRequestSchema, sessionModelsRequestSchema, sessionPromptRequestSchema, + sessionRenameRequestSchema, sessionSelectModelRequestSchema, } from '../api/sessions.schema.ts' import { diff --git a/packages/host/apiproxy/tests/api-proxy-rename.spec.ts b/packages/host/apiproxy/tests/api-proxy-rename.spec.ts index 5bdac4fb11..0e3f0ffe18 100644 --- a/packages/host/apiproxy/tests/api-proxy-rename.spec.ts +++ b/packages/host/apiproxy/tests/api-proxy-rename.spec.ts @@ -2,7 +2,9 @@ * sessions.rename delegation through the composed SessionTitleService. The * agent factory is a structural stub whose createAgent forwards seed/meta into * the real SessionStore, and whose resume never runs (every source here is - * already attached). + * already attached). Cold-session resolution is the shared `agentFor` path — + * api-proxy-cold.spec.ts owns the resume evidence for every unary that rides + * it, rename included. */ import { describe, expect, it } from 'vitest' @@ -82,20 +84,37 @@ describe('sessions.rename', () => { expect(event?.data).toMatchObject({ title: 'new name', source: { kind: 'user' } }) }) - it('maps an empty-normalizing title to title-invalid', async () => { + it('maps only an empty-normalizing title to title-invalid, with a presentable message', async () => { const ctx = await composed() const source = liveAgent(ctx, 'session-rename-bad', 1) - const response = await api(ctx).sessions.rename(request({ sessionId: source.id, title: ' ' })) + // U+200B passes a client-side trim gate but normalizes to empty host-side. + const response = await api(ctx).sessions.rename(request({ sessionId: source.id, title: ' ​ ' })) expect(response.result.ok).toBe(false) if (!response.result.ok) { expect(response.result.error).toMatchObject({ code: 'title-invalid', details: { sessionId: source.id }, }) + // The message renders verbatim in the rename dialog's alert. + expect(response.result.error.message).toBe('session title must contain visible characters') } }) + it('maps a non-validation rename failure (stale session object) to internal, not title-invalid', async () => { + const ctx = await composed() + // The registered agent holds a session object from another store: the + // title service's liveness check throws a plain Error, which must not + // read as the user's fault. + const foreign = await composed(false) + const stale = liveAgent(foreign, 'session-rename-stale', 1) + ctx.agents.register({ id: stale.id, session: stale, status: 'idle', ctx } as Agent) + + const response = await api(ctx).sessions.rename(request({ sessionId: stale.id, title: 'name' })) + expect(response.result.ok).toBe(false) + if (!response.result.ok) expect(response.result.error.code).toBe('internal') + }) + it('answers internal when the composition mounts no session-title service', async () => { const ctx = await composed(false) const source = liveAgent(ctx, 'session-no-titles', 1) @@ -104,7 +123,7 @@ describe('sessions.rename', () => { expect(response.result.ok).toBe(false) if (!response.result.ok) { expect(response.result.error.code).toBe('internal') - expect(response.result.error.message).toMatch(/session-title service is absent/) + expect(response.result.error.message).toMatch(/mounts no session-title service/) } }) }) diff --git a/packages/host/apiproxy/tests/rpc-schemas.spec.ts b/packages/host/apiproxy/tests/rpc-schemas.spec.ts index fd1f107a80..fe2780eadc 100644 --- a/packages/host/apiproxy/tests/rpc-schemas.spec.ts +++ b/packages/host/apiproxy/tests/rpc-schemas.spec.ts @@ -70,11 +70,13 @@ describe('rpcErrorSchema', () => { expect(rpcErrorSchema.parse({ code: 'agent-busy', message: 'm', details: { reason: 'r' } }).code).toBe('agent-busy') expect(rpcErrorSchema.parse({ code: 'command-error', message: 'm', details: {} }).code).toBe('command-error') expect(rpcErrorSchema.parse({ code: 'unknown-command', message: 'm', details: {} }).code).toBe('unknown-command') + expect(rpcErrorSchema.parse({ code: 'title-invalid', message: 'm', details: { sessionId: 's' } }).code).toBe('title-invalid') expect(rpcErrorSchema.parse({ code: 'internal', message: 'm', details: {} }).code).toBe('internal') }) it('rejects a known code with missing details', () => { expect(() => rpcErrorSchema.parse({ code: 'agent-busy', message: 'm', details: {} })).toThrow() + expect(() => rpcErrorSchema.parse({ code: 'title-invalid', message: 'm', details: {} })).toThrow() expect(() => rpcErrorSchema.parse({ code: 'command-error', message: 'm' })).toThrow() expect(() => rpcErrorSchema.parse({ code: 'nope', message: 'm', details: {} })).toThrow() }) diff --git a/packages/host/apiproxy/tsconfig.json b/packages/host/apiproxy/tsconfig.json index 908e2cb546..5185081283 100644 --- a/packages/host/apiproxy/tsconfig.json +++ b/packages/host/apiproxy/tsconfig.json @@ -39,10 +39,10 @@ "path": "../../session-projection/session-projection" }, { - "path": "../../session-title/session-title" + "path": "../../session-projection/session-projection-cache" }, { - "path": "../../session-projection/session-projection-cache" + "path": "../../session-title/session-title" }, { "path": "../../skill/skill"