session

package
v1.7.0 Latest Latest
Warning

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

Go to latest
Published: Jun 12, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package session persists agent conversation history across runs.

Sessions enable multi-turn conversations: a user runs a task, the agent responds, and the user continues the conversation with "odek continue", picking up the full message history from the previous turn.

Storage: ~/.odek/sessions/<id>.json. Each file is a full conversation transcript including system messages, user turns, assistant responses, tool calls, and tool results. Sessions are loaded by ID for continuation or by listing metadata for browsing.

The Store is intentionally minimal — it's a JSON file manager, not a database. Session struct fields are all public, so callers can mutate the session directly and call Save(). This makes advanced operations (editing, truncating, merging sessions) trivial at the CLI layer.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BuildConversationText added in v0.58.0

func BuildConversationText(messages []llm.Message) string

BuildConversationText extracts user and assistant text from messages for embedding. Tool calls and results are excluded — they add noise.

func NovelResources added in v1.0.0

func NovelResources(userText string, toolText string) []string

NovelResources returns resources from toolText that do not appear in userText. Order preserved; case-insensitive comparison.

func ResourcesIn added in v1.0.0

func ResourcesIn(text string) []string

ResourcesIn returns the set of resource-like tokens found in text.

func ValidateSessionID

func ValidateSessionID(id string) error

ValidateSessionID validates that a session ID is safe for filesystem use. Rejects empty strings, path separators, traversal patterns, and dot names.

Types

type AuditIngest added in v1.0.0

type AuditIngest struct {
	Turn        int       `json:"turn"`
	Source      string    `json:"source"`       // e.g. "https://x", "/abs/path", "mcp:server:tool"
	ContentHash string    `json:"content_hash"` // sha256 of the ingested body (first 16 hex)
	At          time.Time `json:"at"`
}

AuditIngest records that the agent ingested an untrusted-source content blob at the given turn.

type AuditLog added in v1.0.0

type AuditLog struct {
	SessionID string        `json:"session_id"`
	Ingests   []AuditIngest `json:"ingests"`
	Turns     []AuditTurn   `json:"turns"`
}

AuditLog is the per-session aggregate.

type AuditStore added in v1.0.0

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

AuditStore manages per-session audit logs under <dir>/audit/.

func NewAuditStore added in v1.0.0

func NewAuditStore(dir string) *AuditStore

NewAuditStore returns a store rooted at dir; the audit subdir is created on first write.

func (*AuditStore) Load added in v1.0.0

func (s *AuditStore) Load(sessionID string) (AuditLog, error)

Load returns the audit log for a session, or empty if not present.

func (*AuditStore) RecordIngest added in v1.0.0

func (s *AuditStore) RecordIngest(sessionID string, turn int, source, content string) error

RecordIngest appends an ingest entry for a session.

func (*AuditStore) RecordTurn added in v1.0.0

func (s *AuditStore) RecordTurn(sessionID string, turn AuditTurn) error

RecordTurn appends a turn assessment for a session.

type AuditTurn added in v1.0.0

type AuditTurn struct {
	Turn                 int      `json:"turn"`
	UserMessage          string   `json:"user_message"`
	ToolCalls            []string `json:"tool_calls"`                // names of tools called this turn
	NovelResources       []string `json:"novel_resources,omitempty"` // resources referenced by tools but not by user
	IngestedUntrusted    bool     `json:"ingested_untrusted"`
	SuspiciousDivergence bool     `json:"suspicious_divergence"`
}

AuditTurn records the per-turn divergence assessment.

type IndexEntry

