memory

package
v0.8.3 Latest Latest
Warning

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

Go to latest
Published: Mar 12, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package episodic provides context injection of episodic memory into the agent's system prompt. It combines curated daily memory files with recency-graded conversation history from the session archive, giving the agent continuity across sessions.

Package memory provides conversation memory storage and session archiving.

The package has two main subsystems:

Active memory (SQLiteStore) manages the working conversation context — messages that are actively used for LLM context windows. Messages can be compacted (summarized) when the context grows too large.

Session archive (ArchiveStore) provides immutable, long-term storage of all conversation transcripts. Messages are archived before any destructive operation (compaction, reset, shutdown), ensuring primary source data is never lost. The archive supports full-text search with gap-aware context expansion — search results include surrounding conversation bounded by natural silence gaps rather than rigid message counts.

Package summarizer provides a background worker that generates session metadata (titles, tags, summaries) for archived sessions that lack it. This decouples metadata generation from session lifecycle events, ensuring summaries are always produced even when sessions end during process shutdown.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ShortID added in v0.3.0

func ShortID(id string) string

ShortID safely truncates an ID to 8 characters for display. Returns the full string if shorter than 8 characters.

Types

type ArchiveAdapter added in v0.3.0

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

ArchiveAdapter bridges the ArchiveStore to the agent.SessionArchiver interface. It manages session lifecycle and coordinates message archival in the unified messages table.

func NewArchiveAdapter added in v0.3.0

func NewArchiveAdapter(store *ArchiveStore, msgStore MessageArchiver, tcStore ToolCallArchiver, logger *slog.Logger) *ArchiveAdapter

NewArchiveAdapter creates an adapter that implements agent.SessionArchiver. The msgStore and tcStore are used to set lifecycle status when archiving messages and tool calls in the unified table.

func (*ArchiveAdapter) ActiveSessionID added in v0.3.0

func (a *ArchiveAdapter) ActiveSessionID(conversationID string) string

ActiveSessionID returns the current session ID for a conversation, or empty.

func (*ArchiveAdapter) ActiveSessionStartedAt added in v0.7.0

func (a *ArchiveAdapter) ActiveSessionStartedAt(conversationID string) time.Time

ActiveSessionStartedAt returns when the active session for a conversation began, or the zero time if there is no active session. Uses the in-memory cache populated by StartSession and ActiveSessionID to avoid per-turn database lookups.

func (*ArchiveAdapter) ArchiveConversation added in v0.3.0

func (a *ArchiveAdapter) ArchiveConversation(conversationID string, messages []Message, reason string) error

ArchiveConversation archives all messages and tool calls from a conversation by setting their status to 'archived' in the unified table.

func (*ArchiveAdapter) ArchiveIterations added in v0.8.0

func (a *ArchiveAdapter) ArchiveIterations(iterations []ArchivedIteration) error

ArchiveIterations persists a batch of iteration records to the archive store.

func (*ArchiveAdapter) EndSession added in v0.3.0

func (a *ArchiveAdapter) EndSession(sessionID string, reason string) error

EndSession ends a session. Session metadata is generated by the background summarizer worker, not here — this avoids a race with process shutdown that previously caused summaries to be lost.

func (*ArchiveAdapter) EnsureSession added in v0.3.0

func (a *ArchiveAdapter) EnsureSession(conversationID string) string

EnsureSession starts a session if none is active for the conversation.

func (*ArchiveAdapter) LinkPendingIterationToolCalls added in v0.8.0

func (a *ArchiveAdapter) LinkPendingIterationToolCalls(sessionID string) error

LinkPendingIterationToolCalls links archived tool calls to their parent iterations using the tool_call_ids stored on the iteration records.

func (*ArchiveAdapter) OnMessage added in v0.3.0

func (a *ArchiveAdapter) OnMessage(_ string)

OnMessage is a no-op retained for interface compatibility. Session message counts are now computed from the unified messages table.

func (*ArchiveAdapter) StartSession added in v0.3.0

func (a *ArchiveAdapter) StartSession(conversationID string) (string, error)

StartSession begins a new session and returns its ID.

func (*ArchiveAdapter) Store added in v0.3.0

func (a *ArchiveAdapter) Store() *ArchiveStore

Store returns the underlying ArchiveStore for direct access (API endpoints, etc.)

type ArchiveConfig added in v0.3.0

type ArchiveConfig struct {
	// SilenceThreshold is the gap duration that signals a conversation boundary.
	// Default: 10 minutes.
	SilenceThreshold time.Duration

	// MaxContextMessages is the hard cap on context messages per direction.
	// Default: 50.
	MaxContextMessages int

	// MaxContextDuration is the time-based hard cap on context expansion.
	// Default: 1 hour.
	MaxContextDuration time.Duration
}

ArchiveConfig configures the archive store.

func DefaultArchiveConfig added in v0.3.0

func DefaultArchiveConfig() ArchiveConfig

DefaultArchiveConfig returns sensible defaults.

type ArchiveContextProvider added in v0.8.0

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

ArchiveContextProvider implements the agent.ContextProvider interface for injecting relevant past conversation excerpts into the system prompt. This is Layer 2 of the pre-warming system: Layer 1 provides knowledge (facts + KB docs), Layer 2 provides experiential judgment (prior reasoning about similar situations).

func NewArchiveContextProvider added in v0.8.0

func NewArchiveContextProvider(store ArchiveSearcher, maxResults, maxBytes int, logger *slog.Logger) *ArchiveContextProvider

NewArchiveContextProvider creates a context provider that searches the conversation archive for relevant past exchanges. maxResults caps the number of search hits; maxBytes caps the formatted output size to prevent context flooding.

func (*ArchiveContextProvider) GetContext added in v0.8.0

func (p *ArchiveContextProvider) GetContext(ctx context.Context, userMessage string) (string, error)

