Help us shape the future of Go! Take the 2026 Go Developer Survey

host

package module
v0.1.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: 5 Imported by: 2

README

looprig/host

host is Looprig's Department runtime host. A Host is a tenant-scoped process that keeps runtime targets resident, consumes commands addressed to it, serves the HostLink realtime surface, and drains on release.

Host is consumed by Factory. It imports no Factory package, no web or terminal UI, and no product repository. That boundary is enforced by import_boundary_test.go, which parses the import declarations of every Go file the module owns rather than searching source text, and fails loudly if it walked nothing.

A nested module or repository under host/ would fall outside that walk by construction, so nestedModuleAllowlist is empty and a separate assertion fails on any undeclared one: a silent hole is converted into a deliberate exemption.

Centrifuge is permitted in exactly one directory, internal/realtime/hostlink/, and the scope is derived from a single constant and compared segment by segment, so a sibling package under internal/realtime/ does not inherit it.

Status

Department, residency, HostLink, warm release and drain are built. internal/sessionstoreadapter binds them to the released github.com/looprig/sessionstore v0.9.0 store, and internal/harnessadapter to github.com/looprig/harness v0.34.0.

A composed Host applies a command end to end: it attaches a disposition session, consumes its durable command stream, claims a command under its residency grant, authorizes exactly one dispatch attempt, drives the runtime with that attempt's identity and settles the command from the runtime's own durable disposition. Read the next section before you plan around it; the mechanism is measured, not cautionary.

Known limitations you must read before deploying

These are properties of the code as it stands, not a backlog. Each is written here because an operator who meets it for the first time in production has been surprised by something this file could have told them.

This entry replaced a deployment gate. It used to say a whole-Host drain request naming an empty TenantID short-circuited the tenant check, so a link authenticated as tenant-a could start a drain covering tenant-b's resident sessions — availability, not confidentiality — and that pooled multi-tenant deployment was gated on it. R-1 closed the hole and that gate is lifted. What follows is what is now refused, stated no wider than it is.

internal/realtime/hostlink/drain.go, drainScope. Two requests are refused, each with RefusalWrongDrainScope and the Core class runtime_unavailable, and each before the drain state machine — a Drainer that has begun cannot be un-begun.

  1. A request naming an empty TenantID — the whole-Host scope — is refused before the tenant comparison, because there is no tenant on it to compare. Every link this package serves is tenant-authenticated (NewMultiplexer requires a valid TenantID), so that scope has no link it may arrive on.
  2. A request naming this Host's fixed session whose requesting tenant does not currently hold that session is refused at the last rung, on a Residencies.Get of {the link's tenant, the fixed session}. Without it the resolver compared a session against a session and attributed nothing: a dedicated Host's FixedSessionID is one bare SessionID handed to every tenant's Multiplexer, so a link authenticated as tenant-b holding nothing could name it and begin the Host-wide drain that released tenant-a's session.

The attribution in (2) is sound because of capacity, and it is sound only because of capacity. Options.validatePlacement pins Capacity to 1 for dedicated placement, so a Host with a fixed session holds at most one resident session; "this tenant holds the fixed session" and "this tenant owns everything a Host-wide drain would touch" are therefore the same statement. On a Host that could hold two tenants at once they would not be, and this rung would not be sufficient.

So exactly one drain is reachable over HostLink: a dedicated Host's fixed session, asked for by the tenant that currently holds it. A pooled Host answers no HostLink drain at all: it has no fixed session, so the session scope is refused too.

A dedicated Host refuses every HostLink drain between process start and its first attach, and that is a behaviour rather than a caveat — Factory will meet it. Rung (2) reads current residency, so until a session is resident there is no tenant that holds the fixed session and every drain request is answered runtime_unavailable with RefusalWrongDrainScope. The reason is that the refusal is the correct answer: a Host holding nothing has nothing for a HostLink drain to cover, and the alternative — acknowledging a drain of nothing — would hand a placement controller a generation it could poll to "drained" without anything having been released. What a controller should do is take the process-lifecycle path (terminate the workload) for a Host it wants gone before it has been used, and reserve the HostLink drain for a Host that is actually holding the session it placed.

