Documentation
¶
Overview ¶
Package state owns Harbor's persistence floor: the single mandatory `StateStore` interface that every persistence-shaped subsystem (sessions, tasks, governance accumulators, planner checkpoints, memory snapshots, steering events) saves through.
The surface is generic by design: identity-scoped CRUD keyed on `(identity.Quadruple, Kind string, Bytes []byte)` with idempotency on a caller-supplied `EventID` (ULID), plus explicitly-elevated maintenance scans (`ListKind` and tenant-bounded `ScanKindForTenant` — RFC §6.11). Consuming subsystems land their typed wrappers at their own layer atop this interface — a `SessionRegistry.Save(s Session)` reduces to `StateStore.Save(StateRecord{Identity: s.Identity, Kind: "session.lifecycle", Bytes: marshal(s)})`.
Three V1 drivers ship to the §9 persistence triad (in-memory, SQLite, Postgres). Harbor ships only the in-memory reference; SQLite and Postgres inherit the conformancetest suite verbatim.
Identity is mandatory at the API boundary. Any `Quadruple` whose tenant / user / session is empty is rejected with `ErrIdentityRequired`. Empty `RunID` is acceptable for state that is session-scoped rather than run-scoped.
Audit redaction is upstream of `Save`. The store stores opaque `Bytes`; mixing redaction into the persistence layer would couple a leaf package to the audit subsystem and split responsibility.
Index ¶
- Constants
- Variables
- func EncodeStateScanContinuation(cursor StateScanCursor, tenantID, literalKindPrefix string, scope ListScope) (string, error)
- func Register(name string, factory Factory)
- func RegisteredDrivers() []string
- func ValidateDeleteIf(expectation SlotExpectation) error
- func ValidateFenceIf(expectation SlotExpectation, fn func() error) error
- func ValidateIdentity(q identity.Quadruple) error
- func ValidateListKind(scope ListScope, kindPrefix string) error
- func ValidateListKindForIdentity(id identity.Quadruple, kindPrefix string) error
- func ValidateListKindForIdentityBounded(id identity.Quadruple, kindPrefix string, limit int) error
- func ValidateRecord(r StateRecord) error
- func ValidateSaveIf(expectations []SlotExpectation, next StateRecord) error
- func ValidateScanKindForTenant(scope ListScope, tenantID, literalKindPrefix string, limit int) error
- func WithStore(ctx context.Context, store StateStore) context.Context
- type EventID
- type Factory
- type ListScope
- type SlotExpectation
- type StateRecord
- type StateScanCursor
- type StateScanPage
- type StateStore
Constants ¶
const ( // MaxStateScanLimit bounds one maintenance page so no caller can turn the // tenant scan into an accidental unbounded read. MaxStateScanLimit = 256 // MaxStateIdentityListLimit bounds one identity-local admission read. It // prevents a caller from using the bounded surface as an unbounded dump. MaxStateIdentityListLimit = 1000 )
const DefaultDriver = "inmem"
DefaultDriver is the production driver name. The SQLite and Postgres drivers register additional names; Open switches on cfg.Driver.
Variables ¶
var ( // ErrNotFound — Load / LoadByEventID was called for a key that // has no record. Wraps drivers' own not-found shapes. ErrNotFound = errors.New("state: record not found") // ErrIdempotencyConflict — Save with a previously-seen EventID // but different Bytes (or routed to a different key). Tells the // caller a retry policy bug exists upstream. ErrIdempotencyConflict = errors.New("state: idempotency conflict") // ErrConditionFailed — SaveIf observed a slot whose EventID did not match // its exact expectation. No SaveIf write was applied. ErrConditionFailed = errors.New("state: condition failed") // ErrIdentityRequired — Save / Load / Delete called with a // Quadruple missing one of (tenant, user, session). Empty RunID // is allowed for session-scoped state. ErrIdentityRequired = errors.New("state: identity triple incomplete") // ErrStoreClosed — Save / Load / Delete called after Close. ErrStoreClosed = errors.New("state: store is closed") // ErrInvalidRecord — record fails structural validation // (empty Kind, empty EventID). ErrInvalidRecord = errors.New("state: invalid record") // ErrUnknownDriver — Open was asked for a driver name no // registered factory handles. ErrUnknownDriver = errors.New("state: unknown driver") // ErrMaintenanceScopeRequired — a maintenance scan was called without the // explicit ListScope.MaintenanceScoped claim. The cross-identity // scan fails closed (CLAUDE.md §6). ErrMaintenanceScopeRequired = errors.New("state: maintenance scan requires an explicit maintenance scope claim") // ErrInvalidScan — ScanKindForTenant received invalid bounds, scope, // tenant/prefix, or an invalid/mismatched opaque continuation. ErrInvalidScan = errors.New("state: invalid tenant scan") )
Sentinel errors. Callers compare via errors.Is.
Functions ¶
func EncodeStateScanContinuation ¶ added in v1.26.0
func EncodeStateScanContinuation(cursor StateScanCursor, tenantID, literalKindPrefix string, scope ListScope) (string, error)
EncodeStateScanContinuation returns the sole opaque continuation format accepted by DecodeStateScanContinuation. Driver code must only pass a tuple it just returned, preserving strict monotonic keyset progression.
func Register ¶
Register installs a driver factory under name. Drivers self-register from their package init(); cmd/harbor blank-imports the production driver to trigger registration. Per AGENTS.md §4.4.
Re-registering the same name panics — the registration model is write-once-at-init and a duplicate signals a build mis-configuration.
func RegisteredDrivers ¶
func RegisteredDrivers() []string
RegisteredDrivers returns a sorted list of driver names. Useful for boot-log output and for surfacing in error messages.
func ValidateDeleteIf ¶ added in v1.26.0
func ValidateDeleteIf(expectation SlotExpectation) error
ValidateDeleteIf validates the exact-present generation predicate used by StateStore.DeleteIf. Conditional deletion never accepts the empty EventID sentinel because absence is not something it can delete.
func ValidateFenceIf ¶ added in v1.26.0
func ValidateFenceIf(expectation SlotExpectation, fn func() error) error
ValidateFenceIf validates one present exact-generation fence predicate.
func ValidateIdentity ¶
ValidateIdentity checks that the triple is fully specified. Empty RunID is accepted (session-scoped state). Returns wrapped ErrIdentityRequired when any of tenant/user/session is empty.
func ValidateListKind ¶ added in v1.3.0
ValidateListKind checks ListKind's fail-closed preconditions shared by every driver: the explicit maintenance scope claim must be set (ErrMaintenanceScopeRequired) and the kind prefix must be non-empty (ErrInvalidRecord — a whole-store dump is never a valid maintenance scan).
func ValidateListKindForIdentity ¶ added in v1.25.0
ValidateListKindForIdentity checks the common preconditions for the identity-scoped enumeration surface.
func ValidateListKindForIdentityBounded ¶ added in v1.26.0
ValidateListKindForIdentityBounded checks the identity-prefix and hard materialization bounds shared by every bounded identity-list driver.
func ValidateRecord ¶
func ValidateRecord(r StateRecord) error
ValidateRecord checks structural invariants Save needs before touching driver storage: identity triple present, EventID non-empty, Kind non-empty.
func ValidateSaveIf ¶ added in v1.26.0
func ValidateSaveIf(expectations []SlotExpectation, next StateRecord) error
ValidateSaveIf validates the common conditional-save invariants before a driver touches storage. Every expected slot is identity scoped, unique, and the next slot is one of the predicates so SaveIf cannot become an unconstrained conditional write.
func ValidateScanKindForTenant ¶ added in v1.26.0
func ValidateScanKindForTenant(scope ListScope, tenantID, literalKindPrefix string, limit int) error
ValidateScanKindForTenant checks the fail-closed preconditions shared by every ScanKindForTenant implementation. It intentionally returns ErrInvalidScan for all malformed scan request details so operators do not need driver-specific parsing errors to identify a bad continuation.
Types ¶
type EventID ¶
type EventID string
EventID is a caller-supplied ULID used as the canonical idempotency key for `Save`. ULID gives us monotonic, lexicographically sortable IDs that work as both primary keys and secondary indices.
Callers are free to construct the value externally; `NewEventID` is provided as a convenience that uses crypto-strong entropy.
func NewEventID ¶
func NewEventID() EventID
NewEventID generates a fresh ULID-shaped EventID using crypto/rand. Implementations may use any ULID source; this helper exists so callers don't need a separate dependency just for generating idempotency keys.
type Factory ¶
type Factory func(config.StateConfig) (StateStore, error)
Factory builds a StateStore from a StateConfig. Drivers expose one Factory each via init() → Register.
type ListScope ¶ added in v1.3.0
type ListScope struct {
// MaintenanceScoped asserts the caller is a runtime maintenance
// loop acting on each returned record under that record's own
// identity. False (the zero value) is rejected with
// ErrMaintenanceScopeRequired.
MaintenanceScoped bool
}
ListScope is the explicit scope claim maintenance scans require. The zero value fails closed: a caller must set MaintenanceScoped to assert it understands the call crosses identity boundaries (CLAUDE.md §6 rule 5; the §13 elevated-scope-claim rule applied to the persistence floor).
type SlotExpectation ¶ added in v1.26.0
SlotExpectation is one exact generation predicate for SaveIf. The complete identity plus Kind names a single StateStore slot. ExpectedEventID == "" means the slot must be absent; any other value must equal the current record's EventID exactly. Event IDs, rather than Version, are used because Save replaces a slot's EventID on every successful generation.
type StateRecord ¶
type StateRecord struct {
ID EventID
Identity identity.Quadruple
Kind string
Version int
Bytes []byte
UpdatedAt time.Time
}
StateRecord is the unit of persistence.
`Bytes` is opaque to the store — callers serialize their domain types and run them through audit redaction upstream of `Save`. The store does not interpret payloads or re-redact. Zero-length payloads are valid; nil and allocated empty slices are byte-equal for Save's idempotency contract.
`Kind` is a free-form caller-namespaced key (e.g. "session.lifecycle", "task.checkpoint", "governance.cost"). Two records with the same (Quadruple, Kind) are treated as a single keyed slot — `Save` overwrites; `Load` returns the latest.
`Version` is a hint for optimistic-concurrency at the typed-wrapper layer (e.g. `SessionRegistry` MAY refuse to apply an update whose Version is stale). It is not a StateStore compare-and-swap token: callers that need durable compare-and-swap use SaveIf with EventIDs.
`UpdatedAt` is set by the store at `Save` time when zero; callers MAY override (useful for tests with controllable clocks).
type StateScanCursor ¶ added in v1.26.0
StateScanCursor is the decoded, driver-neutral keyset position used by ScanKindForTenant. It is exposed only so all mandatory drivers share one strict opaque-cursor codec; callers receive and replay only its encoding.
func DecodeStateScanContinuation ¶ added in v1.26.0
func DecodeStateScanContinuation(continuation, tenantID, literalKindPrefix string, scope ListScope) (StateScanCursor, error)
DecodeStateScanContinuation strictly decodes a ScanKindForTenant cursor. The cursor is intentionally bound to the query dimensions, preventing a cursor issued for one tenant or prefix from widening another scan.
type StateScanPage ¶ added in v1.26.0
type StateScanPage struct {
Records []StateRecord
Continuation string
}
StateScanPage is one stable-ordered page from ScanKindForTenant. An empty Continuation marks the terminal page. Records are ordered lexicographically by (user_id, session_id, run_id, kind), which is also the cursor tuple.
type StateStore ¶
type StateStore interface {
// Save persists a record. Idempotent on `EventID`:
//
// - Same EventID + byte-equal Bytes: no-op (no error, no
// duplicate write).
// - Same EventID + different Bytes: ErrIdempotencyConflict.
//
// If a record already exists at (Identity, Kind) but with a
// different EventID, Save overwrites it (the new EventID becomes
// the active one for that slot; the previous EventID is no
// longer LoadByEventID-resolvable).
Save(ctx context.Context, r StateRecord) error
// SaveIf atomically verifies every expectation and persists next. The
// expectation set is non-empty, has no duplicate slots, and must include
// next's slot. A mismatch returns ErrConditionFailed and leaves every slot
// unchanged. Save's EventID idempotency contract applies to next only after
// all predicates match; it never bypasses a failed predicate.
SaveIf(ctx context.Context, expectations []SlotExpectation, next StateRecord) error
// DeleteIf atomically removes exactly one present slot generation. A
// different or absent generation is a normal concurrent-state outcome and
// returns (false, nil); only an exact EventID match may be deleted. This is
// the conditional-delete counterpart to SaveIf for compensations that must
// restore a genuinely absent pre-operation state without writing a marker.
DeleteIf(ctx context.Context, expectation SlotExpectation) (bool, error)
// FenceIf acquires the driver's cross-instance lock for one exact slot
// generation, verifies the EventID, runs fn while that generation cannot be
// replaced, then releases the lock without mutating the slot. fn MUST NOT
// call this StateStore; it exists for a short process-local publication
// linearization that must serialize with SaveIf on the same durable slot.
// Context cancellation is an admission condition before fn starts; callers
// must not use cancellation to infer that an already-started callback did
// not publish.
FenceIf(ctx context.Context, expectation SlotExpectation, fn func() error) error
// Load returns the record at (id, kind). Returns ErrNotFound
// (wrapped) when no record exists for that key.
Load(ctx context.Context, id identity.Quadruple, kind string) (StateRecord, error)
// LoadByEventID returns the record whose ID matches eventID.
// Useful for replaying a specific event by its idempotency key.
// Returns ErrNotFound (wrapped) when not present.
LoadByEventID(ctx context.Context, eventID EventID) (StateRecord, error)
// Delete removes the record at (id, kind). Returns nil when the
// record is absent (idempotent), wrapped error on store failure.
Delete(ctx context.Context, id identity.Quadruple, kind string) error
// DeleteScope removes EVERY record whose (tenant, user, session)
// matches id, regardless of run_id or kind. It is the kind-agnostic
// cascade primitive a session-erasure (`sessions.delete`) runs to
// remove all data scoped to a session in one call — the session
// lifecycle record, run-scoped trajectories, planner checkpoints, and
// the durable event stream all live under the triple and all go.
//
// Unlike ListKind, DeleteScope is identity-scoped — NOT a
// maintenance-elevated cross-identity scan. It deletes only the
// caller's OWN session, so it needs no ListScope claim: the triple IS
// the scope. It fails closed with ErrIdentityRequired on an
// incomplete triple (empty tenant / user / session); empty RunID is
// irrelevant — the match ignores run_id entirely.
//
// It is idempotent: an absent scope returns (0, nil), never an error,
// so a cascade interrupted mid-flight is safe to re-invoke to
// convergence. Returns the number of records deleted.
DeleteScope(ctx context.Context, id identity.Identity) (int, error)
// ListKind enumerates every record whose Kind starts with
// kindPrefix — the store's unbounded cross-tenant maintenance scan
// (RFC §6.11). Unlike every other method, the scan crosses
// identity boundaries: it exists so runtime maintenance loops (the
// pause sweeper's crash-orphan rescan is the first consumer) can
// find records whose identities the process has never seen. That
// elevation is explicit and fail-closed:
//
// - scope.MaintenanceScoped MUST be true, or the call fails with
// ErrMaintenanceScopeRequired (CLAUDE.md §6 rule 5 / §13 "no
// cross-session queries without an explicit elevated scope
// claim"). There is no identity-scoped mode — identity-scoped
// reads stay on Load / LoadByEventID.
// - kindPrefix MUST be non-empty (ErrInvalidRecord) — a
// whole-store dump is never a valid maintenance scan.
// - Callers MUST act on each returned record under that record's
// OWN identity (every record carries its Quadruple); ListKind
// grants visibility for the scan, never a widened mutation
// scope.
//
// kindPrefix matches literally (no wildcard or case-folding
// interpretation). Result order is
// unspecified; an empty result is ([]StateRecord{} or nil, nil),
// never an error.
ListKind(ctx context.Context, scope ListScope, kindPrefix string) ([]StateRecord, error)
// ListKindForIdentity enumerates records for one complete identity whose
// Kind starts with kindPrefix. Unlike ListKind, this is not an elevated
// maintenance scan: the supplied triple is the complete read boundary.
// Prefix matching is literal and kindPrefix must be non-empty.
ListKindForIdentity(ctx context.Context, id identity.Quadruple, kindPrefix string) ([]StateRecord, error)
// ListKindForIdentityBounded is the identity-scoped counterpart for a
// caller that must cap materialization before it processes records. It
// returns at most limit rows, with the identity and literal-prefix
// semantics of ListKindForIdentity. Callers that must reject overflow ask
// for their accepted bound plus one. It is deliberately not a cursor: it
// is a bounded admission check, not a maintenance traversal.
ListKindForIdentityBounded(ctx context.Context, id identity.Quadruple, kindPrefix string, limit int) ([]StateRecord, error)
// ScanKindForTenant returns one deterministic, tenant-bounded maintenance
// page whose Kind begins with literalKindPrefix. It is deliberately a
// keyset scan, not a database snapshot: callers that need convergence must
// quiesce writers and complete a final verification pass. continuation is
// an opaque cursor returned by the preceding page and is bound to this
// exact maintenance scope, tenant, and literal prefix.
ScanKindForTenant(ctx context.Context, scope ListScope, tenantID, literalKindPrefix string, limit int, continuation string) (StateScanPage, error)
// Close releases driver resources. Subsequent calls return
// ErrStoreClosed (wrapped). Implementations MUST honour ctx
// during long teardowns.
Close(ctx context.Context) error
}
StateStore is Harbor's persistence interface — single mandatory surface, no `Supports*` capability ceremony (AGENTS.md §4.4 + §9).
Implementations MUST be safe for concurrent use by N goroutines against a single shared instance. Mutable state must be guarded; per-call state lives in `ctx`, never on the driver.
func From ¶
func From(ctx context.Context) (StateStore, bool)
From returns the StateStore in ctx and a presence bool. Use when absence is recoverable.
func MustFrom ¶
func MustFrom(ctx context.Context) StateStore
MustFrom returns the StateStore in ctx; panics with ErrStoreClosed (used as the sentinel for "no store configured") when none is present. Use in handler/runtime paths where a store is mandatory.
func Open ¶
func Open(_ context.Context, cfg config.StateConfig) (StateStore, error)
Open returns the StateStore built by the factory whose name matches cfg.Driver (defaults to DefaultDriver when cfg.Driver is empty).
func OpenDriver ¶
func OpenDriver(name string, cfg config.StateConfig) (StateStore, error)
OpenDriver opens a specific driver by name; useful for tests that want to exercise the registry against a non-default driver.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package conformancetest exposes the canonical correctness suite every state.StateStore driver must pass.
|
Package conformancetest exposes the canonical correctness suite every state.StateStore driver must pass. |
|
drivers
|
|
|
inmem
Package inmem is Harbor's V1 in-memory StateStore driver.
|
Package inmem is Harbor's V1 in-memory StateStore driver. |
|
postgres
Package postgres is Harbor's V1 Postgres-backed StateStore driver.
|
Package postgres is Harbor's V1 Postgres-backed StateStore driver. |
|
sqlite
Package sqlite is Harbor's SQLite-backed `state.StateStore` driver.
|
Package sqlite is Harbor's SQLite-backed `state.StateStore` driver. |