sessionruntime

package
v0.33.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2026 License: Apache-2.0 Imports: 49 Imported by: 0

Documentation

Overview

Package sessionruntime implements the private coordinator that constructs, restores, and owns live sessions for package rig. Public consumers use package session contracts.

Index

Constants

View Source
const (
	SnapshotManual     = checkpointManual
	SnapshotOnIdle     = checkpointOnIdle
	SnapshotOnTurnDone = checkpointOnTurnDone
	SnapshotOnStepDone = checkpointOnStepDone
	SnapshotBestEffort = checkpointBestEffort
	SnapshotRequired   = checkpointRequired
)
View Source
const (
	SessionIDGenerationFailed            = sessionapi.SessionIDGenerationFailed
	SessionLoopIDGenerationFailed        = sessionapi.SessionLoopIDGenerationFailed
	SessionLoopExited                    = sessionapi.SessionLoopExited
	SessionLoopNotFound                  = sessionapi.SessionLoopNotFound
	SessionEventChannelClosed            = sessionapi.SessionEventChannelClosed
	SessionContextDone                   = sessionapi.SessionContextDone
	SessionClosing                       = sessionapi.SessionClosing
	SessionFaulted                       = sessionapi.SessionFaulted
	SessionLoopDepthExceeded             = sessionapi.SessionLoopDepthExceeded
	SessionLoopQuotaExceeded             = sessionapi.SessionLoopQuotaExceeded
	SessionForeignBuilderMissing         = sessionapi.SessionForeignBuilderMissing
	SessionCompactionUnsupported         = sessionapi.SessionCompactionUnsupported
	SessionDelegateIntentAppendFailed    = sessionapi.SessionDelegateIntentAppendFailed
	SessionDelegateAdmissionCommitFailed = sessionapi.SessionDelegateAdmissionCommitFailed
	RestoreNoSessionStarted              = sessionapi.RestoreNoSessionStarted
	RestoreNoPrimerLoop                  = sessionapi.RestoreNoPrimerLoop
	RestoreRuntimeMissing                = sessionapi.RestoreRuntimeMissing
	RestoreRuntimeUnavailable            = sessionapi.RestoreRuntimeUnavailable
	RestoreRuntimeTargetMismatch         = sessionapi.RestoreRuntimeTargetMismatch
	RestoreRuntimeCredentialMismatch     = sessionapi.RestoreRuntimeCredentialMismatch
	RestoreRuntimeEffortMismatch         = sessionapi.RestoreRuntimeEffortMismatch
	RestoreLeaseFailed                   = sessionapi.RestoreLeaseFailed
	RestoreJournalFailed                 = sessionapi.RestoreJournalFailed
	RestoreReplayFailed                  = sessionapi.RestoreReplayFailed
	RestoreAppendFailed                  = sessionapi.RestoreAppendFailed
	RestoreAdoptionInvalid               = sessionapi.RestoreAdoptionInvalid
	RestoreLoopFailed                    = sessionapi.RestoreLoopFailed
	RestoreContextDone                   = sessionapi.RestoreContextDone
	RestoreIDGenerationFailed            = sessionapi.RestoreIDGenerationFailed
	RestoreForeignSIDMissing             = sessionapi.RestoreForeignSIDMissing
	RestoreForeignBuilderMissing         = sessionapi.RestoreForeignBuilderMissing
	RestoreMaterializeFailed             = sessionapi.RestoreMaterializeFailed
	GateNotFound                         = sessionapi.GateNotFound
	GateNotReady                         = sessionapi.GateNotReady
	GateKindMismatch                     = sessionapi.GateKindMismatch
	GateActionInvalid                    = sessionapi.GateActionInvalid
	GateCapacity                         = sessionapi.GateCapacity
	GateAppendFailed                     = sessionapi.GateAppendFailed
)

Variables

This section is empty.

Functions

This section is empty.

Types

type AcceptAllDecider

type AcceptAllDecider = sessionapi.AcceptAllDecider

type AcquireCanceledError

type AcquireCanceledError struct{ Cause error }

AcquireCanceledError reports that an Acquire returned because its ctx was done before or during the wait. Cause is the ctx error (context.Canceled or context.DeadlineExceeded). No permit was granted; no waiter remains queued.

func (*AcquireCanceledError) Error

func (e *AcquireCanceledError) Error() string

func (*AcquireCanceledError) Unwrap

func (e *AcquireCanceledError) Unwrap() error

type AgentNameMismatchError

type AgentNameMismatchError = sessionapi.AgentNameMismatchError

type CheckpointError

type CheckpointError struct {
	Kind  CheckpointErrorKind
	Cause error
}

func (*CheckpointError) Error

func (e *CheckpointError) Error() string

func (*CheckpointError) Unwrap

func (e *CheckpointError) Unwrap() error

type CheckpointErrorKind

type CheckpointErrorKind string
const (
	CheckpointUnavailable         CheckpointErrorKind = "unavailable"
	CheckpointNotIdle             CheckpointErrorKind = "not_idle"
	CheckpointFaulted             CheckpointErrorKind = "faulted"
	CheckpointFailed              CheckpointErrorKind = "failed"
	CheckpointTimeout             CheckpointErrorKind = "timeout"
	CheckpointCanceled            CheckpointErrorKind = "canceled"
	CheckpointTriggerAppendFailed CheckpointErrorKind = "trigger_append_failed"
	CheckpointAppendFailed        CheckpointErrorKind = "append_failed"
	CheckpointIDGenerationFailed  CheckpointErrorKind = "id_generation_failed"
)

type CompactionInputTooLargeError

type CompactionInputTooLargeError struct {
	Limit int
	Size  int
}

CompactionInputTooLargeError reports a serialized compaction request that remains over the bound after every old tool-result body has been omitted. User and assistant prose is protected and is never removed to make a fit.

func (*CompactionInputTooLargeError) Error

type ConfigMismatchError

type ConfigMismatchError = sessionapi.ConfigMismatchError

type DefaultPolicyDecider

type DefaultPolicyDecider = sessionapi.DefaultPolicyDecider

type DelegateError

type DelegateError struct {
	Kind       DelegateErrorKind
	Agent      identity.AgentName
	Mode       loop.ModeName
	DelegateID uuid.UUID
}

DelegateError is the typed delegation refusal. Callers errors.As to inspect Kind and the offending agent, mode, or delegate ID.

func (*DelegateError) Error

func (e *DelegateError) Error() string

func (*DelegateError) ModelFacingError

func (e *DelegateError) ModelFacingError() string

ModelFacingError exposes only the fixed, bounded category needed by the model-facing agent result. It deliberately omits all request fields; ordinary delegation errors continue to use the generic tool-result error.

type DelegateErrorKind

type DelegateErrorKind uint8

DelegateErrorKind classifies a delegation refusal. Every refusal denies by default (fail-secure); the model-facing tool renders it as a tool-result string.

const (
	DelegateActionUnavailable DelegateErrorKind = iota + 1
	DelegateUnknownAgent
	DelegateUnauthorizedAgent
	DelegateUnknownMode
	DelegateNotOwned
	DelegateSessionUnavailable
	DelegateMissingDelegateID
	DelegateUnknownOperation
	// DelegateInterruptPending: the parent loop is under an interrupt admission barrier, so a
	// NEW machine-delegate request (start/send) is flushed (refused) until the barrier releases.
	// This is the machine-side of the interrupt queue policy: user input stays queued, but a
	// parent whose interrupted delegate wait resolves cannot open a fresh delegate step.
	DelegateInterruptPending
	DelegateRuntimeUnavailable
	DelegateRuntimeInvalid
	DelegateClosed
)

type FingerprintProvider

type FingerprintProvider func(loop.BoundDefinition) event.ConfigFingerprint

FingerprintProvider projects a bound loop into the immutable behavior fingerprint used for both SessionStarted and restore validation. It must be deterministic and safe for concurrent calls from separate sessions.

type GateCaps

type GateCaps struct {
	MaxOpen    int
	MaxTimeout time.Duration
}

GateCaps bounds the live gate directory. The cap counts preparing + open + claiming so failed activations cannot accumulate invisible prepared entries. Zero means no cap.

type GateError

type GateError = sessionapi.GateError

type GateErrorKind

type GateErrorKind = sessionapi.GateErrorKind

type HustleConstructionError

type HustleConstructionError struct {
	Reason HustleConstructionReason
	Name   hustle.Name
	Index  int
	Field  string
	Cause  error
}

HustleConstructionError reports a transactional hustle composition failure.

func (*HustleConstructionError) Error

func (e *HustleConstructionError) Error() string

func (*HustleConstructionError) Unwrap

func (e *HustleConstructionError) Unwrap() error

type HustleConstructionReason

type HustleConstructionReason string

HustleConstructionReason classifies the frozen-definition binding stage.

const (
	HustleConstructionBindFailed          HustleConstructionReason = "bind_failed"
	HustleConstructionRuntimeFailed       HustleConstructionReason = "runtime_failed"
	HustleConstructionMissingCollaborator HustleConstructionReason = "missing_collaborator"
	HustleConstructionAlreadyBound        HustleConstructionReason = "already_bound"
)

type HustleLimits

type HustleLimits struct {
	BlockingConcurrent   int
	BlockingQueued       int
	BackgroundConcurrent int
	BackgroundQueued     int
	AuditTimeout         time.Duration
	FinalizationTimeout  time.Duration
	WorkerDrainTimeout   time.Duration
}

HustleLimits is sessionruntime's narrow, rig-independent copy of the hustle lane and cleanup bounds.

type HustleModelResolveError

type HustleModelResolveError struct {
	Reason HustleModelResolveReason
	LoopID uuid.UUID
	Cause  error
}

HustleModelResolveError reports why the originating loop cannot supply its current inference binding.

func (*HustleModelResolveError) Error

func (e *HustleModelResolveError) Error() string

func (*HustleModelResolveError) Unwrap

func (e *HustleModelResolveError) Unwrap() error

type HustleModelResolveReason

type HustleModelResolveReason string

HustleModelResolveReason classifies an exact current-loop lookup failure.

const (
	HustleModelResolveInvalidContext HustleModelResolveReason = "invalid_context"
	HustleModelResolveInvalidLoopID  HustleModelResolveReason = "invalid_loop_id"
	HustleModelResolveLoopNotFound   HustleModelResolveReason = "loop_not_found"
	HustleModelResolveForeignLoop    HustleModelResolveReason = "foreign_loop"
	HustleModelResolveLoopExited     HustleModelResolveReason = "loop_exited"
)

type HustleShutdownReentryError

type HustleShutdownReentryError struct{}

HustleShutdownReentryError refuses the structurally circular operation in which a session-owned finalizer passes its supplied trusted context to the shutdown that is itself waiting for that finalizer. The private marker is exact-context defense in depth, not goroutine identity: a callback that drops the context cannot be detected. Production adapters therefore receive focused product capability only, never Session/Shutdown, and must preserve the callback context just as they must honor its deadline.

func (*HustleShutdownReentryError) Error

type InterruptReleasePolicy

type InterruptReleasePolicy interface {
	// AwaitRelease blocks until the interrupt admission barrier should release. ctx is the
	// session lifetime. The returned error is advisory: the marks are cleared once AwaitRelease
	// returns regardless of the error (fail-open on RELEASE, so a barrier can never wedge
	// admission forever). Implementations must be safe for concurrent use.
	AwaitRelease(ctx context.Context) error
}