GetContext searches the conversation archive for excerpts relevant to the current wake context. Subjects are extracted from ctx (set by the wake bridge); if no subjects are available but the user message is short, it falls back to searching by message content.

Returns empty string when there is nothing to search for or no results are found. Errors from the archive store are logged and swallowed — archive injection should never block a wake.

type ArchiveReader added in v0.8.0

type ArchiveReader interface {
	// ListSessions returns sessions ordered newest-first. Pass empty
	// conversationID to list sessions across all conversations.
	ListSessions(conversationID string, limit int) ([]*Session, error)

	// GetSessionTranscript returns all archived messages for a session
	// in chronological order.
	GetSessionTranscript(sessionID string) ([]Message, error)
}

ArchiveReader is the subset of ArchiveStore needed by the episodic memory provider. Defined as an interface for testability.

type ArchiveReason added in v0.3.0

type ArchiveReason string

ArchiveReason describes why messages were archived.

const (
	ArchiveReasonCompaction ArchiveReason = "compaction"
	ArchiveReasonReset      ArchiveReason = "reset"
	ArchiveReasonShutdown   ArchiveReason = "shutdown"
	ArchiveReasonManual     ArchiveReason = "manual"
)

type ArchiveSearcher added in v0.8.0

type ArchiveSearcher interface {
	Search(opts SearchOptions) ([]SearchResult, error)
}

ArchiveSearcher abstracts archive search for testing. ArchiveStore satisfies this interface — no adapter needed.

type ArchiveStore added in v0.3.0

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

ArchiveStore handles immutable session transcript archiving.

func NewArchiveStore added in v0.3.0

func NewArchiveStore(dbPath string, messagesDB *sql.DB, cfg *ArchiveConfig, logger *slog.Logger) (*ArchiveStore, error)

NewArchiveStore creates a new archive store at the given database path. Pass nil for cfg to use DefaultArchiveConfig(). Pass nil for logger to suppress startup logging.

When messagesDB is non-nil (unified mode), message queries use that connection against the "messages" table. When nil (legacy mode), message queries use the archive database's "archive_messages" table.

func NewArchiveStoreFromDB added in v0.8.0

func NewArchiveStoreFromDB(db *sql.DB, cfg *ArchiveConfig, logger *slog.Logger) (*ArchiveStore, error)

NewArchiveStoreFromDB creates an ArchiveStore backed by an existing database connection (typically the main thane.db). The store does NOT own the connection and Close will not close it. All session, iteration, message, and tool-call queries go to the shared connection. This is the "consolidated" mode where archive.db no longer exists.

func (*ArchiveStore) ActiveSession added in v0.3.0

func (s *ArchiveStore) ActiveSession(conversationID string) (*Session, error)

ActiveSession returns the most recent unclosed session for a conversation, if any.

func (*ArchiveStore) ActiveSessionsWithLastActivity added in v0.8.0

func (s *ArchiveStore) ActiveSessionsWithLastActivity() ([]IdleSessionInfo, error)

ActiveSessionsWithLastActivity returns all unclosed sessions and the timestamp of the most recent message in each. Sessions with no messages use the session's started_at as the last activity time. This powers the summarizer's idle session detection — it survives crashes because it reads from the database rather than relying on in-memory state.

func (*ArchiveStore) ArchiveIterations added in v0.8.0

func (s *ArchiveStore) ArchiveIterations(iterations []ArchivedIteration) error

ArchiveIterations copies iteration records to the immutable archive. Iteration indices are automatically offset so that sessions spanning multiple Run() calls never collide on the (session_id, iteration_index) primary key.

func (*ArchiveStore) ArchiveMessages added in v0.3.0

func (s *ArchiveStore) ArchiveMessages(messages []Message) error

ArchiveMessages copies messages to the immutable archive. This is the core "never throw data away" operation.

In unified mode (messagesDB set), this is a no-op — messages already live in the unified table and are archived via status UPDATE by SQLiteStore.

func (*ArchiveStore) ArchiveToolCalls added in v0.3.0

func (s *ArchiveStore) ArchiveToolCalls(calls []ArchivedToolCall) error

ArchiveToolCalls copies tool call records to the immutable archive.

In unified mode (messagesDB set), this is a no-op — tool call archival is handled via status UPDATE by SQLiteStore.ArchiveToolCalls.

func (*ArchiveStore) ClaimActiveMessages added in v0.8.0

func (s *ArchiveStore) ClaimActiveMessages(conversationID, sessionID string) (int64, error)

ClaimActiveMessages stamps session_id on active messages for a conversation so they become retrievable by GetSessionTranscript. This is needed when the summarizer's idle backstop closes a session — active messages in the unified table have session_id=NULL until archival, so without this step the transcript query returns nothing and the session is marked empty.

In legacy mode (archive_messages), session_id is always set at insert time, so this is a no-op. In unified mode, the status column distinguishes active messages from compacted/archived ones.

func (*ArchiveStore) Close added in v0.3.0

func (s *ArchiveStore) Close() error

Close closes the underlying database connection. If the store was created via NewArchiveStoreFromDB with a shared connection, Close is a no-op — the caller owns the connection lifetime.

func (*ArchiveStore) CloseOrphanedSessions added in v0.7.0

func (s *ArchiveStore) CloseOrphanedSessions(before time.Time) (int64, error)

CloseOrphanedSessions ends any sessions that are still open (ended_at IS NULL) but were started before the given cutoff time. This recovers sessions orphaned by crashes (SIGKILL, OOM, panics) where EndSession was never called. Returns the number of sessions closed.

func (*ArchiveStore) DB added in v0.5.0

func (s *ArchiveStore) DB() *sql.DB

DB returns the underlying database connection. This allows other stores (e.g. WorkingMemoryStore) to share the archive database without opening a separate connection.

func (*ArchiveStore) EndSession added in v0.3.0

func (s *ArchiveStore) EndSession(sessionID string, reason string) error