The too-early refusal is indistinguishable from the refusal a tenant gets when ANOTHER tenant holds this Host's fixed session — same Refusal, same reason, same Core class, because both are rung (2). It is not the refusal a foreign tenant gets: a request naming a tenant other than the link's is refused three rungs earlier with RefusalForeignTenant. So the one distinction a controller cannot make is "nothing is attached yet" versus "someone else's session is resident here".

That is deliberate and must stay. Distinguishing the two would disclose cross-tenant occupancy — telling tenant-b that somebody is resident on a Host advertising cross_tenant_isolated — which would pay for an availability fix with a confidentiality leak, and confidentiality was never what R-1 was about. The cost falls where it can be borne: a placement controller can resolve the ambiguity from its own attach state, which it has and this Host does not owe it. Do not "fix" this by splitting the refusal.

AMENDED (O7.3): rung (2) is relaxed on the OBSERVATION path, and only there. hostlink.drain_status also admits the caller that began this very drain, matched on the tenant and session the drain was begun with. Without it the terminal drained answer was unreachable to the only caller entitled to it: release drops the residency, so rung (2) started refusing the beginner at exactly the instant the answer it was waiting for became true. The mutator path is NOT relaxed — hostlink.drain after release is still refused, because beginning a Host-wide drain is attributed to a holder. It discloses nothing new: the match set is a subset of "callers that already received the acknowledgement", a tenant that never held the fixed session began nothing and is refused identically, and that is measured, after completion, by TestRelaxingRungEightForObservationDisclosesNothingToAnyoneElse. Do not generalise the relaxation to "a drain has begun" or to the session alone; either hands cross-tenant occupancy to any link that can name the fixed session.

The transport closes when the process stops, never as a drain step

This entry replaced a design that made the terminal drain answer unreachable. internal/lifecycle used to hold a Link seam and close it as the drain's last cleanup step — before settledState assigned the terminal state. So a HostLink-initiated drain destroyed the connection carrying the drained answer before that answer existed, and the disconnect a Factory was left with is Centrifuge DisconnectShutdown 3001 in every outcome — including the one settledState deliberately withholds drained for. That signal is therefore ambiguous between "release finished, delete the workload" and "FinishRelease refused, this Host still holds the lease", and Core says directly that HostLinkDrainObservation exists "for a Factory to observe without inferring release completion from a transport close".

compose.Service.Stop closes the transports now, after Wait returns. "LAST" is unchanged — it is still after every release step — and TestStopReleasesTheSessionAndClosesEveryTransportLast still holds it. A failure to close is recorded under lifecycle.StepCloseLink rather than returned. TestTheDrainOwnsNoTransportShutdown is the reflective trip-wire: no field of lifecycle.Options may be a transport shutdown.

A link-drained Host stays up and keeps answering, with Ready() == false and Live() == true. That is not a new cost — Run only leaves on ctx.Done(), so such a Host lingered before this change too; it simply lingered refusing connections. Live()'s doc used to be a sentence wider than its probe (it claimed liveness turns false once release finishes); the code was always started && !stopped, the code is the correct one, and the doc was fixed to match.

A released session's routes are dropped explicitly. Closing a transport used to drop its links' bindings as a side effect, so nothing needed to say it. With the transport surviving, sessionWork.stop calls InvalidateSession for the released key — scoped to the session, not the link, so releasing one session does not cut a Factory's routes to the others on the same connection.

A closed Host answers 503, not 400. resolve returned an anonymous error that the handler mapped to 400 "HostLink requires a valid tenant", so a Factory reconnecting to a stopped Host read a client error naming its own credential. It is errHostClosed now, answered 503.

The whole-Host drain is reachable only through the process-lifecycle path — Service.Stop calls Drainer.StartDrain with the zero scope directly and never passes through this resolver — which is the path a platform's termination signal already takes, and it is unchanged.

