소스 검색

fix(apiproxy): title-invalid only for the input's fault, presentable messages

The rename impl narrows on SessionTitleInvalidError: only an
empty-normalizing title maps to title-invalid (its message renders verbatim
in the rename dialog alert), while liveness/disposal races fall to internal;
the absent-service message trims to one presentable sentence. rpc-schemas
gains the title-invalid accept/missing-details lines; cosmetic ordering
(type-only import comment, tsconfig reference, schema import order) restored.
imccyu 2 달 전
부모
커밋
1c3fa6edcd

+ 16 - 6
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: {},
           })
         }
       },

+ 1 - 1
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 {

+ 1 - 1
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 {

+ 23 - 4
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/)
     }
   })
 })

+ 2 - 0
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()
   })

+ 2 - 2
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"