跳到主要内容

项目执行状态真实性重构

实现状态:状态真实性主链与并发扩展已完成;Backend、Wework、Executor 全量回归、MySQL 迁移升降级及真实 Electron 桌面验收均已通过。

硬约束:不新建数据库表。本次只扩展已有 MySQL/SQLite loop_item_executions,并继续使用已有 loop_itemsproject_chat_messages 和 Automation Run。

1. 目标与范围

本次解决的不是枚举命名,而是“用户能否拿到可证明的真实状态”。覆盖 project robot 和 automation manager 在 cloud/local Runtime 上的队列、启动、事件、取消、恢复、重试及其 UI 投影。

TaskResource/Subtask 运行仍以已有 tasks/subtasks 为自己的权威,不复制到 loop_item_executions;本文不声称将两类运行模型合并。

必须满足:

  • claim 只证明控制面领取,不能证明 Runtime 已运行。
  • Start 可能送达后,不能因超时把同一 Attempt 重新排队。
  • 心跳只续控制 lease,不能单独证明进程仍在运行。
  • 无法确认时显示 unknown,不能猜成失败、成功或可重试。
  • Runtime 终态按 Attempt 身份和单调事件序号进行 CAS。
  • 取消意图和“Runtime 已停止”是两个状态。
  • 运行失败重试必须创建新的已有表行,旧 Attempt 永远保留。
  • GET 只读;消息和缓存状态不能反向覆盖执行事实。

2. 权威、投影与连线

连接方向仍然是单向的:Execution → Message/Automation/Workflow 投影。Message、metadata.ai_state、看板列和 UI 不能反向决定 Execution。

设备并发 D 只有一个配置值。占用不是把两套计数相加,也不是取较大值,而是按 Runtime task identity 精确合并:O = Runtime active + 持久化 capacity rows 中 runtime_task_id 不在 active_task_ids 的数量。这样,已被 Runtime 看到的机器人任务不会重复计数,人工对话与尚未进入 Runtime 的 claim 也不会互相漏算。Runtime Scheduler 仍是所有普通对话、机器人和自动化共享的物理硬上限;没有物理槽位时,“立即运行”只能把任务移到队首,已接收的 Execution 显示 waiting_runtime,不能越过 D 或显示 running

设备容量是 Runtime 运行态,不写入 Device Kind/MySQL 冒充事实。Local 领取通过 IPC 直接读取 Runtime;Cloud 由 Runtime 心跳上报 limit/active/active_task_ids/queued/runtimeInstanceId 到已有 Redis 设备在线状态并受同一 TTL 约束。active 必须与唯一、非空的 active_task_ids 一一对应。容量观察缺失、过期、身份不完整或 Runtime 实例不匹配时停止 claim;Claim API 明确拒绝调用方自报 deviceCapacity,也不回退到固定常量。

execution_device_id 只标识 Start 使用的传输路由,不是容量身份。一个 Runtime 安装可以同时暴露 local/app/socket 等多个 route,这些 route 必须按稳定的 runtime_instance_id 合并为同一个设备容量域;否则每条 route 都会独立拿到 D 个槽。Claim 时把 runtime_instance_id 写入已有 Execution 行,设备占用按 owner_user_id + runtime_instance_id 统计,锁也落在同一容量域上。

机器人并发 Ragent_id 跨设备、跨环境全局统计。同一个 Execution 必须同时通过设备、机器人和 execution_scope 三个门才能 claim;Automation Manager 没有 agent_id,只受设备和 scope 约束。unknown 不释放 capacity row,只有已确认终态,或者 Start 围栏前可证明安全的回队,才释放设备与机器人额度。

R > 1 时,每个 Execution 必须使用独立 worktree 或独立会话目录;无法隔离的共享工作区不能启用单机器人并发。这是执行安全前提,不增加新的持久化状态。

3. 零新表数据模型

已有 loop_item_executions 的一行就是一个 Attempt。本次并发迁移只在 MySQL 同名表增加 runtime_instance_id 和普通索引 idx_exec_runtime_capacity,没有 CREATE TABLE;本地 SQLite 在已有同名表上 ALTER TABLE,随后创建 ix_exec_runtime_capacity,schema version 升为 7。