What this does NOT claim.

  • Not "scoped by construction". An earlier version of this entry said the fixed-session drain was scoped by construction. It was not, and a gate probe demonstrated the hole on the tree that claimed it. What scopes it is rung (2), which is a line you can point at, resting on the capacity rule above.
  • Rung (2) is a residency read, not an ownership proof. It says this tenant holds this session on this Host now. It is not the lease, it grants nothing, and it is not durable evidence of anything.
  • Not a confidentiality claim, which was never at issue: the defect was availability. No tenant could read another's material or address its sessions.
  • Not a Factory drain path. A cmd/controller gets no drain over HostLink; if one is needed it must be tenant-scoped or service-principal, and re-admitting the empty tenant would reopen exactly what this closed.

Measured, in both halves, by behaviour and not by reading: TestATenantLinkCannotDrainAnotherTenantsSessions (two tenants, both holding live resident sessions, one shared Drainer) and TestADedicatedHostRefusesADrainFromATenantThatDoesNotHoldItsSession in internal/compose; TestAWholeHostDrainIsRefusedOnEveryTenantAuthenticatedLink and TestADrainOfTheFixedSessionIsRefusedUnlessTheLinkSTenantHoldsIt in hostlink.

Closed as O7.1-hostwide-drain-crosses-tenants / R-1.

A composed Host applies a command, and what that word means

This entry replaces three earlier ones, and the first two were wrong in the same direction — a sentence wider than its evidence. The first said a Host "can consume its inbox against a real store", which was an ADAPTER result stated about a composed Host. The second said a composed Host "cannot attach a disposition session, so it consumes nothing", which was true until the attach-time journal fence was removed. The third said a composed Host deliberately refuses to dispatch, and that is now false too: it described commands.NoDispatch, which has been deleted, and listed as future work the three-repository sequence that has since landed.

What a composed Host does. It attaches a disposition session, reaches BeginOwnership, starts the session's durable command consumer, and for each command in acceptance order: claims it under the residency GRANT this store issued (ClaimDispositionCommand takes a *ResidencyGrant, not an epoch, so a caller cannot name one); authorizes one attempt, recording an immutable identity together with the RUNTIME's own journal grant; dispatches it, carrying that identity to the runtime; and settles it from the durable disposition the runtime wrote. It steps over terminal records and advances the durable consumption cursor.

This is measured in composition (internal/compose.TestAComposedHostAppliesADeliveredCommandEndToEnd) and end to end against both released stores on separate backends (internal/settlement.TestACommandAppliesAndSettlesAcrossBothReleasedStores).

What applied means, exactly, and it is narrower than the word. Under the attempt's journal grant, the runtime durably recorded that it accepted this command into its execution path. It does not mean a turn started; it does not mean a turn folded; it does not mean a later TurnRejected cannot follow; and it does not mean a queued input survives a crash — the runtime's loop mailbox is in memory. Do not widen this sentence, including in any user-facing vocabulary: a product-level "applied" for an input that a crash then discards is a claim this Host does not make.

Host authors no outcome. SettleDispositionCommand takes a command, a revision and a residency and nothing else; the store derives what it expects from its own immutable record, obtains the evidence through its configured reader and verifies it before the write. Host reports the arm the store chose.

Missing evidence is not proof that nothing was applied. A settlement that cannot read a disposition leaves the command applying at an unmoved revision and blocks the pass. A Host that re-dispatched on absence would re-drive an effect that may already have committed. Host's honest exactly-once claim is "at most one authorized attempt, settled only from the runtime's durable disposition".

There is no Host-authored rejection once an attempt exists, and none before one either: the only pre-attempt reason to reject is the apply deadline, and §10.4 gives that to Factory's deadline reconciler. RejectDispositionCommand is deliberately not bound.

What a deployment must wire, and what Host cannot check