EndSession marks a session as ended at the current time.

func (*ArchiveStore) EndSessionAt added in v0.3.0

func (s *ArchiveStore) EndSessionAt(sessionID string, reason string, endedAt time.Time) error

EndSessionAt marks a session as ended at a specific time.

func (*ArchiveStore) ExportSessionMarkdown added in v0.3.0

func (s *ArchiveStore) ExportSessionMarkdown(sessionID string) (string, error)

ExportSessionMarkdown exports a session transcript as human-readable markdown. Includes tool call records interleaved chronologically with messages.

func (*ArchiveStore) FTSEnabled added in v0.3.0

func (s *ArchiveStore) FTSEnabled() bool

FTSEnabled returns whether FTS5 full-text search is available.

func (*ArchiveStore) GetMessagesByTimeRange added in v0.3.0

func (s *ArchiveStore) GetMessagesByTimeRange(from, to time.Time, conversationID string, limit int) ([]Message, error)

GetMessagesByTimeRange returns archived messages within a time range.

func (*ArchiveStore) GetSession added in v0.3.0

func (s *ArchiveStore) GetSession(sessionID string) (*Session, error)

GetSession retrieves a session by ID.

func (*ArchiveStore) GetSessionIterations added in v0.8.0

func (s *ArchiveStore) GetSessionIterations(sessionID string) ([]ArchivedIteration, error)

GetSessionIterations returns archived iterations for a session ordered by iteration index.

func (*ArchiveStore) GetSessionToolCalls added in v0.3.0

func (s *ArchiveStore) GetSessionToolCalls(sessionID string) ([]ArchivedToolCall, error)

GetSessionToolCalls returns archived tool calls for a session in chronological order.

func (*ArchiveStore) GetSessionTranscript added in v0.3.0

func (s *ArchiveStore) GetSessionTranscript(sessionID string) ([]Message, error)

GetSessionTranscript returns all archived messages for a session in chronological order.

func (*ArchiveStore) ImportMessages added in v0.8.0

func (s *ArchiveStore) ImportMessages(messages []Message) error

ImportMessages inserts externally-sourced messages (e.g. from openclaw-import) into the archive. Unlike ArchiveMessages, which is a no-op in unified mode (since archival is a status UPDATE on existing rows), ImportMessages performs real INSERTs in both modes — the data doesn't already exist in any table.

In legacy mode, this delegates to ArchiveMessages. In unified mode, rows are inserted directly into the messages table with status='archived'. FTS triggers keep the full-text index in sync automatically.

func (*ArchiveStore) ImportToolCalls added in v0.8.0

func (s *ArchiveStore) ImportToolCalls(calls []ArchivedToolCall) error

ImportToolCalls inserts externally-sourced tool calls (e.g. from openclaw-import) into the archive. Like ImportMessages, this performs real INSERTs in both modes rather than being a no-op in unified mode.

func (*ArchiveStore) IsImported added in v0.3.0

func (s *ArchiveStore) IsImported(sourceID, sourceType string) (bool, error)

IsImported checks whether an external source ID has already been imported.

func (*ArchiveStore) LinkPendingIterationToolCalls added in v0.8.0

func (s *ArchiveStore) LinkPendingIterationToolCalls(sessionID string) error

LinkPendingIterationToolCalls reads iterations for a session, and for each iteration with stored tool_call_ids, updates the corresponding archive_tool_calls rows. Call this after tool calls have been archived so the UPDATE finds matching rows.

func (*ArchiveStore) LinkToolCallsToIteration added in v0.8.0

func (s *ArchiveStore) LinkToolCallsToIteration(sessionID string, iterationIndex int, toolCallIDs []string) error

LinkToolCallsToIteration sets the iteration_index on tool calls that belong to a specific iteration within a session. In unified mode, this updates the working DB's tool_calls table.

func (*ArchiveStore) ListChildSessions added in v0.8.0

func (s *ArchiveStore) ListChildSessions(parentSessionID string) ([]*Session, error)

ListChildSessions returns sessions whose parent_session_id matches the given ID, ordered chronologically. Used by the session inspector to show delegate sub-sessions.

func (*ArchiveStore) ListSessions added in v0.3.0

func (s *ArchiveStore) ListSessions(conversationID string, limit int) ([]*Session, error)

ListSessions returns sessions, newest first.

func (*ArchiveStore) PurgeImported added in v0.3.0

func (s *ArchiveStore) PurgeImported(sourceType string) (int, error)

PurgeImported removes all archive data that was imported from a given source type. This deletes sessions, messages, tool calls, and import metadata — a clean slate so the import can be re-run with improved logic.

In unified mode, messages live in a different database than sessions and metadata. The method handles this by deleting messages from the messages DB first, then cleaning up sessions and metadata from the archive DB in a transaction.

func (*ArchiveStore) RecordImport added in v0.3.0

func (s *ArchiveStore) RecordImport(sourceID, sourceType, archiveSessionID string) error

RecordImport creates a mapping from an external source ID to an archive session ID. Used by importers to track which external sessions have already been imported, enabling idempotent re-runs.

func (*ArchiveStore) Search added in v0.3.0

func (s *ArchiveStore) Search(opts SearchOptions) ([]SearchResult, error)

Search performs a full-text search with gap-aware context expansion.

func (*ArchiveStore) SetSessionMetadata added in v0.3.0

func (s *ArchiveStore) SetSessionMetadata(sessionID string, meta *SessionMetadata, title string, tags []string) error

SetSessionMetadata updates the full rich metadata for a session, including title, tags, summary, and structured metadata JSON.

func (*ArchiveStore) SetSessionSummary added in v0.3.0

func (s *ArchiveStore) SetSessionSummary(sessionID string, summary string) error

SetSessionSummary updates only the summary text for a session. For richer metadata, use SetSessionMetadata.

func (*ArchiveStore) StartSession added in v0.3.0