InterruptReleasePolicy is the pluggable admission-barrier release seam (Dependency Inversion). After an interrupt fan-out cancels at least one running turn, the session holds every target's interrupt-pending mark until AwaitRelease returns, then clears them. The default policy (sessionIdleRelease) releases once the session reaches idle — WaitIdle returns only after the hub has durably appended SessionIdle, so "release after idle" and "release after SessionIdle is appended" are the same edge. Workspace-backed sessions use checkpoint-controller sweep outcomes directly; this seam remains for headless/custom session-runtime composition.

type InvalidWorkspaceOperationError

type InvalidWorkspaceOperationError struct{ Operation tool.WorkspaceOperation }

InvalidWorkspaceOperationError reports an Acquire with an operation value the coordinator does not recognize (fail-secure: no permit is granted).

func (*InvalidWorkspaceOperationError) Error

type LeaseHealth

type LeaseHealth interface {
	Healthy() error
}

LeaseHealth reports whether the workspace lease that underpins harness-managed mutations is currently healthy. A structured mutator MUST NOT commit when Healthy returns an error (fail-secure). A nil LeaseHealth means "no lease to verify" and is treated as always healthy (the bare/no-lease deployment).

type Lifecycle

type Lifecycle struct {
	// contains filtered or unexported fields
}

Lifecycle binds a design-time loop topology and durable backend into an immutable, reusable factory for live sessions. NewTopologyLifecycle captures the caller-facing options once; NewSession mints a fresh session id and brings up a brand-new session over per-session durable dependencies; RestoreSession rebuilds a prior session from its journal. Everything the narrow serve.Rig interface needs is fixed at NewTopologyLifecycle, so NewSession/RestoreSession take no per-call knobs.

A Lifecycle is safe to reuse across many sessions. Each NewSession/RestoreSession builds its OWN durable wiring (lease, journal, appenders) from the shared store, so two live sessions never share a lease or a journal.

func NewTopologyLifecycle

func NewTopologyLifecycle(topology Topology, store *sessionstore.Store, opts ...LifecycleOption) (*Lifecycle, error)

NewTopologyLifecycle binds an immutable, validated multi-primer graph to storage.

func (*Lifecycle) NewSession

func (r *Lifecycle) NewSession(ctx context.Context, seed workspacestore.Ref, newOpts ...NewSessionOption) (*Session, error)

func (*Lifecycle) RestoreSession

func (r *Lifecycle) RestoreSession(ctx context.Context, id uuid.UUID) (*Session, error)

RestoreSession rebuilds a live session from its durable journal under the id it was created with, delegating to runtime restoration with the Lifecycle's captured cfg, store, and base options. runtime restoration holds the store, so it builds its OWN lease/journal/appenders (and installs the lease-release hook) internally — the Lifecycle supplies only the captured caller options, NOT the per-session appenders NewSession builds. It refuses a config-fingerprint mismatch (typed *ConfigMismatchError) unless WithLifecycleAllowConfigMismatch was compiled in, and surfaces runtime restoration's typed errors unchanged (a *RestoreDiscoveryError for a session with no history, a *RestoreError for a lease/journal/replay failure), never a panic.

type LifecycleOption

type LifecycleOption func(*Lifecycle)

LifecycleOption configures a Lifecycle at NewTopologyLifecycle time. Every caller-facing knob is captured here (the runtime NewSession/RestoreSession take none), mirroring flow's LifecycleOption model. A nil/zero argument is ignored (the default is kept), mirroring the session options' own fail-safe convention.

func WithLifecycleAllowConfigMismatch

func WithLifecycleAllowConfigMismatch() LifecycleOption

