ledger

package
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: Nov 8, 2026 License: AGPL-3.0 Imports: 13 Imported by: 0

Documentation

Overview

Package ledger is the durable half of the session runtime: the record of which runs were admitted, who owns them, and how they ended.

It deliberately holds no liveness. The owner lease lives only in the live backend, so PostgreSQL receives lifecycle writes only — admission, ownership/fencing change, decision, terminal proposal, and terminal transition. Nothing here is a keepalive or a progress sample, which is why write volume is proportional to the number of runs rather than to their duration or token rate. A reader therefore cannot ask this store "is the owner still alive"; only the live backend can answer that, and the reaper is built around that fact.

Index

Constants

View Source
const (
	ResetScopeSession = "session"
	ResetScopeBot     = "bot"
)

Variables

View Source
var (
	// ErrRunNotFound is returned when no row matches the requested identity.
	ErrRunNotFound = errors.New("ledger: session run not found")
	// ErrResumeSuperseded means the interrupted intent no longer owns the latest turn.
	ErrResumeSuperseded = errors.New("ledger: interrupted run has been superseded")
	// ErrSessionNotFound is returned by Admit when the target session does not
	// exist or was deleted, which is distinguishable from a duplicate
	// invocation because a duplicate still resolves to a row.
	ErrSessionNotFound = errors.New("ledger: session not found")
	// ErrSessionBusy means the session already has an active run and this is a
	// different invocation. SR-OWN-001 allows answering busy instead of
	// queueing, and this design takes that option: the caller owns the retry,
	// which keeps a queue state, a queue index, queue promotion and queued
	// aborts out of the runtime entirely.
	//
	// It is retryable by construction — the same invocation_id may be submitted
	// again once the session frees up and will then be admitted normally,
	// because nothing was persisted for the rejected attempt.
	ErrSessionBusy = errors.New("ledger: session already has an active run")
	// ErrHistoryResetInProgress means admission reached the durable reset fence.
	// Nothing was persisted and the same invocation may be retried after the
	// lease expires or its owner releases it.
	ErrHistoryResetInProgress = errors.New("ledger: session history reset is in progress")
	// ErrResetScopeNotFound means the bot or session named by a reset lease no
	// longer exists (deleted or soft-deleted). Acquire loops must fail fast on
	// it instead of retrying: the scope can never become acquirable again.
	ErrResetScopeNotFound = errors.New("ledger: reset scope no longer exists")
)

Functions

func HasResumeContext added in v0.21.0

func HasResumeContext(input []byte) bool

HasResumeContext reports whether a run's admission input still carries the resume intent saved at turn start. The recovery worker retires the intent once it can no longer continue, which also removes the run from the pending-resume index.

Types

type AdmitParams

type AdmitParams struct {
	// ResumeRunID is checked inside the admission transaction, under the parent lock.
	ResumeRunID      string
	RunID            string
	BotID            string
	SessionID        string
	InvocationID     string
	TurnID           string
	Input            []byte
	InputFingerprint string
}

AdmitParams is one durable admission. RunID and TurnID are minted by the caller so the identity it hands back to a client is the identity that was committed.

type ClaimParams

type ClaimParams struct {
	RunID string
	// OwnerID is empty for a single-instance memory backend, where there is
	// only ever one owner and no cross-process claim to arbitrate.
	OwnerID        string
	FencingToken   int64
	LiveGeneration string
}

ClaimParams moves ownership of an accepted run to one owner.

type Cursor

type Cursor struct {
	LiveGeneration string
	RunID          string
}

Cursor is a keyset position in the stale-generation sweep. The zero Cursor starts a sweep from the beginning.

type FinalizeParams

type FinalizeParams struct {
	RunID        string
	FencingToken int64
	State        State
	ErrorCode    string
	// ExpectedState is optional. Shutdown uses it to avoid terminalizing a
	// decision that parked after its classification read.
	ExpectedState State
}

FinalizeParams is the fenced terminal write, used by the owner for completed/aborted/failed and by the reaper for lost.

type OrphanQuery

type OrphanQuery struct {
	MinAge time.Duration
	Limit  int32
}

OrphanQuery finds admissions that committed but were never claimed.

type OrphanResetStore

type OrphanResetStore interface {
	FenceAndFinalizeOrphan(ctx context.Context, reset ResetLease, run Run) (Run, bool, error)
}

OrphanResetStore atomically invalidates a disappeared owner's persistence token and terminalizes its active run while the same reset lease is still valid. PostgreSQL implements this as one parent-locked transaction.

type PostgresStore

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

PostgresStore implements Store over the session_runs table.

func (*PostgresStore) AcquireReset

func (s *PostgresStore) AcquireReset(ctx context.Context, lease ResetLease, ttl time.Duration) (ResetLease, bool, error)

func (*PostgresStore) ActiveRun

func (s *PostgresStore) ActiveRun(ctx context.Context, sessionID string) (Run, error)

func (*PostgresStore) ActiveRunsByBot

func (s *PostgresStore) ActiveRunsByBot(ctx context.Context, botID string) ([]Run, error)

