# Session catalog

> List and inspect stored Session identities and metadata.

- Path: `Guides > Harness > Session Persistence > Session Store > Session catalog`
- Human: https://looprig.com/docs/guides/harness/session-persistence/session-store/catalog
- Machine index: https://looprig.com/llms.txt

The catalog is a replay-free KV projection for session pickers and status
readers. It is derived from the event ledger and can be rebuilt at any time.
The appender updates it after a successful durable event append; readers never
need to open a ledger cursor for the common list or one-session lookup.

## Catalog contract

The exact public types and methods are:

```go
type CatalogOption func(*catalogOptions)

func WithCatalogClock(CatalogClock) CatalogOption
func WithCatalogLogger(CatalogLogger) CatalogOption
func WithCatalogReplayer(EventReplayerOpener) CatalogOption

type CatalogClock func() time.Time

type CatalogLogger interface {
	CatalogUpdateFailed(error)
}

type EventReplayerOpener interface {
	OpenInternalEventReplayer(uuid.UUID, ReplayRequest) (journal.EventReplayer, error)
}

func (s *Store) OpenCatalog(...CatalogOption) *Catalog
func (c *Catalog) UpdateOnEvent(context.Context, event.Event, uint64) error
func (c *Catalog) ListSessions(context.Context) ([]SessionMeta, error)
func (c *Catalog) ReadMeta(context.Context, uuid.UUID) (SessionMeta, bool, error)
func (c *Catalog) RepairCatalog(context.Context, uuid.UUID) (SessionMeta, error)
```

`UpdateOnEvent` always returns nil by contract. A KV failure is sent to the
optional logger and repaired later. `ListSessions` and `ReadMeta` surface typed
read/decode errors. `RepairCatalog` surfaces failures because repair was an
explicit request.

## Session meta fields

The exact JSON-backed projection is:

```go
type SessionMeta struct {
	SessionID         uuid.UUID
	Title             string
	CreatedAt         time.Time
	LastActiveAt      time.Time
	Status            SessionStatus
	AgentKind         string
	LoopCount         int
	ConfigFingerprint event.ConfigFingerprint
	State             SessionState
	LastJournalSeq    uint64
	ActiveTurnID      uuid.UUID
	WaitingGateID     uuid.UUID
	LastCheckpoint    CheckpointSummary
	CurrentWorkspace  WorkspacePointer
	Loops             []LoopUsageMeta
	Hustles           []HustleUsageAggregate
	// LastTurn and LastStep are codec-safe private eventSummary pointers.
}
```

The source adds JSON tags and private `LastTurn`/`LastStep` summaries. Their
wire form stores the journal sequence plus marshaled event bytes, not a bare
interface value.

`SessionStatus` has `StatusActive` and `StatusStopped`. `SessionState` has
`StateRunning`, `StateWaitingOnGate`, `StateIdle`, `StateFailed`,
`StateInterrupted`, and `StateStopped`. `StateStopped` wins over every later
projection and survives on disk.

The bounded loop projection is:

```go
type LoopUsageMeta struct {
	LoopID          uuid.UUID
	Runtime         event.ModelRuntime
	RuntimeSeq      uint64
	RuntimeValueSeq uint64
	CumulativeUsage content.Usage
	ContextSeq      uint64
	ContextValueSeq uint64
	CurrentContext  event.ContextMeasurement
}

type WorkspacePointer struct {
	Ref     workspacestore.Ref
	EventID uuid.UUID
	Seq     uint64
	Source  WorkspacePointerSource
}

type CheckpointSummary struct {
	Ref         workspacestore.Ref
	EventID     uuid.UUID
	Seq         uint64
	Consistency event.SnapshotConsistency
}
```

`Loops` is sorted by loop ID and folds authoritative StepDone usage exactly
once. Context and runtime watermarks prevent delayed lifecycle events from
regressing the projection.

## Projection and CAS

`applyEvent` maps only catalog-relevant events. `SessionStarted` seeds identity
and the primary loop; `TurnStarted`, `GateOpened`, gate resolution, turn
terminal events, `StepDone`, `RestoreDone`, `LoopStarted`, and `SessionStopped`
update bounded fields. Every changed event advances `LastJournalSeq` by max,
never by blind assignment.

The online path is a read-modify-write under storage KV revision CAS. It retries
conflicts up to eight times; exhaustion returns `*CatalogConflictError` to
repair logic, while the best-effort appender path logs and swallows it.

```mermaid
%%{init: {"theme":"dark"}}%%
sequenceDiagram
    participant A as Event appender
    participant C as Catalog
    participant K as KV
    participant L as Ledger

    A->>L: durable append event
    A->>C: UpdateOnEvent(event, seq)
    C->>K: Get sessions/UUID, revision
    C->>C: applyEvent(meta, event, seq)
    C->>K: Put(expected revision)
    alt CAS conflict
        C->>K: re-read and retry, up to 8
    end
    Note over L,K: KV is a cache; RepairCatalog folds L when needed
```

## Read and repair

`ListSessions` reads keys then values from KV and returns deterministic session
ID order. A key deleted between those operations is skipped. `ReadMeta` makes
one KV load and returns `(zero, false, nil)` for absence.

`RepairCatalog` opens privileged event replay, requires a `SessionStarted`,
folds from the beginning, and writes under CAS. If a newer cached
`LastJournalSeq` appears while scanning, it rescans rather than overwriting a
newer projection. Missing starts return `*EmptySessionError`; corrupt cache
data is replaceable by repair.

## Catalog example

```go
catalog := store.OpenCatalog()
meta, found, err := catalog.ReadMeta(ctx, id)
if err != nil {
	var read *sessionstore.CatalogReadError
	if errors.As(err, &read) {
		meta, err = catalog.RepairCatalog(ctx, id)
		found = err == nil
	}
}
if err != nil {
	return err
}
if found {
	fmt.Printf("%s %s %s\\n", meta.SessionID, meta.State, meta.Title)
}
```

Do not use `SessionMeta` as authorization or restore truth. It contains bounded
derived summaries; the journal contains the authoritative event and command
history.

## Source and proof

- [`SessionMeta` and catalog types](https://github.com/looprig/harness/blob/main/pkg/sessionstore/catalog.go)
- [`catalog options and methods`](https://github.com/looprig/harness/blob/main/pkg/sessionstore/catalog.go)
- [`catalog fold and CAS tests`](https://github.com/looprig/harness/blob/main/pkg/sessionstore/catalog_test.go)
- [`projection validation tests`](https://github.com/looprig/harness/blob/main/pkg/sessionstore/catalog_validation_test.go)
- [`usage and context projection tests`](https://github.com/looprig/harness/blob/main/pkg/sessionstore/catalog_usage_test.go)
