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
- Variables
- type HarnessListOptions
- type HarnessRecord
- type HarnessState
- type ListOptions
- type Log
- func (l *Log) Append(expectedLastSeq, fence int64, ev api.Event) (eventlog.Record, error)
- func (l *Log) CheckWritable() error
- func (l *Log) Head() (int64, error)
- func (l *Log) NewFence() (int64, error)
- func (l *Log) Read(fromSeq int64) ([]eventlog.Record, error)
- func (l *Log) Snapshot() ([]eventlog.Record, error)
- func (l *Log) Verify() error
- type Option
- type RegisterResult
- type SessionInfo
- type SessionMeta
- type SessionPage
- type Store
- func (s *Store) Close() error
- func (s *Store) CreateOwnedSession(m SessionMeta, owner api.CallerIdentity, registered bool) error
- func (s *Store) DeleteSession(ctx context.Context, uid string) (SessionInfo, error)
- func (s *Store) ForkSession(meta SessionMeta) *Log
- func (s *Store) Harness(name string) (HarnessRecord, error)
- func (s *Store) ListHarnesses(opts HarnessListOptions) ([]HarnessRecord, error)
- func (s *Store) ListSessions(opts ListOptions) (SessionPage, error)
- func (s *Store) PutSession(m SessionMeta) error
- func (s *Store) PutSessionOnActiveHarness(m SessionMeta, registered bool) error
- func (s *Store) RegisterHarness(rec HarnessRecord) (HarnessRecord, RegisterResult, error)
- func (s *Store) ReserveHarnessNames(names []string) error
- func (s *Store) RetireHarness(name, reason string) (HarnessRecord, error)
- func (s *Store) Session(uid string) *Log
- func (s *Store) SessionInfo(uid string) (SessionInfo, error)
Constants ¶
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.
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.
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 ¶
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") )
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.
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.
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.
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.
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.
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 ¶
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 ¶
CheckWritable is a read-only preflight. Unknown logs stay writable for direct implicit creation.
func (*Log) NewFence ¶
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 ¶
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) Verify ¶
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 ¶
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 ¶
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) 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 ¶
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 ¶
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) SessionInfo ¶
func (s *Store) SessionInfo(uid string) (SessionInfo, error)
SessionInfo returns one session's metadata together with its derived log cursor and compute state.