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

sessionstoreadapter

package
v0.18.1 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package sessionstoreadapter binds Host's narrow local store seams to the released github.com/looprig/sessionstore v0.7.0 Store.

EVERY ADAPTER HERE DELEGATES. Where residency and commands describe a concept the released module owns — the epoch fence, the inbox compare-and-swap, the journal application prefix, the durable Host route — the method below is a translation of identities and a classification of errors, and nothing more. A concept re-implemented here would be a second authority for a durable rule the store already enforces, which is the failure mode O3.3 exists to end rather than one to relocate.

What this package is the audit of

Host's twelve accepted tasks were written against local interfaces and local fakes; no compiled Host file named sessionstore until O3.3. The table below is the step-1 inventory: every local seam that stands in for SessionStore, the released call it binds to, and — where there is none — the finding that replaces the binding. It is kept in the package that would have to change if any row became false.

LOCAL SEAM                          RELEASED COUNTERPART
host.SessionStore.LoadSession       Store.GetCatalogEntry            (F1, F13)
residency.SessionLeases.Acquire…    Store.AcquireResidency           (F16)
residency.Lease.Epoch               ResidencyGrant.Epoch
residency.Lease.Lost                ResidencyGrant.Lost              (F3, closed)
residency.Lease.Release             ResidencyGrant.Release
Store.OpenSession                   Store.OpenJournal                (F2, unwired)
commands.JournalWrites.AppendAp…    JournalWriter.Append             (F2, unwired)
residency.DurableStore.LoadSess…    Store.GetCatalogEntry            (F4, F5, F13)
residency.Workspaces.Ensure/Rel…    none                             (F6)
host.WorkspaceProvider.Ensure…      none                             (F6)
residency.Locations.PublishResi…    Store.PutHostRegistration        (F14)
residency.Locations.TombstoneRe…    Store.ClearHostRegistration
commands.Inbox.ListOrdered          none                             (F7)
commands.Cursors.LoadCursor         none                             (F8)
commands.CursorWrites.SaveCursor    none                             (F8)
commands.CommandRecords.LoadCom…    Store.GetCommand                 (F11, unwired)
commands.CommandRecords.LoadPay…    Store.GetCommand                 (F9)
commands.Applications.FindAppli…    Store.FindCommandApplication
commands.Gates.LoadGate             Store.ReadGates                  (F10)
commands.InboxWrites.ClaimComma…    Store.ClaimCommand               (F12, unwired)
commands.InboxWrites.BeginApply…    Store.BeginApplyingCommand       (unwired)
commands.InboxWrites.CompleteCo…    Store.CompleteCommand            (unwired)
commands.InboxWrites.RejectComm…    Store.RejectCommand              (unwired)
commands.Fence.Held/Lost/Write      none                             (F3)

"UNWIRED" IS A STATEMENT ABOUT THE COMPOSITION, NOT ABOUT THIS PACKAGE. Every row so marked is bound, exported and tested against the released store here, and no composed Host calls it: compose.Options declares no session opener and no application seams, and the Processor a composed Host runs is the removed dispatch refusal. They are the legacy command family, which a Host cannot reach on a session it can hold, and they are what the attempt-aware applier will be rebuilt from rather than dead weight. Do not re-wire one without reading commands/dispatch.go.

Seams NOT in this table stand in for nothing durable: registry.LocalRegistry, residency.HeartbeatRegistry, residency.TeardownObserver, residency.Ownership, residency.OwnershipHandle, residency.Admissions, commands.Processor, hostlink.Residencies, hostlink.Admission, hostlink.CommandConsumer(s) and hostlink.Authenticator are each satisfied by a Host type in this module. They are seams for composition, not stand-ins for a released module, and binding them here would invent a dependency rather than record one. host.AuthVerifier is the one seam whose counterpart is a module Host does not name at all (credentials/secrets), so it stays local by the same rule that kept drain out.

Findings — the both-directions audit of step 2

Each row is a place a fake exhibits behaviour the released module does not, or a released behaviour no fake can produce. They are numbered because they are cited from the code that has to live with them.

F1. host.SessionStore.LoadSession returns opaque []byte and the store returns a decoded CatalogEntry. The direction that fails is store→fake: no released call answers with bytes, so the adapter must choose an encoding, and any choice it makes is Host's rather than the store's. The seam has one production caller shape (existence and classification), so the adapter answers with the record's canonical JSON and callers are told not to parse it.

F2. THE LEASE AND THE OPENING FENCE ARE ONE CALL, not two — AND THE FINDING IS NOW MOOT FOR HOST, which is worth more than the row itself. It described residency modelling AcquireSessionLease and CommitOpeningFence as separate seams while Store.OpenJournal fuses them, so a fake could produce "lease granted, fence refused, lease still held" and the released store could not. Host no longer commits an opening fence at all: the fence bound the session scope to ProtocolModeLegacy and therefore failed for every session a Host can hold, and the journal grant is the RUNTIME's. residency.JournalFencer is gone and nothing in a composed Host calls OpenSession. The row stays because Store.OpenSession is still exported here and still has this shape, and a future caller must know the fusion is the store's rather than this adapter's.

