sqlitelog

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package sqlitelog is the persistent, single-writer event log backing agentsessions on durable storage. It implements the same append-only, hash-chained, CAS + fencing semantics as the in-memory eventlog.Log, but survives process death — the property that makes "suspend on one pod, resume on another" real. content_hash is computed once at append via package canon (RFC 8785 JCS over proto3-JSON) and stored; reads return it verbatim and never re-canonicalize. A single database holds many sessions (a parent plus its forked children). It uses the pure-Go modernc.org/sqlite driver (no cgo), so it runs in a plain container image. See determinism contract §1, §5, §7.

Index

Constants

View Source
const (
	DefaultPageSize = 50
	MaxPageSize     = 500
)

Page sizing for ListSessions. A caller asking for nothing gets DefaultPageSize; a caller asking for more than MaxPageSize is clamped rather than refused, since an oversized ask is not an error, it just cannot be honored.

View Source
const DefaultProject = "default"

DefaultProject is the tenant a session lands in when none is supplied. A session is never stored with an empty project: a listing filters on an exact project, so a project-less row would be invisible to every caller that asks for a real one.

View Source
const SchemaVersion = 6

SchemaVersion is the schema shape this build writes, stamped into PRAGMA user_version so a build can identify a database without inspecting its columns. Version 1 is what v0.1.0 through v0.1.2 shipped. Version 2 adds the harness registry; version 3 adds approval reporting; version 4 adds permanent deletion intent; version 5 adds caller ownership and an authorization marker; version 6 records explicit session model-policy eligibility.

Variables

View Source
var (
	// ErrHarnessNotFound is returned for a name with no registration row.
	ErrHarnessNotFound = errors.New("sqlitelog: harness not found")
	// ErrHarnessSpecConflict is returned when a name is registered again with a different spec.
	// Registrations are immutable; a new spec needs a new name.
	ErrHarnessSpecConflict = errors.New("sqlitelog: harness name holds a different spec")
	// ErrHarnessRetired is returned when a new session selects a retired harness.
	ErrHarnessRetired = errors.New("sqlitelog: harness is retired")
	// ErrHarnessNameReserved is returned when a registration names a harness that a host sharing
	// the database reserved for a static harness.
	ErrHarnessNameReserved = errors.New("sqlitelog: harness name is reserved for a static harness")
	// ErrHarnessNameCollision is returned when a name a host serves as a static harness also has a
	// registration row.
	ErrHarnessNameCollision = errors.New("sqlitelog: registered harness collides with a static harness name")
)
View Source
var ErrInvalidPageToken = errors.New("sqlitelog: invalid page token")

ErrInvalidPageToken is returned for a page token the store cannot parse. Resuming from page one instead would silently re-deliver rows the caller already saw, so a bad cursor is an error.

View Source
var ErrSessionExists = errors.New("sqlitelog: session already exists")

ErrSessionExists is returned by CreateOwnedSession for any existing UID, including unbound log-created rows and tombstones. Creation never claims them.

View Source
var ErrSessionNotFound = errors.New("sqlitelog: session not found")

ErrSessionNotFound is returned for a session UID with no metadata row. It is distinct from an empty log: a session exists from the moment it is created, before it has any events.

View Source
var ErrTampered = eventlog.ErrTampered

ErrTampered indicates the stored hash-chain failed verification: a prev_hash link or a content_hash does not match what canon re-derives. It is deliberately distinct from an internal canonicalize/decode failure (a bug or storage corruption), which is returned wrapped and does NOT satisfy errors.Is(err, ErrTampered) — different severities, per the integrity contract. It is eventlog.ErrTampered, so a check of either matches every store's verification failure.

View Source
var ErrUnsupportedAuthorization = errors.New("sqlitelog: unsupported journal authorization")

ErrUnsupportedAuthorization is returned by Open when the journal requires caller enforcement, has an unknown authorization mode, or lacks its authorization marker. This host has no enforcement capability, even if it understands the journal's schema.

