Conversation Storage
August 30, 2026 · View on GitHub
The CLI supports configurable conversation storage, allowing you to save, resume, and manage your chat sessions across different invocations. By default, conversations are stored using JSONL (JSON Lines) files, which provides zero-dependency persistent storage. You can also choose SQLite, PostgreSQL, Redis, Cloudflare D1, or in-memory storage.
Overview
The conversation storage system provides:
- Configurable Storage: Choose between JSONL (default), SQLite, PostgreSQL, Redis, Cloudflare D1, or in-memory
- Conversation Management: List, save, load, and delete conversations using
/conversations - Unified Interface: Consistent API across all storage backends
Storage Backends
JSONL (Default - Recommended for personal use)
JSONL (JSON Lines) provides a simple, file-based storage solution perfect for personal use with zero dependencies.
Configuration:
storage:
enabled: true
type: jsonl
jsonl:
path: ~/.infer/conversations
Pros:
- No external dependencies (no database, no CGO)
- Human-readable format (text files)
- Easy to backup, sync, and version control
- Git-friendly (text-based)
- Zero setup required
- Works on all platforms
- Fast for typical usage (dozens to hundreds of conversations)
Cons:
- Not suitable for thousands of conversations
- No advanced querying capabilities
- Sequential file access (vs indexed database)
File Structure:
Each conversation is stored in a separate JSONL file. With the default (unset)
path, each project gets its own directory under ~/.infer/projects/:
~/.infer/projects/<project-slug>/conversations/
├── <conversation-id-1>.jsonl
├── <conversation-id-2>.jsonl
└── <conversation-id-3>.jsonl
Each file is append-only (format v2): the CLI appends one line per conversation entry as the chat progresses, then appends a fresh metadata line on every save. The loader reads the last metadata line, so each save publishes the latest token and cost stats. A file therefore grows to many lines:
- One entry line per message -
{"type": "entry", "index": N, "entry": {...}} - A trailing metadata line -
{"v": 2, "type": "metadata", "metadata": {...}}- re-appended on each save
Legacy v1 files (a single metadata line followed by a single entries array) are still read for backward compatibility.
Backup:
Simply copy the conversations directory:
cp -r ~/.infer/projects/<project-slug>/conversations ~/backups/conversations-$(date +%Y%m%d)
Version Control:
Works well with Git:
cd ~/.infer/projects/<project-slug>/conversations
git init
git add *.jsonl
git commit -m "Save conversations"
SQLite (Alternative for local use)
SQLite provides a lightweight, file-based storage solution perfect for personal use. By default the database lives at
~/.infer/conversations.db - one shared file across projects; each conversation records its project so listings
scope to the current one:
storage:
type: sqlite
sqlite:
path: ~/.infer/conversations.db
Pros:
- No external dependencies
- Fast local access
- Automatic schema management
- Perfect for single-user scenarios
Cons:
- Not suitable for multi-user environments
- Limited concurrent access
PostgreSQL (Recommended for teams)
PostgreSQL offers enterprise-grade features for team environments:
storage:
type: postgres
postgres:
host: localhost
port: 5432
database: infer_conversations
username: infer_user
password: your_password
ssl_mode: require
Pros:
- Multi-user support
- ACID compliance
- Advanced indexing and search
- JSON/JSONB support for metadata
Cons:
- Requires PostgreSQL server
- More complex setup
Redis (Recommended for temporary storage)
Redis provides fast, in-memory storage with optional persistence:
storage:
type: redis
redis:
host: localhost
port: 6379
password: "" # optional
db: 0 # Redis database number
Pros:
- Extremely fast access
- Built-in expiration (TTL)
- Scalable clustering support
- Great for temporary conversations
Cons:
- Requires Redis server
- Memory-based (can be expensive for large datasets)
- Data loss risk if not properly configured for persistence
D1 (Cloudflare D1)
Cloudflare D1 is serverless SQLite exposed over an HTTP query API - useful when you want hosted, queryable conversation storage without running your own database server.
storage:
enabled: true
type: d1
d1:
account_id: "<cloudflare-account-id>"
database_id: "<d1-database-id>"
# api_token is a secret - inject it via the environment, not the config file:
# export INFER_STORAGE_D1_API_TOKEN=...
base_url: "" # optional override for the D1 API endpoint
Pros:
- Hosted and serverless - no database server to run
- SQL querying with SQLite semantics
- Reachable from anywhere over HTTPS
Cons:
- Requires a Cloudflare account and a provisioned D1 database
- Network latency on every operation
- API token must be supplied (normally via
INFER_STORAGE_D1_API_TOKEN)
Configuration
Add storage configuration to your .infer/config.yaml:
# Storage configuration
storage:
enabled: true # true to enable persistent storage
type: jsonl # Options: jsonl (default), sqlite, postgres, redis, memory
# JSONL configuration (used when type: jsonl)
jsonl:
path: ~/.infer/projects/<project-slug>/conversations # optional; unset = per-project default
# SQLite configuration (used when type: sqlite)
sqlite:
path: ~/.infer/conversations.db # optional; default when unset
# PostgreSQL configuration (used when type: postgres)
postgres:
host: localhost
port: 5432
database: infer_conversations
username: "%POSTGRES_USER%" # Can use environment variables
password: "%POSTGRES_PASSWORD%" # Can use environment variables
ssl_mode: prefer
# Redis configuration (used when type: redis)
redis:
host: localhost
port: 6379
password: "%REDIS_PASSWORD%" # Can use environment variables
db: 0 # Redis database number
Per-Project Grouping
Every session records the project it ran in (the absolute working directory). Listings scope to that project by default:
- The
/conversationsTUI picker shows only the current project's conversations. infer conversations listdoes the same; pass--all-projectsto list every project's conversations.- JSONL (default) keeps one store per project under
~/.infer/projects/<project-slug>/conversations/; SQLite keeps one shared database (~/.infer/conversations.db) and groups rows by theprojectcolumn; server backends (PostgreSQL, Redis, D1) group by the same field in conversation metadata. - An explicit
storage.jsonl.pathorstorage.sqlite.pathalways wins and only ever lists itself. - Nothing conversation-related is written to the project directory anymore; old project-dir stores are simply orphaned (delete them manually).
Enabling Storage
-
For JSONL storage (default):
storage: enabled: true type: jsonl jsonl: path: ~/.infer/conversations -
For SQLite storage:
storage: enabled: true type: sqlite sqlite: path: conversations.db -
For PostgreSQL storage:
storage: enabled: true type: postgres postgres: host: your-postgres-host port: 5432 database: infer_conversations username: "%POSTGRES_USER%" password: "%POSTGRES_PASSWORD%" -
For Redis storage:
storage: enabled: true type: redis redis: host: your-redis-host port: 6379 password: "%REDIS_PASSWORD%" -
For in-memory storage:
- Set
enabled: falseortype: memory - Conversations are lost when the CLI exits
- Set
Usage
Starting a New Conversation
When you start the CLI, you automatically begin a new conversation. To explicitly start with a title:
/save My Important Discussion
Saving Conversations
Save your current conversation:
# Save with auto-generated title
/save
# Save with custom title
/save Discussion about API Design
# Save with multi-word title
/save Planning the Q4 Product Roadmap
Resuming Conversations
List recent conversations:
/conversations
This shows:
Select a Conversation
Press / to search • 4 conversations available
ID │ Summary │ Updated │ Messages
─────────────────────────────────────────────────────────────────────────────────────────────────────
▶ fdd90f83-0b84-486... │ Implementing Redis cache layer │ 2025-08-27 00:55:29 │ 2
22de96f6-577d-4df... │ Debugging API authentication flow │ 2025-08-27 00:32:25 │ 12
b199fae0-b0cd-418... │ Setting up PostgreSQL migrations │ 2025-08-27 00:27:20 │ 6
ca79a501-ef90-4e0... │ Refactoring conversation storage │ 2025-08-26 23:52:59 │ 4
─────────────────────────────────────────────────────────────────────────────────────────────────────
Resume by number or ID:
# Use /conversations to select and load a conversation interactively
/conversations
Managing Conversations
Delete a conversation:
# Use /conversations to select a conversation and press 'd' to delete it
/conversations
Data Structure
Conversation Metadata
Each conversation includes:
- ID: Unique identifier (UUID)
- Project: absolute working directory of the session; scopes listings per project
- Title: Human-readable title
- Created/Updated: Timestamps
- Message Count: Number of messages
- Token Statistics: Usage tracking
- Model: AI model used
- Tags: Organizational labels
- Summary: Optional conversation summary
Message Storage
Messages are stored with:
- Content: Message text
- Role: user, assistant, system, or tool
- Timestamp: When the message was created
- Model: AI model used for this message
- Tool Execution: Results of tool calls
- System Reminder Flag: Internal system messages
Database Schema
SQLite / PostgreSQL / Cloudflare D1
All three SQL backends share a single-table schema: the conversation messages
are stored as an embedded JSON blob in the messages column rather than in a
separate normalized table, so one shared SQL core drives every SQL backend (the
dialects differ only in placeholder style and datetime type). PostgreSQL uses
TIMESTAMP WITH TIME ZONE where SQLite uses DATETIME.
-- Conversations table (messages embedded as a JSON array in `messages`)
CREATE TABLE conversations (
id TEXT PRIMARY KEY,
project TEXT NOT NULL DEFAULT '',
title TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
messages TEXT NOT NULL,
total_input_tokens INTEGER NOT NULL DEFAULT 0,
total_output_tokens INTEGER NOT NULL DEFAULT 0,
request_count INTEGER NOT NULL DEFAULT 0,
cost_stats TEXT DEFAULT '{}',
models TEXT DEFAULT '[]',
tags TEXT DEFAULT '[]',
title_generated BOOLEAN DEFAULT FALSE,
title_invalidated BOOLEAN DEFAULT FALSE,
title_generation_time TIMESTAMP,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
-- Session-group index for channel-keyed session rollover
CREATE TABLE session_groups (
group_key TEXT PRIMARY KEY,
current_session_id TEXT NOT NULL,
history TEXT NOT NULL DEFAULT '[]',
last_rollover TIMESTAMP,
updated_at TIMESTAMP NOT NULL
);
Redis
# Conversation metadata
conversation:{id} -> JSON metadata
# Conversation entries
conversation:{id}:entries -> JSON array of entries
# Conversation index (sorted by update time)
conversations:index -> sorted set (score: timestamp, member: conversation_id)
Best Practices
Performance
- SQLite: Keep database file on fast storage (SSD)
- PostgreSQL: Use connection pooling for high-concurrency scenarios
- Redis: Configure appropriate memory policies and persistence
Security
- Database Credentials: Use environment variables or secure credential storage
- Network Security: Use SSL/TLS for network connections
- Access Control: Implement proper user authentication and authorization
Backup
- SQLite: Regular file system backups of the
.dbfile - PostgreSQL: Use
pg_dumpfor regular backups - Redis: Configure RDB or AOF persistence
Monitoring
- Health Checks: The storage interface includes health check methods
- Error Handling: Failed operations are logged and don't interrupt the session
- Auto-save: Conversations are automatically saved after each interaction
Troubleshooting
Common Issues
SQLite Permission Errors
# Ensure directory exists and is writable
mkdir -p ~/.infer
chmod 755 ~/.infer
PostgreSQL Connection Issues
# Check connection parameters
storage:
type: postgres
postgres:
host: localhost # Verify host
port: 5432 # Verify port
ssl_mode: disable # Try without SSL first
Redis Connection Issues
# Verify Redis is running
redis:
host: localhost
port: 6379
database: 0
password: "" # Remove if no auth
Migration
When switching storage backends, you'll need to export/import conversations manually. The CLI provides export functionality that can help with migration:
/compact # Exports current conversation to markdown
API Reference
Storage Interface
type ConversationStorage interface {
SaveConversation(ctx context.Context, conversationID string,
entries []domain.ConversationEntry, metadata ConversationMetadata) error
LoadConversation(ctx context.Context, conversationID string) (
[]domain.ConversationEntry, ConversationMetadata, error)
ListConversations(ctx context.Context, project string, limit, offset int) ([]ConversationSummary, error)
DeleteConversation(ctx context.Context, conversationID string) error
UpdateConversationMetadata(ctx context.Context, conversationID string,
metadata ConversationMetadata) error
Close() error
Health(ctx context.Context) error
}
Factory Function
// Create storage instance from configuration
storage, err := storage.NewStorage(config)
if err != nil {
log.Fatal("Failed to create storage:", err)
}
defer storage.Close()