type IndexEntry struct {
	ID        string    `json:"id"`
	Title     string    `json:"title"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
	Turns     int       `json:"turns"`
}

IndexEntry holds minimal session metadata for the session index. This avoids loading every session file just to list or find the latest.

type SearchResult added in v0.58.0

type SearchResult struct {
	SessionID string  `json:"session_id"`
	Score     float32 `json:"score"` // cosine similarity, higher = more relevant
}

SearchResult holds a single session search result.

type Session

type Session struct {
	ID        string        `json:"id"`               // e.g. "20260518-abc123"
	CreatedAt time.Time     `json:"created_at"`       // first message time
	UpdatedAt time.Time     `json:"updated_at"`       // last append time
	Model     string        `json:"model"`            // model name used
	Turns     int           `json:"turns"`            // number of user turns
	Task      string        `json:"task"`             // first user message (label)
	Sandbox   bool          `json:"sandbox"`          // was sandboxed — auto-apply on resume
	Messages  []llm.Message `json:"messages"`         // full conversation history
	Buffer    []string      `json:"buffer,omitempty"` // last N turn summaries (memory tier 2)
}

Session represents a single multi-turn conversation with the agent. All fields are exported for direct manipulation at the CLI layer.

func (*Session) GetMessages

func (s *Session) GetMessages() []llm.Message

GetMessages returns the session's message slice. Nil-safe. Returns an empty (non-nil) slice for a session with no messages.

type Store

type Store struct {

	// Vec is the optional semantic search index. When non-nil, every
	// Save/Delete/Cleanup call updates the vector index automatically.
	// Call InitVectorIndex() to initialize.
	Vec *VectorIndex
	// contains filtered or unexported fields
}

Store manages session files in a directory on disk. Operations are simple file reads/writes — no locking, no caching.

func NewStore

func NewStore() (*Store, error)

NewStore creates a session store rooted at ~/.odek/sessions/. The directory is created if it doesn't exist.

func (*Store) Append

func (s *Store) Append(id string, newMsgs []llm.Message) error

Append adds new messages to an existing session, updates timestamps and turn counts, and saves the result atomically. The full read-modify-write is serialized by s.mu to prevent both concurrent-write data loss and symlink-swap TOCTOU attacks.

func (*Store) Cleanup

func (s *Store) Cleanup(before time.Time) (int, error)

Cleanup deletes all sessions whose UpdatedAt is before the given time. Returns the count of deleted sessions. Idempotent — nonexistent files are skipped silently. Uses the session index for efficient batch operations. Falls back to scanning individual session files when no index exists (backward compat).

func (*Store) Create

func (s *Store) Create(messages []llm.Message, model, task string) (*Session, error)

Create persists a new session with the given messages and metadata. It generates an ID, sets timestamps, counts user turns, and saves.

func (*Store) Delete

func (s *Store) Delete(id string) error

Delete removes a session file from disk and removes its entry from the session index. Returns nil if the file doesn't exist (idempotent delete).

func (*Store) Dir

func (s *Store) Dir() string

Dir returns the session store directory path. Exported for testing and debugging.

func (*Store) InitVectorIndex added in v0.58.0

func (s *Store) InitVectorIndex(cfg *embedding.Config) error

InitVectorIndex initializes the semantic search index using the embedding backend selected by cfg (nil = default RandomProjections). Must be called after NewStore, before the first Save. Safe to call multiple times — subsequent calls are no-ops once the index is ready.

func (*Store) Latest

func (s *Store) Latest() (*Session, error)

Latest returns the most recently updated session, or nil if no sessions exist. Returns an error when no sessions exist. Uses the session index for O(1) lookups. Falls back to scanning individual session files when no index exists (backward compat).

func (*Store) List

func (s *Store) List(limit int) ([]Session, error)

List returns session summaries ordered by UpdatedAt descending (most recent first). limit caps the number returned (0 = all). Only metadata fields are populated — Messages is nil to keep listings lightweight. Uses the session index for O(n) reads (n = session count, but no JSON parsing per session). Falls back to loading each session file when no index exists (backward compat).

func (*Store) Load

func (s *Store) Load(id string) (*Session, error)

Load reads a session from disk by ID. Returns an error if the file doesn't exist or can't be parsed.

func (*Store) Path

func (s *Store) Path(id string) string

Path returns the absolute filesystem path for a session file. Exported for testing and debugging.

func (*Store) Save

func (s *Store) Save(sess *Session) error

Save writes a session to disk atomically and durably via fsatomic.WriteFile (temp-file → fsync → rename → dir fsync). This prevents:

  • Partial writes from crashes (rename is atomic on POSIX)
  • Data loss on power failure (the fsync flushes bytes before the rename)
  • Symlink-following TOCTOU attacks (os.Rename replaces the directory entry itself — it does NOT follow symlinks)

type VectorIndex added in v0.58.0

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

VectorIndex provides semantic session search over the shared embedding backend (internal/embedding): RandomProjections by default, or any OpenAI-compatible HTTP embeddings API when configured.

Lifecycle:

  1. Init / InitWithConfig loads persisted state (when the fingerprint matches), or fits+embeds from all sessions.
  2. On Add: embed conversation text and insert into the store.
  3. On Search: embed query, k-NN search, return ranked results.
  4. On Remove: delete from the store.
  5. Save persists store + embedder state + meta atomically.

Resilience: with a remote backend, a down server never fails a session save or surfaces an error to search — Add/Search degrade silently and a 30s cool-down (failedAt) keeps the backend from being hammered. The keyword fallback in the session_search tool is the safety net while the embedder is unavailable.

Thread-safe: all exported methods hold the RWMutex.

func (*VectorIndex) Add added in v0.58.0

func (vi *VectorIndex) Add(sessionID string, messages []llm.Message) error

Add embeds the conversation text and adds the session to the index, replacing any existing entry. Best-effort: a missing/failing embedding backend never fails the caller's session save — the entry is simply skipped and a retry cool-down starts. If the index was not ready (e.g. the backend was down at init), a rebuild is attempted first; it already picks up the just-saved session from disk.

func (*VectorIndex) Init added in v0.58.0

func (vi *VectorIndex) Init(dir string) error

Init creates or loads the vector index using the default RandomProjections backend. Equivalent to InitWithConfig(dir, nil).

func (*VectorIndex) InitWithConfig added in v1.6.0

func (vi *VectorIndex) InitWithConfig(dir string, cfg *embedding.Config) error

InitWithConfig creates or loads the vector index using the embedding backend selected by cfg (nil = default RandomProjections). If persisted state in the same embedding space exists it is loaded directly; otherwise the index is rebuilt from all session files. A backend that is down at init time is not fatal — the index stays empty (search falls back to keyword) and retries after the cool-down.

func (*VectorIndex) Ready added in v0.58.0

func (vi *VectorIndex) Ready() bool

Ready returns true if the index has been initialized.

func (*VectorIndex) Remove added in v0.58.0

func (vi *VectorIndex) Remove(sessionID string) error

Remove deletes a session from the index. Idempotent.

func (*VectorIndex) Save added in v0.58.0

func (vi *VectorIndex) Save() error

Save persists the store, embedder state, and meta to disk.

func (*VectorIndex) Search added in v0.58.0

func (vi *VectorIndex) Search(query string, k int) ([]SearchResult, error)

Search embeds the query and returns the k most similar sessions ranked by cosine similarity. Returns nil (no error) when the index is unavailable — not ready, empty, or the embedding backend failed — so the caller falls back to keyword search. If the index was not ready, one rebuild is attempted (subject to the cool-down).

Jump to

Keyboard shortcuts

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