func (*PostgresStore) Admit

func (s *PostgresStore) Admit(ctx context.Context, params AdmitParams) (Run, bool, error)

func (*PostgresStore) Claim

func (s *PostgresStore) Claim(ctx context.Context, params ClaimParams) (Run, bool, error)

func (*PostgresStore) EffectiveReset

func (s *PostgresStore) EffectiveReset(ctx context.Context, botID, sessionID string) (ResetLease, bool, error)

func (*PostgresStore) FenceAndFinalizeOrphan

func (s *PostgresStore) FenceAndFinalizeOrphan(ctx context.Context, reset ResetLease, run Run) (Run, bool, error)

func (*PostgresStore) Finalize

func (s *PostgresStore) Finalize(ctx context.Context, params FinalizeParams) (Run, bool, error)

func (*PostgresStore) Get

func (s *PostgresStore) Get(ctx context.Context, runID string) (Run, error)

func (*PostgresStore) GetByInvocation

func (s *PostgresStore) GetByInvocation(ctx context.Context, sessionID, invocationID string) (Run, error)

func (*PostgresStore) LatestRun

func (s *PostgresStore) LatestRun(ctx context.Context, sessionID string) (Run, error)

func (*PostgresStore) NextFencingToken

func (s *PostgresStore) NextFencingToken(ctx context.Context) (int64, error)

func (*PostgresStore) OrphanedRuns

func (s *PostgresStore) OrphanedRuns(ctx context.Context, query OrphanQuery) ([]Run, error)

func (*PostgresStore) PrepareFinish added in v0.20.0

func (s *PostgresStore) PrepareFinish(ctx context.Context, params PrepareFinishParams) (Run, bool, error)

func (*PostgresStore) ReleaseReset

func (s *PostgresStore) ReleaseReset(ctx context.Context, lease ResetLease) (bool, error)

func (*PostgresStore) RenewReset

func (s *PostgresStore) RenewReset(ctx context.Context, lease ResetLease, ttl time.Duration) (ResetLease, bool, error)

func (*PostgresStore) RequestAbort

func (s *PostgresStore) RequestAbort(ctx context.Context, runID string) (Run, bool, error)

func (*PostgresStore) Resume

func (s *PostgresStore) Resume(ctx context.Context, runID string, fencingToken int64) (Run, bool, error)

func (*PostgresStore) SessionIDsByBot added in v0.22.0

func (s *PostgresStore) SessionIDsByBot(ctx context.Context, botID string) ([]string, error)

func (*PostgresStore) SetWaitingDecision

func (s *PostgresStore) SetWaitingDecision(ctx context.Context, runID string, fencingToken int64) (Run, bool, error)

func (*PostgresStore) StaleGenerationRuns

func (s *PostgresStore) StaleGenerationRuns(ctx context.Context, query StaleGenerationQuery) ([]Run, error)

type PrepareFinishParams added in v0.20.0

type PrepareFinishParams struct {
	RunID                string
	FencingToken         int64
	State                State
	ErrorCode            string
	AllowWaitingDecision bool
}

PrepareFinishParams records the fenced, recoverable terminal proposal. A proposal ordinarily starts from running. AllowWaitingDecision is reserved for an explicit abort/failure that must terminalize a parked execution; ordinary terminal stream events must leave waiting_decision parked.

type ResetLease

type ResetLease struct {
	Scope     string
	BotID     string
	SessionID string
	Token     string
	ExpiresAt time.Time
}

ResetLease is the PostgreSQL half of one crash-recoverable history reset. Token-scoped renewal/release prevents an expired owner from disturbing a successor that acquired the same scope.

func (ResetLease) Valid

func (l ResetLease) Valid() bool

type ResetStore

type ResetStore interface {
	AcquireReset(ctx context.Context, lease ResetLease, ttl time.Duration) (ResetLease, bool, error)
	RenewReset(ctx context.Context, lease ResetLease, ttl time.Duration) (ResetLease, bool, error)
	ReleaseReset(ctx context.Context, lease ResetLease) (bool, error)
	EffectiveReset(ctx context.Context, botID, sessionID string) (ResetLease, bool, error)
	ActiveRunsByBot(ctx context.Context, botID string) ([]Run, error)
	// SessionIDsByBot lists every session a bot-wide reset covers, so the
	// live projections of all of them can be invalidated with the history.
	SessionIDsByBot(ctx context.Context, botID string) ([]string, error)
}

ResetStore is the durable admission/reset arbiter implemented by PostgreSQL. It stays separate from Store so focused runtime tests and non-admission embedders do not need to fake lifecycle mutation they never exercise.

type Run

type Run struct {
	RunID        string
	BotID        string
	SessionID    string
	InvocationID string
	// TurnID and TurnPosition are allocated at admission so that a terminal
	// write targets a pre-decided turn. Combined with the existing unique index
	// on (turn_id, turn_message_seq), that makes terminal replay idempotent
	// without any new machinery.
	TurnID       string
	TurnPosition int64

	State            State
	Input            []byte
	InputFingerprint string

	OwnerID      string
	FencingToken int64
	OwnerSince   time.Time
	// LiveGeneration is the live backend incarnation that claimed this run. It
	// is stamped once and never updated, which is what makes it a stable keyset
	// cursor for the fail-closed recovery sweep.
	LiveGeneration string

	AbortRequestedAt  time.Time
	ProposedState     State
	ProposedErrorCode string
	// ProposedErrorMessage and ErrorMessage are read from rows written before
	// the ledger stopped recording error text. Nothing writes them: a run's
	// failure is its code, and the cause belongs to the run's result record.
	ProposedErrorMessage string
	FinishProposedAt     time.Time
	ErrorCode            string
	ErrorMessage         string
	CreatedAt            time.Time
	UpdatedAt            time.Time
}

Run is one row of the durable ledger. Zero values mean "not set" rather than "empty": OwnerID is empty until the run is claimed, and AbortRequestedAt is the zero time until an abort is requested.

type StaleGenerationQuery

type StaleGenerationQuery struct {
	CurrentGeneration string
	After             Cursor
	Limit             int32
}

StaleGenerationQuery pages through runs whose claiming backend incarnation is no longer current.

type State

type State string

State is a durable run state. The set matches section 3 of docs/design/session-runtime-requirements.md and the session_runs CHECK constraint; do not add a value to one without the other.

const (
	// StateAccepted means the input and admission result are persisted, so the
	// caller can stop retrying, but no owner has claimed the run yet.
	StateAccepted State = "accepted"
	// StateRunning means a valid owner is executing.
	StateRunning State = "running"
	// StateWaitingDecision means execution is parked on a decision_id.
	StateWaitingDecision State = "waiting_decision"
	// StateFinishing means the final output is durable and a fenced terminal
	// outcome has been proposed, but the terminal row/live projection handshake
	// has not completed yet. It remains active so admission and lease renewal
	// cannot pass it.
	StateFinishing State = "finishing"

	StateCompleted State = "completed"
	StateAborted   State = "aborted"
	StateFailed    State = "failed"
	// StateLost means the owner disappeared and the accepted input was not
	// lost, but execution did not finish. It is not a success terminal.
	StateLost State = "lost"
)

func (State) Active

func (s State) Active() bool

Active reports whether a run occupies its session's single active slot.

func (State) Terminal

func (s State) Terminal() bool

Terminal reports whether a run can no longer transition.

type Store

type Store interface {
	// Admit persists one admission. created is false when this invocation was
	// already admitted, in which case the original row is returned unchanged
	// and no turn position is consumed. Callers compare InputFingerprint to
	// decide between "same submit, same answer" and a conflict.
	//
	// It returns ErrSessionBusy when the session already has a different active
	// run. Nothing is persisted in that case, so the same invocation may be
	// submitted again later. An implementation is expected to reject busy
	// cheaply — contention is ordinary traffic here, not an exceptional event.
	Admit(ctx context.Context, params AdmitParams) (run Run, created bool, err error)

	Get(ctx context.Context, runID string) (Run, error)
	GetByInvocation(ctx context.Context, sessionID, invocationID string) (Run, error)
	// ActiveRun returns the session's single active run, or ErrRunNotFound.
	ActiveRun(ctx context.Context, sessionID string) (Run, error)
	// LatestRun backs the snapshot fallback when live state is gone: it answers
	// what PostgreSQL can prove about the most recent run and nothing more.
	LatestRun(ctx context.Context, sessionID string) (Run, error)

	// NextFencingToken draws from the monotonic sequence shared with the
	// PostgreSQL persistence fence, so one token orders both.
	NextFencingToken(ctx context.Context) (int64, error)

	Claim(ctx context.Context, params ClaimParams) (run Run, applied bool, err error)
	SetWaitingDecision(ctx context.Context, runID string, fencingToken int64) (run Run, applied bool, err error)
	Resume(ctx context.Context, runID string, fencingToken int64) (run Run, applied bool, err error)
	PrepareFinish(ctx context.Context, params PrepareFinishParams) (run Run, applied bool, err error)
	Finalize(ctx context.Context, params FinalizeParams) (run Run, applied bool, err error)

	// RequestAbort records the intent, which is not fenced: an abort may arrive
	// at any instance and the owner applies it.
	RequestAbort(ctx context.Context, runID string) (run Run, applied bool, err error)

	StaleGenerationRuns(ctx context.Context, query StaleGenerationQuery) ([]Run, error)
	OrphanedRuns(ctx context.Context, query OrphanQuery) ([]Run, error)
}

Store is the durable session run ledger.

Every mutation after admission is a fenced idempotent statement: it applies only when the caller's fencing token still matches the row, and it reports applied=false rather than an error when it does not. False means "already applied, or superseded by a newer owner" — both of which are ordinary outcomes for a retrying owner or a reaper that failed over mid-transition.

func NewPostgres

func NewPostgres(q *dbsqlc.Queries, pools ...*pgxpool.Pool) Store

NewPostgres returns a Store backed by the PostgreSQL sqlc queries.

Jump to

Keyboard shortcuts

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