sessions

package
v0.0.0-...-bea457f Latest Latest
Warning

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

Go to latest
Published: Jul 24, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrAlreadyExists = errors.New("session already exists")

ErrAlreadyExists is returned by Create when a session with the same ID is already in the store. Callers should use errors.Is to match.

View Source
var ErrNotFound = errors.New("session not found")

ErrNotFound is returned by Get / Update when the session doesn't exist. Callers should use errors.Is(err, ErrNotFound) to match.

View Source
var ErrSessionExpired = errors.New("session is expired and cannot be updated")

ErrSessionExpired is returned by Update when the target session's current Status is StatusExpired AND the update would transition it out of Expired. Slice 6.4 (codex pass 2) caught the race where `sessions sweep` retires an idle chat's row but the still-running REPL then writes StatusActive / StatusPaused / StatusEnded over it on its next turn or exit, "reviving" a session the operator meant to retire. Enforcing the terminal-status guard at the Store layer makes the check atomic (impl-side: SQL WHERE clause for SQLite, mutex-protected read-modify for Memory). Callers MUST match this sentinel with errors.Is and stop touching the session.

Functions

func DefaultSQLiteStorePath

func DefaultSQLiteStorePath() (string, error)

DefaultSQLiteStorePath returns the conventional location for the session store database. Follows XDG when $XDG_DATA_HOME is set (Linux convention, also honored on macOS); otherwise falls back to $HOME/.local/share/agent-controller/sessions.db.

Callers should not assume the parent directory exists — NewSQLiteStore will mkdir it.

Types

type ListFilter

type ListFilter struct {
	AgentName string
	Status    SessionStatus
	Limit     int
}

ListFilter narrows List() results. All fields are optional and combine as AND. Limit <= 0 returns all matches.

type MemoryStore

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

MemoryStore is the in-memory Store implementation. Suitable for tests and ephemeral REPL sessions where persistence across process exit is explicitly not wanted. Sessions die with the process; the SQLite store (slice 6.2) is the default for actual persistence.

Concurrency: a sync.RWMutex guards the map. Reads (Get, List) take the read lock; writes (Create, Update, Delete) take the write lock.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore returns an empty in-memory store ready for use.

func (*MemoryStore) Close

func (m *MemoryStore) Close() error

func (*MemoryStore) Create

func (m *MemoryStore) Create(ctx context.Context, s Session) error

func (*MemoryStore) Delete

func (m *MemoryStore) Delete(ctx context.Context, id string) error

func (*MemoryStore) Get

func (m *MemoryStore) Get(ctx context.Context, id string) (Session, error)

func (*MemoryStore) List

func (m *MemoryStore) List(ctx context.Context, filter ListFilter) ([]Session, error)

func (*MemoryStore) MarkExpired

func (m *MemoryStore) MarkExpired(ctx context.Context, cutoff time.Time) ([]string, error)

func (*MemoryStore) Update

func (m *MemoryStore) Update(ctx context.Context, s Session) error

type SQLiteStore

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

SQLiteStore is the file-backed Store implementation. Default ships with slice 6.2; v0.8 adds Postgres/Redis impls of the same interface.

File path: callers pass an explicit path. For the default location (slice 6.3 will wire this), use DefaultSQLiteStorePath() which resolves to $XDG_DATA_HOME/agent-controller/sessions.db (or the platform-appropriate fallback).

func NewSQLiteStore

func NewSQLiteStore(path string) (*SQLiteStore, error)

NewSQLiteStore opens (or creates) the SQLite database at path, applies the schema + pragmas, and returns a ready-to-use Store. The parent directory is created with 0o700 if missing (session data is per-user — no group/other access).

Accepted path forms:

  • ":memory:" — ephemeral in-process database (tests, REPL fallback)
  • A plain filesystem path (relative or absolute)

