screenpipe Pipe 执行可靠性架构解析:从内存态调度、边缘故障清单到 DB 持久化方案
【免费下载链接】screenpipeYC (S26) | Open Computer History | Record your screen continuously locally and provide context to your agents (Claude, Codex, Openclaw, Hermes, Runner...)项目地址: https://gitcode.com/GitHub_Trending/sc/screenpipe
本篇技术指南围绕 screenpipe 的Pipe 执行可靠性规范(Pipe Execution Reliability Spec)展开,梳理当前 Pipe 调度与执行架构的全部状态依赖、九大类边缘故障(模型解析、电源事件、日志、进程管理、调度、配置、网络、聊天会话与 Tauri 生命周期)的成因与修复要求,并完整解读其提出的pipe_executions/pipe_scheduler_state两表持久化方案、执行状态机与 P0–P3 实施优先级。读者读完可以掌握 screenpipe 内部 Pipe 调度器的真实工作方式、所有已知可靠性短板,以及仓库中已经落地的 SQLite 迁移、睡眠/唤醒监控、速率限制重试等实现的对应位置。
一、当前架构:一切状态都在内存里
规范首先给出了当前实现的架构基线,全部位于crates/screenpipe-core/src/pipes模块(主要在 mod.rs):
- 所有 Pipe 状态都保存在进程内内存中,典型结构是
Arc<Mutex<HashMap>>。规范中明确指出运行中的 Pipe 映射为running: Arc<Mutex<HashMap<String, ExecutionHandle>>>(对应 mod.rs)。 - 执行日志以 JSON 文件形式写入
~/.screenpipe/pipes/{name}/logs/。 - 一个全局信号量串行化所有 Pipe 执行:
Semaphore::new(1),在调度循环中即execution_semaphore = Arc::new(tokio::sync::Semaphore::new(1))(见 mod.rs)。 - Pi Agent 子进程使用
wait_with_output()且没有任何超时。对应实现位于 pi.rs,即let output = child.wait_with_output().await?;。 - PID 追踪存在缺陷:spawn 时通过
child.id()捕获了真实 PID(pi.rs),但在 running map 中存入的是ExecutionHandle { pid: 0 },真实 PID 从未回写。
注:规范文件头部标注为Drifting(2026-05-22 后未再全量核对),并提示以
crates/screenpipe-core/src/pipes为准,可用bun scripts/check-doc-freshness.ts检查漂移程度。以下内容均以「规范描述 + 仓库现状对照」的方式呈现。
单次 Pipe 运行会触及的状态依赖
规范用一棵依赖树说明了「一次 Pipe 运行会触碰哪些可变状态」:
pipe.md (config + prompt) ├── store.bin (AI preset → model + provider) │ └── may be written by Tauri app concurrently ├── ~/.pi/agent/models.json (provider config, merged on every run) ├── ~/.pi/agent/auth.json (API keys, merged on every run) ├── pi binary (found via PATH/known locations) ├── screenpipe API at localhost:3030 (queried by pipe during execution) │ └── SQLite DB (OCR, audio, UI data the pipe reads) ├── LLM provider API (external network call) │ └── API key (from auth.json or SCREENPIPE_API_KEY env) ├── ./output/ directory (pipe writes results here) └── ./logs/ directory (execution logs written after completion)规范强调:这些依赖中的每一个都可能在中途独立失败。这正是整份可靠性规范的存在理由——任何一环故障都不应让调度器崩溃或让用户面对无法理解的神秘失败。
二、完整边缘案例清单(Edge Case Inventory)
规范按九大主题(A–I)枚举了全部已知边缘场景,每个场景都遵循「Current(现状)→ Required(要求)」的结构。以下完整继承并逐条展开。
A. 模型与 Provider 解析(Model & Provider Resolution)
A1. Pipe 运行时用户在 UI 切换模型
- 现状:preset 在排队时解析(对应
resolve_preset()调用点,如 mod.rs),运行中的执行不受影响。 - 风险:
resolve_preset()读取store.bin时没有加锁。若 Tauri 恰好在调度器读取的瞬间写入,会出现半读 → JSON 解析失败 → 返回None→静默回退到 pipe.md 默认值。 - 要求:原子化读取
store.bin(先读入缓冲区再解析),或将 preset 也迁移到 SQLite。
A2. 排队与执行之间模型被切换
- 现状:当前没有真正的队列,preset 在
executor.run()前才解析。但一旦引入 DB 队列,Pipe 可能在队列中等待数分钟,期间用户可能改了 preset。 - 要求:在排队时就把解析好的 model/provider 快照进执行记录行,执行时使用快照而非重新实时解析。
A3. Pipe 引用的 preset 被删除
- 现状:
resolve_preset()返回None→ 静默回退到 pipe.md 的model:字段。用户以为在用 Claude Opus,实际 Pipe 可能跑在 Haiku(pipe.md 默认值)上。 - 要求:preset 不存在时直接失败,给出明确错误:"Preset 'xyz' no longer exists. Update pipe config or set a new default."。
A4. 模型名拼写错误或 provider 上没有该模型
- 现状:Pi 子进程收到 LLM API 的错误,stderr 被捕获但截断到 5KB,记为泛化失败。
- 要求:从 stderr 解析常见 LLM API 错误(
model_not_found、invalid_api_key、rate_limited),在 API 响应中输出结构化error_type,让 UI 给出可操作的提示。
A5. Provider 需要 API key 但 key 缺失/过期
- 现状:
ensure_pi_config()会写入SCREENPIPE_API_KEY环境变量并合并 auth.json。但如果 screenpipe cloud token 过期,Pi 收到 401,stderr 显示 "unauthorized",用户看到的却是泛化失败。 - 要求:识别鉴权错误,在执行错误中输出 "API key expired" 或 "API key missing for provider X"。
A6. 自定义 Provider URL 变更或不可达
- 现状:Provider URL 来自 preset → models.json 合并。URL 不可达时,Pi 会挂在 HTTP 超时上(取决于 Pi 内部 HTTP 客户端,可能是 30s–2min)。
- 要求:这是通用超时问题的子集,**执行超时(提议 5 分钟)**可以覆盖。
A7. Ollama 模型未拉取 / 未运行
- 现状:provider=ollama 时若 Ollama 未运行或模型未拉取,Pi 立即连接拒绝失败,但错误埋在 stderr 里。
- 要求:预检(pre-flight check):provider=ollama 时先
curl http://localhost:11434/api/tags确认可达,再以结构化错误输出 "Ollama not running" 或 "Model X not found in Ollama"。
A8.store.bin损坏 / 半写状态
- 现状:
serde_json::from_str()返回Err→resolve_preset()返回None→ 静默回退。 - 要求:记录警告 "store.bin is corrupted, using pipe defaults";创建 bootstrap store.bin 时考虑原子写(先写临时文件再 rename)。
A9. Provider 映射不完整
- 现状:只映射
pi、native-ollama、openai、custom四种。任何其他 provider 字符串 →None→ 不传 provider 给 Pi → Pi 用自己的默认值。 - 要求:要么对未知 provider 显式失败,要么原样透传给 Pi 自行处理。
B. 计算机重启 / 睡眠 / 电源(Computer Restart / Sleep / Power)
B1. Pipe 运行时计算机重启
- 现状:进程被 OS 杀死,内存态全部丢失,没有任何执行记录;Pipe 可能正写到一半输出文件 → 部分/损坏输出。
- 要求:DB 行
status='running'在重启后存续;启动时检测孤儿行并标记为failed(error = "interrupted by system restart");检查部分输出文件并清理或标记不完整。
B2. macOS 睡眠期间执行
- 现状:进程被 OS 挂起。唤醒后进程恢复但墙钟已跳变,LLM API 连接大概率超时/关闭,Pi 子进程可能收到 broken pipe 或 connection reset。
- 要求:唤醒后检查运行中执行是否存活(PID 检查);进程在睡眠期间死掉则标记 failed;进程恢复但挂起(唤醒后 >60s 无输出)则超时并 kill;可考虑利用已有的
sleep_monitor.rs检测睡眠/唤醒事件,在睡眠前主动 kill 运行中的 Pipe。
B3. macOS App Nap 节流 screenpipe
- 现状:App Nap 可能挂起 server 进程 → 调度器停摆 → Pipe 不按计划运行;解除 Napping 后调度恢复,但错过的窗口已消失。
- 要求:Tauri App 已通过
NSProcessInfoactivity assertion 缓解;但 CLI(screenpipe二进制)没有此保护,需在文档中说明 App Nap 可能影响调度可靠性。
B4. 计算机时钟跳变(NTP 同步、时区变化、夏令时)
- 现状:调度器使用
Utc::now()和Local::now(),last_run是内存 HashMap。时钟向前跳 → 多个 Pipe 突然"到期";时钟向后跳 → 刚跑过的 Pipe 看起来没跑(重复执行)。 - 要求:间隔调度改用单调时钟;
last_run存 DB(重启存活、单一事实来源);catch-up 上限:时钟跳变后若积压超过 3 次,只补跑一次;cron 调度仍用墙钟(cron 的本义就是墙钟)。
B5. 笔记本合盖 → WiFi 断开 → LLM API 响应中途断连
- 现状:Pi 阻塞在 HTTP 读上,最终 OS TCP 超时(可能数分钟)后 Pi 报错,Pipe 失败。
- 要求:属于超时子集。5 分钟执行超时会在 TCP 超时前 kill 掉 Pipe;但 LLM 的部分响应会丢失,建议(未来、较复杂)Pi 支持部分工作 checkpoint。
C. 日志与输出(Logs & Output)
C1. 日志目录不存在
- 现状:
std::fs::create_dir_all(&log_dir)自动创建,静默处理。 - 状态:OK。
C2. 写日志时磁盘已满
- 现状:
std::fs::write()返回 Err 被let _ =忽略(mod.rs 附近),日志丢失。 - 要求:至少用 tracing 打到 stderr;DB 场景 SQLite 会返回 SQLITE_FULL,需优雅处理、不崩溃。
C3. 日志无限累积
- 现状:无轮转,每次运行都在
~/.screenpipe/pipes/{name}/logs/新建一个 JSON 文件,数月后成千上万个小文件。 - 要求:DB 场景默认清理 30 天前的执行记录(可配置);启动时删除 7 天前的 JSON 日志;内存缓存已封顶 50(良好)。
C4. 日志中的敏感数据
- 现状:stdout/stderr 可能包含 OCR 文本、转录、个人数据,以明文 JSON 文件存储。
- 要求:JSON 日志文件应设置受限权限(0600);DB 使用与主 screenpipe DB 相同的文件权限;考虑日志脱敏选项(剥离 API key、PII),但复杂,放 Phase 3+。
C5. 日志截断丢失关键调试信息
- 现状:stdout 截断到 10KB、stderr 到 5KB。Pi Agent 可能产生大量输出。
- 要求:DB 存完整输出(SQLite TEXT 无实际限制);API 分页获取输出(
GET /pipes/:id/executions/:exec_id/output?offset=0&limit=10000);仅对内存缓存与列表接口保留截断。
C6. 崩溃/超时运行的输出文件
- 现状:Pipe 崩溃前可能已在
./output/写入部分文件,无清理;下次运行可能追加或覆盖。 - 要求:每次执行写入带时间戳的子目录
./output/{execution_id}/;超时/崩溃时目录保留但标记不完整;不自动删除——用户可能想要部分结果。
C7. 并发日志写入
- 现状:手动运行与定时运行不会重叠(semaphore=1)。但若信号量提高,同一 Pipe 的两次运行可能写同一个日志目录。
- 要求:采用执行级输出目录(见 C6);日志写入 DB 行(无文件冲突)。
C8. 无法将 Pipe 输出与 screenpipe 数据关联
- 现状:Pipe 带时间范围查询 screenpipe API,输出写文件,无法建立"Pipe 看到了哪些帧/音频"与"它产出了什么"之间的链接。
- 要求(未来):在执行行中保存时间范围与查询参数,实现"展示本次 Pipe 运行基于哪些数据"。
D. 进程管理(Process Management)
D1. PID 追踪错误
- 现状:
child.id()在 pi.rs 捕获并放入AgentOutput.pid;但ExecutionHandle { pid: 0 }在executor.run()之前就插入 running map(对应 mod.rs 的running.insert(name.to_string(), handle))。真实 PID 只在run()返回后才可得——对取消操作来说太晚了。 - 要求:让
AgentExecutor::run()通过回调/通道上报 PID;或返回(PID, Future<AgentOutput>);或拆分为spawn() → ExecutionHandle与wait(handle) → AgentOutput;spawn 后立即把 PID 写入 DB 行。
D2. Pi 派生子进程
- 现状:Pi 可能 spawn bash 命令、npm 进程等,
kill_process()只杀 Pi 自身 PID,子进程成为孤儿。 - 要求:使用进程组:
cmd.process_group(0)(Rust unstable)或 Unixsetsid;杀整个进程组kill(-pgid, SIGTERM);Windows 使用Job Objects分组子进程。
D3. Kill 发送 SIGTERM 但进程无视
- 现状:只发一次
kill -TERM,无后续动作。若 Pi 卡在 syscall 里无视 SIGTERM,进程永久运行。 - 要求:SIGTERM → 等 5s → SIGKILL;无论 kill 是否成功都更新状态为
cancelled或timed_out;无论如何都释放信号量。
D4. Pi 二进制在找到后又被移除
- 现状:
is_available()在运行前检查find_pi_executable()(见 pi.rs),但检查与run()之间 Pi 可能被卸载(bun uninstall、PATH 变化),cmd.spawn()返回 Err。 - 要求:已处理——
spawn()错误会传播到PipeRunLog { success: false },但错误信息应明确:"pi binary not found at {path}"。
D5. Pi 二进制版本不匹配
- 现状:无版本检查。Pi 通过
bun add -g @earendil-works/pi-coding-agent安装且未锁定版本,自动更新可能破坏兼容性。 - 要求:在
ensure_installed()中锁定 Pi 版本;至少检查pi --version输出并在异常时告警。
D6. 多个 screenpipe 实例同时运行 Pipe
- 现状:每个实例有独立的 PipeManager 和独立内存态,两个实例可能跑同一 Pipe、spawn 重复 Pi 进程。
- 要求:基于 DB 的锁。执行开始前
INSERT INTO pipe_executions带唯一约束或 advisory lock,第二个实例得到冲突。
E. 调度(Scheduling)
E1. 排队期间调度被修改
- 现状:当前无队列(调度器内联触发)。引入队列后:执行按旧调度排队,用户改为 "manual" 或不同间隔。
- 要求:队列条目存储触发原因;若 Pipe 被禁用或调度改为 "manual",出队挂起的运行。
E2. 启用的 Pipe 其调度"几小时前"就到期
- 现状:
last_run默认DateTime::UNIX_EPOCH(mod.rs 附近可见unwrap_or(DateTime::UNIX_EPOCH))。因此启用 "every 30m" 的 Pipe 会立即触发一次运行。 - 要求:这其实是正确行为,但应文档化;可提供
pipe.mdfrontmatter 的run_on_enable: false来抑制立即运行。
E3. 两个定时触发快速连续发生
- 现状:调度器立即设置
last_run,然后 spawn 异步任务。若调度循环在 Pipe 启动前再次运行(30s 粒度),is_running检查防止重复。但存在竞态:last_run.insert()先于running.insert()发生,存在狭窄窗口使两次都通过运行检查。 - 要求:原子操作——在同一锁作用域内同时插入 running map 并更新 last_run。
E4. 调度任务 panic
- 现状:
tokio::spawn()的闭包 panic 后任务静默死亡,Pipe 停止调度,用户看不到任何错误。 - 要求:调度循环用
catch_unwind包裹;记录 panic;重启调度器;在 health 端点暴露。
E5. 高频调度("every 1m")+ 慢 Pipe
- 现状:Pipe 跑 3 分钟,信号量阻塞,调度器每 30s 尝试都命中 "already running" 检查跳过;完成后下个 tick 又跑,实际上背靠背连续运行。
- 要求:基本可接受但浪费;建议在 Pipe 持续慢于调度时记录警告:"pipe 'X' takes avg 3min but is scheduled every 1min."。
E6. Cron 表达式求值为过去时间
- 现状:
should_run()比较now >= next_occurrence(last_run)。时钟向后跳时now可能早于下次出现时间 → Pipe 不运行。 - 要求:cron 基于墙钟,此行为可接受;文档化:cron 调度依赖准确系统时钟。
F. 配置与文件系统(Config & File System)
F1. Pipe 运行时 pipe.md 被修改
- 现状:配置在调度/运行时间加载,执行已持有副本,prompt 已渲染,无冲突。但
load_pipes()不会自动重读——Pipe 只在启动时加载一次(mod.rs 区间),只有显式 API 调用(enable/disable/update)才修改,调度器用内存快照。 - 要求:文件 watcher 或每个调度 tick 重读;或接受"配置变更只能经 API 生效"的现状并文档化。
F2. pipe.md 的 YAML frontmatter 非法
- 现状:YAML 解析失败 →
load_pipes()跳过该 Pipe,无任何错误浮出。 - 要求:记录带 Pipe 名与解析错误的警告;API
GET /pipes应包含带config_error字段的 Pipe。
F3. Pipe 运行时其目录被删除
- 现状:Pi 子进程的 working_dir 指向 Pipe 目录,若中途被删则文件操作失败,Pi 可能崩溃。
- 要求:删除操作应先取消运行中的执行,等待完成后再删除。
F4. 输出目录权限
- 现状:
./output/由 Pi 在执行期间创建,若父目录权限错误则 Pi 失败。 - 要求:spawn Pi 前先
ensure_dir_exists(pipe_dir.join("output"))。
F5. 从不信任 URL 安装 Pipe
- 现状:
install_pipe()从 URL 拉取并写 pipe.md,无校验、无沙箱。 - 要求(未来):内容校验——pipe.md 必须有合法 frontmatter;警告未知 agent;不自动启用已安装的 Pipe。
G. 网络与 API 依赖(Network & API Dependencies)
G1. Pipe 运行时 screenpipe API 尚未就绪
- 现状:Pipe prompt 中写死 "Screenpipe API: http://localhost:3030",Pi 查询之。若服务器仍在启动,Pi 收到 connection refused,失败。
- 要求:Pipe 调度器应仅在服务器开始监听后启动。目前调度器与服务器启动之间存在潜在竞态。
G2. screenpipe API 端口不是 3030
- 现状:端口硬编码在
render_prompt()中("Screenpipe API: http://localhost:3030")。用户跑在 3031 端口时 Pipe 查询错误端口。 - 要求:把实际服务器端口传给 PipeManager,注入 prompt 模板。
G3. LLM Provider 限流
- 现状:Pi 收到 429 后作为 stderr 返回,记为失败。
- 要求:解析限流错误,设置结构化
error_type = "rate_limited",考虑内置退避重试。
G4. LLM 响应过大 / 畸形
- 现状:Pi 捕获全部 stdout,若 LLM 产生垃圾输出(编码问题、二进制输出),stdout 可能巨大或不可解析。
- 要求:将 stdout 捕获限制在合理上限(1MB?),尽早检测二进制/非 UTF-8 内容。
H. Pi 聊天会话(交互式,Pi Chat Sessions)
H1. 聊天会话进程追踪
- 现状:聊天通过 Tauri command spawn Pi,进程由前端
standalone-chat.tsx管理,无服务端追踪。 - 要求:聊天会话与 Pipe 不同(交互 vs 批处理),保持分离;但共享进程管理关注点(PID、超时、kill);考虑
chat_sessions表或扩展pipe_executions加execution_type = 'chat' | 'pipe'。
H2. 聊天卡住 / 无响应
- 现状:用户无法知道 Pi 是在思考还是挂起,无超时。
- 要求:心跳——Pi 无 stdout 达 60s 时 UI 显示 "response may be delayed";手动取消按钮真正 kill 进程(需要可靠 PID);服务端在独立 map 中追踪聊天 PID 并经 API 暴露。
H3. 多个聊天会话
- 现状:多个聊天窗口会 spawn 多个 Pi 进程,无协调。
- 要求:文档化"一次一个聊天会话";或追踪所有活跃聊天 PID 并在系统状态中展示。
H4. 聊天与 Pipe 竞争资源
- 现状:聊天不使用 Pipe 信号量,两者可同时 spawn Pi。
- 要求:决策:聊天是否应尊重 Pipe 信号量?大概率不应(用户期望即时响应),但需监控资源使用。
I. Tauri App / 内嵌服务器(Tauri App / Embedded Server)
I1. Pipe 运行时 App 退出
- 现状:Tauri App 关闭会杀掉内嵌服务器,内存态丢失,Pi 进程是否被杀取决于进程组继承。
- 要求:优雅关闭——取消运行中的 Pipe,最多等 5s,强制 kill,退出前向 DB 写最终状态。
I2. Pipe 运行时 App 更新
- 现状:Updater 替换二进制,重启后等同 B1(计算机重启)。
- 要求:更新前钩子:取消运行中的 Pipe;或 updater 等待 Pipe 完成(带超时)。
I3. store.bin 被 Tauri 锁定
- 现状:
resolve_preset()用std::fs::read_to_string()读 store.bin,Tauri plugin-store 写同一文件,无协调。 - 要求:Tauri plugin-store 内部用原子写(写 tmp 再 rename),但某些文件系统上 rename 期间读取仍可能看到部分内容;修复:JSON 解析失败时重试读取(简单且覆盖该竞态)。
I4. 执行期间权限对话框阻塞
- 现状:macOS 可能弹出权限对话框(录屏、麦克风),阻塞线程。
- 要求:与 Pipe 无直接关系(Pipe 查询 API 而非硬件)。但若 screenpipe server 被权限对话框阻塞,Pi 的 API 请求挂起 → 超时 kill。在有超时的情况下可接受。
三、与仓库源码的对照验证:哪些"现状"已经改变
规范标注为 Drifting,因此在阅读时需要结合仓库当前代码判断哪些条目已经落地。以下是可直接验证的事实:
1. 信号量与进程管理
Semaphore::new(1)全局串行化执行在 mod.rs 仍然成立,且还引入了event_semaphore(mod.rs),说明事件触发的执行也有独立信号量。进程组方面,pi.rs 的注释确认子进程组与父进程共享 pid(spawn 时用setsid()),并在运行结束后调用reap_lingering_process_group(pid)清理残留孙进程(pi.rs),kill_process_group()(pi.rs)实现了按进程组杀进程,Windows 走taskkill /F /T /PID。这与 D2 的"进程组 + 整组 kill"要求方向一致。
2. 限流重试已经落地
规范 G3 建议内置退避重试,而 pi.rs 中已存在is_rate_limit_error()、parse_rate_limit_reset_secs()与重试等待逻辑,说明 Pi 执行器对 429 限流已有实际处理。
3. 睡眠/唤醒监控已有完整实现
规范 B2 提到的sleep_monitor.rs确实存在(crates/screenpipe-engine/src/sleep_monitor.rs),并已相当完善:
- macOS:
CGSessionCopyCurrentDictionary每 2s 轮询锁屏 +CFNotificationCenter事件驱动 +NSWorkspacesleep/wake 通知; - Windows:
PowerRegisterSuspendResumeNotification订阅挂起/恢复,OpenInputDesktop每 5s 轮询锁屏,并用时钟间隙检测唤醒; - Linux:时钟间隙轮询推断挂起/恢复;
- 暴露
recently_woke_from_sleep()、screen_is_locked()、system_is_suspended()等原子标志,供捕获循环与调度侧停摆。 这为 B2 的"检测睡眠/唤醒并主动处理"提供了现成基础设施。
4. 持久化表已经落地(迁移文件即证据)
规范提议的pipe_executions与pipe_scheduler_state两表,在 crates/screenpipe-db/src/migrations/20260213000000_create_pipe_executions.sql 中已按几乎相同的结构落地:包含status(默认'queued')、trigger_type、pid、model、provider、started_at/finished_at、stdout/stderr、exit_code、error_type、error_message、duration_ms,以及同名的三个索引和pipe_scheduler_state(pipe_name主键 +last_run_at/last_success_at/consecutive_failures)。
此外 20260728000000_create_pipe_event_runs.sql 还追加了pipe_event_runs表,用(pipe_name, event_name, event_key)主键 +INSERT OR IGNORE实现事件触发运行的幂等性,并为pipe_executions增加trigger_event/trigger_key列——这回应了规范 E1 中"队列条目存储触发原因"以及 C8 中"记录数据上下文"的方向。
5. HTTP API 面
crates/screenpipe-engine/src/pipes_api.rs 实现了 Pipe 管理 API:列表、启停(enable_pipe/stop_pipe)、执行历史(get_executions/get_execution)、日志、安装与删除等,其中trigger_type校验逻辑(run_trigger_type)只信任onboarding作为非手动触发,scheduled会被降级为manual——与规范中"触发原因可信性"的主题一脉相承。
四、提议 Schema:两表分工的持久化方案
规范给出了更新后的完整建表 SQL,这是全文的核心交付物之一,必须完整继承:
CREATE TABLE pipe_executions ( id INTEGER PRIMARY KEY AUTOINCREMENT, pipe_name TEXT NOT NULL, -- lifecycle status TEXT NOT NULL DEFAULT 'queued', -- queued → running → completed | failed | cancelled | timed_out queued_at TIMESTAMP NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), started_at TIMESTAMP, finished_at TIMESTAMP, -- process tracking pid INTEGER, -- OS process ID, set immediately after spawn -- execution context (snapshot at queue time — immutable after creation) agent TEXT NOT NULL DEFAULT 'pi', model TEXT NOT NULL, provider TEXT, preset_id TEXT, preset_snapshot TEXT, -- JSON: full preset config at queue time trigger TEXT NOT NULL, -- 'manual' | 'scheduled' | 'retry' rendered_prompt TEXT, -- full prompt sent to agent (for debugging) api_port INTEGER, -- screenpipe port used in prompt -- output (updated during/after execution) stdout TEXT DEFAULT '', stderr TEXT DEFAULT '', exit_code INTEGER, error_type TEXT, -- structured: 'timeout' | 'crash' | 'rate_limited' | -- 'model_not_found' | 'auth_failed' | 'network' | -- 'cancelled' | 'agent_not_found' | NULL (success) error_message TEXT, -- human-readable error for UI -- metadata duration_ms INTEGER, retry_of INTEGER REFERENCES pipe_executions(id), retry_count INTEGER DEFAULT 0, -- data context (what screenpipe data window this execution covered) context_start TIMESTAMP, -- start of time range in rendered prompt context_end TIMESTAMP -- end of time range in rendered prompt ); CREATE INDEX idx_pe_name_status ON pipe_executions(pipe_name, status); CREATE INDEX idx_pe_running ON pipe_executions(status) WHERE status = 'running'; CREATE INDEX idx_pe_name_time ON pipe_executions(pipe_name, queued_at DESC); -- Scheduler state (persisted across restarts) CREATE TABLE pipe_scheduler_state ( pipe_name TEXT PRIMARY KEY, last_run_at TIMESTAMP, last_success_at TIMESTAMP, consecutive_failures INTEGER DEFAULT 0 );为什么是两张表
pipe_executions是append-only(每次运行一行),pipe_scheduler_state是每 Pipe 一行、原地更新。分离的好处:
- 调度器只读一张小表来决定"下一步跑什么";
- 执行历史查询扫描另一张独立表;
- 无需在每个调度 tick 上执行
SELECT MAX(queued_at) FROM pipe_executions GROUP BY pipe_name。
值得注意的设计点:
preset_snapshot/rendered_prompt/api_port直接回应 A2(排队时快照)、G2(注入实际端口而非硬编码 3030)与 C8(记录数据上下文)——执行记录不可变,事后可完整复现"这次运行到底用了什么配置、看了什么数据窗口"。error_type枚举与 A4、A5、G3 的结构化错误要求一一对应:timeout、crash、rate_limited、model_not_found、auth_failed、network、cancelled、agent_not_found,成功则为 NULL。retry_of/retry_count支撑状态机中的自动重试(P2 的 retry with backoff)。- 部分索引(
idx_pe_name_status、idx_pe_running部分索引、idx_pe_name_time)已在迁移文件中落地,说明这套查询路径已被实际使用。
五、执行状态机(State Machine)
规范为每行执行定义了明确的生命周期:
┌─────────┐ queue │ queued │ ────────► │ │ └────┬────┘ │ semaphore acquired, spawn process ▼ ┌─────────┐ │ running │──── PID set in DB │ │──── stdout/stderr streaming to DB └────┬────┘ ╱ │ ╲ ╱ │ ╲ ▼ ▼ ▼ ┌──────┐ ┌──────┐ ┌──────────┐ │compl.│ │failed│ │timed_out │ └──────┘ └──┬───┘ └──────────┘ │ retry_count < max? yes → new row (trigger='retry', retry_of=id) no → stay failed At any point from queued or running: user cancel → cancelled server shutdown → failed (error_type='shutdown') sleep/crash → failed (error_type='interrupted', detected on startup)关键语义:
queued → running的唯一入口是信号量获取成功 + spawn 进程;running期间持续做两件事:PID 写入 DB(对应 D1 的修复)与stdout/stderr 流式写入 DB(对应 P2);failed状态下若retry_count < max则新建一行(trigger='retry'、retry_of=原id),否则保持 failed;- 从
queued或running随时可中断:用户取消 →cancelled;服务器关闭 →failed(error_type='shutdown');睡眠/崩溃 → 启动时检测为failed(error_type='interrupted')。
六、实施优先级(P0 → P3)
规范以「阻塞关系」为主线排定了实施顺序,这是团队对"先修什么"的明确决策:
| Phase | What | Blocks |
|---|---|---|
| P0 | Execution timeout(默认 5min)+ 进程组 kill | 一切——没有它,一个挂死的 Pipe = 整个系统死机 |
| P0 | 修复 PID 追踪:executor 拆分为 spawn+wait,立即把 PID 写入 running map | 取消操作 |
| P0 | pipe_executions+pipe_scheduler_state表 | 重启恢复、可观测性、一切 |
| P0 | 启动时孤儿恢复 | 干净状态保证 |
| P1 | 排队时快照 preset/model | 用户改设置后仍正确执行 |
| P1 | 向 prompt 注入实际 API 端口(而非硬编码 3030) | 非默认端口的正确性 |
| P1 | 结构化错误类型(解析 stderr 中的常见失败) | UI 可操作的错误提示 |
| P1 | 调度状态入 DB(last_run、consecutive_failures) | 重启存活、防止重复运行 |
| P1 | 优雅关闭:App 退出时取消 Pipe | 干净退出 |
| P2 | 执行期间流式写 stdout/stderr 到 DB | 实时可见性 |
| P2 | WebSocket 实时输出跟随 | 用户实时观察 Pipe 运行 |
| P2 | 带退避的重试 | 处理瞬时 LLM 失败 |
| P2 | API 中的队列位置 | 用户知道自己在哪 |
| P3 | 执行级输出目录 | 干净的部分输出处理 |
| P3 | 日志清理(30 天保留) | 磁盘管理 |
| P3 | 并发执行(semaphore > 1) | 性能 |
| P3 | 预检(Ollama 可达、模型存在) | 更好的 UX |
对照仓库现状可以确认的进展:
- P0 的超时 + 进程组 kill:进程组 kill 已有实现(
kill_process_group/reap_lingering_process_group);D3 提出的"SIGTERM → 5s → SIGKILL"级联在 pi.rs 中已有按 PID 杀进程组的代码路径(handle.current_pid()+kill_process_group(pid))。 - P0 的持久化表:迁移文件 20260213000000_create_pipe_executions.sql 已落地,
pipe_scheduler_state也被调度循环实际使用(mod.rs 在首次 tick 从 DB 加载last_run)。 - P1 的调度状态入 DB:同样已随迁移落地,且
advance_scheduler_last_run(mod.rs)将last_run_at持久化到pipe_scheduler_state。 - P3 的预检:仍是未来项;但 P0/P1 的多数基础设施已就位,说明规范的整体演进路径与实际代码保持了一致方向。
七、总结
docs/PIPE_EXECUTION_SPEC.md是一份典型的「现状审计 + 目标架构」型可靠性规范:它先如实记录内存态调度、无超时子进程、PID 追踪缺陷等现实约束,再以九大类、数十个边缘场景枚举故障模式并给出可落地的修复要求,最终收敛为两表持久化 Schema、状态机与分阶段实施计划。结合仓库源码可以看到,规范中的核心结论已经部分兑现——pipe_executions/pipe_scheduler_state迁移、睡眠/唤醒监控、进程组 kill、限流重试等均已存在,而"执行超时(默认 5 分钟)""排队时 preset 快照""结构化 error_type""执行级输出目录"等仍是值得继续推进的方向。对于希望深入理解 screenpipe 调度器内部机制或为其贡献可靠性改进的开发者,这份规范连同 mod.rs、pi.rs 与 pipes_api.rs 构成了完整的阅读路径。
【免费下载链接】screenpipeYC (S26) | Open Computer History | Record your screen continuously locally and provide context to your agents (Claude, Codex, Openclaw, Hermes, Runner...)项目地址: https://gitcode.com/GitHub_Trending/sc/screenpipe
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考