memory

package
v0.9.2 Latest Latest
Warning

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

Go to latest
Published: May 1, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

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.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FitPrefix

func FitPrefix(n, byteCap int, render func(k int) []byte) []byte

FitPrefix returns the largest count k in [0, n] such that render(k) is within byteCap. render must produce monotonically non-decreasing output as k grows. Used by prefix-fit clipping (e.g., search results, where the tail entries are lower-relevance and are the right ones to drop). Output is always rendered with truncated=true when k < n.

func FitSuffix

func FitSuffix(n, byteCap int, render func(drop int) []byte) []byte

FitSuffix returns the smallest count k in [0, n] such that render(k) is within byteCap. render must produce monotonically non-increasing output as k grows (k is the number of items dropped from the front). Used by suffix-fit clipping where older entries are dropped first to preserve the most-recent tail.

func FormatRecentMessages

func FormatRecentMessages(messages []Message, now time.Time, truncated bool) []byte

FormatRecentMessages renders messages as JSON for tool output or system-prompt context blocks. Each entry includes a delta timestamp and the originating session ID, so the model can chain into archive_session_transcript when it wants more context around a turn.

func FormatSearchResults

func FormatSearchResults(results []SearchResult, now time.Time, truncated bool) []byte

FormatSearchResults renders archive search hits as JSON. Each result carries the matched message plus the surrounding context window in chronological order. SessionID is emitted on every message; context messages may belong to a different session than the match because context expansion is bounded by silence gaps, not session edges. Context lists are trimmed to the [maxSearchContextPerSide] messages closest to the match on each side — for context_before that's the last N, for context_after the first N.

func FormatSessionsList

func FormatSessionsList(sessions []*Session, now time.Time, truncated bool) []byte

FormatSessionsList renders sessions as JSON suitable for tool output or system-prompt context blocks. Always emits a non-nil "sessions" array and a "truncated" boolean so the model can detect cap-driven truncation without parsing prose.

func ShortID

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

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

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) ActiveConversationIDs

func (a *ArchiveAdapter) ActiveConversationIDs() []string

ActiveConversationIDs returns the conversation IDs with currently open sessions. It prefers the in-memory cache for hot-path accuracy and merges in any active sessions found in the store so startup recovery and direct store writes are also reflected.

func (*ArchiveAdapter) ActiveSessionID

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

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

func (*ArchiveAdapter) ActiveSessionStartedAt

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

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

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

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

func (*ArchiveAdapter) EndSession

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

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

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

func (*ArchiveAdapter) LinkPendingIterationToolCalls

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

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

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

StartSession begins a new session and returns its ID.

func (*ArchiveAdapter) Store

func (a *ArchiveAdapter) Store() *ArchiveStore

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

type ArchiveConfig

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

func DefaultArchiveConfig() ArchiveConfig

DefaultArchiveConfig returns sensible defaults.

type ArchiveContextProvider

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

ArchiveContextProvider implements [agent.TagContextProvider] 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). Registered via [agent.Loop.RegisterAlwaysContextProvider].

func NewArchiveContextProvider

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) TagContext

TagContext 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 req.UserMessage is short, it falls back to searching by message content. Implements [agent.TagContextProvider]; registered via RegisterAlwaysContextProvider.

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

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)
}

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

type ArchiveReason

type ArchiveReason string

ArchiveReason describes why messages were archived.

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

type ArchiveSearcher

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

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

type ArchiveStore

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

ArchiveStore handles immutable session transcript archiving.

func NewArchiveStore

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

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

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

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

func (*ArchiveStore) ActiveSessionCount

func (s *ArchiveStore) ActiveSessionCount() (int, error)

ActiveSessionCount returns the number of unclosed (active) sessions. This is a lightweight query for telemetry dashboards — use ArchiveStore.ActiveSessionsWithLastActivity when per-session details are needed.

func (*ArchiveStore) ActiveSessionsWithLastActivity

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

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

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

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

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

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

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

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

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

EndSession marks a session as ended at the current time.

func (*ArchiveStore) EndSessionAt

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

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

func (s *ArchiveStore) FTSEnabled() bool

FTSEnabled returns whether FTS5 full-text search is available.

func (*ArchiveStore) GetMessagesByTimeRange

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

GetMessagesByTimeRange returns archived messages within a time range.

func (*ArchiveStore) GetMessagesInRange

func (s *ArchiveStore) GetMessagesInRange(opts RangeOptions) ([]Message, bool, error)

GetMessagesInRange returns archived messages bounded by time, with an optional MinMessages floor that guarantees a useful tail even on quiet conversations. Results are ordered chronologically (oldest first). The boolean return is true when MaxMessages clipped the result.