A disposition session's journal is not in the orchestration store. The immutable binding says where it is, and a harness Store answers only for the keyspace it owns — it holds no registry of the bindings it serves, so it cannot route on Binding.StorageBindingID. A product therefore supplies Bootstrap.JournalStores(), one reader per binding; Host builds the router and hands it to Bootstrap.Store(ctx, evidence), which must open the store with sessionstore.WithDispositionEvidence.

Three wiring conditions are unenforceable, and they share one signature — each fails only after every dispatch has happened, strands the command applying with a real effect behind it, and is invisible until the first settlement:

# condition diagnosed as
(a) the product takes the reader and drops it evidence_unavailable
(b) the orchestration tenant and the journal store's tenant disagree evidence_unroutable
(c) a router key is present but names the wrong journal store evidence_unavailable

(b) and an unregistered binding are machine-distinguishable inside Host and are reported as evidence_unroutable, which points an operator at the wiring rather than at a runtime that did its job. (c) is genuinely ambiguous at settlement time and correctly so — that store is healthy, at the correct tenant, and truthfully reports it holds no such record. Closing it needs a construction-time answer (a journal store declaring which bindings it serves) that no released API offers; a harness Tenant() accessor would close (b) and not (c).

Operationally, a blocked pass answers the delivery accepted, makes no further progress, leaves Consumer.Failures() at zero (a blocked pass returns a nil error) and logs nothing. The signal is the Prometheus gauge host_sessions_command_blocked. Unlike before, a non-zero value is a fault: it means a command this Host could not settle, not one it refused to start.

Host opens no journal writer, and must not

The attach sequence used to commit an opening in-stream journal fence at step 3, before hydration. It is gone. Store.OpenJournal binds the session scope through bindSessionScopeMode(..., ProtocolModeLegacy), which conflicts with the disposition witness AcquireResidency pins — so that fence failed for every session a Host can hold, and no composed Host ever reached BeginOwnership.

Under Bundle B, where disposition is the only supported mode, the journal grant is the runtime's and Host never takes one. The journal epoch Host needs is read from the launched runtime through department.LeaseEpochReporter and reported as residency.Residency.JournalEpoch. compose.Options declares no session opener, so the absence is held by the compiler, and internal/residency.TestNoProductionFileOpensAJournalWriter holds it structurally. Do not reinstate it.

Build and test

GOWORK=off go mod tidy
GOWORK=off go test -race ./...
GOWORK=off make check

License

Apache 2.0. See LICENSE.

Documentation

Overview

Package host is the Looprig Department runtime host.

A Host owns resident sessions for one tenant: it consumes commands, keeps runtime targets resident, serves the HostLink realtime surface, and drains on release. It is consumed BY Factory and imports nothing from it; the dependency boundary that states this is enforced by import_boundary_test.go rather than by convention.

A Host is constructed from validated Options and holds a resolved, immutable configuration: identity and tenant, the HostLink address it advertises, its Department, its collaborators, and the placement, capacity and timing bounds it runs under. The Department it serves — the immutable set of launch targets, and the segregated runtime capabilities Host consumes — lives in the department subpackage. Residency, command consumption, HostLink and drain follow.

Index

Constants

View Source
const MaxCommandQueueSize = 65536

MaxCommandQueueSize bounds the command queue. A queue is bounded because an unbounded one converts backpressure into memory growth and turns a slow consumer into an OOM three minutes later, which is harder to diagnose than a refusal.

View Source
const MaxReconcileBatch = 4096

MaxReconcileBatch bounds one reconciliation pass, so a Host that has drifted far does not attempt to converge in a single unbounded sweep.

View Source
const MinClaimAttemptsBeforeDeadline = 2

MinClaimAttemptsBeforeDeadline is how many claim lifetimes must fit inside the apply deadline.

A claim that lapses mid-apply is retried by another Host. If only one claim lifetime fits inside the deadline, the first lapse spends the entire budget and the command misses its deadline with no attempt left; two means a lapsed claim still has a whole attempt in front of it.

