feat: add message queue for sequential run processing (#501)
Allow sending multiple messages while a run is active. Messages are queued on the server and processed sequentially after each run completes. Each completed assistant message triggers speech playback independently, and the UI shows queue status with a badge indicator. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ export interface StartRunRequest {
|
||||
instructions?: string
|
||||
session_id?: string
|
||||
model?: string
|
||||
queue_id?: string
|
||||
}
|
||||
|
||||
export interface StartRunResponse {
|
||||
@@ -45,6 +46,8 @@ export interface RunEvent {
|
||||
}
|
||||
/** session_id tag added by server for client-side filtering */
|
||||
session_id?: string
|
||||
/** Queue length from run.queued event */
|
||||
queue_length?: number
|
||||
}
|
||||
|
||||
// ============================
|
||||
@@ -73,6 +76,7 @@ const sessionEventHandlers = new Map<string, {
|
||||
onAbortStarted: (event: RunEvent) => void
|
||||
onAbortCompleted: (event: RunEvent) => void
|
||||
onUsageUpdated: (event: RunEvent) => void
|
||||
onRunQueued?: (event: RunEvent) => void
|
||||
}>()
|
||||
|
||||
/**
|
||||
@@ -179,7 +183,8 @@ function globalRunCompletedHandler(event: RunEvent): void {
|
||||
handlers.onRunCompleted(event)
|
||||
}
|
||||
|
||||
// Auto-cleanup session handlers on completion
|
||||
// Auto-cleanup session handlers on completion (skip if more runs queued)
|
||||
if ((event as any).queue_remaining > 0) return
|
||||
sessionEventHandlers.delete(sid)
|
||||
}
|
||||
|
||||
@@ -195,10 +200,24 @@ function globalRunFailedHandler(event: RunEvent): void {
|
||||
handlers.onRunFailed(event)
|
||||
}
|
||||
|
||||
// Auto-cleanup session handlers on failure
|
||||
// Auto-cleanup session handlers on failure (skip if more runs queued)
|
||||
if ((event as any).queue_remaining > 0) return
|
||||
sessionEventHandlers.delete(sid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Global run.queued event handler
|
||||
*/
|
||||
function globalRunQueuedHandler(event: RunEvent): void {
|
||||
const sid = event.session_id
|
||||
if (!sid) return
|
||||
|
||||
const handlers = sessionEventHandlers.get(sid)
|
||||
if (handlers?.onRunQueued) {
|
||||
handlers.onRunQueued(event)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global compression.started event handler
|
||||
*/
|
||||
@@ -250,6 +269,9 @@ function globalAbortCompletedHandler(event: RunEvent): void {
|
||||
handlers.onAbortCompleted(event)
|
||||
}
|
||||
|
||||
// If abort completion is followed by queued runs, keep the handler alive so
|
||||
// the next run.started/message.delta/run.completed events are still received.
|
||||
if ((event as any).queue_length > 0) return
|
||||
sessionEventHandlers.delete(sid)
|
||||
}
|
||||
|
||||
@@ -289,6 +311,7 @@ export function registerSessionHandlers(
|
||||
onAbortStarted: (event: RunEvent) => void
|
||||
onAbortCompleted: (event: RunEvent) => void
|
||||
onUsageUpdated: (event: RunEvent) => void
|
||||
onRunQueued?: (event: RunEvent) => void
|
||||
}
|
||||
): () => void {
|
||||
sessionEventHandlers.set(sessionId, handlers)
|
||||
@@ -361,6 +384,7 @@ export function connectChatRun(): Socket {
|
||||
chatRunSocket.on('run.started', globalRunStartedHandler)
|
||||
chatRunSocket.on('run.failed', globalRunFailedHandler)
|
||||
chatRunSocket.on('run.completed', globalRunCompletedHandler)
|
||||
chatRunSocket.on('run.queued', globalRunQueuedHandler)
|
||||
|
||||
// Compression events
|
||||
chatRunSocket.on('compression.started', globalCompressionStartedHandler)
|
||||
@@ -395,7 +419,7 @@ export function disconnectChatRun(): void {
|
||||
*/
|
||||
export function resumeSession(
|
||||
sessionId: string,
|
||||
onResumed: (data: { session_id: string; messages: any[]; isWorking: boolean; isAborting?: boolean; events: any[]; inputTokens?: number; outputTokens?: number }) => void,
|
||||
onResumed: (data: { session_id: string; messages: any[]; isWorking: boolean; isAborting?: boolean; events: any[]; inputTokens?: number; outputTokens?: number; queueLength?: number }) => void,
|
||||
): Socket {
|
||||
const socket = connectChatRun()
|
||||
|
||||
@@ -418,6 +442,18 @@ export function startRunViaSocket(
|
||||
}
|
||||
|
||||
let closed = false
|
||||
const socket = connectChatRun()
|
||||
|
||||
if (sessionEventHandlers.has(sid)) {
|
||||
socket.emit('run', body)
|
||||
return {
|
||||
abort: () => {
|
||||
if (!closed) {
|
||||
socket.emit('abort', { session_id: sid })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Define event handlers for this session
|
||||
const handlers = {
|
||||
@@ -453,12 +489,14 @@ export function startRunViaSocket(
|
||||
onRunCompleted: (evt: RunEvent) => {
|
||||
if (closed) return
|
||||
onEvent(evt)
|
||||
if ((evt as any).queue_remaining > 0) return
|
||||
closed = true
|
||||
onDone()
|
||||
},
|
||||
onRunFailed: (evt: RunEvent) => {
|
||||
if (closed) return
|
||||
onEvent(evt)
|
||||
if ((evt as any).queue_remaining > 0) return
|
||||
closed = true
|
||||
onError(new Error(evt.error || 'Run failed'))
|
||||
},
|
||||
@@ -477,6 +515,7 @@ export function startRunViaSocket(
|
||||
onAbortCompleted: (evt: RunEvent) => {
|
||||
if (closed) return
|
||||
onEvent(evt)
|
||||
if ((evt as any).queue_length > 0) return
|
||||
closed = true
|
||||
onDone()
|
||||
},
|
||||
@@ -484,13 +523,16 @@ export function startRunViaSocket(
|
||||
if (closed) return
|
||||
onEvent(evt)
|
||||
},
|
||||
onRunQueued: (evt: RunEvent) => {
|
||||
if (closed) return
|
||||
onEvent(evt)
|
||||
},
|
||||
}
|
||||
|
||||
// Register handlers in the global session map
|
||||
sessionEventHandlers.set(sid, handlers)
|
||||
|
||||
// Emit run request
|
||||
const socket = connectChatRun()
|
||||
socket.emit('run', body)
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user