维度字段语义
控制状态statuspending_approvalqueuedclaimedrunningcancel_requestedcompletedfailedcancelled
Runtime 观察observed_stateobserved_atunconfirmedacceptedrunningsucceededfailedcancelled 及最后证据时间
同步健康sync_statependingin_syncstalediverged
Attempt 因果attempt_noprevious_execution_id第几次执行及其上一次 Attempt
并发域execution_scopeproject robot 按任务,manager 按 Automation Run 隔离
Start 围栏claimed_atstart_requested_at区分“安全释放的领取”和“可能已送达 Runtime 的启动”
Runtime 身份runtime_device_idruntime_task_idtask id 固定为 codex-queue-{execution.id};服务层严格校验
容量身份runtime_instance_idowner_user_id + runtime_instance_id 合并同一 Runtime 的多个传输 route
事件围栏last_event_seq只接受更大的 Runtime 事件序号
取消/终止cancel_requested_attermination_reason取消意图时间与已确认终止原因
控制租约heartbeat_atlease_expires_atdispatcher/claim 存活性,不等于 Runtime 进程证据

没有给 runtime_task_id 增加唯一索引:插入时 ID 尚未生成,历史默认空值也会产生错误冲突。身份由 codex-queue-{id} 确定性生成,并在所有写入口校验;并发占用由 execution_scope、agent、owner/device 锁和 CAS 共同控制。

4. 三个独立维度与展示状态

展示态由三个维度即时计算,优先级固定:

因此 heartbeat_at 更新不会把 starting/unknown 改成 running;终态也不会被 stale 覆盖成 unknown。

5. Cloud 正常启动时序

RPC 传输异常与明确 emitted=false 被区分;前者在 Start 围栏之后只能进入 unknown。

6. Local/App 正常启动时序

执行器注册后立即发送在线心跳,异步读取真实容量;读取完成后立即补发携带容量的心跳,供云端项目的 App 领取接口使用,不能等待下一个 30 秒周期。慢速容量读取不阻塞在线心跳。

beforeDispatch 由最终发送请求的适配器在准备完成后调用,Hybrid 层透传回调。云端项目在本机执行时采用相同顺序,围栏和失败状态写入后端。云端模型必须携带目录返回的命名空间和资源所有者;缺失身份属于发送前失败,不能标记为结果未知。

App IPC 不再提供 executions.complete/executions.fail 给调度器伪造终态。终态由 Local Executor 的 turn outcome 写回。

7. Runtime 事件、乱序与原子终态

人工拒绝也遵守相同事务边界:Execution、Activity、Automation 投影与任务版本 CAS 一次提交,提交前不会由内部 helper 偷跑 commit()

8. 取消时序

本地 Queue 停止按钮先调用本地 executions.cancel,再使用该执行行的 Runtime 地址调用 cancelRuntimeTask;不再误用 cloud stop API。

9. 失败重试与迟到事件隔离

Runtime 已证明失败时才可自动重试并消耗 retry budget。Start 前明确失败可复用原行恢复 queued,因为可以证明 Runtime 进程不存在。

10. Lease 过期、unknown 与对账

Cloud Scan 与 Local App 都按持久化的 device/task identity 对账;Local App 通过 executions.list_staleexecutions.reconcile 补回重启期间丢失的事件。task.status=active 本身不是运行证明,必须结合 runningturnStatus

“运行超过阈值且无文本”只触发 cancel_requested 和 Runtime cancel,不直接制造 failed

11. 并发、容量与公平性连线

固定顺序是新鲜观察 → owner lock → runtime-instance lock → DB CAS。批量 CAS 必须全部命中,否则整批回滚,不允许把 Runtime identity 写到未 claim 的行。unknown 不释放容量;否则同一真实进程可能与新 Attempt 并行。最高优先级先执行,但同一优先级在机器人之间 round-robin、机器人内部 FIFO,因此一个机器人即使排了 20 个任务,也不能饿死另一个机器人。

12. 纯读取与 UI 一致性