THIS IS STRICTER THAN THE RUNBOOK, and the difference is not derived from anything written down. 04-host.md says "claim TTL below apply deadline" — merely below — and reserves the words "safely below" for the heartbeat rule alone. That asymmetry is deliberate on the runbook's part, so this constant is an engineering choice and not a reading of the spec: it will REFUSE DEPLOYMENTS THE RUNBOOK PERMITS, such as a 30s claim under a 45s deadline.

The failure model it is chosen against: at 1.5x, a claim that lapses at the end of its life leaves half an attempt before the deadline, so the retry is guaranteed to be cut off and the command fails having consumed two Hosts. At 2x the retry has a full attempt. If a deployment needs the looser rule the runbook allows, change THIS CONSTANT — do not weaken validateTiming, which is what makes the boundary testable.

View Source
const MinHeartbeatsBeforeExpiry = 3

MinHeartbeatsBeforeExpiry is how many heartbeat intervals must fit inside the registry expiry.

"Safely below" is a MARGIN, not `<`, and the difference is the whole rule. A heartbeat one nanosecond under the expiry satisfies `<` and loses the registry entry on the first scheduling hiccup, GC pause or network retry — which is to say, immediately, in production. Three means two consecutive heartbeats may be late or lost before Factory stops seeing this Host, which is the smallest number that tolerates a single miss AND the retry of it.

Variables

This section is empty.

Functions

This section is empty.

Types

type AuthVerifier

type AuthVerifier interface {
	// VerifyTenant reports whether a presented credential acts for the tenant.
	VerifyTenant(context.Context, sessionwire.TenantID, string) error
}

AuthVerifier authenticates an inbound HostLink or command caller.

type Checkpointer

type Checkpointer interface {
	Checkpoint(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID) error
}

Checkpointer commits the checkpoints a nonterminal release requires.

IT HAS NO IMPLEMENTATION IN THIS MODULE AND THAT IS A STATEMENT ABOUT WHERE THE KNOWLEDGE LIVES, not an omission to be filled in with a no-op. A release checkpoint is the runtime's conversation state and the workspace's contents, and Host holds neither: department.Runtime exposes no checkpoint method and WorkspaceProvider materializes a workspace without ever committing one. A composition that silently treated the step as done would report a drained Host whose sessions had lost whatever was not already durable, which is the one drain failure that destroys work.

So the seam is REQUIRED rather than optional. A Host composed without one does not start.

type Clock

type Clock interface {
	Now() time.Time
	NewTimer(time.Duration) *time.Timer
}

Clock is the time source.

It is FORWARD-DECLARED for O2.x's residency, heartbeat and drain timers, which is why NewTimer exists with no caller yet. An earlier version of this comment justified it as making "the timing rules below" testable without sleeping; those rules are pure arithmetic over the Options and never consult a clock, so the justification described a task that had not landed. Requiring it now is deliberate — a Host that reaches residency without a Clock would have to be reconstructed — but it is a forward dependency and says so.

type Host

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

Host is the tenant-scoped Department runtime host.

It holds a RESOLVED configuration: every option was validated once at construction and nothing here changes afterwards. Residency, command consumption, HostLink and drain are separate tasks; what this type fixes is that a Host which exists is a Host whose configuration was coherent.

The fields are unexported and the accessors return values. Nothing mutable is handed out, and nothing the caller still holds can reach in: Options is taken BY VALUE, so a caller mutating its own copy afterwards changes nothing here. The two reference-typed fields are deliberate exceptions with reasons rather than oversights — Department is immutable by construction, so copying it would buy nothing, and the collaborator interfaces must be shared to be useful at all. Copying a SessionStore is not a defence; it is a different SessionStore.

func New

func New(options Options) (*Host, error)

New validates options and returns a Host.

It returns *InvalidOptionsError, carrying a stable OptionErrorCode, for every refusal. A Host with an incoherent configuration is never returned: there is no partially valid Host and no package-global default to fall back on, so a caller that ignores the error has nothing to use.

func (*Host) ApplyDeadline

func (h *Host) ApplyDeadline() time.Duration

ApplyDeadline returns the outer bound for applying an admitted command.

func (*Host) Auth

func (h *Host) Auth() AuthVerifier

Auth returns the credential verifier.

func (*Host) Capacity

func (h *Host) Capacity() uint64

Capacity returns how many sessions this Host may hold resident.

func (*Host) ClaimTTL

func (h *Host) ClaimTTL() time.Duration

ClaimTTL returns how long a command claim is held.

func (*Host) Clock

func (h *Host) Clock() Clock

Clock returns the time source.

func (*Host) CommandQueueSize

func (h *Host) CommandQueueSize() int

CommandQueueSize returns the inbound command buffer size.

func (*Host) Department

func (h *Host) Department() *department.Department

Department returns the immutable set of launch targets this Host serves.

func (*Host) FixedSessionID

func (h *Host) FixedSessionID() sessionwire.SessionID

FixedSessionID returns the session a dedicated Host is bound to, or empty for a pooled Host.

func (*Host) ID

func (h *Host) ID() sessionwire.HostID

ID returns the Host's identity.

func (*Host) InternalEndpoint

func (h *Host) InternalEndpoint() sessionwire.InternalEndpoint

InternalEndpoint returns the address Factory dials for HostLink.

func (*Host) IsolationClass

func (h *Host) IsolationClass() sessionwire.HostIsolationClass

IsolationClass returns the advertised cross-tenant boundary, which is also this Host's pooled admission rule. There is no TenantID accessor beside it: see Options.IsolationClass for H8.

func (*Host) Placement

func (h *Host) Placement() sessionwire.HostPlacement

Placement returns the admission model this Host runs under.

func (*Host) ReconcileBatch

func (h *Host) ReconcileBatch() int

ReconcileBatch returns the bound on one reconciliation pass.

func (*Host) ReconcileInterval

func (h *Host) ReconcileInterval() time.Duration

ReconcileInterval returns how often reconciliation runs.

func (*Host) RegistryExpiry

func (h *Host) RegistryExpiry() time.Duration

RegistryExpiry returns how long an advertisement survives without a refresh.

func (*Host) RegistryHeartbeat

func (h *Host) RegistryHeartbeat() time.Duration

RegistryHeartbeat returns the advertisement refresh interval.

func (*Host) SessionStore

func (h *Host) SessionStore() SessionStore

SessionStore returns the durable session state collaborator.

func (*Host) WarmTTL

func (h *Host) WarmTTL() time.Duration

WarmTTL returns how long a released session stays warm before drain.

func (*Host) Workspaces

func (h *Host) Workspaces() WorkspaceProvider

Workspaces returns the workspace collaborator. It does not checkpoint; see WorkspaceProvider.

type InvalidOptionsError

type InvalidOptionsError struct {
	Code   OptionErrorCode
	Field  string
	Reason string
	Cause  error
}

InvalidOptionsError reports an option Host may not run with.

Code is what a caller branches on, Field names the option, Reason is for a person, and Cause carries the typed error a lower layer produced — Core's *RequestValidationError for an endpoint, for instance — so errors.As reaches it instead of a caller substring-matching a sentence.

func (*InvalidOptionsError) Error

func (e *InvalidOptionsError) Error() string

func (*InvalidOptionsError) Unwrap

func (e *InvalidOptionsError) Unwrap() error

Unwrap returns the typed error this one was built from, if any.

type OptionErrorCode

type OptionErrorCode string

OptionErrorCode is a stable, machine-readable reason an option was refused.

It exists for the reason department.DefinitionErrorCode does: Reason is free text a caller must not branch on, and a constructor that refuses a dozen ways is useless to a caller who cannot tell which one happened without matching a sentence that will be reworded.

const (
	OptionErrorCodeMissing         OptionErrorCode = "missing"
	OptionErrorCodeInvalid         OptionErrorCode = "invalid"
	OptionErrorCodeUnknownEnum     OptionErrorCode = "unknown_enum"
	OptionErrorCodeNotPositive     OptionErrorCode = "not_positive"
	OptionErrorCodeAboveBound      OptionErrorCode = "above_bound"
	OptionErrorCodeHeartbeatMargin OptionErrorCode = "heartbeat_margin"
	OptionErrorCodeClaimMargin     OptionErrorCode = "claim_margin"
	OptionErrorCodeDedicatedFixed  OptionErrorCode = "dedicated_requires_fixed_session"
	OptionErrorCodeDedicatedCap    OptionErrorCode = "dedicated_requires_capacity_one"
	OptionErrorCodePooledFixed     OptionErrorCode = "pooled_rejects_fixed_session"
)

type Options

type Options struct {
	// HostID identifies this Host to Factory and in HostLink bindings.
	HostID sessionwire.HostID

	// InternalEndpoint is the credential-free WebSocket address Factory dials.
	InternalEndpoint sessionwire.InternalEndpoint

	// IsolationClass is advertised with capacity so Factory can place
	// cross-tenant work correctly, AND IT IS THIS HOST'S POOLED ADMISSION RULE.
	// Pooled placement alone does not establish the boundary.
	//
	// THERE IS NO TenantID BESIDE IT, and that is human gate H8, answered
	// 2026-09-04 as option (a). A Host used to be constructed with a fixed
	// tenant and the residency manager refused every request naming another, so
	// a pooled deployment needed one Host Deployment per tenant and this field
	// — which spec §12 says decides the question — had no reader that could
	// matter. A Host advertising cross_tenant_isolated may hold several tenants
	// resident; one advertising tenant_exclusive is exclusive by PLACEMENT, not
	// by construction, which is §12's own wording: "Factory placement enforces
	// the restriction". internal/service's admission ledger holds the local
	// backstop for it, atomically, because the ledger's lock is the only
	// Host-wide critical section an admission passes through.
	IsolationClass sessionwire.HostIsolationClass

	// Department is the immutable set of launch targets this Host serves.
	Department *department.Department

	// SessionStore, Workspaces, Clock and Auth are Host's collaborators.
	SessionStore SessionStore
	Workspaces   WorkspaceProvider
	Clock        Clock
	Auth         AuthVerifier

	// Placement is the admission model this Host runs under.
	Placement sessionwire.HostPlacement

	// Capacity is how many sessions this Host may hold resident. Dedicated
	// placement requires exactly one.
	Capacity uint64

	// FixedSessionID binds a dedicated Host to one session for its lifetime. It
	// is REQUIRED for dedicated placement and REJECTED for pooled.
	FixedSessionID sessionwire.SessionID

	// WarmTTL is how long a released session stays warm before drain.
	WarmTTL time.Duration

	// RegistryHeartbeat and RegistryExpiry are the advertisement lifetime.
	RegistryHeartbeat time.Duration
	RegistryExpiry    time.Duration

	// ClaimTTL and ApplyDeadline bound command application.
	ClaimTTL      time.Duration
	ApplyDeadline time.Duration

	// CommandQueueSize is the inbound command buffer.
	CommandQueueSize int

	// ReconcileInterval and ReconcileBatch bound reconciliation.
	ReconcileInterval time.Duration
	ReconcileBatch    int
}

Options configures a Host. Every field is required unless its documentation says otherwise; nothing here is discovered, and there is no package-global default Host to fall back on.

type Registrar

type Registrar interface {
	Register(context.Context) ([]department.Registration, error)
}

Registrar produces the Department registrations one deployment serves.

IT IS CALLED ONCE, AT STARTUP, AND ITS RESULT IS IMMUTABLE. Department.New validates the registrations and fixes them for the life of the process, so a Registrar that answered differently later would be answering a question nobody asks again. It takes a context because building a launch target may require I/O — reading a model catalogue, resolving a runtime build — and a startup that can block must be cancellable.

type RegistrarFunc

type RegistrarFunc func(context.Context) ([]department.Registration, error)

