Chuyển tới nội dung chính

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_detailscodex_reasoning_items được lưu dưới dạng chuỗi JSON
  • reasoning lư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ảnThay đổi
1Lược đồ ban đầu (phiên, tin nhắn, FTS5)
2Thêm cột finish_reason vào tin nhắn
3Thêm cột title vào phiên
4Thê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)
5Thê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
6Thê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ápVí dụÝ nghĩa
Từ khóadocker deploymentCả 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ẶCdocker OR kubernetesHoặc là kỳ hạn
Boolean KHÔNGpython NOT javaLoạ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 , timestamp
  • snippet — Đ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 ANDhello )

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.