Conversation Store
Package matrix/neo/internal/conversation is Neo's durable chat-thread memory. It persists each turn as append-only JSONL per conversation_id with a retained-turn cap and archive rollover.
Package matrix/neo/internal/conversation is Neo's durable chat-thread memory. It persists each turn as append-only JSONL (one line per turn) per conversation_id, so history survives reloads, new chats, suspend, and redeploy at O(1) write cost per turn.
Source file: neo/internal/conversation/store.go.
Design decisions
Unified history with the daemon. The on-disk shape is byte-compatible with the daemon's own conversation store (executor/cmd/mcl-execute/daemon_conversation.go). A pre-Neo daemon thread written as a single JSON object is still readable (the loader falls back to the legacy single-object parse when the file is not JSONL). New writes are always JSONL. Neo derives the SAME directory (filepath.Dir(cortexRoot)/conversations = /data/conversations in prod), so a user's pre-Neo daemon threads and their new Neo threads list together as one unified history.
Pure side-channel. It never touches cortex, signs anything, or perturbs replay. Conversation continuity and the audit/replay chain are independent storage.
O(1) append writes. Each turn is appended as one JSONL line with fsync. The rename-based atomic write (tmp + rename) is reserved for the archive rollup/compaction step, never for the per-turn append.
Retained-turn cap with archive rollover. The hot set (live .jsonl) is bounded by RetainedTurns (config NEO_CONVERSATION_RETAINED_TURNS, default 1000). Turns that scroll past the cap roll to a sibling archive file (<convID>.archive.jsonl) -- durable, NOT dropped -- and remain retrievable via History/Archived/Recent.
Best-effort. IO errors are logged, never fatal. A blank conversation id or text is ignored.
Store
type Store struct {
mu sync.Mutex
dir string
}
store := conversation.Open(dir) // dir="" yields a disabled store (safe no-op)
Turn
type Turn struct {
Role string `json:"role"` // "user" | "assistant"
Text string `json:"text"`
IntentID string `json:"intent_id,omitempty"`
TS time.Time `json:"ts"`
}
Record
type Record struct {
ConversationID string `json:"conversation_id"`
Title string `json:"title,omitempty"`
Turns []Turn `json:"turns"`
Updated time.Time `json:"updated"`
}
Operations
Append
func (s *Store) Append(convID string, turn Turn)
func (s *Store) AppendUser(convID, text string)
func (s *Store) AppendAssistant(convID, intentID, text string)
Records one turn and persists atomically. Zero TS is filled with time.Now().UTC().
Recent
func (s *Store) Recent(convID string, n int) []Turn
Returns the last n turns (oldest-first), or nil when there are none. Used for resume seeding. DefaultRecallTurns = 16.
Get
func (s *Store) Get(convID string) *Record
Returns the full turn log for one conversation, or nil when not found.
List
func (s *Store) List() []Summary
Returns a summary of every persisted conversation, newest-first:
type Summary struct {
ConversationID string `json:"conversation_id"`
Title string `json:"title"`
Preview string `json:"preview"`
TurnCount int `json:"turn_count"`
Updated time.Time `json:"updated"`
}
Title derives from the first user turn (trimmed to 60 runes). Preview is the most recent turn (trimmed to 100 runes).
History and Archived
func (s *Store) History(convID string, limit int) []Turn // hot + archive, newest-first
func (s *Store) Archived(convID string) []Turn // archive file only
History merges the hot set and the archive, returning the most recent limit turns across both. Archived returns only the rolled-off turns.
Directory resolution
func Dir(override, cortexRoot string) string
- Explicit
NEO_CONVERSATIONS_DIRwins - Otherwise derives from
filepath.Dir(cortexRoot)then sibling of cortex root - Returns
""when neither is available (persistence disabled)
This matches how server.MediaDir derives /data/media from /data/cortex.
Daemon compatibility
The JSON tags match the daemon store and the web client's ConversationTurn. A file written by the daemon is readable by Neo, and vice versa. This is tested in TestDaemonFileCompatible.
Modifying the store
| What to change | Where |
|---|---|
| Default recall turns | conversation/store.go - DefaultRecallTurns (16) |
| Retained-turn cap | config/config.go - NEO_CONVERSATION_RETAINED_TURNS (1000) |
| Title/preview length | conversation/store.go - truncateLabel() |
| Directory derivation | conversation/store.go - Dir() |
| JSON shape | conversation/store.go - Turn, Record, Summary structs |