func (*ArchiveStore) GetSession

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

GetSession retrieves a session by ID.

func (*ArchiveStore) GetSessionIterations

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

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

func (*ArchiveStore) GetSessionToolCalls

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

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

func (*ArchiveStore) GetSessionTranscript

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

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

func (*ArchiveStore) ImportMessages

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

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

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

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

func (*ArchiveStore) LinkPendingIterationToolCalls

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

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

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

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

ListSessions returns sessions, newest first.

func (*ArchiveStore) PurgeImported

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

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

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

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

func (*ArchiveStore) SetSessionMetadata

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

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

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

StartSession creates a new session record with the current time.

func (*ArchiveStore) StartSessionAt

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

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

StartSessionWithOptions creates a new session record with optional parent linkage and metadata snapshots. Use WithParentSession, WithParentToolCall, and WithChannelBinding to stamp archive-only session context at creation time.

func (*ArchiveStore) Stats

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

Stats returns archive statistics.

func (*ArchiveStore) UnsummarizedSessions

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

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

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 ChannelBinding

type ChannelBinding struct {
	Channel     string `json:"channel,omitempty"`
	Address     string `json:"address,omitempty"`
	ContactID   string `json:"contact_id,omitempty"`
	ContactName string `json:"contact_name,omitempty"`
	TrustZone   string `json:"trust_zone,omitempty"`
	LinkSource  string `json:"link_source,omitempty"`
	IsOwner     bool   `json:"is_owner,omitempty"`
}

ChannelBinding captures the runtime identity of a channel-backed conversation. It links a live channel/address pair to any known contact record so downstream code can gate on a typed binding instead of reconstructing identity from hints.

func (*ChannelBinding) Clone

func (b *ChannelBinding) Clone() *ChannelBinding

Clone returns a deep copy of the binding.

func (*ChannelBinding) Normalize

func (b *ChannelBinding) Normalize() *ChannelBinding

Normalize returns a trimmed copy of the binding, or nil when the binding carries no meaningful channel identity.

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

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

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"`
	Metadata  *ConversationMetadata `json:"metadata,omitempty"`
	CreatedAt time.Time             `json:"created_at"`
	UpdatedAt time.Time             `json:"updated_at"`
}

Conversation holds the state of a single conversation.

type ConversationMetadata

type ConversationMetadata struct {
	ChannelBinding *ChannelBinding `json:"channel_binding,omitempty"`
}

ConversationMetadata holds typed metadata associated with a live conversation. It is stored as JSON so new fields can be added without schema churn.

func (*ConversationMetadata) Clone

Clone returns a deep copy of the metadata.

func (*ConversationMetadata) Normalize

Normalize returns a cleaned copy of the metadata, or nil when it contains no meaningful fields.

type DelegationMetadata

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

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. Converted to a byte cap (×4) when fitting
	// the JSON catalog block.
	HistoryTokens int
}

EpisodicConfig holds configuration for the episodic memory provider.

type EpisodicProvider

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

EpisodicProvider implements [agent.TagContextProvider] for episodic memory. It injects two unrelated context blocks into the system prompt:

  • "Daily Notes" — markdown content from per-day notes files (a human-authored journal) for the configured lookback window.

  • "Recent Sessions" — a JSON catalog of the most recent closed sessions across all conversations, keyed for archive_search and archive_session_transcript follow-ups. Rendered via FormatSessionsList for schema parity with the archive_* tools and the message_channel context provider.

Per-channel verbatim history (the model's "what did we just say?" view for message-channel conversations) lives in a separate provider gated on the message_channel capability tag — see MessageChannelProvider. EpisodicProvider intentionally stays channel-agnostic and emits the same JSON shape on every code path.

func NewEpisodicProvider

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

NewEpisodicProvider creates an episodic memory context provider.

func (*EpisodicProvider) TagContext

TagContext returns episodic memory context for injection into the system prompt. It assembles daily memory notes and the recent- sessions JSON catalog from the archive. Implements [agent.TagContextProvider]; registered via RegisterAlwaysContextProvider.

type ExtractFunc

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

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

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

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

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

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

func (e *Extractor) SetExtractFunc(fn ExtractFunc)

SetExtractFunc configures the LLM extraction function.

func (*Extractor) SetTimeout

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

SetTimeout configures the LLM call timeout for extraction.

func (*Extractor) ShouldExtract

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

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

Timeout returns the configured extraction timeout.

type FactSetter

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

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

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
}

MemoryStore is the interface for memory storage backends (in-memory and SQLite). Both Store and SQLiteStore satisfy it.

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

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

MessageArchiver sets lifecycle status on messages in the unified table.

type MessageChannelProvider

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

MessageChannelProvider injects two context blocks into the system prompt for message-channel conversations:

  • "Recent Conversation" — verbatim tail of recent archived messages (last [TailWindow] OR floor of [TailMinMessages] of the most recent, whichever yields more, capped at [TailMaxMessages] and [TailByteCap]). Crosses session boundaries. Excludes the active session's currently in-memory rows so the model does not see them twice (once in its working message list, again here).

  • "Older Sessions" — JSON metadata block for sessions ending before the verbatim window. Acts as enticement to call archive_session_transcript or archive_search for fuller content.

Implements [agent.TagContextProvider] via structural typing; gated on the message_channel capability tag asserted by Signal (and future Matrix/iMessage) inbound bridges.

Output sits in the system prompt's DYNAMIC CONTEXT section per docs/anthropic-caching.md: the delta timestamps tick every turn so the block is intrinsically uncached, but the cached prefix above it stays warm.

func NewMessageChannelProvider

func NewMessageChannelProvider(archive *ArchiveStore, conversationIDFromCtx func(context.Context) string, cfg MessageChannelProviderConfig, logger *slog.Logger) *MessageChannelProvider

NewMessageChannelProvider creates the provider. The conversationIDFromCtx function extracts the active conversation ID from a request context — pass [tools.ConversationIDFromContext]. Zero-valued config fields fall back to defaults documented on MessageChannelProviderConfig.

func (*MessageChannelProvider) TagContext

TagContext returns the verbatim tail + older-sessions blocks for the active conversation. Returns the empty string when there is nothing to emit (no conversation context, no archived content).

type MessageChannelProviderConfig

type MessageChannelProviderConfig struct {
	// TailWindow is the time bound on the verbatim tail. Default: 30m.
	TailWindow time.Duration

	// TailMinMessages is the floor: at least this many of the most
	// recent archived messages on the conversation are returned even
	// if the time window is empty. Default: 50.
	TailMinMessages int

	// TailMaxMessages caps the verbatim tail before byte-cap fitting.
	// Default: 200.
	TailMaxMessages int

	// TailByteCap is the JSON output ceiling for the verbatim tail.
	// Default: 32000.
	TailByteCap int

	// OlderSessionsLimit caps the number of older-session entries
	// listed in the catalog block. Default: 20.
	OlderSessionsLimit int

	// OlderSessionsByteCap is the JSON output ceiling for the older-
	// sessions catalog. Default: 16000.
	OlderSessionsByteCap int
}

MessageChannelProviderConfig configures the verbatim-tail + older- sessions context provider for message-channel conversations (Signal, Matrix, iMessage). Zero values fall back to defaults.

type MessageView

type MessageView struct {
	T                string `json:"t"`
	Role             string `json:"role"`
	Content          string `json:"content"`
	ContentTruncated bool   `json:"content_truncated"`
	SessionID        string `json:"session_id"`
}

MessageView is the JSON-facing projection of an archived message. T is a signed-second delta via promptfmt.FormatDeltaOnly. SessionID is always emitted (empty string when unknown) so the model sees a stable schema across calls. ContentTruncated signals when Content was clipped to [maxMessageContentBytes].

type RangeOptions

type RangeOptions struct {
	// ConversationID restricts the result to a single conversation when
	// non-empty. Empty matches all conversations.
	ConversationID string

	// ExcludeSessionID drops messages from the named session when
	// non-empty. Useful for system-prompt context providers that want
	// archived/older messages but not the active session's currently
	// in-memory rows (which the model already sees in its working
	// message list).
	ExcludeSessionID string

	// From is the earliest timestamp to include (inclusive). Zero means
	// unbounded — combined with MinMessages, this is how the "give me at
	// least N most-recent messages regardless of age" query is expressed.
	From time.Time

	// To is the latest timestamp to include (inclusive). Zero is treated
	// as time.Now() at query time.
	To time.Time

	// MinMessages is a floor: ensure at least this many of the most
	// recent (≤ To) messages are returned, even when fewer fall inside
	// [From, To]. Zero disables the floor. Useful for "last X minutes
	// OR Y messages, whichever is more" without two callsite queries.
	MinMessages int

	// MaxMessages caps the result. Non-positive uses the default of 200.
	// When the cap clips the result, the second return value of
	// GetMessagesInRange is true.
	MaxMessages int
}

RangeOptions configures ArchiveStore.GetMessagesInRange. All fields are optional — the zero value of RangeOptions returns the most recent 200 messages across all conversations, ordered chronologically.

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 NewSQLiteStoreWithLogger

func NewSQLiteStoreWithLogger(dbPath string, maxMessages int, logger *slog.Logger) (*SQLiteStore, error)

NewSQLiteStoreWithLogger creates a new SQLite-backed store and uses logger for non-fatal data-integrity warnings encountered while reading existing rows. Nil falls back to slog.Default.

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

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

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) BindConversationChannel

func (s *SQLiteStore) BindConversationChannel(conversationID string, binding *ChannelBinding) error

BindConversationChannel updates only the channel-binding portion of a conversation's typed metadata.

func (*SQLiteStore) Clear

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

Clear removes a conversation and its messages.

func (*SQLiteStore) ClearToolCalls

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

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

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) PutConversationMetadata

func (s *SQLiteStore) PutConversationMetadata(conversationID string, metadata *ConversationMetadata) error

PutConversationMetadata replaces the typed metadata for a conversation, creating the conversation row if needed.

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

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

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 SearchResultView

type SearchResultView struct {
	Match         MessageView   `json:"match"`
	ContextBefore []MessageView `json:"context_before"`
	ContextAfter  []MessageView `json:"context_after"`
	Highlight     string        `json:"highlight"`
}

SearchResultView is the JSON-facing projection of an archive search hit. Match is the message that matched; ContextBefore/ContextAfter are the surrounding messages in chronological order, bounded by the configured silence threshold.

type Session

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

type SessionMetadata struct {
	// ChannelBinding is the channel/contact binding snapshot active when
	// the session was created. This preserves the security-policy
	// identity view Thane had at the time for later forensics.
	ChannelBinding *ChannelBinding `json:"channel_binding,omitempty"`

	// 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

type SessionOption func(*Session)

SessionOption configures optional fields when starting a session.

func WithChannelBinding

func WithChannelBinding(binding *ChannelBinding) SessionOption

WithChannelBinding snapshots the effective conversation/channel binding onto the archived session at creation time.

func WithParentSession

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

func WithParentToolCall(id string) SessionOption

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

type SessionView

type SessionView struct {
	ID              string   `json:"id"`
	ConversationID  string   `json:"conversation_id"`
	Started         string   `json:"started"`
	Ended           string   `json:"ended"`
	DurationSeconds int      `json:"duration_seconds"`
	Messages        int      `json:"messages"`
	Title           string   `json:"title"`
	Tags            []string `json:"tags"`
	Summary         string   `json:"summary"`
}

SessionView is the JSON-facing projection of an archived session. Field shape is stable across calls — empty strings and zero values are emitted explicitly rather than omitted, so the model can rely on schema invariants when comparing entries across turns.

Started/Ended are signed-second deltas via promptfmt.FormatDeltaOnly (e.g., "-7200s"). Ended is the empty string when the session is still active; DurationSeconds is 0 in the same case.

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 is an in-memory conversation memory backend. For persistent storage see SQLiteStore.

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) BindConversationChannel

func (s *Store) BindConversationChannel(conversationID string, binding *ChannelBinding) error

BindConversationChannel updates only the channel-binding portion of a conversation's typed metadata.

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) PutConversationMetadata

func (s *Store) PutConversationMetadata(conversationID string, metadata *ConversationMetadata) error

PutConversationMetadata replaces the typed metadata for a conversation, creating the conversation record if needed.

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

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. Zero disables
	// idle session closing. The summarizer worker is the sole owner
	// of session idle close — message-channel continuity across the
	// rotation boundary is delivered via the message_channel context
	// provider's verbatim tail, not an LLM-driven carry-forward.
	IdleTimeout time.Duration
}

SummarizerConfig controls the summarizer worker behavior.

func DefaultSummarizerConfig

func DefaultSummarizerConfig() SummarizerConfig

DefaultSummarizerConfig returns sensible defaults for the summarizer worker.

type SummarizerWorker

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

SummarizerWorker periodically scans for unsummarized sessions and generates metadata (title, tags, summaries) using an LLM via the model router.

func NewSummarizerWorker

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

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

func (*SummarizerWorker) SetInteractionCallback

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

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

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

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

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

type WorkingMemoryProvider

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

WorkingMemoryProvider implements [agent.TagContextProvider] 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. Registered via [agent.Loop.RegisterAlwaysContextProvider].

func NewWorkingMemoryProvider

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) TagContext

TagContext returns the working memory content for the current conversation, formatted for system prompt injection. Returns empty string if no working memory exists. Implements [agent.TagContextProvider]; registered via RegisterAlwaysContextProvider.

type WorkingMemoryReader

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

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

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

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

Delete removes the working memory for a conversation.

func (*WorkingMemoryStore) Get

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

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