View Source
var ErrUnsupportedSchema = errors.New("sqlitelog: unsupported database schema")

ErrUnsupportedSchema reports a database written by a build with a newer schema.

Functions

This section is empty.

Types

type HarnessListOptions

type HarnessListOptions struct {
	After          string // return names strictly greater than this; empty starts at the beginning
	Limit          int    // at most this many rows; must be positive
	IncludeRetired bool
}

HarnessListOptions bounds one page of ListHarnesses.

type HarnessRecord

type HarnessRecord struct {
	Name         string
	UID          string
	Spec         string
	SpecDigest   string
	State        HarnessState
	RetireReason string
	RetiredAt    time.Time // zero unless retired
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

HarnessRecord is one stored registration. Spec is opaque to the store; SpecDigest is what makes a repeat registration recognizable.

type HarnessState

type HarnessState string

HarnessState is a registration's stored state.

const (
	HarnessActive  HarnessState = "active"
	HarnessRetired HarnessState = "retired"
)

type ListOptions

type ListOptions struct {
	Project   string // exact match; empty means DefaultProject, never "all tenants"
	PageSize  int
	PageToken string
	Owner     *api.CallerIdentity // nil leaves listing unchanged; non-nil requires a complete exact pair
}

ListOptions bounds one page of ListSessions. Enforcing callers must always set Owner: nil lists every owner's ACTIVE sessions in the selected project.

type Log

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

Log is one session's durable, single-writer, hash-chained event log.

func (*Log) Append

func (l *Log) Append(expectedLastSeq, fence int64, ev api.Event) (eventlog.Record, error)

Append commits ev as the next record iff expectedLastSeq == head (single-writer CAS) and fence is current, inside one transaction. It assigns seq, computes content_hash once via canon, and stores it alongside the wire-encoded event.

func (*Log) CheckWritable

func (l *Log) CheckWritable() error

CheckWritable is a read-only preflight. Unknown logs stay writable for direct implicit creation.

func (*Log) Head

func (l *Log) Head() (int64, error)

Head returns the seq of the last committed record (0 if empty).

func (*Log) NewFence

func (l *Log) NewFence() (int64, error)

NewFence advances and returns this session's fencing token. A new incarnation calls it to take over; appends carrying an older token are rejected with eventlog.ErrFenced.

func (*Log) Read

func (l *Log) Read(fromSeq int64) ([]eventlog.Record, error)

Read returns all records with seq >= fromSeq, in order. It returns the stored hashes verbatim and does not re-canonicalize — that is Verify's job (reviewer note: never re-hash on read).

func (*Log) Snapshot

func (l *Log) Snapshot() ([]eventlog.Record, error)

Snapshot returns a copy of the full chain (used to seed a fork).

func (*Log) Verify

func (l *Log) Verify() error

Verify walks the chain and re-derives each content_hash via canon. It returns ErrTampered if a prev_hash link or a content_hash does not match (an integrity violation), or a wrapped error for an internal decode/canonicalize failure (a bug/corruption) — keeping the two severities distinct so a caller can alert on tamper without conflating it with an operational fault.

type Option

type Option func(*options)

Option configures a Store at Open.

func WithDefaultProject

func WithDefaultProject(project string) Option

WithDefaultProject sets the tenant used for sessions created without one.

type RegisterResult

type RegisterResult int

RegisterResult says what RegisterHarness did.

const (
	// HarnessCreated: the name was free and now holds an active registration.
	HarnessCreated RegisterResult = iota + 1
	// HarnessUnchanged: the name already held this spec and was active.
	HarnessUnchanged
	// HarnessReactivated: the name already held this spec and was retired; it is active again.
	HarnessReactivated
)

type SessionInfo

type SessionInfo struct {
	SessionMeta
	Owner               api.CallerIdentity // immutable caller key; zero means unbound, not public
	LastSeq             int64
	ComputeState        api.ComputeState
	DeletionState       api.DeletionState
	DeletionRequestedAt time.Time
	DeletionSeq         int64 // DELETE anchor; its predecessor is the pre-intent audit head
	PendingApproval     api.ApprovalRef
}

SessionInfo captures metadata, cursor and append-transaction projections in one query. PendingApproval is an audit value snapshot, not command authority or a mutable cache. DeletionRequestedAt is absent (zero time) while ACTIVE.

type SessionMeta

type SessionMeta struct {
	UID     string
	Project string
	Name    string
	Harness string
	Model   string
	// ModelPolicy records creation on a policy-enabled route, independently of eligibility.
	// Hosts use this sticky opt-in to refuse losing a required binding. Neither flag is API metadata.
	ModelPolicy bool
	// ModelPolicyEligible marks new sessions, even on unconfigured routes. Their selected
	// route's policy applies; genuine legacy rows keep both flags false. Old opt-in rows are eligible.
	ModelPolicyEligible bool
	ParentUID           string // set iff this session was forked
	ForkSeq             int64  // parent event seq forked at
	CreatedAt           time.Time
	UpdatedAt           time.Time

	// Caller-supplied metadata, carried as proto3-JSON. The store round-trips these without
	// interpreting them: they describe a session, they do not control it. Identity especially is
	// provenance and not authorization; nothing checks it. See docs/security.md.
	Labels      string
	Annotations string
	Origin      string
	Identity    string
}

SessionMeta is mutable workload and provenance metadata, separate from verified caller ownership. It is durable before the first event, making empty sessions listable.

type SessionPage

type SessionPage struct {
	Sessions      []SessionInfo
	NextPageToken string // empty on the last page
}

SessionPage is one page of results plus the cursor that continues it.

type Store

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

Store is a durable, multi-session event log over a single sqlite database.

func Open

func Open(path string, opts ...Option) (*Store, error)

Open opens (creating if needed) the sqlite database at path and migrates it to SchemaVersion. path is a file name, a "file:" URI, or either with a "?" query of modernc.org/sqlite DSN parameters. Use ":memory:" for an ephemeral store. A file name without a query is opened as a "file:" URI, so '#' and "%XX" in it are URI syntax ("/d/a#b.db" opens "/d/a"); to name such a file, pass a "file:" URI with those characters percent-escaped. For a file-backed store it uses BEGIN IMMEDIATE (via the _txlock DSN parameter, added unless path already sets a non-empty one) so cross-process writers take the write lock upfront and serialize cleanly instead of deadlocking on a lock upgrade (SQLITE_BUSY), and synchronous=FULL so every committed transaction is durable across power loss, not just process death.

Durability boundary: a *committed* append survives process death and power loss. A crash mid-append (before commit) drops only that uncommitted event, which is re-driven on resume (determinism contract I3/I4). TestPersistenceAcrossReopen proves the clean-restart case.

func (*Store) Close

func (s *Store) Close() error

Close closes the underlying database.

func (*Store) CreateOwnedSession added in v0.3.0

func (s *Store) CreateOwnedSession(m SessionMeta, owner api.CallerIdentity, registered bool) error

CreateOwnedSession atomically inserts metadata and an immutable verified owner, admitting the harness under the same transaction as PutSessionOnActiveHarness. registered distinguishes registered from static harness names. The trusted host must verify owner; this method does not authenticate it. Existing UIDs are refused.

func (*Store) DeleteSession

func (s *Store) DeleteSession(ctx context.Context, uid string) (SessionInfo, error)

DeleteSession commits permanent deletion intent, not runtime cleanup. The retained journal anchors audit evidence; its predecessor remains the pre-delete head, independent of compute. A retry against PENDING returns the original snapshot without any writes; an unrecognized state fails closed with eventlog.ErrDeleted.

func (*Store) ForkSession added in v0.3.0

func (s *Store) ForkSession(meta SessionMeta) *Log

ForkSession returns a log handle for a fresh fork child. It defers inherited metadata until NewFence, then commits it atomically with the child's first fence. Obtaining the handle publishes nothing, so refused/unprovisioned clones leave no phantom sessions. Use Session for later access.

func (*Store) Harness

func (s *Store) Harness(name string) (HarnessRecord, error)

Harness returns one registration, or ErrHarnessNotFound.

func (*Store) ListHarnesses

func (s *Store) ListHarnesses(opts HarnessListOptions) ([]HarnessRecord, error)

ListHarnesses returns registrations in name order, starting after opts.After.

func (*Store) ListSessions

func (s *Store) ListSessions(opts ListOptions) (SessionPage, error)

ListSessions enumerates one project's ACTIVE sessions, newest first. Tombstones are filtered before keyset paging and LIMIT so hidden rows cannot consume page slots. A non-nil Owner also filters before paging; nil preserves the project's full ACTIVE population.

Paging is keyset, not offset: the cursor carries the last row's (created_at, uid) and the next page resumes strictly after it. Sessions are created while a client pages through them, and an offset would then skip or repeat rows. The (created_at DESC, session) ordering is total, so the cursor always identifies exactly one position.

func (*Store) PutSession

func (s *Store) PutSession(m SessionMeta) error

PutSession writes a session's metadata, creating the row if the session is new.

It preserves the incarnation fence and all append-owned projections, so re-registering an active session updates its metadata without rewinding its identity or reporting state. Both model-policy flags are sticky: ordinary metadata updates cannot clear either one. Setting them on an existing event-created row lets Fork attach the parent's admission state after copying. This lets Fork write child metadata around a controller.Fork that already populated the log.

`created_at` follows the caller: an explicit CreatedAt is honored, and a zero one means "now on insert, leave alone on update", so a later re-registration does not restamp a session as new.

func (*Store) PutSessionOnActiveHarness

func (s *Store) PutSessionOnActiveHarness(m SessionMeta, registered bool) error

PutSessionOnActiveHarness writes a new session's metadata only if its harness may take a new session. The harness check and the insert share one transaction, so a RetireHarness that has returned is always seen: no session created after it can land on the retired harness, even when another process shares the database.

registered is how the caller says the name is not one of its static harnesses. For a registered name, a missing row is ErrHarnessNotFound and a retired one is ErrHarnessRetired. For a static name, any row is ErrHarnessNameCollision: another host registered the name, so it means two harnesses, and the session is refused rather than placed on either.

func (*Store) RegisterHarness

func (s *Store) RegisterHarness(rec HarnessRecord) (HarnessRecord, RegisterResult, error)

RegisterHarness stores rec as an active registration, or recognizes it. The same name and SpecDigest is HarnessUnchanged when active and HarnessReactivated when retired; either way the stored UID and creation time are kept. A different digest under the name is ErrHarnessSpecConflict. The read and the write share one transaction, so two racing registrations of one name cannot both report HarnessCreated, even from two processes sharing the database.

func (*Store) ReserveHarnessNames

func (s *Store) ReserveHarnessNames(names []string) error

ReserveHarnessNames records names as static harness names, so no host sharing the database can register them, and fails with ErrHarnessNameCollision if any of them already has a registration row, active or retired. The check and the reservation share one transaction, and RegisterHarness checks reservations in its own, so a name is either reserved or registered, never both, however the hosts that share the database interleave. A reservation is permanent: a host that stops serving a static harness does not release its name, because another host may still serve it.

func (*Store) RetireHarness

func (s *Store) RetireHarness(name, reason string) (HarnessRecord, error)

RetireHarness marks a registration retired and returns it. Retiring a retired harness changes nothing and keeps its first retire time and reason, so a retry does not rewrite history.

func (*Store) Session

func (s *Store) Session(uid string) *Log

Session returns a handle to one session's append-only chain.

func (*Store) SessionInfo

func (s *Store) SessionInfo(uid string) (SessionInfo, error)

SessionInfo returns one session's metadata together with its derived log cursor and compute state.

Jump to

Keyboard shortcuts

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