Hard Aborts vs UI Handoff Interrupts in Session Management: Key Differences Explained

Hard aborts permanently terminate a Craft Agents session by aborting the underlying AbortController, while UI handoff interrupts temporarily pause execution at specific pause points—such as authentication requests—to allow user interaction before resuming.

The craft-ai-agents/craft-agents-oss repository distinguishes between two critical session lifecycle mechanisms that control how agent processing stops. Understanding the difference between hard aborts and UI handoff interrupts in session management is essential for building responsive agent applications that handle both definitive cancellations and temporary UI-driven pauses correctly.

What Are Hard Aborts in Session Management?

Purpose and Use Cases

Hard aborts represent true cancellation events that tear down the entire session. According to the documentation in packages/shared/CLAUDE.md, these occur when a user explicitly stops the chat, when a redirect fallback is needed, or when the session is being dismantled. The system treats these as terminal states that require full resource cleanup.

The typical reasons for hard aborts include:

  • AbortReason.UserStop – User explicitly terminates the conversation
  • AbortReason.Redirect – Navigation requires ending the current session
  • AbortReason.Teardown – System-initiated session destruction

Implementation via forceAbort

In packages/shared/src/agent/base-agent.ts, the BaseAgent class defines the abstract forceAbort(reason) method. Concrete implementations abort the underlying AbortController and trigger comprehensive cleanup routines. The AbortReason enum—defined in packages/shared/src/agent/backend/types.ts—provides the specific reason codes that inform the backend why the termination occurred.

When forceAbort executes, the session manager logs the completion and runs cleanup logic. As implemented in packages/server-core/src/sessions/SessionManager.ts, this results in the log message "Chat completed after explicit handoff/stop" and explicitly skips normal completion handling.

// Somewhere in UI code handling a "Stop" button
await session.abort(AbortReason.UserStop);
// Triggers BaseAgent.forceAbort downstream, aborting the AbortController

What Are UI Handoff Interrupts?

Purpose and Pause Points

UI handoff interrupts occur at designated "pause points" where control passes back to the UI without terminating the session. These handle events like AuthRequest or PlanSubmitted, allowing the user to authenticate or review plans before the agent continues processing. Unlike hard aborts, the session is not finished; it merely yields to the UI so the user can act.

Implementation via interruptForHandoff

The BaseAgent class provides interruptForHandoff(reason) with a default implementation that delegates to forceAbort. However, backend-specific agents can override this to use lighter-weight interruptions. For example, the Claude backend overrides this method to call this.query.interrupt() instead of aborting the AbortController, preserving the underlying connection and session state while the UI handles the handoff.

The test file packages/shared/src/agent/__tests__/claude-agent-handoff.test.ts verifies that these handoff interrupts use Query.interrupt() rather than triggering a hard abort.

// In ClaudeAgent implementation (simplified)
interruptForHandoff(reason: AbortReason) {
  this.debug('Claude handoff interrupt');
  // Uses SDK interrupt instead of AbortController abort
  this.query.interrupt();
}

Key Differences Between Hard Aborts and UI Handoff Interrupts

Understanding how these mechanisms diverge helps you choose the correct approach for your session management logic:

  • Session Lifecycle Impact: Hard aborts mark the session as finished, releasing resources and preventing further events for that turn. UI handoff interrupts defer cleanup and allow the session to resume after the UI completes its work.

  • Implementation Mechanism: Hard aborts always call forceAbort and abort the AbortController. UI handoff interrupts call interruptForHandoff, which may use backend-specific interruption logic (like Claude's Query.interrupt()) or default to a hard abort.

  • Abort Reasons: Hard aborts use AbortReason.UserStop, AbortReason.Redirect, and AbortReason.Teardown. UI handoff interrupts use AbortReason.AuthRequest and AbortReason.PlanSubmitted.

Practical Code Examples

Triggering a Hard Abort

When implementing a "Stop" button or handling fatal errors, use forceAbort to ensure complete termination:

import { BaseAgent, AbortReason } from '@craft-agents/shared';

class MyAgent extends BaseAgent {
  async handleUserStop() {
    // This triggers a full session teardown
    await this.forceAbort(AbortReason.UserStop);
  }
}

Implementing UI Handoff Interrupts

For authentication flows or plan reviews, override interruptForHandoff to preserve session state:

// Conceptual implementation in Claude backend
class ClaudeAgent extends BaseAgent {
  interruptForHandoff(reason: AbortReason) {
    if (reason === AbortReason.AuthRequest) {
      // Lighter weight than forceAbort - preserves the stream
      this.query.interrupt();
      return;
    }
    super.interruptForHandoff(reason);
  }
}

Backend-Agnostic Control Flow

You can conditionally choose between the two approaches based on the abort reason:

function stopOrHandoff(session: BaseAgent, reason: AbortReason) {
  if (reason === AbortReason.UserStop || reason === AbortReason.Teardown) {
    session.forceAbort(reason);   // Hard abort
  } else {
    session.interruptForHandoff(reason); // UI handoff interrupt
  }
}

Summary

  • Hard aborts permanently terminate Craft Agents sessions via forceAbort, aborting the AbortController and running full cleanup routines in SessionManager.
  • UI handoff interrupts use interruptForHandoff to pause at specific points, allowing the UI to handle authentication or plan review before resuming the session.
  • The default implementation of interruptForHandoff delegates to forceAbort, but backends like Claude override this to preserve session state using Query.interrupt() as verified in claude-agent-handoff.test.ts.
  • Hard aborts use reasons like UserStop and Redirect, while handoff interrupts use AuthRequest and PlanSubmitted defined in packages/shared/src/agent/backend/types.ts.

Frequently Asked Questions

When should I use forceAbort versus interruptForHandoff?

Use forceAbort when you need to completely stop the session and release all resources, such as when a user clicks a "Stop" button or when handling a redirect. Use interruptForHandoff when you need to pause execution for UI interaction but expect the session to continue afterward, such as during authentication flows or plan submission reviews.

Does interruptForHandoff always perform a hard abort?

No. While the default implementation in BaseAgent delegates to forceAbort, specific backends can override this behavior. For example, the Claude backend implements a lighter-weight interruption via Query.interrupt(), which pauses the model without tearing down the underlying stream or aborting the AbortController.

What happens to session state during a UI handoff interrupt?

The session state is preserved during a UI handoff interrupt. Unlike hard aborts, which trigger cleanup and mark the session as finished, handoff interrupts defer session-level cleanup. The system skips normal completion handling for the current turn but maintains the session context, allowing execution to resume after the UI completes the handoff process.

Where are the AbortReason enum values defined?

The AbortReason enum is defined in packages/shared/src/agent/backend/types.ts. This enum includes values for both hard aborts (UserStop, Redirect, Teardown) and UI handoff interrupts (AuthRequest, PlanSubmitted), allowing the system to distinguish between terminal cancellations and temporary pause points.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →