session

package
v0.34.0 Latest Latest
Warning

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

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

README

pkg/session

pkg/session exposes the live data-plane and control-plane contracts for one running rig. It is the surface a consumer (TUI, CLI, HTTP, test) holds and drives; session construction and restoration are owned exclusively by pkg/rig.

Operation hooks are also installed at the rig boundary, not on a live session. Pass a hook.Set with rig.WithHooks when defining the rig. Every new or restored session then uses that immutable compiled policy for new native-loop work and checked journal appends; sessions expose no mutable hook registry.

It is a contracts package — it defines interfaces, not implementations. The concrete Session lives in internal/sessionruntime and is returned to callers as session.SessionController.

What is session?

Two session views live here, layered by trust, plus a set of segregated capabilities a consumer discovers by type assertion:

  • Session — the ordinary data plane: identity, the active loop, the loop registry, submit, compact, subscribe, respond to a gate, interrupt.
  • SessionController — the trusted policy and lifecycle view. It embeds Session and adds SetActiveLoop, LoopController, CheckpointWorkspace, RestoreWorkspace, Shutdown.
  • GateHost — the capability to raise a host-owned gate (a form or an out-of-band URL) and wait on its answer. A separate contract from SessionController, because opening a gate is not part of running a session and the two roles have different holders (an MCP binding servicing an elicitation is a GateHost; the client that answers is a Session).
Segregated capabilities

None of these is a method on Session or SessionController. Each is obtained by asserting on the value rig returns, and a false result means "this session cannot do that", never an error. The argument for keeping each one out of the two views is the same one spelled out for GateHost below.

  • CommittedPublicEventSource / CommittedPublicEventProvider — a live event stream whose every delivery carries the exact canonical bytes the durable append stored. Discovered through the provider rather than a bare assertion, because the capability is a property of the session's persistence, not of its Go type.
  • IdleWaiterWaitIdle, whole-session quiescence. Most controller consumers submit work; a supervisor that waits usually does not submit.
  • LivenessDone() <-chan struct{}, closed when teardown begins. A broadcast a drain supervisor can select on, deliberately not an Alive(ctx) error poll. It is the same interface as pkg/serve.SessionDone, duplicated because serve does not import this package.
  • ReleaserReleaseResidency, giving up this process's resident runtime while leaving the logical session restorable. Named in full because it is nonterminal: unlike Shutdown it appends no SessionStopped. The live runtime does not implement it yet, so the assertion returns false today; see task H4.2.

Everything else exported here is an error type. pkg/session is contracts plus errors, and a test enforces exactly that.

How to use

// From pkg/rig:
session, err := r.NewSession(ctx)
if err != nil { return err }
defer session.Shutdown(ctx)

// Submit user input to the primary loop.
inputID, err := session.Submit(ctx, []content.Block{
    &content.TextBlock{Text: "Summarize README.md."},
})
if err != nil { return err }

// Or target a specific loop (e.g. a delegate).
_, err = session.SubmitToLoop(ctx, reviewerLoopID, blocks)

// Subscribe to the event stream before submitting so no event is missed.
sub, err := session.SubscribeEvents(nil)
go func() {
    for delivery := range sub.Events() {
        handleEvent(delivery.Event)
        if delivery.Event.EndsTurn() { /* ... */ }
    }
}()

// Answer a permission gate raised by a tool.
session.RespondGate(ctx, gate.GateResponse{
    GateID: openGateID,
    Action: gate.ApproveActionApprove,
})

// Trust a host-owned gate (a form an MCP integration raised, e.g.):
host, ok := session.(session.GateHost)
if ok {
    id, _ := host.OpenHostGate(ctx, loopID, gate.KindForm, payload)
    answer, _ := host.AwaitGateAnswer(ctx, id)
    _ = answer
}

Sibling packages

  • pkg/rig — the composition root that returns a SessionController.
  • pkg/loop — the loop.Handle / loop.Controller values the session exposes.
  • pkg/eventevent.EventFilter and event.Subscription for SubscribeEvents.
  • pkg/gategate.GateResponse for RespondGate and gate.Gate/gate.Payload for OpenHostGate.
  • pkg/hook — rig-installed guards and around observers for native runtime operations and checked journal appends.
  • pkg/workspacestoreworkspacestore.Ref for CheckpointWorkspace / RestoreWorkspace.

How it is designed

The session is a coordinator with no goroutine. It owns a *hub.Hub, a loop registry, the journal, and the workspace placement; methods serialize access with normal RWMutexes. The loops are the actors; the session dispatches commands to them and fans their events back in through the hub.

                       Consumer (TUI / CLI / HTTP / test)
                                  │
                                  │  Submit / SubmitToLoop / SubscribeEvents
                                  │  RespondGate / Interrupt / Shutdown
                                  ▼
                ┌─────────────────────────────────────────────────┐
                │ SessionController (this package — contracts)     │
                │  • Submit → command.UserInput → active loop       │
                │  • SubmitToLoop → command.UserInput → target loop │
                │  • RespondGate → command.ApproveToolCall / Deny…  │
                │  • Interrupt → priority Interrupt to every loop   │
                │  • Shutdown → priority Shutdown, drain, close     │
                └──────┬────────────────────────┬───────────────────┘
                       │                        │
            publish    │                        │  dispatch
                       ▼                        ▼
                ┌──────────────┐         ┌────────────────────┐
                │  pkg/hub     │ ◀────  │ Loop actor          │
                │  (fan-in)    │  publish │ (internal/         │
                │              │         │  loopruntime)       │
                └──────┬───────┘         └────────────────────┘
                       │
                       ▼
                Subscribers (TUI / SSE / journal / tests)
Why GateHost is separate from SessionController

Two independent reasons, both from the interface-segregation rule:

  1. Coupling. Almost every SessionController consumer — TUI, CLI, test — submits work, watches events, and answers gates. Almost none raise one. Widening SessionController would force every implementation to grow three methods only an integration host calls.
  2. Holder. A SessionController is the session's operator; a GateHost is whatever opened a particular gate and is blocked on its answer — an MCP binding servicing an elicitation, say. The two ends of the same gate should not collapse into one god-interface.

A live session implements GateHost, so a host obtains one by asserting: host, ok := controller.(session.GateHost). The contract is host-owned gates only (gate.KindForm, gate.KindOpenURL with gate.ResolverSession); there is deliberately no way to mint a permission or ask-user gate through it, because a host that could mint one could park — or forge an approval against — a loop that is not its own.

Documentation

Overview

Package session exposes the live session data-plane and control-plane contracts. Session construction and restoration are owned exclusively by package rig.

Index

Constants

