Skip to content

Conversation

@ethanndickson
Copy link
Member

Overview

Implement soft-interrupts that allow graceful stream termination at content block boundaries instead of immediately aborting mid-content. When the user presses Escape the first time, the UI shows "⏸️ Interrupting..." and the stream completes at the next block boundary. Second Escape triggers immediate hard abort.

Architecture

Flow

  1. First Escape press → Call interruptStream() IPC, backend sets softInterruptPending = true
  2. Backend emits stream-delta with softInterruptPending: true flag
  3. Aggregator updates StreamingContext.softInterruptPending = true
  4. WorkspaceStore derives softInterruptPending from aggregator
  5. UI updates to show "⏸️ Interrupting..."
  6. Stream continues until next block boundary (tool-result, tool-error, reasoning-end, text-end, finish-step, tool-input-end)
  7. At boundary: abortController.abort() triggers graceful shutdown
  8. Emits stream-end with all completed content

Second Escape → Backend sees flag already set, does immediate hard abort (existing behavior)

Changes

Frontend:

  • Extended StreamingContext interface with softInterruptPending: boolean field
  • Added getSoftInterruptPending() method to StreamingMessageAggregator
  • Updated handleStreamDelta() to handle softInterruptPending flag from events
  • Extended WorkspaceState interface with softInterruptPending field
  • Updated getWorkspaceState() to derive flag from aggregator
  • Updated StreamingBarrier to show "⏸️ Interrupting..." when pending
  • Pass softInterruptPending prop through AIView to StreamingBarrier

Backend:

  • Extended WorkspaceStreamInfo interface with softInterruptPending: boolean field
  • Updated stopStream() to implement soft/hard interrupt logic:
    • First call: set flag and emit stream-delta with flag
    • Second call: hard abort via cancelStreamSafely()
  • Added checkSoftInterrupt() helper method
  • Check flag at each block boundary in processStreamWithCleanup():
    • After reasoning-end
    • After tool-result
    • After tool-error
    • After finish-step, text-end, tool-input-end

Types:

  • Extended StreamDeltaEvent with optional softInterruptPending field

Benefits

  • Graceful tool completion: Tools finish execution instead of aborting mid-operation
  • Better UX: Clear feedback that interrupt is pending
  • Escape hatch preserved: Second Escape still provides immediate hard abort
  • Minimal changes: Reuses existing stream-delta event infrastructure (~75 LoC)
  • Consistent patterns: Follows existing isCompacting flag pattern

Testing

Manual testing via UI:

  1. Start a streaming response with tool calls or extended thinking
  2. Press Escape once → Verify "⏸️ Interrupting..." appears immediately
  3. Verify stream ends after current tool/reasoning completes (not mid-execution)
  4. Press Escape twice → Verify immediate hard interrupt

Integration tests added in tests/ipcMain/softInterrupt.test.ts:

  • Second interrupt is immediate (hard)
  • Soft interrupt waits for reasoning-end

Generated with mux

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@ethanndickson ethanndickson changed the base branch from main to queued-messages November 14, 2025 02:22
@ethanndickson ethanndickson marked this pull request as draft November 14, 2025 02:57
Change soft-interrupt communication from IPC request/response to
stream-delta event propagation. When user presses Escape the first time,
stopStream() emits an empty stream-delta with softInterruptPending=true,
which the frontend aggregator receives and updates StreamingContext.

Benefits:
- Semantically correct: stream-delta events signal state transitions
- Consistent with existing patterns: uses event flow like other updates
- Simpler frontend: no manual state setting needed
- Type-safe: compiler enforces optional field handling

Changes:
- streamManager.stopStream(): Emit stream-delta with flag on first Escape
- StreamDeltaEvent: Added optional softInterruptPending field
- StreamingMessageAggregator: Handle flag from stream-delta, skip empty deltas
- Removed unused setSoftInterruptPending() method
- Updated all canInterrupt references to use new interruptible field

Generated with `mux`
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.

1 participant