[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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* Handle chat run abort lifecycle

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-05-05 13:03:14 +08:00
committed by GitHub
parent f13ce3a080
commit e3d28f4659
8 changed files with 524 additions and 231 deletions
+45 -3
View File
@@ -70,6 +70,8 @@ const sessionEventHandlers = new Map<string, {
onRunFailed: (event: RunEvent) => 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 })
}
},