From e3d28f465972212dc282d8755f991fde1beeded4 Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Tue, 5 May 2026 13:03:14 +0800 Subject: [PATCH] [codex] Handle chat run abort lifecycle (#454) * feat: call upstream stop API when aborting a run - Modified handleAbort to call POST /v1/runs/{run_id}/stop endpoint - Use profile-specific upstream URL and API key from gatewayManager - Add 5-second timeout with error handling and logging - Keep local abortController.abort() for EventSource cleanup - Change handleAbort to async method and update call site This ensures the upstream Hermes gateway is properly notified when a user aborts a run, allowing graceful termination. Co-Authored-By: Claude Sonnet 4.6 * fix: close ChatRunSocket connections on shutdown to prevent hanging - Add close() method to ChatRunSocket to abort all active runs and clear session state - Pass chatRunServer to bindShutdown and close it before groupChatServer during shutdown - This prevents EventSource connections and abort controllers from keeping the process alive during nodemon restart Fixes the "still waiting for sub-process to finish" issue. Co-Authored-By: Claude Sonnet 4.6 * Handle chat run abort lifecycle --------- Co-authored-by: Claude Sonnet 4.6 --- packages/client/src/api/hermes/chat.ts | 48 ++- .../src/components/hermes/chat/ChatInput.vue | 1 + .../hermes/chat/HistoryMessageList.vue | 44 +- .../components/hermes/chat/MessageList.vue | 44 +- packages/client/src/stores/hermes/chat.ts | 207 ++++----- packages/server/src/index.ts | 5 +- .../src/services/hermes/chat-run-socket.ts | 398 ++++++++++++------ packages/server/src/services/shutdown.ts | 8 +- 8 files changed, 524 insertions(+), 231 deletions(-) diff --git a/packages/client/src/api/hermes/chat.ts b/packages/client/src/api/hermes/chat.ts index 6c1199c..a4c653d 100644 --- a/packages/client/src/api/hermes/chat.ts +++ b/packages/client/src/api/hermes/chat.ts @@ -70,6 +70,8 @@ const sessionEventHandlers = new Map void onCompressionStarted: (event: RunEvent) => void onCompressionCompleted: (event: RunEvent) => void + onAbortStarted: (event: RunEvent) => void + onAbortCompleted: (event: RunEvent) => void onUsageUpdated: (event: RunEvent) => void }>() @@ -223,6 +225,34 @@ function globalCompressionCompletedHandler(event: RunEvent): void { } } +/** + * Global abort.started event handler + */ +function globalAbortStartedHandler(event: RunEvent): void { + const sid = event.session_id + if (!sid) return + + const handlers = sessionEventHandlers.get(sid) + if (handlers?.onAbortStarted) { + handlers.onAbortStarted(event) + } +} + +/** + * Global abort.completed event handler + */ +function globalAbortCompletedHandler(event: RunEvent): void { + const sid = event.session_id + if (!sid) return + + const handlers = sessionEventHandlers.get(sid) + if (handlers?.onAbortCompleted) { + handlers.onAbortCompleted(event) + } + + sessionEventHandlers.delete(sid) +} + /** * Global usage.updated event handler */ @@ -256,6 +286,8 @@ export function registerSessionHandlers( onRunFailed: (event: RunEvent) => void onCompressionStarted: (event: RunEvent) => void onCompressionCompleted: (event: RunEvent) => void + onAbortStarted: (event: RunEvent) => void + onAbortCompleted: (event: RunEvent) => void onUsageUpdated: (event: RunEvent) => void } ): () => void { @@ -333,6 +365,8 @@ export function connectChatRun(): Socket { // Compression events chatRunSocket.on('compression.started', globalCompressionStartedHandler) chatRunSocket.on('compression.completed', globalCompressionCompletedHandler) + chatRunSocket.on('abort.started', globalAbortStartedHandler) + chatRunSocket.on('abort.completed', globalAbortCompletedHandler) // Usage events chatRunSocket.on('usage.updated', globalUsageUpdatedHandler) @@ -361,7 +395,7 @@ export function disconnectChatRun(): void { */ export function resumeSession( sessionId: string, - onResumed: (data: { session_id: string; messages: any[]; isWorking: 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 }) => void, ): Socket { const socket = connectChatRun() @@ -436,6 +470,16 @@ export function startRunViaSocket( if (closed) return onEvent(evt) }, + onAbortStarted: (evt: RunEvent) => { + if (closed) return + onEvent(evt) + }, + onAbortCompleted: (evt: RunEvent) => { + if (closed) return + onEvent(evt) + closed = true + onDone() + }, onUsageUpdated: (evt: RunEvent) => { if (closed) return onEvent(evt) @@ -452,8 +496,6 @@ export function startRunViaSocket( return { abort: () => { if (!closed) { - closed = true - sessionEventHandlers.delete(sid) socket.emit('abort', { session_id: sid }) } }, diff --git a/packages/client/src/components/hermes/chat/ChatInput.vue b/packages/client/src/components/hermes/chat/ChatInput.vue index 4a020bc..a052fd4 100644 --- a/packages/client/src/components/hermes/chat/ChatInput.vue +++ b/packages/client/src/components/hermes/chat/ChatInput.vue @@ -317,6 +317,7 @@ function isImage(type: string): boolean { v-if="chatStore.isStreaming" size="small" type="error" + :disabled="chatStore.isAborting" @click="chatStore.stopStreaming()" > {{ t('chat.stop') }} diff --git a/packages/client/src/components/hermes/chat/HistoryMessageList.vue b/packages/client/src/components/hermes/chat/HistoryMessageList.vue index 83531ba..034ac97 100644 --- a/packages/client/src/components/hermes/chat/HistoryMessageList.vue +++ b/packages/client/src/components/hermes/chat/HistoryMessageList.vue @@ -145,7 +145,7 @@ watch( :highlight="chatStore.focusMessageId === msg.id" /> -
+