RegistrarFunc adapts a function to Registrar.

func (RegistrarFunc) Register

Register calls f.

type SessionStore

type SessionStore interface {
	// LoadSession returns the durable record for a session, or an error the
	// caller can classify.
	LoadSession(context.Context, sessionwire.TenantID, sessionwire.SessionID) ([]byte, error)
}

SessionStore is the durable session state Host reads and writes.

type WorkspaceProvider

type WorkspaceProvider interface {
	// EnsureWorkspace makes a workspace available and returns its root.
	EnsureWorkspace(context.Context, sessionwire.TenantID, sessionwire.SessionID) (string, error)
}

WorkspaceProvider materializes session workspaces.

It does NOT checkpoint them. An earlier version of this comment said it did, which is the doc-versus-reality drift host/CLAUDE.md exists to stop and the third time this lane has shipped a comment promising more than the code holds. Checkpointing arrives with O4.x/O6.x and will be its own method or its own interface; until then this says only what EnsureWorkspace does.

Directories

Path Synopsis
cmd
host command
Command host runs one headless Department runtime host.
Command host runs one headless Department runtime host.
Package department defines the immutable set of launch targets a Host serves.
Package department defines the immutable set of launch targets a Host serves.
internal
commands
Applying an admitted command is §10.4's durable protocol around ONE call into a runtime, and this file is that protocol.
Applying an admitted command is §10.4's durable protocol around ONE call into a runtime, and this file is that protocol.
compose
Package compose is Host's composition root: it builds the Department runtime, the residency manager, the command consumers, the live event relay, the per-tenant HostLink servers, the metrics and the drain into one running service, with an explicit start and stop order.
Package compose is Host's composition root: it builds the Department runtime, the residency manager, the command consumers, the live event relay, the per-tenant HostLink servers, the metrics and the drain into one running service, with an explicit start and stop order.
harnessadapter
Package harnessadapter binds Host's department seams to the released github.com/looprig/harness v0.33.0 rig and session API.
Package harnessadapter binds Host's department seams to the released github.com/looprig/harness v0.33.0 rig and session API.
lifecycle
Package lifecycle owns this Host's drain: the single-owner state machine that stops admission, releases every resident session in order, and reports when the Host is drained.
Package lifecycle owns this Host's drain: the single-owner state machine that stops admission, releases every resident session in order, and reports when the Host is drained.
modfiles
Package modfiles enumerates Go source files owned by this module while excluding structural directories and nested repository or module boundaries.
Package modfiles enumerates Go source files owned by this module while excluding structural directories and nested repository or module boundaries.
realtime/hostlink
Package hostlink owns the authenticated realtime server used by Factory to reach one Host.
Package hostlink owns the authenticated realtime server used by Factory to reach one Host.
registry
Package registry indexes the sessions a Host currently holds resident.
Package registry indexes the sessions a Host currently holds resident.
residency
Package residency creates and restores sessions under the durable session lease, in one fixed order, and releases everything it took when any step of that order fails.
Package residency creates and restores sessions under the durable session lease, in one fixed order, and releases everything it took when any step of that order fails.
service
Package service derives what a Host publishes about itself, and relays what its resident sessions have already committed.
Package service derives what a Host publishes about itself, and relays what its resident sessions have already committed.
sessionstoreadapter
Package sessionstoreadapter binds Host's narrow local store seams to the released github.com/looprig/sessionstore v0.7.0 Store.
Package sessionstoreadapter binds Host's narrow local store seams to the released github.com/looprig/sessionstore v0.7.0 Store.
settlement
Package settlement holds the end-to-end proof that one command APPLIES and SETTLES across the three modules that had to release for it to be possible.
Package settlement holds the end-to-end proof that one command APPLIES and SETTLES across the three modules that had to release for it to be possible.
testkit
Package testkit holds test doubles shared by this module's tests.
Package testkit holds test doubles shared by this module's tests.

Jump to

Keyboard shortcuts

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