func (s *ArchiveStore) StartSession(conversationID string) (*Session, error)

StartSession creates a new session record with the current time.

func (*ArchiveStore) StartSessionAt added in v0.3.0

func (s *ArchiveStore) StartSessionAt(conversationID string, startedAt time.Time) (*Session, error)

StartSessionAt creates a new session record with a specific start time. Use for imports where the original timestamp must be preserved.

func (*ArchiveStore) StartSessionWithOptions added in v0.8.0

func (s *ArchiveStore) StartSessionWithOptions(conversationID string, opts ...SessionOption) (*Session, error)

StartSessionWithOptions creates a new session record with optional parent linkage. Use WithParentSession and WithParentToolCall to set parent fields for delegate sessions.

func (*ArchiveStore) Stats added in v0.3.0

func (s *ArchiveStore) Stats() (map[string]any, error)

Stats returns archive statistics.

func (*ArchiveStore) UnsummarizedSessions added in v0.7.0

func (s *ArchiveStore) UnsummarizedSessions(limit int) ([]*Session, error)

UnsummarizedSessions returns ended sessions that have no metadata yet, ordered oldest-first for catch-up processing. Only sessions with at least one message are returned — the message count is checked via a post-query filter because messages may live in a different database than sessions in unified mode.

type ArchivedIteration added in v0.8.0

type ArchivedIteration struct {
	SessionID      string    `json:"session_id"`
	IterationIndex int       `json:"iteration_index"`
	Model          string    `json:"model"`
	InputTokens    int       `json:"input_tokens"`
	OutputTokens   int       `json:"output_tokens"`
	ToolCallCount  int       `json:"tool_call_count"`
	ToolCallIDs    []string  `json:"tool_call_ids,omitempty"`
	ToolsOffered   []string  `json:"tools_offered,omitempty"`
	StartedAt      time.Time `json:"started_at"`
	DurationMs     int64     `json:"duration_ms"`
	HasToolCalls   bool      `json:"has_tool_calls"`
	BreakReason    string    `json:"break_reason,omitempty"`
}

ArchivedIteration represents one pass through an agent or delegate loop preserved in the archive. Each iteration corresponds to one LLM call plus any tool calls that follow.

type ArchivedToolCall added in v0.3.0

type ArchivedToolCall struct {
	ID             string     `json:"id"`
	ConversationID string     `json:"conversation_id"`
	SessionID      string     `json:"session_id"`
	ToolName       string     `json:"tool_name"`
	Arguments      string     `json:"arguments"`
	Result         string     `json:"result,omitempty"`
	Error          string     `json:"error,omitempty"`
	StartedAt      time.Time  `json:"started_at"`
	CompletedAt    *time.Time `json:"completed_at,omitempty"`
	DurationMs     int64      `json:"duration_ms,omitempty"`
	ArchivedAt     time.Time  `json:"archived_at"`
	IterationIndex *int       `json:"iteration_index,omitempty"`
}

ArchivedToolCall represents a tool call preserved in the archive.

type CompactableStore

type CompactableStore interface {
	GetTokenCount(conversationID string) int
	GetMessagesForCompaction(conversationID string, keep int) []Message
	MarkCompacted(conversationID string, before time.Time) error
	AddCompactionSummary(conversationID, summary string) error
}

CompactableStore is the interface for stores that support compaction.

type CompactionConfig

type CompactionConfig struct {
	MaxTokens            int     // Context window size
	TriggerRatio         float64 // Trigger compaction at this ratio (e.g., 0.7 = 70%)
	KeepRecent           int     // Number of recent messages to always keep
	MinMessagesToCompact int     // Minimum messages before considering compaction
}

CompactionConfig controls compaction behavior.

func DefaultCompactionConfig

func DefaultCompactionConfig() CompactionConfig

DefaultCompactionConfig returns sensible defaults.

type Compactor

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

Compactor handles conversation compaction.

func NewCompactor

func NewCompactor(store CompactableStore, config CompactionConfig, summarizer Summarizer, logger *slog.Logger) *Compactor

NewCompactor creates a new compactor.

func (*Compactor) Compact

func (c *Compactor) Compact(ctx context.Context, conversationID string) error

Compact performs compaction on a conversation.

func (*Compactor) CompactionStats

func (c *Compactor) CompactionStats(conversationID string) map[string]any

CompactionStats returns stats about compaction for a conversation.

func (*Compactor) CompactionThreshold added in v0.8.2

func (c *Compactor) CompactionThreshold() int

CompactionThreshold returns the token count at which compaction triggers.

func (*Compactor) NeedsCompaction

func (c *Compactor) NeedsCompaction(conversationID string) bool

NeedsCompaction checks if a conversation needs compaction.

func (*Compactor) SetWorkingMemoryStore added in v0.5.0

func (c *Compactor) SetWorkingMemoryStore(wm WorkingMemoryReader)

SetWorkingMemoryStore configures a working memory store so that the compactor can include experiential context in the compaction prompt.

type Conversation

