fix(session-title): serialize provider request writes

This commit is contained in:
Tianyi Cui
2026-07-21 14:39:09 +08:00
parent b3ba4345f5
commit d9557fa5e0
11 changed files with 150 additions and 18 deletions

View File

@@ -8,7 +8,12 @@ import z from 'schemastery'
import type { Branded } from '@deepseek-ai/dsh-brand'
import { deepFreeze, isAgentLoopRequest } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import type {
OutOfBandSessionEventType,
Session,
SessionEvent,
SessionEventMap,
} from '@deepseek-ai/dsh-session'
import { fallbackSessionTitle, normalizeSessionTitle } from './normalize.ts'
export { fallbackSessionTitle, normalizeSessionTitle, truncateTitleUtf8 } from './normalize.ts'
@@ -95,6 +100,46 @@ declare module '@deepseek-ai/dsh-session' {
}
}
/** Per-session settlement tails for title-capability out-of-band writes. */
const SESSION_TITLE_WRITE_TAILS = new WeakMap<Session, Promise<void>>()
/** Convert either write outcome into a fulfilled queue tail. */
function settleSessionTitleWrite(): void {}
/**
* Serialize one title-capability out-of-band event with its session peers.
* Cancellation is checked when the write reaches the head of the queue; once
* the core append starts, its durability contract runs to completion.
* @param ctx - context exposing the live session store.
* @param session - exact live session that owns the title-capability event.
* @param type - plugin-declared log-only title event type.
* @param data - typed JSON payload for the event.
* @param signal - service or provider lifetime checked before publication starts.
* @returns the durably accepted event.
*/
export async function appendSessionTitleOutOfBand<T extends OutOfBandSessionEventType>(
ctx: Context,
session: Session,
type: T,
data: SessionEventMap[T],
signal: AbortSignal,
): Promise<SessionEvent<T>> {
const predecessor = SESSION_TITLE_WRITE_TAILS.get(session)
const run = Promise.resolve(predecessor).then(() => {
signal.throwIfAborted()
return ctx.sessions.appendOutOfBand(session, type, data, { kind: 'session-title' })
})
const tail = run.then(settleSessionTitleWrite, settleSessionTitleWrite)
SESSION_TITLE_WRITE_TAILS.set(session, tail)
try {
return await run
} finally {
if (SESSION_TITLE_WRITE_TAILS.get(session) === tail) {
SESSION_TITLE_WRITE_TAILS.delete(session)
}
}
}
/** One eligible human text message exposed to title providers. */
export interface SessionTitleUserMessage {
/** Source `user/message` event seq. */
@@ -481,7 +526,7 @@ export class SessionTitleService extends Service {
})
this.assertCurrent(session, work)
const accepted = this.validateResult(result, messages)
await this.ctx.sessions.appendOutOfBand(session, 'session/title', {
await appendSessionTitleOutOfBand(this.ctx, session, 'session/title', {
title: accepted.title,
messageSeqs: [...accepted.messageSeqs],
source: {
@@ -489,7 +534,7 @@ export class SessionTitleService extends Service {
provider: work.registration.provider.id,
...accepted.model === undefined ? {} : { model: accepted.model },
},
}, { kind: 'session-title' })
}, work.signal)
return this.get(session)
} finally {
const state = this.work.get(session)
@@ -662,11 +707,11 @@ export class SessionTitleService extends Service {
this.config.fallbackMaxBytes,
)
if (title.length === 0) return undefined
await this.ctx.sessions.appendOutOfBand(session, 'session/title', {
await appendSessionTitleOutOfBand(this.ctx, session, 'session/title', {
title,
messageSeqs: [first.seq],
source: { kind: 'fallback' },
}, { kind: 'session-title' })
}, this.lifetime.signal)
return this.get(session)
}
}