F3. LEASE LOSS IS NOT OBSERVABLE AS AN EVENT — AND THE SIGNAL EXISTS ONE LAYER DOWN. residency.Lease.Lost is a channel a heartbeat and a drain supervisor select on. storage.Leaser HAS exactly that: its contract says "Lease.Lost closes when it can no longer safely assert ownership, including Release, expiry, or takeover". sessionstore acquires such a lease inside OpenJournal and KEEPS IT UNEXPORTED — JournalWriter publishes Epoch, Sequence, Append and Close and nothing else — so the capability Host needs is present in the dependency and swallowed by the module above it. That makes this a sessionstore release owed (expose the grant's loss channel), not a missing capability.

Until then the adapter's Lost() is closed by the ADAPTER when a delegated call classifies as lease_lost or fenced, so it is SILENT FOR AN IDLE SESSION: a Host that stops writing stops learning. It is also why the takeover arm is unreachable from this package's tests — see TestLostReportsOnlyWhatThisGrantDid — and why O7.1's Fence adapter cannot be discharged by delegation; see the grantFence comment in applier_test.go.

F3 AT O3.4 — THE RESIDENCY HALF IS NOW CLOSED AND THE JOURNAL HALF IS NOT. residency.Lease is no longer satisfied by Grant at all; it is satisfied by ResidencyLease in residency.go, whose Lost() is ResidencyGrant.Lost() — Storage's own lease channel, "the provider's actual notification, never inferred from journal events, release errors or caller cancellation". So the heartbeat and the drain supervisor now select on a real signal. What remains open is exactly what the paragraphs below describe: the JOURNAL grant still publishes no loss channel, so commands.Fence and Grant.Lost are still this adapter's own echo. The paragraphs are kept rather than rewritten because the finding they record did not go away, it narrowed.

AND IT IS NOT A TAKEOVER CLAIM. ResidencyGrant's liveness is the provider's: its doc says memstore offers neither TTL nor crash takeover, so under this module's tests the only thing that closes the channel is a release. A real takeover measurement needs pgstore behind PGSTORE_TEST_DSN and this module has no such lane.

F3 AT v0.6.0 — PARTIALLY DISCHARGED, AND NOT THE PART THIS SEAM NEEDS. The rebind re-ran this row against the released module and the answer changed without the finding going away, so it is recorded rather than deleted. v0.6.0 adds Store.AcquireResidency returning a *ResidencyGrant with exactly Epoch(), Lost() and Release() — the shape residency.Lease declares, and the loss channel this row said was owed. But it is a DIFFERENT LEASE. The grant is taken in the session's ".../residency/lease" namespace, its epoch is the distinct type ResidencyEpoch whose own doc says it "must never be compared with, or used as, a journal epoch", and the type states that "residency loss does not fence a journal: a successor must independently acquire the journal grant before application". JournalWriter — the object Grant below actually wraps — still publishes Epoch, Sequence, Append and Close and NO Lost. So the journal grant's loss is still unobservable, this package's Lost() is still the adapter's own signal, and everything above about idle silence still holds.

F4. THE DURABLE STATE CARRIES NO NAMESPACE. residency.SessionState.Namespace is required whether or not the session exists, and is documented as read from the store "because the layout is SessionStore's". sessionstore v0.6.0 exposes no object namespace, prefix or layout accessor at all; the layout is internal. The adapter cannot answer this member from the store, so it is Host's to derive or SessionStore's to publish. Reported, not papered over.

F5. THE DURABLE STATE CARRIES NO RIG SESSION ID — SUPERSEDED AT v0.6.0, AND REPLACED BY THE OPPOSITE DEFECT. As written against v0.1.0 this said CatalogRecord had no member for residency.SessionState.RigSessionID and no SessionPointerKind named one. v0.6.0 adds SessionBinding, immutable at creation and carried on CatalogRecord, whose RuntimeSessionID is exactly that durable member and is reachable through the GetCatalogEntry this package already calls. The store no longer has nothing to restore from.

What replaces the finding is F11's shape in the other direction. SessionBinding.RuntimeSessionID is a bounded opaque UTF-8 string, validated by validateOpaque and by nothing else, and residency.SessionState.RigSessionID is a uuid.UUID. HOST'S SEAM IS THEREFORE TIGHTER THAN THE STORE: a binding written by an allocator that is not Harness is durable, readable and valid to the store, and cannot be represented in Host at all. That is not a reason to widen the seam blind — Host's restore really does hand the value to Harness, which really does want a UUID — but the refusal has to be Host's, stated, and reached before the launch rather than at a parse deep inside it.

RELATED TO, AND NOT THE SAME AS, H9's COST 2 in harnessruntime. They point in OPPOSITE directions over DIFFERENT subjects: this row is Host's seam being too narrow for a durable value the store will hand it, and Cost 2 is Harness's keyspace being too narrow for the identity Factory admitted. They are recorded separately because either could be fixed alone. But ONE decision about whether a Host session identity is a UUID probably settles both, so a reader resolving one should look at the other rather than assume it followed.

F6. NO RELEASED CALL MATERIALIZES A WORKSPACE BY TENANT AND SESSION. Harness's workspacestore is content-addressed — Materialize(ctx, Ref, dest) — and picks neither the destination nor the pairing. EnsureWorkspace/ReleaseWorkspace are therefore a Host responsibility over a released Ref, not a delegation.

F7. THERE IS NO PER-SESSION ORDERED INBOX READ. commands.Inbox.ListOrdered pages one session's commands strictly after an acceptance order. The store's only listing is ListDueCommands, which pages ONE CONTROL SHARD by deadline across every tenant and session, skipping rows it cannot decode. The two are not the same query and one cannot be built from the other without reading the whole shard. This is the seam the consumer's entire ordering rule stands on.

F8. THERE IS NO DURABLE CONSUMPTION CURSOR. commands.Cursors is §10.4's per-session cursor. SessionPointerKind is a closed set of three roles — active-continuation, workspace-checkpoint, runtime-checkpoint — and none is a consumption cursor. F7 and F8 together are why internal/commands has no adapter for its read path here.

F9. THE PAYLOAD IS NOT A SEPARATE LOAD. commands.CommandRecords declares LoadCommand and LoadPayload as two calls so that "the payload is loaded AFTER the claim" is a thing a test can see. InboxRecord carries Payload and PayloadRef inline, so one GetCommand answers both. The direction that fails is fake→store: a fake can refuse a payload load to a caller that has not claimed, and the store cannot — it hands the body to any reader of the record.

F10. THE GATE PROJECTION NAMES NO OWNER. commands.Gate carries OwnerHostID and OwnerEpoch, and §9.4's release-race backstop is decided from them. Store.ReadGates answers with sessionwire.GatePage, whose GateProjection has gate id, kind, prompt, opened event and sequence, deadline and answerability — and no owning Host and no lease epoch. The adapter therefore CANNOT answer LoadGate soundly and refuses rather than answering with zero owners, which would read as "owned by no Host" and make every gate response resumable.

F11. THE STORE IMPOSES NO UUID GRAMMAR ON THE RUNTIME COMMAND IDENTITY. sessionstore.RuntimeCommandID is validated as bounded opaque UTF-8, and the package says why: a grammar check would be a second statement of a rule it does not own. commands.Record and department.RuntimeCommand both declare uuid.UUID, so a durable record written by an allocator that is not Harness cannot be represented in Host at all. Measured in TestLoadCommandRefusesARuntimeIdentityThatIsNotAUUID, which first admits such a record through the store and only then finds the adapter refusing it.

F12. A LOST COMPARE-AND-SWAP IS INDISTINGUISHABLE FROM A STORE FAULT. commands.ApplyRefusal declares RefusalClaimHeld and RefusalDeadlinePassed, and the applier reaches both from its OWN precondition checks against a record it has read; the store's claim_held, deadline and terminal codes arrive after that decision and reach the applier as RefusalStore. classifyInbox promotes only InboxErrorEpoch, which is an ownership statement, and leaves the other eighteen alone — asserted in both directions by TestClassifyInboxReportsASupersededEpochAsTheSentinel and TestClassifyInboxLeavesEveryOtherCodeAlone. THE DEFERRAL IS DELIBERATE: commands/apply.go wraps a store failure with its Cause preserved, so the code stays recoverable by errors.As and only the label is coarse; promoting one would make a lost race indistinguishable from a precondition the applier evaluated, which is the identical-error mask. Whether a lost race should be separately diagnosable is O5/O6's decision, not this adapter's.

F13. A SESSION THAT NEVER EXISTED IS NOT A CATALOG ERROR. GetCatalogEntry for a session with no scope binding fails at the KEYSPACE, with *KeyspaceError{binding_not_found}, before the catalog is consulted at all. An adapter matching only CatalogErrorNotFound and CatalogErrorDeleted therefore reports the commonest case — a brand new session on the create path — as a store failure. This was found by running the create path against the store and could not have been found against residency's fakeDurable, which answers SessionState{Exists:false} and has no error to classify. All three arms are asserted, in both directions, by TestIsCatalogAbsentAdmitsEveryWayTheStoreSaysThereIsNoRecord and TestIsCatalogAbsentRefusesEveryFailureThatIsNotAbsence.

F14. THE DURABLE HOST ROUTE CARRIES NO WIRE VERSION. sessionwire.HostLinkRegistryObservation has a Version whose own Validate refuses an unsupported value; PutHostRegistrationRequest has no version member at all, and the store stamps CurrentWireVersion when it projects the record back. So a wholly delegating PublishResidency would be LOOSER than the seam it satisfies for exactly one member — it would write an observation Core refuses and read it back as current. PublishResidency checks that member rather than delegating it; see UnsupportedWireVersionError.

F15. THE ONE LEASE HOST DECLARES IS TWO GRANTS IN THE RELEASED MODULE, IN TWO EPOCH DOMAINS. residency.Lease fuses them: Epoch() is documented as the value Core publishes as the registry observation's lease_epoch, and the SAME value was what residency.JournalFencer.CommitOpeningFence stamped into the in-stream fence, before that seam was removed. v0.6.0 splits the two deliberately and gives them distinct types — ResidencyEpoch, the Host's orchestration grant, and JournalEpoch, "a runtime grant in a session's bound agent journal" — and forbids the identification in terms. Neither released type satisfies residency.Lease alone: ResidencyGrant has Lost() and an epoch that must not stamp a journal fence, JournalWriter has the journal epoch and no Lost().

THE BOUND MATTERS AND AN EARLIER VERSION OF THIS ROW GOT IT WRONG. It said the fakes' single uint64 "is a state production has no way to produce". That is FALSE IN LEGACY MODE, which is the mode Host is bound to today: Grant.Epoch in journal.go returns JournalWriter.Epoch, residency's manager takes that one uint64 at manager.go:986 and both stamps the opening fence with it and publishes it as the registry observation's lease_epoch. So in legacy mode the RELEASED STORE genuinely produces one number playing every role, and fakeLease models it correctly.

F15 IS A DISPOSITION-MODE FINDING. The conflation becomes wrong exactly where v0.6.0 introduced the split, and it has two live readers there: a disposition attempt records JournalEpoch and ResidencyEpoch as separate members, and the settlement fence orders AuthorJournalEpoch against AttemptJournalEpoch. A Host that copied its residency epoch into JournalEpoch would make a disposition command UNSETTLEABLE FOREVER. So the finding stands and is booked as O3.4; what was wrong was its scope, not its existence.

F15 IS DISCHARGED AT O3.4, BY OWNERSHIP AND NOT BY FIELD COUNT. Host does not track two grants: it HOLDS one and READS the other. residency.Lease is narrowed to the residency grant and answers residency.ResidencyEpoch; the journal epoch is department.LeaseEpochReporter, a capability of the launched RUNTIME, and reaches Host as residency.JournalEpoch — a different defined type, so the substitution no longer compiles. Three structural consequences are worth naming, because each closes a place the fusion could come back:

  • residency.JournalFencer is GONE, and its removal is the completion of this line rather than a separate decision. CommitOpeningFence first lost its epoch parameter — Host holds no journal grant, so there was no number it could soundly name — and then lost its caller: Host makes no journal write at any point, so there is nothing left for the fusion to come back into.
  • harnessruntime's WithLeaseEpoch is GONE. It let a composition supply the epoch every admitted command was applied under, and harness compares that number for equality against the lease the runtime itself holds — right only while two independent counters agreed. boundSession reads the capability per command instead.
  • the fakes are seeded from DIFFERENT BASES: residency mints residency epochs from 1000 and testkit mints journal epochs from 1. Both used to start at 1, which is why the fusion passed for twelve tasks.

The LEGACY reading above stays true and stays here: against a legacy-mode session Store.OpenJournal really does hand out one number playing every role. It is recorded rather than deleted because the correction is bounded — legacy is not a supported deployment target, not a mode in which Host was wrong.

F16. THE RESIDENCY GRANT IS ADMITTED ONLY FOR A DISPOSITION-MODE SESSION, AND A FAILED ACQUISITION CAN STILL OWE A RELEASE. Two properties of AcquireResidency that no fake in this module produces. First, it refuses any session whose immutable catalog binding is not ProtocolModeDisposition, with catalogInvalid("binding.protocol_mode"), and binds the session scope mode as a side effect; residency.SessionLeases.AcquireSessionLease grants unconditionally in every fake here. Host does not create catalog records — it only reads them — so WHICH protocol mode Host's sessions carry is decided by their creator and is a question for the program, not a defect in this module; it is recorded because a Host bound to AcquireResidency against legacy-mode sessions would fail at every attach with a validation error and no fake would have predicted it. Second, ResidencyAcquireCleanupError is an error that returns no grant AND leaves a provider lease and a Store admission held, obliging the caller to retry Release until it succeeds or Store.Close times out. AcquireSessionLease's (Lease, error) signature cannot represent "refused, and you still owe a release": every fake here treats a non-nil error as nothing acquired. THE FAKE IS LOOSER THAN THE DEPENDENCY in the way that leaks rather than the way that fails.

What step 4 could run against the adapter, and what it could not

The instruction is to run the residency, commands and hostlink suites against both the fakes and the adapters. Which of them CAN run is itself a result:

  • commands' APPLY path runs, end to end, against sessionstore v0.6.0 over Storage's memory provider. TestApplierRunsTheWholeProtocolAgainstTheReleasedStore drives commands.Applier.Process through the record read, the claim, the payload load, the applying transition and the journal application prefix, and then asserts the durable state a successor's recovery would read.
  • commands' CONSUME path cannot run at all: it is built on Inbox.ListOrdered and Cursors, and neither has a released counterpart (F7, F8).
  • residency's LOCATIONS and DURABLE-STATE seams run individually, and the ATTACH does not: it needs Workspaces, which delegates to nothing (F6), and its lease/fence sequencing is a state the released store cannot produce (F2). Building a two-seam shim over the fused call would have hidden F2 rather than reported it, so this package does not have one.
  • hostlink's seams stand in for nothing durable, so there is nothing to rebind: see the paragraph on composition seams above.

Two properties of this package's own tests, stated rather than assumed

THE FINDING ROWS CITE TESTS BY NAME AND THE CITATIONS ARE GUARDED. TestCommentsCiteTestsThatExist in the root package resolves every Test… identifier appearing in a comment in this package and in harnessruntime against the module's real test functions. It exists because two rounds of this task shipped a row whose evidence did not exist, and because nothing else notices: TestDocCommentsNameTheirOwnDeclaration asks whether a comment names the declaration it is ATTACHED to, which is a different question.

PublishResidency IS THE ONE PLACE THIS PACKAGE IS NOT PURELY MECHANICAL. Every other method here delegates; that one adds a refusal of its own for F14's member, because delegating it would be strictly looser than the seam. It is called out here so "the adapters are mechanical" is not read as covering this file without qualification.

Index

Constants

This section is empty.

Variables

View Source
var ErrForeignLease = errors.New("sessionstoreadapter: this residency lease was not issued by this store, so it carries no grant a disposition claim could be derived from")

ErrForeignLease is the refusal DispositionWriterFor returns for a lease this package did not issue.

IT IS A REFUSAL RATHER THAN A DEGRADED WRITER. A writer with no grant could not claim anything, and would discover that at the first command of a session this Host had already taken residency of — which is the failure mode the composition root exists to make impossible.

View Source
var ErrGateOwnerUnavailable = errors.New(
	"sessionstoreadapter: the released gate projection names no owning Host or lease epoch")

ErrGateOwnerUnavailable is the refusal LoadGate returns for a gate that exists on a LEGACY session. A disposition session's gate is answered by residency epoch; see LoadGate.

THE BACKSTOP IS DECIDED FROM MEMBERS THE PROJECTION DOES NOT CARRY — finding F10. commands.Gate holds OwnerHostID and OwnerEpoch, and §9.4's release-race backstop resumes a gate response only while both still name this Host's current residency. Store.ReadGates answers with sessionwire.GatePage, whose GateProjection carries the gate id, kind, prompt, opening event and sequence, deadline and answerability — and names no owning Host and no lease epoch.

ANSWERING WITH ZERO OWNERS WOULD BE WORSE THAN REFUSING, which is why this exists rather than a partially filled Gate. A zero HostID matches no live residency, so a caller comparing it against its own would refuse every gate response — but a caller comparing OwnerEpoch against zero, or one that treats "no owner" as "not owned by anyone else", would resume a gate whose runtime was released and relaunched. The seam cannot express "I know the gate is open and I do not know who owns it", so it refuses.

View Source
var ErrNilOption = errors.New("sessionstoreadapter: a nil option was supplied")

ErrNilOption is the refusal New returns for a nil option, rather than the nil dereference that would otherwise happen while applying it.

View Source
var ErrNoGateJournals = errors.New("sessionstoreadapter: no gate journals were supplied; see WithGateJournals")

ErrNoGateJournals is GateSessionFor's refusal for a store built without WithGateJournals.

View Source
var ErrNoStore = errors.New("sessionstoreadapter: no session store to adapt")

ErrNoStore is the refusal New returns when it is handed nothing to adapt.

View Source
var ErrUnroutableBinding = errors.New("sessionstoreadapter: no settlement evidence reader is registered for this session's storage binding")

ErrUnroutableBinding is the sentinel an unroutable binding carries.

IT IS A SENTINEL AS WELL AS A TYPE because the thing a caller usually wants to know is "was this a configuration gap or a real journal failure", and that is a yes-or-no question. The type carries which binding, for the operator who has to fix it.

Functions

func IsRuntimeJournal added in v0.3.0

func IsRuntimeJournal(reader sessionstore.DispositionEvidenceReader) bool

IsRuntimeJournal reports whether a journal store registered for a binding can answer RuntimeJournal: the released harness session store, or a value embedding one. A composition refuses a reader that cannot, because every create routed to it would be refused.

Types

type DispositionWriter

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

DispositionWriter is one session's disposition transitions, bound to the residency GRANT they are authorized under.

THE GRANT IS HELD RATHER THAN PASSED, and that is the structural half of the claim edge's rule. sessionstore's ClaimDispositionCommand takes a *ResidencyGrant and derives the residency epoch from it — "the epoch comes off the grant and from nowhere else, so no number a caller chose can reach the record's mark" — and a writer that took a grant per call would be handing that choice back to a caller. Binding it once, to the session the grant is for, is what makes the rule hold for every edge here at once.

THE RESIDENCY EPOCH IS STILL PASSED AT THE OTHER TWO EDGES, and that is not an inconsistency. BeginDispositionAttempt fences its epoch to EQUAL the claim's own, so it cannot raise the record's mark; SettleDispositionCommand records its epoch as settlement CONTEXT and compares it with no journal epoch. Only the claim edge writes the mark, so only the claim edge is gated on a grant.

func (*DispositionWriter) BeginAttempt

BeginAttempt CASes a claimed record into applying and records the attempt.

func (*DispositionWriter) ClaimDisposition

func (w *DispositionWriter) ClaimDisposition(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	command sessionwire.CommandID,
	claim commands.DispositionClaim,
) (uint64, error)

ClaimDisposition CASes a pending record into claimed under the bound grant.

func (*DispositionWriter) RejectDisposition added in v0.4.0

func (w *DispositionWriter) RejectDisposition(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	command sessionwire.CommandID,
	rejection commands.DispositionRejection,
) error

RejectDisposition rejects a claimed command before any attempt was authorized. The released edge refuses a record carrying an attempt and admits only the live claim's holder, so the residency passed is this Host's own and cannot move any mark.

func (*DispositionWriter) SettleDisposition

func (w *DispositionWriter) SettleDisposition(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	command sessionwire.CommandID,
	settlement commands.DispositionSettlement,
) (commands.State, error)

SettleDisposition settles an applying command from durable evidence and reports the terminal state the STORE chose.

type EventReplayers added in v0.4.0

type EventReplayers interface {
	OpenEventReplayer(uuid.UUID, harnesssessionstore.ReplayRequest) (journal.EventReplayer, error)
}

EventReplayers opens a positioned replay of one runtime session's PUBLIC events. *harness sessionstore.Store satisfies it.

type EvidenceKey added in v0.2.0

type EvidenceKey struct {
	TenantID         sessionwire.TenantID
	StorageBindingID string
}

EvidenceKey names the journal store that serves one tenant's sessions under one storage binding.

THE TENANT IS HALF OF THE KEY AND THAT IS WHAT CLOSES CONDITION (b). A harness Store files ONE tenant's sessions for its whole life and refuses an evidence request naming another, and the orchestration tenant and the journal store's tenant are configured independently with no type relating them. A router keyed on the binding alone could hand tenant-b's settlement to tenant-a's journal store, which would refuse it — correctly, and too late, after the dispatch. Keying on both turns that misconfiguration into an UnroutableBindingError at settlement naming the tenant, and lets a composition refuse a table that names no reader for a tenant it serves.

type EvidenceRouter

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

EvidenceRouter selects the settlement evidence reader that serves one session's storage binding.

func NewEvidenceRouter

func NewEvidenceRouter(readers map[string]sessionstore.DispositionEvidenceReader) (*EvidenceRouter, error)

NewEvidenceRouter builds a router over one reader per storage binding.

IT COPIES THE TABLE. A composition root that kept its map and mutated it later could otherwise re-point a live router at another store, and a settlement's evidence source must be fixed for the process's life: a command settled from one journal and a command settled from another are two different durable statements, and which one a record got would depend on when it was settled.

func (*EvidenceRouter) Bindings

func (r *EvidenceRouter) Bindings() []string

Bindings reports the storage bindings this router serves, sorted, so a composition can log what it can settle for at startup rather than at the first command that cannot be.

func (*EvidenceRouter) ReadDispositionEvidence

ReadDispositionEvidence forwards the request to the reader that serves its binding, unchanged.

NOTHING IS REINTERPRETED ON THE WAY. Every member of the request was derived by the orchestration store from its own immutable record — the pinned binding, the durable runtime mapping and the immutable attempt — and a router that rewrote one would be substituting its own answer for the store's. Only the routing binding is read, and nothing else is touched.

An unregistered binding is refused and NEVER answered empty. The released contract says a reader must NEVER report a missing record as an empty DispositionEvidence, because absence is not a disposition and the zero value is refused precisely so a reader which did so settles nothing. A router is bound by the same rule: the command is left unsettled and an operator is told which binding has no reader.

type ForeignGrantError

type ForeignGrantError struct {
	GrantTenant   sessionwire.TenantID
	GrantSession  sessionwire.SessionID
	CalledTenant  sessionwire.TenantID
	CalledSession sessionwire.SessionID
}

ForeignGrantError reports a durable write routed to a grant that does not own the session it names.

func (*ForeignGrantError) Error

func (e *ForeignGrantError) Error() string

type GateJournals added in v0.4.0

type GateJournals interface {
	GateJournal(tenant sessionwire.TenantID, storageBindingID string) (EventReplayers, bool)
}

GateJournals resolves the runtime journal a session's gates are folded from. TenantEvidenceRouter satisfies it: the journal store registered for a (tenant, storage binding) is the one the runtime writes, which is the only journal whose GateOpened events describe this session's gates.

type GateOwnerUnavailableError

type GateOwnerUnavailableError struct {
	GateID sessionwire.GateID
}

GateOwnerUnavailableError names the gate whose owner could not be read.

func (*GateOwnerUnavailableError) Error

func (e *GateOwnerUnavailableError) Error() string

func (*GateOwnerUnavailableError) Unwrap

func (e *GateOwnerUnavailableError) Unwrap() error

type GateSession added in v0.4.0

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

GateSession is one resident session's durable gate surface, bound to the residency GRANT this store issued for it.

THE GRANT IS HELD AND NEVER PASSED, for the claim edge's reason: sessionstore v0.12.0's gate writes on a disposition session take a *ResidencyGrant and refuse a bare LeaseEpoch, because the grant's epoch becomes the projection's residency mark and a caller-chosen number could lock every later holder out.

func (*GateSession) Open added in v0.4.0

func (g *GateSession) Open(ctx context.Context, projection sessionwire.GateProjection) error

Open projects one gate under the bound grant.

func (*GateSession) Projected added in v0.4.0

func (g *GateSession) Projected(ctx context.Context) ([]sessionwire.GateProjection, error)

Projected returns the gates the durable projection holds open.

func (*GateSession) Replay added in v0.4.0

func (g *GateSession) Replay(ctx context.Context, from uint64, visit func(event.Event, uint64) error) error

Replay visits the runtime session's public events from the inclusive sequence from.

func (*GateSession) Resolve added in v0.4.0

func (g *GateSession) Resolve(ctx context.Context, gate sessionwire.GateID) error

Resolve removes one gate from the projection under the bound grant.

func (*GateSession) Scope added in v0.4.0

func (g *GateSession) Scope(ctx context.Context) (gates.Scope, error)

Scope reads the session's catalog record once: the Core identity the projection is answered under, and the runtime identity its events carry, from the SAME read of the immutable binding.

type Grant

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

Grant is one open, epoch-fenced single-writer grant over a session's journal.

IT IS ONE OBJECT WHERE HOST DECLARES TWO SEAMS, which is finding F2 and the largest structural difference this task found. residency models the lease acquisition and the opening fence as independent steps, so an attach can hold a lease whose fence was refused. Store.OpenJournal acquires the lease, reads the tip exactly once, commits the opening fence at that tip and — on every failure after the grant — releases the grant before returning. There is no state in which the released store holds a lease and no fence, so this type cannot offer one and does not pretend to.

func (*Grant) AppendApplicationPrefix

func (g *Grant) AppendApplicationPrefix(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	prefix commands.Prefix,
) error

AppendApplicationPrefix commits the private correlation record before the runtime-visible effect begins.

THE LEASE EPOCH IS NOT A PARAMETER AND IS NOT SET HERE. The released writer refuses a prefix whose LeaseEpoch a caller chose and stamps its own grant onto the record, which is exactly what commands.Prefix's doc says it wants: the epoch on a prefix is the epoch that actually held the stream. The two agree, so the field is left zero deliberately rather than by omission.

func (*Grant) Epoch

func (g *Grant) Epoch() uint64

Epoch is the fencing epoch of this grant's lease.

func (*Grant) Lost

func (g *Grant) Lost() <-chan struct{}

Lost closes when this grant is known to be gone.

IT IS NOT THE STORE'S SIGNAL, AND THERE IS NO SIGNAL ON THIS GRANT — finding F3. The claim is bounded to the JOURNAL grant deliberately, because at sessionstore v0.6.0 it is no longer true of the module as a whole: a ResidencyGrant from Store.AcquireResidency does publish Lost(). It is a different lease in a different epoch domain and this type does not hold one (see F15). JournalWriter, which it does hold, exposes Epoch, Sequence, Append and Close and no loss channel or renewal callback; it learns it lost the stream by latching the failure of an append IT made. So this channel closes when a call THROUGH THIS ADAPTER classified as a lost or fenced grant, and it is SILENT FOR AN IDLE SESSION: a Host that stops writing stops learning. residency.Lease.Lost documents itself as "the FAST guard only" with the non-rebasing journal CAS as the hard backstop, and against this store the fast guard is strictly weaker than that sentence implies — it is not a timer, it is an echo of this Host's own writes.

func (*Grant) Release

func (g *Grant) Release(ctx context.Context) error

Release drops the grant. It is not termination: the session remains durable and resumable.

type InvalidEvidenceTableError

type InvalidEvidenceTableError struct {
	Binding string
	Reason  string
}

InvalidEvidenceTableError reports a routing table that could not answer.

func (*InvalidEvidenceTableError) Error

func (e *InvalidEvidenceTableError) Error() string

type NamespaceLayout

type NamespaceLayout func(sessionwire.TenantID, sessionwire.SessionID) string

NamespaceLayout answers the durable object prefix a session's runtime writes under.

IT IS A PARAMETER RATHER THAN A DELEGATION BECAUSE F4 SAYS IT HAS TO BE. residency.SessionState documents Namespace as read from the store "because the layout is SessionStore's", and sessionstore v0.6.0 publishes no layout, prefix or namespace accessor — the keyspace derivation is unexported. So the adapter cannot answer this member by delegating, and it will not answer it by inventing one either: a prefix chosen here would be a second authority for a layout the store already owns, and the two would disagree the first time either moved. A composition root that has an answer states it; one that does not gets UnavailableMemberError rather than an empty string.

type NoDispositionCatalogError

type NoDispositionCatalogError struct {
	TenantID  sessionwire.TenantID
	SessionID sessionwire.SessionID
	Cause     error
}

NoDispositionCatalogError reports a session this store will not answer disposition-family questions about.

IT IS NOT AN OWNERSHIP STATEMENT, and that is asserted rather than assumed: it unwraps to the store's own error and to nothing in residency's sentinel vocabulary, so residency.epochFence leaves the grant alone when one reaches it through a fenced write. What it IS is a permanent refusal with a name, which is what separates it from the ambiguous store failure the fence would otherwise retry forever.

func (*NoDispositionCatalogError) Error

func (e *NoDispositionCatalogError) Error() string

func (*NoDispositionCatalogError) Unwrap

func (e *NoDispositionCatalogError) Unwrap() error

type Option

type Option func(*Store)

Option configures an adapted store.

func WithGateJournals added in v0.4.0

func WithGateJournals(journals GateJournals) Option

WithGateJournals supplies the journals gates are folded from. Without it, GateSessionFor refuses.

N7, RECORDED: THE REFUSAL COSTS THE WHOLE ATTACH, NOT JUST THE GATES. A composition that wires Gates and omits this option fails GateSessionFor, and internal/compose takes that failure in beginWork -- before the consumer starts -- so the session is not held at all. Every attach on such a Host is refused, not merely gate publication. It is FAIL-CLOSED and it is the right answer: a Host holding a session whose gates no Factory could ever see would strand every approval the agent raises. It is also unreachable through host.Compose, which installs the router itself; only a composition assembling this package directly can produce it.

func WithNamespaceLayout

func WithNamespaceLayout(layout NamespaceLayout) Option

WithNamespaceLayout supplies the object prefix LoadSessionState reports.

func WithRigSessionIDs

func WithRigSessionIDs(ids RigSessionIDs) Option

WithRigSessionIDs supplies the Harness identity LoadSessionState reports for an existing session whose record carries no binding.

func WithRuntimeJournals added in v0.3.0

func WithRuntimeJournals(journals RuntimeJournals) Option

WithRuntimeJournals supplies the journal reader LoadSessionState consults for a bound session. Without one every bound session reports RuntimeJournalUnknown, and a create over it is refused.

type ResidencyLease

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

ResidencyLease is Host's own residency grant over one session.

IT IS NOT THE JOURNAL GRANT, and keeping the two types apart in this package is the point of O3.4 rather than a filing decision. Grant, in journal.go, wraps a JournalWriter: its Epoch is the runtime-facing journal epoch, and it no longer satisfies residency.Lease at all — the compiler now refuses the substitution that used to be a comment. This one wraps a *sessionstore.ResidencyGrant, whose own doc says the epoch it mints "must never be compared with, or used as, a journal epoch" and that "residency loss does not fence a journal".

IT IS WHAT FINALLY DISCHARGES THE OTHER HALF OF F3. residency.Lease.Lost is a channel a heartbeat and a drain supervisor select on, and against the journal grant this package had to close it itself from the classification of a write Host made — an echo of Host's own traffic, silent for an idle session. ResidencyGrant.Lost is Storage's lease loss channel, forwarded unmodified: "the provider's actual notification, never inferred from journal events, release errors or caller cancellation". F3 stands for the JOURNAL grant, which still publishes no loss signal.

WHAT IT IS NOT IS A TAKEOVER GUARANTEE. Liveness is the provider's: ResidencyGrant's doc says memstore provides neither TTL nor crash takeover, so under this module's own tests the only thing that closes this channel is a release or a deliberately revocable wrapper. A real takeover needs pgstore.

func (*ResidencyLease) Epoch

Epoch is this grant's residency epoch, in Host's own domain.

func (*ResidencyLease) Lost

func (l *ResidencyLease) Lost() <-chan struct{}

Lost is the PROVIDER's loss signal for this grant, forwarded unmodified.

func (*ResidencyLease) Release

func (l *ResidencyLease) Release(ctx context.Context) error

Release returns this grant to the provider. It is not termination: the session remains durable and resumable.

Failure is retryable and does not pretend cleanup completed: until a release succeeds the Store admission is retained and Close may time out.

IT DOES NOT GO THROUGH classifyResidency, AND THE OMISSION IS THE DECISION. That classifier exists to turn an ACQUISITION vocabulary into Host's ownership sentinels, and neither of its arms is a statement a release can make: you cannot lose contention to another holder by handing a grant back, so ErrLeaseHeld here would tell residency's fence this grant was superseded when it merely could not be returned — and ResidencyAcquireCleanupError is raised inside AcquireResidency and never by Release. Wrapping the call was a no-op on every input this method can produce, which measured as an unkillable mutation; a transformation that cannot fire is worse than none, because it reads as a rule being applied. The store's own error therefore survives verbatim, and the caller that owns it is residency's unwinder, which reports a failed release in AttachError.Unreleased rather than classifying it.

type RigSessionIDs

RigSessionIDs answers Harness's own identity for a session whose durable record carries NO BINDING — a legacy record — and for nothing else.

A BOUND RECORD NEVER REACHES IT. Since sessionstore v0.6.0 the immutable SessionBinding names the runtime session (RuntimeSessionID), and that durable member is the authority: it is what Factory derived and recorded at create, and a second answer from a composition could only agree with it or be wrong. Everything below is the pre-binding history of this seam.

IT IS A PARAMETER FOR F5'S REASON, which is F4's reason with a different owner. residency.SessionState.RigSessionID is documented as something Host records at create and cannot derive, and no released record holds it: CatalogRecord has no member for it and SessionPointerKind is a closed set of three roles, none of them a runtime identity. Until something durable does, the composition root that made the record answers here — and one that recorded nothing gets UnavailableMemberError instead of uuid.UUID{}, which is what Rig.RestoreSession would otherwise be handed.

type RuntimeJournalProbeError added in v0.4.0

type RuntimeJournalProbeError struct {
	TenantID         sessionwire.TenantID
	SessionID        sessionwire.SessionID
	StorageBindingID string
	RuntimeSessionID uuid.UUID
	Cause            error
}

RuntimeJournalProbeError reports a runtime-journal read that FAILED while deciding a create. It names the component and the session, which the harness store's own errors do not, so an operator can tell it from every other durable-read failure (F12). It is transient by nature and carries no placement code: the attach is refused with the empty code and may succeed on retry, unlike a RuntimeSessionIDError.

func (*RuntimeJournalProbeError) Error added in v0.4.0

func (e *RuntimeJournalProbeError) Error() string

func (*RuntimeJournalProbeError) Unwrap added in v0.4.0

func (e *RuntimeJournalProbeError) Unwrap() error

Unwrap returns the harness store's failure.

type RuntimeJournals added in v0.3.0

type RuntimeJournals interface {
	RuntimeJournal(context.Context, sessionwire.TenantID, sessionstore.SessionBinding, uuid.UUID) (residency.RuntimeJournal, error)
}

RuntimeJournals answers whether the runtime's own journal already holds a conversation under a session's runtime identity. See residency.RuntimeJournal for why a create depends on it. An implementation answers residency.RuntimeJournalUnknown, with no error, when it has no way to look; an error is a lookup that failed.

type RuntimeSessionIDError added in v0.3.0

type RuntimeSessionIDError struct {
	TenantID  sessionwire.TenantID
	SessionID sessionwire.SessionID
	Cause     error
}

RuntimeSessionIDError reports a durable binding whose runtime session id is not a Harness identity: not a UUID, or the zero UUID. The store accepts any bounded opaque string there; Harness identifies a session by UUID, so Host cannot launch or restore under it and refuses before the launch rather than hand Harness the zero id.

func (*RuntimeSessionIDError) Error added in v0.3.0

func (e *RuntimeSessionIDError) Error() string

func (*RuntimeSessionIDError) Is added in v0.4.0

func (e *RuntimeSessionIDError) Is(target error) bool

Is reports residency.ErrInvalidRuntimeIdentity, the sentinel the residency manager classifies a PERMANENTLY unrunnable binding by (F12): the binding is immutable and every Host reads the same one, so it is not a failed read.

func (*RuntimeSessionIDError) Unwrap added in v0.3.0

func (e *RuntimeSessionIDError) Unwrap() error

Unwrap returns the parse failure, if any.

type Store

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

Store is the released session store, adapted to Host's seams.

It holds the store rather than embedding it, so nothing in Host reaches a released method this package has not audited. Every method below is on this type or on a value it produces.

func New

func New(store *sessionstore.Store, options ...Option) (*Store, error)

New adapts an open store.

It refuses a nil store rather than deferring the nil dereference to the first durable call. A composition root that got this wrong would otherwise discover it inside an attach that has already taken an admission.

func (*Store) AcquireSessionLease

func (s *Store) AcquireSessionLease(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
) (residency.Lease, error)

AcquireSessionLease takes the session's residency grant.

THE SESSION MUST BE IN DISPOSITION MODE, and that is the store's rule rather than this adapter's: AcquireResidency reads the immutable catalog binding and refuses anything that is not ProtocolModeDisposition with catalogInvalid("binding.protocol_mode"), binding the session scope mode as a side effect. It is finding F16(a) and it is not softened here — a legacy-mode session simply has no residency grant to take, and papering over that would hand Host a lease the store never issued.

func (*Store) BeginApplying

func (s *Store) BeginApplying(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	command sessionwire.CommandID,
	applying commands.Applying,
) (uint64, error)

BeginApplying CASes a claimed record into applying and returns its new revision.

func (*Store) ClaimCommand

func (s *Store) ClaimCommand(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	command sessionwire.CommandID,
	claim commands.Claim,
) (uint64, error)

ClaimCommand CASes a record into claimed and returns its new revision.

func (*Store) CompleteCommand

func (s *Store) CompleteCommand(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	command sessionwire.CommandID,
	completion commands.Completion,
) error

CompleteCommand settles an applying command as applied.

func (*Store) DispositionWriterFor

func (s *Store) DispositionWriterFor(lease residency.Lease) (commands.DispositionWrites, error)

DispositionWriterFor binds one session's disposition writes to the residency grant this store issued for it.

IT TAKES A residency.Lease AND ASSERTS, rather than taking a *sessionstore.ResidencyGrant, because the composition holds a Lease: the grant is private to ResidencyLease and reaches no caller, which is the property O3.4 established and which this must not undo. The assertion is the narrowest way to recover it, and the refusal below is what stops a foreign Lease — a fake, or a future second implementation — from producing a writer with no grant at all. IT RETURNS THE SEAM RATHER THAN THE CONCRETE TYPE, and the refusal path returns an explicit nil for it. A `(*DispositionWriter, error)` signature would put a TYPED NIL into every caller's interface field on the error path — a non-nil interface holding nothing — and the composition's own check is `writer == nil`. That is the same defect the closer's absence arm has, found once already in this task's own fixture, and it is worth not shipping twice.

func (*Store) FindApplication

func (s *Store) FindApplication(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	command sessionwire.CommandID,
) (commands.Application, error)

FindApplication correlates the journal against the command's own durable identities.

func (*Store) GateSessionFor added in v0.4.0

func (s *Store) GateSessionFor(lease residency.Lease, tenant sessionwire.TenantID, session sessionwire.SessionID) (gates.Session, error)

GateSessionFor binds one session's gate writes to the residency grant this store issued for it. It refuses a lease this package did not issue, exactly as DispositionWriterFor does, and returns the seam rather than the concrete type so its refusal is a real nil.

func (*Store) ListOrdered

func (s *Store) ListOrdered(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	afterOrder uint64,
	limit int,
) ([]commands.Command, error)

ListOrdered returns at most limit records strictly after afterOrder, in the store's immutable acceptance order.

THE BOUND IS THE CALLER'S AND THE ORDERING IS THE STORE'S, and nothing here sorts, filters or re-ranks the page: the released call returns rows in strictly increasing AcceptedOrder, every one of them above the bound, and a page this adapter re-ordered would be Host inferring an order rather than reading one.

IT FAILS CLOSED ON A ROW IT CANNOT VOUCH FOR, and the failure is the store's rather than this adapter's: ListSessionDispositionCommands refuses a page containing a row that does not decode, disagrees with its filing, or carries another session's identities, because a consumer handed a page with a row quietly missing would advance its durable cursor PAST the missing command. Nothing here weakens that into a skip, and nothing here has anywhere to record one if it wanted to.

func (*Store) LoadCommand

func (s *Store) LoadCommand(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	command sessionwire.CommandID,
) (commands.Record, error)

LoadCommand returns the full inbox record for one command.

func (*Store) LoadCursor

func (s *Store) LoadCursor(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
) (uint64, error)

LoadCursor returns the greatest acceptance order durably consumed for a session that EXISTS in the disposition catalog, or zero when none has been recorded.

THE QUALIFICATION IS THE WHOLE OF FINDING C-1 and it is why the seam's own documentation moved to meet this. A session with no disposition catalog — absent, or bound to another protocol — is REFUSED here rather than answered about, because the two states are not one state:

  • zero means "this session exists and has consumed nothing", and a consumer acts on it by listing from the head of the stream;
  • a missing or wrongly-bound catalog means the stream cannot be read by this Host at all, and answering it with zero would send every pass to the head of a stream it has no authority over, forever, reporting accepted work that it can never consume.

The released reader draws the same line for the same reason and says so: "a caller must not translate it into 'no commands'".

func (*Store) LoadDispositionCommand

func (s *Store) LoadDispositionCommand(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	command sessionwire.CommandID,
) (commands.DispositionRecord, error)

LoadDispositionCommand returns the full disposition record for one command.

func (*Store) LoadDispositionPayload

func (s *Store) LoadDispositionPayload(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	command sessionwire.CommandID,
) (commands.Payload, error)

LoadDispositionPayload returns the private body or its object reference.

IT IS A SECOND CALL AGAINST A STORE THAT ANSWERS BOTH IN ONE, exactly as LoadPayload is, and for the same reason: commands declares two methods so that "the payload is loaded AFTER the claim" is a thing a test can see. The ordering is a Host discipline, not a store-enforced one.

func (*Store) LoadGate

func (s *Store) LoadGate(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	gate sessionwire.GateID,
) (commands.Gate, bool, error)

LoadGate returns the durable gate and whether it exists.

A gate that is NOT open is answerable soundly: absence from the page is the whole answer, and the owner members are documented as the state a gate was opened under. A gate that IS open is refused; see ErrGateOwnerUnavailable.

A DISPOSITION SESSION'S OPEN GATE IS ANSWERED, BY RESIDENCY EPOCH. Since sessionstore v0.12.0 every gate write on such a session carries the Host's store-issued residency grant and raises the record's LeaseEpoch — which on a disposition record holds a RESIDENCY epoch, never a journal epoch — to it. So the mark is the residency of the last Host to write a gate, a successor's fencing write moves it, and it is exactly the owner the backstop needs: OwnerEpoch is that mark and OwnerHostID is left empty, because the store records no Host identity. A legacy session's gate still carries no owner and is still refused.

func (*Store) LoadPayload

func (s *Store) LoadPayload(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	command sessionwire.CommandID,
) (commands.Payload, error)

LoadPayload returns the private body or its object reference.

IT IS A SECOND CALL AGAINST A STORE THAT ANSWERS BOTH IN ONE, which is finding F9. commands declares two methods so that "the payload is loaded AFTER the claim" is a thing a test can see; the released record carries Payload and PayloadRef inline, so this reads the same record again. The ordering the two seams express is therefore a Host discipline here and not a store-enforced one — a fake can refuse an unclaimed payload read and the store cannot.

func (*Store) LoadSession

func (s *Store) LoadSession(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
) ([]byte, error)

LoadSession returns the durable record for a session as opaque bytes.

THE BYTES ARE THIS ADAPTER'S, NOT THE STORE'S, which is finding F1. No released call answers with an encoded record, so the encoding below is a choice; it is the catalog record's canonical JSON so that a reader debugging a failure sees the record rather than a length. NOTHING MAY PARSE IT: a caller that needs a member reads residency.SessionState, which is the seam that declares its members.

func (*Store) LoadSessionState

func (s *Store) LoadSessionState(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
) (residency.SessionState, error)

LoadSessionState reports the durable state a hydration reads.

A session with no catalog record is Exists=false. A record with a BINDING (every session Factory creates) reports the binding's runtime session id as RigSessionID, refusing one that is not a non-zero UUID, and the runtime journal's state under it (a journal read that fails is returned as an error, never as Absent). A record without a binding (legacy) takes its identity from the RigSessionIDs collaborator, and refuses without one rather than answering with a zero RigSessionID, because the caller's next act is to hand that UUID to Harness; its journal is never probed, so it reports RuntimeJournalUnknown.

func (*Store) MaxAdvertisementTTL

func (s *Store) MaxAdvertisementTTL() time.Duration

MaxAdvertisementTTL is the longest expiry promise the durable target directory will store.

IT IS REPORTED RATHER THAN RESTATED. The released store refuses an advertisement whose promise exceeds MaxHostTargetTTL, and host.Options puts no ceiling on RegistryExpiry at all — so a Host with a one-hour expiry is constructible, passes every option rule, and can never publish a single row. The composition reads this and refuses at construction, which hands the operator the configuration instead of a Host that starts and goes silent.

func (*Store) OpenSession

func (s *Store) OpenSession(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
) (*Grant, error)

OpenSession takes the lease and commits the opening fence in one call.

A CALLER STILL SEQUENCES ITS ROLLBACK AROUND TWO OUTCOMES, and they are the two the store distinguishes: ErrLeaseHeld means another holder has the session and nothing was taken, ErrFenceConflict means this grant was spent against a successor's committed sequence and the store has already released it. Neither leaves a lease for the caller to unwind, which is the property residency's rollback ladder must be read against.

func (*Store) PublishResidency

func (s *Store) PublishResidency(ctx context.Context, observation sessionwire.HostLinkRegistryObservation) error

PublishResidency writes the epoch-fenced durable Host route.

It DELEGATES THE VALIDATION as well as the write, FOR EVERY MEMBER BUT ONE. Core defines what a route means — that the endpoint is credential-free, that a routed session's residency is attaching, resident or releasing rather than cold, that the expiry falls after the observation — and the store applies exactly those rules through HostRegistration.Observation, which is the same projection a Factory's peer reads. Re-checking any of them here would be a second enumeration free to drift from the one that decides.

VERSION IS THE EXCEPTION AND IT IS CHECKED HERE — finding F14. PutHostRegistrationRequest has no version member at all, and the store stamps CurrentWireVersion when it projects the record back. So an observation carrying an unsupported version, which HostLinkRegistryObservation.Validate refuses with unsupported_version, would be published without refusal and read back as current: this adapter would be LOOSER than the seam it satisfies, which is exactly the class-4 defect O3.3 exists to remove rather than relocate. The check is the smallest thing that closes it, and it is stated as a finding rather than as a rule of this package's own, because a route's version is Core's business.

func (*Store) PublishTarget

func (s *Store) PublishTarget(ctx context.Context, advertisement service.Advertisement) error

PublishTarget writes one derived advertisement to the durable target directory, and is also the heartbeat.

THE ADVERTISEMENT'S OWN PLACEMENT FIELDS ARE NOT WRITTEN, and the omission is the released store's design rather than a loss. Namespace, RankingScope, StableKey, Rank, Ranked and DueAt describe where an OrderedIndex row goes and how it is ordered; the store derives every one of them itself from the key and the advertisement, in one compare-and-swap that moves the stored value, the rank and the due time together. Writing this module's copies alongside would be a second answer to each, free to drift from the one that decides a placement page.

VERSION IS CHECKED HERE FOR F14'S REASON. PublishHostTargetRequest carries no version member and the store stamps the current one when it projects the row back, so an advertisement at a version Core refuses would be published without refusal and read back as current.

func (*Store) RejectCommand

func (s *Store) RejectCommand(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	command sessionwire.CommandID,
	rejection commands.Rejection,
) error

RejectCommand settles a command with a durable typed reason.

func (*Store) SaveCursor

func (s *Store) SaveCursor(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	epoch uint64,
	order uint64,
) error

SaveCursor records the cursor under the writer's lease epoch.

THE PARAMETER ORDER IS epoch THEN order, which is commands.CursorWrites' declared order, and both are uint64 — the shape this repository has now got wrong three times. TestSaveCursorRecordsThePositionAndLoadCursorReadsItBack reads the durable record's two members back separately for that reason, and TestNoImplementationTransposesAdjacentSameTypedParameters holds the declaration.

EVERY REFUSAL IS CLASSIFIED, AND THERE ARE THREE KINDS. This call goes through commands.Fence.Write, and residency.epochFence records ownership as gone for exactly residency.ErrEpochSuperseded and residency.ErrFenceConflict, treating everything else as an ambiguous store failure that is retried:

  1. InboxErrorEpoch is the store refusing a write below the record's committed high-water mark — a later lease has taken the session. It becomes ErrEpochSuperseded, which is terminal and correct.

  2. InboxErrorOrder is v0.7.0's twentieth code and means RE-READ AND RETRY. It is a statement about the caller's DATA and not about its ownership, and it is deliberately left alone: mapping it onto ErrEpochSuperseded would make a consumer that retried an ambiguous save surrender a session it still holds, permanently, since the fence never reopens.

  3. A *CatalogError or *KeyspaceError is the session itself being one this store will not vouch for. It reaches the fence as neither sentinel — which is right — but it is NOT ambiguous and NOT transient, so leaving it unclassified would put a permanent misconfiguration on a retry loop that runs once per ReconcileInterval forever with nothing naming the cause. It becomes NoDispositionCatalogError, which names the session and carries the store's own refusal.

func (*Store) TombstoneResidency

func (s *Store) TombstoneResidency(
	ctx context.Context,
	tenant sessionwire.TenantID,
	session sessionwire.SessionID,
	epoch uint64,
) error

TombstoneResidency writes the epoch-fenced tombstone that removes the route without erasing the fencing high-water mark.

The store calls it a cleared registration and Host calls it a tombstone; they are one record. Route is nil exactly when the registration is a tombstone, so the high-water mark survives the removal, which is the property a rollback depends on and the reason this is not a delete.

func (*Store) WithdrawTarget

func (s *Store) WithdrawTarget(ctx context.Context, advertisement service.Advertisement) error

WithdrawTarget removes this Host's offer for one target, gracefully and immediately.

IT IS NOT A PUBLICATION WITH Accepting FALSE. A row that still carried an advertisement would still be ranked and still be due, so a Factory paging candidates would go on finding this Host and then filtering it; the withdrawn row has no advertisement at all, which is what makes "withdrawn implies not ranked and not due" a property of the record's shape. That is the durability the drain's acknowledgement rests on.

The version is checked for the same reason it is checked on a publication, and the consequence differs: a withdrawal this adapter refused would leave a live offer standing while the Host believed it had retired it.

type TenantEvidenceRouter added in v0.2.0

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

TenantEvidenceRouter forwards each settlement evidence request to the reader registered for its tenant and storage binding.

func NewTenantEvidenceRouter added in v0.2.0

func NewTenantEvidenceRouter(readers map[EvidenceKey]sessionstore.DispositionEvidenceReader) (*TenantEvidenceRouter, error)

NewTenantEvidenceRouter builds a router over one reader per (tenant, binding). It copies the table, for the reason NewEvidenceRouter gives.

func (*TenantEvidenceRouter) GateJournal added in v0.4.0

func (r *TenantEvidenceRouter) GateJournal(tenant sessionwire.TenantID, storageBindingID string) (EventReplayers, bool)

GateJournal returns the journal store serving (tenant, binding), when it can replay events.

func (*TenantEvidenceRouter) Keys added in v0.2.0

func (r *TenantEvidenceRouter) Keys() []EvidenceKey

Keys reports the (tenant, binding) pairs this router serves, sorted.

func (*TenantEvidenceRouter) ReadDispositionEvidence added in v0.2.0

ReadDispositionEvidence forwards the request, unchanged, to the reader that serves its tenant and binding; an unregistered pair is refused and never answered empty, for the reason EvidenceRouter.ReadDispositionEvidence gives.

func (*TenantEvidenceRouter) RuntimeJournal added in v0.3.0

func (r *TenantEvidenceRouter) RuntimeJournal(
	ctx context.Context,
	tenant sessionwire.TenantID,
	binding sessionstore.SessionBinding,
	runtimeID uuid.UUID,
) (residency.RuntimeJournal, error)

RuntimeJournal reports whether the journal store serving the session's tenant and storage binding already holds a conversation under runtimeID.

THE STORE IS THE SETTLEMENT READER, BECAUSE IT HAS TO BE THE SAME JOURNAL. The reader registered for (tenant, binding) is the store the runtime's disposition evidence is read from, which is the journal the runtime writes; asking any other store would answer about a different journal.

UNKNOWN IS AN ANSWER, NOT AN ERROR, when there is no way to look: no reader for the binding, or a reader that is not a harness store. A create is then refused upstream rather than launched. An error is reserved for a look that failed.

THE CATALOG IS A SHORTCUT AND THE LEDGER IS THE AUTHORITY. harness folds its catalog after each append, best-effort, so an entry proves a conversation and its absence proves nothing: a crash between an append and the fold, or a swallowed KV error, leaves a journal with no entry. So an entry answers Present with one KV read, and anything else — absent, or unreadable — falls through to the journal itself, where any event at all is a conversation. (A journal holding only non-event records has no conversation to lose; a create over it is not a restart.)

type UnavailableMemberError

type UnavailableMemberError struct {
	Member string
	Reason string
}

UnavailableMemberError reports a member of a Host record that the released store holds no source for.

IT IS A REFUSAL AND NOT A ZERO VALUE, and that is the whole of findings F4 and F5. A hydration handed Namespace="" scopes every object the launched runtime writes to nothing and reports success; a restore handed the zero RigSessionID asks Harness to restore a session that does not exist. Both are silent in a fake that fills the member in and loud here, which is the direction this task exists to make loud.

func (*UnavailableMemberError) Error

func (e *UnavailableMemberError) Error() string

type UnmappedValueError

type UnmappedValueError struct {
	Vocabulary string
	Value      string
	Cause      error
}

UnmappedValueError reports a released value Host has no member for.

func (*UnmappedValueError) Error

func (e *UnmappedValueError) Error() string

func (*UnmappedValueError) Unwrap

func (e *UnmappedValueError) Unwrap() error

type UnroutableBindingError

type UnroutableBindingError struct {
	TenantID  string
	Binding   string
	CommandID string
}

UnroutableBindingError names the binding no reader serves. TenantID is set when the router that refused was tenant-keyed (TenantEvidenceRouter), and empty otherwise.

func (*UnroutableBindingError) Error

func (e *UnroutableBindingError) Error() string

func (*UnroutableBindingError) Unwrap

func (e *UnroutableBindingError) Unwrap() error

type UnsupportedWireVersionError

type UnsupportedWireVersionError struct {
	Version sessionwire.WireVersion
}

UnsupportedWireVersionError reports an observation this adapter refuses to write, because the released record cannot carry its version.

It is not sessionwire's own refusal restated. Core refuses an unsupported version because the record could not be read; this refuses it because the record would be STORED WITHOUT IT and read back as current, which is a different failure and deserves a different sentence.

func (*UnsupportedWireVersionError) Error

Jump to

Keyboard shortcuts

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