# PicoBot 数据库表结构

数据库为 SQLite，默认位于配置目录（`~/.picobot`）`data/` 下的 `picobot.db`，与 workspace 相互独立。

连接启用 WAL、`synchronous=NORMAL`、foreign keys、5 秒 busy timeout，连接池最多 8 个连接。当前 `PRAGMA user_version=11`；启动时会在单个事务内迁移旧库，遇到比程序更新的 schema version 会拒绝启动。

## sessions 表

会话表，一个 session 对应一个 (channel, chat_id, dialog_id) 组合。

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | TEXT PK | session ID，格式 `<channel>:<chat_id>:<dialog_id>` |
| `channel` | TEXT | 渠道名称 |
| `chat_id` | TEXT | 聊天/群组标识 |
| `dialog_id` | TEXT | 对话标识 |
| `title` | TEXT | 会话标题（默认 "新对话"） |
| `created_at` | INTEGER | 创建时间（Unix 毫秒） |
| `last_active_at` | INTEGER | 最后活跃时间（Unix 毫秒） |
| `message_count` | INTEGER | 消息计数 |
| `routing_info` | TEXT | 路由信息 |
| `archived_at` | INTEGER | 归档时间（Unix 毫秒），NULL 表示未归档 |
| `deleted_at` | INTEGER | 软删除时间戳 |
| `last_consolidated_at` | INTEGER | 上次记忆归并时间 |
| `last_compressed_message_at` | INTEGER | 最近 checkpoint 时间戳（兼容/诊断字段，不作为恢复边界） |
| `active_context_checkpoint_id` | TEXT | 当前 Provider 历史投影使用的 checkpoint ID；NULL 表示使用全部原始消息 |
| `context_generation` | INTEGER | checkpoint CAS 提交代；历史清空/改写时递增并清除活动指针 |
| `delivery_context` | TEXT | 渠道声明的可跨 Turn 复用投递上下文 JSON（如飞书 thread/root 身份）；一次性 reply/reaction ID 永不写入 |
| `delivery_context_updated_at` | INTEGER | delivery_context 最后更新时间 |

`session_turn_usage` 以 `turn_id` 幂等保存已提交 Turn 的 Provider usage，包括累计输入、输出、缓存输入、请求数和最后一次请求的 prompt tokens。它与 Turn 消息批次在同一事务中提交，供 WebUI 状态栏和 `/info` 使用；升级前历史无法可靠回填，因此统计起点以首条 usage 记录为准。

`(channel, chat_id, dialog_id)` 唯一。普通列表排除 `deleted_at`；是否包含归档记录由查询参数决定。

## messages 表

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | TEXT PK | 消息 UUID |
| `session_id` | TEXT FK | 所属会话，外键关联 sessions(id) |
| `seq` | INTEGER | 消息序号 |
| `role` | TEXT | 角色: user / assistant / tool / system |
| `content` | TEXT | 消息内容 |
| `media_refs` | TEXT | 多媒体引用 JSON |
| `tool_call_id` | TEXT | 工具调用 ID |
| `tool_name` | TEXT | 工具名称 |
| `tool_calls` | TEXT | 工具调用参数 JSON |
| `source` | TEXT | 消息来源（跨会话消息时标记来源 session_id） |
| `created_at` | INTEGER | 创建时间（Unix 毫秒） |
| `reasoning_content` | TEXT | 可展示的模型 reasoning（如有） |
| `provider_state` | TEXT | Provider 私有回放状态 JSON；只回放给匹配 Provider，不下发客户端或 Channel |
| `turn_id` | TEXT | 产生该消息的活动 Turn ID |
| `iteration` | INTEGER | Agent 工具循环中的迭代序号 |
| `completion_status` | TEXT | `completed` / `cancelled` / `interrupted`，旧数据默认 completed |
| `client_visibility` | TEXT | `visible` / `hidden`，默认 visible；hidden 只供模型回放（continuation 内部触发），客户端历史/投影/投递一律过滤 |
| `turn_origin` | TEXT | `user` / `agent_continuation` / `scheduled`，默认 user；客户端据此渲染"后台结果处理"标签而不创建用户气泡 |

`(session_id, seq)` 有唯一索引，防止并发写入重复序号。物理删除 session 会通过外键级联删除 messages；普通对话删除使用 `deleted_at` 软删除，因此保留关联行。索引 `(session_id, client_visibility, seq)` 支撑按可见性分层查询。

## context_checkpoints 表（schema v10）

checkpoint 只保存累计摘要和精确 raw-tail 边界，不复制或删除原始消息。每个 Session 的 `active_context_checkpoint_id` 最多指向其中一行；历史行保留用于审计。

| 字段 | 说明 |
|------|------|
| `id` | checkpoint ID，主键 |
| `session_id` / `generation` | 所属 Session 与单调提交代；组合唯一 |
| `parent_checkpoint_id` | 上一个累计 checkpoint（审计链） |
| `summary` | 不含 Provider 私有 reasoning 的累计摘要 |
| `first_retained_seq` | Provider 原样保留尾部的第一条 durable seq |
| `source_max_seq` | 生成候选时快照的最大 seq |
| `trigger_reason` | manual / auto / overflow |
| `provider_kind` / `model` | 生成摘要的 Provider/model |
| `tokens_before` / `tokens_after` | 候选验证和诊断数据 |
| `degraded` | 是否为 Provider overflow 或发送前硬超限的确定性无语义降级 |
| `created_at` | 创建时间 |

checkpoint 插入、Session 活动指针更新与 `context_generation` 递增在同一事务内完成。`/clear` 原子删除 messages 并使活动 checkpoint 失效；只有物理删除 Session 才会通过外键级联删除全部 checkpoint，普通 `/delete` 软删除会保留 checkpoint 行。

## agent_runs 表（schema v8，Agent 编排）

每次具名委托（foreground 与 background 一致）先落库再执行；`execution_id` 条件更新保证迟到结果丢弃。批量委托只是多个 run 的集合，不再存在组头（schema v7 的 `agent_run_groups` 表已删除）。

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | TEXT PK | run ID |
| `root_session_id` | TEXT | 根会话 |
| `parent_run_id` | TEXT FK | 父 run（RESTRICT），NULL 表示 Root 直接委托 |
| `caller_agent_id` / `caller_scope_id` | TEXT | 调用方身份；Root 的 caller_scope_id 固定 `"ROOT"` |
| `agent_id` / `definition_hash` / `provider_profile` | TEXT | Definition 快照（绑定运行代，运行中不热切换） |
| `provider_name` / `model_id` | TEXT | Provider 与模型 |
| `mode` | TEXT | foreground / background |
| `depth` | INTEGER | 委托深度（>=1） |
| `plan_item_id` | TEXT | 绑定计划子项（接纳时原子领取） |
| `execution_id` | TEXT | 执行尝试 ID，唯一索引 |
| `task` / `context_json` | TEXT | 任务与调用方上下文 |
| `budget_json` | TEXT | 树级剩余预算 |
| `signal_contract_json` / `signal_delivery` | TEXT | Definition 信号契约快照与投递 lane（queue/steer） |
| `status` | TEXT | queued / running / waiting_children / completed / failed / timed_out / cancelled / interrupted |
| `result` / `error` | TEXT | 终态完整结果/错误（get_result 与 tool 结果同源） |
| `prompt_tokens` / `completion_tokens` / `cost` | INTEGER/REAL | Provider usage |
| `tool_calls_count` / `iterations` | INTEGER | 执行统计 |
| `runtime_generation` / `attempt` | INTEGER | 运行代与重试次数 |
| `completion_slot_reserved` | INTEGER | background 完成槽预留 |
| `deadline_at` / `started_at` / `finished_at` / `created_at` / `updated_at` | INTEGER | 时间线 |
| `revision` | INTEGER | 客户端投影修订号 |

索引：`execution_id` 唯一、`(root_session_id, caller_scope_id, idempotency_key)` 部分唯一、`(root_session_id, created_at DESC)`、`(parent_run_id, created_at)`、`(runtime_generation, status, deadline_at)`（恢复扫描）。

## agent_session_state 表（schema v8，Agent 编排）

每根会话一行，inbox 容量与客户端 revision 的权威计数：

| 字段 | 说明 |
|------|------|
| `root_session_id` | TEXT PK |
| `revision` | 单调客户端投影修订号 |
| `pending_event_count` | 未消费事件数（容量条件更新） |
| `reserved_completion_slots` | 已接纳 background run 预留的完成槽 |
| `updated_at` | 最后更新时间 |

容量判断在同一写事务内做条件 `UPDATE`（`pending + reserved + 新增 <= 上限`），杜绝并发 `COUNT(*)` 漂移。

## agent_inbox_events 表（schema v8，Agent 编排）

background 完成/信号投递的唯一事实源：`pending → leased → admitted → consumed`，失败按 token 释放回 pending，超限进 dead-letter，崩溃靠 lease 过期恢复。

| 字段 | 说明 |
|------|------|
| `id` | TEXT PK |
| `root_session_id` | TEXT | 根会话 |
| `run_id` | TEXT FK NOT NULL（RESTRICT） |
| `event_type` | signal / completion |
| `event_key` | 去重键（signal 含冷却窗口 id） |
| `delivery` | queue / steer |
| `requires_continuation` | 是否反向启动 continuation Turn（cancel 产物为 false） |
| `severity` / `payload_json` | 信号级别与结构化载荷（completion 含 signal_ids） |
| `status` | pending / leased / admitted / consumed / superseded / dead_letter |
| `attempt_count` / `lease_token` / `lease_until` / `next_attempt_at` | 投递尝试与租约 |
| `admitted_turn_id` | steer 事件接纳的 Turn（/stop 按此条件释放） |
| `last_error` | 最近失败原因 |
| `revision` | 投影修订号 |
| `updated_at` | 最后更新时间 |
| `created_at` / `consumed_at` / `superseded_at` / `dead_lettered_at` / `fallback_notified_at` / `fallback_suppressed_reason` | 状态时间线 |

`(run_id, event_type, event_key)` 唯一（signal 冷却窗去重）。消费/死信会同步递减 `agent_session_state.pending_event_count`。

## task_plans / task_items 表

`task_plans` 保存 session 级任务计划，通过部分唯一索引保证每个 session 最多一个 `status='active'` 的计划。`version` 在任何子项变化时递增，用于 WebSocket 快照排序和乐观并发检查。

`task_items` 以 `(plan_id, id)` 为复合主键，因此每个计划都可使用 `T1`、`T2` 等稳定显示 ID。子项状态为 `pending`、`in_progress`、`completed` 或 `blocked`；`executor_kind` 和 `execution_id` 将并行子 Agent 执行绑定到具体子项。只有 active plan 且 execution ID 匹配时，异步结果才能提交。

## memories 表

长期记忆存储。

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | TEXT PK | 记忆 UUID |
| `key` | TEXT UNIQUE | 记忆唯一键 |
| `content` | TEXT | 记忆内容 |
| `category` | TEXT | 类别: knowledge / timeline |
| `importance` | REAL | 重要性权重 (0-1) |
| `session_id` | TEXT | 关联会话 |
| `created_at` | TEXT | 创建时间 |
| `updated_at` | TEXT | 更新时间 |

配套 FTS5 全文索引虚拟表 `memory_fts(key, content)`，用于关键词搜索，通过触发器自动同步。

