Skip to content

Commit 8756a6b

Browse files
committed
fix(heartbeat): observe the installed automation prompt binding every turn
Installed automation bodies were reconciled once, at `update --apply` time. A running App cannot be written through the offline adapter, so the pending adoption was reported only inside that batch: a lane that was repaired, resumed, or activated later silently kept applying a frozen body, and nothing in the recurring contract ever re-observed the installed prompt. `quota should-run` now observes the caller's own installed automation and projects it as `scheduler_hint.app_automation.prompt_binding` (`codex_app_automation_prompt_binding_v0`: current, adoption_required, blocked, ambiguous, absent, unavailable). A stale entry carries `host_action=adopt_managed_bootstrap`, its no-spend policy, both prompt digests, and the same reviewed prompt-only `automation_update` request that update-time reconciliation returns, so the obligation survives in the live contract instead of a completed report. Update-time and turn-time requests are built by one renderer so their shapes cannot drift. The observation is read-only, bounded, and fail-open: an unreadable store reports a status instead of failing the turn, and a lane without an installed automation projects no field. A recognized loader is always compared with its own binding, because a turn must never retarget another home, registry, runtime root, or CLI binary; only an unrecognized body is reviewed against the caller's registry. The thin/compact/full scheduler-hint rules name the new host action within their existing output budgets. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
1 parent cd9cfa6 commit 8756a6b

11 files changed

Lines changed: 581 additions & 48 deletions

File tree

‎docs/heartbeat-automation-prompt.md‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,13 @@ heartbeats should search/use `automation_update` when available. If
637637
`scheduler_hint.app_automation.host_action=pause_or_delete_current_heartbeat`: in
638638
that terminal case, call `automation_update` once to pause the current
639639
heartbeat (delete only if pause is unavailable), verify the host result, spend
640-
no quota, and end the turn without a scheduler ACK. Otherwise call it only when
640+
no quota, and end the turn without a scheduler ACK. When the same lane reports
641+
`scheduler_hint.app_automation.prompt_binding.host_action=adopt_managed_bootstrap`,
642+
its installed body is not the current managed loader: apply the reported
643+
prompt-only `api_update_request` through `automation_update` once after checking
644+
the reported prompt hash, read the automation back, and spend no quota. That
645+
request preserves the binding, schedule and status and is independent of
646+
`automation_update` cadence handling. Otherwise call it only when
641647
`scheduler_hint.app_automation.stateful_backoff.apply_needed=true` and
642648
`scheduler_hint.app_automation.recommended_rrule` is present. After a successful
643649
RRULE update, run `loopx` with
@@ -815,7 +821,10 @@ automations can all share the same LoopX quota guard without hard-coding
815821
different wait loops. Host implementations should first honor a terminal
816822
`app_automation.host_action=pause_or_delete_current_heartbeat` by stopping the
817823
current heartbeat once, verifying the result, and ending without scheduler ACK
818-
or quota spend. Otherwise they should read the compact
824+
or quota spend, and they should honor
825+
`app_automation.prompt_binding.host_action=adopt_managed_bootstrap` by applying
826+
that reported prompt-only request once, verifying the readback, and spending no
827+
quota. Otherwise they should read the compact
819828
`app_automation.stateful_backoff` packet, call `automation_update` only when
820829
`apply_needed=true`, and then let `quota scheduler-ack-current` persist the
821830
applied RRULE state from the latest scheduler hint without spending quota. A

‎docs/reference/automation-prompt-upgrades.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,28 @@ read-only preview, not the upgrade executor. Do not infer a manual-only policy
6868
from its `adoption_required` status. Custom or inconsistent entries still need
6969
review; automatic prompt migration never grants scheduler or thread authority.
7070

71+
## Live turn observation
72+
73+
Install-time reconciliation is one-shot: a pending adoption that nobody applies
74+
would otherwise never reappear, and a repaired or resumed lane can keep running
75+
yesterday's frozen body. `quota should-run` therefore reports the caller's own
76+
installed automation in `scheduler_hint.app_automation.prompt_binding` (schema
77+
`codex_app_automation_prompt_binding_v0`: `current`, `adoption_required`,
78+
`blocked`, `ambiguous`, `absent`, or `unavailable`). A stale entry adds
79+
`host_action=adopt_managed_bootstrap`, its no-spend policy, both prompt digests,
80+
and the same reviewed prompt-only `automation_update` request that update-time
81+
reconciliation returns, so the obligation survives in the live contract instead
82+
of a completed report.
83+
84+
The observation is bounded, read-only, and fail-open: an unreadable or
85+
disagreeing store reports a status and never fails the turn, and a lane without
86+
an installed automation projects no field at all. A recognized loader is always
87+
compared with its own binding, because a turn must never retarget another Codex
88+
home, registry, runtime root, or CLI binary; only an unrecognized body is
89+
reviewed against the caller's registry. Adoption stays explicit and
90+
non-blocking: it is not delivery permission, not a scheduler authority, and a
91+
deliberate owner-pinned body is reviewed rather than overwritten.
92+
7193
On the qualified macOS heartbeat schema, direct migration requires the App
7294
closed. The adapter holds a SQLite writer transaction through TOML delivery,
7395
compares the entire previewed manifest, preserves every non-prompt field, and
@@ -202,3 +224,13 @@ gh 登录,仍失败则明确要求已核验 SHA,不切换分支或静默覆
202224
日程、暂停状态、模型、线程、通知偏好和历史均不迁移。
203225
不支持的存储仍需原生 API;运行中的本轮不热切换。普通测试不消耗模型 token,
204226
真实模型发布资格仍需独立评测,不能由迁移成功推断。
227+
228+
安装期对账只报告一次,因此 `quota should-run` 每轮都把本轮 lane 已安装的
229+
automation 观测投影为 `scheduler_hint.app_automation.prompt_binding`:状态为
230+
`current`/`adoption_required`/`blocked`/`ambiguous`/`absent`/`unavailable`。
231+
非当前 loader 时附带 `host_action=adopt_managed_bootstrap`、no-spend 策略、
232+
两个 prompt 摘要,以及与升级期完全相同的、仅改 prompt 的
233+
`automation_update` 请求,使采纳义务留在实时契约里,而不只存在于一次性报告。
234+
该观测只读、有界、失败即降级,未安装 automation 的 lane 不投影该字段;已识别
235+
的 loader 一律按自身绑定比对,turn 不会改标到其他 home、registry、runtime root
236+
或 CLI。采纳仍需显式执行,既不授予交付权限也不接管调度。

‎examples/control_plane/heartbeat-prompt-smoke.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,7 @@ def main() -> int:
594594
"具体user todo未投影",
595595
"Observed capabilities -> `--available-capability`; never user gates",
596596
"host_action=pause_or_delete_current_heartbeat->automation_update stop(no-spend)",
597+
"prompt_binding=adopt(no-spend);",
597598
"else RRULE/projected-fallback_hint/ack/fail",
598599
"no-change=`surface_only`/no spend",
599600
"unchanged->`--vision-unchanged-reason`",
@@ -696,6 +697,7 @@ def main() -> int:
696697
"NOTIFY缺动作→",
697698
"具体user todo未投影",
698699
"host_action=pause_or_delete_current_heartbeat->automation_update stop(no-spend)",
700+
"prompt_binding=adopt(no-spend);",
699701
"else RRULE/projected-fallback_hint/ack/fail",
700702
"no-change=`surface_only`/no spend",
701703
"unchanged->`--vision-unchanged-reason`",

0 commit comments

Comments
 (0)