优先级为最新 Execution → 与其绑定的终态消息上下文 → legacy cache。过期 cache 只能在响应中变为 unknown,GET 不落库。failedcancelledskippedsucceeded 在 UI 中保持独立。

13. 已实现入口与删除的旧入口

Cloud/App 启动协议:

  • start-requested:持久化 Start 围栏。
  • runtime-start:只记录 Runtime accepted,不冒充 running。
  • dispatch-unknown:Start 结果不确定,保持容量并显示 unknown。
  • dispatch-failed:仅 Start 前 preflight 明确失败。
  • Runtime events / trusted status query:唯一 running 与执行终态来源。

已删除 App 调度器可调用的直接 complete/fail 入口。Heartbeat 必须匹配 execution、device 和 task identity,并且只续 lease。

14. 验收矩阵

场景必须看到禁止出现
claim 成功、Start 未发startingrunning
Runtime 已接受、尚无活跃 turnwaiting_runtimestartingrunning
Start 响应丢失unknown 且占容量原行重排、双跑
Runtime 首事件running,写 observed_at/eventSeq用 heartbeat 冒充
缺序号、重复、乱序或终态后的事件Execution 与所有下游投影都忽略消息/活动绕过门禁继续推进
Start 前取消直接 cancelled无意义 Runtime cancel
Start 后取消cancelling 到 ACK/event立即假 cancelled
lease 过期且未 Start原行 queued、retry 不变新建重复 Attempt
lease 过期且可能 Startunknown、对账、占容量自动失败/重发
Runtime failed + retry旧行 failed、新行 queued修改旧行为 queued
GET / 刷新页面状态不改变读取时写状态
My Work/Queue/Detail/Automation同一精确展示态pending/claimed 被显示 running
容量心跳缺失/过期/实例不匹配停止 claim,已有状态保持固定常量或调用方容量回退
Runtime active 与 durable claim 指向同一 task id只计一次双计导致假满
Runtime 人工任务与尚未送达的 durable claim 不同两者都计入 Omax() 漏计导致超领
单机器人达到 R其他机器人仍可按公平顺序 claim热机器人饿死队列或跨设备绕过 R
绑定非 Git 项目设置 R > 1配置时拒绝,启动前再次校验在共享目录并发执行
“立即运行”且 Runtime 已满移到 Runtime 队首,仍等待槽位越过 D 启动第 D+1 个任务
Migration只 ALTER 现有表和建索引任何新表

15. 自动化与人工验证

自动化必须至少覆盖:迁移无 create_table、容量身份与心跳 TTL、active_task_ids 精确去重、多个 device route 共享 D、机器人全局 R、同优先级 round-robin、公平扫描无固定候选窗口、批量 CAS 全有或全无、非 Git 并发拒绝、Runtime 硬上限与 force-start、claim/identity/start fence、事件序号、竞争终态、取消前后边界、ambiguous dispatch、cloud/local recovery/reconcile(含 runningturnStatus)、retry 新 Attempt、投影同事务、纯 GET、local IPC/store、UI 状态映射和 TypeScript/Rust 编译。

人工验收按以下顺序:

  1. cloud 与 local 各跑一次正常任务,确认 queued → starting →(可选 waiting_runtime)→ running → succeeded
  2. Start 后断开设备,确认显示 unknown 且没有第二次启动。
  3. 分别在 queued 和 running 阶段取消,确认前者立即终态,后者先 cancelling。
  4. 制造 Runtime failure,确认旧 Attempt 保留且 retry 使用新 task id。
  5. 同时打开 Queue、Task Activity、My Work、Automation 和 Overlay,确认状态一致。
  6. 刷新和重复 GET,确认没有任何状态被读取动作改变。
  7. 将设备 D 设为 2,混合启动普通对话和机器人任务,确认物理 active 永不超过 2,已接受任务显示 waiting_runtime。
  8. 将一个机器人 R 设为 2,并从两条 device route 同时领取,确认全局最多 2 个;再验证同优先级的第二个机器人不会被长队饿死。
  9. 在 Git 项目中验证 R > 1 产生不同 worktree;在普通目录中确认保存配置即被拒绝。