xiaoxixi d9ad58b84b feat(scheduler): unify scheduled task execution and delivery
Replace the dual task/monitor model, NO_REPLY string protocol, and Agent
self-delivery with a single Scheduled Run path: claim-time JobRun snapshots,
isolated Root/named Agent execution, exactly-once complete_scheduled_run
termination, and Scheduler-owned policy delivery through a persistent outbox.

- SQLite v11: drop job_kind/model/delete_after_run, add job_runs with
  status/outcome joint constraints and delivery lease columns; one-shot
  BEGIN IMMEDIATE migration with atomic rollback.
- Non-blocking JoinSet event loop with bounded run/delivery concurrency;
  terminal commit before any channel I/O; recover unfinished runs as unknown.
- ExecutionOrigin::Scheduled propagates to descendants, completion sink is
  top-level only, background delegation downgrades to foreground.
- Typed delivery receipts, fixed target_session_id, idempotent
  scheduled:<job_run_id> history insert.
- New cron_runs read-only tool; cron_add/update drop kind/model; WebUI and
  Health consume the same JobRun projection.
- Bump version to 1.22.0.
2026-08-21 14:59:02 +08:00

238 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 IDNULL 表示使用全部原始消息 |
| `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`,默认 visiblehidden 只供模型回放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 v8Agent 编排)
每次具名委托foreground 与 background 一致)先落库再执行;`execution_id` 条件更新保证迟到结果丢弃。批量委托只是多个 run 的集合不再存在组头schema v7 的 `agent_run_groups` 表已删除)。
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | TEXT PK | run ID |
| `root_session_id` | TEXT | 根会话 |
| `parent_run_id` | TEXT FK | 父 runRESTRICTNULL 表示 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 信号契约快照与投递 lanequeue/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 v8Agent 编排)
每根会话一行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 v8Agent 编排)
background 完成/信号投递的唯一事实源:`pending → leased → admitted → consumed`,失败按 token 释放回 pending超限进 dead-letter崩溃靠 lease 过期恢复。
| 字段 | 说明 |
|------|------|
| `id` | TEXT PK |
| `root_session_id` | TEXT | 根会话 |
| `run_id` | TEXT FK NOT NULLRESTRICT |
| `event_type` | signal / completion |
| `event_key` | 去重键signal 含冷却窗口 id |
| `delivery` | queue / steer |
| `requires_continuation` | 是否反向启动 continuation Turncancel 产物为 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 | 调度规则 JSONat/every/cron |
| `prompt` | TEXT | 任务提示词 |
| `agent_id` | TEXT | 可选命名 AgentNULL 表示 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 标志等摘要;错误响应仍可能包含服务端回显内容。