type Conversation struct {
	ID        string    `json:"id"`
	Messages  []Message `json:"messages"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Conversation holds the state of a single conversation.

type DelegationMetadata added in v0.8.0

type DelegationMetadata struct {
	Task          string `json:"task"`
	Guidance      string `json:"guidance,omitempty"`
	Profile       string `json:"profile"`
	Model         string `json:"model"`
	Iterations    int    `json:"iterations"`
	MaxIterations int    `json:"max_iterations"`
	InputTokens   int    `json:"input_tokens"`
	OutputTokens  int    `json:"output_tokens"`
	Exhausted     bool   `json:"exhausted"`
	ExhaustReason string `json:"exhaust_reason,omitempty"`
	ResultContent string `json:"result_content,omitempty"`
	DurationMs    int64  `json:"duration_ms"`
	Error         string `json:"error,omitempty"`

	// Messages is the raw JSON-serialized conversation history from the
	// legacy delegations table. Preserved to avoid data loss — these
	// messages predate the per-message storage in the messages table.
	Messages string `json:"messages,omitempty"`
}

DelegationMetadata holds delegation-specific fields preserved from the legacy delegations table during migration (#446).

type EpisodicConfig added in v0.8.0

type EpisodicConfig struct {
	// Timezone is the IANA timezone string (e.g. "America/Chicago").
	Timezone string

	// DailyDir is the directory containing YYYY-MM-DD.md daily memory
	// files. Supports ~ expansion. Empty disables daily file injection.
	DailyDir string

	// LookbackDays is how many days of daily memory files to include.
	LookbackDays int

	// HistoryTokens is the approximate token budget for recent
	// conversation history.
	HistoryTokens int

	// SessionGapMinutes is the silence duration between sessions that
	// triggers a gap annotation in the output.
	SessionGapMinutes int
}

Config holds configuration for the episodic memory provider.

type EpisodicProvider added in v0.8.0

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

Provider implements the agent.ContextProvider interface for episodic

It injects daily memory notes and recent conversation history

into the system prompt.

func NewEpisodicProvider added in v0.8.0

func NewEpisodicProvider(archive ArchiveReader, logger *slog.Logger, cfg EpisodicConfig) *EpisodicProvider

NewProvider creates an episodic memory context provider.

func (*EpisodicProvider) GetContext added in v0.8.0

func (p *EpisodicProvider) GetContext(_ context.Context, _ string) (string, error)

GetContext returns episodic memory context for injection into the system prompt. It assembles daily memory notes and recent conversation history from the session archive.

type ExtractFunc added in v0.5.0

type ExtractFunc func(ctx context.Context, userMessage, assistantResponse string, recentHistory []Message) (*ExtractionResult, error)

ExtractFunc calls an LLM to extract facts from a single interaction. It receives the current user message, assistant response, and recent conversation history for context.

type ExtractedFact added in v0.5.0

type ExtractedFact struct {
	Category   string  `json:"category"`
	Key        string  `json:"key"`
	Value      string  `json:"value"`
	Confidence float64 `json:"confidence"`
}

ExtractedFact is a single fact parsed from the LLM extraction response. Category must be one of the valid fact categories (user, home, device, routine, preference, architecture). Confidence is 0–1.

type ExtractionResult added in v0.5.0

type ExtractionResult struct {
	Facts           []ExtractedFact `json:"facts"`
	WorthPersisting bool            `json:"worth_persisting"`
}

ExtractionResult is the structured JSON response from an LLM fact extraction call. WorthPersisting acts as a top-level gate: when false, the Facts slice is ignored even if populated.

type Extractor added in v0.5.0

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

Extractor runs automatic fact extraction after each interaction. It is fully async and best-effort — failures are logged but never propagate to the caller or affect the user-facing response.

func NewExtractor added in v0.5.0

func NewExtractor(facts FactSetter, logger *slog.Logger, minMessages int) *Extractor

NewExtractor creates an Extractor that persists facts via the given FactSetter. The minMessages threshold controls the minimum conversation length before extraction is attempted.

func (*Extractor) Extract added in v0.5.0

func (e *Extractor) Extract(ctx context.Context, userMsg, assistantResp string, recentHistory []Message) error

Extract calls the configured ExtractFunc and persists any discovered facts via the FactSetter. Incomplete facts (missing category, key, or value) are silently skipped. Errors from individual SetFact calls are logged but do not stop processing of remaining knowledge.

func (*Extractor) SetExtractFunc added in v0.5.0

func (e *Extractor) SetExtractFunc(fn ExtractFunc)

SetExtractFunc configures the LLM extraction function.

func (*Extractor) SetTimeout added in v0.5.0

func (e *Extractor) SetTimeout(d time.Duration)

SetTimeout configures the LLM call timeout for extraction.

func (*Extractor) ShouldExtract added in v0.5.0

func (e *Extractor) ShouldExtract(userMsg, assistantResp string, messageCount int, skipContext bool) bool

ShouldExtract reports whether the given interaction is worth analyzing for knowledge. It filters out simple device commands, short responses, and auxiliary requests to keep LLM extraction calls to roughly 30–50% of interactions.

func (*Extractor) Timeout added in v0.5.0

func (e *Extractor) Timeout() time.Duration

Timeout returns the configured extraction timeout.

type FactSetter added in v0.5.0

type FactSetter interface {
	SetFact(category, key, value, source string, confidence float64) error
}

FactSetter persists extracted facts to long-term storage. Implementations may apply additional logic such as confidence reinforcement on upsert.

type IdleSessionInfo added in v0.8.0

type IdleSessionInfo struct {
	SessionID      string
	ConversationID string
	LastActivity   time.Time
}

IdleSessionInfo holds an active session's identity and last activity time for idle timeout evaluation by the summarizer worker.

type InteractionCallback added in v0.8.0

type InteractionCallback func(conversationID string, sessionID string, endedAt time.Time, topics []string)

InteractionCallback is called after successful session summarization to update the contact's last interaction metadata. Parameters: conversationID, sessionID, endedAt, topics (tags).

type LLMSummarizer

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

LLMSummarizer uses an LLM to generate summaries.

func NewLLMSummarizer

func NewLLMSummarizer(llmFunc func(ctx context.Context, prompt string) (string, error)) *LLMSummarizer

NewLLMSummarizer creates a summarizer that uses an LLM.

func (*LLMSummarizer) Summarize

func (s *LLMSummarizer) Summarize(ctx context.Context, messages []Message, workingMemory string) (string, error)

Summarize generates a summary of the messages using an LLM. When workingMemory is non-empty, it is included in the prompt so the summarizer preserves experiential context through compaction.

type MemoryStore

type MemoryStore interface {
	GetMessages(conversationID string) []Message
	AddMessage(conversationID, role, content string) error
	GetConversation(id string) *Conversation
	Clear(conversationID string) error
	Stats() map[string]any
}

Store is the interface for memory storage.

type Message

type Message struct {
	ID             string    `json:"id"`                        // Stable UUIDv7 assigned at creation time
	ConversationID string    `json:"conversation_id,omitempty"` // Set for archived messages
	SessionID      string    `json:"session_id,omitempty"`      // Set for archived messages
	Role           string    `json:"role"`                      // system, user, assistant, tool
	Content        string    `json:"content"`
	Timestamp      time.Time `json:"timestamp"`
	TokenCount     int       `json:"token_count,omitempty"`    // Estimated token count
	ToolCalls      string    `json:"tool_calls,omitempty"`     // JSON array of tool calls (assistant messages)
	ToolCallID     string    `json:"tool_call_id,omitempty"`   // Tool call ID (tool response messages)
	ArchivedAt     time.Time `json:"archived_at,omitzero"`     // When the message was archived
	ArchiveReason  string    `json:"archive_reason,omitempty"` // Why: compaction, reset, shutdown, import
}

Message represents a conversation message. This is the unified type for both active working-memory messages and archived session transcripts. Archive-specific fields (ConversationID, SessionID, TokenCount, ArchivedAt, ArchiveReason) are zero-valued for active messages.

type MessageArchiver added in v0.8.0

type MessageArchiver interface {
	ArchiveMessages(conversationID, sessionID, reason string) (int64, error)
}

MessageArchiver sets lifecycle status on messages in the unified table.

type SQLiteStore

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

SQLiteStore is a SQLite-backed memory store.

func NewSQLiteStore

func NewSQLiteStore(dbPath string, maxMessages int) (*SQLiteStore, error)

NewSQLiteStore creates a new SQLite-backed store.

func (*SQLiteStore) AddCompactionSummary

func (s *SQLiteStore) AddCompactionSummary(conversationID, summary string) error

AddCompactionSummary adds a compaction summary message.

func (*SQLiteStore) AddMessage

func (s *SQLiteStore) AddMessage(conversationID, role, content string) error

AddMessage adds a message to a conversation.

func (*SQLiteStore) ArchiveMessages added in v0.8.0

func (s *SQLiteStore) ArchiveMessages(conversationID, sessionID, reason string) (int64, error)

ArchiveMessages updates messages in the unified table to archived status. This replaces the cross-DB copy that the legacy archive flow used.

func (*SQLiteStore) ArchiveToolCalls added in v0.8.0

func (s *SQLiteStore) ArchiveToolCalls(conversationID, sessionID string) (int64, error)

ArchiveToolCalls updates tool calls in the unified table to archived status. This replaces the cross-DB copy that the legacy archive flow used.

func (*SQLiteStore) Clear

func (s *SQLiteStore) Clear(conversationID string) error

Clear removes a conversation and its messages.

func (*SQLiteStore) ClearToolCalls added in v0.7.0

func (s *SQLiteStore) ClearToolCalls(conversationID string) error

ClearToolCalls deletes tool call records for a conversation from the working store. Called after archiving to prevent re-archival on the next session split.

func (*SQLiteStore) Close

func (s *SQLiteStore) Close() error

Close closes the database connection.

func (*SQLiteStore) CompleteToolCall

func (s *SQLiteStore) CompleteToolCall(toolCallID, result, errMsg string) error

CompleteToolCall records the result of a tool call.

func (*SQLiteStore) DB added in v0.8.0

func (s *SQLiteStore) DB() *sql.DB

DB returns the underlying database connection for use by the unification migration and by ArchiveStore when reading from the unified messages table.

func (*SQLiteStore) GetAllConversations

func (s *SQLiteStore) GetAllConversations() []*Conversation

GetAllConversations returns all conversations for checkpointing.

func (*SQLiteStore) GetAllMessages added in v0.3.0

func (s *SQLiteStore) GetAllMessages(conversationID string) []Message

GetAllMessages retrieves ALL messages for a conversation, including compacted ones. Includes tool call data for full-fidelity archiving — never lose primary sources.

func (*SQLiteStore) GetConversation

func (s *SQLiteStore) GetConversation(id string) *Conversation

GetConversation retrieves a conversation by ID.

func (*SQLiteStore) GetMessages

func (s *SQLiteStore) GetMessages(conversationID string) []Message

GetMessages retrieves messages for a conversation.

func (*SQLiteStore) GetMessagesForCompaction

func (s *SQLiteStore) GetMessagesForCompaction(conversationID string, keep int) []Message

GetMessagesForCompaction retrieves messages that should be compacted. Keeps the most recent 'keep' messages.

func (*SQLiteStore) GetOrCreateConversation

func (s *SQLiteStore) GetOrCreateConversation(id string) (*Conversation, error)

GetOrCreateConversation ensures a conversation exists and returns it.

func (*SQLiteStore) GetTokenCount

func (s *SQLiteStore) GetTokenCount(conversationID string) int

GetTokenCount returns the total token count for a conversation.

func (*SQLiteStore) GetToolCalls

func (s *SQLiteStore) GetToolCalls(conversationID string, limit int) []ToolCall

GetToolCalls retrieves tool calls, optionally filtered by conversation. If conversationID is empty, returns all recent tool calls.

func (*SQLiteStore) GetToolCallsByName

func (s *SQLiteStore) GetToolCallsByName(toolName string, limit int) []ToolCall

GetToolCallsByName retrieves tool calls filtered by tool name.

func (*SQLiteStore) MarkCompacted

func (s *SQLiteStore) MarkCompacted(conversationID string, before time.Time) error

MarkCompacted marks messages as compacted.

func (*SQLiteStore) NeedsCompaction

func (s *SQLiteStore) NeedsCompaction(conversationID string, maxTokens int) bool

NeedsCompaction checks if a conversation needs compaction.

func (*SQLiteStore) RecordToolCall

func (s *SQLiteStore) RecordToolCall(conversationID, messageID, toolCallID, toolName, arguments string) error

RecordToolCall records a tool call execution. messageID can be empty - it will be stored as NULL.

func (*SQLiteStore) Stats

func (s *SQLiteStore) Stats() map[string]any

Stats returns memory statistics.

func (*SQLiteStore) ToolCallStats

func (s *SQLiteStore) ToolCallStats() map[string]any

ToolCallStats returns statistics about tool usage.

type SearchOptions added in v0.3.0

type SearchOptions struct {
	Query            string
	ConversationID   string        // optional filter
	SilenceThreshold time.Duration // gap that stops context expansion
	MaxMessages      int           // hard cap per direction
	MaxDuration      time.Duration // time-based cap per direction
	Limit            int           // max results
	NoContext        bool          // if true, return matches only (no surrounding context)
}

SearchOptions configures a search query.

type SearchResult added in v0.3.0

type SearchResult struct {
	Match         Message   `json:"match"`
	SessionID     string    `json:"session_id"`
	ContextBefore []Message `json:"context_before"`
	ContextAfter  []Message `json:"context_after"`
	Highlight     string    `json:"highlight,omitempty"`
}

SearchResult represents a search hit with surrounding context.

type Session added in v0.3.0

type Session struct {
	ID               string           `json:"id"`
	ConversationID   string           `json:"conversation_id"`
	StartedAt        time.Time        `json:"started_at"`
	EndedAt          *time.Time       `json:"ended_at,omitempty"`
	EndReason        string           `json:"end_reason,omitempty"`
	MessageCount     int              `json:"message_count"`
	Summary          string           `json:"summary,omitempty"`
	Title            string           `json:"title,omitempty"`
	Tags             []string         `json:"tags,omitempty"`
	Metadata         *SessionMetadata `json:"metadata,omitempty"`
	ParentSessionID  string           `json:"parent_session_id,omitempty"`
	ParentToolCallID string           `json:"parent_tool_call_id,omitempty"`
}

Session represents a conversation session with boundaries.

type SessionMetadata added in v0.3.0

type SessionMetadata struct {
	// Summaries at different lengths for different display contexts.
	OneLiner  string `json:"one_liner,omitempty"` // ~10 words
	Paragraph string `json:"paragraph,omitempty"` // 2-4 sentences
	Detailed  string `json:"detailed,omitempty"`  // full summary

	// Key decisions or outcomes from the session.
	KeyDecisions []string `json:"key_decisions,omitempty"`

	// People involved or mentioned.
	Participants []string `json:"participants,omitempty"`

	// Characterization of the session's nature.
	SessionType string `json:"session_type,omitempty"` // e.g. "debugging", "architecture", "philosophy", "casual"

	// Tools used during the session (tool name → call count).
	ToolsUsed map[string]int `json:"tools_used,omitempty"`

	// Files touched or discussed during the session.
	FilesTouched []string `json:"files_touched,omitempty"`

	// Model(s) used, if known.
	Models []string `json:"models,omitempty"`

	// Legacy delegation execution details, preserved from the delegations
	// table migration (#446). Only populated for imported delegation records.
	Delegation *DelegationMetadata `json:"delegation,omitempty"`
}

SessionMetadata holds rich, LLM-generated metadata for human-oriented search and browsing. Stored as JSON in the database for flexibility — new fields can be added without schema migrations.

type SessionOption added in v0.8.0

type SessionOption func(*Session)

SessionOption configures optional fields when starting a session.

func WithParentSession added in v0.8.0

func WithParentSession(id string) SessionOption

WithParentSession sets the parent session ID for a child session (e.g. a delegate spawned from a parent session).

func WithParentToolCall added in v0.8.0

func WithParentToolCall(id string) SessionOption

WithParentToolCall sets the tool call ID that triggered this child session (e.g. the thane_delegate tool call in the parent).

type SimpleSummarizer

type SimpleSummarizer struct{}

SimpleSummarizer creates a basic summary without LLM (fallback).

func (*SimpleSummarizer) Summarize

func (s *SimpleSummarizer) Summarize(ctx context.Context, messages []Message, _ string) (string, error)

Summarize creates a simple extractive summary.

type Store

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

Store manages conversation memory. Currently in-memory; will add SQLite persistence later.

func NewStore

func NewStore(maxMessages int) *Store

NewStore creates a new memory store.

func (*Store) AddMessage

func (s *Store) AddMessage(conversationID string, role, content string) error

AddMessage adds a message to a conversation.

func (*Store) Clear

func (s *Store) Clear(conversationID string) error

Clear removes a conversation.

func (*Store) GetAllConversations

func (s *Store) GetAllConversations() []*Conversation

GetAllConversations returns all conversations for checkpointing.

func (*Store) GetConversation

func (s *Store) GetConversation(id string) *Conversation

GetConversation retrieves a conversation by ID. Returns nil if not found.

func (*Store) GetMessages

func (s *Store) GetMessages(conversationID string) []Message

GetMessages retrieves messages for a conversation. Returns empty slice if conversation doesn't exist.

func (*Store) GetOrCreateConversation

func (s *Store) GetOrCreateConversation(id string) *Conversation

GetOrCreateConversation retrieves or creates a conversation.

func (*Store) GetTokenCount

func (s *Store) GetTokenCount(conversationID string) int

GetTokenCount returns estimated token count for a conversation.

func (*Store) RestoreConversations

func (s *Store) RestoreConversations(convs []*Conversation)

RestoreConversations replaces all conversations from a checkpoint.

func (*Store) Stats

func (s *Store) Stats() map[string]any

Stats returns memory statistics.

type Summarizer

type Summarizer interface {
	Summarize(ctx context.Context, messages []Message, workingMemory string) (string, error)
}

Summarizer generates summaries from messages. When workingMemory is non-empty, it is included in the prompt so the summarizer preserves experiential context through compaction.

type SummarizerConfig added in v0.8.0

type SummarizerConfig struct {
	// Interval between periodic scans for unsummarized sessions.
	// Default: 5 minutes.
	Interval time.Duration

	// Timeout per individual session summarization LLM call.
	// Default: 60 seconds.
	Timeout time.Duration

	// PauseBetween is the delay between processing consecutive sessions
	// to avoid overwhelming the LLM or starving interactive requests.
	// Default: 5 seconds.
	PauseBetween time.Duration

	// BatchSize is the max number of unsummarized sessions to fetch per scan.
	// Default: 10.
	BatchSize int

	// ModelPreference is a soft hint for which model to use.
	// Passed as HintModelPreference to the router. If empty, the router
	// picks freely based on other hints.
	ModelPreference string

	// IdleTimeout is the duration of inactivity after which an open
	// session is silently closed by the summarizer as a backstop.
	// Zero disables idle session closing. This complements the
	// interactive idle check in the channel bridge, which sends
	// farewell messages. The summarizer-based check recovers from
	// crashes where in-memory state was lost.
	IdleTimeout time.Duration
}

Config controls the summarizer worker behavior.

func DefaultSummarizerConfig added in v0.8.0

func DefaultSummarizerConfig() SummarizerConfig

DefaultConfig returns sensible defaults for the summarizer worker.

type SummarizerWorker added in v0.8.0

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

Worker periodically scans for unsummarized sessions and generates metadata using an LLM via the model router.

func NewSummarizerWorker added in v0.8.0

func NewSummarizerWorker(store *ArchiveStore, llmClient llm.Client, rtr *router.Router, logger *slog.Logger, cfg SummarizerConfig) *SummarizerWorker

New creates a summarizer worker. Call Start to begin processing.

func (*SummarizerWorker) SetInteractionCallback added in v0.8.0

func (w *SummarizerWorker) SetInteractionCallback(cb InteractionCallback)

SetInteractionCallback registers a callback invoked after each successful session summarization. The callback receives the conversation ID, session ID, session end time, and LLM-generated topic tags, allowing callers to update contact interaction history without coupling the memory package to the contacts package.

func (*SummarizerWorker) Start added in v0.8.0

func (w *SummarizerWorker) Start(ctx context.Context)

Start begins the background summarization worker. It performs an immediate scan on startup (to catch up on missed sessions), then scans periodically at the configured interval.

func (*SummarizerWorker) Stop added in v0.8.0

func (w *SummarizerWorker) Stop()

Stop cancels the worker and waits for its goroutine to exit.

type ToolCall

type ToolCall struct {
	ID             string     `json:"id"`
	MessageID      string     `json:"message_id"`
	ConversationID string     `json:"conversation_id"`
	ToolName       string     `json:"tool_name"`
	Arguments      string     `json:"arguments"`
	Result         string     `json:"result,omitempty"`
	Error          string     `json:"error,omitempty"`
	StartedAt      time.Time  `json:"started_at"`
	CompletedAt    *time.Time `json:"completed_at,omitempty"`
	DurationMs     int64      `json:"duration_ms,omitempty"`
}

ToolCall represents a recorded tool invocation.

type ToolCallArchiver added in v0.8.0

type ToolCallArchiver interface {
	ArchiveToolCalls(conversationID, sessionID string) (int64, error)
}

ToolCallArchiver sets lifecycle status on tool calls in the unified table.

type WorkingMemoryProvider added in v0.5.0

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

WorkingMemoryProvider implements the agent.ContextProvider interface for auto-injecting working memory into the system prompt. When the current conversation has working memory content, it is included under a "### Working Memory" heading so the agent has experiential continuity without needing to explicitly read it.

func NewWorkingMemoryProvider added in v0.5.0

func NewWorkingMemoryProvider(store *WorkingMemoryStore, convFunc func(context.Context) string) *WorkingMemoryProvider

NewWorkingMemoryProvider creates a context provider that auto-injects working memory for the current conversation. The convFunc parameter extracts the conversation ID from the request context — typically [tools.ConversationIDFromContext].

func (*WorkingMemoryProvider) GetContext added in v0.5.0

func (p *WorkingMemoryProvider) GetContext(ctx context.Context, _ string) (string, error)

GetContext returns the working memory content for the current conversation, formatted for system prompt injection. Returns empty string if no working memory exists.

type WorkingMemoryReader added in v0.5.0

type WorkingMemoryReader interface {
	Get(conversationID string) (string, time.Time, error)
}

WorkingMemoryReader is the subset of WorkingMemoryStore needed by the compactor. Defined as an interface for testability and to avoid coupling the compactor to the concrete store type.

type WorkingMemoryStore added in v0.5.0

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

WorkingMemoryStore persists free-form working memory per conversation. Working memory captures experiential context that mechanical summarisation destroys: emotional tone, conversational arc, relationship temperature, and unresolved threads. The table lives in archive.db alongside session transcripts.

func NewWorkingMemoryStore added in v0.5.0

func NewWorkingMemoryStore(db *sql.DB) (*WorkingMemoryStore, error)

NewWorkingMemoryStore creates a working memory store using the given database connection (typically from ArchiveStore.DB). It creates the working_memory table if it does not already exist.

func (*WorkingMemoryStore) Delete added in v0.5.0

func (s *WorkingMemoryStore) Delete(conversationID string) error

Delete removes the working memory for a conversation.

func (*WorkingMemoryStore) Get added in v0.5.0

func (s *WorkingMemoryStore) Get(conversationID string) (string, time.Time, error)

Get returns the working memory content and last-updated timestamp for a conversation. If no working memory exists, it returns an empty string and zero time with no error.

func (*WorkingMemoryStore) Set added in v0.5.0

func (s *WorkingMemoryStore) Set(conversationID, content string) error

Set writes or replaces the working memory content for a conversation.

Jump to

Keyboard shortcuts

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