Skip to content

Conversation

@ammar-agent
Copy link
Collaborator

Problem

When InterruptStream (Ctrl+C) was called during a pending bash tool execution, the stream would hang indefinitely. This was especially problematic for SSH workspaces where long-running commands could block the UI for minutes.

Root Cause

The AI SDK's for await loop blocks waiting for the current async iterator operation to complete. When a bash tool is executing, the iterator doesn't yield until tool.execute() returns.

Even though we call abortController.abort(), the bash tool's abort listener was effectively empty:

abortListener = () => {
  if (!resolved) {
    // Runtime handles the actual cancellation  ← BUG: Does nothing!
    // We just need to clean up our side
  }
};

This caused:

  1. cancelStreamSafely() calls abortController.abort()
  2. Abort signal propagates but bash tool doesn't resolve
  3. AI SDK iterator stays blocked waiting for tool to return
  4. await streamInfo.processingPromise hangs indefinitely
  5. IPC call never returns, UI frozen

For SSH workspaces, the SSH runtime's abort handler only kills the local SSH client - the remote command keeps running, making this worse.

Solution

Make the bash tool actively resolve its promise when aborted instead of passively waiting:

abortListener = () => {
  if (!resolved) {
    // Immediately resolve with abort error to unblock AI SDK stream
    teardown();
    resolveOnce({
      success: false,
      error: "Command execution was aborted",
      exitCode: -2,
      wall_duration_ms: Math.round(performance.now() - startTime),
    });
  }
};

This unblocks the chain:

  • Tool promise resolves immediately with error
  • AI SDK iterator yields
  • Stream processing loop detects abort and exits
  • processingPromise resolves
  • IPC returns instantly

Testing

Added integration test that verifies interrupt completes in < 2 seconds even when a sleep 60 command is running:

test("should interrupt stream with pending bash tool call near-instantly", async () => {
  // Start sleep 60
  void sendMessage(workspaceId, "Run this bash command: sleep 60");
  await collector.waitForEvent("tool-call-start");
  
  // Measure interrupt time
  const start = performance.now();
  await interruptStream(workspaceId);
  const duration = performance.now() - start;
  
  expect(duration).toBeLessThan(2000); // Must be near-instant
});

Generated with cmux

When InterruptStream was called with a pending bash tool execution,
the stream would hang because:

1. AI SDK's async iterator blocks waiting for tool.execute() to complete
2. Bash tool's abort listener did nothing (just a comment saying runtime handles it)
3. cancelStreamSafely() waits for processingPromise which never resolves
4. IPC call hangs indefinitely

The fix makes the bash tool actively resolve its promise when aborted:
- Abort listener immediately calls teardown() and resolveOnce() with error
- This unblocks the AI SDK iterator
- Stream processing loop can detect abort and exit
- processingPromise resolves and IPC returns instantly

For SSH workspaces, this is critical because the SSH abort handler only
kills the local SSH client - it doesn't terminate the remote command.
By making the tool promise resolve immediately, we don't wait for the
remote process to finish.

Added integration test that verifies interrupt completes in < 2 seconds
even with a 'sleep 60' command running.
- Wait for stream-start instead of tool-call-start (more reliable)
- Add 2s delay to ensure tool is actually executing before interrupting
- Use clearer prompt to ensure bash tool is called
- Increase timeout to 25s to account for delay
@ammario ammario added this pull request to the merge queue Oct 29, 2025
Merged via the queue into main with commit cb523ed Oct 29, 2025
13 checks passed
@ammario ammario deleted the fix-interrupt-stream branch October 29, 2025 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants