Skip to documentation
Documentation navigation

Documentation navigation

Documentation / guides

Runtime errors

Classify command, Turn, tool, and Session runtime failures.

developer

Runtime errors occur after definitions have passed their construction boundary. The session, loop, journal, and owned-run types preserve the distinction between admission, execution, persistence, and cleanup.

Session and turn errors

session.SessionError carries a Kind and optional Cause, and unwraps the cause. The exact kinds are:

KindMeaning
id_generation_failedSession ID could not be minted.
loop_id_generation_failedLoop ID could not be minted.
loop_exitedThe target loop actor has exited.
loop_not_foundThe requested loop is not present.
event_channel_closedThe event channel closed without a terminal event.
context_doneThe session or caller context ended.
session_closingAdmission is closed during teardown.
session_faultedA durable persistence or workspace-integrity fault is latched.
loop_depth_exceededA nested loop exceeded the configured depth.
loop_quota_exceededA loop spawn quota was exceeded.
foreign_builder_missingA restored foreign engine has no builder.
compaction_unsupportedThe selected loop does not support native compaction.
delegate_intent_append_failedRequired durable delegation intent append failed.
delegate_admission_commit_failedDelegate admission commit failed after durable intent.

session.TurnRejectedError carries event.RejectReason; the current reasons include queue full, shutting down, and transient internal failure. A rejection is not the same as an execution failure: the corresponding TurnRejected event is the durable reply for the submit. loop.InputRejectedError is the point-to-point admission error for a managed delegate input and also preserves its reason and cause.

loop.CommitError carries Reason and Cause. Its current CommitCancelReason value is turn cancelled. A committed step remains in the loop state; an uncommitted step is discarded when the handshake is cancelled.

Hustle and tool runs

Before a run owns capacity, internal/hustleruntime returns AdmissionError or RequestError. Their exact reason sets are:

TypeReasons
AdmissionErrorinvalid_context, invalid_participation, nil_finalizer, run_id, full, closed, poisoned
RequestErrorinvalid_context, runtime_unavailable, unknown_definition, invalid_cause, invalid_input, input_too_large, nil_validator
QueueFailureErrorcanceled, timeout, closed, poisoned while waiting for a lane

Once admitted, an owned run returns *hustleruntime.RunError with Name, RunID, Stage, ReasonCode, Cause, TerminalErr, FinalizerErr, and CleanupErr. It implements Unwrap() []error, so a caller can inspect the primary execution cause and any finalizer or cleanup failure without flattening them into one string. Queue failures expose the same primary-versus-cleanup fields through QueueFailureError.

The runtime also uses redacted typed classifications for unsafe provider or tool results: OutputError, ToolResponseError, and EvidenceError carry closed reason values and do not retain provider content. Worker and callback panics become WorkerPanicError, EvidenceWorkerPanicError, or CallbackPanicError; a poisoned worker is WorkerPoisonError with an inspectable cause.

Tool request validation uses tool.RequestValidationError with exact kinds invalid_field, duplicate_requirement, duplicate_candidate, duplicate_grant_pair, invalid_command_grant, and missing_grant_binding. The Field identifies the checked request location; the error does not carry raw tool arguments.

Durable failure boundary

Journal failures must not be treated as an ordinary provider failure. The journal types expose the fencing context:

TypeDurable meaning
journal.JournalNotReadyErrorThe opening LeaseFence has not been acknowledged.
journal.JournalLeaseLostErrorThe session no longer owns its writer lease; it unwraps to LeaseLostError.
journal.AppendErrorPersistence definitely failed; Subject, MsgID, and Expected identify the attempted append.
journal.AmbiguousAckErrorThe backend outcome is unresolved after bounded verification. The fence remains unadvanced, so do not assume success or blindly duplicate the record.
journal.RecordTooLargeErrorInline persistence exceeded the threshold and blob offload failed.
sessionstore.BlobIntegrityErrorFetched offloaded bytes do not match the pointer hash.
sessionstore.BlobUnavailableErrorAn offloaded blob cannot be read; it unwraps the storage cause.
sessionstore.ReplayDecodeError or ReplayReadErrorReplay cannot safely decode or advance the ledger cursor.

When a journal append fails, the session can latch SessionFaulted and reject new work. An ambiguous acknowledgement is intentionally not converted into a successful event. Read the durable ledger with the session ID before choosing to retry a side effect or construct a replacement.

%%{init: {"theme":"dark"}}%%
flowchart TD
    A[Accepted input] --> B[Loop or hustle runtime]
    B --> C{Owned run?}
    C -- no --> D[AdmissionError or RequestError]
    C -- yes --> E{Execution result}
    E -- provider/tool shape --> F[Typed redacted RunError child]
    E -- persistence --> G[AppendError or AmbiguousAckError]
    G --> H[SessionFaulted or durable recovery path]
    E -- success --> I[Durable terminal event]
    E -- cleanup failure --> J[FinalizerError or CleanupErr child]

Inspect primary and cleanup causes

Use errors.As repeatedly. A multi-error wrapper is not a signal to choose the last child as the primary failure. RunError.Cause and TerminalErr describe the execution boundary; FinalizerErr and CleanupErr describe follow-up ownership work.

var runErr *hustleruntime.RunError
if errors.As(err, &runErr) {
	log.Printf("run=%s stage=%s reason=%s", runErr.RunID,
		runErr.Stage, runErr.ReasonCode)
	if runErr.FinalizerErr != nil || runErr.CleanupErr != nil {
		log.Printf("run cleanup also failed")
	}
}

var ambiguous *journal.AmbiguousAckError
if errors.As(err, &ambiguous) {
	// Inspect the ledger before deciding whether an application retry is safe.
	log.Printf("ambiguous subject=%s expected=%d", ambiguous.Subject, ambiguous.Expected)
}

Source and runnable proof

Session and loop runtime errors are defined in pkg/session/errors.go and pkg/loop/errors.go. Owned-run classifications and multi-error unwrapping are in internal/hustleruntime/errors.go. Journal and replay errors are in pkg/journal/errors.go and pkg/sessionstore/replay.go. The behavior is covered by pkg/loop/errors_test.go, internal/hustleruntime/failure_test.go, internal/hustleruntime/cleanup_test.go, pkg/journal/appender_test.go, and internal/sessionruntime/fault_test.go.

← back to documentation