WithLifecycleAllowConfigMismatch captures the restore-only opt-in to resume a session whose persisted config fingerprint no longer matches the live config. AMBIGUITY A2: classified NewTopologyLifecycle-time (fixed for the Lifecycle's lifetime) so the narrow serve.Rig interface exposes no per-call knob. NewSession ignores it; only RestoreSession honors it.

func WithLifecycleFingerprint

func WithLifecycleFingerprint(fingerprint event.ConfigFingerprint) LifecycleOption

WithLifecycleFingerprint captures a rig-time frozen compatibility fingerprint.

func WithLifecycleFingerprintProvider

func WithLifecycleFingerprintProvider(provider FingerprintProvider) LifecycleOption

WithLifecycleFingerprintProvider captures the deterministic projection used by both NewSession and RestoreSession. The provider may be called concurrently for different sessions and must be concurrency-safe.

func WithLifecycleForeignBuilderRegistry

func WithLifecycleForeignBuilderRegistry(registry *foreign.BuilderRegistry) LifecycleOption

WithLifecycleForeignBuilderRegistry forwards profile-keyed foreign routing to every live/restored session while preserving the legacy builder pair option.

func WithLifecycleForeignBuilders

func WithLifecycleForeignBuilders(b foreign.Builder, rb foreign.RestoredBuilder) LifecycleOption

WithLifecycleForeignBuilders captures the composition-root seams that construct foreign- engine loops (live + restored). Either seam being nil leaves foreign engines unsupported, so both are captured together. Forwarded to both NewSession and RestoreSession as WithForeignBuilders.

func WithLifecycleForeignServicesBuilders

func WithLifecycleForeignServicesBuilders(b foreign.ServicesBuilder, rb foreign.ServicesRestoredBuilder) LifecycleOption

WithLifecycleForeignServicesBuilders captures the additive foreign builder seams for every live/restored session. Session construction starts one private broker only when a foreign loop uses this opt-in seam, then mints a fresh descriptor for each origin loop. Native and legacy builders remain zero-services paths.

func WithLifecycleGateCaps

func WithLifecycleGateCaps(caps GateCaps) LifecycleOption

WithLifecycleGateCaps captures the live gate-directory bounds. Zero (the default) means no cap. Forwarded to both NewSession and RestoreSession as WithGateCaps.

func WithLifecycleHooks

func WithLifecycleHooks(runner *hook.Runner) LifecycleOption

WithLifecycleHooks captures one immutable compiled runner for every native loop and journal built by NewSession or RestoreSession.

func WithLifecycleHustles

func WithLifecycleHustles(definitions []hustle.Definition, limits HustleLimits) LifecycleOption

WithLifecycleHustles captures immutable hustle registrations for both NewSession and RestoreSession composition.

func WithLifecycleLimits

func WithLifecycleLimits(l Limits) LifecycleOption

WithLifecycleLimits captures the in-session agent-spawn safety caps (depth + quota) the session enforces. Forwarded to both NewSession and RestoreSession as WithLimits.

func WithLifecycleManifest

func WithLifecycleManifest(manifest event.ConfigManifest) LifecycleOption

WithLifecycleManifest captures the rig-assembled ConfigManifest counterpart to the frozen fingerprint. It is stamped onto the construction-time SessionStarted's additive Manifest field, giving a newly created session a real (SchemaVersion>=1) manifest baseline.

func WithLifecycleOffloadGC

func WithLifecycleOffloadGC(policy OffloadGCPolicy) LifecycleOption

WithLifecycleOffloadGC captures the session offload-blob GC cadence. An unconfigured (zero) policy is ignored. Forwarded to both NewSession and RestoreSession, which wire the journal-admission gate + GC runner over the session lease.

func WithLifecyclePermissionReview

func WithLifecyclePermissionReview(classifiers gate.PermissionClassifierSet, policy gate.PermissionReviewPolicy) LifecycleOption

WithLifecyclePermissionReview installs the registered classifier set and local decision policy every session in this lifecycle uses for automatic permission-gate review (design §20), mirroring WithLifecycleHustles' capture-once-forward-to-both shape. Both NewSession and RestoreSession apply it via the private withPermissionReview Option (gates.go). Omitting this option leaves every session's permissionClassifiers at its zero value, preserving StartPermissionReview's pre-Task-16 no-op default exactly.

func WithLifecyclePermissionReviewBreaker

func WithLifecyclePermissionReviewBreaker(limits PermissionReviewBreakerLimits) LifecycleOption

WithLifecyclePermissionReviewBreaker installs the bounded circuit-breaker thresholds every session in this lifecycle applies to automatic permission review (design §18). Both NewSession and RestoreSession apply it via the private withPermissionReviewBreaker Option (review_state.go). Omitting this option leaves every session's breaker limits at their zero value (every counter disabled), matching withPermissionReviewBreaker's own zero-preserves-behavior default.

func WithLifecyclePermissionReviewEvidence

func WithLifecyclePermissionReviewEvidence(access gate.EvidenceAccessEvaluator, containment gate.EvidenceContainmentVerifier, allowedKinds []string) LifecycleOption

WithLifecyclePermissionReviewEvidence installs the consumer-supplied read-only evidence-tool access boundary every registered permission classifier's evidence tools authorize against (design §13.1), mirroring WithLifecyclePermissionReview's capture-once-forward-to-both shape. Both NewSession and RestoreSession apply it via the private withPermissionReviewEvidence Option (gates.go). Omitting this option leaves every session's evidence collaborators at their zero value; newHustleController (hustle.go) then fails CONSTRUCTION closed — never silently permissive — for any registered classifier whose definition actually needs evidence tools.

func WithLifecyclePermissionReviewObservations

func WithLifecyclePermissionReviewObservations(verifier gate.EvidenceObservationVerifier) LifecycleOption

WithLifecyclePermissionReviewObservations installs the consumer-supplied, OPTIONAL recheck seam (design §13.4, TOCTOU) every classifier-originated auto-approval's recorded observations are verified against immediately before the gate is claimed, mirroring WithLifecyclePermissionReviewEvidence's capture-once-forward-to-both shape. Both NewSession and RestoreSession apply it via the private withPermissionReviewObservationVerifier Option (gates.go) — but, unlike Evidence/SecurityCeiling, ONLY when this option was actually called: omitting it leaves every session's permissionReviewObservationVerifier at its nil zero value, which respondFromClassifier's verifyPermissionReviewObservations then handles as a fully supported terminal state whenever no observation was ever recorded, and as a fail-closed (stale) outcome whenever one was — see that method's own doc comment for the full reasoning, including why this cannot be a Define()-time "missing" pairing error the way WithLifecyclePermissionReviewEvidence's absence is.

func WithLifecyclePermissionReviewSecurityCeiling

func WithLifecyclePermissionReviewSecurityCeiling(ceiling string) LifecycleOption

WithLifecyclePermissionReviewSecurityCeiling installs the consumer-supplied effective security posture (design §13.1/§21) every registered permission classifier's ReviewContext/ReviewBasis carries as SecurityCeiling, mirroring WithLifecyclePermissionReviewEvidence's capture-once-forward-to-both shape. Both NewSession and RestoreSession apply it via the private withPermissionReviewSecurityCeiling Option (gates.go). Omitting this option leaves every session's permissionReviewSecurityCeiling at its zero value; loopReviewContext (gates.go) then returns nil for that session (capture stays off) rather than ever falling back to a Harness-invented placeholder — see withPermissionReviewSecurityCeiling's doc comment for why a fixed sentinel is exactly the bug this option exists to close (Finding 2, Phase 6 spec-compliance review).

func WithLifecyclePlacement

func WithLifecyclePlacement(p WorkspacePlacement) LifecycleOption

WithLifecyclePlacement captures the managed-workspace placement the rig declared. The unconfigured zero value is ignored (no managed workspace). Forwarded to NewSession and RestoreSession, which resolve it per-session after acquiring the session lease.

func WithLifecycleRestoreDecider

func WithLifecycleRestoreDecider(decider RestoreDecider) LifecycleOption

WithLifecycleRestoreDecider captures the restore-only application policy that answers a configuration-drift assessment (the successor seam to WithLifecycleAllowConfigMismatch). A nil decider is ignored, leaving RestoreSession on the fail-secure DefaultPolicyDecider{} default. NewSession ignores it; only RestoreSession forwards it (as WithRestoreDecider).

func WithLifecycleRuntimeCatalog

func WithLifecycleRuntimeCatalog(catalog loop.RuntimeCatalog) LifecycleOption

WithLifecycleRuntimeCatalog forwards the immutable parent-scoped runtime snapshot to every session constructed by this lifecycle.

func WithLifecycleRuntimeCatalogProvider

func WithLifecycleRuntimeCatalogProvider(provider RuntimeCatalogProvider) LifecycleOption

WithLifecycleRuntimeCatalogProvider forwards parent-specific runtime catalog selection to every live/restored session.

func WithLifecycleRuntimeRestoreResolver added in v0.27.0

func WithLifecycleRuntimeRestoreResolver(resolver RuntimeRestoreResolver) LifecycleOption

WithLifecycleRuntimeRestoreResolver captures the optional composition-owned runtime reconstruction fallback for restored sessions.

func WithLifecycleSessionResourceStorage

func WithLifecycleSessionResourceStorage(resolve SessionResourceStorageResolver) LifecycleOption

WithLifecycleSessionResourceStorage captures the durable process-resource storage resolver for both new and restored live constructions.

func WithLifecycleSnapshotPolicy

func WithLifecycleSnapshotPolicy(policy SnapshotPolicy) LifecycleOption

WithLifecycleSnapshotPolicy captures the validated native checkpoint policy and forwards it to every new/restored session. Rig enforces that it is paired with a managed placement.

func WithLifecycleToolResultCapture added in v0.31.0

func WithLifecycleToolResultCapture(objects loopruntime.ToolResultObjectStore, spillBase string) LifecycleOption

WithLifecycleToolResultCapture captures the SessionObjectStore durable tool-result retention writes into, and forwards it to both NewSession and RestoreSession as WithToolResultCapture. A nil store is ignored, which leaves retention unconfigured rather than installing a store the loop would nil-deref on.

func WithLifecycleWorkspaceCheckpointing

func WithLifecycleWorkspaceCheckpointing(ws *workspacestore.Store, root string) LifecycleOption

WithLifecycleWorkspaceCheckpointing captures the workspace snapshot store and root the session checkpoints into (and RestoreSession materializes from). A nil store is ignored. Forwarded to both NewSession and RestoreSession as WithWorkspaceCheckpointing.

type Limits

type Limits struct {
	// Depth is the maximum spawn-chain nesting depth (sub-loops below the primary). Zero
	// → defaultDepth. A spawn whose parent chain is already this deep is refused.
	Depth int

	// Quota is the maximum total number of sub-loops the session may spawn over its
	// lifetime. Zero → defaultQuota. A spawn once this many have been reserved is refused.
	Quota int
}

Limits are the in-session agent-spawn safety caps applied by NewLoop: Depth bounds the spawn-chain nesting and Quota bounds the total sub-loops a session may spawn. A zero (or negative — a wiring slip) field adopts the package default via withDefaults, so a caller can never accidentally disable a cap; an explicit positive value overrides it.

type MissingFingerprintProviderError

type MissingFingerprintProviderError struct{}

MissingFingerprintProviderError reports incomplete composition wiring.

func (*MissingFingerprintProviderError) Error

type MissingStoreError

type MissingStoreError struct{}

MissingStoreError reports that NewTopologyLifecycle was handed a nil *sessionstore.Store. The durable backend is a required dependency (DIP): the Lifecycle mints per-session leases/journals/ appenders from it, so a nil store is rejected at NewTopologyLifecycle rather than deferring a nil-deref to the first NewSession/RestoreSession.

func (*MissingStoreError) Error

func (*MissingStoreError) Error() string

type MissingTopologyError

type MissingTopologyError struct{}

func (*MissingTopologyError) Error

func (*MissingTopologyError) Error() string

type NewSessionError

type NewSessionError struct {
	Kind  NewSessionErrorKind
	Cause error
}

NewSessionError is the typed wrapper for a NewSession failure. Kind classifies the stage; Cause chains the underlying typed error (a *journal.LeaseHeldError, a *SessionError, etc.) so a caller can errors.As both this and the cause. On any failure after the lease is acquired the Lifecycle releases it best-effort before returning, so a failed NewSession never strands single-writer ownership.

func (*NewSessionError) Error

func (e *NewSessionError) Error() string

func (*NewSessionError) Unwrap

func (e *NewSessionError) Unwrap() error

type NewSessionErrorKind

type NewSessionErrorKind string

NewSessionErrorKind classifies a NewSession failure before the live session exists — the per-session durable wiring the Lifecycle builds itself (lease, journal, checked appenders) or the session construction that consumes them.

const (
	// NewSessionContextDone: the NewSession context was already cancelled.
	NewSessionContextDone NewSessionErrorKind = "context_done"
	// NewSessionIDGenerationFailed: a crypto/rand failure minting the fresh session id.
	NewSessionIDGenerationFailed NewSessionErrorKind = "id_generation_failed"
	// NewSessionLeaseFailed: the single-writer lease could not be acquired (another owner holds
	// it, or a backend read failed). The session must not come up.
	NewSessionLeaseFailed NewSessionErrorKind = "lease_failed"
	// NewSessionJournalFailed: the SessionJournal could not be opened (its opening fence was
	// rejected, or stream setup failed).
	NewSessionJournalFailed NewSessionErrorKind = "journal_failed"
	// NewSessionAppenderFailed: a checked journal appender (event/command/gate) could not be
	// constructed over the opened journal.
	NewSessionAppenderFailed NewSessionErrorKind = "appender_failed"
	// NewSessionRuntimeFailed: NewSession refused to build the live session over the wired dependencies.
	NewSessionRuntimeFailed NewSessionErrorKind = "session_failed"
)

type NewSessionOption added in v0.33.0

type NewSessionOption func(*newSessionConfig)

NewSession mints a fresh session ID and brings up a brand-new live session over per-session durable deps built from the Lifecycle's store: a single-writer lease, the session journal, and the three checked appenders (event — carrying the catalog — command, and gate). It returns the minted id, the live session, and a typed *NewSessionError on any failure. On ANY failure after the lease is acquired the lease is released best-effort, so a failed NewSession never strands single-writer ownership. NewSessionOption configures one Lifecycle.NewSession call. It is separate from Option (which configures the Session under construction) because these are decided per call and are consumed BEFORE the session exists.

func AdoptSessionID added in v0.33.0

func AdoptSessionID(id uuid.UUID) NewSessionOption

AdoptSessionID makes this NewSession call run under an externally-minted id instead of minting one. The id is consumed at the very TOP of NewSession, before the lease is acquired and before the journal is opened, so every per-session durable artifact is built under it — which is the whole point: an id adopted after the journal was bound would leave the runtime and its durable stream naming different sessions.

A zero id is ignored (NewSession mints one) so a wiring slip cannot produce a zero-id session here. The PUBLIC surface, rig.WithSessionID, refuses a zero id outright rather than relying on that fallback; this layer keeps the fallback because it is also the no-option default path.

type OffloadGCPolicy

type OffloadGCPolicy struct {
	Interval time.Duration
	Timeout  time.Duration
}

OffloadGCPolicy is the validated cadence for one session's offload-blob GC: how often a pass runs (Interval) and the per-pass deadline (Timeout). The rig validates the fields before constructing it; a zero value means "off". This is SESSION OFFLOAD GC only — it reaps orphaned content-addressed offload blobs (a crash gap of the writer's blob-durable-before-pointer discipline), never workspace snapshots.

func (OffloadGCPolicy) Configured

func (p OffloadGCPolicy) Configured() bool

Configured reports whether the policy is armed (both fields positive). An unarmed policy wires no gate and no runner, leaving the journal undecorated (unchanged behavior).

type Option

type Option func(*Session)

Option configures an optional session dependency at construction. The bare New(ctx, cfg) installs the nop command appender; an Option overrides it. This mirrors the hub's Option pattern so the composition root injects the durable intent-log appender (Phase 10) without New growing a positional parameter.

func WithAllowConfigMismatch

func WithAllowConfigMismatch() Option

WithAllowConfigMismatch is the restore-only opt-in to resume a session whose persisted config fingerprint no longer matches the live config (a different model, system prompt, or tool policy). Restore is fail-secure by DEFAULT — a mismatch rejects with *ConfigMismatchError so a conversation never silently resumes under behavior it never ran with — so this option exists for an operator who knowingly accepts the drift. New ignores it (only Restore checks fingerprints).

func WithCommandAppender

func WithCommandAppender(a commandAppender) Option

