feat: title projection unit in dsh-session-title (key 'title', last-wins over session/title)

This commit is contained in:
imccyu
2026-07-27 21:57:02 +08:00
parent 7bf9051b94
commit 1097330df2
5 changed files with 118 additions and 1 deletions

View File

@@ -5,6 +5,7 @@
import { Context, FiberState, Service, type Fiber } from 'cordis'
import z from 'schemastery'
import { z as zod } from 'zod'
import type { Branded } from '@deepseek-ai/dsh-brand'
import { deepFreeze, isAgentLoopRequest } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
@@ -14,6 +15,8 @@ import type {
SessionEvent,
SessionEventMap,
} from '@deepseek-ai/dsh-session'
// Type-only: resolves ctx.sessionProjections for the optional unit child.
import type {} from '@deepseek-ai/dsh-session-projection'
import { fallbackSessionTitle, normalizeSessionTitle } from './normalize.ts'
export { fallbackSessionTitle, normalizeSessionTitle, truncateTitleUtf8 } from './normalize.ts'
@@ -100,6 +103,17 @@ declare module '@deepseek-ai/dsh-session' {
}
}
declare module '@deepseek-ai/dsh-session-projection/types' {
interface SessionProjectionMap {
/**
* The session's current normalized title — the latest `session/title`
* event's text (last-wins), or `null` before the first title lands. A
* plain string: the shape the client list rows consume.
*/
title: string | null
}
}
/** Per-session settlement tails for title-capability out-of-band writes. */
const SESSION_TITLE_WRITE_TAILS = new WeakMap<Session, Promise<void>>()
@@ -323,6 +337,21 @@ export class SessionTitleService extends Service {
this.work.clear()
}, 'sessionTitle lifecycle')
// The title projection unit: pure last-wins fold of session/title events
// (the same events foldSessionTitle consumes), serving the plain title
// string clients list rows read. The unit child activates only when a
// projection registry is composed (headless assemblies stay unaffected).
ctx.inject(['sessionProjections'], (projectionCtx) => {
projectionCtx.sessionProjections.register<'title', string | null>({
key: 'title',
schema: zod.union([zod.string().min(1), zod.null()]),
init: () => null,
apply: (state, event) => (event.type === 'session/title' ? event.data.title : state),
view: state => state,
stateVersion: 1,
})
})
ctx.on('session/event', (session, event) => {
switch (event.type) {
case 'user/message':