NOT accepted: SQLite `file:` URIs (`file:/path/to/db?cache=shared`). They're rejected because our filesystem prep + permission hardening resolve the parent dir via filepath.Dir, which mangles URIs. Codex pass 4 of slice 6.2 caught the mismatch between an aspirational doc-comment and the actual behavior.

func (*SQLiteStore) Close

func (s *SQLiteStore) Close() error

Close releases the SQLite connection pool. Idempotent and safe for concurrent use:

  • sync.Once guarantees the underlying db.Close runs exactly once even under racing Close calls from multiple goroutines.
  • The *sql.DB handle is NOT nil'd. After Close, database/sql returns sql.ErrConnDone from any subsequent operation, so a racing Create/Get/List/Update/Delete fails cleanly with an error instead of panicking on a nil-pointer dereference.

Codex pass 1 of slice 6.2 caught the original implementation's unsynchronized read/write of s.db.

func (*SQLiteStore) Create

func (s *SQLiteStore) Create(ctx context.Context, sess Session) error

func (*SQLiteStore) Delete

func (s *SQLiteStore) Delete(ctx context.Context, id string) error

func (*SQLiteStore) Get

func (s *SQLiteStore) Get(ctx context.Context, id string) (Session, error)

func (*SQLiteStore) List

func (s *SQLiteStore) List(ctx context.Context, filter ListFilter) ([]Session, error)

func (*SQLiteStore) MarkExpired

func (s *SQLiteStore) MarkExpired(ctx context.Context, cutoff time.Time) ([]string, error)

func (*SQLiteStore) Path

func (s *SQLiteStore) Path() string

Path returns the file path the store was opened against. Exposed for tests + future `agentctl sessions ls` output.

func (*SQLiteStore) Update

func (s *SQLiteStore) Update(ctx context.Context, sess Session) error

type Session

type Session struct {
	// ID is the primary key. Stable across resumes. Format is
	// `s_<base36-millis>` from the runtime adapter, BUT callers must
	// not parse it — the format is an opaque identifier.
	ID string

	// AgentName is `spec.metadata.name` at session start. Stored for
	// list-by-agent queries; the canonical source is the Spec field.
	AgentName string

	// RuntimeType is `spec.runtime.type` at start. Pi vs opencode dispatch
	// looks at this to know which adapter the session was bound to;
	// resuming under a mismatched runtime is rejected by the caller
	// (slice 6.3 wires the check).
	RuntimeType string

	// Status is the session's current lifecycle position. See the
	// StatusXxx constants. Slice 6.1 only sets active/ended; later
	// slices wire paused/expired/failed.
	Status SessionStatus

	// CreatedAt is the wall-clock time the session was first created.
	// Stable. UTC at write; callers may localize for display.
	CreatedAt time.Time

	// LastActiveAt is the wall-clock time of the last Update — i.e.
	// the last time an adapter reported progress for this session.
	// Used for List() ordering and for the expiry TTL check in 6.4.
	LastActiveAt time.Time

	// Spec is the CompiledSpec the session was started with. Snapshotted
	// at creation so resume can replay it. Storing the full spec (not
	// just a path) lets the store outlive the spec file on disk.
	Spec adl.CompiledSpec

	// TraceContext is the W3C `traceparent` header of the session's
	// root OTel span. Each resumed turn opens a child span under it
	// (slice 6.5 wires this). Empty when tracing was off at creation.
	TraceContext string

	// AdapterState is an opaque map the adapter (Pi or opencode) uses
	// to find its own session state on disk. Format is adapter-defined.
	// For the Pi adapter today this carries the path to the underlying
	// Pi session directory; opencode's state surfaces here in 6.3.
	AdapterState map[string]any
}

Session is the durable record of a long-running agent run.

Field mutability split:

  • Immutable after Create: ID, AgentName, RuntimeType, CreatedAt, Spec, TraceContext. Store implementations IGNORE attempts to change these on Update — the caller can't drift session identity.
  • Mutable on Update: Status, LastActiveAt, AdapterState.