WithCommandAppender injects the intent-log appender (the composition root's adapter over SessionJournal). A nil appender is ignored (the nop default stays installed) so a caller can never accidentally null out the field and nil-deref the dispatch path.

func WithEventAppender

func WithEventAppender(a eventAppender) Option

WithEventAppender injects the hub's REQUIRED durable event tap (the composition root's adapter over SessionJournal — journal.JournalEventAppender). New forwards it into the hub (hub.WithAppender) so every Enduring event is durably appended before fan-out (fail-secure: an append failure faults the session). A nil appender is ignored (the hub's nop default stays installed) so a caller can never null out the tap and silently persist nothing. This is the event-side counterpart to WithCommandAppender (the audit-only intent log).

func WithFingerprint

func WithFingerprint(fingerprint event.ConfigFingerprint) Option

WithFingerprint installs a definition-time frozen compatibility fingerprint. Unlike a provider, it is available before any loop definition is bound during restore.

func WithFingerprintProvider

func WithFingerprintProvider(provider FingerprintProvider) Option

WithFingerprintProvider installs the composition root's immutable projection.

func WithForeignBuilderRegistry

func WithForeignBuilderRegistry(registry *foreign.BuilderRegistry) Option

WithForeignBuilderRegistry injects profile-keyed foreign construction while retaining the legacy function-pair seam for EngineForeignClaude/Codex.

func WithForeignBuilders

func WithForeignBuilders(b foreign.Builder, rb foreign.RestoredBuilder) Option

WithForeignBuilders wires the composition-root seam that constructs foreign-engine loops (live + restored). Without it, a foreign-engine definition fails closed at newLoop (SessionForeignBuilderMissing) and at restore (RestoreForeignBuilderMissing) — a foreign engine never silently resolves to a native loop. The two seams travel together (a live build and a restored build of the same agent), so they are wired as one option; either being nil leaves foreign engines unsupported for that path.

func WithForeignServicesBuilders

func WithForeignServicesBuilders(b foreign.ServicesBuilder, rb foreign.ServicesRestoredBuilder) Option

WithForeignServicesBuilders wires the additive foreign-engine seam. Runtime supplies a fresh Services snapshot for the exact session+loop construction: the broker descriptor is minted for that origin loop and Delivery remains a loop-scoped hook. Legacy builders remain on the zero-services adapter.

func WithGateAppender

func WithGateAppender(a gateAppender) Option

WithGateAppender injects the strict durable append seam for gate prepare/open/resolve. A nil appender is ignored (the nop default stays installed). It is the gate-directory counterpart to WithCommandAppender.

func WithGateCaps

func WithGateCaps(caps GateCaps) Option

WithGateCaps injects the live gate directory bounds. Zero (the default) means no cap. The cap counts preparing + open + claiming.

func WithHooks

func WithHooks(runner *hook.Runner) Option

WithHooks installs the immutable operation-hook runner for native loops.

func WithInterruptReleasePolicy

func WithInterruptReleasePolicy(p InterruptReleasePolicy) Option

WithInterruptReleasePolicy installs the pluggable admission-barrier release policy (the Dependency-Inversion seam of the interrupt machinery). After an interrupt cancels a running turn, the session holds the interrupt-pending marks until the policy's AwaitRelease returns, then clears them. Without this option the session uses the default (sessionIdleRelease): release once the session next reaches idle (SessionIdle durably appended). Task 16 injects a workspace-aware policy that may hold the barrier through a checkpoint. A nil policy is ignored (the default stays installed). See interrupt.go.

func WithLeaseRelease

func WithLeaseRelease(release func(context.Context) error) Option

WithLeaseRelease installs the single-writer-lease release hook the session calls ONCE at the end of Shutdown (after the loops have drained, so the journal's last append is durable before ownership is relinquished). The composition root passes lease.Release for a NEW session; Restore installs it from the lease it acquired, so both paths free ownership on a clean exit and a successor can re-acquire at all (no backend expires a grant). A nil hook is ignored (headless mode stays a no-op). It takes a context so the release I/O is bounded by Shutdown's ctx.

func WithLimits

func WithLimits(l Limits) Option

WithLimits sets the in-session agent-spawn safety caps (depth + quota) NewLoop enforces. A zero (or negative) field in the supplied Limits adopts the package default (Depth 3 / Quota 64) when newSession applies withDefaults, so a caller can never disable a cap with a missing or bad value. Without this option a session uses the defaults. It applies on both New and Restore (the restore path re-seeds the spawn counter from the durable log, then enforces these caps against it).

func WithManifest

func WithManifest(manifest event.ConfigManifest) Option

WithManifest installs the rig-assembled ConfigManifest counterpart to the frozen fingerprint. The session stamps it onto the construction-time SessionStarted's additive Manifest field.

func WithRestoreDecider

func WithRestoreDecider(decider RestoreDecider) Option

WithRestoreDecider installs the restore-only application policy that answers a configuration-drift assessment (the successor seam to WithAllowConfigMismatch). A nil decider is ignored so a wiring slip cannot null the field — the session then keeps its fail-secure DefaultPolicyDecider{} default. New ignores the decider (only Restore assesses drift); a later task consumes it in the restore path.

func WithRuntimeCatalog

func WithRuntimeCatalog(catalog loop.RuntimeCatalog) Option

WithRuntimeCatalog installs one immutable parent-scoped catalog snapshot. The same value feeds agent schema/preparation and controller revalidation.

func WithRuntimeCatalogProvider

func WithRuntimeCatalogProvider(provider RuntimeCatalogProvider) Option

WithRuntimeCatalogProvider installs parent-specific catalog snapshots. It takes precedence over WithRuntimeCatalog when both are supplied.

func WithRuntimeCommands added in v0.31.0

func WithRuntimeCommands(log runtimeCommandLog, lease leaseEpochSource) Option

WithRuntimeCommands injects the durable application-prefix log and the session's lease, enabling the segregated runtime-command capability. Both are required: an applier without a lease cannot reject a superseded admission, and one without a deduplicating log cannot honor duplicate delivery. A nil argument leaves the capability unadvertised rather than half-wired.

func WithRuntimeRestoreResolver added in v0.27.0

func WithRuntimeRestoreResolver(resolver RuntimeRestoreResolver) Option

WithRuntimeRestoreResolver installs the optional composition-owned runtime reconstruction fallback. A nil resolver preserves the fail-closed default.

func WithSessionID

func WithSessionID(id uuid.UUID) Option

WithSessionID injects an externally-minted sessionID for New to adopt instead of minting its own. It resolves the journal chicken-and-egg: the durable journal needs the sessionID (to bind the per-session stream and write the opening LeaseFence) BEFORE the session exists, so the composition root mints the id first, builds the journal/lease/appenders from it, then hands the SAME id to New here. A zero id is ignored (New mints one) so a wiring slip can never produce a zero-id session. Restore takes the sessionID positionally and ignores this option.

func WithSnapshotPolicy

func WithSnapshotPolicy(policy SnapshotPolicy) Option

WithSnapshotPolicy carries the already-validated rig policy into one session. It is meaningful only with a managed placement; rig enforces that pairing.

func WithToolResultCapture added in v0.31.0

func WithToolResultCapture(objects loopruntime.ToolResultObjectStore, spillBase string) Option

WithToolResultCapture wires the SessionObjectStore that every loop in this session retains oversized tool results into. Without it retention is off and a loop commits only the shaped model preview, so wiring this option is what turns "no layer may silently discard the tail of a tool result" from a policy into an enforced one. A nil store is ignored, so a wiring slip leaves the default unconfigured state rather than installing a store the loop would nil-deref on. spillBase is the directory this session's spill root is created under. An empty base keeps each capture's retained prefix in memory, bounded by the same ceiling; pkg/rig's public option refuses to build one, so an empty base here means an internal caller that predates the spill.

func WithWorkspaceCheckpointing

func WithWorkspaceCheckpointing(ws *workspacestore.Store, root string) Option

WithWorkspaceCheckpointing wires the workspace snapshot store and the workspace root this session checkpoints. Both are required for CheckpointWorkspace; without this option the capability is unconfigured and CheckpointWorkspace fails closed with a typed *WorkspaceNotConfiguredError. The composition root decides WHEN to checkpoint (a quiescence point); looprig only exposes the capability. A nil store is ignored (the default unconfigured state stays), so a wiring slip can never install a store the capability would nil-deref on.

type PermissionReviewBreakerLimits

type PermissionReviewBreakerLimits struct {
	MaxConsecutiveNeedsHuman int
	MaxInvalidOrFailed       int
	MaxIdenticalSubjects     int
	MaxStaleResponses        int
	InterruptOnTrip          bool
	Session                  PermissionReviewSessionBreakerLimits
}

PermissionReviewBreakerLimits are the consumer-configurable bounded per-turn and per-session circuit-breaker thresholds (design §18). A zero value disables the corresponding counter — see reviewCircuitBreakerLimits' doc comment (review_state.go) for why the turn-scoped and session-scoped thresholds are deliberately separate configuration surfaces.

type PermissionReviewSessionBreakerLimits

type PermissionReviewSessionBreakerLimits struct {
	MaxConsecutiveNeedsHuman int
	MaxInvalidOrFailed       int
	MaxIdenticalSubjects     int
	MaxStaleResponses        int
}

PermissionReviewSessionBreakerLimits is PermissionReviewBreakerLimits' session-scoped counterpart (design §18: "track per-turn AND per-session bounded counters"), mirroring reviewSessionCircuitBreakerLimits.

type PlacementResolutionError

type PlacementResolutionError struct {
	Reason string
}

PlacementResolutionError reports an invalid placement resolution (e.g. a per-session placement with a zero session id). It is a construction-time, fail-closed failure.

func (*PlacementResolutionError) Error

func (e *PlacementResolutionError) Error() string

type ProcessLifecycleOwnerMismatchError

type ProcessLifecycleOwnerMismatchError struct {
	Want uuid.UUID
	Got  uuid.UUID
}

ProcessLifecycleOwnerMismatchError reports that a Tools-supplied ProcessLifecycleMetadata named a session other than the one the checked publisher was attached to. A process resource is bound to exactly one session for its whole lifetime, so metadata naming a different session can only be a bug or a forged call — never a legitimate publish — and is rejected fail-secure before it ever reaches the durable journal or the hub.

func (*ProcessLifecycleOwnerMismatchError) Error

type ProcessNotificationDeliveryStoppedError

type ProcessNotificationDeliveryStoppedError struct {
	LoopID uuid.UUID
}

ProcessNotificationDeliveryStoppedError reports that a durably-appended (or deduplicated) ProcessNotification could not be delivered to its owning loop right now: the loop's bounded live notification set is full, or the loop exited concurrently with dispatch. The durable command remains authoritative — the caller (Tools' supervisor) retries dispatch later with the SAME CommandID; no re-append happens on that retry (24A's idempotency index already recognizes the id).

func (*ProcessNotificationDeliveryStoppedError) Error

type ProcessNotificationOwnerMismatchError

type ProcessNotificationOwnerMismatchError struct {
	Want uuid.UUID
	Got  uuid.UUID
}

ProcessNotificationOwnerMismatchError reports that a Tools-supplied ProcessCompletionNotification named a session other than the one this Session was constructed for. A process resource is bound to exactly one session for its whole lifetime, so a notification naming a different session can only be a bug or a forged call — never a legitimate delivery — and is rejected fail-secure before it ever reaches the durable journal or a loop's command sink.

func (*ProcessNotificationOwnerMismatchError) Error

type ProcessNotificationUnsupportedError

type ProcessNotificationUnsupportedError struct {
	LoopID uuid.UUID
	Engine loop.Engine
}

ProcessNotificationUnsupportedError reports that a process completion notification was addressed to a loop whose bound definition is not EngineNative. Foreign engines never receive process notifications — they have no backend arm to deliver one to, so accepting it would either block forever or silently drop it; this refuses structurally, on the ENGINE, mirroring loopHandle.ReplaceExternalTools' identical foreign-engine guard (see loop_tools.go) rather than testing bindings or backend shape.

func (*ProcessNotificationUnsupportedError) Error

type ProcessServicesUnsupportedError

type ProcessServicesUnsupportedError struct {
	Engine loop.Engine
}

ProcessServicesUnsupportedError rejects process-enabled definitions on engines whose lifecycle events cannot be published by Harness.

func (*ProcessServicesUnsupportedError) Error

type ResidencyReleaseRefusedError added in v0.31.0

type ResidencyReleaseRefusedError struct{ Cause error }

ResidencyReleaseRefusedError reports that a nonterminal residency release was refused BEFORE any teardown ran. The session is untouched: still resident, still admitting work, still the caller's to Shutdown or to release again later. It is a distinct type from every teardown failure precisely so a host can tell "I could not release" apart from "I released badly".

func (*ResidencyReleaseRefusedError) Error added in v0.31.0

func (*ResidencyReleaseRefusedError) Unwrap added in v0.31.0

func (e *ResidencyReleaseRefusedError) Unwrap() error

type RestoreDecider

type RestoreDecider = sessionapi.RestoreDecider

type RestoreDecision

type RestoreDecision = sessionapi.RestoreDecision

type RestoreDiscoveryError

type RestoreDiscoveryError = sessionapi.RestoreDiscoveryError

type RestoreDiscoveryErrorKind

type RestoreDiscoveryErrorKind = sessionapi.RestoreDiscoveryErrorKind

type RestoreError

type RestoreError = sessionapi.RestoreError

type RestoreErrorKind

type RestoreErrorKind = sessionapi.RestoreErrorKind

type RestoreRejectedError

type RestoreRejectedError = sessionapi.RestoreRejectedError

type RestoreRuntimeMismatchError

type RestoreRuntimeMismatchError = sessionapi.RestoreRuntimeMismatchError

type RestoreRuntimeMismatchKind

type RestoreRuntimeMismatchKind = string

type RestoredContextModelMismatchError

type RestoredContextModelMismatchError struct {
	Runtime     model.ModelKey
	Measurement model.ModelKey
}

RestoredContextModelMismatchError reports a replay projection that would seed a current measurement under a different restored runtime model.

func (*RestoredContextModelMismatchError) Error

type RuntimeCatalogProvider

type RuntimeCatalogProvider func(parent loop.Definition) (loop.RuntimeCatalog, bool)

RuntimeCatalogProvider returns the immutable runtime catalog visible to one parent definition. Returning false preserves the native/no-choice surface for that parent. A provider is composition-root policy and must not derive entries from model-controlled request data.

type RuntimeRestoreRequest added in v0.27.0

type RuntimeRestoreRequest = sessionapi.RuntimeRestoreRequest

type RuntimeRestoreResolver added in v0.27.0

type RuntimeRestoreResolver = sessionapi.RuntimeRestoreResolver

type Session

type Session struct {
	// contains filtered or unexported fields
}

func New

func New(ctx context.Context, cfg loop.Definition, opts ...Option) (*Session, error)

New constructs a Session and starts its zero-parent root loop's actor goroutine. It owns the session fan-in hub and emits the session-scoped SessionStarted through it.

This SessionStarted (the s.hub.PublishEvent below) is the SOLE SessionStarted: the session publishes it through the HUB to its SUBSCRIBERS (TUI/CLI fan-in), and the loop never emits one. It is published before any subscriber attaches, so a subscriber that connects later does not observe it; reliable delivery of the session start to late subscribers is a separate future follow-on.

func NewTopology

func NewTopology(ctx context.Context, topology Topology, opts ...Option) (*Session, error)

NewTopology constructs every configured primer as an independent root loop.

func RestoreTopology

func RestoreTopology(ctx context.Context, topology Topology, sessionID uuid.UUID, store *sessionstore.Store, opts ...Option) (*Session, error)

RestoreTopology reconstructs the durable loop topology and brings it up idle. It does not publish SessionStarted.

Order (per the design — RestoreStarted is the FIRST restore mutation, after the lease fence the journal writes at construction):

  1. Acquire the single-writer lease, construct the SessionJournal (writes the opening LeaseFence — the handover boundary) and the EventReplayer.
  2. Replay the stream; read the persisted config fingerprint and compare to the live config. On mismatch → *ConfigMismatchError UNLESS WithAllowConfigMismatch is set.
  3. Append RestoreStarted (the first restore mutation).
  4. Validate, bind, and independently fold every durable declared loop.
  5. Append checked crash closures for every open turn.
  6. Materialize the workspace, build all loops, and validate the active loop.
  7. Append RestoreDone as the final commit point and return the controller.

On ANY failure in steps 2–7 (a config mismatch, a discovery/replay/decode/object failure, or an append failure) Restore durably records a RestoreErrored and returns a typed error; the session does NOT come up (fail-secure / no silent drift). On a FAILED restore the acquired lease is released here before returning (so a successor can re-acquire). On SUCCESS the journal HOLDS the lease for the live session's lifetime; releasing it at session teardown is the Phase-10 composition root's wiring (mirroring how the lease is composition-root-owned in the durable-tap path), out of scope here. RestoreTopology reconstructs the entire topology in one leased replay transaction.

func (*Session) ActivateGate

func (s *Session) ActivateGate(ctx context.Context, id gate.ID, route gate.Route) error

ActivateGate is called by the owner after its local blocker/continuation exists. It requires a preparing gate, appends the public GateOpened event via the strict gateAppender, stores the private route, and flips the entry to open so ListGates returns it. A failed append leaves the gate preparing. An unknown or non-preparing gate returns a typed *GateError.

func (*Session) ActiveLoop

func (s *Session) ActiveLoop() loop.Handle

func (*Session) ActiveLoopID

func (s *Session) ActiveLoopID() uuid.UUID

ActiveLoopID returns the session's mutable active loop id, the default target for Submit. It is safe to call concurrently.

func (*Session) AdmissionFaultErr

func (s *Session) AdmissionFaultErr() error

AdmissionFaultErr is the loop actor's last-moment admission probe, closing the Session submit check→send race without changing the narrower change-event FaultErr seam.

func (*Session) ApplyRuntimeCommand added in v0.31.0

func (s *Session) ApplyRuntimeCommand(ctx context.Context, admitted runtimecommand.Admitted) (runtimecommand.Disposition, error)

ApplyRuntimeCommand applies one previously admitted command.

The ordering is the contract. The private application prefix — public CommandID, the one RuntimeCommandID it maps to, and the lease epoch — is made durable BEFORE any runtime-visible effect. A delivery that finds the prefix already durable applied nothing and returns the ORIGINAL disposition; a delivery whose public id is durably bound to a DIFFERENT runtime id fails closed.

Harness allocates no identity here. The dispatched command carries the admitted RuntimeCommandID verbatim, and the opaque public id is never parsed as a UUID.

A NON-NIL ERROR DOES NOT MEAN THE COMMAND MAY BE RE-OFFERED, and the RETURNED DISPOSITION is how a caller tells the two apart:

  • zero Disposition + error: nothing durable was written (validation, a lost or stale lease, a mapping conflict, an unreadable prefix, a failed prefix append). The command is untouched and may be re-offered.
  • non-zero PrefixSequence + error: the prefix COMMITTED and the effect then failed — an exited loop, a cancelled context. Every later delivery is a duplicate that applies nothing.

The second case is inherent to writing the correlation before the effect, and it is the safe direction: the alternative is applying the command twice. Returning the earned disposition alongside the error is what keeps it observable without forcing a redelivery to find out. See TestEffectFailureAfterTheDurablePrefixStrandsTheCommand.

KNOWN GAP — A HOST ADAPTER MUST NOT BLOCK ON ANY APPLICATION SETTLING, of any kind. The released settlement correlation resolves a prefix by ADJACENCY: the record at prefix+1 must be the public event the command caused, and its resolve() switches on that record's envelope kind without ever inspecting the command's kind. This applier writes the prefix LAST before the effect, so adjacency holds when nothing else is writing — but nothing GUARANTEES it. A concurrent legacy Submit's audit intent record, another loop's event in a multi-loop session, or a checkpoint landing in that slot resolves an INPUT unresolved exactly as readily as an interrupt. The interrupt is merely the kind that is always in that position: it has no guaranteed public event at all, and an idle interrupt is fail-quiet.

Unresolved never licenses a rejection, so this is liveness and not correctness — which is precisely why it is written down: nothing fails, the command simply never settles, and a caller that waits for it waits forever. Closing it needs either a guaranteed durable effect record per kind or the prefix-and-effect pair serialized against every other append; both are decisions about the public event vocabulary and the writer's admission rather than about this seam, so neither is made here. pkg/sessionstore's TestAdjacencyIsNotGuaranteedForAnyCommandKind measures the consequence for both kinds and holds the semantics still while it waits.

func (*Session) AwaitGateAnswer

func (s *Session) AwaitGateAnswer(ctx context.Context, id gate.ID) (gate.Answer, error)

AwaitGateAnswer blocks until a HOST-OWNED gate is answered and returns the validated answer, including the form values that are deliberately absent from every durable record.

It is the host's half of the loop's command dispatch: the opener of a form or open-url gate calls PrepareGateOpen, ActivateGate, then this. It returns a typed *GateError{GateNotFound} for a gate that is not host-owned, was never prepared, or whose answer was already taken — an answer is delivered exactly once.

The opener MUST either await or CloseGate, which is the same obligation ActivateGate already places on it. Both paths free the slot.

A ctx cancellation abandons the wait and frees the slot; it does NOT close the gate, because the gate is durable state and ctx is the caller's. An opener that gives up should CloseGate.

func (*Session) CheckpointWorkspace

func (s *Session) CheckpointWorkspace(ctx context.Context) (workspacestore.Ref, error)

CheckpointWorkspace durably snapshots the session's configured workspace root and records a WorkspaceCheckpointed enduring event pointing at the snapshot — the resume token the restore path materializes from. It returns the snapshot Ref (also carried on the event) so the caller can correlate the checkpoint.

Snapshot-before-append: the archive bytes are durable (Snapshot's Blobs.Put has returned) BEFORE the event is appended, so a crash between the two leaks an unreferenced blob (GC's job) — never a dangling ref. That ordering is guaranteed by call order here (Snapshot returns before PublishEvent runs), not by any stronger signal.

The WorkspaceCheckpointed is Enduring, so it flows through the hub's REQUIRED durable tap: the event is appended synchronously BEFORE fan-out, and a durable-append failure surfaces as a SESSION FAULT (the hub latches it via ReportFault) exactly like every other enduring event — PublishEvent itself returns nil on that path. This method adds no stronger durability signal; on the happy path the event IS durably appended by the time it returns.

WHEN to call this — a quiescence point (a turn done, a user question, about to suspend) — is the composition root's decision, consistent with looprig-as-SDK and the foreign-loop quiescence model; the session only exposes the capability. Without WithWorkspaceCheckpointing the capability is unconfigured and this fails closed with a typed *WorkspaceNotConfiguredError, having touched nothing. ctx bounds the snapshot I/O and the durable append.

func (*Session) CloseGate

func (s *Session) CloseGate(ctx context.Context, id gate.ID, reason gate.CloseReason) error

CloseGate closes or abandons a session-owned gate without dispatching an answer to the resolver. Preparing gates were never public, so they are removed without a public GateResolved. Open gates are durably resolved before removal.

func (*Session) CommitBoundary

func (s *Session) CommitBoundary(ctx context.Context, ev event.Event) error

CommitBoundary is the native loop-actor boundary seam. Without a configured workspace policy it preserves ordinary publication; with one it delegates the already-stamped StepDone/turn terminal to the session checkpoint controller.

func (*Session) CommitContextBoundary

func (s *Session) CommitContextBoundary(ctx context.Context, ev event.Event) (bool, error)

CommitContextBoundary is the context-mutating boundary seam. The committed result distinguishes an event append failure from a later checkpoint failure, so the loop actor can keep live history aligned with durable restore state.

func (*Session) CommitSessionIdle

func (s *Session) CommitSessionIdle(ctx context.Context, idle event.SessionIdle, commit func() error) error

CommitSessionIdle is the hub's narrow derived-idle collaborator. The hub retains append/fanout ownership in commit; the controller brackets it with the workspace permit and accepted checkpoint walk when idle is the configured trigger.

func (*Session) CommittedPublicEvents added in v0.31.0

func (s *Session) CommittedPublicEvents() (sessionapi.CommittedPublicEventSource, bool)

CommittedPublicEvents reports the segregated committed-public-event capability (sessionapi.CommittedPublicEventProvider). It is true only when this session's injected event appender can report the EXACT canonical public bytes a durable append stored; a headless/no-persistence session and one over a journal without that seam both answer (nil, false).

The capability is discovered rather than assumed because it is a property of the injected persistence, not of *Session: the same type serves both. Returning the session itself as the source once the hub confirms support keeps the refusal at discovery time, before a consumer has subscribed and started advancing a cursor.

func (*Session) Compact

func (s *Session) Compact(ctx context.Context) (uuid.UUID, error)

Compact requests manual compaction of the currently active loop. The active id is sampled once, then routed through the exact-target implementation.

func (*Session) CompactToLoop

func (s *Session) CompactToLoop(ctx context.Context, loopID uuid.UUID) (uuid.UUID, error)

CompactToLoop requests manual compaction of one exact live native loop. The trusted session boundary owns user agency and command coordinates; callers receive only the correlation id used by durable waiter outcomes.

func (*Session) Done added in v0.30.0

func (s *Session) Done() <-chan struct{}

Done reports session liveness: the returned channel is closed once Shutdown has BEGUN (teardown admits no new work) and never reopens. It is the signal an out-of-process observer needs, because a shut-down session is otherwise indistinguishable from a live one from outside: Hub.SubscribeEvents still succeeds after the hub is stopped, so an event stream over a dead session would heartbeat forever, pinning its handler goroutine, its subscription and this whole Session.

A receive on it must never be read as "cleanup finished" — it fires at the start of teardown, deliberately.

The channel is minted here on first use under the same shutdownMu that Shutdown closes it under, so every Session reports liveness however it was built. Done takes only shutdownMu and calls nothing, and Shutdown never calls Done, so the two cannot deadlock.

func (*Session) EnterExecution

func (s *Session) EnterExecution(ctx context.Context, loopID uuid.UUID) (func(), error)

EnterExecution is the inference-step session-wide checkpoint and loop-scoped interrupt admission seam.

func (*Session) EnterTurnStart

func (s *Session) EnterTurnStart(ctx context.Context, loopID uuid.UUID) (loopruntime.TurnStartCapability, error)

EnterTurnStart reserves the Hub activity transition before acquiring the first checkpoint reader. Its returned capability publishes the exact opening event and releases the checkpoint reader when the first inference step ends.

func (*Session) FaultErr

func (s *Session) FaultErr() error

FaultErr is the loop actor's post-emit durable-fault probe (loopruntime type-asserts the session for it). It returns the latched persistence fault (a required Enduring append failed) or nil. After emitting a mode/inference change event, the actor calls it: because the hub raises the fault INLINE (synchronously on the same actor goroutine, via ReportFault) when the append fails, a non-nil result here means the change event did not persist, so the actor declines to apply the change (fail-secure, no partial apply).

func (*Session) Interrupt

func (s *Session) Interrupt(ctx context.Context) (bool, error)

Interrupt is the human "stop everything": it cancels the running turn of EVERY live loop in the session — every registered loop — marking each interrupt-pending before fanning the command.Interrupt out to all of them CONCURRENTLY (design: "Session.Interrupt marks every live loop interrupt-pending before concurrently sending commands"). Idle loops ack false and are harmless; Interrupt returns true iff ANY loop reported it cancelled a running turn. A fully-idle interrupt is fail-quiet: it returns false and appends no events. ctx bounds the fan-out so a slow actor cannot wedge it.

Unlike Shutdown, Interrupt does NOT latch closing and does NOT tear loops down. It stamps Agency=AgencyUser (a human pressed interrupt). Selection + marking + concurrent delivery + the admission barrier live in interrupt.go (runInterrupt); this is the session-wide scope.

func (*Session) LeaseEpoch added in v0.33.0

func (s *Session) LeaseEpoch() (uint64, bool)

LeaseEpoch is the exported session.LeaseEpochReporter capability: it reports the single-writer journal lease epoch this resident process currently holds, and whether it holds one at all. The contract, and the argument for the two-result shape, live on session.LeaseEpochReporter; this is where the answer is produced.

It is gated on Valid(), which is the ONE way it differs from the unexported leaseEpoch() the residency record uses. That difference is deliberate and the two must not be merged. leaseEpoch() runs inside teardown, while the lease is still held, and records the epoch that produced the work being released — history, which stays true afterwards. This one answers a live consumer asking what it may stamp NOW, and a lease that has been released or lost may not be stamped against at all, so it reports absence rather than the number it used to hold. journal.Lease keeps reporting its Epoch after Release (no pinned provider zeroes it), so consulting Valid is the only thing that distinguishes the two states.

It reads two fields set once at construction and does no I/O, so it neither blocks nor takes the session lock.

func (*Session) ListGates

func (s *Session) ListGates(context.Context) []gate.Gate

ListGates returns the public envelopes of all open gates — preparing, claiming, and closed entries are excluded. The returned slice is a snapshot; mutating it does not affect the directory.

func (*Session) Loop

func (s *Session) Loop(id uuid.UUID) (loop.Handle, bool)

func (*Session) LoopController

func (s *Session) LoopController(id uuid.UUID) (loop.Controller, bool)

func (*Session) NewLoop

func (s *Session) NewLoop(parent loop.Provenance, cfg loop.Definition) (uuid.UUID, error)

NewLoop creates another loop inside this session. The new loop shares SessionID but receives its own loop id and loop goroutine. parent is the provenance of the spawning turn/step (zero for a root loop); the session records it in the registry and passes it to loopruntime.New. The session stores the loop handle and returns only the loop id, because callers route through session methods rather than writing to a loop command channel directly.

func (*Session) NotifyProcessCompletion

func (s *Session) NotifyProcessCompletion(ctx context.Context, n tool.ProcessCompletionNotification) error

NotifyProcessCompletion is Task 24C's real implementation of tool.ProcessCompletionNotifier, attached behind the session-owned sessionProcessServiceBridge exactly like 24B's checked lifecycle publisher (see process_services.go's attachProcessCompletionNotifier). It reports only the boundary contract's plain error (nil on ACCEPTED or DUPLICATE, the durable append's typed collision on COLLISION, and a typed reason on STOPPED); notifyProcessCompletion below carries the richer command.ProcessNotificationResult a caller inspecting the disposition (tests, and a future retry policy) needs.

func (*Session) OpenHostGate

func (s *Session) OpenHostGate(ctx context.Context, loopID uuid.UUID, g gate.Gate, payload gate.Payload) (gate.ID, error)

OpenHostGate opens a HOST-OWNED gate and returns its id, ready to be awaited.

It is the ONLY gate-opening entry point on the published session.GateHost contract, and it is deliberately not PrepareGateOpen. PrepareGateOpen takes an arbitrary kind, resolver, payload, and — through the ActivateGate route that must follow it — an arbitrary target loop, which together are exactly the loop-owned command path (dispatchGateCommand). A host must not be able to reach that: minting an "approve this tool call" gate against someone else's loop is not a capability an integration should acquire by being able to ask a human a question. This entry point takes the same arguments minus the route and refuses everything that is not host-owned, so the loop path is unreachable through it by construction rather than by convention.

It collapses prepare and activate into one call, which is safe precisely because the gate is host-owned. The two phases exist so a LOOP can install its blocker while the gate is still private; a host's blocker is the answer slot, and PrepareGateOpen installs that itself. There is no window here for the opener to miss.

Every rejection is a *GateError{GateKindMismatch}, checked BEFORE anything is journaled:

  • hostOwnedGate — the same predicate that governs answer time, so open time and answer time cannot drift apart. A form gate declaring ResolverLoop is refused here rather than accepted and then refused after a human has already answered it.
  • the payload must match the kind, so a form gate cannot be opened with an open-url payload (which would strand its answer at ParseFormAnswers).
  • the payload's own invariants (ValidateFormSchema / ValidateOpenURLPayload) must hold, because an unanswerable or targetless prompt should never reach a human.

It then DERIVES the trusted half of the public projection from the validated payload (projectHostGatePrompt), so the envelope a renderer sees cannot disagree with the payload a response is validated against.

The caller MUST either AwaitGateAnswer or CloseGate; both free the slot.

func (*Session) PrepareGateOpen

func (s *Session) PrepareGateOpen(ctx context.Context, loopID uuid.UUID, g gate.Gate, payload gate.Payload) (gate.ID, error)

PrepareGateOpen durably commits the public envelope plus private payload as a private GatePreparedRecord. It mints the GateID, stamps the GatePrepared event with the session's coordinates and the caller's loopID, appends the record via the strict gateAppender, and — only on success — inserts a non-listable preparing entry. A failed append returns a typed *GateError{GateAppendFailed} and does not mutate the directory. loopID is the producing loop's id (the GatePrepared event is loopScoped); TurnID/StepID are read from the gate's Subject.

func (*Session) PublishEvent

func (s *Session) PublishEvent(ctx context.Context, ev event.Event) error

PublishEvent is the session's eventPublisher implementation passed to loopruntime.New. It delegates to the hub, which fans the event out to matching subscribers and applies any quiescence transition the event implies. The loop depends only on the narrow eventPublisher interface; it never sees the hub, its subscriber set, or its shutdown state (Interface Segregation / least privilege).

func (*Session) PublishEventChecked

func (s *Session) PublishEventChecked(ctx context.Context, ev event.Event) error

PublishEventChecked is the transactional publication path for state transitions whose caller must not mutate live state unless the required append commits. Delegate acceptance, public gate open/resolve transitions, and native checkpoint boundaries use it to receive append failures directly while retaining durable-first fan-out.

func (*Session) ReleaseResidency added in v0.31.0

func (s *Session) ReleaseResidency(ctx context.Context) error

ReleaseResidency gives up THIS process's resident runtime for the session — subscriptions, actors, supervised processes, leases, local contexts — while leaving the logical session restorable elsewhere. It is the nonterminal counterpart of Shutdown and satisfies session.Releaser.

It deliberately does NOT call Shutdown. Shutdown's hub stop durably appends SessionStopped, which makes the logical session terminal; a registry loser that used it to hand a session over would have ended someone's session instead of releasing it. Everything else about the teardown IS Shutdown's: both drive the one sequence in Session.teardown, differing only at the plan seams below.

The order is:

  1. Admission. The session must not be faulted and must be whole-session idle. This runs before the teardown owner is elected, so a refusal leaves the session exactly as it was and returns *ResidencyReleaseRefusedError.
  2. The shared teardown: latch closing, revoke collaboration origins, close hustle admission, shut down and drain every loop, stop the checkpoint controller and the offload GC, terminate the session resource registry.
  3. Take and durably commit the anchoring workspace checkpoint, then append SessionResidencyReleased carrying that checkpoint's sequence and the lease epoch. A checkpoint that does not commit writes NO residency record — a record naming an anchor that does not exist is worse than no record.
  4. Close the hub LOCALLY, appending no SessionStopped.
  5. Release the workspace root lease then the session lease, cancel the session context, and abandon foreign delivery hooks.

Done closes at the START of step 2, not at the end, so a supervisor learns the session is going away while teardown is still running.

Concurrent and repeated calls — and a race with Shutdown — join ONE teardown owner and receive its result. Whoever is elected runs its own mode to completion; the stream therefore carries either a SessionResidencyReleased or a SessionStopped, never both.

WHICH one it carries is not in this call's return value — a joined caller gets the owner's result, and nil means "teardown succeeded" under either mode. A caller that needs to know reads the session catalog, which projects the two apart on purpose (sessionstore.applyEvent): a release leaves Status/State untouched and sets Residency=cold, so the session reads cold AND restorable, while a stop sets Residency=cold together with Status=stopped/State=stopped. Residency alone never distinguishes them — no process is resident after either — so a caller deciding whether a session can still be restored must read the terminality axis, not residency.

The idle admission is not a lock: a Submit to an already-registered loop may still be accepted between the admission check and the closing latch. It is not lost and it does not corrupt the anchor — step 2 sends every loop a Shutdown command and waits for the drain, so the checkpoint in step 3 is taken with no loop running.

func (*Session) ReportFault

func (s *Session) ReportFault(_ context.Context, fault *hub.SessionPersistenceFault)

ReportFault is the session's fail-secure response to a hub persistence fault (a required durable append failed): it latches the faulted state (so every new Submit/NewLoop is refused) and wakes every blocked WaitIdle waiter with the fault. It is the hub's FaultReporter — the hub calls it inline (outside the hub lock) when AppendEvent fails the durable tap. It is idempotent: the FIRST fault latches and records the cause; a later fault still wakes any new waiters but keeps the first recorded cause (the root failure). The session is NOT torn down here — restore / operator action owns recovery; this only stops admitting new work and unblocks callers stuck waiting on a session that can no longer reach idle durably.

func (*Session) RespondGate

func (s *Session) RespondGate(ctx context.Context, response gate.GateResponse) error

RespondGate claims an open gate, durably appends GateResolved, and dispatches the translated command to the owning loop. It is durable-first: the GateResolved append happens BEFORE the command dispatch, so a crash after the append leaves the gate closed (not re-answerable) even if the command was not yet consumed. A failed append reverts the in-memory claim and leaves the gate answerable. Command dispatch uses s.sessionCtx (not the caller's ctx) so a client disconnect after the durable commit does not cancel delivery.

A caller-supplied response whose Source.Kind is gate.ResponseFromClassifier is rejected outright, before any locking or validation: only the private respondFromClassifier method (called exclusively from the permission-review adapter once gate.CombinePermissionAssessments reports eligibility) may produce that provenance. gate.ResponseSource's fields are exported — a public caller CAN construct one with Kind set to ResponseFromClassifier — so this is a runtime, defense-in-depth check rather than a type-system guarantee; respondFromClassifier reaches the shared core below directly and is not subject to it.

func (*Session) RestoreWorkspace

func (s *Session) RestoreWorkspace(ctx context.Context, ref workspacestore.Ref) error

RestoreWorkspace rewinds the managed workspace to ref. It fails closed with *WorkspaceNotConfiguredError when no placement is configured, and with a typed *WorkspaceRestoreError on any admission, staging, or commit failure.

func (*Session) RunAgent

func (s *Session) RunAgent(ctx context.Context, parent loop.Provenance, cfg loop.Definition, blocks []content.Block, parentToolUseID string) (string, error)

RunAgent creates an in-session sub-loop, runs one turn on it for the given blocks (machine-originated), and returns the sub-loop's final assistant text. It is the SOLE exported entry point of the agent composition: it wires together the unexported building blocks (NewLoop + SubscribeEvents + submitToLoop + drainToFinalText + interruptLoopID) so the agent collaboration tools' injected capability (a later task) has one method to call and the blocks stay package-private.

cfg is the sub-loop's loop.Definition — the CALLER builds a FRESH cfg per call (its own ToolSet and access gate) so each sub-loop has independent approval state; RunAgent never reuses a shared ToolSet. parent is the spawning loop/turn/step provenance (recorded on the sub-loop's registry entry and stamped on its LoopStarted). The submit is stamped Agency=AgencyMachine — an agent turn is a machine action, never falsely attributed to a human.

The sub-loop PERSISTS idle after the turn (loops are never deleted, design §8): RunAgent closes only the SUBSCRIPTION, never the loop. The ordering is load-bearing: it subscribes (scoped to the new sub-loop) BEFORE submitting, so the opening TurnStarted the drain correlates on cannot be missed — the hub has no replay (design §4).

Errors propagate from the first block that fails: NewLoop (e.g. SessionClosing while shutting down, or id-gen failure), SubscribeEvents, the submit, or the drain's typed §5 failures (*drainFailedError / *TurnRejectedError / *drainInterruptedError / *drainLostError). ctx is the calling turn's context; because submits carry no ctx, a ctx cancel cannot reach the sub-loop's turn — the drain translates it into a single loop-targeted Interrupt (the closure below) and drains to the resulting TurnInterrupted terminal.

func (*Session) RuntimeCommands added in v0.31.0

func (s *Session) RuntimeCommands() (runtimecommand.Applier, bool)

RuntimeCommands reports whether this session can apply Host-admitted runtime commands, and hands back the applier when it can.

This is a SEGREGATED capability, not a method on session.Session, for the same reason CommittedPublicEventSource is: almost every Session implementation — every test double, the TUI's view, an adapter that only reads — will never apply an admitted command, and widening the base contract would force all of them to grow a method they cannot honor. The two-result form is what lets a Host adapter learn the answer BEFORE it has acknowledged a command as accepted.

func (*Session) SessionActivated

func (s *Session) SessionActivated()

SessionActivated lets the hub cancel an active best-effort quiescent walk before newly active work proceeds. Required and shared-fuzzy policies ignore activation.

func (*Session) SessionID

func (s *Session) SessionID() uuid.UUID

func (*Session) SetActiveLoop

func (s *Session) SetActiveLoop(ctx context.Context, id uuid.UUID) error

func (*Session) Shutdown

func (s *Session) Shutdown(ctx context.Context) error

Shutdown drives the WHOLE session to its stopped phase and blocks until every loop's actor exits. Sub-loops are retained (each runs its own goroutine and loopCtx), so Shutdown must reach every loop. The order is deliberate:

  1. Latch closing AND snapshot the loops in ONE loopsMu critical section. This is the atomicity NewLoop's registration check pairs with (it re-tests closing under the same lock): a loop is either already in this snapshot or refused by NewLoop — never registered after the snapshot is taken.
  2. Close hustle admission and cancel queued/executing inference. Keep the checkpoint controller, hub, and session context open for owned cleanup.
  3. Send command.Shutdown to EVERY loop in the snapshot, recording each reached loop's (loop, ack) pair. Per loop: mint a CommandID; on id-gen failure SKIP that loop's graceful shutdown (the final sessionCancel hard-cancels it) rather than aborting the whole Shutdown. A loop already exited is skipped.
  4. Wait for every recorded ack, then join hustle terminal audit, finalizers, and blocking activity release through Controller.Drained.
  5. Stop/join checkpoints and offload GC, terminate session resources, append SessionStopped/stop the hub, release root/session leases, and cancel sessionCtx last.
  6. Loop/checkpoint/hub phases have private deadlines derived from validated component bounds. Hustle audit, finalization, and worker drain use their own trusted inner bounds and are always joined; an outer deadline never detaches owned cleanup. Caller cancellation is diagnostic only.

Concurrent and repeated calls join one teardown owner and receive the same cleanup result, augmented with each caller's own context error after cleanup completes. That owner is shared with ReleaseResidency, so a session is either stopped or released, never both.

Steps 1-5 are Session.teardown, the sequence both teardown modes run; Shutdown supplies only the terminal plan (terminalTeardown).

func (*Session) StartPermissionReview

func (s *Session) StartPermissionReview(ctx context.Context, req loopruntime.PermissionReviewRequest)

StartPermissionReview implements the loop actor's private permissionReviewStarter seam (internal/loopruntime/gate.go): the ONLY thing the actor calls, inline and fire-and-forget, after a gatePermission registration activates and the runner has been acked (design §14.2/§14.3). It MUST return promptly — the actor never wraps this call in a goroutine of its own, so classifier inference is moved onto a fresh goroutine here (`go adapter.review(...)`) before this method returns.

StartPermissionReview no-ops (starts no review) whenever:

  • no permission classifiers are configured for this session (s.permissionClassifiers is the zero PermissionClassifierSet); or
  • the session's shared Hustle runtime is not yet bound (s.hustleController == nil — cannot happen once a session has finished construction, but is checked rather than assumed); or
  • the classifier/policy configuration itself is invalid (newPermissionReviewAdapter's own validation).

Every one of those is a "nothing to review" outcome, never a denial or an approval: this method has no path to RespondGate at all (see permissionReviewAdapter's type doc), so the human gate this handoff accompanies is always preserved regardless of what happens here.

func (*Session) Submit

func (s *Session) Submit(ctx context.Context, input []content.Block) (uuid.UUID, error)

Submit is the HUMAN-ONLY submit entry point: it stamps Agency=AgencyUser (a person authored this input). Programmatic/machine callers go through submitToLoop with Agency=AgencyMachine (the agent path).

Submit sends input as a queueable UserInput to the active loop, FIRE-AND-FORGET: it returns the InputID (the submit command's id, == the Cause.CommandID on the resulting Reply events) and a transport error only if the command could not be handed to the loop. The outcome — InputQueued / TurnStarted / TurnFoldedInto / TurnRejected / InputCancelled — is observed on the event fan-in (each Reply carries Cause.CommandID == this returned id), NOT returned here.

A submit while a turn is running QUEUES rather than rejecting; a submit while idle starts a turn. Submit never reads a reply, so it returns the instant the command is accepted by the loop.

The send carries the standard escapes: ctx.Done() → SessionContextDone, the loop's Done → SessionLoopExited, and a missing active loop → SessionLoopNotFound. On any of those the returned id is the zero UUID, because nothing was sent and there is no correlation to hand back.

func (*Session) SubmitToLoop

func (s *Session) SubmitToLoop(ctx context.Context, loopID uuid.UUID, blocks []content.Block) (uuid.UUID, error)

SubmitToLoop is the loop-targeted counterpart of Submit: it sends human-authored (AgencyUser) input to a SPECIFIC loop's CommandSink rather than the active selection. It is the modern viewport's "submit to the FOCUSED loop" primitive — a submit while focused on a agent runs a NEW turn on THAT loop (accepted: a submit to an idle-but-tracked agent starts a fresh turn on it), while a submit to the active loop id behaves exactly like Submit.

Like Submit it stamps command.UserInput with Agency=AgencyUser and is FIRE-AND-FORGET: it returns the minted InputID (the Cause.CommandID the resulting Reply events carry on the session fan-in) and a transport error only; the turn outcome — InputQueued / TurnStarted / TurnFoldedInto / TurnRejected / InputCancelled — is observed on the event fan-in, never returned here. The send carries the same escapes as Submit: ctx.Done() → SessionContextDone, the loop's Done → SessionLoopExited, and an unknown loop id → SessionLoopNotFound. On any of those the returned id is the zero UUID, because nothing was sent and there is no correlation to hand back. It delegates to the shared loop-targeted core submitToLoop with AgencyUser, exactly as Submit does for the active loop.

func (*Session) SubscribeCommittedPublicEvents added in v0.31.0

func (s *Session) SubscribeCommittedPublicEvents(filter event.EventFilter) (event.Subscription, error)

SubscribeCommittedPublicEvents attaches a consumer to the committed public event stream, on which every delivery carries the canonical body the durable append stored. It delegates to the hub, which refuses with *hub.CommittedPublicEventsUnavailableError if the capability is not supported — so a caller that reached this method without going through CommittedPublicEvents still cannot obtain a stream the persistence cannot back.

func (*Session) SubscribeEvents

func (s *Session) SubscribeEvents(filter event.EventFilter) (event.Subscription, error)

SubscribeEvents attaches a consumer to the session fan-in with the given filter. The returned subscription's Events() channel yields the filtered stream; the caller must Close it when done. It delegates to the hub.

func (*Session) WaitIdle

func (s *Session) WaitIdle(ctx context.Context) error

WaitIdle blocks until the session is quiescent, ctx is done, or the session has stopped (hub.ErrSessionStopped). It is the headless caller's "is the whole interaction at rest?" primitive; it delegates to the hub's quiescence model.

func (*Session) WorkspaceStatus added in v0.31.0

func (s *Session) WorkspaceStatus() sessionapi.WorkspaceStatus

WorkspaceStatus reports what this session knows about the workspace it came up on. It satisfies sessionapi.WorkspaceReporter, the segregated capability a Host discovers by assertion; the contract's doc comment is authoritative for what each field means.

A session with no managed workspace reports the zero value. On a fresh session the boundary fields are zero because there is no prior stream to fold; Restore is the only caller that has one.

FRESHNESS: Root and LogicalRoot are read live, the boundary fields are AS OF RESTORE and never refresh. A session that checkpoints again after coming up still reports the boundary it came up on, so a caller polling this for a live checkpoint position gets a mix of live and frozen fields. Refreshing it would mean folding the stream again on every call; the report exists for the restore decision, not as a checkpoint cursor.

It is a REPORT. Nothing in it recovers a lost mutation or bounds how much was lost.

type SessionError

type SessionError = sessionapi.SessionError

type SessionErrorKind

type SessionErrorKind = sessionapi.SessionErrorKind

type SessionResourceStorageError

type SessionResourceStorageError struct {
	Kind  SessionResourceStorageErrorKind
	Path  string
	Cause error
}

SessionResourceStorageError preserves a stable classification while wrapping provider and filesystem causes.

func (*SessionResourceStorageError) Error

func (*SessionResourceStorageError) Unwrap

func (e *SessionResourceStorageError) Unwrap() error

type SessionResourceStorageErrorKind

type SessionResourceStorageErrorKind string

SessionResourceStorageErrorKind classifies fail-closed durable root and identity-anchor validation failures.

const (
	SessionResourceStorageInvalid          SessionResourceStorageErrorKind = "invalid"
	SessionResourceStorageUnavailable      SessionResourceStorageErrorKind = "unavailable"
	SessionResourceStorageIdentityMismatch SessionResourceStorageErrorKind = "identity_mismatch"
	SessionResourceStorageAnchorMissing    SessionResourceStorageErrorKind = "anchor_missing"
	SessionResourceStorageAnchorCorrupt    SessionResourceStorageErrorKind = "anchor_corrupt"
	SessionResourceStorageWorkspaceOverlap SessionResourceStorageErrorKind = "workspace_overlap"
)

type SessionResourceStorageResolver

type SessionResourceStorageResolver func(context.Context, uuid.UUID) (root, identity string, err error)

SessionResourceStorageResolver is the narrow lifecycle seam adapted from the public rig provider. It returns a durable root and provider-owned identity for the supplied non-zero session ID.

type ShutdownCleanupPhase

type ShutdownCleanupPhase string

ShutdownCleanupPhase identifies the session-owned teardown phase that exceeded its private cleanup deadline.

const (
	ShutdownCleanupLoopSend         ShutdownCleanupPhase = "loop_send"
	ShutdownCleanupLoopDrain        ShutdownCleanupPhase = "loop_drain"
	ShutdownCleanupCheckpointDrain  ShutdownCleanupPhase = "checkpoint_drain"
	ShutdownCleanupResidencyAnchor  ShutdownCleanupPhase = "residency_anchor"
	ShutdownCleanupCollabBroker     ShutdownCleanupPhase = "collab_broker"
	ShutdownCleanupSessionResources ShutdownCleanupPhase = "session_resources"
	ShutdownCleanupHubStop          ShutdownCleanupPhase = "hub_stop"
)

type ShutdownCleanupTimeoutError

type ShutdownCleanupTimeoutError struct {
	Phase   ShutdownCleanupPhase
	Timeout time.Duration
	Cause   error
}

ShutdownCleanupTimeoutError reports a finite session-owned teardown deadline. Cause remains inspectable with errors.Is(err, context.DeadlineExceeded).

func (*ShutdownCleanupTimeoutError) Error

func (*ShutdownCleanupTimeoutError) Unwrap

func (e *ShutdownCleanupTimeoutError) Unwrap() error

type SnapshotPolicy

type SnapshotPolicy struct {
	Trigger  checkpointTrigger
	Priority checkpointPriority
	Timeout  time.Duration
}

SnapshotPolicy is the internal composition form of rig.SnapshotPolicy. pkg/rig owns the public API and converts its validated values into this dependency-only shape.

type Topology

type Topology struct {
	Definitions  []loop.Definition
	Primers      []identity.AgentName
	ActivePrimer identity.AgentName
}

Topology is the immutable set of loop definitions and roots captured by a Lifecycle. Callers must pass already-validated, uniquely named definitions.

type TurnRejectedError

type TurnRejectedError = sessionapi.TurnRejectedError

type WorkflowActivityOwnerMismatchError

type WorkflowActivityOwnerMismatchError struct {
	Want uuid.UUID
	Got  uuid.UUID
}

WorkflowActivityOwnerMismatchError reports a workflow activity addressed to a session other than the one that owns the resource. A session resource never gets a generic cross-session event publisher.

func (*WorkflowActivityOwnerMismatchError) Error

type WorkspaceLifetimeAccessError

type WorkspaceLifetimeAccessError struct {
	Kind   tool.WorkspaceAccessKind
	Reason string
}

WorkspaceLifetimeAccessError reports an invalid authoritative access summary. Invalid and non-canonical scope sets fail secure without enqueuing a waiter.

func (*WorkspaceLifetimeAccessError) Error

type WorkspaceNotConfiguredError

type WorkspaceNotConfiguredError = sessionapi.WorkspaceNotConfiguredError

type WorkspacePathError

type WorkspacePathError struct {
	Operation tool.WorkspaceOperation
	Reason    string
}

WorkspacePathError reports an Acquire whose canonicalPath does not match its operation (a path mutation with an empty path, or a whole-workspace operation with a non-empty path).

func (*WorkspacePathError) Error

func (e *WorkspacePathError) Error() string

type WorkspacePlacement

type WorkspacePlacement struct {
	Mode      WorkspacePlacementMode
	Store     *workspacestore.Store
	Root      string
	BaseDir   string
	Leaser    storage.Leaser
	LeaseName string
}

WorkspacePlacement is the resolved-at-Define descriptor the Lifecycle carries to bring up a session's managed workspace. rig builds it from its placement options and validates the persistence-overlap invariant before constructing the Lifecycle. Store is the blob store checkpoints/restores go through; Root is the canonical fixed root (exclusive and shared); BaseDir is the canonical per-session base (session mode); Leaser + LeaseName are the exclusive root lease (exclusive mode only).

func (WorkspacePlacement) Configured

func (p WorkspacePlacement) Configured() bool

Configured reports whether the placement provisions a managed workspace.

type WorkspacePlacementMode

type WorkspacePlacementMode uint8

WorkspacePlacementMode selects how a session's managed workspace root is provisioned.

const (
	// PlacementNone is the zero value: no managed workspace.
	PlacementNone WorkspacePlacementMode = iota
	// PlacementExclusive fences one canonical fixed root with an exclusive root lease.
	PlacementExclusive
	// PlacementSession derives an isolated baseDir/<sessionID> root per session.
	PlacementSession
	// PlacementShared shares one canonical fixed root with no lease.
	PlacementShared
)

type WorkspaceRecoveryError

type WorkspaceRecoveryError = sessionapi.WorkspaceRecoveryError

type WorkspaceRestoreError

type WorkspaceRestoreError struct {
	Kind  WorkspaceRestoreErrorKind
	Cause error
}

WorkspaceRestoreError is the typed failure of a workspace rewind. Kind classifies the stage; Cause chains the underlying error (a *workspacestore error, a filesystem error, or the coordinator's typed acquisition/health error).

func (*WorkspaceRestoreError) Error

func (e *WorkspaceRestoreError) Error() string

func (*WorkspaceRestoreError) Unwrap

func (e *WorkspaceRestoreError) Unwrap() error

type WorkspaceRestoreErrorKind

type WorkspaceRestoreErrorKind string

WorkspaceRestoreErrorKind classifies a RestoreWorkspace failure.

const (
	WorkspaceRestoreContextDone      WorkspaceRestoreErrorKind = "context_done"
	WorkspaceRestoreFaulted          WorkspaceRestoreErrorKind = "faulted"
	WorkspaceRestoreClosing          WorkspaceRestoreErrorKind = "closing"
	WorkspaceRestorePermitFailed     WorkspaceRestoreErrorKind = "permit_failed"
	WorkspaceRestoreLeaseUnhealthy   WorkspaceRestoreErrorKind = "lease_unhealthy"
	WorkspaceRestoreSymlinkRoot      WorkspaceRestoreErrorKind = "symlink_root"
	WorkspaceRestoreSymlinkComponent WorkspaceRestoreErrorKind = "symlink_component"
	WorkspaceRestoreEscape           WorkspaceRestoreErrorKind = "path_escape"
	WorkspaceRestoreSwapFailed       WorkspaceRestoreErrorKind = "swap_failed"
	WorkspaceRestoreRollbackFailed   WorkspaceRestoreErrorKind = "rollback_failed"
	WorkspaceRestoreAppendFailed     WorkspaceRestoreErrorKind = "append_failed"
)

type WorkspaceRootBusyError

type WorkspaceRootBusyError = sessionapi.WorkspaceRootBusyError

type WorkspaceRootLeaseLostError

type WorkspaceRootLeaseLostError = sessionapi.WorkspaceRootLeaseLostError

type WorkspaceSeedError

type WorkspaceSeedError struct {
	Reason string
	Cause  error
}

WorkspaceSeedError reports an invalid or failed workspace seed: a shared placement, a non-empty root, or a materialize failure (a ref that does not resolve). It is a fail-closed NewSession failure — the session never comes up on a bad seed.

func (*WorkspaceSeedError) Error

func (e *WorkspaceSeedError) Error() string

func (*WorkspaceSeedError) Unwrap

func (e *WorkspaceSeedError) Unwrap() error

type ZeroSuppliedCommandIDError added in v0.31.0

type ZeroSuppliedCommandIDError struct{}

ZeroSuppliedCommandIDError reports that a supplied-id submit was handed the zero UUID. It is deliberately NOT SessionError{SessionIDGenerationFailed}: nothing was generated on that path, so that kind would send a reader looking for a crypto/rand failure that never happened. Reaching it at all is a caller bug — every production path validates the admitted record first — so it names the real condition.

func (*ZeroSuppliedCommandIDError) Error added in v0.31.0

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL