Documentation
¶
Overview ¶
Package session manages persistent conversation sessions.
Sessions are stored as JSON files in ~/.config/moa/sessions/. Each session contains conversation messages, metadata, and a unique ID. The Store provides CRUD operations with atomic writes (temp + rename) to prevent corruption on crash.
Index ¶
- Constants
- Variables
- func ApplyPreservedMetadata(meta, preserved map[string]any) map[string]any
- func DeepCopyMessage(msg core.AgentMessage) core.AgentMessage
- func DeleteByID(baseDir, id string) error
- func FindSession(baseDir, id string) (*Session, *FileStore, error)
- func FindSessionReadOnly(baseDir, id string) (*Session, *FileStore, error)
- func FormatTranscript(entries []Entry) string
- func MigrateV1ToV2(sess *Session) error
- func PreservedMetadata(meta map[string]any) map[string]any
- func RemoveArtifacts(sessionDir, sessionID string) error
- func RemoveTranscriptSnapshots(sessionDir, sessionID string) error
- func TranscriptSnapshotDir(sessionDir, sessionID string) string
- func ValidateEntries(entries []Entry, leafID string) error
- func ValidateID(id string) error
- func WriteTranscriptSnapshot(dir string, entries []Entry) (string, error)
- type Artifact
- type ArtifactMeta
- type ArtifactStore
- type CWDScan
- type CompactionData
- type ConfigChangeData
- type Entry
- type EntryType
- type FileStore
- func (s *FileStore) Create() *Session
- func (s *FileStore) Delete(id string) error
- func (s *FileStore) Dir() string
- func (s *FileStore) Latest() (*Session, error)
- func (s *FileStore) List() ([]Summary, error)
- func (s *FileStore) Load(id string) (*Session, error)
- func (s *FileStore) LoadReadOnly(id string) (*Session, error)
- func (s *FileStore) Save(sess *Session) error
- type Session
- func (s *Session) CompactAtMeta() int
- func (s *Session) FastMeta() bool
- func (s *Session) Origin() string
- func (s *Session) PathMeta() (scope string, allowedPaths []string)
- func (s *Session) Rename(title string, maxLen int)
- func (s *Session) RuntimeMeta() (model, cwd, permissionMode, thinking string)
- func (s *Session) SetAutoTitle(title string, maxLen int)
- func (s *Session) SetIdempotencyKey(key string)
- func (s *Session) SetOrigin(origin string)
- func (s *Session) SetPathMetadata(scope string, allowedPaths []string)
- func (s *Session) SetRuntimeMetadata(model, cwd, permissionMode, thinking string)
- func (s *Session) SetTitle(text string, maxLen int)
- func (s *Session) TitleIsManual() bool
- type SessionStore
- type SubagentStore
- func (s *SubagentStore) Dir() string
- func (s *SubagentStore) LegacyResult(jobID string) (string, error)
- func (s *SubagentStore) List() ([]SubagentTranscript, error)
- func (s *SubagentStore) ListSummaries() ([]SubagentTranscript, error)
- func (s *SubagentStore) Load(jobID string) (*SubagentTranscript, error)
- func (s *SubagentStore) Remove() error
- func (s *SubagentStore) Save(t SubagentTranscript) error
- type SubagentTranscript
- type Summary
- type Tree
- func (t *Tree) AllMessages() []core.AgentMessage
- func (t *Tree) Append(e Entry) string
- func (t *Tree) Branch(entryID string) error
- func (t *Tree) BuildContext() ([]core.AgentMessage, int)
- func (t *Tree) Children(entryID string) []Entry
- func (t *Tree) Clear()
- func (t *Tree) DisplayMessagesSince(entryID string) ([]core.AgentMessage, bool)
- func (t *Tree) Entries() []Entry
- func (t *Tree) Entry(id string) (Entry, bool)
- func (t *Tree) HasMsgID(msgID string) bool
- func (t *Tree) LeafID() string
- func (t *Tree) Len() int
- func (t *Tree) Path() []Entry
- func (t *Tree) Snapshot() ([]Entry, string)
- func (t *Tree) ValidBranchTarget(entryID string) error
Constants ¶
const ( MetaModel = "model" MetaCWD = "cwd" MetaPermissionMode = "permission_mode" MetaThinking = "thinking" MetaPathScope = "path_scope" MetaAllowedPaths = "allowed_paths" MetaCompactAt = "compact_at" MetaFast = "fast" MetaOrigin = "origin" // Automation bookkeeping written by the Automation API. The callback fields // are set at creation; the idempotency key is only written once the run's // first prompt was accepted (see SetIdempotencyKey). MetaIdempotencyKey = "idempotency_key" MetaCallbackURL = "callback_url" MetaCallbackSecret = "callback_secret" // MetaMCPServers holds the per-run MCP servers an automation caller attached // to the session (a list of {name, url, headers}). They are session-scoped — // never written to any config file — and are replayed on resume so the // session reconnects them. MetaMCPServers = "mcp_servers" // MetaAutomationCreated marks a session the Automation API created itself. // Unlike MetaOrigin — a free-form label any creator may pass — it is written // on exactly one code path, so it is the authority check for the scoped // automation interaction endpoints. MetaAutomationCreated = "automation_created" )
RuntimeMetadata keys used for persisting session configuration.
const ( TitleSourceAuto = "auto" TitleSourceManual = "manual" )
Title source values. Empty (legacy) is treated as auto.
const ArtifactsVersion = 1
ArtifactsVersion is the on-disk schema version of the artifact sidecar.
const OriginUser = "user"
OriginUser is the implicit origin of a session created by a human through the web client. Sessions persisted before origins existed carry no key and are treated as user-originated.
const SessionVersion = 2
SessionVersion is the current session format version. V1 (implicit 0): flat Messages array. V2: entry-based tree with branching support.
Variables ¶
var ErrArtifactsDeleted = errors.New("session: artifacts deleted")
ErrArtifactsDeleted is returned by Upsert once DeleteReferences has run for this store instance: the conversation is gone, so a tool call still inside its critical section must not recreate the sidecar.
var ErrNotFound = errors.New("session: not found")
ErrNotFound is returned by Load when the session ID does not exist.
Functions ¶
func ApplyPreservedMetadata ¶ added in v0.20.0
ApplyPreservedMetadata copies preserved creation-time keys into a freshly built metadata map, without overwriting values the runtime already set.
func DeepCopyMessage ¶
func DeepCopyMessage(msg core.AgentMessage) core.AgentMessage
DeepCopyMessage creates a deep copy of an AgentMessage, including Custom map and Content slices with their nested maps.
func DeleteByID ¶
DeleteByID searches all project stores under baseDir and deletes the session.
func FindSession ¶
FindSession searches all project stores under baseDir for a session by ID. Returns the session, the store it was found in, and any error.
func FindSessionReadOnly ¶
FindSessionReadOnly searches all project stores without migrating or writing the matching session. It is the read-only counterpart to FindSession.
func FormatTranscript ¶ added in v0.35.0
FormatTranscript renders a root→leaf path as a line-oriented transcript that keeps messages and tool calls and stays reasonably paginable with the read tool's offset/limit. Thinking and binary payloads are omitted.
entries must be Tree.Path() (the full active branch), not BuildContext(): compaction does not delete history, and the snapshot is evidence, so pre-compaction messages stay in the file even though the parent model no longer sees them.
func MigrateV1ToV2 ¶
MigrateV1ToV2 converts a v1 session (flat Messages) to v2 (entry-based tree). Idempotent: returns nil immediately if already v2+. Each message becomes an Entry with a sequential parent chain. If the first message is a compaction_summary, it becomes a CompactionEntry followed by the remaining messages (recording that compaction already happened).
func PreservedMetadata ¶ added in v0.20.0
PreservedMetadata extracts the creation-time keys that survive snapshots. Returns nil when none are present.
func RemoveArtifacts ¶ added in v0.37.0
RemoveArtifacts deletes the sidecar of a session that has no live writer (an unloaded conversation being deleted).
func RemoveTranscriptSnapshots ¶ added in v0.35.0
RemoveTranscriptSnapshots deletes the snapshot sidecar. No-op if absent.
func TranscriptSnapshotDir ¶ added in v0.35.0
TranscriptSnapshotDir is the sidecar directory that holds frozen parent transcript snapshots for one session: <sessionDir>/<sessionID>.snapshots/.
func ValidateEntries ¶
ValidateEntries checks the integrity of a session's entry tree. Returns an error if:
- duplicate entry IDs
- missing parent references
- leaf ID not found
- cycle detected (from leaf to root)
func ValidateID ¶
ValidateID accepts the opaque identifier generated for persisted sessions. Keeping IDs filename-safe prevents external API inputs from escaping a store.
func WriteTranscriptSnapshot ¶ added in v0.35.0
WriteTranscriptSnapshot freezes entries (the caller's copy of Tree.Path) into a markdown file and returns its absolute path. The file is immutable in the sense that moa writes it once and never rewrites it; later appends to the live tree cannot appear in it.
Types ¶
type Artifact ¶ added in v0.37.0
type Artifact struct {
ID string `json:"id"`
Path string `json:"path"`
Name string `json:"name"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Mime string `json:"mime"`
Size int64 `json:"size"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Artifact is one durable reference from a conversation to a live canonical path. It never holds bytes, versions or inode identity: reads always open Path again, so an atomic replacement at the same location is visible.
type ArtifactMeta ¶ added in v0.37.0
ArtifactMeta carries the observed metadata of one publication. Title and Description only overwrite stored values when non-empty, so a bare re-send does not silently erase a caption the agent gave earlier.
type ArtifactStore ¶ added in v0.37.0
type ArtifactStore struct {
// contains filtered or unexported fields
}
ArtifactStore persists one conversation's artifact references in a sidecar next to its session JSON: "<sessionDir>/<sessionID>.artifacts".
The extension is deliberately NOT ".json": FileStore.List walks every "*.json" in the directory, and a catalog without a session ID in its header would be read in full on every session listing.
The writer instance is unique and owned by the live ManagedSession; handlers open read-only instances, which the atomic rename always shows either the previous or the new catalog.
func NewArtifactStore ¶ added in v0.37.0
func NewArtifactStore(sessionDir, sessionID string) *ArtifactStore
NewArtifactStore returns the store for one session. sessionDir is the directory holding "<sessionID>.json" (FileStore.Dir()). Nothing is created until the first Upsert.
func (*ArtifactStore) DeleteReferences ¶ added in v0.37.0
func (s *ArtifactStore) DeleteReferences() error
DeleteReferences removes the sidecar and tombstones this instance so a concurrent publication that had not yet entered the critical section cannot recreate it. Original files are never touched.
func (*ArtifactStore) Get ¶ added in v0.37.0
func (s *ArtifactStore) Get(id string) (Artifact, bool, error)
Get returns one artifact by ID.
func (*ArtifactStore) List ¶ added in v0.37.0
func (s *ArtifactStore) List() ([]Artifact, error)
List returns the stored artifacts, newest update first (ties broken by ID so the order is stable). A missing sidecar is an empty collection, not an error.
func (*ArtifactStore) Path ¶ added in v0.37.0
func (s *ArtifactStore) Path() string
Path returns the sidecar path (may not exist).
func (*ArtifactStore) Upsert ¶ added in v0.37.0
func (s *ArtifactStore) Upsert(canonicalPath string, meta ArtifactMeta) (Artifact, error)
Upsert publishes canonicalPath in this conversation and returns the stored artifact. One entry per canonical path: a re-send keeps the ID and CreatedAt and refreshes the observed metadata. Returns ErrArtifactsDeleted once the conversation has been deleted.
type CWDScan ¶ added in v0.25.0
type CWDScan struct {
// CWDs holds the distinct working directories, newest session first.
CWDs []string
// Unreadable counts the session files that could not be decoded at all:
// truncated or corrupt JSON, a shape the header decoder does not
// recognize, a file that would not open. It is kept apart from NoCWD
// because it is the only half that can change: such a file may decode on
// the next run, so a caller that needs the full picture has a reason to
// look again.
Unreadable int
// NoCWD counts the sessions that decoded fine and simply never recorded a
// working directory — written by a moa old enough not to store one.
// Re-reading them produces the same answer forever, so a caller deciding
// whether to retry must not mistake this for evidence it could recover.
NoCWD int
}
CWDScan is what ScanCWDs could learn about where sessions ran.
func ScanCWDs ¶ added in v0.25.0
ScanCWDs reports the working directories recorded across every project-scoped store under baseDir, reading only each session's header.
It exists next to ListAll rather than on top of it because it must never fall back to a full read: ListAll's readSummary retries a file it could not stream by loading all of it, which for a damaged multi-megabyte transcript means reading the whole thing to answer a question about its first hundred bytes. A caller that only wants the header treats such a file as unreadable and says so — that is what Unreadable is for. ListAll keeps its fallback, since a session list that silently dropped recoverable sessions would be a regression for every other caller.
func (CWDScan) Unmappable ¶ added in v0.25.0
Unmappable is the number of sessions that could not say which directory they ran in, for either reason.
type CompactionData ¶
type CompactionData struct {
Summary string `json:"summary"`
FirstKeptEntryID string `json:"first_kept_entry_id"`
TokensBefore int `json:"tokens_before"`
ReadFiles []string `json:"read_files,omitempty"`
ModifiedFiles []string `json:"modified_files,omitempty"`
}
CompactionData records a non-destructive compaction event.
func (CompactionData) IsEmpty ¶
func (c CompactionData) IsEmpty() bool
IsEmpty returns true if the CompactionData has no summary (zero value).
type ConfigChangeData ¶
type ConfigChangeData struct {
Model string `json:"model,omitempty"`
Thinking string `json:"thinking,omitempty"`
}
ConfigChangeData records a configuration change (model, thinking, etc.).
func (ConfigChangeData) IsEmpty ¶
func (c ConfigChangeData) IsEmpty() bool
IsEmpty returns true if the ConfigChangeData is zero-valued.
type Entry ¶
type Entry struct {
ID string `json:"id"`
ParentID string `json:"parent_id,omitempty"`
Timestamp time.Time `json:"ts"`
Type EntryType `json:"type"`
// Type-specific data (only one populated per entry):
Message core.AgentMessage `json:"message,omitempty"`
Compaction CompactionData `json:"compaction,omitempty"`
Config ConfigChangeData `json:"config,omitempty"`
Label string `json:"label,omitempty"`
}
Entry is a single immutable unit in the session log. All fields are stored by value to enforce immutability.
func DeepCopyEntry ¶
DeepCopyEntry creates a deep copy of an Entry, including all mutable nested data.
type FileStore ¶
type FileStore struct {
// contains filtered or unexported fields
}
FileStore manages session persistence on disk. Sessions are stored as individual JSON files in a directory. Writes are atomic (temp file + rename) to prevent corruption.
func FindSessionStoreReadOnly ¶ added in v0.37.0
FindSessionStoreReadOnly locates the project store owning a session without loading its transcript. It opens only the exact "<id>.json" candidate and decodes the header prefix, so a handler that just needs the session's directory (artifact catalog, sidecars) does not pay for the whole conversation history on every request — unlike FindSessionReadOnly, which calls LoadReadOnly.
A missing session returns ErrNotFound; a candidate that exists but cannot be read or whose header does not identify it is a real error, so callers can answer 404 and 500 differently.
func NewFileStore ¶
NewFileStore creates a FileStore for sessions scoped to the given CWD. baseDir is the root sessions directory (empty = ~/.config/moa/sessions/). cwd determines the project subdirectory. Empty cwd uses baseDir directly (legacy/tests).
func (*FileStore) Latest ¶
Latest returns the most recently updated session. Returns nil, nil if no sessions exist.
func (*FileStore) List ¶
List returns summaries of all sessions, sorted by Updated descending (newest first). Uses a streaming json.Decoder that stops as soon as it reaches the entries/messages field, so it never reads (or allocates) the — potentially multi-megabyte — conversation history just to show a session list.
func (*FileStore) Load ¶
Load reads a session by ID. Returns ErrNotFound (wrapped) if the session does not exist.
func (*FileStore) LoadReadOnly ¶
LoadReadOnly reads a session without performing the legacy v1 migration. Consumers which promise a read-only operation (for example transcript export) must use this method: Load may write a migrated copy to disk.
type Session ¶
type Session struct {
// Header fields — read by partial-read list optimization
ID string `json:"id"`
Version int `json:"version"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Title string `json:"title"`
// TitleSource records how Title was set: "manual" (user renamed) or "auto"
// (derived / LLM-generated). Empty is legacy and treated as auto.
TitleSource string `json:"title_source,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
// V2: entry-based tree log
LeafID string `json:"leaf_id,omitempty"`
Entries []Entry `json:"entries,omitempty"`
// V1 legacy (only present in old sessions, cleared after migration)
Messages []core.AgentMessage `json:"messages,omitempty"`
CompactionEpoch int `json:"compaction_epoch,omitempty"`
}
Session represents a persistent conversation.
Field ordering matters: summary fields (ID, Version, Title, Metadata) come first so the readSummary partial-read optimization (4KB prefix) still works.
func (*Session) CompactAtMeta ¶
CompactAtMeta returns the persisted soft compaction threshold in tokens, or 0 when none was set (the default window-based behavior). Metadata round-trips through JSON, so a number read back from disk arrives as float64 while one set in this process is still an int — both are accepted.
func (*Session) FastMeta ¶ added in v0.35.0
FastMeta returns whether premium speed was enabled when this session was last saved. Missing and malformed values preserve the historical default.
func (*Session) Origin ¶ added in v0.20.0
Origin returns the persisted origin, defaulting to OriginUser when absent.
func (*Session) Rename ¶
Rename sets a user-chosen title and marks it manual so auto-titling never overwrites it.
func (*Session) RuntimeMeta ¶
RuntimeMeta returns the persisted runtime configuration from Metadata. Missing keys return empty strings.
func (*Session) SetAutoTitle ¶
SetAutoTitle applies an auto-generated title, unless the user has manually renamed the session. Empty/whitespace titles are ignored.
func (*Session) SetIdempotencyKey ¶ added in v0.20.0
SetIdempotencyKey records the Automation API key this session answers. It is written only after the run's first prompt was accepted, so a session that never received one stays unreachable by key. An empty key is not stored.
func (*Session) SetOrigin ¶ added in v0.20.0
SetOrigin records who created the session (e.g. "user", "automation", or a caller-chosen label such as "linear-webhook"). An empty origin is not stored: missing means user.
func (*Session) SetPathMetadata ¶
SetPathMetadata persists path scope and allowed paths to session metadata.
func (*Session) SetRuntimeMetadata ¶
SetRuntimeMetadata persists the core session configuration (model, cwd, permission mode, thinking level) into Metadata. Called on every state change and at session creation. Centralizes what gets persisted so all frontends (serve, headless CLI) stay consistent.
func (*Session) SetTitle ¶
SetTitle sets the session title from a user message. Only sets if title is empty (first message). Truncates to maxLen.
func (*Session) TitleIsManual ¶
TitleIsManual reports whether the user explicitly renamed the session. A legacy empty source counts as auto.
type SessionStore ¶
type SessionStore interface {
Create() *Session
Save(sess *Session) error
Load(id string) (*Session, error)
Latest() (*Session, error)
List() ([]Summary, error)
Delete(id string) error
}
SessionStore abstracts session persistence. FileStore implements this for disk-based storage. External consumers (e.g., HTTP servers) implement it for database storage.
Contract:
- Create returns a new Session with a unique ID and timestamps set. It does NOT persist — call Save.
- Save persists the session. It MUST set Updated to the current time before writing.
- Load returns the session or ErrNotFound (wrapped or direct — use errors.Is).
- Latest returns the most recently updated session, or (nil, nil) if the store is empty.
- List returns summaries sorted by Updated descending. Empty store returns (nil, nil).
- Delete is idempotent — deleting a non-existent session returns nil.
type SubagentStore ¶
type SubagentStore struct {
// contains filtered or unexported fields
}
SubagentStore persists subagent transcripts for one parent session in a side directory: <session dir>/<sessionID>.subagents/<jobID>.json.
func NewSubagentStore ¶
func NewSubagentStore(sessionDir, sessionID string) *SubagentStore
NewSubagentStore returns a store rooted at "<sessionDir>/<sessionID>.subagents". sessionDir is the directory holding the parent session's <id>.json (e.g. FileStore.Dir()). The directory is created lazily on first Save.
func (*SubagentStore) Dir ¶
func (s *SubagentStore) Dir() string
Dir returns the side-directory path (may not exist yet).
func (*SubagentStore) LegacyResult ¶ added in v0.30.0
func (s *SubagentStore) LegacyResult(jobID string) (string, error)
LegacyResult reads the final assistant text from one legacy transcript without constructing its complete Messages slice. Completed transcripts written before Result existed need that fallback for their restored cards, but retaining every message merely to recover its final text defeats the summary path's memory savings.
func (*SubagentStore) List ¶
func (s *SubagentStore) List() ([]SubagentTranscript, error)
List returns all persisted transcripts for the session, newest-finished first. Missing directory yields an empty slice (not an error).
func (*SubagentStore) ListSummaries ¶ added in v0.30.0
func (s *SubagentStore) ListSummaries() ([]SubagentTranscript, error)
ListSummaries returns persisted transcript headers, newest-finished first. It stops before Messages because callers that only render subagent cards do not need to allocate every child conversation merely to show its metadata. Missing directories and unreadable headers keep List's established result: an empty slice and skipped sidecars, respectively.
The decoded headers are reused while every sidecar's size and mtime stay unchanged: WebSocket init asks for this list on every reconnect, and decoding the same files again was the dominant heap cost of those payloads.
func (*SubagentStore) Load ¶
func (s *SubagentStore) Load(jobID string) (*SubagentTranscript, error)
Load reads one transcript by jobID. Returns ErrNotFound (wrapped) if absent.
func (*SubagentStore) Remove ¶
func (s *SubagentStore) Remove() error
Remove deletes the entire side directory (used when the parent session is deleted). No-op if it doesn't exist.
func (*SubagentStore) Save ¶
func (s *SubagentStore) Save(t SubagentTranscript) error
Save atomically writes one transcript to <jobID>.json.
type SubagentTranscript ¶
type SubagentTranscript struct {
JobID string `json:"job_id"`
Task string `json:"task"`
Title string `json:"title,omitempty"`
Model string `json:"model"`
Status string `json:"status"`
// Result and Error retain the terminal outcome separately from the child
// transcript so a parent timeline can restore the correct terminal card.
Result string `json:"result,omitempty"`
Error string `json:"error,omitempty"`
Thinking string `json:"thinking,omitempty"`
Async bool `json:"async"`
StartedAt time.Time `json:"started_at,omitempty"`
FinishedAt time.Time `json:"finished_at,omitempty"`
Usage *core.Usage `json:"usage,omitempty"`
CostUSD float64 `json:"cost_usd,omitempty"`
// ContextPercent is how full the child's own window was when it finished
// (0-100). Stored alongside usage so reopening a finished subagent
// restores the same reading it had while it ran. A POINTER because 0 is a
// real reading (a child that barely used its window) and has to stay
// distinguishable from a transcript written before this was recorded:
// nil means unknown, and unknown hides the ring instead of drawing an
// empty one.
ContextPercent *int `json:"context_percent,omitempty"`
Messages []core.AgentMessage `json:"messages"`
}
SubagentTranscript is the persisted record of one subagent's sub-conversation. Stored in a side directory next to the parent session so a subagent's transcript survives restarts and can be reopened after it finished, without bloating the parent session's own tree/history.
type Summary ¶
type Summary struct {
ID string `json:"id"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Title string `json:"title"`
TitleSource string `json:"title_source,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}
Summary is a lightweight session descriptor without messages. Used for listing sessions without loading full conversation data.
type Tree ¶
type Tree struct {
// contains filtered or unexported fields
}
Tree is an append-only entry log that supports branching. It maintains an index for O(1) lookup and a leaf pointer for the current branch tip. All public methods are safe for concurrent use.
Key methods:
- BuildContext() returns messages for the LLM (handles compaction)
- AllMessages() returns ALL messages along the current path (for display)
- Branch() moves the leaf to enable forking
- Snapshot() returns a deep-copied (entries, leafID) pair for persistence
func NewTreeFromEntries ¶
NewTreeFromEntries reconstructs a tree from persisted entries. Returns an error if the entries are invalid (see ValidateEntries).
func (*Tree) AllMessages ¶
func (t *Tree) AllMessages() []core.AgentMessage
AllMessages returns ALL messages along the current path (for display). Includes pre-compaction messages. Compaction entries become synthetic status messages.
func (*Tree) Append ¶
Append adds an entry as a child of the current leaf and advances the leaf. The entry's ID and Timestamp are set automatically. ParentID is set to the current leaf. The Message field is deep-copied to enforce immutability. Returns the assigned entry ID.
func (*Tree) Branch ¶
Branch moves the leaf pointer to the given entry ID. Next Append creates a child of that entry (starting a new branch). Returns an error if:
- the entry ID doesn't exist
- the target is a tool_result (would leave dangling tool_call)
- the resulting path (as BuildContext would see it) leaves any assistant tool_call without a matching tool_result
func (*Tree) BuildContext ¶
func (t *Tree) BuildContext() ([]core.AgentMessage, int)
BuildContext returns messages for the LLM provider and the compaction epoch.
Algorithm:
- Walk the current path (root → leaf)
- Find the LAST compaction entry (most recent wins for multi-compaction)
- If compaction found: emit compaction_summary + messages from firstKeptEntryID → leaf
- If no compaction: emit all message entries
- Only include LLM-relevant roles (user, assistant, tool_result, compaction_summary)
func (*Tree) Children ¶
Children returns direct children of the given entry. Returned entries are shallow copies; do not mutate.
func (*Tree) DisplayMessagesSince ¶ added in v0.28.0
func (t *Tree) DisplayMessagesSince(entryID string) ([]core.AgentMessage, bool)
DisplayMessagesSince returns the display projection strictly after entryID when entryID is on the current root-to-leaf path. The boolean is false for an empty, unknown, or off-path token; callers must then use a full snapshot. A compaction remains a display marker, exactly as it does in AllMessages.
func (*Tree) Entries ¶
Entries returns a shallow copy of all entries. For persistence, prefer Snapshot() which deep-copies messages.
func (*Tree) HasMsgID ¶ added in v0.21.0
HasMsgID reports whether any entry in the tree — on ANY branch, not just the current path — carries a message with this ID. Uniqueness of a message identity is a property of the whole tree: branching away from a message does not delete it, and a client can navigate back to that branch, so an ID reused on another branch would collide there.
func (*Tree) Path ¶
Path returns entries from root to the current leaf, in order. Returned entries are shallow copies; do not mutate.
func (*Tree) Snapshot ¶
Snapshot returns a deep copy of all entries and the current leaf ID atomically. Entries are deep-copied (including messages) so the caller can serialize without races. Use this instead of separate Entries()+LeafID() calls for persistence.
func (*Tree) ValidBranchTarget ¶
ValidBranchTarget reports whether branching to entryID would be accepted by Branch, without mutating the tree. Callers building branch-picker UIs should use this to filter candidates so they never offer a target that Branch would reject (e.g. an assistant turn with unresolved tool calls).