Documentation
¶
Overview ¶
Package localstore provides single-process exp.SessionStore implementations suitable for local development, tests, and single-instance apps (CLI tools, desktop apps, local web services). For multi-instance production deployments use a real database-backed store.
Index ¶
- type FileSessionStore
- func (s *FileSessionStore[State]) GetLatestSnapshot(ctx context.Context, sessionID string) (*exp.SessionSnapshot[State], error)
- func (s *FileSessionStore[State]) GetSnapshot(ctx context.Context, snapshotID string) (*exp.SessionSnapshot[State], error)
- func (s *FileSessionStore[State]) OnSnapshotStatusChange(ctx context.Context, snapshotID string) <-chan exp.SnapshotStatus
- func (s *FileSessionStore[State]) SaveSnapshot(ctx context.Context, id string, ...) (*exp.SessionSnapshot[State], error)
- type FileStoreOption
- type InMemorySessionStore
- func (s *InMemorySessionStore[State]) GetLatestSnapshot(_ context.Context, sessionID string) (*exp.SessionSnapshot[State], error)
- func (s *InMemorySessionStore[State]) GetSnapshot(_ context.Context, snapshotID string) (*exp.SessionSnapshot[State], error)
- func (s *InMemorySessionStore[State]) OnSnapshotStatusChange(ctx context.Context, snapshotID string) <-chan exp.SnapshotStatus
- func (s *InMemorySessionStore[State]) SaveSnapshot(_ context.Context, id string, ...) (*exp.SessionSnapshot[State], error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type FileSessionStore ¶
type FileSessionStore[State any] struct { // contains filtered or unexported fields }
FileSessionStore is a snapshot store that persists snapshots as JSON files on the local filesystem. Each snapshot is written to its own file named "<snapshotID>.json", under a per-call subdirectory ("prefix"):
<dir>/<prefix>/<snapshotID>.json
The prefix is derived from each call's context (see WithSnapshotPathPrefix) and defaults to "global" when none is configured; snapshots therefore always live under a subdirectory, never directly in the store root. That default places every session in one shared "global" directory, so pass WithSnapshotPathPrefix to scope them per tenant when identifiers could repeat across users (e.g. per-user session IDs).
The snapshot ID is the primary key: GetSnapshot, the by-ID SaveSnapshot (heartbeat, abort, finalize), and OnSnapshotStatusChange all open that file directly. GetLatestSnapshot, the only by-session lookup, resolves through a small per-session pointer file ("<prefix>/.pointers/<sessionID>.json") that records the session's current greatest-CreatedAt snapshot - one pointer read plus one snapshot read instead of scanning and parsing every row in the prefix. A missing (legacy store), stale, or unusable pointer transparently falls back to scanning the prefix directory for the most-recently-created row, then rewrites the pointer so later lookups are fast again. The prefix is always known on a by-ID call - unlike the session ID, which a by-ID caller does not have. That is why snapshots are grouped by prefix and kept flat within it rather than nested under a per-session directory.
The store is safe for concurrent use, and FileSessionStore.OnSnapshotStatusChange surfaces status changes written by other processes (or other store instances) sharing the directory by polling the snapshot files on an interval (see WithPollInterval); that cross-process visibility is what lets one process abort a detached turn another process is running. The store still does not provide cross-process transactions: the last successful rename wins. Each write is atomic (temp file + rename), so a concurrent reader sees either the old file or the new one, never a torn write.
func NewFileSessionStore ¶
func NewFileSessionStore[State any](dir string, opts ...FileStoreOption) (*FileSessionStore[State], error)
NewFileSessionStore creates a file-based snapshot store rooted at dir. The directory is created (mode 0o700) if it does not already exist. Returns an error if dir is empty, cannot be created, or an option is set more than once. See WithMaxPersistedChainLength and WithSnapshotPathPrefix.
func (*FileSessionStore[State]) GetLatestSnapshot ¶
func (s *FileSessionStore[State]) GetLatestSnapshot(ctx context.Context, sessionID string) (*exp.SessionSnapshot[State], error)
GetLatestSnapshot returns the session's most recently created snapshot regardless of status, per the exp.SnapshotReader.GetLatestSnapshot contract. It resolves under the call's prefix directory (see WithSnapshotPathPrefix), so a session is only resolvable under the prefix it was written with.
The fast path reads the session's pointer file ([pointerDoc]) and loads the row it names - one pointer read plus one snapshot read. When the pointer is missing, stale (names a row that no longer exists or belongs to another session), or corrupt, it falls back to scanning the prefix directory and then rewrites the pointer for next time.
Recency is judged by the exp.SessionSnapshot.CreatedAt field (not file mtime), so a later rewrite of an older row - which preserves CreatedAt - does not move it ahead of a newer-created sibling. Ties are broken by snapshot ID. A file that fails to parse or vanishes mid-scan is skipped, so one corrupted row cannot hide every other session.
func (*FileSessionStore[State]) GetSnapshot ¶
func (s *FileSessionStore[State]) GetSnapshot(ctx context.Context, snapshotID string) (*exp.SessionSnapshot[State], error)
GetSnapshot retrieves a snapshot by ID. Returns nil if not found.
func (*FileSessionStore[State]) OnSnapshotStatusChange ¶
func (s *FileSessionStore[State]) OnSnapshotStatusChange(ctx context.Context, snapshotID string) <-chan exp.SnapshotStatus
OnSnapshotStatusChange subscribes to status changes for a snapshot. The returned channel yields the status at subscription time and every subsequent change until ctx is cancelled. A change is surfaced immediately when written through this store instance, and within one poll interval when written by another process sharing the directory (see WithPollInterval); polling re-reads the file the snapshot was found under at subscription time. If the snapshot does not exist at subscription time, the channel is closed without yielding a value.
Values are level-triggered: the latest status is always delivered, but a slow reader may skip intermediate values, and a subscriber that joins concurrently with a change may observe that status twice. Treat a received value as "the status is now X", not "X just happened once".
func (*FileSessionStore[State]) SaveSnapshot ¶
func (s *FileSessionStore[State]) SaveSnapshot( ctx context.Context, id string, fn func(existing *exp.SessionSnapshot[State]) (*exp.SessionSnapshot[State], error), ) (*exp.SessionSnapshot[State], error)
SaveSnapshot atomically reads, applies fn, and persists. See exp.SnapshotWriter for the full contract; this implementation calls fn exactly once per call.
type FileStoreOption ¶
type FileStoreOption interface {
// contains filtered or unexported methods
}
FileStoreOption configures a FileSessionStore at construction. It applies only to NewFileSessionStore.
func WithMaxPersistedChainLength ¶
func WithMaxPersistedChainLength(n int) FileStoreOption
WithMaxPersistedChainLength bounds how many snapshots a single conversation chain keeps on disk. Each save walks the new snapshot's parentId chain and unlinks every row older than the newest n, capping per-conversation disk use.
n must be at least 1; NewFileSessionStore rejects 0 or a negative value. A window of 1 retains only the latest snapshot (each save prunes its predecessor). Omitting the option entirely (the default) leaves pruning disabled, and chains grow without bound.
Snapshots are self-contained (each carries the full session state), so dropping an old ancestor only removes it as a resume point; every surviving row remains fully loadable. Retention follows parentId links, whereas FileSessionStore.GetLatestSnapshot resolves recency by CreatedAt, so pruning only ever removes rows reachable from the saved snapshot's own chain; a sibling branch (e.g. after a regenerate) is pruned independently when it is itself extended.
func WithPollInterval ¶
func WithPollInterval(d time.Duration) FileStoreOption
WithPollInterval sets how often the store re-reads subscribed snapshot files to surface status changes that other processes (or other store instances) sharing the directory write through FileSessionStore.OnSnapshotStatusChange. That cross-process visibility is what lets one process observe an abort (or any status change) another process commits, for example to stop a detached turn it is running.
The default is one second. A value <= 0 disables cross-process polling: subscriptions then observe only changes written through this same store instance.
func WithSnapshotPathPrefix ¶
func WithSnapshotPathPrefix(fn func(ctx context.Context) string) FileStoreOption
WithSnapshotPathPrefix derives a per-call subdirectory from the operation's context, isolating snapshots by tenant: a snapshot written under one prefix is visible only to calls that derive the same prefix. A typical fn pulls a stable identity (e.g. an authenticated user or org ID) out of ctx.
The returned value may contain "/" to nest several levels (e.g. "org-42/user-7"). It must be non-empty: an empty or separator-only result is rejected at call time, since the way to request the default "global" prefix is to omit this option entirely, not to return an empty value. The value must be stable for a given snapshot's lifetime, since every read recomputes it - derive it from stable identity, not from per-request state. A value that would escape the store directory (contains "..", a backslash, or a segment starting with ".") is likewise rejected at call time.
type InMemorySessionStore ¶
type InMemorySessionStore[State any] struct { // contains filtered or unexported fields }
InMemorySessionStore provides a thread-safe in-memory snapshot store. State is lost when the process exits; use FileSessionStore or a real backend when persistence is needed.
It implements exp.SessionStore and exp.SnapshotSubscriber.
func NewInMemorySessionStore ¶
func NewInMemorySessionStore[State any]() *InMemorySessionStore[State]
NewInMemorySessionStore creates a new in-memory snapshot store.
func (*InMemorySessionStore[State]) GetLatestSnapshot ¶
func (s *InMemorySessionStore[State]) GetLatestSnapshot(_ context.Context, sessionID string) (*exp.SessionSnapshot[State], error)
GetLatestSnapshot returns the session's most recently created snapshot regardless of status, per the exp.SnapshotReader.GetLatestSnapshot contract. Ties on CreatedAt are broken by SnapshotID so resolution is deterministic. The returned snapshot is a deep copy.
func (*InMemorySessionStore[State]) GetSnapshot ¶
func (s *InMemorySessionStore[State]) GetSnapshot(_ context.Context, snapshotID string) (*exp.SessionSnapshot[State], error)
GetSnapshot retrieves a snapshot by ID. Returns nil if not found.
func (*InMemorySessionStore[State]) OnSnapshotStatusChange ¶
func (s *InMemorySessionStore[State]) OnSnapshotStatusChange(ctx context.Context, snapshotID string) <-chan exp.SnapshotStatus
OnSnapshotStatusChange subscribes to status changes for a snapshot. The returned channel yields the current status (if any) and any subsequent changes, until ctx is cancelled.
func (*InMemorySessionStore[State]) SaveSnapshot ¶
func (s *InMemorySessionStore[State]) SaveSnapshot( _ context.Context, id string, fn func(existing *exp.SessionSnapshot[State]) (*exp.SessionSnapshot[State], error), ) (*exp.SessionSnapshot[State], error)
SaveSnapshot atomically reads, applies fn, and persists. See exp.SnapshotWriter for the full contract; this implementation calls fn exactly once per call.