## scheduled_jobs 表

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | TEXT PK | 任务 UUID |
| `name` | TEXT | 任务名称 |
| `schedule` | TEXT | 调度规则 JSON（at/every/cron） |
| `prompt` | TEXT | 任务提示词 |
| `agent_id` | TEXT | 可选命名 Agent；NULL 表示 Root |
| `channel` | TEXT | 目标渠道 |
| `chat_id` | TEXT | 目标对话 |
| `delivery_policy` | TEXT | `always` / `on_alert` / `never` |
| `enabled` | INTEGER | 是否启用 (1/0) |
| `next_run_at` | INTEGER | 下次执行时间 |
| `last_run_at` | INTEGER | 上次执行时间 |
| `last_outcome` | TEXT | 最近结构化结果：ok/alert/failed/refused/unknown |
| `locked_at` | INTEGER | 本次领取时间 |
| `lock_owner` | TEXT | 本次 occurrence 的唯一 owner token |
| `lease_until` | INTEGER | 租约到期时间 |
| `created_at` | INTEGER | 创建时间（Unix 毫秒） |
| `updated_at` | INTEGER | 更新时间（Unix 毫秒） |

Scheduler 在领取事务中插入 JobRun、快照执行/投递字段并推进下次时间。`At` 在领取时立即禁用；执行失败或崩溃不重放同一个 occurrence。

## job_runs 表

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | INTEGER PK | 自增 ID |
| `job_id` | TEXT FK | 关联任务，外键关联 scheduled_jobs(id) |
| `scheduled_for` | INTEGER | 本 occurrence 原计划时间 |
| `agent_run_id` | TEXT FK | 顶层 AgentRun 审计记录 |
| `agent_id` | TEXT | claim-time Agent 快照 |
| `delivery_policy` | TEXT | claim-time 投递策略快照 |
| `target_channel` / `target_chat_id` | TEXT | claim-time 目标快照 |
| `target_session_id` | TEXT | 首次投递时固定的目标 dialog |
| `started_at` | INTEGER | 开始时间 |
| `finished_at` | INTEGER | 结束时间 |
| `status` | TEXT | claimed/running/completed/failed/timed_out/cancelled/interrupted/unknown |
| `outcome` | TEXT | ok/alert/failed/refused/unknown；与 status 有联合约束 |
| `message` | TEXT | 面向用户的结构化结果 |
| `diagnostic` | TEXT | 有界内部诊断 |
| `duration_ms` | INTEGER | 耗时（毫秒） |
| `delivery_status` | TEXT | awaiting_result/not_requested/suppressed/pending/delivering/delivered/failed |
| `delivery_attempts` | INTEGER | 持久化投递尝试次数，最多 3 次 |
| `delivery_next_attempt_at` | INTEGER | 瞬态失败后的退避时间 |
| `delivery_lease_owner` / `delivery_lease_until` | TEXT / INTEGER | outbox 领取租约 |
| `delivery_error` | TEXT | 清洗后的投递失败摘要 |

JobRun 是执行结果和投递状态的唯一权威。顶层 AgentRun 与 JobRun 终态、Job 最近摘要和租约释放原子提交；启动恢复将遗留运行归为 `unknown`，不会自动重跑。

## llm_calls 表

记录所有 LLM API 调用的请求/响应详情，自动保留最近 1000 条。

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | INTEGER PK | 自增 ID |
| `created_at` | INTEGER | 调用时间 |
| `provider` | TEXT | 提供商类型 |
| `model` | TEXT | 模型名称 |
| `request_body` | TEXT | 请求摘要 JSON；旧记录可能是完整请求体 |
| `response_body` | TEXT | 响应体 JSON |
| `error` | TEXT | 错误信息 |
| `duration_ms` | INTEGER | 耗时（毫秒） |

旧数据中的 `request_body`/`response_body` 可能包含用户内容，排障和导出数据库时应按敏感数据处理。新 Provider 请求的 `request_body` 只保存模型、消息数、工具数和 stream 标志等摘要；错误响应仍可能包含服务端回显内容。
