Lưu trữ phiên
Hermes Agent sử dụng cơ sở dữ liệu SQLite ( ~/.hermes/state.db ) để duy trì phiên
siêu dữ liệu, lịch sử tin nhắn đầy đủ và cấu hình mô hình trên CLI và cổng
phiên. Điều này thay thế cách tiếp cận tệp JSONL mỗi phiên trước đó.
Tệp nguồn: hermes_state.py
Tổng quan về kiến trúc
~/.hermes/state.db (SQLite, WAL mode)
├── sessions — Session metadata, token counts, billing
├── messages — Full message history per session
├── messages_fts — FTS5 virtual table for full-text search
└── schema_version — Single-row table tracking migration state
Các quyết định thiết kế chính:
- Chế độ WAL dành cho người đọc đồng thời + một người viết (cổng đa nền tảng)
- Bảng ảo FTS5 để tìm kiếm văn bản nhanh chóng trên tất cả các tin nhắn trong phiên
- Dòng phiên thông qua chuỗi
parent_session_id(phân tách do nén) - Gắn thẻ nguồn (
cli,telegram,discord, v.v.) để lọc nền tảng - Quỹ đạo hàng loạt và quỹ đạo RL KHÔNG được lưu trữ ở đây (các hệ thống riêng biệt)
Lược đồ SQLite
Bảng phiên
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
source TEXT NOT NULL,
user_id TEXT,
model TEXT,
model_config TEXT,
system_prompt TEXT,
parent_session_id TEXT,
started_at REAL NOT NULL,
ended_at REAL,
end_reason TEXT,
message_count INTEGER DEFAULT 0,
tool_call_count INTEGER DEFAULT 0,
input_tokens INTEGER DEFAULT 0,
output_tokens INTEGER DEFAULT 0,
cache_read_tokens INTEGER DEFAULT 0,
cache_write_tokens INTEGER DEFAULT 0,
reasoning_tokens INTEGER DEFAULT 0,
billing_provider TEXT,
billing_base_url TEXT,
billing_mode TEXT,
estimated_cost_usd REAL,
actual_cost_usd REAL,
cost_status TEXT,
cost_source TEXT,
pricing_version TEXT,
title TEXT,
FOREIGN KEY (parent_session_id) REFERENCES sessions(id)
);
CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source);
CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title_unique
ON sessions(title) WHERE title IS NOT NULL;
Bảng tin nhắn
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(id),
role TEXT NOT NULL,
content TEXT,
tool_call_id TEXT,
tool_calls TEXT,
tool_name TEXT,
timestamp REAL NOT NULL,
token_count INTEGER,
finish_reason TEXT,
reasoning TEXT,
reasoning_details TEXT,
codex_reasoning_items TEXT
);
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
Ghi chú:
tool_callsđược lưu trữ dưới dạng chuỗi JSON (danh sách các đối tượng gọi công cụ được tuần tự hóa)reasoning_detailsvàcodex_reasoning_itemsđược lưu dưới dạng chuỗi JSONreasoninglưu trữ văn bản lý luận thô cho các nhà cung cấp hiển thị nó- Dấu thời gian là các dấu phẩy kỷ nguyên Unix (
time.time())
Tìm kiếm toàn văn FTS5
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
content,
content=messages,
content_rowid=id
);
Bảng FTS5 được giữ đồng bộ thông qua ba trình kích hoạt kích hoạt INSERT, UPDATE,
và XÓA bảng messages:
CREATE TRIGGER IF NOT EXISTS messages_fts_insert AFTER INSERT ON messages BEGIN
INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
END;
CREATE TRIGGER IF NOT EXISTS messages_fts_delete AFTER DELETE ON messages BEGIN
INSERT INTO messages_fts(messages_fts, rowid, content)
VALUES('delete', old.id, old.content);
END;
CREATE TRIGGER IF NOT EXISTS messages_fts_update AFTER UPDATE ON messages BEGIN
INSERT INTO messages_fts(messages_fts, rowid, content)
VALUES('delete', old.id, old.content);
INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
END;
Phiên bản lược đồ và di chuyển
Phiên bản lược đồ hiện tại: 6
Bảng schema_version lưu trữ một số nguyên duy nhất. Khi khởi tạo,
_init_schema() kiểm tra phiên bản hiện tại và áp dụng các lần di chuyển theo tuần tự:
| Phiên bản | Thay đổi |
|---|---|
| 1 | Lược đồ ban đầu (phiên, tin nhắn, FTS5) |
| 2 | Thêm cột finish_reason vào tin nhắn |
| 3 | Thêm cột title vào phiên |
| 4 | Thêm chỉ mục duy nhất trên title (Cho phép NULL, không phải NULL phải là duy nhất) |
| 5 | Thêm các cột thanh toán: cache_read_tokens , cache_write_tokens , reasoning_tokens , billing_provider , billing_base_url , billing_mode , estimated_cost_usd , actual_cost_usd , cost_status , cost_source , pricing_version |
| 6 | Thêm cột lý do vào tin nhắn: reasoning , reasoning_details , codex_reasoning_items |
Mỗi lần di chuyển sử dụng ALTER TABLE ADD COLUMN được gói trong thử/ngoại trừ để xử lý
trường hợp cột đã tồn tại (idempotent). Số phiên bản bị thay đổi sau
mỗi khối di chuyển thành công.
Viết Xử lý tranh chấp
Nhiều quy trình Hermes (cổng + phiên CLI + tác nhân cây công việc) chia sẻ một
state.db . Lớp SessionDB xử lý tranh chấp ghi với:
- Thời gian chờ SQLite ngắn (1 giây) thay vì 30 giây mặc định
- Thử lại ở cấp độ ứng dụng với jitter ngẫu nhiên (20-150 mili giây, tối đa 15 lần thử lại)
- BẮT ĐẦU NGAY LẬP TỨC giao dịch để giải quyết tranh chấp khóa bề mặt khi bắt đầu giao dịch
- Điểm kiểm tra WAL định kỳ cứ sau 50 lần ghi thành công (chế độ THỤ ĐỘNG)
Điều này tránh được "hiệu ứng đoàn xe" trong đó sự chờ đợi nội bộ mang tính quyết định của SQLite khiến tất cả người viết cạnh tranh phải thử lại ở những khoảng thời gian như nhau.
_WRITE_MAX_RETRIES = 15
_WRITE_RETRY_MIN_S = 0.020
# 20ms
_WRITE_RETRY_MAX_S = 0.150
# 150ms
_CHECKPOINT_EVERY_N_WRITES = 50
Hoạt động chung
Khởi tạo
from hermes_state import SessionDB
db = SessionDB()
# Default: ~/.hermes/state.db
db = SessionDB(db_path=Path("/tmp/test.db"))
# Custom path
Tạo và quản lý phiên
# Create a new session
db.create_session(
session_id="sess_abc123",
source="cli",
model="anthropic/claude-sonnet-4.6",
user_id="user_1",
parent_session_id=None,
# or previous session ID for lineage
)
# End a session
db.end_session("sess_abc123", end_reason="user_exit")
# Reopen a session (clear ended_at/end_reason)
db.reopen_session("sess_abc123")
Lưu trữ tin nhắn
msg_id = db.append_message(
session_id="sess_abc123",
role="assistant",
content="Here's the answer...",
tool_calls=[{"id": "call_1", "function": {"name": "terminal", "arguments": "{}"}}],
token_count=150,
finish_reason="stop",
reasoning="Let me think about this...",
)
Truy xuất tin nhắn
# Raw messages with all metadata
messages = db.get_messages("sess_abc123")
# OpenAI conversation format (for API replay)
conversation = db.get_messages_as_conversation("sess_abc123")
# Returns: [{"role": "user", "content": "..."}, {"role": "assistant", ...}]
Tiêu đề phiên
# Set a title (must be unique among non-NULL titles)
db.set_session_title("sess_abc123", "Fix Docker Build")
# Resolve by title (returns most recent in lineage)
session_id = db.resolve_session_by_title("Fix Docker Build")
# Auto-generate next title in lineage
next_title = db.get_next_title_in_lineage("Fix Docker Build")
# Returns: "Fix Docker Build #2"
Tìm kiếm toàn văn
Phương thức search_messages() hỗ trợ cú pháp truy vấn FTS5 với tính năng tự động
vệ sinh đầu vào của người dùng.
Tìm kiếm cơ bản
results = db.search_messages("docker deployment")
Cú pháp truy vấn FTS5
| Cú pháp | Ví dụ | Ý nghĩa |
|---|---|---|
| Từ khóa | docker deployment | Cả hai thuật ngữ (ẩn AND) |
| Cụm từ được trích dẫn | "exact phrase" | Đối sánh cụm từ chính xác |
| Boolean HOẶC | docker OR kubernetes | Hoặc là kỳ hạn |
| Boolean KHÔNG | python NOT java | Loại trừ thuật ngữ |
| Tiền tố | deploy* | Trận đấu tiền tố |
Tìm kiếm đã lọc
# Search only CLI sessions
results = db.search_messages("error", source_filter=["cli"])
# Exclude gateway sessions
results = db.search_messages("bug", exclude_sources=["telegram", "discord"])
# Search only user messages
results = db.search_messages("help", role_filter=["user"])
Định dạng kết quả tìm kiếm
Mỗi kết quả bao gồm:
id,session_id,role,timestampsnippet— Đoạn mã do FTS5 tạo với các điểm đánh dấu>>>match<<<context— 1 tin nhắn trước và sau trận đấu (nội dung được cắt ngắn còn 200 ký tự)source,model,session_started— từ phiên chính
Phương thức _sanitize_fts5_query() xử lý các trường hợp đặc biệt:
- Loại bỏ các trích dẫn chưa từng có và các ký tự đặc biệt
- Gói các thuật ngữ có dấu gạch nối trong dấu ngoặc kép (
chat-send→"chat-send") - Loại bỏ các toán tử boolean lơ lửng (
hello AND→hello)
Dòng dõi phiên
Phiên có thể tạo thành chuỗi thông qua parent_session_id . Điều này xảy ra khi bối cảnh
quá trình nén sẽ kích hoạt sự phân chia phiên trong cổng.
Truy vấn: Tìm dòng phiên
-- Find all ancestors of a session
WITH RECURSIVE lineage AS (
SELECT * FROM sessions WHERE id = ?
UNION ALL
SELECT s.* FROM sessions s
JOIN lineage l ON s.id = l.parent_session_id
)
SELECT id, title, started_at, parent_session_id FROM lineage;
-- Find all descendants of a session
WITH RECURSIVE descendants AS (
SELECT * FROM sessions WHERE id = ?
UNION ALL
SELECT s.* FROM sessions s
JOIN descendants d ON s.parent_session_id = d.id
)
SELECT id, title, started_at FROM descendants;
Truy vấn: Phiên gần đây có bản xem trước
SELECT s.*,
COALESCE(
(SELECT SUBSTR(m.content, 1, 63)
FROM messages m
WHERE m.session_id = s.id AND m.role = 'user' AND m.content IS NOT NULL
ORDER BY m.timestamp, m.id LIMIT 1),
''
) AS preview,
COALESCE(
(SELECT MAX(m2.timestamp) FROM messages m2 WHERE m2.session_id = s.id),
s.started_at
) AS last_active
FROM sessions s
ORDER BY s.started_at DESC
LIMIT 20;
Truy vấn: Thống kê sử dụng token
-- Total tokens by model
SELECT model,
COUNT(*) as session_count,
SUM(input_tokens) as total_input,
SUM(output_tokens) as total_output,
SUM(estimated_cost_usd) as total_cost
FROM sessions
WHERE model IS NOT NULL
GROUP BY model
ORDER BY total_cost DESC;
-- Sessions with highest token usage
SELECT id, title, model, input_tokens + output_tokens AS total_tokens,
estimated_cost_usd
FROM sessions
ORDER BY total_tokens DESC
LIMIT 10;
Xuất và dọn dẹp
# Export a single session with messages
data = db.export_session("sess_abc123")
# Export all sessions (with messages) as list of dicts
all_data = db.export_all(source="cli")
# Delete old sessions (only ended sessions)
deleted_count = db.prune_sessions(older_than_days=90)
deleted_count = db.prune_sessions(older_than_days=30, source="telegram")
# Clear messages but keep the session record
db.clear_messages("sess_abc123")
# Delete session and all messages
db.delete_session("sess_abc123")
Vị trí cơ sở dữ liệu
Đường dẫn mặc định: ~/.hermes/state.db
Điều này bắt nguồn từ hermes_constants.get_hermes_home() giải quyết
~/.hermes/ theo mặc định hoặc giá trị của biến môi trường HERMES_HOME.
Tệp cơ sở dữ liệu, tệp WAL ( state.db-wal ) và tệp bộ nhớ dùng chung
( state.db-shm ) đều được tạo trong cùng một thư mục.