type SessionStatus

type SessionStatus string

SessionStatus enumerates the lifecycle states a session passes through.

Slice 6.1 only sets `active` and `ended`. The other three are reserved for slice 6.4 (lifecycle wire events) — defining them now keeps the type stable so 6.4 doesn't have to widen an in-use enum.

const (
	// StatusActive is a session currently being driven by an adapter.
	StatusActive SessionStatus = "active"
	// StatusEnded is a normally-completed session. Terminal.
	StatusEnded SessionStatus = "ended"
	// StatusPaused (slice 6.4): a chat-mode session the user stepped
	// away from (Ctrl-C between turns) but did not end.
	StatusPaused SessionStatus = "paused"
	// StatusExpired (slice 6.4): a session whose TTL was hit before
	// completion. Terminal.
	StatusExpired SessionStatus = "expired"
	// StatusFailed is a session that exited with an error.
	StatusFailed SessionStatus = "failed"
)

type Store

type Store interface {
	// Create persists a new session. Returns ErrAlreadyExists if a
	// session with the same ID is already in the store.
	Create(ctx context.Context, s Session) error

	// Get returns the session by ID. Returns ErrNotFound when the
	// session doesn't exist.
	Get(ctx context.Context, id string) (Session, error)

	// Update overwrites the mutable fields (Status, LastActiveAt,
	// AdapterState) of an existing session. Returns ErrNotFound when
	// the session doesn't exist. Immutable fields on the input
	// (ID, AgentName, RuntimeType, CreatedAt, Spec, TraceContext) are
	// kept at their original values — see the Session doc-comment.
	//
	// Returns ErrSessionExpired if the existing row's Status is
	// already StatusExpired AND the incoming update would change
	// it to something other than Expired. The terminal-status
	// guard is enforced atomically by the impl so a chat process
	// can't undo a concurrent `sessions sweep` even mid-turn.
	// Slice 6.4 (codex pass 2).
	Update(ctx context.Context, s Session) error

	// Delete removes the session. Idempotent: deleting a missing
	// session is NOT an error (matches the best-effort-cleanup pattern
	// the K8s backend uses for Pods and Secrets).
	Delete(ctx context.Context, id string) error

	// List returns sessions matching the filter, ordered by
	// LastActiveAt descending (most-recently-active first).
	// Empty filter returns every session.
	List(ctx context.Context, filter ListFilter) ([]Session, error)

	// MarkExpired bulk-transitions every currently-Active session
	// whose LastActiveAt is strictly before `cutoff` to
	// StatusExpired. Returns the IDs of sessions that were
	// transitioned. Used by the `agentctl sessions sweep`
	// subcommand to retire idle sessions past their TTL.
	//
	// Idempotent: a second sweep with the same cutoff is a no-op
	// because the sessions transitioned by the first sweep are no
	// longer StatusActive. Only Active sessions are touched —
	// already-Paused or already-Ended sessions are left alone so
	// the lifecycle history remains intact. Added in slice 6.4.
	MarkExpired(ctx context.Context, cutoff time.Time) ([]string, error)

	// Close releases any resources the store holds (DB handles, file
	// locks, etc.). Idempotent — calling Close on an already-closed
	// store is a no-op. MemoryStore.Close drops all data; SQLiteStore
	// (6.2) closes the DB handle but keeps the file.
	Close() error
}

Store is the abstraction every backing implementation satisfies. MemoryStore (6.1), SQLiteStore (6.2), and any future Postgres / Redis stores all expose this same surface.

Concurrency contract:

  • Implementations MUST be safe for concurrent use. The REPL (6.3) and the future HTTP server (v0.8) both call from multiple goroutines.
  • All methods take a context.Context. Implementations honor cancellation where the backing store supports it; in-memory ops return immediately and ignore ctx beyond a check at entry.

Jump to

Keyboard shortcuts

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