📊 agentprof 可视化指南 · 目录 Wiki 11 / 14
Wiki

存储层 hybrid mode

agentprof 的持久化层只有一个文件 + 一个 enumagentprof_storage::Dbrusqlite::Connection 的薄封装,开库时自动跑全部嵌入式 migration;StorageMode 枚举(Cache / Store)决定 SQLite 文件落在 $XDG_CACHE_HOME/agentprof/cache.sqlite 还是 $XDG_DATA_HOME/agentprof/store.sqlite。「hybrid」不是「双 DB 同步」,而是「同一套 schema、两种生命周期策略」—— 用户按场景选 mode,agentprof 行为完全一致,只是数据落点不同。

字段类型必填说明
idTEXT PRIMARY KEYSession UUID
agentTEXT NOT NULLagent 名(copilot/claude/codex)
started_at_msINTEGERsession 起始 ms epoch
raw_pathTEXT NOT NULL原 events.jsonl 路径 or "otlp://<id>"
raw_mtime_msINTEGER NOT NULLraw_path 的 mtime
ingested_at_secsINTEGER NOT NULL进入 SQLite 时间戳
analysis_report_jsonTEXT NOT NULLAnalysisReport 序列化
episodes_jsonTEXT NOT NULL DEFAULT '{}'Episodes 序列化(M2.1.1 加列)
🍎 类比 — 像 macOS Time Machine 的 local snapshot 和外接 backup volume
  • 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.sqlite
fallback ~/.cache/agentprof/cache.sqlite
auto_prune_days = 30(30 天自动清)
store(显式)
StorageMode::Store
CI 跨 run 累计 / 团队共享 / 长期 7-30 天趋势分析 / OTLP push gateway 接收端$XDG_DATA_HOME/agentprof/store.sqlite
fallback ~/.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) 点击展开
📋 真实 schema(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。

🧮 为什么是 3 表 + 1 大 JSON blob,不是「关系正交化」?
关系拆细会膨胀写入路径(一份 session ≈ 50-500 turn + 30-200 tool),SQLite 单事务里大几千 row 的 insert 比一条 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 原始数据。
🔗 索引设计
两个索引覆盖了 99% 的 query pattern:idx_sessions_started(按时间倒序列出最近 N 个 session — list --since 7d)+ idx_sessions_agent_started(同上但限 agent — list --agent claude)。tools_loadedturn_buckets 的复合 PK 已经覆盖按 session_id 的 join,不需要额外索引。
2 cache vs store 决策维度对比 点击展开
维度 cache (默认) store (显式)
XDG varXDG_CACHE_HOMEXDG_DATA_HOME
fallback~/.cache/agentprof/~/.local/share/agentprof/
文件名cache.sqlitestore.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 读路径 点击展开
📜 ADR-0019 摘要 — 为什么不只有一种 mode?
两种用户画像冲突个人 dogfooder要「装一次、随时跑、清盘没顾虑」(cache 语义);团队 / 多月审计要「保住每一份 session、自定路径、不被 cron 清掉」(store 语义)。一种 mode 满足不了两边 —— 默认 cache 太激进会让团队丢数据,默认 store 太保守会让个人用户的 disk 越来越大。ADR-0019 的决策:同一套 schema、同一套 code path,配置层选 mode。OS 已有 XDG 约定区分 cache vs data,agentprof 借用即可,零教育成本。
🔀 dual-path 读路径(ADR-0018 + ADR-0020)
// 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)。

📊 哪些 cli 走 dual-path
listanalyze(隐式)、mcp-waste不走(仅 SQLite)serve(要求 store mode)、db 子命令(直接 SQL)。暂未走aggregate — 它需要 Episodes 这层数据,当前由 002_episodes_column 列承担,对未 re-ingest 的旧 row 退化为 Episodes::default()(零贡献到 percentile pool)。下一次 ingest 自动补齐。
🔄 Migration 策略
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。
🤔 为什么不用 SeaORM / sqlx 这类 ORM?
agentprof 的 schema 只有 3 表 + 一个 blob,没有 join 链 > 2 张表的 query;ORM 引入编译时 cost(sqlx 的 compile-time 校验需要 live DB 或 prepared cache)和运行时 cost(动态 schema reflection),换来的 type safety 已被 rusqlite + 手写 FromRow + 集成测试覆盖。rusqlitebundled 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