agentprof 的持久化层只有一个文件 + 一个 enum:agentprof_storage::Db 是 rusqlite::Connection 的薄封装,开库时自动跑全部嵌入式 migration;StorageMode 枚举(Cache / Store)决定 SQLite 文件落在 $XDG_CACHE_HOME/agentprof/cache.sqlite 还是 $XDG_DATA_HOME/agentprof/store.sqlite。「hybrid」不是「双 DB 同步」,而是「同一套 schema、两种生命周期策略」—— 用户按场景选 mode,agentprof 行为完全一致,只是数据落点不同。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | TEXT PRIMARY KEY | ✓ | Session UUID |
agent | TEXT NOT NULL | ✓ | agent 名(copilot/claude/codex) |
started_at_ms | INTEGER | — | session 起始 ms epoch |
raw_path | TEXT NOT NULL | ✓ | 原 events.jsonl 路径 or "otlp://<id>" |
raw_mtime_ms | INTEGER NOT NULL | ✓ | raw_path 的 mtime |
ingested_at_secs | INTEGER NOT NULL | ✓ | 进入 SQLite 时间戳 |
analysis_report_json | TEXT NOT NULL | ✓ | AnalysisReport 序列化 |
episodes_json | TEXT NOT NULL DEFAULT '{}' | — | Episodes 序列化(M2.1.1 加列) |
- cache mode(默认) = local snapshot:随系统清理可删;不出现在备份计划里;为了「快、零配置、随时可丢」。
- store mode(显式) = 外接 backup volume:用户主动挂载;需要保护;长期累积成趋势。
- dual-path 读(ADR-0018 / ADR-0020)= Time Machine 的「按时间挑最新版本」:
SessionDataSource同时看 SQLite 和 live adapter scan,按raw_mtime挑新的。
「Hybrid」的关键洞察:schema 一样、code path 一样、只有策略不同。这样 cli 子命令不需要写两套读写逻辑,只在配置层做选择。
三种使用模式对比(recon 真实路径)
| 模式 | 何时用 | 落点路径 / 数据策略 |
|---|---|---|
cache(默认)StorageMode::Cache | 本地 dev、单次分析、CI 跑完即弃;想要「随时删都没事」 | $XDG_CACHE_HOME/agentprof/cache.sqlitefallback ~/.cache/agentprof/cache.sqliteauto_prune_days = 30(30 天自动清) |
store(显式)StorageMode::Store | CI 跨 run 累计 / 团队共享 / 长期 7-30 天趋势分析 / OTLP push gateway 接收端 | $XDG_DATA_HOME/agentprof/store.sqlitefallback ~/.local/share/agentprof/store.sqlite用户负责备份; --storage-path 可显式覆写 |
dual-path 读SessionDataSource (ADR-0018/0020) | list / analyze / mcp-waste 默认走这条 — 既看 SQLite 也看 adapter scan | 不是「同时写两个 DB」;是「同时读两个 source」,按 raw_mtime 选最新;--no-cache 降级为纯 adapter scan |
⚠️ Recon 校正:「dual-path」在 agentprof 里指 read-path 融合(ADR-0018 + ADR-0020),不是「写到两个 DB」。OTLP receiver 写的是单一当前 storage(由 --storage-mode 决定 cache 或 store)。aggregate 已接入 dual-path;它需要的 Episodes 数据由 002_episodes_column 这一列承担。
👇 三张卡片:① SQLite schema 真实 DDL(3 表 + episodes 列)· ② cache vs store 决策维度对比 · ③ ADR-0019 摘要 + dual-path 读路径解释。
1 SQLite schema 真实 DDL(migration 001 + 002) 点击展开
crates/agentprof-storage/migrations/001_initial.sql + 002_episodes_column.sql)-- 001_initial.sql (schema_version = 1)
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
agent TEXT NOT NULL,
dominant_model TEXT,
started_at INTEGER,
duration_ms INTEGER,
raw_path TEXT NOT NULL,
raw_mtime INTEGER NOT NULL, -- 驱动 dual-path freshness 比较
total_input_tokens INTEGER,
total_output_tokens INTEGER,
total_cache_read INTEGER,
total_cache_creation INTEGER,
schema_version INTEGER NOT NULL DEFAULT 1,
ingested_at INTEGER NOT NULL,
analysis_report_json TEXT NOT NULL -- 完整 AnalysisReport 序列化
);
CREATE INDEX idx_sessions_started ON sessions(started_at DESC);
CREATE INDEX idx_sessions_agent_started ON sessions(agent, started_at DESC);
CREATE TABLE tools_loaded (
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
tool_name TEXT NOT NULL,
source TEXT NOT NULL, -- Builtin / Mcp / Skill / User / Unknown
call_count INTEGER NOT NULL,
total_duration_ms INTEGER NOT NULL,
tokens INTEGER,
token_source TEXT, -- heuristic / tokenizer / config / sidecar
PRIMARY KEY (session_id, tool_name)
);
CREATE TABLE turn_buckets (
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
turn_index INTEGER NOT NULL,
input_tokens INTEGER,
output_tokens INTEGER,
cache_read INTEGER,
cache_creation INTEGER,
model TEXT,
PRIMARY KEY (session_id, turn_index)
);
-- 002_episodes_column.sql (additive migration, M2.1.1)
ALTER TABLE sessions ADD COLUMN episodes_json TEXT NOT NULL DEFAULT '{}';
开库时自动应用:Db::open_and_migrate(path) 先设 PRAGMA journal_mode=WAL + synchronous=NORMAL + foreign_keys=ON,再按 MIGRATIONS 数组顺序跑全部 SQL。幂等 —— 重开旧库是 no-op。
analysis_report_json blob 慢 5-10x;blob 缺点是不能 SQL 直接 group / filter。折中方案:关键聚合列(total tokens、cache read/creation、dominant_model、started_at)单独成列让 list / aggregate 子命令能直接 SQL 查;per-turn / per-tool 明细有自己的表给 mcp-waste 用;完整 AnalysisReport则 blob 化让 analyze --from-cache 秒级还原渲染。episodes_json 列(M2.1.1 加)是 aggregate 的 escape hatch — 它需要 per-call/per-turn 原始数据。idx_sessions_started(按时间倒序列出最近 N 个 session — list --since 7d)+ idx_sessions_agent_started(同上但限 agent — list --agent claude)。tools_loaded 和 turn_buckets 的复合 PK 已经覆盖按 session_id 的 join,不需要额外索引。2 cache vs store 决策维度对比 点击展开
| 维度 | cache (默认) | store (显式) |
|---|---|---|
| XDG var | XDG_CACHE_HOME | XDG_DATA_HOME |
| fallback | ~/.cache/agentprof/ | ~/.local/share/agentprof/ |
| 文件名 | cache.sqlite | store.sqlite |
| auto-prune | 默认 30 天(T2.7+) | 建议 0(永不自动清,用户管) |
| 能删? | ✅ 随便删,下一次 ingest 重建 | ⚠️ 删了等于丢历史 trend,要先备份 |
| 需要备份? | ❌ 不要 — cache 应该是 derivative | ✅ 用户责任,但 SQLite 文件单一好备 |
| 团队共享? | 不适合 — 每人本地一份 | 可放共享 volume,但读为主写要谨慎 |
| OTLP receiver 目标 | 能用,但更适合 store | ✅ 长期 ingest,serve 子命令要求 store(ADR-0024 D-5) |
决策口诀:「单人单机 dev → cache;CI 或团队或 OTLP push 或 serve → store」。从 cache 升级到 store 不需要数据迁移 —— 重新 analyze --storage-mode store 即可(SQLite 文件可以并存)。
3 ADR-0019 hybrid mode + dual-path 读路径 点击展开
// SessionDataSource 抽象(agentprof-storage::SessionDataSource)
// 关键接口:list_sessions(filter) -> Vec<SessionRef>
// load_episodes(session_id) -> Episodes
//
// 默认实现 fan out 到两个 source:
// 1. SQLite(如果存在)— 通过 raw_mtime 判断新鲜度
// 2. Live adapter scan — fallback 或 freshness 后备
// 取 union,遇到同 id 时按 raw_mtime 取 newer。
这个设计让用户在没显式 ingest 的情况下也能跑 list / analyze —— SQLite 是优化 path,不是必经 path。--no-cache 显式跳过 SQLite I/O,dual-path 就降级成纯 adapter scan(M2.1 给「不想留痕」的 use case 的 escape)。
list、analyze(隐式)、mcp-waste。不走(仅 SQLite):serve(要求 store mode)、db 子命令(直接 SQL)。暂未走:aggregate — 它需要 Episodes 这层数据,当前由 002_episodes_column 列承担,对未 re-ingest 的旧 row 退化为 Episodes::default()(零贡献到 percentile pool)。下一次 ingest 自动补齐。MIGRATIONS 静态数组(db.rs:24)— 按数字前缀顺序、用 rusqlite_migration 跑。新增 migration 永远 append-only,加 NOT NULL DEFAULT '...' 这种 additive 改动;禁止 drop column / rename column。schema_version 列在 sessions 表里保留 — 未来如果 analysis_report_json 反序列化失败,能按 version 走 fallback decoder。rusqlite + 手写 FromRow + 集成测试覆盖。rusqlite 的 bundled feature 还省了「用户先装 libsqlite」的安装摩擦。下一步
本课讲清了 StorageMode 二选一的决策、3 表 schema 的取舍、以及 dual-path 读路径的真实 fan-out 逻辑。下一课「OTLP receiver」深入 agentprof serve --ingest-otlp 背后的 gRPC/HTTP 双栈、4 层防御(ADR-0022),以及 receiver → router → buffer → flush sink → upsert 的真实 pipeline。
📂 相关源码:
agentprof-storage/db.rs
Db
📂 相关源码:
agentprof-storage/config.rs
StorageMode