第 5 讲:Host、Heartbeat 与 Stateful Backoff¶
本讲结论: Host 拥有唤醒和外部 effect,LoopX 拥有 cadence proposal 与验证规则; 只有绑定 proposal identity 的 host readback 才能形成 durable scheduler ACK。
建议时长:90 分钟。讲解 55 分钟、时序推演 20 分钟、实验 15 分钟。
学习目标¶
完成本讲后,开发者应该能够:
- 区分 host trigger、heartbeat task body 和 LoopX decision kernel。
- 解释 thin prompt 为什么比项目专属大 prompt 更可靠。
- 读懂 scheduler hint、stateful backoff 和 bound ACK。
- 区分 quiet skip、monitor poll、wait、run-now 和 terminal stop。
- 解释为什么 monitor-poll 的 before/after decision 必须共享同一 execution context。
- 设计一个不会重复 spend、不会无限刷盘的 host adapter。
本讲技术契约¶
| 边界 | LoopX 中的答案 |
|---|---|
| Decision owner | LoopX 从 quota decision 投影 cadence、backoff 与 apply/ACK obligation |
| Execution context | host_surface + scheduler_owner + execution_mode 共同限定 effect authority |
| Effect owner | Host 创建 session、触发 turn、修改 RRULE 或调用外部 runtime |
| Commit receipt | ACK 必须绑定 state key、reset token、identity signature 与实际 host readback |
| Recovery | Quiet 保持 automation;未结算 proposal 继续 apply/ACK,不伪装成 delivery |
三个 Showcase 的 Cadence 不同,ACK 合同相同¶
Scheduler 不懂 GitHub 或研究指标,但它能根据 Kernel 已分类的 action 调整唤醒节奏:
| 场景 | Kernel 已知事实 | Cadence 倾向 | 何时重置 backoff |
|---|---|---|---|
| PR 有 failing checks successor | runnable advancement | 较快继续 | successor 被领取或 material transition |
| PR checks pending 且连续无变化 | monitor wait | 逐步放慢 | checks/review fingerprint 改变 |
| Auto ML 候选正在实现或 preflight | runnable advancement | 较快继续 | validated launch proposal 写回 |
| 外部训练/评估任务仍运行 | task monitor wait | 按任务阶段逐步放慢 | task state、artifact 或 metric fingerprint 改变 |
| 资源槽已满,候选 deferred | capacity wait | 较慢观察资源 source | capacity readback 使 resume_when 成立 |
| 研究 lane 有 holdout todo | runnable frontier | 较快继续 | evidence packet 写回 |
| 研究暂无可执行 frontier,等待外部证据 | fresh-evidence wait | 较慢轮询 | 新 evidence event 或 user decision |
无论哪种场景,LoopX 只产生 cadence proposal;host 应用 RRULE 或下一次触发时间后,必须
回写绑定 goal + agent + action class + proposal revision 的 readback。只看到宿主当前值
“碰巧相同”不能结算一条新 proposal,因为那无法证明 host 应用了本次决定。
Explore Graph 的 material finding 会改变下一轮 evidence frontier,Explore Harness 的新 portfolio 可能改变 advisory ranking;二者本身都不应创建额外 heartbeat。只有它们经 Kernel 形成 runnable todo、due monitor、replan obligation 或明确 wait condition 后, scheduler hint 才改变 cadence。这样一张展示图刷新失败不会让昂贵实验提前启动,planner 每次重新排序也不会造成高频空转。
Host 是触发器,不是第二控制面¶
LoopX 支持多种 host:
| Host surface | 如何开始 | 谁持续触发 |
|---|---|---|
| Codex App | $loopx <task> |
App heartbeat automation |
| Codex CLI | 设置稳定的 thin re-entry body 与可见 goal/session | 用户或 CLI 可见循环 |
| Claude Code | /loopx <task>,可选 native loop adapter |
Claude native loop 或用户 |
| 其他 shell/agent | start-goal --guided |
外部 runner 或人工触发 |
Host 负责:
- 创建或恢复 executor turn;
- 传入 LoopX 生成的稳定 re-entry body;
- 唤醒后调用 CLI,取得当前 Turn 的动态 packet;
- 声明 host capability;
- 应用 scheduler cadence;
- 返回真实 effect receipt。
Host 不负责:
- 自己解析 todo 选择 runnable;
- 自己决定 user gate 是否阻塞;
- 把 model output 当 execution receipt;
- 在本地 prompt 中复制 quota 状态机。
Thin Heartbeat Prompt¶
命令:
loopx heartbeat-prompt \
--thin \
--goal-id <goal-id> \
--agent-id <agent-id> \
--agent-scope "<registered host lane>"
实现入口是 loopx/heartbeat_prompt.py::build_heartbeat_prompt。Thin body 只保留生命周期不变量:
CLI is source of truth
inspect registry / status / history / repo
run exact quota should-run
follow interaction_contract
apply scheduler_hint
write plans/done into LoopX state
spend only after validated writeback
safe P1/P2 may continue when permitted
monitor quiet skips do not spend
stop at private/credential/destructive/unauthorized boundaries
项目专属 watch 不应写进 generic heartbeat prompt。例如“每轮看 PR #123”应建模成 continuous_monitor todo,带 target、cadence 和 due time。
一次 Heartbeat Turn 的 Host 时序¶
Codex App automation fires
-> opens/resumes target thread
-> executor reads thin task body
-> executor runs quota should-run with same agent identity
-> interaction_contract determines this turn
-> agent performs bounded work or quiet action
-> durable writeback and validation
-> spend once if allowed
-> host applies scheduler hint
-> host records bound ACK when required
Agent identity 必须从 prompt、quota、todo writeback 到 spend 保持一致。不能用 agent-a 的 quota,写 agent-b 的 todo,再用无 agent id 的 spend。
Scheduler Hint 是派生协议¶
Scheduler 根据已经解析的 lifecycle 状态决定下一次 cadence,不应独立做业务推理。
典型 action class:
| 状态 | 建议初始/最大 cadence | 含义 |
|---|---|---|
| runnable active work | 约 3m / 10m | 尽快继续,但允许渐进退避 |
| user wait | 约 30m / 120m | 等用户,不高频打扰 |
| reassigned/handoff | 约 10m / 60m | 给新 peer 反应时间 |
| material monitor | 约 15m / 60m | 按外部证据节奏检查 |
| fresh evidence wait | 约 60m / 240m | 没有新证据前低频 |
| quiet wait | 约 30m / 120m | 保持 alive,不制造噪声 |
| terminal no-followup | stop | 完整 closure 后停止 |
具体值由当前 schema 和配置决定,上表用于理解类别,不应复制到另一个 hard-coded scheduler。
Stateful Backoff¶
固定 2 分钟 heartbeat 会产生两个问题:
- 有工作时可能太慢;
- 等用户或外部证据时会反复空转、刷日志和占用模型。
Stateful backoff 保存同一等待身份的连续轮次,逐步增加间隔。当状态变化时 reset。
可以把 identity key 理解成:
当 key 不变:
当 todo、gate、evidence、monitor due state 或 agent lane 变化时,backoff 应重置到新类别的初始 cadence。
为什么需要 Scheduler ACK¶
模型输出“已将 automation 改成 30 分钟”不是执行证据。App 必须实际更新 RRULE,随后运行 CLI 给出的 ACK:
loopx quota scheduler-ack-current \
--goal-id <goal-id> \
--agent-id <agent-id> \
--surface codex_app \
--state-key scheduler_hint.codex_app.stateful_backoff \
--reset-token <proposal-reset-token> \
--identity-signature <proposal-identity-signature> \
--applied-rrule 'FREQ=MINUTELY;INTERVAL=30' \
--execute
ACK 绑定:
- goal;
- agent;
- host surface;
- scheduler state key;
- proposal reset token 与 identity signature;
- 实际 applied RRULE。
这样下一轮能够区分:
Scheduler ACK 本身不构成 delivery,不 spend。
Proposal、Host Effect 与 Durable Receipt¶
Scheduler 交互包含三个时间点,不能压成一个 RRULE matches:
P: quota 产生 proposal identity 与 target RRULE
O: host 更新 automation,并 readback observed RRULE
R: LoopX 持久化 ACK,证明 O 对应 P
O.target_matches=true 只说明宿主当前值正确,不等于 R.persisted=true。如果 adapter
先更新 RRULE,再重新计算一份已经显示 settled 的 hint,原 proposal 的 ACK obligation
可能被跳过。完整资格测试必须保留 P 的 state_key、reset_token、
identity_signature 和 target RRULE,按以下顺序验证:
quota proposal
-> host update
-> host readback
-> ACK original proposal identity
-> final quota guard observes persisted scheduler state
最后一步不能只断言 host 仍是目标 RRULE,还要断言控制面不再返回同一 ACK obligation, 并且 durable scheduler state 已记录对应 identity。这样才能区分“宿主碰巧已经正确”和 “这次控制面 transition 已经结算”。
Monitor 的 Quiet Contract¶
continuous_monitor 是最容易产生空转的 lane。
未到期¶
行为:不 poll,保持 automation active,按 due time/backoff 唤醒,不 spend。
到期且状态变化¶
做一次 bounded external evidence poll,写入新 evidence,并根据结果:
- 完成 monitor;
- 创建/unblock successor advancement todo;
- 更新 next due;
- 形成 blocker/user gate。
有 material writeback 时才 spend。
到期但状态未变¶
最多一次 poll,记录 compact no-change 或更新 due/backoff,然后 quiet。不要每次 heartbeat 重复写完全相同的失败日志。
连续无推进¶
连续两轮 no-progress 应触发 self-repair:
- cadence 是否过快?
- target key 是否稳定?
- 外部证据是否可订阅而非轮询?
- 是否缺少 successor/resume condition?
- projection 是否一直失败?
组合回放:Monitor、Gate、Replan 与 ACK¶
考虑一个外部 PR monitor。它到期后发现 review 要求改变 public contract,因此一次 poll 同时产生新证据、scoped user gate、blocked successor 和 replan trigger:
monitor due
-> one bounded poll
-> material evidence changed
-> user gate G1 + blocked successor T1
-> replan obligation
-> new interaction contract
-> new scheduler identity and cadence
-> host applies RRULE
-> CLI ACKs the exact current hint
这条链上有四个容易混淆的边界:
- monitor 只负责观察并写 evidence,不决定 G1 是否获批;
- replan 负责重算 frontier,不得绕过 T1 的 decision scope;
- scheduler hint 从新 interaction contract 派生,旧 monitor cadence 不能因为“比较省”而继续沿用;
- host ACK 只证明某个 identity signature、reset token 和 RRULE 已应用,不证明 PR 修复已交付,因此 ACK 不 spend。
G1 未解决期间,heartbeat 仍应保持存活并按 human-gate/replan cadence 唤醒,但不得重复 poll 已结算的 M1,也不得把相同 gate 每轮重新写成 material progress。这个组合场景同时检验 todo lifecycle、quota precedence、scheduler reset 和 no-spend policy。
Projection 失败日志去重¶
本地异常如果每 2 秒写一条相同日志,会带来:
- event ledger 噪声;
- status 变慢;
- 真实新错误被淹没;
- automation 被误判为“持续有进展”。
合理的错误身份应包含:
相同身份在没有状态变化时只更新 backoff 或 compact count,不反复追加等价 payload。新 source revision、error family 或修复结果出现时再记录新事件。
Stop 的严格条件¶
Host 只有在 CLI 派生 terminal_no_followup 时才应自动停止。它至少要求以下 frontier 全部关闭:
- open agent todo;
- open user gate/action;
- due 或 active monitor;
- pending successor/handoff;
- replan obligation;
- acceptance/vision gap;
- retryable projection/sink postcondition;
- unresolved blocker/resume route。
因此:
只是必要条件之一,不是充分条件。
terminal_no_followup 是 LoopX 自己派生的 runtime closure,不是用户自定义 status,也不应由 host 根据自然语言猜测。
原生 Codex Goal 与 LoopX Host Surface¶
原生 Goal 的持久边界¶
Codex 原生 Goal 不是“在 prompt 前写 /goal”的文本约定。自动化 surface 通过
thread/goal/set 把 objective、status 与可选 token budget 绑定到 thread,通过
thread/goal/get 读回,再用 turn/start 继续执行。只有普通 prompt 的 slash 前缀、
外部 polling loop 或 codex exec 成功,都不能单独证明 native Goal 已激活。
这层 object 解决了一个重要问题:目标和 host lifecycle 不必只存在于当前模型上下文。 LoopX 再补充项目拥有的结构化状态和过程:todo/claim/gate、领域 observation、effect receipt、 跨 host cadence、replan 与 terminal audit。两者不是竞争状态机,也不能把 LoopX packet 伪装成 native Goal baseline。
Codex App 的 LoopX 路径¶
- App heartbeat automation 持有稳定的薄 task body;
- 每次唤醒时,executor 按 task body 调用 LoopX CLI,从最新 state 取得本轮 packet;
- host 可以按
scheduler_hint更新 RRULE; - scheduler hint 可要求 App update、readback 与 ACK;
- App thread/session 不成为 LoopX canonical state owner。
Codex CLI 的 LoopX 路径¶
- 当前可见 session 通常由用户启动;
- LoopX 提供 bootstrap message / thin re-entry body,并可让用户设为可见
/goal <task_body>; - 可见 Goal 保留 objective 与 re-entry contract;每一 Turn 的 packet 仍由 CLI 重新生成;
- 这里借用 Goal 作为持续 host surface,但长期 todo、gate、evidence 与 cadence 仍从 LoopX 读取;
- CLI 不应假设用户安装的
/loopx自定义命令一定可用; - final-check/self-stop 通过当前可用 host surface 完成。
两条 LoopX 路径共享同一个 quota/status/todo state kernel,不应复制两套 lifecycle。Native Goal objective 可以长期稳定,CLI packet 则必须随状态变化重新生成;把旧 packet 固定在 host 里,会让 host 继续执行过期 frontier。
Host Capability 与 Effect Receipt¶
有些动作需要 host 才能执行,例如:
- session message injection;
- session termination;
- state fork;
- workspace transfer;
- visible TUI pane launch。
Control plane 的正确关系是:
typed proposal
-> host checks declared capability and authority
-> host performs effect
-> host returns typed result
-> LoopX records capability-matched receipt
缺 capability 时 proposal 保持未执行。不能把 model response 当 host receipt。
实验:推演 Cadence,不修改 App¶
1. 生成 thin prompt¶
loopx heartbeat-prompt \
--thin \
--goal-id <lab-goal> \
--agent-id <lab-agent> \
--agent-scope "training heartbeat lane"
找出 prompt 中真正长期稳定的 6 条规则。
2. 读取 scheduler hint¶
loopx --format json quota should-run \
--goal-id <lab-goal> \
--agent-id <lab-agent> \
--available-capability shell \
| jq '{interaction_contract, scheduler_hint, automation_liveness}'
不要在课堂实验中实际更新个人 App automation。只回答:
- action class 是什么?
apply_needed是否为 true?- spend policy 是什么?
- state identity 何时 reset?
3. 构造 monitor 状态表¶
手工推演四种输入:未到期、到期有变化、到期无变化、连续无变化。为每种情况写出:
核心代码领读:LoopX 决策怎样变成 host cadence¶
这一讲最容易把三件事混在一起:heartbeat prompt、scheduler hint、host automation。先画清权责:
heartbeat_prompt 生成稳定的“下一轮怎样重新进入 LoopX”任务体
quota 从最新 state 生成当前轮的动态 packet 与执行义务
scheduler_hint 把义务投影成 cadence/backoff 建议
host 真正修改 RRULE、触发 session
scheduler_ack 证明 host 应用了哪个 RRULE
1. Scheduler hint 是纯 projection¶
loopx/control_plane/scheduler/scheduler_hint.py::build_scheduler_hint 的 docstring 就是边界:
def build_scheduler_hint(payload, *, scheduler_execution_context=None, ...):
"""Project host-runtime cadence/backoff policy from a quota decision.
This helper is intentionally pure: callers provide the few quota-local
classification facts it needs, and it returns the public scheduler contract
without reading files, mutating state, or depending on the full quota module.
"""
execution_context = resolve_scheduler_execution_context(
scheduler_execution_context
)
if not execution_context.ok:
return {
"action": "repair_scheduler_execution_context",
"spend_policy": "no quota spend for scheduler context repair",
"codex_app": {"apply": "none", "host_action": "none"},
...,
}
纯函数意味着它不能声称“RRULE 已更新”。它只能返回 apply_needed、推荐 interval、reset token 和 ack plan;effect 必须由 host 执行。
Execution context 也必须由调用方显式提供。缺少 context 的通用调用会返回
repair_scheduler_execution_context,不会再假定自己运行在 Codex App。
Codex App heartbeat 的兼容入口是明确的
--runtime-profile codex_app_heartbeat(生成命令使用等价的紧凑别名
--codex-app);Codex CLI、Claude Code 和外部
controller 则传各自的 host_surface、scheduler_owner 和
execution_mode。这样测试与真实 host 使用同一个 typed contract,而不是靠一个
库级 legacy 默认碰巧命中 App 行为。
2. Stateful backoff 的 identity 不只是 goal id¶
源码构建的 identity keys 包含:
base_identity_keys = [
"goal_id",
"agent_identity.agent_id",
"effective_action",
"heartbeat_recommendation.recommended_mode",
"interaction_contract.mode",
"recommended_action",
]
只要工作身份发生 material 变化,backoff progression 就应 reset。否则“等待同一个外部结果的第 5 次 poll”和“刚切换到新 runnable todo 的第一次执行”会错误共享慢 cadence。
3. ACK 同时校验 Proposal Identity 与 Host 回报¶
scheduler/ack.py 中的 build_scheduler_ack_plan 逐层比对 state identity:
if not agent_id:
return {"ok": False, "reason": "... requires --agent-id"}
if not stateful_backoff:
return {"ok": False, "reason": "... no ... stateful scheduler packet"}
if stateful_backoff.get("state_key") != state_key:
return {"ok": False, "reason": "--state-key does not match ..."}
if reset_token and reset_token != stateful_backoff.get("reset_token"):
return {"ok": False, "reason": "--reset-token does not match ..."}
if identity_signature and identity_signature != stateful_backoff.get("identity_signature"):
return {"ok": False, "reason": "--identity-signature does not match ..."}
if not apply_needed and not ack_needed:
return {"ok": True, "already_applied": True, ...}
if not applied_rrule:
return {"ok": False, "reason": "... requires --applied-rrule ..."}
这些检查拒绝把任意 host 值写成当前 proposal 的 receipt。端到端测试还必须把 apply 前 生成的 token 原样带过 host effect;若只在 effect 后重新读取当前 hint,测试仍可能漏掉 ACK obligation 丢失。ACK 不是 delivery,所以不 spend;但没有 durable ACK,下一轮也 不能把“建议值”或“当前 host 恰好匹配”当成已结算状态。
4. Monitor writeback 用 result hash 区分观察与推进¶
loopx/control_plane/scheduler/monitor_poll_writeback.py 是 no-change backoff 的具体落点:
safe_result_hash = str(result_hash or "").strip()
if not safe_result_hash:
raise ValueError("monitor todo writeback requires --result-hash")
if not material_change and not effective_next_due_at:
raise ValueError(
"unchanged monitor todo writeback requires --next-due-at or a parseable cadence"
)
consecutive_no_change = (
0
if material_change or (previous_hash and previous_hash != safe_result_hash)
else previous_no_change + 1
)
monitor_metadata = {
"last_checked_at": generated_at,
"result_hash": safe_result_hash,
"consecutive_no_change": str(consecutive_no_change),
"material_change": "true" if material_change else "false",
"next_due_at": effective_next_due_at,
}
if material_change and next_agent_todo:
add_goal_todo(..., task_class="advancement_task", ...)
if material_change and next_user_todo:
add_goal_todo(..., task_class="user_gate", ...)
四个不变量:
- 每次 poll 必须有稳定
result_hash; - unchanged poll 必须给出下次 due,不能形成热循环;
- hash 改变或 material change 会把连续无变化计数清零;
- 只有 material change 能创建 successor todo。
因此“看了一眼但没变化”通常是 no-spend observation;“新证据改变 frontier 并写回 successor”才可能成为 accountable delivery。
5. Monitor-poll 的前后决策必须共享 Execution Context¶
一次 monitor poll 会先计算 before,写回 observation,再用新状态计算 after。两次
decision 必须使用同一个 typed scheduler context:
before = build_quota_should_run(
status_payload,
goal_id=goal_id,
agent_id=agent_id,
scheduler_execution_context=scheduler_execution_context,
)
return record_quota_monitor_poll_for_decision(
before,
status_payload,
after_decision=lambda after_status: build_quota_should_run(
after_status,
goal_id=goal_id,
agent_id=agent_id,
scheduler_execution_context=scheduler_execution_context,
),
...,
)
Execution context 不是渲染选项,而是 decision input。若 CLI 入口识别出 Codex App,
但 wrapper 只把 context 传给 before,after 会误以为自己处在未知 host,返回
repair_scheduler_execution_context。这不是 monitor 产生了新阻塞,而是调用链丢失
了 authority context。
对应回归应同时证明:
- App context 在 before/after 都保持 App scheduler contract;
- generic CLI context 在 before/after 都保持 external-controller contract;
- 非法或缺失 context 两边一致 fail closed,不能由 wrapper 静默补默认值。
6. 一次真实 host 循环¶
App tick
-> heartbeat prompt 要求先 quota should-run
-> quota 返回 interaction_contract + scheduler_hint
-> 若 apply_needed: host 更新 RRULE
-> host 用 scheduler-ack-current 写回实际 RRULE
-> 若 agent.must_attempt: 执行 bounded work
-> refresh-state 写回 outcome
-> validation 通过后 spend 一次
在课堂上故意制造一个 unchanged monitor,确认链路在 monitor writeback 后结束,没有虚假的 refresh/spend。
断点建议¶
scheduler_hint.py:457:观察非法 execution context 如何 fail closed;scheduler/ack.py:120:依次改错 state key、reset token、identity signature;write_monitor_poll_todo_state:98:比较 hash 相同、hash 变化、显式 material change;record_quota_monitor_poll:确认 before/after 收到同一个 scheduler context;- host adapter:确认只有 host 层真正调用 automation update。
读完这一段应能回答¶
- 为什么 scheduler hint 不能证明 RRULE 已应用?
- 为什么 ACK 不消耗 delivery slot?
- 哪些字段变化应 reset stateful backoff?
- unchanged monitor 为什么必须写
next_due_at? - 为什么 monitor writeback 不能重新猜测 host surface?
- automation 为什么在 quiet wait 时仍可能保持 active?
代码阅读路线¶
docs/heartbeat-automation-prompt.mddocs/operations/long-task-cadence-policy.mdloopx/heartbeat_prompt.pyloopx/control_plane/scheduler/loopx/quota.py的 final composition:interaction contract、scheduler hint、protocol action packetdocs/product/core-control-plane/state-machine.md的 Scheduler/Heartbeat 状态机
代表性 Smoke¶
examples/control_plane/heartbeat-quota-flow-smoke.pyexamples/control_plane/quota-scheduler-state-ack-smoke.pyexamples/control_plane/interaction-scheduler-authority-smoke.pyexamples/control_plane/monitor-scheduler-contract-smoke.pyexamples/control_plane/monitor-poll-writeback-smoke.pyexamples/control_plane/quota-terminal-no-followup-smoke.py
常见实现错误¶
Automation prompt 携带项目专属 watch¶
把 watch 写成 monitor todo,让 heartbeat 保持通用。
更新 RRULE 后不 ACK¶
下一轮无法证明 host 实际应用了哪个 cadence。
Quiet no-op 也 spend¶
会让配额账和真实推进失去关系。
should_run=false 就停 automation¶
等待用户、未到期 monitor 和 evidence backoff 都需要继续存活。
把 launcher 称为 supervisor agent¶
Auto Research 的 tmux launcher 只是 host launcher,不是拥有 todo 决策权的 leader agent。可选 peer supervisor 是另一套 proposal-only overlay。
课后检查¶
- Thin prompt 为什么不能替代 quota decision?
- Stateful backoff 的 reset key 应包含哪些信息?
- 为什么 scheduler ACK 不 spend?
- Monitor no-change 时怎样避免刷盘?
- Host 自动停止前必须检查哪些 closure 面?
下一讲关注一个长期系统最难的部分:如何证明真正推进过,以及状态漂移后如何自修复。