Skip to content

Commit d3cb757

Browse files
committed
Abort defense
1 parent 8d14a51 commit d3cb757

File tree

3 files changed

+74
-8
lines changed

3 files changed

+74
-8
lines changed

apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,33 @@ import { executeToolAndReport, waitForToolCompletion, waitForToolDecision } from
2222

2323
const logger = createLogger('CopilotSseHandlers')
2424

25+
/**
26+
* When the Sim→Go stream is aborted, avoid starting server-side tool work and
27+
* unblock the Go async waiter with a terminal 499 completion.
28+
*/
29+
function abortPendingToolIfStreamDead(
30+
toolCall: ToolCallState,
31+
toolCallId: string,
32+
options: OrchestratorOptions,
33+
context: StreamingContext
34+
): boolean {
35+
if (!options.abortSignal?.aborted && !context.wasAborted) {
36+
return false
37+
}
38+
toolCall.status = 'cancelled'
39+
toolCall.endTime = Date.now()
40+
markToolResultSeen(toolCallId)
41+
markToolComplete(toolCall.id, toolCall.name, 499, 'Request aborted before tool execution', {
42+
cancelled: true,
43+
}).catch((err) => {
44+
logger.error('markToolComplete fire-and-forget failed (stream aborted)', {
45+
toolCallId: toolCall.id,
46+
error: err instanceof Error ? err.message : String(err),
47+
})
48+
})
49+
return true
50+
}
51+
2552
/**
2653
* Extract the `ui` object from a Go SSE event. The Go backend enriches
2754
* tool_call events with `ui: { requiresConfirmation, clientExecutable, ... }`.
@@ -321,7 +348,9 @@ export const sseHandlers: Record<string, SSEHandler> = {
321348

322349
if (options.interactive === false) {
323350
if (options.autoExecuteTools !== false) {
324-
fireToolExecution()
351+
if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) {
352+
fireToolExecution()
353+
}
325354
}
326355
return
327356
}
@@ -345,7 +374,9 @@ export const sseHandlers: Record<string, SSEHandler> = {
345374
await emitSyntheticToolResult(toolCallId, toolCall.name, completion, options)
346375
return
347376
}
348-
fireToolExecution()
377+
if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) {
378+
fireToolExecution()
379+
}
349380
return
350381
}
351382

@@ -406,7 +437,9 @@ export const sseHandlers: Record<string, SSEHandler> = {
406437
// delegate to the client (React UI) and wait for completion.
407438
if (clientExecutable) {
408439
if (isToolAvailableOnSimSide(toolName)) {
409-
fireToolExecution()
440+
if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) {
441+
fireToolExecution()
442+
}
410443
} else {
411444
toolCall.status = 'executing'
412445
const completion = await waitForToolCompletion(
@@ -421,7 +454,9 @@ export const sseHandlers: Record<string, SSEHandler> = {
421454
}
422455

423456
if (options.autoExecuteTools !== false) {
424-
fireToolExecution()
457+
if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) {
458+
fireToolExecution()
459+
}
425460
}
426461
},
427462
reasoning: (event, context) => {
@@ -575,7 +610,9 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
575610

576611
if (options.interactive === false) {
577612
if (options.autoExecuteTools !== false) {
578-
fireToolExecution()
613+
if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) {
614+
fireToolExecution()
615+
}
579616
}
580617
return
581618
}
@@ -598,7 +635,9 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
598635
await emitSyntheticToolResult(toolCallId, toolCall.name, completion, options)
599636
return
600637
}
601-
fireToolExecution()
638+
if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) {
639+
fireToolExecution()
640+
}
602641
return
603642
}
604643
if (decision?.status === 'rejected' || decision?.status === 'error') {
@@ -655,7 +694,9 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
655694

656695
if (clientExecutable) {
657696
if (isToolAvailableOnSimSide(toolName)) {
658-
fireToolExecution()
697+
if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) {
698+
fireToolExecution()
699+
}
659700
} else {
660701
toolCall.status = 'executing'
661702
const completion = await waitForToolCompletion(
@@ -670,7 +711,9 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
670711
}
671712

672713
if (options.autoExecuteTools !== false) {
673-
fireToolExecution()
714+
if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) {
715+
fireToolExecution()
716+
}
674717
}
675718
},
676719
tool_result: (event, context) => {

apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,25 @@ export async function executeToolAndReport(
433433
if (toolCall.status === 'executing') return
434434
if (wasToolResultSeen(toolCall.id)) return
435435

436+
if (options?.abortSignal?.aborted || context.wasAborted) {
437+
toolCall.status = 'cancelled'
438+
toolCall.endTime = Date.now()
439+
markToolResultSeen(toolCall.id)
440+
markToolComplete(
441+
toolCall.id,
442+
toolCall.name,
443+
499,
444+
'Request aborted before tool execution',
445+
{ cancelled: true }
446+
).catch((err) => {
447+
logger.error('markToolComplete failed (aborted before execute)', {
448+
toolCallId: toolCall.id,
449+
error: err instanceof Error ? err.message : String(err),
450+
})
451+
})
452+
return
453+
}
454+
436455
toolCall.status = 'executing'
437456

438457
logger.info('Tool execution started', {

apps/sim/lib/copilot/orchestrator/sse/parser.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export async function* parseSSEStream(
2828
buffer = lines.pop() || ''
2929

3030
for (const line of lines) {
31+
if (abortSignal?.aborted) {
32+
logger.info('SSE stream aborted mid-chunk (between events)')
33+
return
34+
}
3135
if (!line.trim()) continue
3236
if (!line.startsWith('data: ')) continue
3337

0 commit comments

Comments
 (0)