扫描和认领是两个独立步骤会出现 TOCTOU:两个 worker 都读到可认领,都写入 owner。P17 用 SQLite:BEGIN IMMEDIATE → 回收过期 lease → 加载验证 DAG → 找第一个 ready → 条件 UPDATE → COMMIT。
BEGIN IMMEDIATE
→ 回收已到期 lease
→ 同一快照加载并验证 DAG
→ 按 sequence 找首个 ready task
→ 条件 UPDATE pending -> in_progress
→ COMMIT
sequence 是真正创建顺序(不等同 UUID 字典序);task_dependencies 用外键拒绝不存在依赖;task_claim_tokens 保存历史 token,旧 token 不可复用;CHECK 把状态不变量压进数据库。
CREATE TABLE tasks(
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
status TEXT CHECK(status IN ('pending','in_progress','completed')),
owner TEXT, claim_token TEXT UNIQUE,
lease_expires_at_utc TEXT, ...
);
-- CHECK: pending 无 owner/token/lease;
-- in_progress 三者齐全;completed 清空 token/lease
claimNext 在同一事务内选择并迁移;条件 UPDATE ... WHERE status='pending' AND owner IS NULL 是最终互斥点,只认 getRowsModified() === 1,否则说明被人抢先,返回 undefined 不抛错。认领凭证插入历史表在条件 UPDATE 之前——token 冲突发生在 UPDATE 之前,回滚不需要补偿。
UPDATE tasks SET status='in_progress', owner=?, claim_token=?,
lease_expires_at_utc=?
WHERE id=? AND status='pending' AND owner IS NULL;
-- 只有 rowcount == 1 才成功
[claimed_at, lease_expires_at)。到达截止瞬间已过期:释放用 <=,完成检查用 >=,边界正好差一毫秒。expired 任务在下一次事务原子回到 pending(清空 owner/token/lease),旧 owner 只能得到 lease expired/claim mismatch。owner 来自 ToolContext.identity,claim_task 的 schema 里根本没有 owner 字段,strictObject 让伪造尝试直接失败。
到期任务原子恢复:
status = 'pending', owner = NULL,
claim_token = NULL, lease_expires_at_utc = NULL
持有 teammate registry lock → 检查是否关闭 → 先 claim Mailbox → 有 Protocol 就确定性路由 → 无消息查最新 plan 是否允许 effectful → 允许才 SQLite claimNext → 仍无工作则进入可唤醒等待。自动认领到任务后它不会自己完成——模型仍须显式调 complete_task 并出示 claim token(token 同时是本轮幂等键)。空转到 maxIdlePolls 就退出,idleTimeoutCount 可观察,drainEvents() 是空的;唤醒不消耗空转预算。
Mailbox 先于任务板(shutdown/plan 不被 ready task 饿死)
plan 未批准 → 不调用 claimNext,SQLite 零写入
pollIntervalSeconds=5, maxIdlePolls=12(可注入)
create_task 建 A、B、C(C 依赖 A/B)。
两人并发进 idle scanning。
A、B 被不同 worker 认领;C 不可同时认领。
A/B 都完成才解锁 C;C 只被第一个提交的事务认领。