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.
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
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/conversationsPros:
- 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 in the configured directory:
~/.infer/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/conversations ~/backups/conversations-$(date +%Y%m%d)Version Control:
Works well with Git:
cd ~/.infer/conversations
git init
git add *.jsonl
git commit -m "Save conversations"SQLite provides a lightweight, file-based storage solution perfect for personal use:
storage:
type: sqlite
sqlite:
path: ~/.infer/conversations.dbPros:
- 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 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: requirePros:
- Multi-user support
- ACID compliance
- Advanced indexing and search
- JSON/JSONB support for metadata
Cons:
- Requires PostgreSQL server
- More complex setup
Redis provides fast, in-memory storage with optional persistence:
storage:
type: redis
redis:
host: localhost
port: 6379
password: "" # optional
db: 0 # Redis database numberPros:
- 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
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 endpointPros:
- 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)
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/conversations # Directory for JSONL files
# SQLite configuration (used when type: sqlite)
sqlite:
path: conversations.db # Relative to .infer directory or absolute path
# 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-
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
When you start the CLI, you automatically begin a new conversation. To explicitly start with a title:
/save My Important DiscussionSave 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 RoadmapList recent conversations:
/conversationsThis 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
/conversationsDelete a conversation:
# Use /conversations to select a conversation and press 'd' to delete it
/conversationsEach conversation includes:
- ID: Unique identifier (UUID)
- 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
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
-- Conversations table
CREATE TABLE conversations (
id VARCHAR(255) PRIMARY KEY,
title TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
message_count INTEGER NOT NULL DEFAULT 0,
model VARCHAR(255),
tags JSON,
summary TEXT,
token_stats JSON
);
-- Conversation entries table
CREATE TABLE conversation_entries (
id BIGSERIAL PRIMARY KEY,
conversation_id VARCHAR(255) NOT NULL,
entry_data JSON NOT NULL,
sequence_number INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL,
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
);# 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)
- SQLite: Keep database file on fast storage (SSD)
- PostgreSQL: Use connection pooling for high-concurrency scenarios
- Redis: Configure appropriate memory policies and persistence
- 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
- SQLite: Regular file system backups of the
.dbfile - PostgreSQL: Use
pg_dumpfor regular backups - Redis: Configure RDB or AOF persistence
- 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
# Ensure directory exists and is writable
mkdir -p ~/.infer
chmod 755 ~/.infer# Check connection parameters
storage:
type: postgres
postgres:
host: localhost # Verify host
port: 5432 # Verify port
ssl_mode: disable # Try without SSL first# Verify Redis is running
redis:
host: localhost
port: 6379
database: 0
password: "" # Remove if no authWhen 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 markdowntype 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, 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
}// Create storage instance from configuration
storage, err := storage.NewStorage(config)
if err != nil {
log.Fatal("Failed to create storage:", err)
}
defer storage.Close()