Documentation / guides
Interrupt
Interrupt active work while preserving already committed Steps.
Interrupt is a cancellation request, not a rollback. A running loop stops at a safe boundary, publishes its terminal interruption event, and retains the Steps that were already committed. The public session method fans the request out to every live loop; a loop controller can scope interruption to one loop and its delegate subtree.
Public contracts
type Session interface {
Interrupt(context.Context) (bool, error)
}
type Controller interface {
Handle
SetMode(context.Context, ModeName) error
Change(context.Context, ...Change) error
Interrupt(context.Context) error
}
Session.Interrupt stamps user agency and returns true if at least one live
loop reported that it cancelled a running turn. It returns false for an idle
session or a session already shutting down. A context or durable-session fault
is returned as a typed *session.SessionError; a failed interrupt does not
claim that work was stopped.
The low-level command is intentionally session-wide and carries no coordinates:
const (
CommandInterrupt CommandName = "Interrupt"
InterruptAck CommandField = "Ack"
)
type Interrupt struct {
Header
Ack chan<- bool `json:"-"`
}
func (c Interrupt) Validate() error
The actor sends one boolean to the required live channel. The channel must be non-nil. It is not a durable reply path and is omitted from JSON. A session runtime keeps the dispatch target alongside the command when it writes an audit intent record, because the command itself cannot address one loop.
%%{init: {"theme":"dark"}}%%
sequenceDiagram
participant App as Application
participant S as Session
participant L1 as Primary loop
participant L2 as Delegate loop
participant E as Event fan-in
App->>S: Interrupt(ctx)
S->>S: mark every live loop interrupt-pending
par concurrent fan-out
S->>L1: Interrupt{Ack}
S->>L2: Interrupt{Ack}
end
L1-->>S: true or false
L2-->>S: true or false
L1->>E: TurnInterrupted (if active)
L2->>E: TurnInterrupted (if active)
S-->>App: anyCancelled, error
What interruption preserves
The turn actor owns the commit handshake. If interruption arrives before a Step
commit, that in-flight step is discarded and the terminal event is
event.TurnInterrupted. Previously committed event.StepDone records remain in
the loop’s history. The loop’s context cancellation also returns queued input as
event.InputCancelled with CancelTurnInterrupted or CancelTurnFailed, as
appropriate. A no-op interrupt on an idle loop produces no terminal turn event.
The subtree-scoped controller method also prevents a parent delegate wait from opening a fresh child step while the interrupted subtree drains. This is why a controller interrupt is not just a convenience wrapper around a single command.
Error handling
func stopAll(ctx context.Context, s session.Session) error {
stopped, err := s.Interrupt(ctx)
if err != nil {
var se *session.SessionError
if errors.As(err, &se) {
log.Printf("interrupt failed at session boundary: %s", se.Kind)
}
return err
}
if !stopped {
// The session was idle or already closing. There is no active turn to
// await, so do not manufacture a success event.
return nil
}
return nil
}
command.InvalidCommandError identifies a missing low-level Ack. It is an
internal construction error, not a normal user cancellation result:
cmd := command.Interrupt{Ack: make(chan bool, 1)}
if err := cmd.Validate(); err != nil {
var invalid *command.InvalidCommandError
if errors.As(err, &invalid) {
// invalid.Field is command.InterruptAck when Ack is nil
}
}
Durability, shutdown, and proof
Interrupt intent may be appended before best-effort dispatch for audit and
restore classification, but the live Ack is never serialized. Shutdown has
different ownership and cleanup guarantees; it closes admission and joins all
loops. Use Shutdown when the session itself must end.
The public behavior is exercised by
internal/sessionruntime/session_test.go,
which checks session-wide and exact-loop fan-out. The actor’s commit-cancellation
contract is proved by pkg/loop/errors.go
and the turn tests that assert committed Steps survive interruption. The command
channel and serialization contract is covered by
pkg/command/interrupt_test.go
and pkg/command/marshal_test.go.