fix(server): accept client messageID to enable optimistic message dedup

This commit is contained in:
DoSun 2026-05-11 15:30:37 +08:00
parent d18a2af37d
commit 922bcf7660
2 changed files with 12 additions and 5 deletions

View File

@ -27,6 +27,7 @@ function ssePrompt(
content: string, content: string,
c: import('hono').Context, c: import('hono').Context,
parts?: Array<Record<string, unknown>>, parts?: Array<Record<string, unknown>>,
messageID?: string,
) { ) {
return streamSSE(c, async stream => { return streamSSE(c, async stream => {
const startTime = Date.now() const startTime = Date.now()
@ -82,7 +83,7 @@ function ssePrompt(
}) })
try { try {
await handle.prompt(content, { parts }) await handle.prompt(content, { parts, messageID })
} catch { } catch {
if (!resultWritten) { if (!resultWritten) {
pendingWrite = pendingWrite.then(() => pendingWrite = pendingWrite.then(() =>
@ -374,6 +375,7 @@ export function createSessionRoutes(
images?: unknown[] images?: unknown[]
model?: { providerID?: string; modelID?: string } model?: { providerID?: string; modelID?: string }
agent?: string agent?: string
messageID?: string
}>() }>()
const textContent = body.content ?? body.parts const textContent = body.content ?? body.parts
@ -396,7 +398,7 @@ export function createSessionRoutes(
try { await handle.setAgent(effectiveAgent) } catch {} try { await handle.setAgent(effectiveAgent) } catch {}
} }
return ssePrompt(handle, id, content, c, body.parts) return ssePrompt(handle, id, content, c, body.parts, body.messageID)
}) })
.post('/session/:sessionID/prompt_async', async c => { .post('/session/:sessionID/prompt_async', async c => {
const id = c.req.param('sessionID') const id = c.req.param('sessionID')
@ -427,6 +429,7 @@ export function createSessionRoutes(
images?: unknown[] images?: unknown[]
model?: { providerID?: string; modelID?: string } model?: { providerID?: string; modelID?: string }
agent?: string agent?: string
messageID?: string
}>() }>()
const textContent = body.content ?? body.parts const textContent = body.content ?? body.parts
@ -457,7 +460,8 @@ export function createSessionRoutes(
if (effectiveAgent && effectiveAgent !== handle.agent) { if (effectiveAgent && effectiveAgent !== handle.agent) {
try { await handle.setAgent(effectiveAgent) } catch {} try { await handle.setAgent(effectiveAgent) } catch {}
} }
handle.prompt(content, { parts: body.parts }).catch(() => {}) handle.prompt(content, { parts: body.parts, messageID: body.messageID }).catch(() => {})
} catch {} } catch {}
})() })()

View File

@ -468,7 +468,7 @@ export class SessionHandle {
return (response.mcpServers as unknown[]) ?? [] return (response.mcpServers as unknown[]) ?? []
} }
async prompt(content: string, _opts?: { parts?: Array<Record<string, unknown>> }): Promise<{ done: boolean; error?: string }> { async prompt(content: string, opts?: { parts?: Array<Record<string, unknown>>; messageID?: string }): Promise<{ done: boolean; error?: string }> {
if (this._status === 'stopped') { if (this._status === 'stopped') {
throw new Error( throw new Error(
`Session ${this.sessionId} is stopped`, `Session ${this.sessionId} is stopped`,
@ -487,10 +487,12 @@ export class SessionHandle {
this.emitBusyStatus() this.emitBusyStatus()
this.lastActiveAt = Date.now() this.lastActiveAt = Date.now()
const uuid = opts?.messageID ?? crypto.randomUUID()
const userMsg = jsonStringify({ const userMsg = jsonStringify({
type: 'user', type: 'user',
content, content,
uuid: crypto.randomUUID(), uuid,
session_id: this.sessionId, session_id: this.sessionId,
message: { role: 'user', content }, message: { role: 'user', content },
parent_tool_use_id: null, parent_tool_use_id: null,
@ -499,6 +501,7 @@ export class SessionHandle {
this.emitEvent('message', { this.emitEvent('message', {
type: 'user', type: 'user',
content, content,
uuid,
session_id: this.sessionId, session_id: this.sessionId,
model: this._model ?? '', model: this._model ?? '',
provider_id: this._providerId ?? '', provider_id: this._providerId ?? '',