View Source
const (
	RestoreRuntimeMissing            = "missing_runtime"
	RestoreRuntimeUnavailable        = "runtime_unavailable"
	RestoreRuntimeTargetMismatch     = "target_mismatch"
	RestoreRuntimeCredentialMismatch = "credential_mismatch" // #nosec G101 -- closed error-category label, not a credential
	RestoreRuntimeEffortMismatch     = "effort_mismatch"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type AcceptAllDecider

type AcceptAllDecider struct{}

AcceptAllDecider accepts every assessment. It backs the deprecated WithAllowConfigMismatch shim (wired in a later task).

func (AcceptAllDecider) DecideRestore

type AgentNameMismatchError

type AgentNameMismatchError struct{ Persisted, Configured identity.AgentName }

func (*AgentNameMismatchError) Error

func (e *AgentNameMismatchError) Error() string

type CommittedPublicEventProvider added in v0.31.0

type CommittedPublicEventProvider interface {
	CommittedPublicEvents() (CommittedPublicEventSource, bool)
}

CommittedPublicEventProvider is implemented by a session that MAY be able to serve committed public events. CommittedPublicEvents reports the capability: ok is false — with a nil source — when this session's persistence cannot report the exact canonical bytes it stored, which includes a headless/no-persistence session and one over a journal that predates the committed-bytes seam.

The two-result form is the point. A single-result form would hand back a source that fails only once the consumer is already subscribed and already advancing a cursor, which is exactly the shape of failure this capability exists to prevent.

type CommittedPublicEventSource added in v0.31.0

type CommittedPublicEventSource interface {
	// SubscribeCommittedPublicEvents attaches a consumer to the committed public
	// stream with the given filter. The caller must Close the returned subscription.
	SubscribeCommittedPublicEvents(event.EventFilter) (event.Subscription, error)
}

CommittedPublicEventSource is the segregated committed-public-event capability: a live event stream on which EVERY delivery carries the exact canonical public body the durable append stored, the public EventID it committed under, and a CoveredThrough watermark equal to that append's own sequence.

It is a SEPARATE contract from Session.SubscribeEvents, and the difference is not cosmetic. SubscribeEvents is the compatibility stream: it serves a TUI/CLI on a headless session with no persistence at all, it carries ephemeral events, and it promises nothing about bytes. This one promises committed bytes on every delivery, which a session whose persistence cannot report the stored bytes is unable to keep. Folding it into Session would force every implementation to advertise a guarantee only some of them can honor — and a consumer that joins a durable tail to a live stream would have no way to learn, before it starts persisting cursors, that this session was not one of them.

A consumer obtains one through CommittedPublicEventProvider rather than a bare type assertion, because the capability is a property of the session's persistence, not of its Go type. Two notes for a consumer building tail-join logic on this stream.

First, the live delivery is the more available of the two sources. It always carries the committed bytes; the durable public read does not, for a body large enough to be offloaded above the released reader's inline ceiling (see the caveat on event.Delivery.PublicBody, which states the exact condition). Join by taking the live bytes as authoritative for any sequence you already hold, and never discard a held body because a read of the same sequence failed.

That covers a consumer that was connected. It does NOT cover a cold start or a reconnect: such a consumer never held those bytes, and the reader fails the whole PAGE rather than the one record, so its durable tail is unreadable at that page. Surface it as a bounded gap rather than retrying the same page forever.

Second, a subscription on this stream can terminate with *hub.SubscriptionLossError for DIFFERENT reasons that call for opposite responses. Egress overflow (a nil cause) is congestion: resubscribe and resync. A loss wrapping hub.ErrCommittedBodyMissing or hub.ErrCommitEventMismatch is a broken invariant — an enduring public event delivered without its committed body, or one whose committed append belongs to a different event — and resubscribing loops forever against a hub that cannot satisfy the contract. Check errors.Is before retrying.

type ConfigMismatchError

type ConfigMismatchError struct{ Persisted, Live event.ConfigFingerprint }

ConfigMismatchError is the legacy config-drift restore error. For a manifest-carrying session it is superseded by RestoreRejectedError, which carries a typed drift assessment; the legacy fingerprint path (a session with no ConfigManifest configured) still returns it during the deprecation window. Its formal deprecation and removal path is open question 9 in docs/plans/2026-07-16-session-versioning-migration-design.md; it is not marked Deprecated here because internal code still depends on it.

func (*ConfigMismatchError) Error

func (e *ConfigMismatchError) Error() string

type DefaultPolicyDecider

type DefaultPolicyDecider struct{}

DefaultPolicyDecider is the fail-secure default: accept when every change is Info, reject when any change is Warn.

func (DefaultPolicyDecider) DecideRestore

type GateError

type GateError struct {
	GateID gate.ID
	Kind   GateErrorKind
	Cause  error
}

func (*GateError) Error

func (e *GateError) Error() string

func (*GateError) GateErrorKind

func (e *GateError) GateErrorKind() string

func (*GateError) Unwrap

func (e *GateError) Unwrap() error

type GateErrorKind

type GateErrorKind string
const (
	GateNotFound      GateErrorKind = "not_found"
	GateNotReady      GateErrorKind = "not_ready"
	GateKindMismatch  GateErrorKind = "kind_mismatch"
	GateActionInvalid GateErrorKind = "action_invalid"
	GateCapacity      GateErrorKind = "capacity"
	GateAppendFailed  GateErrorKind = "append_failed"
)

type GateHost

type GateHost interface {
	// OpenHostGate opens g and returns its id. The gate is public and answerable
	// when it returns. The caller MUST then either AwaitGateAnswer or CloseGate;
	// abandoning it without either leaks the answer slot for the session's life.
	OpenHostGate(context.Context, uuid.UUID, gate.Gate, gate.Payload) (gate.ID, error)
	// AwaitGateAnswer blocks until the gate is answered and returns the validated
	// answer, including the form values that are absent from every durable record.
	// An answer is delivered exactly once. Cancelling the context abandons the
	// wait and frees the slot but does NOT close the gate — the gate is durable
	// state and the context is the caller's, so an opener that gives up must
	// CloseGate.
	AwaitGateAnswer(context.Context, gate.ID) (gate.Answer, error)
	// CloseGate withdraws a gate without answering it, waking any awaiter with a
	// *GateError{GateNotFound}. It is how an opener cleans up after a cancelled or
	// timed-out request.
	CloseGate(context.Context, gate.ID, gate.CloseReason) error
}

GateHost is the capability to raise a HOST-OWNED gate: to put a structured question or an out-of-band action to a human and receive the answer directly.

It is a SEPARATE contract rather than three more methods on SessionController, for two independent reasons.

The first is segregation. Opening a gate is not part of running a session: almost every consumer of a SessionController — the TUI, the CLI, a test — submits work, watches events, and answers gates, and none of them raise one. Widening SessionController would force every implementation to grow three methods that only an integration host calls, which is the exact coupling the interface rules forbid.

The second is that the two contracts have different holders. A SessionController is the session's operator. A GateHost is whatever opened a particular gate and is blocked on its answer — an MCP binding servicing an elicitation, say. Those are the two ends of the same gate, and RespondGate (on Session) is the other end: a client answers, the host receives. Keeping them separate keeps that asymmetry visible instead of collapsing both roles into one god-interface.

A live session implements it, so a host obtains one by asserting on the controller rig returns:

host, ok := controller.(session.GateHost)

The contract is host-owned gates ONLY (gate.KindForm and gate.KindOpenURL with gate.ResolverSession). There is deliberately no way to open a permission or ask-user gate through it: those are answered by resuming a parked loop, and a host that could mint one could park — or forge an approval against — a loop that is not its own. An implementation MUST refuse anything else at open time rather than at answer time, so a caller learns its request was invalid before a human is shown a prompt that can never be delivered.

type IdleWaiter added in v0.31.0

type IdleWaiter interface {
	WaitIdle(context.Context) error
}

IdleWaiter is the segregated whole-session quiescence capability: WaitIdle blocks until the session has no work in flight, the context is done, or the session has failed or stopped, in which case it returns that terminal reason rather than reporting idleness.

It is a SEPARATE contract rather than another method on SessionController for the reason segregation always applies here: almost every controller consumer — the TUI, the CLI, a test harness — submits work and watches events without ever waiting on whole-session quiescence, and a supervisor that waits usually does not submit. It is discovered by assertion, the same way runtimecommand.Provider and GateHost are:

waiter, ok := controller.(session.IdleWaiter)

That discovery asserts on the DYNAMIC type. A wrapper around a live session MUST forward WaitIdle, or it silently opts its wrapped session out and the caller sees ok == false with no error anywhere — the same hazard pkg/serve states for SessionDone, and it applies to all three capabilities here. Nothing pins that rig keeps returning the runtime type unwrapped.

The contract here is the SHAPE and the fact that a live session satisfies it. The idleness semantics are the runtime's existing ones, unchanged by this declaration; in particular a foreign primary loop is a known gap that does not reach whole-session idle, and nothing in this interface repairs that.

type LeaseEpochReporter added in v0.33.0

type LeaseEpochReporter interface {
	// LeaseEpoch reports the epoch of the single-writer lease this process holds, and
	// whether it holds one. It does no I/O and does not block.
	LeaseEpoch() (epoch uint64, held bool)
}

LeaseEpochReporter is the segregated single-writer-lease-epoch reporting capability: it answers which journal lease epoch THIS resident process currently holds for the session.

It exists because that number has no other route out of the runtime while the session is alive. It is already published on the way OUT — event.SessionResidencyReleased carries the epoch the releasing process held — but a live consumer that must STAMP it onto something had no way to read it, and the fact is not otherwise derivable: the epoch is minted by the storage lease the composition root acquired, and nothing on Session or SessionController returns it.

WHICH EPOCH. A deployment typically has two monotonic per-session counters, and they are not the same number and must never be substituted for one another. This one is the journal's single-writer lease epoch — the fence the runtime stamps into its opening LeaseFence and the value runtimecommand.Admitted.LeaseEpoch is checked against. An orchestrator's own residency/ownership epoch is a different grant with a different issuer; that the two often coincide early in a session's life (each counter starting at 1 under a fresh in-memory backend) is an accident of initial conditions, not a relationship. Source this from here, never from the caller's own grant.

THE TWO RESULTS ARE THE POINT. ok reports whether this process holds a lease that reports an epoch AT ALL; the epoch is meaningful only when ok is true, and is zero otherwise. A single-result form would collapse "this session has no single-writer lease" — a headless or no-persistence session, or one simply not wired for durable commands, all legitimate configurations — into "epoch 0", which is exactly the ambiguity a consumer must resolve BEFORE it stamps a value that a fencing check will later compare for equality.

ok IS ALSO FALSE ONCE THE LEASE IS GONE. Reporting is gated on the lease still being held, so a session whose lease has been released or lost answers (0, false) rather than the stale number it used to hold. That is deliberate and fail-secure: the only use for this value is to stamp work that a live fencing check will reject anyway, so handing back a dead epoch would only move the failure later. It is NOT in tension with event.SessionResidencyReleased.LeaseEpoch, whose "Zero when the session was not wired to a lease that reports an epoch" describes a snapshot taken while the lease was still held; that record is history, this is a live read.

IT IS A REPORT, NOT AN AUTHORIZATION AND NOT A RESERVATION. A true ok is a statement about the instant of the call. Nothing here holds the lease, and nothing prevents the epoch from being superseded between this read and whatever the caller does with it — so a stamped command may still be refused with a stale-epoch error, and a caller must handle that rather than treating a true ok as a promise.

Like Releaser and WorkspaceReporter it is discovered by assertion:

reporter, ok := controller.(session.LeaseEpochReporter)

and a caller MUST treat a false ok as "this session does not report a lease epoch", not as an error. A wrapper around a live session that does not forward the method silently opts its wrapped session out, which is why the discovery result is a capability answer rather than an error.

type Liveness added in v0.31.0

type Liveness interface {
	Done() <-chan struct{}
}

Liveness is the segregated teardown-broadcast capability. The channel returned by Done is closed when the session begins tearing down, so an out-of-process supervisor can select on it alongside its own cancellation instead of polling.

A broadcast, not a poll, is the deliberate shape. A drain supervisor's whole job is to block on whichever of several things happens first; an Alive(context.Context) error poll cannot be composed into that select and would have to be wrapped in a goroutine by every caller. The two are different in kind, not in style.

A receive MUST NOT be read as "teardown finished". The channel closes at the START of teardown, deliberately, so a watcher learns immediately that the session is going away rather than after the last lease is released.

DUPLICATE, KNOWINGLY: pkg/serve.SessionDone is this interface — same method, same semantics, same segregation argument — and neither type references the other in code, because serve does not import pkg/session at all. The duplication is the price of that independence, not an oversight. Change one and change the other.

type Releaser added in v0.31.0

type Releaser interface {
	ReleaseResidency(context.Context) error
}

Releaser is the segregated NONTERMINAL residency-release capability: it gives up this process's resident runtime for the session — subscriptions, actors, leases, local contexts — while leaving the logical session restorable elsewhere.

The name is ReleaseResidency and not Release because the distinction from Shutdown is the entire content of the contract. Shutdown durably appends SessionStopped and makes the logical session terminal. This does not: after it returns, the session is cold and restorable, and a registry loser that released its runtime has not ended anyone's session. The bare name loses exactly that at the boundary where a host is choosing between the two.

It is declared here as a capability discovered by assertion:

releaser, ok := controller.(session.Releaser)

and a caller MUST treat a false ok as "this session cannot be released nonterminally", not as an error. The live runtime satisfies it; a wrapper that does not forward the method silently opts its wrapped session out, which is why the discovery result is a capability answer rather than an error.

type RestoreDecider

type RestoreDecider interface {
	DecideRestore(ctx context.Context, assessment event.DriftAssessment) (RestoreDecision, error)
}

RestoreDecider answers a restore drift assessment. It runs while the restore lease is held; ctx carries the restore deadline, and a timeout is a rejection.

An ACCEPTING RestoreDecision must honor the RestoreDecision contract above: a valid Source (empty defaults to policy) and bounded Actor/Message (truncated, not rejected). A decider therefore cannot brick a restore with a malformed decision — the constructor normalizes an accepting decision before it becomes a durable ConfigurationAdopted.

type RestoreDecision

type RestoreDecision struct {
	Accept  bool
	Source  event.DecisionSource // user | policy | operator (migration reserved for Harness)
	Actor   string
	Message string
}

RestoreDecision is an application's answer to a drift assessment. Source, Actor, and Message are recorded durably on the resulting ConfigurationAdopted.

Contract for an ACCEPTING decision (Accept == true): the valid decider Sources are user | policy | operator. `migration` is RESERVED for Harness itself (a Phase-2 migration) and must never be stamped by a decider. An empty OR migration Source on accept is normalized to policy by the restore constructor, so a decider can neither omit a source nor forge a migration adoption. Actor and Message are BOUNDED audit fields: the restore constructor truncates them (to MaxConfigActorLen / MaxConfigMessageLen bytes) before writing the durable adoption, so an over-long value is silently shortened rather than bricking the restore — never rely on their full length surviving.

type RestoreDiscoveryError

type RestoreDiscoveryError struct {
	Kind      RestoreDiscoveryErrorKind
	SessionID uuid.UUID
}

func (*RestoreDiscoveryError) Error

func (e *RestoreDiscoveryError) Error() string

type RestoreDiscoveryErrorKind

type RestoreDiscoveryErrorKind string
const (
	RestoreNoSessionStarted RestoreDiscoveryErrorKind = "no_session_started"
	RestoreNoPrimerLoop     RestoreDiscoveryErrorKind = "no_primer_loop"
)

type RestoreError

type RestoreError struct {
	Kind  RestoreErrorKind
	Cause error
}

func (*RestoreError) Error

func (e *RestoreError) Error() string

func (*RestoreError) Unwrap

func (e *RestoreError) Unwrap() error

type RestoreErrorKind

type RestoreErrorKind string
const (
	RestoreLeaseFailed   RestoreErrorKind = "lease_failed"
	RestoreJournalFailed RestoreErrorKind = "journal_failed"
	RestoreReplayFailed  RestoreErrorKind = "replay_failed"
	RestoreAppendFailed  RestoreErrorKind = "append_failed"
	// RestoreAdoptionInvalid names the specific failure of building/validating the
	// durable ConfigurationAdopted (event.ValidateEvent rejected it), distinct from
	// RestoreAppendFailed (an actual journal Append failure — lost lease, storage
	// error). It lets a caller tell "the decision produced a malformed adoption"
	// apart from "the journal write failed".
	RestoreAdoptionInvalid       RestoreErrorKind = "adoption_invalid"
	RestoreLoopFailed            RestoreErrorKind = "loop_failed"
	RestoreContextDone           RestoreErrorKind = "context_done"
	RestoreIDGenerationFailed    RestoreErrorKind = "id_generation_failed"
	RestoreForeignSIDMissing     RestoreErrorKind = "foreign_sid_missing"
	RestoreForeignBuilderMissing RestoreErrorKind = "foreign_builder_missing"
	RestoreMaterializeFailed     RestoreErrorKind = "materialize_failed"
)

type RestoreRejectedError

type RestoreRejectedError struct {
	Assessment event.DriftAssessment
	Source     event.DecisionSource
	Cause      error
}

RestoreRejectedError reports a restore refused by the configured RestoreDecider (or by default policy). It carries the full typed assessment so callers and operators see exactly which fields drifted and how severely. Cause is set only when the rejection was caused by the decider ITSELF failing (a returned error or a timeout — a timeout is a rejection): it stays inspectable via Unwrap so callers can errors.As/errors.Is through to the underlying cause (e.g. context.DeadlineExceeded). A plain policy rejection leaves Cause nil.

func (*RestoreRejectedError) Error

func (e *RestoreRejectedError) Error() string

func (*RestoreRejectedError) Unwrap

func (e *RestoreRejectedError) Unwrap() error

Unwrap exposes the underlying decider failure (nil for a plain policy rejection) so errors.As/errors.Is reach it.

type RestoreRuntimeMismatchError

type RestoreRuntimeMismatchError struct {
	Kind    string
	Harness loop.AgentHarnessName
	Cause   error
}

RestoreRuntimeMismatchError reports a fail-closed adapter restore decision. Its public text is deliberately category-only: journal selectors, model keys, credentials, and catalog/provider details never reach model-facing errors.

func (*RestoreRuntimeMismatchError) Error

func (*RestoreRuntimeMismatchError) Unwrap

func (e *RestoreRuntimeMismatchError) Unwrap() error

type RuntimeRestoreRequest added in v0.27.0

type RuntimeRestoreRequest struct {
	AgentName       identity.AgentName
	Harness         loop.AgentHarnessName
	Profile         loop.RuntimeProfileName
	Source          loop.RuntimeSourceName
	Credential      loop.CredentialMode
	Target          model.ModelKey
	Effort          model.Effort
	SelectionKind   loop.RuntimeSelectionKind
	SmallModelAlias loop.ModelAlias
	Mismatch        string
	Catalog         loop.RuntimeCatalog
}

RuntimeRestoreRequest is the bounded, secret-free runtime selection context a composition may use when exact durable runtime reconstruction fails.

type RuntimeRestoreResolver added in v0.27.0

type RuntimeRestoreResolver interface {
	ResolveRuntimeRestore(context.Context, RuntimeRestoreRequest) (loop.Resolved, error)
}

RuntimeRestoreResolver lets the composition layer authorize a current runtime selection for a durable loop. Omitting it keeps exact, fail-closed reconstruction.

type Session

type Session interface {
	SessionID() uuid.UUID
	ActiveLoop() loop.Handle
	Loop(uuid.UUID) (loop.Handle, bool)
	Submit(context.Context, []content.Block) (uuid.UUID, error)
	SubmitToLoop(context.Context, uuid.UUID, []content.Block) (uuid.UUID, error)
	Compact(context.Context) (uuid.UUID, error)
	CompactToLoop(context.Context, uuid.UUID) (uuid.UUID, error)
	SubscribeEvents(event.EventFilter) (event.Subscription, error)
	RespondGate(context.Context, gate.GateResponse) error
	Interrupt(context.Context) (bool, error)
}

Session is the ordinary data-plane view of one live rig execution.

type SessionController

type SessionController interface {
	Session
	SetActiveLoop(context.Context, uuid.UUID) error
	LoopController(uuid.UUID) (loop.Controller, bool)
	CheckpointWorkspace(context.Context) (workspacestore.Ref, error)
	RestoreWorkspace(context.Context, workspacestore.Ref) error
	Shutdown(context.Context) error
}

SessionController is the trusted policy and lifecycle view of a Session.

type SessionError

type SessionError struct {
	Kind  SessionErrorKind
	Cause error
}

func (*SessionError) Error

func (e *SessionError) Error() string

func (*SessionError) Unwrap

func (e *SessionError) Unwrap() error

type SessionErrorKind

type SessionErrorKind string
const (
	SessionIDGenerationFailed            SessionErrorKind = "id_generation_failed"
	SessionLoopIDGenerationFailed        SessionErrorKind = "loop_id_generation_failed"
	SessionLoopExited                    SessionErrorKind = "loop_exited"
	SessionLoopNotFound                  SessionErrorKind = "loop_not_found"
	SessionEventChannelClosed            SessionErrorKind = "event_channel_closed"
	SessionContextDone                   SessionErrorKind = "context_done"
	SessionClosing                       SessionErrorKind = "session_closing"
	SessionFaulted                       SessionErrorKind = "session_faulted"
	SessionLoopDepthExceeded             SessionErrorKind = "loop_depth_exceeded"
	SessionLoopQuotaExceeded             SessionErrorKind = "loop_quota_exceeded"
	SessionForeignBuilderMissing         SessionErrorKind = "foreign_builder_missing"
	SessionCompactionUnsupported         SessionErrorKind = "compaction_unsupported"
	SessionDelegateIntentAppendFailed    SessionErrorKind = "delegate_intent_append_failed"
	SessionDelegateAdmissionCommitFailed SessionErrorKind = "delegate_admission_commit_failed"
)

type TurnRejectedError

type TurnRejectedError struct{ Reason event.RejectReason }

func (*TurnRejectedError) Error

func (e *TurnRejectedError) Error() string

type WorkspaceNotConfiguredError

type WorkspaceNotConfiguredError struct{}

func (*WorkspaceNotConfiguredError) Error

type WorkspaceRecoveryError

type WorkspaceRecoveryError struct {
	Path   string
	Reason string
	Cause  error
}

WorkspaceRecoveryError reports that a per-session workspace destination could not be established or recovered safely. Path identifies the refused filesystem object, Reason is a stable diagnostic, and Cause preserves any syscall failure.

func (*WorkspaceRecoveryError) Error

func (e *WorkspaceRecoveryError) Error() string

func (*WorkspaceRecoveryError) Unwrap

func (e *WorkspaceRecoveryError) Unwrap() error

type WorkspaceReporter added in v0.31.0

type WorkspaceReporter interface {
	WorkspaceStatus() WorkspaceStatus
}

WorkspaceReporter is the segregated workspace-boundary reporting capability: it answers where the session's managed workspace is and which checkpoint it came up on.

It exists because the two facts have no other route out of the runtime. A Host in another module holds a SessionController; the boundary lives on the concrete runtime type, so without a contract here a caller could not even NAME the return type to declare a local interface for it.

Like Releaser it is discovered by assertion:

reporter, ok := controller.(session.WorkspaceReporter)

and a caller MUST treat a false ok as "this session does not report a workspace boundary", not as an error — a session composed without a managed workspace is a legitimate configuration, not a failure. It is segregated rather than folded onto SessionController for the same reason Releaser is: a wrapper that does not forward the method opts its wrapped session out, and that is a capability answer.

It is deliberately READ-ONLY and deliberately separate from CheckpointWorkspace / RestoreWorkspace, which are workspace CONTROL and already live on SessionController. Reporting a boundary and moving one are different authorities.

type WorkspaceRootBusyError

type WorkspaceRootBusyError struct {
	Root        string
	HolderEpoch uint64
	Cause       error
}

WorkspaceRootBusyError reports that an exclusive workspace root is already leased by another session. HolderEpoch is copied from the storage refusal so callers never need an internal runtime type to diagnose contention.

func (*WorkspaceRootBusyError) Error

func (e *WorkspaceRootBusyError) Error() string

func (*WorkspaceRootBusyError) Unwrap

func (e *WorkspaceRootBusyError) Unwrap() error

type WorkspaceRootLeaseLostError

type WorkspaceRootLeaseLostError struct{}

WorkspaceRootLeaseLostError reports that an exclusive workspace lease ended while its session was live. It is the public leaf chained by SessionFaulted.

func (*WorkspaceRootLeaseLostError) Error

type WorkspaceStatus added in v0.31.0

type WorkspaceStatus struct {
	// LogicalRoot is the session-derived, model-visible workspace path. Empty when the
	// session has no managed workspace or no session identity.
	LogicalRoot string
	// Root is this process's physical workspace root.
	Root string
	// CheckpointSeq is the JOURNAL sequence of the workspace transition the live tree
	// was materialized from — the last checkpoint or rewind in the replayed stream. It
	// is read from the journal's own sequence, so it is available after a crash and not
	// only after a clean release. Meaningless when HasCheckpoint is false.
	CheckpointSeq uint64
	// HasCheckpoint distinguishes "anchored at sequence 0" from "never checkpointed".
	HasCheckpoint bool
	// PostCheckpointEvents counts the loop-scoped durable records that follow that
	// transition: journalled loop work that MAY have mutated the workspace after the
	// tree the restore materialized. Nothing inspects a record for whether it actually
	// touched the workspace, so this is a deliberate OVER-APPROXIMATION — it is a count
	// of records, not of losses, and it is the input to PostCheckpointLoss rather than a
	// measure of how much was lost.
	PostCheckpointEvents int
}

WorkspaceStatus is what a session reports about the managed workspace it came up on: where the workspace is, and which durable checkpoint the live tree was materialized from.

It is a REPORT, never a repair. Nothing on it recovers a mutation, bounds how much any record lost, or prevents anything. A session with no managed workspace reports the zero value — there is no boundary to name and no tree to have lost anything from.

The two roots answer different questions and must not be collapsed. Root is THIS process's physical location and varies with the Host's runtime root. LogicalRoot is the session-derived path the same tree is exposed at inside the agent/tool namespace; it is derived from session identity alone, so a session released by one Host and restored by another keeps it, which is what makes a previously journalled "read this file" still resolve.

FRESHNESS IS NOT UNIFORM ACROSS THESE FIELDS. Root and LogicalRoot are live. The boundary fields — CheckpointSeq, HasCheckpoint, PostCheckpointEvents — are AS OF RESTORE and never refresh: a session that checkpoints again after coming up still reports the boundary it came up on. A caller polling this for a live checkpoint position is reading the wrong thing.

func (WorkspaceStatus) PostCheckpointLoss added in v0.31.0

func (s WorkspaceStatus) PostCheckpointLoss() bool

PostCheckpointLoss reports whether the journal records loop work after the transition the live tree was materialized from — the divergence a restore must not present silently.

It requires HasCheckpoint. With no checkpoint in the stream there is no such transition: the restore materializes nothing and leaves the live tree exactly as it found it, so a never-checkpointed warm restart has lost nothing and must not raise an alarm, however much loop work the journal holds. Reading PostCheckpointEvents without this gate is the false positive that would fire on every such restart.

A true result is a MAY, not a DID: see PostCheckpointEvents.

Jump to

Keyboard shortcuts

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