Documentation / guides
Interruptions
Describe session interruption and the event.TurnInterrupted outcome at the conceptual Turn boundary.
An interruption cancels the current conceptual Turn through the session control
plane. The durable terminal event is event.TurnInterrupted. It is distinct
from shutdown, from a client retract of queued input, and from a provider
failure. The event stream, not a point-to-point reply, is the source of the
turn’s outcome.
Interrupt contract
The public data-plane contract is small and deliberately session-scoped:
// pkg/session/session.go
type Session interface {
Submit(context.Context, []content.Block) (uuid.UUID, error)
SubmitToLoop(context.Context, uuid.UUID, []content.Block) (uuid.UUID, error)
SubscribeEvents(event.EventFilter) (event.Subscription, error)
Interrupt(context.Context) (bool, error)
}
Interrupt returns true if at least one live loop reported that a running
Turn was cancelled. It returns false, nil when every loop was idle. The
context bounds selection, fan-out, and acknowledgements; a slow actor cannot
make an unbounded interrupt call. A context failure is returned as the typed
session error rather than being represented as a turn event.
At the command boundary the loop receives an acknowledgement-bearing command:
// pkg/command/interrupt.go
type Interrupt struct {
Header
Ack chan<- bool `json:"-"`
}
The acknowledgement is required. A malformed command is rejected locally by
validation. There is no durable command reply: the boolean is an in-process
control acknowledgement, while TurnInterrupted is the durable execution
outcome.
Session-wide scope
The session implementation snapshots every registered live loop, marks each interrupt-pending, then sends the command concurrently. Idle loops answer false and are harmless. This means a call on a session with one active and one idle loop returns true, and both loops still receive the command. It does not latch session closing or tear down loops.
%%{init: {"theme":"dark"}}%%
sequenceDiagram
participant U as caller
participant S as Session
participant A as active loop actor
participant I as idle loop actor
participant J as event journal
U->>S: Interrupt(ctx)
S->>S: snapshot and mark all live loops
par concurrent fan-out
S->>A: command.Interrupt(Ack)
A-->>S: true
A->>J: TurnInterrupted (enduring terminal)
and
S->>I: command.Interrupt(Ack)
I-->>S: false
end
S-->>U: true, nil
An entirely idle session is fail-quiet: the call returns false, nil and
publishes no terminal event. This is useful for UI stop buttons that may race
with normal completion.
Queue disposition
Cancellation is cooperative. The running inference or tool boundary observes
the canceled Turn context and returns event.TurnInterrupted. The actor then
resolves queued work:
| Queued entry | Ordinary user interrupt | Machine continuation or hand-back |
|---|---|---|
| Still in inbox | Retained for a later start after the loop reaches idle. | Removed with InputCancelled{Reason: event.CancelTurnInterrupted}. |
| In the draining buffer before a fold commit | Retained in FIFO order. | Returned with InputCancelled{Reason: event.CancelTurnInterrupted}. |
| Already folded | Remains part of the committed Turn. | Remains part of the committed Turn. |
The returned event carries the interrupted Turn ID in Header.TurnID; a pure
client retract outside a Turn instead has a zero Turn ID. If an interruption
happens while the actor is waiting for session admission, no running Turn was
cancelled, so the command acknowledgement is false even though queued machine
entries may be returned.
The loop commits the terminal event before its idle transition. Retained user input is admitted only after the idle edge, which lets the session quiescence barrier release the interrupt sweep before work starts again.
Observe the interruption
Subscribe before submitting if the UI needs the full causal sequence. Filter on
Enduring because terminal events are durable; a live-only filter is not enough.
sub, err := sess.SubscribeEvents(event.EventFilter{
Enduring: event.LoopScope{All: true},
})
if err != nil {
return err
}
defer sub.Close()
if _, err := sess.Submit(ctx, []content.Block{
&content.TextBlock{Text: "perform the long operation"},
}); err != nil {
return err
}
stopped, err := sess.Interrupt(ctx)
if err != nil {
return err
}
if !stopped {
// The Turn had already reached an idle boundary, or no loop was active.
return nil
}
for delivery := range sub.Events() {
if _, ok := delivery.Event.(event.TurnInterrupted); ok {
// Stop rendering the active Turn. Queued input is resolved separately.
break
}
}
Do not synthesize a terminal event when Interrupt returns false. It only says
that no active Turn was canceled; the event stream remains authoritative for a
Turn that may have completed concurrently.
Source and proof
pkg/session/session.godefinesSession.Interrupt.pkg/command/interrupt.godefines the acknowledgement-bearing interrupt command.internal/sessionruntime/session.goimplements the session-wide snapshot and concurrent fan-out.internal/sessionruntime/interrupt_test.goproves idle fail-quiet behavior, multi-loop fan-out, and retained user input.internal/loopruntime/loop.gomaps cancellation to terminal publication and queued-entry resolution.pkg/event/turn.godefinesTurnInterruptedandInputCancelled.
This page reserves the approved Harness navigation structure. Session interruption ends the conceptual Turn with the exact durable event.TurnInterrupted outcome.