Skip to documentation
Documentation navigation

Documentation navigation

Documentation / guides

Shutdown

Close command admission and release Session-owned runtime resources.

developer

Shutdown is the session lifecycle boundary. It closes command admission, cancels active work, sends graceful shutdown to every registered loop, joins owned workers and finalizers, publishes event.SessionStopped, stops the event hub, releases leases and resources, and only then cancels the session context. It is stronger than Interrupt, which leaves the session alive.

Public API and low-level command

type SessionController interface {
	Session
	Shutdown(context.Context) error
}

const (
	CommandShutdown CommandName  = "Shutdown"
	ShutdownAck     CommandField = "Ack"
)

type Shutdown struct {
	Header
	Ack chan<- error `json:"-"`
}

func (c Shutdown) Validate() error

type LoopTerminatedError struct{ Cause error }

func (e *LoopTerminatedError) Error() string
func (e *LoopTerminatedError) Unwrap() error

Applications call SessionController.Shutdown; they do not construct command.Shutdown. The runtime creates one command per live loop with a fresh command ID and a buffered live ack. The Ack is nil on a restored wire command because live channels never serialize. A clean loop exit sends nil. If its root context is canceled before cleanup completes, the ack carries *command.LoopTerminatedError wrapping the cancellation cause.

Teardown order

%%{init: {"theme":"dark"}}%%
flowchart TD
    A[Latch closing and snapshot loops] --> B[Close hustle admission]
    B --> C[Cancel queued and executing inference]
    C --> D[Send Shutdown to every loop]
    D --> E[Wait for loop acks]
    E --> F[Join finalizers, audits, and workers]
    F --> G[Stop checkpoints and hub]
    G --> H[Publish SessionStopped]
    H --> I[Release workspace and session leases]
    I --> J[Cancel session context last]

The close latch and loop snapshot share the same lock used by new-loop registration. A loop is therefore either in the shutdown snapshot or refused registration; it cannot appear after the graceful fan-out has started. The session sends shutdown to primary and sub-loops, not just the active loop.

Caller cancellation is diagnostic, not permission to detach cleanup. Concurrent or repeated Shutdown calls join the existing teardown owner and receive the same cleanup result, augmented with their own context error after cleanup. A shutdown call from a session-owned hustle finalizer is rejected as re-entry.

func closeSession(ctx context.Context, c session.SessionController) error {
	if err := c.Shutdown(ctx); err != nil {
		var terminated *command.LoopTerminatedError
		if errors.As(err, &terminated) {
			log.Printf("a loop terminated during cleanup: %v", terminated)
		}
		return err
	}
	return nil
}

Durability and ownership

Shutdown commands can be appended as audit intent with their target loop held by the journal record, since the command itself is session-wide and has no route. SessionStopped is the durable session terminal event. After the hub is stopped, new event subscribers and commands are refused by the session’s closing/fault contracts. Session-owned resources receive their own Shutdown calls during teardown; callers retain ownership of resources they supplied outside the session.

The source lifecycle is internal/sessionruntime/session.go, with resource shutdown in internal/sessionruntime/session_resources.go. The command validation and live-channel contract are proved by pkg/command/shutdown_test.go. The session tests prove all-loop teardown, repeated-call joining, and closing admission. These are runtime proofs rather than a standalone shutdown command.

Source and proof

← back to documentation